blob: d51d0fb1db98a7b7d8d172b302cbd5f19d883557 [file]
#
# Copyright (c) 2026 Project CHIP Authors
# All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Shared helpers for the spoken and audio Messages cluster test cases.
TC-MESS-3.1, 3.2 and 3.3 all present a message and then follow its lifecycle through
the MessageQueued / MessagePresented / MessageComplete / MessageNotPresented events.
"""
import logging
import queue
import time
from collections.abc import Sequence
from typing import Any
from mobly import asserts
import matter.clusters as Clusters
from matter.clusters import ClusterObjects
from matter.clusters.Types import NullValue
from matter.testing.decorators import EndpointCheckFunction, has_feature
from matter.testing.event_attribute_reporting import EventSubscriptionHandler
log = logging.getLogger(__name__)
_MESSAGES = Clusters.Messages
# MessageIDs used by the test plan. Both are 16-byte octet strings.
MESSAGE_ID_1 = bytes.fromhex("AABBCCDDEEFF00112233445566778899")
MESSAGE_ID_2 = bytes.fromhex("BBCCDDEE0011223344556677889900AA")
def has_spoken_or_audio_messages() -> EndpointCheckFunction:
"""Accept endpoints implementing either the SpokenMessages or AudioMessages feature."""
spoken = has_feature(_MESSAGES, _MESSAGES.Bitmaps.Feature.kSpokenMessages)
audio = has_feature(_MESSAGES, _MESSAGES.Bitmaps.Feature.kAudioMessages)
def accept(wildcard, endpoint) -> bool:
return spoken(wildcard, endpoint) or audio(wildcard, endpoint)
return accept
class MESSTestBase:
async def start_message_event_subscription(self, endpoint) -> EventSubscriptionHandler:
"""Subscribe to all Messages cluster events on the given endpoint."""
handler = EventSubscriptionHandler(expected_cluster=_MESSAGES)
await handler.start(self.default_controller, self.dut_node_id, endpoint)
return handler
async def send_present_message(self, endpoint, message_id: bytes, message_control: int,
priority=_MESSAGES.Enums.MessagePriorityEnum.kLow,
message_text: str = "", language_code: str | None = None,
message_uri: str | None = None, duration_ms: int | None = None):
"""Send PresentMessagesRequest and assert it is accepted.
``duration_ms`` of None sends a null Duration, which the specification defines as
"until changed" - the message stays presented and never completes on its own. Pass a
value when the test needs the message to finish.
"""
return await self.send_single_cmd(
cmd=_MESSAGES.Commands.PresentMessagesRequest(
messageID=message_id,
priority=priority,
messageControl=message_control,
startTime=NullValue,
duration=duration_ms if duration_ms is not None else NullValue,
messageText=message_text,
languageCode=language_code,
messageURI=message_uri),
endpoint=endpoint)
async def send_cancel_message(self, endpoint, message_ids: list[bytes]):
return await self.send_single_cmd(
cmd=_MESSAGES.Commands.CancelMessagesRequest(messageIDs=message_ids), endpoint=endpoint)
def wait_for_message_event(self, handler: EventSubscriptionHandler, event, message_id: bytes,
timeout_sec: float = 30.0):
"""Wait for ``event`` carrying ``message_id``, ignoring events for other messages.
Messages left over from an earlier step can still be in flight, so events for other
MessageIDs are discarded rather than failing the wait.
"""
deadline = time.monotonic() + timeout_sec
while True:
remaining = deadline - time.monotonic()
if remaining <= 0:
asserts.fail(f"Timed out waiting for {event.__name__} with MessageID {message_id.hex()}")
data = handler.wait_for_event_type_report(event, timeout_sec=remaining)
if data.messageID == message_id:
return data
log.info("Ignoring %s for unrelated MessageID %s", event.__name__, data.messageID.hex())
def wait_for_one_of_message_events(self, handler: EventSubscriptionHandler,
event_types: Sequence[type[ClusterObjects.ClusterEvent]],
message_id: bytes,
timeout_sec: float = 30.0) -> tuple[type[ClusterObjects.ClusterEvent], Any]:
"""Wait for whichever of ``event_types`` arrives first for ``message_id``.
``wait_for_message_event`` discards every event that is not the one type it wants, so a
test that waits for the wrong outcome only learns that it timed out. Waiting on several
types at once lets the caller report which outcome the DUT actually produced.
Returns a ``(event_type, data)`` tuple.
"""
wanted = {event.event_id: event for event in event_types}
deadline = time.monotonic() + timeout_sec
while True:
remaining = deadline - time.monotonic()
if remaining <= 0:
names = ", ".join(event.__name__ for event in event_types)
asserts.fail(f"Timed out waiting for one of [{names}] with MessageID {message_id.hex()}")
try:
event = handler.get_event_from_queue(block=True, timeout=remaining)
except queue.Empty:
continue
event_type = wanted.get(event.Header.EventId)
if event_type is None:
log.info("Ignoring event %s while waiting for a decision on MessageID %s",
event.Header.EventId, message_id.hex())
continue
if event.Data.messageID == message_id:
return event_type, event.Data
log.info("Ignoring %s for unrelated MessageID %s", event_type.__name__, event.Data.messageID.hex())
async def read_messages_attribute(self, endpoint):
return await self.read_single_attribute_check_success(
endpoint=endpoint, cluster=_MESSAGES, attribute=_MESSAGES.Attributes.Messages)
async def assert_message_in_list(self, endpoint, message_id: bytes, expected_present: bool):
messages = await self.read_messages_attribute(endpoint)
present = any(message.messageID == message_id for message in messages)
if expected_present:
asserts.assert_true(present, f"MessageID {message_id.hex()} should still be in the Messages attribute")
else:
asserts.assert_false(present, f"MessageID {message_id.hex()} should no longer be in the Messages attribute")
return messages