Enable Multi-Fabric test commands with chip-repl runner (#24349)
* Enable Multi-Fabric test commands with chip-repl runner
* Restyle
diff --git a/src/controller/python/chip/yaml/format_converter.py b/src/controller/python/chip/yaml/format_converter.py
index 3f740d7..d3305f4 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 @@
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 @@
# 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 @@
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 a4414b6..1fa9d5b 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 @@
ERROR = 'error'
+class _TestFabricId(IntEnum):
+ ALPHA = 1,
+ BETA = 2,
+ GAMMA = 3
+
+
@dataclass
class _ActionResult:
status: _ActionStatus
@@ -68,13 +74,18 @@
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 @@
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 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 @@
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 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 @@
''' 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 @@
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 @@
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 @@
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 @@
# 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 @@
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: