Skip to content

Commit

Permalink
Enable Multi-Fabric test commands with chip-repl runner (#24349)
Browse files Browse the repository at this point in the history
* Enable Multi-Fabric test commands with chip-repl runner

* Restyle
  • Loading branch information
tehampson authored and pull[bot] committed Jun 2, 2023
1 parent 5f070c7 commit a6ff7cf
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 21 deletions.
34 changes: 26 additions & 8 deletions src/controller/python/chip/yaml/format_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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):
Expand Down
112 changes: 99 additions & 13 deletions src/controller/python/chip/yaml/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,6 +39,12 @@ class _ActionStatus(Enum):
ERROR = 'error'


class _TestFabricId(IntEnum):
ALPHA = 1,
BETA = 2,
GAMMA = 3


@dataclass
class _ActionResult:
status: _ActionStatus
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand All @@ -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)

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit a6ff7cf

Please sign in to comment.