Skip to content

Commit 98dc92b

Browse files
authored
Add read/subscribe event to chip-repl based yamltests (#24904)
* chip-repl read event yamltests working with hacks Need to clean this up * Address PR comments
1 parent 11c0cdf commit 98dc92b

File tree

3 files changed

+195
-19
lines changed

3 files changed

+195
-19
lines changed

scripts/tests/chiptest/__init__.py

-7
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,6 @@ def _GetInDevelopmentTests() -> Set[str]:
131131
Goal is for this set to become empty.
132132
"""
133133
return {
134-
# TODO: Event not yet supported:
135-
"Test_TC_ACL_2_10.yaml",
136-
"Test_TC_ACL_2_7.yaml",
137-
"Test_TC_ACL_2_8.yaml",
138-
"Test_TC_ACL_2_9.yaml",
139-
"TestEvents.yaml",
140-
141134
"TestGroupMessaging.yaml", # Needs group support in repl
142135
}
143136

src/controller/python/chip/ChipDeviceCtrl.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1164,7 +1164,7 @@ async def ReadEvent(self, nodeid: int, events: typing.List[typing.Union[
11641164
typing.Tuple[int, typing.Type[ClusterObjects.Cluster], int],
11651165
# Concrete path
11661166
typing.Tuple[int, typing.Type[ClusterObjects.ClusterEvent], int]
1167-
]], eventNumberFilter: typing.Optional[int] = None, reportInterval: typing.Tuple[int, int] = None, keepSubscriptions: bool = False):
1167+
]], eventNumberFilter: typing.Optional[int] = None, fabricFiltered: bool = True, reportInterval: typing.Tuple[int, int] = None, keepSubscriptions: bool = False):
11681168
'''
11691169
Read a list of events from a target node, this is a wrapper of DeviceController.Read()
11701170
@@ -1188,7 +1188,7 @@ async def ReadEvent(self, nodeid: int, events: typing.List[typing.Union[
11881188
reportInterval: A tuple of two int-s for (MinIntervalFloor, MaxIntervalCeiling). Used by establishing subscriptions.
11891189
When not provided, a read request will be sent.
11901190
'''
1191-
res = await self.Read(nodeid=nodeid, events=events, eventNumberFilter=eventNumberFilter, reportInterval=reportInterval, keepSubscriptions=keepSubscriptions)
1191+
res = await self.Read(nodeid=nodeid, events=events, eventNumberFilter=eventNumberFilter, fabricFiltered=fabricFiltered, reportInterval=reportInterval, keepSubscriptions=keepSubscriptions)
11921192
if isinstance(res, ClusterAttribute.SubscriptionTransaction):
11931193
return res
11941194
else:

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

+193-10
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
import chip.yaml.format_converter as Converter
2727
import stringcase
2828
from chip.ChipDeviceCtrl import ChipDeviceController, discovery
29-
from chip.clusters.Attribute import AttributeStatus, SubscriptionTransaction, TypedAttributePath, ValueDecodeFailure
29+
from chip.clusters.Attribute import (AttributeStatus, EventReadResult, SubscriptionTransaction, TypedAttributePath,
30+
ValueDecodeFailure)
3031
from chip.exceptions import ChipStackError
3132
from chip.yaml.errors import ParsingError, UnexpectedParsingError
3233
from matter_yamltests.pseudo_clusters.clusters.delay_commands import DelayCommands
@@ -56,6 +57,11 @@ class _GetCommissionerNodeIdResult:
5657
node_id: int
5758

5859

60+
@dataclass
61+
class EventResponse:
62+
event_result_list: list[EventReadResult]
63+
64+
5965
@dataclass
6066
class _ActionResult:
6167
status: _ActionStatus
@@ -69,6 +75,12 @@ class _AttributeSubscriptionCallbackResult:
6975
result: _ActionResult
7076

7177

78+
@dataclass
79+
class _EventSubscriptionCallbackResult:
80+
name: str
81+
result: _ActionResult
82+
83+
7284
@dataclass
7385
class _ExecutionContext:
7486
''' Objects that is commonly passed around this file that are vital to test execution.'''
@@ -78,7 +90,8 @@ class _ExecutionContext:
7890
subscriptions: list = field(default_factory=list)
7991
# The key is the attribute/event name, and the value is a queue of subscription callback results
8092
# that been sent by device under test. For attribute subscription the queue is of type
81-
# _AttributeSubscriptionCallbackResult.
93+
# _AttributeSubscriptionCallbackResult, for event the queue is of type
94+
# _EventSubscriptionCallbackResult.
8295
subscription_callback_result_queue: dict = field(default_factory=dict)
8396

8497

@@ -266,6 +279,55 @@ def parse_raw_response(self, raw_resp) -> _ActionResult:
266279
return _ActionResult(status=_ActionStatus.SUCCESS, response=return_val)
267280

268281

282+
class ReadEventAction(BaseAction):
283+
''' Read Event action to be executed.'''
284+
285+
def __init__(self, test_step, cluster: str, context: _ExecutionContext):
286+
'''Converts 'test_step' to read event action that can execute with ChipDeviceController.
287+
288+
Args:
289+
'test_step': Step containing information required to run read event action.
290+
'cluster': Name of cluster read event action is targeting.
291+
'context': Contains test-wide common objects such as DataModelLookup instance.
292+
Raises:
293+
UnexpectedParsingError: Raised if there is an unexpected parsing error.
294+
'''
295+
super().__init__(test_step)
296+
self._event_name = stringcase.pascalcase(test_step.event)
297+
self._cluster = cluster
298+
self._endpoint = test_step.endpoint
299+
self._node_id = test_step.node_id
300+
self._cluster_object = None
301+
self._request_object = None
302+
self._event_number_filter = test_step.event_number
303+
self._fabric_filtered = False
304+
305+
if test_step.fabric_filtered is not None:
306+
self._fabric_filtered = test_step.fabric_filtered
307+
308+
self._request_object = context.data_model_lookup.get_event(self._cluster,
309+
self._event_name)
310+
if self._request_object is None:
311+
raise UnexpectedParsingError(
312+
f'ReadEvent failed to find cluster:{self._cluster} Event:{self._event_name}')
313+
314+
if test_step.arguments:
315+
raise UnexpectedParsingError(
316+
f'ReadEvent should not contain arguments. {self.label}')
317+
318+
def run_action(self, dev_ctrl: ChipDeviceController) -> _ActionResult:
319+
try:
320+
urgent = 0
321+
request = [(self._endpoint, self._request_object, urgent)]
322+
resp = asyncio.run(dev_ctrl.ReadEvent(self._node_id, events=request, eventNumberFilter=self._event_number_filter,
323+
fabricFiltered=self._fabric_filtered))
324+
except chip.interaction_model.InteractionModelError as error:
325+
return _ActionResult(status=_ActionStatus.ERROR, response=error)
326+
327+
parsed_resp = EventResponse(event_result_list=resp)
328+
return _ActionResult(status=_ActionStatus.SUCCESS, response=parsed_resp)
329+
330+
269331
class WaitForCommissioneeAction(BaseAction):
270332
''' Wait for commissionee action to be executed.'''
271333

@@ -327,6 +389,27 @@ def name(self) -> str:
327389
return self._name
328390

329391

392+
class EventChangeAccumulator:
393+
def __init__(self, name: str, expected_event, output_queue: queue.SimpleQueue):
394+
self._name = name
395+
self._expected_event = expected_event
396+
self._output_queue = output_queue
397+
398+
def __call__(self, event_result: EventReadResult, transaction: SubscriptionTransaction):
399+
if (self._expected_event.cluster_id == event_result.Header.ClusterId and
400+
self._expected_event.event_id == event_result.Header.EventId):
401+
event_response = EventResponse(event_result_list=[event_result])
402+
result = _ActionResult(status=_ActionStatus.SUCCESS, response=event_response)
403+
404+
item = _EventSubscriptionCallbackResult(self._name, result)
405+
logging.debug(f'Got subscription report on client {self.name}')
406+
self._output_queue.put(item)
407+
408+
@property
409+
def name(self) -> str:
410+
return self._name
411+
412+
330413
class SubscribeAttributeAction(ReadAttributeAction):
331414
'''Single subscribe attribute action to be executed.'''
332415

@@ -382,6 +465,63 @@ def run_action(self, dev_ctrl: ChipDeviceController) -> _ActionResult:
382465
return self.parse_raw_response(raw_resp)
383466

384467

468+
class SubscribeEventAction(ReadEventAction):
469+
'''Single subscribe event action to be executed.'''
470+
471+
def __init__(self, test_step, cluster: str, context: _ExecutionContext):
472+
'''Converts 'test_step' to subscribe event action that can execute with ChipDeviceController.
473+
474+
Args:
475+
'test_step': Step containing information required to run subscribe event action.
476+
'cluster': Name of cluster subscribe event action is targeting.
477+
'context': Contains test-wide common objects such as DataModelLookup instance.
478+
Raises:
479+
ParsingError: Raised if there is a benign error, and there is currently no
480+
action to perform for this subscribe event.
481+
UnexpectedParsingError: Raised if there is an unexpected parsing error.
482+
'''
483+
super().__init__(test_step, cluster, context)
484+
self._context = context
485+
if test_step.min_interval is None:
486+
raise UnexpectedParsingError(
487+
f'SubscribeEvent action does not have min_interval {self.label}')
488+
self._min_interval = test_step.min_interval
489+
490+
if test_step.max_interval is None:
491+
raise UnexpectedParsingError(
492+
f'SubscribeEvent action does not have max_interval {self.label}')
493+
self._max_interval = test_step.max_interval
494+
495+
def run_action(self, dev_ctrl: ChipDeviceController) -> _ActionResult:
496+
try:
497+
urgent = 0
498+
request = [(self._endpoint, self._request_object, urgent)]
499+
subscription = asyncio.run(
500+
dev_ctrl.ReadEvent(self._node_id, events=request, eventNumberFilter=self._event_number_filter,
501+
reportInterval=(self._min_interval, self._max_interval),
502+
keepSubscriptions=False))
503+
except chip.interaction_model.InteractionModelError as error:
504+
return _ActionResult(status=_ActionStatus.ERROR, response=error)
505+
506+
self._context.subscriptions.append(subscription)
507+
output_queue = self._context.subscription_callback_result_queue.get(self._event_name,
508+
None)
509+
if output_queue is None:
510+
output_queue = queue.SimpleQueue()
511+
self._context.subscription_callback_result_queue[self._event_name] = output_queue
512+
513+
while not output_queue.empty():
514+
output_queue.get(block=False)
515+
516+
subscription_handler = EventChangeAccumulator(self.label, self._request_object, output_queue)
517+
518+
subscription.SetEventUpdateCallback(subscription_handler)
519+
520+
events = subscription.GetEvents()
521+
response = EventResponse(event_result_list=events)
522+
return _ActionResult(status=_ActionStatus.SUCCESS, response=response)
523+
524+
385525
class WriteAttributeAction(BaseAction):
386526
'''Single write attribute action to be executed.'''
387527

@@ -462,9 +602,15 @@ def __init__(self, test_step, context: _ExecutionContext):
462602
UnexpectedParsingError: Raised if the expected queue does not exist.
463603
'''
464604
super().__init__(test_step)
465-
self._attribute_name = stringcase.pascalcase(test_step.attribute)
466-
self._output_queue = context.subscription_callback_result_queue.get(self._attribute_name,
467-
None)
605+
if test_step.attribute is not None:
606+
queue_name = stringcase.pascalcase(test_step.attribute)
607+
elif test_step.event is not None:
608+
queue_name = stringcase.pascalcase(test_step.event)
609+
else:
610+
raise UnexpectedParsingError(
611+
f'WaitForReport needs to wait on either attribute or event, neither were provided')
612+
613+
self._output_queue = context.subscription_callback_result_queue.get(queue_name, None)
468614
if self._output_queue is None:
469615
raise UnexpectedParsingError(f'Could not find output queue')
470616

@@ -477,6 +623,8 @@ def run_action(self, dev_ctrl: ChipDeviceController) -> _ActionResult:
477623
except queue.Empty:
478624
return _ActionResult(status=_ActionStatus.ERROR, response=None)
479625

626+
if isinstance(item, _AttributeSubscriptionCallbackResult):
627+
return item.result
480628
return item.result
481629

482630

@@ -621,14 +769,15 @@ def _attribute_read_action_factory(self, test_step, cluster: str):
621769
'cluster': Name of cluster read attribute action is targeting.
622770
Returns:
623771
ReadAttributeAction if 'test_step' is a valid read attribute to be executed.
624-
None if we were unable to use the provided 'test_step' for a known reason that is not
625-
fatal to test execution.
626772
'''
627773
try:
628774
return ReadAttributeAction(test_step, cluster, self._context)
629775
except ParsingError:
630776
return None
631777

778+
def _event_read_action_factory(self, test_step, cluster: str):
779+
return ReadEventAction(test_step, cluster, self._context)
780+
632781
def _attribute_subscribe_action_factory(self, test_step, cluster: str):
633782
'''Creates subscribe attribute command from TestStep provided.
634783
@@ -648,6 +797,17 @@ def _attribute_subscribe_action_factory(self, test_step, cluster: str):
648797
# propogated.
649798
return None
650799

800+
def _attribute_subscribe_event_factory(self, test_step, cluster: str):
801+
'''Creates subscribe event command from TestStep provided.
802+
803+
Args:
804+
'test_step': Step containing information required to run subscribe attribute action.
805+
'cluster': Name of cluster write attribute action is targeting.
806+
Returns:
807+
SubscribeEventAction if 'test_step' is a valid subscribe attribute to be executed.
808+
'''
809+
return SubscribeEventAction(test_step, cluster, self._context)
810+
651811
def _attribute_write_action_factory(self, test_step, cluster: str):
652812
'''Creates write attribute command TestStep.
653813
@@ -712,11 +872,11 @@ def encode(self, request) -> BaseAction:
712872
elif command == 'readAttribute':
713873
action = self._attribute_read_action_factory(request, cluster)
714874
elif command == 'readEvent':
715-
# TODO need to implement _event_read_action_factory
716-
# action = self._event_read_action_factory(request, cluster)
717-
pass
875+
action = self._event_read_action_factory(request, cluster)
718876
elif command == 'subscribeAttribute':
719877
action = self._attribute_subscribe_action_factory(request, cluster)
878+
elif command == 'subscribeEvent':
879+
action = self._attribute_subscribe_event_factory(request, cluster)
720880
elif command == 'waitForReport':
721881
action = self._wait_for_report_action_factory(request)
722882
else:
@@ -779,6 +939,29 @@ def decode(self, result: _ActionResult):
779939
}
780940
return decoded_response
781941

942+
if isinstance(response, EventResponse):
943+
if not response.event_result_list:
944+
# This means that the event result we got back was empty, below is how we
945+
# represent this.
946+
decoded_response = [{}]
947+
return decoded_response
948+
decoded_response = []
949+
for event in response.event_result_list:
950+
if event.Status != chip.interaction_model.Status.Success:
951+
error_message = stringcase.snakecase(event.Status.name).upper()
952+
decoded_response.append({'error': error_message})
953+
continue
954+
cluster_id = event.Header.ClusterId
955+
cluster_name = self._test_spec_definition.get_cluster_name(cluster_id)
956+
event_id = event.Header.EventId
957+
event_name = self._test_spec_definition.get_event_name(cluster_id, event_id)
958+
event_definition = self._test_spec_definition.get_event_by_name(cluster_name, event_name)
959+
is_fabric_scoped = bool(event_definition.is_fabric_sensitive)
960+
decoded_event = Converter.from_data_model_to_test_definition(
961+
self._test_spec_definition, cluster_name, event_definition.fields, event.Data, is_fabric_scoped)
962+
decoded_response.append({'value': decoded_event})
963+
return decoded_response
964+
782965
if isinstance(response, ChipStackError):
783966
decoded_response['error'] = 'FAILURE'
784967
return decoded_response

0 commit comments

Comments
 (0)