From e69eb262b3ce965b58bff446acf7ae9be4e979ff Mon Sep 17 00:00:00 2001 From: Terence Hampson Date: Thu, 12 Jan 2023 14:37:43 -0500 Subject: [PATCH] Enable Multi-Fabric test commands with chip-repl runner (#24349) * Enable Multi-Fabric test commands with chip-repl runner * Restyle --- .../python/chip/yaml/format_converter.py | 34 ++++-- src/controller/python/chip/yaml/runner.py | 112 ++++++++++++++++-- 2 files changed, 125 insertions(+), 21 deletions(-) diff --git a/src/controller/python/chip/yaml/format_converter.py b/src/controller/python/chip/yaml/format_converter.py index 3f740d7e1d0e1e..d3305f42338e9e 100644 --- a/src/controller/python/chip/yaml/format_converter.py +++ b/src/controller/python/chip/yaml/format_converter.py @@ -17,10 +17,18 @@ import enum import typing +from dataclasses import dataclass from chip.clusters.Types import Nullable, NullValue from chip.tlv import float32, uint from chip.yaml.errors import ValidationError +from matter_idl import matter_idl_types + + +@dataclass +class _TargetTypeInfo: + field: typing.Union[list[matter_idl_types.Field], matter_idl_types.Field] + is_fabric_scoped: bool def _case_insensitive_getattr(object, attr_name, default): @@ -30,15 +38,16 @@ def _case_insensitive_getattr(object, attr_name, default): return default -def _get_target_type_fields(test_spec_definition, cluster_name, target_name): +def _get_target_type_info(test_spec_definition, cluster_name, target_name) -> _TargetTypeInfo: element = test_spec_definition.get_type_by_name(cluster_name, target_name) if hasattr(element, 'fields'): - return element.fields - return None + is_fabric_scoped = test_spec_definition.is_fabric_scoped(element) + return _TargetTypeInfo(element.fields, is_fabric_scoped) + return _TargetTypeInfo(None, False) def from_data_model_to_test_definition(test_spec_definition, cluster_name, response_definition, - response_value): + response_value, is_fabric_scoped=False): '''Converts value from data model to definitions provided in test_spec_definition. Args: @@ -56,6 +65,10 @@ def from_data_model_to_test_definition(test_spec_definition, cluster_name, respo # that need to be worked through recursively to properly convert the value to the right type. if isinstance(response_definition, list): rv = {} + # is_fabric_scoped will only be relevant for struct types, hence why it is only checked + # here. + if is_fabric_scoped: + rv['FabricIndex'] = _case_insensitive_getattr(response_value, 'fabricIndex', None) for item in response_definition: value = _case_insensitive_getattr(response_value, item.name, None) if item.is_optional and value is None: @@ -82,18 +95,23 @@ def from_data_model_to_test_definition(test_spec_definition, cluster_name, respo if response_value_type == float32 and response_definition.data_type.name.lower() == 'single': return float('%g' % response_value) - response_sub_definition = _get_target_type_fields(test_spec_definition, cluster_name, - response_definition.data_type.name) + target_type_info = _get_target_type_info(test_spec_definition, cluster_name, + response_definition.data_type.name) + + response_sub_definition = target_type_info.field + is_sub_definition_fabric_scoped = target_type_info.is_fabric_scoped # Check below is to see if the field itself is an array, for example array of ints. if response_definition.is_list: return [ from_data_model_to_test_definition(test_spec_definition, cluster_name, - response_sub_definition, item) for item in response_value + response_sub_definition, item, + is_sub_definition_fabric_scoped) for item in response_value ] return from_data_model_to_test_definition(test_spec_definition, cluster_name, - response_sub_definition, response_value) + response_sub_definition, response_value, + is_sub_definition_fabric_scoped) def convert_list_of_name_value_pair_to_dict(arg_values): diff --git a/src/controller/python/chip/yaml/runner.py b/src/controller/python/chip/yaml/runner.py index a4414b69d46c70..1fa9d5b90c3feb 100644 --- a/src/controller/python/chip/yaml/runner.py +++ b/src/controller/python/chip/yaml/runner.py @@ -20,7 +20,7 @@ import queue from abc import ABC, abstractmethod from dataclasses import dataclass, field -from enum import Enum +from enum import Enum, IntEnum import chip.interaction_model import chip.yaml.format_converter as Converter @@ -39,6 +39,12 @@ class _ActionStatus(Enum): ERROR = 'error' +class _TestFabricId(IntEnum): + ALPHA = 1, + BETA = 2, + GAMMA = 3 + + @dataclass class _ActionResult: status: _ActionStatus @@ -68,13 +74,18 @@ class _ExecutionContext: class BaseAction(ABC): '''Interface for a single YAML action that is to be executed.''' - def __init__(self, label): + def __init__(self, label, identity): self._label = label + self._identity = identity @property def label(self): return self._label + @property + def identity(self): + return self._identity + @abstractmethod def run_action(self, dev_ctrl: ChipDeviceCtrl) -> _ActionResult: pass @@ -95,9 +106,10 @@ def __init__(self, test_step, cluster: str, context: _ExecutionContext): action to perform for this write attribute. UnexpectedParsingError: Raised if there is an unexpected parsing error. ''' - super().__init__(test_step.label) + super().__init__(test_step.label, test_step.identity) self._command_name = stringcase.pascalcase(test_step.command) self._cluster = cluster + self._interation_timeout_ms = test_step.timed_interaction_timeout_ms self._request_object = None self._expected_response_object = None self._endpoint = test_step.endpoint @@ -128,8 +140,9 @@ def __init__(self, test_step, cluster: str, context: _ExecutionContext): def run_action(self, dev_ctrl: ChipDeviceCtrl) -> _ActionResult: try: - resp = asyncio.run(dev_ctrl.SendCommand(self._node_id, self._endpoint, - self._request_object)) + resp = asyncio.run(dev_ctrl.SendCommand( + self._node_id, self._endpoint, self._request_object, + timedRequestTimeoutMs=self._interation_timeout_ms)) except chip.interaction_model.InteractionModelError as error: return _ActionResult(status=_ActionStatus.ERROR, response=error) @@ -152,13 +165,17 @@ def __init__(self, test_step, cluster: str, context: _ExecutionContext): action to perform for this read attribute. UnexpectedParsingError: Raised if there is an unexpected parsing error. ''' - super().__init__(test_step.label) + super().__init__(test_step.label, test_step.identity) self._attribute_name = stringcase.pascalcase(test_step.attribute) self._cluster = cluster self._endpoint = test_step.endpoint self._node_id = test_step.node_id self._cluster_object = None self._request_object = None + self._fabric_filtered = True + + if test_step.fabric_filtered is not None: + self._fabric_filtered = test_step.fabric_filtered self._possibly_unsupported = bool(test_step.optional) @@ -185,7 +202,8 @@ def __init__(self, test_step, cluster: str, context: _ExecutionContext): def run_action(self, dev_ctrl: ChipDeviceCtrl) -> _ActionResult: try: raw_resp = asyncio.run(dev_ctrl.ReadAttribute(self._node_id, - [(self._endpoint, self._request_object)])) + [(self._endpoint, self._request_object)], + fabricFiltered=self._fabric_filtered)) except chip.interaction_model.InteractionModelError as error: return _ActionResult(status=_ActionStatus.ERROR, response=error) @@ -215,7 +233,7 @@ class WaitForCommissioneeAction(BaseAction): ''' Wait for commissionee action to be executed.''' def __init__(self, test_step): - super().__init__(test_step.label) + super().__init__(test_step.label, test_step.identity) self._node_id = test_step.node_id self._expire_existing_session = False # This is the default when no timeout is provided. @@ -337,7 +355,7 @@ def __init__(self, test_step, cluster: str, context: _ExecutionContext): action to perform for this write attribute. UnexpectedParsingError: Raised if there is an unexpected parsing error. ''' - super().__init__(test_step.label) + super().__init__(test_step.label, test_step.identity) self._attribute_name = stringcase.pascalcase(test_step.attribute) self._cluster = cluster self._endpoint = test_step.endpoint @@ -398,7 +416,7 @@ def __init__(self, test_step, context: _ExecutionContext): Raises: UnexpectedParsingError: Raised if the expected queue does not exist. ''' - super().__init__(test_step.label) + super().__init__(test_step.label, test_step.identity) self._attribute_name = stringcase.pascalcase(test_step.attribute) self._output_queue = context.subscription_callback_result_queue.get(self._attribute_name, None) @@ -417,16 +435,50 @@ def run_action(self, dev_ctrl: ChipDeviceCtrl) -> _ActionResult: return item.result +class CommissionerCommandAction(BaseAction): + '''Single Commissioner Command action to be executed.''' + + def __init__(self, test_step): + '''Converts 'test_step' to commissioner command action. + + Args: + 'test_step': Step containing information required to run wait for report action. + Raises: + UnexpectedParsingError: Raised if the expected queue does not exist. + ''' + super().__init__(test_step.label, test_step.identity) + if test_step.command != 'PairWithCode': + raise UnexpectedParsingError(f'Unexpected CommisionerCommand {test_step.command}') + + args = test_step.arguments['values'] + request_data_as_dict = Converter.convert_list_of_name_value_pair_to_dict(args) + self._setup_payload = request_data_as_dict['payload'] + self._node_id = request_data_as_dict['nodeId'] + + def run_action(self, dev_ctrl: ChipDeviceCtrl) -> _ActionResult: + resp = dev_ctrl.CommissionWithCode(self._setup_payload, self._node_id) + + if resp: + return _ActionResult(status=_ActionStatus.SUCCESS, response=None) + else: + return _ActionResult(status=_ActionStatus.ERROR, response=None) + + class ReplTestRunner: '''Test runner to encode/decode values from YAML test Parser for executing the TestStep. Uses ChipDeviceCtrl from chip-repl to execute parsed YAML TestSteps. ''' - def __init__(self, test_spec_definition, dev_ctrl): + def __init__(self, test_spec_definition, certificate_authority_manager): self._test_spec_definition = test_spec_definition - self._dev_ctrl = dev_ctrl self._context = _ExecutionContext(data_model_lookup=PreDefinedDataModelLookup()) + self._certificate_authority_manager = certificate_authority_manager + self._dev_ctrls = {} + + ca_list = certificate_authority_manager.activeCaList + dev_ctrl = ca_list[0].adminList[0].NewController() + self._dev_ctrls['alpha'] = dev_ctrl def _invoke_action_factory(self, test_step, cluster: str): '''Creates cluster invoke action command from TestStep. @@ -513,12 +565,21 @@ def _wait_for_report_action_factory(self, test_step): # propogated. return None + def _commissioner_command_action_factory(self, test_step): + try: + return CommissionerCommandAction(test_step) + except ParsingError: + return None + def encode(self, request) -> BaseAction: action = None cluster = request.cluster.replace(' ', '').replace('/', '') command = request.command + if cluster == 'CommissionerCommands': + return self._commissioner_command_action_factory(request) # Some of the tests contain 'cluster over-rides' that refer to a different # cluster than that specified in 'config'. + if cluster == 'DelayCommands' and command == 'WaitForCommissionee': action = self._wait_for_commissionee_action_factory(request) elif command == 'writeAttribute': @@ -588,8 +649,33 @@ def decode(self, result: _ActionResult): return decoded_response + def _get_fabric_id(self, id): + return _TestFabricId[id.upper()].value + + def _get_dev_ctrl(self, action: BaseAction): + if action.identity is not None: + dev_ctrl = self._dev_ctrls.get(action.identity, None) + if dev_ctrl is None: + fabric_id = self._get_fabric_id(action.identity) + certificate_authority = self._certificate_authority_manager.activeCaList[0] + fabric = None + for existing_admin in certificate_authority.adminList: + if existing_admin.fabricId == fabric_id: + fabric = existing_admin + + if fabric is None: + fabric = certificate_authority.NewFabricAdmin(vendorId=0xFFF1, + fabricId=fabric_id) + dev_ctrl = fabric.NewController() + self._dev_ctrls[action.identity] = dev_ctrl + else: + dev_ctrl = self._dev_ctrls['alpha'] + + return dev_ctrl + def execute(self, action: BaseAction): - return action.run_action(self._dev_ctrl) + dev_ctrl = self._get_dev_ctrl(action) + return action.run_action(dev_ctrl) def shutdown(self): for subscription in self._context.subscriptions: