Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable Multi-Fabric test commands with chip-repl runner #24349

Merged
merged 3 commits into from
Jan 12, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -15,12 +15,20 @@
# limitations under the License.
#

from dataclasses import dataclass
import enum
import typing

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
109 changes: 98 additions & 11 deletions src/controller/python/chip/yaml/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,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 +100,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 +134,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 +159,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 +196,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 @@ -303,7 +315,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 @@ -364,7 +376,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 @@ -383,16 +395,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):
tehampson marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -470,12 +516,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 command == 'writeAttribute':
action = self._attribute_write_action_factory(request, cluster)
elif command == 'readAttribute':
Expand Down Expand Up @@ -543,8 +598,40 @@ def decode(self, result: _ActionResult):

return decoded_response

def _get_fabric_id(self, identity):
if identity == 'alpha':
return 1
tehampson marked this conversation as resolved.
Show resolved Hide resolved
if identity == 'beta':
return 2
if identity == 'gamma':
return 3

raise ValueError(f'Unknown identity {identity}')

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