Skip to content

Commit b36439e

Browse files
authored
Enable Multi-Fabric test commands with chip-repl runner (#24349)
* Enable Multi-Fabric test commands with chip-repl runner * Restyle
1 parent 1f9638c commit b36439e

File tree

2 files changed

+125
-21
lines changed

2 files changed

+125
-21
lines changed

src/controller/python/chip/yaml/format_converter.py

+26-8
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,18 @@
1717

1818
import enum
1919
import typing
20+
from dataclasses import dataclass
2021

2122
from chip.clusters.Types import Nullable, NullValue
2223
from chip.tlv import float32, uint
2324
from chip.yaml.errors import ValidationError
25+
from matter_idl import matter_idl_types
26+
27+
28+
@dataclass
29+
class _TargetTypeInfo:
30+
field: typing.Union[list[matter_idl_types.Field], matter_idl_types.Field]
31+
is_fabric_scoped: bool
2432

2533

2634
def _case_insensitive_getattr(object, attr_name, default):
@@ -30,15 +38,16 @@ def _case_insensitive_getattr(object, attr_name, default):
3038
return default
3139

3240

33-
def _get_target_type_fields(test_spec_definition, cluster_name, target_name):
41+
def _get_target_type_info(test_spec_definition, cluster_name, target_name) -> _TargetTypeInfo:
3442
element = test_spec_definition.get_type_by_name(cluster_name, target_name)
3543
if hasattr(element, 'fields'):
36-
return element.fields
37-
return None
44+
is_fabric_scoped = test_spec_definition.is_fabric_scoped(element)
45+
return _TargetTypeInfo(element.fields, is_fabric_scoped)
46+
return _TargetTypeInfo(None, False)
3847

3948

4049
def from_data_model_to_test_definition(test_spec_definition, cluster_name, response_definition,
41-
response_value):
50+
response_value, is_fabric_scoped=False):
4251
'''Converts value from data model to definitions provided in test_spec_definition.
4352
4453
Args:
@@ -56,6 +65,10 @@ def from_data_model_to_test_definition(test_spec_definition, cluster_name, respo
5665
# that need to be worked through recursively to properly convert the value to the right type.
5766
if isinstance(response_definition, list):
5867
rv = {}
68+
# is_fabric_scoped will only be relevant for struct types, hence why it is only checked
69+
# here.
70+
if is_fabric_scoped:
71+
rv['FabricIndex'] = _case_insensitive_getattr(response_value, 'fabricIndex', None)
5972
for item in response_definition:
6073
value = _case_insensitive_getattr(response_value, item.name, None)
6174
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
8295
if response_value_type == float32 and response_definition.data_type.name.lower() == 'single':
8396
return float('%g' % response_value)
8497

85-
response_sub_definition = _get_target_type_fields(test_spec_definition, cluster_name,
86-
response_definition.data_type.name)
98+
target_type_info = _get_target_type_info(test_spec_definition, cluster_name,
99+
response_definition.data_type.name)
100+
101+
response_sub_definition = target_type_info.field
102+
is_sub_definition_fabric_scoped = target_type_info.is_fabric_scoped
87103

88104
# Check below is to see if the field itself is an array, for example array of ints.
89105
if response_definition.is_list:
90106
return [
91107
from_data_model_to_test_definition(test_spec_definition, cluster_name,
92-
response_sub_definition, item) for item in response_value
108+
response_sub_definition, item,
109+
is_sub_definition_fabric_scoped) for item in response_value
93110
]
94111

95112
return from_data_model_to_test_definition(test_spec_definition, cluster_name,
96-
response_sub_definition, response_value)
113+
response_sub_definition, response_value,
114+
is_sub_definition_fabric_scoped)
97115

98116

99117
def convert_list_of_name_value_pair_to_dict(arg_values):

src/controller/python/chip/yaml/runner.py

+99-13
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import queue
2121
from abc import ABC, abstractmethod
2222
from dataclasses import dataclass, field
23-
from enum import Enum
23+
from enum import Enum, IntEnum
2424

2525
import chip.interaction_model
2626
import chip.yaml.format_converter as Converter
@@ -39,6 +39,12 @@ class _ActionStatus(Enum):
3939
ERROR = 'error'
4040

4141

42+
class _TestFabricId(IntEnum):
43+
ALPHA = 1,
44+
BETA = 2,
45+
GAMMA = 3
46+
47+
4248
@dataclass
4349
class _ActionResult:
4450
status: _ActionStatus
@@ -68,13 +74,18 @@ class _ExecutionContext:
6874
class BaseAction(ABC):
6975
'''Interface for a single YAML action that is to be executed.'''
7076

71-
def __init__(self, label):
77+
def __init__(self, label, identity):
7278
self._label = label
79+
self._identity = identity
7380

7481
@property
7582
def label(self):
7683
return self._label
7784

85+
@property
86+
def identity(self):
87+
return self._identity
88+
7889
@abstractmethod
7990
def run_action(self, dev_ctrl: ChipDeviceCtrl) -> _ActionResult:
8091
pass
@@ -95,9 +106,10 @@ def __init__(self, test_step, cluster: str, context: _ExecutionContext):
95106
action to perform for this write attribute.
96107
UnexpectedParsingError: Raised if there is an unexpected parsing error.
97108
'''
98-
super().__init__(test_step.label)
109+
super().__init__(test_step.label, test_step.identity)
99110
self._command_name = stringcase.pascalcase(test_step.command)
100111
self._cluster = cluster
112+
self._interation_timeout_ms = test_step.timed_interaction_timeout_ms
101113
self._request_object = None
102114
self._expected_response_object = None
103115
self._endpoint = test_step.endpoint
@@ -128,8 +140,9 @@ def __init__(self, test_step, cluster: str, context: _ExecutionContext):
128140

129141
def run_action(self, dev_ctrl: ChipDeviceCtrl) -> _ActionResult:
130142
try:
131-
resp = asyncio.run(dev_ctrl.SendCommand(self._node_id, self._endpoint,
132-
self._request_object))
143+
resp = asyncio.run(dev_ctrl.SendCommand(
144+
self._node_id, self._endpoint, self._request_object,
145+
timedRequestTimeoutMs=self._interation_timeout_ms))
133146
except chip.interaction_model.InteractionModelError as error:
134147
return _ActionResult(status=_ActionStatus.ERROR, response=error)
135148

@@ -152,13 +165,17 @@ def __init__(self, test_step, cluster: str, context: _ExecutionContext):
152165
action to perform for this read attribute.
153166
UnexpectedParsingError: Raised if there is an unexpected parsing error.
154167
'''
155-
super().__init__(test_step.label)
168+
super().__init__(test_step.label, test_step.identity)
156169
self._attribute_name = stringcase.pascalcase(test_step.attribute)
157170
self._cluster = cluster
158171
self._endpoint = test_step.endpoint
159172
self._node_id = test_step.node_id
160173
self._cluster_object = None
161174
self._request_object = None
175+
self._fabric_filtered = True
176+
177+
if test_step.fabric_filtered is not None:
178+
self._fabric_filtered = test_step.fabric_filtered
162179

163180
self._possibly_unsupported = bool(test_step.optional)
164181

@@ -185,7 +202,8 @@ def __init__(self, test_step, cluster: str, context: _ExecutionContext):
185202
def run_action(self, dev_ctrl: ChipDeviceCtrl) -> _ActionResult:
186203
try:
187204
raw_resp = asyncio.run(dev_ctrl.ReadAttribute(self._node_id,
188-
[(self._endpoint, self._request_object)]))
205+
[(self._endpoint, self._request_object)],
206+
fabricFiltered=self._fabric_filtered))
189207
except chip.interaction_model.InteractionModelError as error:
190208
return _ActionResult(status=_ActionStatus.ERROR, response=error)
191209

@@ -215,7 +233,7 @@ class WaitForCommissioneeAction(BaseAction):
215233
''' Wait for commissionee action to be executed.'''
216234

217235
def __init__(self, test_step):
218-
super().__init__(test_step.label)
236+
super().__init__(test_step.label, test_step.identity)
219237
self._node_id = test_step.node_id
220238
self._expire_existing_session = False
221239
# This is the default when no timeout is provided.
@@ -337,7 +355,7 @@ def __init__(self, test_step, cluster: str, context: _ExecutionContext):
337355
action to perform for this write attribute.
338356
UnexpectedParsingError: Raised if there is an unexpected parsing error.
339357
'''
340-
super().__init__(test_step.label)
358+
super().__init__(test_step.label, test_step.identity)
341359
self._attribute_name = stringcase.pascalcase(test_step.attribute)
342360
self._cluster = cluster
343361
self._endpoint = test_step.endpoint
@@ -398,7 +416,7 @@ def __init__(self, test_step, context: _ExecutionContext):
398416
Raises:
399417
UnexpectedParsingError: Raised if the expected queue does not exist.
400418
'''
401-
super().__init__(test_step.label)
419+
super().__init__(test_step.label, test_step.identity)
402420
self._attribute_name = stringcase.pascalcase(test_step.attribute)
403421
self._output_queue = context.subscription_callback_result_queue.get(self._attribute_name,
404422
None)
@@ -417,16 +435,50 @@ def run_action(self, dev_ctrl: ChipDeviceCtrl) -> _ActionResult:
417435
return item.result
418436

419437

438+
class CommissionerCommandAction(BaseAction):
439+
'''Single Commissioner Command action to be executed.'''
440+
441+
def __init__(self, test_step):
442+
'''Converts 'test_step' to commissioner command action.
443+
444+
Args:
445+
'test_step': Step containing information required to run wait for report action.
446+
Raises:
447+
UnexpectedParsingError: Raised if the expected queue does not exist.
448+
'''
449+
super().__init__(test_step.label, test_step.identity)
450+
if test_step.command != 'PairWithCode':
451+
raise UnexpectedParsingError(f'Unexpected CommisionerCommand {test_step.command}')
452+
453+
args = test_step.arguments['values']
454+
request_data_as_dict = Converter.convert_list_of_name_value_pair_to_dict(args)
455+
self._setup_payload = request_data_as_dict['payload']
456+
self._node_id = request_data_as_dict['nodeId']
457+
458+
def run_action(self, dev_ctrl: ChipDeviceCtrl) -> _ActionResult:
459+
resp = dev_ctrl.CommissionWithCode(self._setup_payload, self._node_id)
460+
461+
if resp:
462+
return _ActionResult(status=_ActionStatus.SUCCESS, response=None)
463+
else:
464+
return _ActionResult(status=_ActionStatus.ERROR, response=None)
465+
466+
420467
class ReplTestRunner:
421468
'''Test runner to encode/decode values from YAML test Parser for executing the TestStep.
422469
423470
Uses ChipDeviceCtrl from chip-repl to execute parsed YAML TestSteps.
424471
'''
425472

426-
def __init__(self, test_spec_definition, dev_ctrl):
473+
def __init__(self, test_spec_definition, certificate_authority_manager):
427474
self._test_spec_definition = test_spec_definition
428-
self._dev_ctrl = dev_ctrl
429475
self._context = _ExecutionContext(data_model_lookup=PreDefinedDataModelLookup())
476+
self._certificate_authority_manager = certificate_authority_manager
477+
self._dev_ctrls = {}
478+
479+
ca_list = certificate_authority_manager.activeCaList
480+
dev_ctrl = ca_list[0].adminList[0].NewController()
481+
self._dev_ctrls['alpha'] = dev_ctrl
430482

431483
def _invoke_action_factory(self, test_step, cluster: str):
432484
'''Creates cluster invoke action command from TestStep.
@@ -513,12 +565,21 @@ def _wait_for_report_action_factory(self, test_step):
513565
# propogated.
514566
return None
515567

568+
def _commissioner_command_action_factory(self, test_step):
569+
try:
570+
return CommissionerCommandAction(test_step)
571+
except ParsingError:
572+
return None
573+
516574
def encode(self, request) -> BaseAction:
517575
action = None
518576
cluster = request.cluster.replace(' ', '').replace('/', '')
519577
command = request.command
578+
if cluster == 'CommissionerCommands':
579+
return self._commissioner_command_action_factory(request)
520580
# Some of the tests contain 'cluster over-rides' that refer to a different
521581
# cluster than that specified in 'config'.
582+
522583
if cluster == 'DelayCommands' and command == 'WaitForCommissionee':
523584
action = self._wait_for_commissionee_action_factory(request)
524585
elif command == 'writeAttribute':
@@ -588,8 +649,33 @@ def decode(self, result: _ActionResult):
588649

589650
return decoded_response
590651

652+
def _get_fabric_id(self, id):
653+
return _TestFabricId[id.upper()].value
654+
655+
def _get_dev_ctrl(self, action: BaseAction):
656+
if action.identity is not None:
657+
dev_ctrl = self._dev_ctrls.get(action.identity, None)
658+
if dev_ctrl is None:
659+
fabric_id = self._get_fabric_id(action.identity)
660+
certificate_authority = self._certificate_authority_manager.activeCaList[0]
661+
fabric = None
662+
for existing_admin in certificate_authority.adminList:
663+
if existing_admin.fabricId == fabric_id:
664+
fabric = existing_admin
665+
666+
if fabric is None:
667+
fabric = certificate_authority.NewFabricAdmin(vendorId=0xFFF1,
668+
fabricId=fabric_id)
669+
dev_ctrl = fabric.NewController()
670+
self._dev_ctrls[action.identity] = dev_ctrl
671+
else:
672+
dev_ctrl = self._dev_ctrls['alpha']
673+
674+
return dev_ctrl
675+
591676
def execute(self, action: BaseAction):
592-
return action.run_action(self._dev_ctrl)
677+
dev_ctrl = self._get_dev_ctrl(action)
678+
return action.run_action(dev_ctrl)
593679

594680
def shutdown(self):
595681
for subscription in self._context.subscriptions:

0 commit comments

Comments
 (0)