Skip to content

Commit e086a20

Browse files
asirko-softcecillekhodya
authored
mypy conformance for matter_testing.py (#39586)
* fix first 5 mypy errors * fix next 5 errors * fix several mypy errors * fix almost all of the remaining errors * ignore mypy here, as a code is functionally correct in runtime * autopep ignore lines of mypy ignore * lint code * review comments and adding file to gh workflow * fix imports after merge * fix regex that prevents TestFrameworkArgParsing.py from passing * moving type aliases to ChipDeviceCtrl * fix issues after merging latest master * linter fix * micro fix * fix for app_pipe to pipe_name mapping * fix path in workflow * revert one change that caused test failure and silenced mypy here --------- Co-authored-by: C Freeman <[email protected]> Co-authored-by: Andrey Khodyrev <[email protected]>
1 parent 78567b7 commit e086a20

File tree

5 files changed

+71
-29
lines changed

5 files changed

+71
-29
lines changed

.github/workflows/mypy-validation.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ jobs:
8989
src/python_testing/matter_testing_infrastructure/matter/testing/choice_conformance.py \
9090
src/python_testing/matter_testing_infrastructure/matter/testing/commissioning.py \
9191
src/python_testing/matter_testing_infrastructure/matter/testing/decorators.py \
92-
src/python_testing/matter_testing_infrastructure/matter/testing/basic_composition.py"
93-
92+
src/python_testing/matter_testing_infrastructure/matter/testing/basic_composition.py \
93+
src/python_testing/matter_testing_infrastructure/matter/testing/matter_testing.py"
94+
9495
# Print a reminder about expanding coverage
9596
echo "⚠️ NOTE: Currently only checking a subset of files. Remember to expand coverage!"

src/controller/python/matter/ChipDeviceCtrl.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,30 @@
5757
from .interaction_model import SessionParameters, SessionParametersStruct
5858
from .native import PyChipError
5959

60-
__all__ = ["ChipDeviceController", "CommissioningParameters"]
60+
__all__ = ["ChipDeviceController", "CommissioningParameters",
61+
"AttributeReadRequest", "AttributeReadRequestList", "SubscriptionTargetList"]
62+
63+
# Type aliases for ReadAttribute method to improve type safety
64+
AttributeReadRequest = typing.Union[
65+
None, # Empty tuple, all wildcard
66+
typing.Tuple[int], # Endpoint
67+
# Wildcard endpoint, Cluster id present
68+
typing.Tuple[typing.Type[ClusterObjects.Cluster]],
69+
# Wildcard endpoint, Cluster + Attribute present
70+
typing.Tuple[typing.Type[ClusterObjects.ClusterAttributeDescriptor]],
71+
typing.Tuple[int, typing.Type[ClusterObjects.Cluster]
72+
], # Wildcard attribute id
73+
# Concrete path
74+
typing.Tuple[int, typing.Type[ClusterObjects.ClusterAttributeDescriptor]],
75+
ClusterAttribute.TypedAttributePath # Directly specified attribute path
76+
]
77+
78+
AttributeReadRequestList = typing.Optional[typing.List[AttributeReadRequest]]
79+
80+
# Type alias for subscription target specifications
81+
SubscriptionTargetList = typing.List[typing.Tuple[int,
82+
typing.Union[ClusterObjects.Cluster, ClusterObjects.ClusterAttributeDescriptor]]]
83+
6184

6285
# Defined in $CHIP_ROOT/src/lib/core/CHIPError.h
6386
CHIP_ERROR_TIMEOUT: int = 50

src/python_testing/matter_testing_infrastructure/matter/testing/matter_test_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class MatterTestConfig:
5959

6060
wifi_ssid: Optional[str] = None
6161
wifi_passphrase: Optional[str] = None
62-
thread_operational_dataset: Optional[str] = None
62+
thread_operational_dataset: Optional[bytes] = None
6363

6464
pics: dict[str, bool] = field(default_factory=dict)
6565

src/python_testing/matter_testing_infrastructure/matter/testing/matter_testing.py

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from dataclasses import dataclass
3030
from datetime import datetime, timedelta, timezone
3131
from enum import IntFlag
32-
from typing import Any, Callable, List, Optional
32+
from typing import Any, Callable, List, Optional, Type, Union
3333

3434
import matter.testing.conversions as conversions
3535
import matter.testing.decorators as decorators
@@ -66,6 +66,10 @@
6666
# TODO: Add utility to commission a device if needed
6767
# TODO: Add utilities to keep track of controllers/fabrics
6868

69+
# Type aliases for common patterns to improve readability
70+
StepNumber = Union[int, str] # Test step numbers can be integers or strings
71+
OptionalTimeout = Optional[int] # Optional timeout values
72+
6973
logger = logging.getLogger("matter.python_testing")
7074
logger.setLevel(logging.INFO)
7175

@@ -312,18 +316,18 @@ def certificate_authority_manager(self) -> matter.CertificateAuthority.Certifica
312316
def dut_node_id(self) -> int:
313317
return self.matter_test_config.dut_node_ids[0]
314318

315-
def get_endpoint(self, default: Optional[int] = 0) -> int:
319+
def get_endpoint(self, default: int = 0) -> int:
316320
return self.matter_test_config.endpoint if self.matter_test_config.endpoint is not None else default
317321

318-
def get_wifi_ssid(self, default: str = "") -> str:
322+
def get_wifi_ssid(self, default: Optional[str] = None) -> Optional[str]:
319323
''' Get WiFi SSID
320324
321325
Get the WiFi networks name provided with flags
322326
323327
'''
324328
return self.matter_test_config.wifi_ssid if self.matter_test_config.wifi_ssid is not None else default
325329

326-
def get_credentials(self, default: str = "") -> str:
330+
def get_credentials(self, default: Optional[str] = None) -> Optional[str]:
327331
''' Get WiFi passphrase
328332
329333
Get the WiFi credentials provided with flags
@@ -428,21 +432,22 @@ async def open_commissioning_window(self, dev_ctrl: Optional[ChipDeviceCtrl.Chip
428432
node_id = self.dut_node_id
429433
try:
430434
commissioning_params = await dev_ctrl.OpenCommissioningWindow(nodeid=node_id, timeout=timeout, iteration=1000,
431-
discriminator=rnd_discriminator, option=1)
435+
discriminator=rnd_discriminator, option=dev_ctrl.CommissioningWindowPasscode.kTokenWithRandomPin)
432436
params = CustomCommissioningParameters(commissioning_params, rnd_discriminator)
433437
return params
434438

435439
except InteractionModelError as e:
436440
asserts.fail(e.status, 'Failed to open commissioning window')
441+
raise # Help mypy understand this never returns
437442

438443
async def read_single_attribute(
439-
self, dev_ctrl: ChipDeviceCtrl.ChipDeviceController, node_id: int, endpoint: int, attribute: object, fabricFiltered: bool = True) -> object:
444+
self, dev_ctrl: ChipDeviceCtrl.ChipDeviceController, node_id: int, endpoint: int, attribute: Type[ClusterObjects.ClusterAttributeDescriptor], fabricFiltered: bool = True) -> object:
440445
result = await dev_ctrl.ReadAttribute(node_id, [(endpoint, attribute)], fabricFiltered=fabricFiltered)
441446
data = result[endpoint]
442447
return list(data.values())[0][attribute]
443448

444449
async def read_single_attribute_all_endpoints(
445-
self, cluster: Clusters.ClusterObjects.ClusterCommand, attribute: Clusters.ClusterObjects.ClusterAttributeDescriptor,
450+
self, cluster: ClusterObjects.Cluster, attribute: Type[ClusterObjects.ClusterAttributeDescriptor],
446451
dev_ctrl: Optional[ChipDeviceCtrl.ChipDeviceController] = None, node_id: Optional[int] = None):
447452
"""Reads a single attribute of a specified cluster across all endpoints.
448453
@@ -454,24 +459,24 @@ async def read_single_attribute_all_endpoints(
454459
dev_ctrl = self.default_controller
455460
if node_id is None:
456461
node_id = self.dut_node_id
457-
458-
read_response = await dev_ctrl.ReadAttribute(node_id, [(attribute)])
462+
# mypy expects tuple-shaped items here. Some tests crash when attribute requests are wrapped in a single-element tuple here.
463+
# We pass the plain attribute to avoid the runtime issue; so we ignore that type.
464+
read_response = await dev_ctrl.ReadAttribute(node_id, [attribute]) # type: ignore[list-item]
459465
attrs = {}
460466
for endpoint in read_response:
461467
attr_ret = read_response[endpoint][cluster][attribute]
462468
attrs[endpoint] = attr_ret
463469
return attrs
464470

465471
async def read_single_attribute_check_success(
466-
self, cluster: Clusters.ClusterObjects.ClusterCommand, attribute: Clusters.ClusterObjects.ClusterAttributeDescriptor,
472+
self, cluster: ClusterObjects.Cluster, attribute: Type[ClusterObjects.ClusterAttributeDescriptor],
467473
dev_ctrl: Optional[ChipDeviceCtrl.ChipDeviceController] = None, node_id: Optional[int] = None, endpoint: Optional[int] = None, fabric_filtered: bool = True, assert_on_error: bool = True, test_name: str = "") -> object:
468474
if dev_ctrl is None:
469475
dev_ctrl = self.default_controller
470476
if node_id is None:
471477
node_id = self.dut_node_id
472478
if endpoint is None:
473479
endpoint = self.get_endpoint()
474-
475480
result = await dev_ctrl.ReadAttribute(node_id, [(endpoint, attribute)], fabricFiltered=fabric_filtered)
476481
attr_ret = result[endpoint][cluster][attribute]
477482
read_err_msg = f"Error reading {str(cluster)}:{str(attribute)} = {attr_ret}"
@@ -494,7 +499,7 @@ async def read_single_attribute_check_success(
494499
return attr_ret
495500

496501
async def read_single_attribute_expect_error(
497-
self, cluster: object, attribute: object,
502+
self, cluster: ClusterObjects.Cluster, attribute: Type[ClusterObjects.ClusterAttributeDescriptor],
498503
error: Status, dev_ctrl: Optional[ChipDeviceCtrl.ChipDeviceController] = None, node_id: Optional[int] = None, endpoint: Optional[int] = None,
499504
fabric_filtered: bool = True, assert_on_error: bool = True, test_name: str = "") -> object:
500505
if dev_ctrl is None:
@@ -503,7 +508,6 @@ async def read_single_attribute_expect_error(
503508
node_id = self.dut_node_id
504509
if endpoint is None:
505510
endpoint = self.get_endpoint()
506-
507511
result = await dev_ctrl.ReadAttribute(node_id, [(endpoint, attribute)], fabricFiltered=fabric_filtered)
508512
attr_ret = result[endpoint][cluster][attribute]
509513
err_msg = "Did not see expected error when reading {}:{}".format(str(cluster), str(attribute))
@@ -520,7 +524,7 @@ async def read_single_attribute_expect_error(
520524

521525
return attr_ret
522526

523-
async def write_single_attribute(self, attribute_value: object, endpoint_id: Optional[int] = None, expect_success: bool = True) -> Status:
527+
async def write_single_attribute(self, attribute_value: ClusterObjects.ClusterAttributeDescriptor, endpoint_id: Optional[int] = None, expect_success: bool = True) -> Status:
524528
"""Write a single `attribute_value` on a given `endpoint_id` and assert on failure.
525529
526530
If `endpoint_id` is None, the default DUT endpoint for the test is selected.
@@ -543,7 +547,7 @@ async def write_single_attribute(self, attribute_value: object, endpoint_id: Opt
543547
async def send_single_cmd(
544548
self, cmd: Clusters.ClusterObjects.ClusterCommand,
545549
dev_ctrl: Optional[ChipDeviceCtrl.ChipDeviceController] = None, node_id: Optional[int] = None, endpoint: Optional[int] = None,
546-
timedRequestTimeoutMs: typing.Union[None, int] = None,
550+
timedRequestTimeoutMs: OptionalTimeout = None,
547551
payloadCapability: int = ChipDeviceCtrl.TransportPayloadCapability.MRP_PAYLOAD) -> object:
548552
if dev_ctrl is None:
549553
dev_ctrl = self.default_controller
@@ -556,7 +560,7 @@ async def send_single_cmd(
556560
payloadCapability=payloadCapability)
557561
return result
558562

559-
async def send_test_event_triggers(self, eventTrigger: int, enableKey: bytes = None):
563+
async def send_test_event_triggers(self, eventTrigger: int, enableKey: Optional[bytes] = None):
560564
"""This helper function sends a test event trigger to the General Diagnostics cluster on endpoint 0
561565
562566
The enableKey can be passed into the function, or omitted which will then
@@ -576,7 +580,7 @@ async def send_test_event_triggers(self, eventTrigger: int, enableKey: bytes = N
576580

577581
try:
578582
# GeneralDiagnostics cluster is meant to be on Endpoint 0 (Root)
579-
await self.send_single_cmd(endpoint=0, cmd=Clusters.GeneralDiagnostics.Commands.TestEventTrigger(enableKey, eventTrigger))
583+
await self.send_single_cmd(endpoint=0, cmd=Clusters.GeneralDiagnostics.Commands.TestEventTrigger(enableKey, uint(eventTrigger)))
580584

581585
except InteractionModelError as e:
582586
asserts.fail(
@@ -591,7 +595,7 @@ async def check_test_event_triggers_enabled(self):
591595
test_event_enabled = await self.read_single_attribute_check_success(endpoint=0, cluster=cluster, attribute=full_attr)
592596
asserts.assert_equal(test_event_enabled, True, "TestEventTriggersEnabled is False")
593597

594-
def print_step(self, stepnum: typing.Union[int, str], title: str) -> None:
598+
def print_step(self, stepnum: StepNumber, title: str) -> None:
595599
logging.info(f'***** Test Step {stepnum} : {title}')
596600

597601
def record_error(self, test_name: str, location: ProblemLocation, problem: str, spec_location: str = ""):
@@ -735,7 +739,7 @@ async def _populate_wildcard(self):
735739
None, None, GlobalAttributeIds.FEATURE_MAP_ID), Attribute.AttributePath(None, None, GlobalAttributeIds.ACCEPTED_COMMAND_LIST_ID)]), timeout=60)
736740
self.stored_global_wildcard = await global_wildcard
737741

738-
async def attribute_guard(self, endpoint: int, attribute: ClusterObjects.ClusterAttributeDescriptor):
742+
async def attribute_guard(self, endpoint: int, attribute: ClusterObjects.ClusterAttributeDescriptor) -> bool:
739743
"""Similar to pics_guard above, except checks a condition and if False marks the test step as skipped and
740744
returns False using attributes against attributes_list, otherwise returns True.
741745
For example can be used to check if a test step should be run:
@@ -754,7 +758,7 @@ async def attribute_guard(self, endpoint: int, attribute: ClusterObjects.Cluster
754758
self.mark_current_step_skipped()
755759
return attr_condition
756760

757-
async def command_guard(self, endpoint: int, command: ClusterObjects.ClusterCommand):
761+
async def command_guard(self, endpoint: int, command: ClusterObjects.ClusterCommand) -> bool:
758762
"""Similar to attribute_guard above, except checks a condition and if False marks the test step as skipped and
759763
returns False using command id against AcceptedCmdsList, otherwise returns True.
760764
For example can be used to check if a test step should be run:
@@ -773,7 +777,7 @@ async def command_guard(self, endpoint: int, command: ClusterObjects.ClusterComm
773777
self.mark_current_step_skipped()
774778
return cmd_condition
775779

776-
async def feature_guard(self, endpoint: int, cluster: ClusterObjects.ClusterObjectDescriptor, feature_int: IntFlag):
780+
async def feature_guard(self, endpoint: int, cluster: ClusterObjects.ClusterObjectDescriptor, feature_int: IntFlag) -> bool:
777781
"""Similar to command_guard and attribute_guard above, except checks a condition and if False marks the test step as skipped and
778782
returns False using feature id against feature_map, otherwise returns True.
779783
For example can be used to check if a test step should be run:
@@ -813,7 +817,7 @@ def skip_step(self, step):
813817
self.step(step)
814818
self.mark_current_step_skipped()
815819

816-
def mark_all_remaining_steps_skipped(self, starting_step_number: typing.Union[int, str]) -> None:
820+
def mark_all_remaining_steps_skipped(self, starting_step_number: StepNumber) -> None:
817821
"""Mark all remaining test steps starting with provided starting step
818822
starting_step_number gives the first step to be skipped, as defined in the TestStep.test_plan_number
819823
starting_step_number must be provided, and is not derived intentionally.
@@ -850,6 +854,8 @@ def mark_step_range_skipped(self, starting_step_number: typing.Union[int, str],
850854
starting_step_idx = idx
851855
break
852856
asserts.assert_is_not_none(starting_step_idx, "mark_step_ranges_skipped was provided with invalid starting_step_num")
857+
# Help mypy understand starting_step_idx is not None after the assert
858+
assert starting_step_idx is not None
853859

854860
ending_step_idx = None
855861
# If ending_step_number is None, we skip all steps until the end of the test
@@ -860,6 +866,8 @@ def mark_step_range_skipped(self, starting_step_number: typing.Union[int, str],
860866
break
861867

862868
asserts.assert_is_not_none(ending_step_idx, "mark_step_ranges_skipped was provided with invalid ending_step_num")
869+
# Help mypy understand ending_step_idx is not None after the assert
870+
assert ending_step_idx is not None
863871
asserts.assert_greater(ending_step_idx, starting_step_idx,
864872
"mark_step_ranges_skipped was provided with ending_step_num that is before starting_step_num")
865873
skipping_steps = steps[starting_step_idx:ending_step_idx+1]
@@ -869,7 +877,7 @@ def mark_step_range_skipped(self, starting_step_number: typing.Union[int, str],
869877
for step in skipping_steps:
870878
self.skip_step(step.test_plan_number)
871879

872-
def step(self, step: typing.Union[int, str]):
880+
def step(self, step: StepNumber):
873881
test_name = self.current_test_info.name
874882
steps = self.get_test_steps(test_name)
875883

@@ -1042,7 +1050,12 @@ def get_cluster_from_command(command: ClusterObjects.ClusterCommand) -> ClusterO
10421050

10431051
async def _get_all_matching_endpoints(self: MatterBaseTest, accept_function: EndpointCheckFunction) -> list[uint]:
10441052
""" Returns a list of endpoints matching the accept condition. """
1045-
wildcard = await self.default_controller.Read(self.dut_node_id, [(Clusters.Descriptor), Attribute.AttributePath(None, None, GlobalAttributeIds.ATTRIBUTE_LIST_ID), Attribute.AttributePath(None, None, GlobalAttributeIds.FEATURE_MAP_ID), Attribute.AttributePath(None, None, GlobalAttributeIds.ACCEPTED_COMMAND_LIST_ID)])
1053+
wildcard = await self.default_controller.Read(self.dut_node_id, [
1054+
(Clusters.Descriptor,), # single-element tuple needs trailing comma
1055+
Attribute.AttributePath(None, None, GlobalAttributeIds.ATTRIBUTE_LIST_ID),
1056+
Attribute.AttributePath(None, None, GlobalAttributeIds.FEATURE_MAP_ID),
1057+
Attribute.AttributePath(None, None, GlobalAttributeIds.ACCEPTED_COMMAND_LIST_ID)
1058+
])
10461059
matching = [e for e in wildcard.attributes.keys()
10471060
if accept_function(wildcard, e)]
10481061
return matching
@@ -1068,7 +1081,9 @@ async def _get_all_matching_endpoints(self: MatterBaseTest, accept_function: End
10681081
has_command = decorators.has_command
10691082
has_feature = decorators.has_feature
10701083
should_run_test_on_endpoint = decorators.should_run_test_on_endpoint
1071-
_get_all_matching_endpoints = decorators._get_all_matching_endpoints
1084+
# autopep8: off
1085+
_get_all_matching_endpoints = decorators._get_all_matching_endpoints # type: ignore[assignment]
1086+
# autopep8: on
10721087
_has_feature = decorators._has_feature
10731088
_has_command = decorators._has_command
10741089
_has_attribute = decorators._has_attribute
@@ -1081,3 +1096,5 @@ async def _get_all_matching_endpoints(self: MatterBaseTest, accept_function: End
10811096

10821097
# Backward compatibility aliases for relocated functions
10831098
parse_matter_test_args = runner.parse_matter_test_args
1099+
1100+
# isort: off

src/python_testing/matter_testing_infrastructure/matter/testing/runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,7 @@ def convert_args_to_matter_config(args: argparse.Namespace):
737737
config.tests = list(chain.from_iterable(args.tests or []))
738738
config.timeout = args.timeout # This can be none, we pull the default from the test if it's unspecified
739739
config.endpoint = args.endpoint # This can be None, the get_endpoint function allows the tests to supply a default
740+
# Map CLI arg to the current config field name used by tests
740741
config.pipe_name = args.app_pipe
741742
if config.pipe_name is not None and not os.path.exists(config.pipe_name):
742743
# Named pipes are unique, so we MUST have consistent paths

0 commit comments

Comments
 (0)