Skip to content

Commit 9b58d4c

Browse files
mkardous-silabsrestyled-commitsyunhanw-googlecjandhyalaandreilitvin
authoredSep 6, 2024··
[ICD] Add ICDM 5.1 Automated Test Script (#34817)
* Add ICDM 5.1 Automated Test Script * fix restyle * Restyled by prettier-json * Restyled by isort * Increase discovery timeout * Set log to true to debug CI * Update tests.yaml * Increase discovery timeout * Update manualTests.json * debugging TC_ICDM_5_1.py * dump ufw for mdns discovery in ci * Apply fixes for style and dnssd logic * Remove some debug code * Undo ufw change * One more readability update now that I update this file * Better error message on service not found * A few more updates for logging and logic when to unlock events * Restyle --------- Co-authored-by: Restyled.io <commits@restyled.io> Co-authored-by: yunhanw-google <yunhanw@google.com> Co-authored-by: cjandhyala <68604034+cjandhyala@users.noreply.github.com> Co-authored-by: Andrei Litvin <andreilitvin@google.com> Co-authored-by: Andrei Litvin <andy314@gmail.com>
1 parent 36f0c50 commit 9b58d4c

File tree

4 files changed

+254
-187
lines changed

4 files changed

+254
-187
lines changed
 

‎src/app/tests/suites/certification/Test_TC_ICDM_5_1.yaml

-145
This file was deleted.

‎src/app/tests/suites/manualTests.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@
117117
"GeneralCommissioning": ["Test_TC_CGEN_2_2"],
118118
"GeneralDiagnostics": ["Test_TC_DGGEN_2_2"],
119119
"Identify": ["Test_TC_I_3_2"],
120-
"IcdManagement": ["Test_TC_ICDM_4_1", "Test_TC_ICDM_5_1"],
120+
"IcdManagement": [],
121121
"IlluminanceMeasurement": [],
122122
"InteractionDataModel": [
123123
"Test_TC_IDM_1_1",

‎src/python_testing/TC_ICDM_5_1.py

+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#
2+
# Copyright (c) 2023 Project CHIP Authors
3+
# All rights reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments
19+
# for details about the block below.
20+
#
21+
# === BEGIN CI TEST ARGUMENTS ===
22+
# test-runner-runs: run1
23+
# test-runner-run/run1/app: ${LIT_ICD_APP}
24+
# test-runner-run/run1/factoryreset: True
25+
# test-runner-run/run1/quiet: True
26+
# test-runner-run/run1/app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json
27+
# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --PICS src/app/tests/suites/certification/ci-pics-values --trace-to json:${TRACE_TEST_JSON}.json --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto
28+
# === END CI TEST ARGUMENTS ===
29+
30+
import logging
31+
from dataclasses import dataclass
32+
33+
import chip.clusters as Clusters
34+
from chip.interaction_model import InteractionModelError, Status
35+
from matter_testing_support import MatterBaseTest, TestStep, async_test_body, default_matter_test_main
36+
from mdns_discovery import mdns_discovery
37+
from mobly import asserts
38+
39+
Cluster = Clusters.Objects.IcdManagement
40+
Commands = Cluster.Commands
41+
Attributes = Cluster.Attributes
42+
OperatingModeEnum = Cluster.Enums.OperatingModeEnum
43+
ClientTypeEnum = Cluster.Enums.ClientTypeEnum
44+
45+
46+
@dataclass
47+
class Client:
48+
checkInNodeID: int
49+
subjectId: int
50+
key: bytes
51+
clientType: ClientTypeEnum
52+
53+
54+
logger = logging.getLogger(__name__)
55+
kRootEndpointId = 0
56+
57+
client1 = Client(
58+
checkInNodeID=1,
59+
subjectId=1,
60+
key=bytes([x for x in range(0x10, 0x20)]),
61+
clientType=ClientTypeEnum.kEphemeral
62+
)
63+
64+
65+
class TC_ICDM_5_1(MatterBaseTest):
66+
67+
#
68+
# Class Helper functions
69+
#
70+
71+
async def _read_icdm_attribute_expect_success(self, attribute) -> OperatingModeEnum:
72+
return await self.read_single_attribute_check_success(endpoint=kRootEndpointId, cluster=Cluster, attribute=attribute)
73+
74+
async def _send_single_icdm_command(self, command):
75+
return await self.send_single_cmd(command, endpoint=kRootEndpointId)
76+
77+
async def _get_icd_txt_record(self) -> OperatingModeEnum:
78+
discovery = mdns_discovery.MdnsDiscovery(verbose_logging=True)
79+
service = await discovery.get_operational_service(
80+
node_id=self.dut_node_id,
81+
compressed_fabric_id=self.default_controller.GetCompressedFabricId(),
82+
log_output=True, discovery_timeout_sec=240)
83+
84+
asserts.assert_is_not_none(
85+
service, f"Failed to get operational node service information for {self.dut_node_id} on {self.default_controller.GetCompressedFabricId()}")
86+
87+
icdTxtRecord = OperatingModeEnum(int(service.txt_record['ICD']))
88+
if icdTxtRecord.value != int(service.txt_record['ICD']):
89+
raise AttributeError(f'Not a known ICD type: {service.txt_record["ICD"]}')
90+
91+
return icdTxtRecord
92+
93+
#
94+
# Test Harness Helpers
95+
#
96+
97+
def desc_TC_ICDM_5_1(self) -> str:
98+
"""Returns a description of this test"""
99+
return "[TC-ICDM-5.1] Operating Mode with DUT as Server"
100+
101+
def steps_TC_ICDM_5_1(self) -> list[TestStep]:
102+
steps = [
103+
TestStep(0, "Commissioning, already done", is_commissioning=True),
104+
TestStep(1, "TH reads from the DUT the RegisteredClients attribute"),
105+
TestStep("2a", "TH reads from the DUT the OperatingMode attribute."),
106+
TestStep("2b", "Verify that the ICD DNS-SD TXT key is present."),
107+
TestStep("3a", "TH sends RegisterClient command."),
108+
TestStep("3b", "TH reads from the DUT the OperatingMode attribute."),
109+
TestStep("3c", "Verify that mDNS is advertising ICD key."),
110+
TestStep(4, "TH sends UnregisterClient command with CheckInNodeID1."),
111+
TestStep("5a", "TH reads from the DUT the OperatingMode attribute."),
112+
TestStep("5b", "Verify that the ICD DNS-SD TXT key is present."),
113+
]
114+
return steps
115+
116+
def pics_TC_ICDM_5_1(self) -> list[str]:
117+
""" This function returns a list of PICS for this test case that must be True for the test to be run"""
118+
pics = [
119+
"ICDM.S",
120+
"ICDM.S.F02",
121+
]
122+
return pics
123+
124+
#
125+
# ICDM 5.1 Test Body
126+
#
127+
128+
@async_test_body
129+
async def test_TC_ICDM_5_1(self):
130+
131+
# Commissioning
132+
self.step(0)
133+
134+
try:
135+
self.step(1)
136+
registeredClients = await self._read_icdm_attribute_expect_success(
137+
Attributes.RegisteredClients)
138+
139+
for client in registeredClients:
140+
try:
141+
await self._send_single_icdm_command(Commands.UnregisterClient(checkInNodeID=client.checkInNodeID))
142+
except InteractionModelError as e:
143+
asserts.assert_equal(
144+
e.status, Status.Success, "Unexpected error returned")
145+
146+
self.step("2a")
147+
operatingMode = await self._read_icdm_attribute_expect_success(Attributes.OperatingMode)
148+
asserts.assert_equal(operatingMode, OperatingModeEnum.kSit)
149+
150+
self.step("2b")
151+
icdTxtRecord = await self._get_icd_txt_record()
152+
asserts.assert_equal(icdTxtRecord, OperatingModeEnum.kSit, "OperatingMode Is not in SIT mode.")
153+
154+
self.step("3a")
155+
try:
156+
await self._send_single_icdm_command(Commands.RegisterClient(checkInNodeID=client1.checkInNodeID, monitoredSubject=client1.subjectId, key=client1.key, clientType=client1.clientType))
157+
except InteractionModelError as e:
158+
asserts.assert_equal(
159+
e.status, Status.Success, "Unexpected error returned")
160+
161+
self.step("3b")
162+
operatingMode = await self._read_icdm_attribute_expect_success(Attributes.OperatingMode)
163+
asserts.assert_equal(operatingMode, OperatingModeEnum.kLit)
164+
165+
self.step("3c")
166+
icdTxtRecord = await self._get_icd_txt_record()
167+
asserts.assert_equal(icdTxtRecord, OperatingModeEnum.kLit, "OperatingMode Is not in Lit mode.")
168+
169+
self.step(4)
170+
try:
171+
await self._send_single_icdm_command(Commands.UnregisterClient(checkInNodeID=client1.checkInNodeID))
172+
except InteractionModelError as e:
173+
asserts.assert_equal(
174+
e.status, Status.Success, "Unexpected error returned")
175+
176+
self.step("5a")
177+
operatingMode = await self._read_icdm_attribute_expect_success(Attributes.OperatingMode)
178+
asserts.assert_equal(operatingMode, OperatingModeEnum.kSit)
179+
180+
self.step("5b")
181+
icdTxtRecord = await self._get_icd_txt_record()
182+
asserts.assert_equal(icdTxtRecord, OperatingModeEnum.kSit, "OperatingMode Is not in SIT mode.")
183+
184+
finally:
185+
registeredClients = await self._read_icdm_attribute_expect_success(
186+
Attributes.RegisteredClients)
187+
188+
for client in registeredClients:
189+
try:
190+
await self._send_single_icdm_command(Commands.UnregisterClient(checkInNodeID=client.checkInNodeID))
191+
except InteractionModelError as e:
192+
asserts.assert_equal(
193+
e.status, Status.Success, "Unexpected error returned")
194+
195+
196+
if __name__ == "__main__":
197+
default_matter_test_main()

‎src/python_testing/mdns_discovery/mdns_discovery.py

+56-41
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@
1818

1919
import asyncio
2020
import json
21+
import logging
2122
from dataclasses import asdict, dataclass
2223
from enum import Enum
2324
from typing import Dict, List, Optional
2425

2526
from zeroconf import IPVersion, ServiceStateChange, Zeroconf
2627
from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconfServiceTypes
2728

29+
logger = logging.getLogger(__name__)
30+
2831

2932
@dataclass
3033
class MdnsServiceInfo:
@@ -79,7 +82,7 @@ class MdnsDiscovery:
7982

8083
DISCOVERY_TIMEOUT_SEC = 15
8184

82-
def __init__(self):
85+
def __init__(self, verbose_logging: bool = False):
8386
"""
8487
Initializes the MdnsDiscovery instance.
8588
@@ -99,9 +102,15 @@ def __init__(self):
99102
# A list of service types
100103
self._service_types = []
101104

105+
# Filtering to apply for received data items
106+
self._name_filter = None
107+
102108
# An asyncio Event to signal when a service has been discovered
103109
self._event = asyncio.Event()
104110

111+
# Verbose logging
112+
self._verbose_logging = verbose_logging
113+
105114
# Public methods
106115
async def get_commissioner_service(self, log_output: bool = False,
107116
discovery_timeout_sec: float = DISCOVERY_TIMEOUT_SEC
@@ -116,6 +125,7 @@ async def get_commissioner_service(self, log_output: bool = False,
116125
Returns:
117126
Optional[MdnsServiceInfo]: An instance of MdnsServiceInfo or None if timeout reached.
118127
"""
128+
self._name_filter = None
119129
return await self._get_service(MdnsServiceType.COMMISSIONER, log_output, discovery_timeout_sec)
120130

121131
async def get_commissionable_service(self, log_output: bool = False,
@@ -131,10 +141,12 @@ async def get_commissionable_service(self, log_output: bool = False,
131141
Returns:
132142
Optional[MdnsServiceInfo]: An instance of MdnsServiceInfo or None if timeout reached.
133143
"""
144+
self._name_filter = None
134145
return await self._get_service(MdnsServiceType.COMMISSIONABLE, log_output, discovery_timeout_sec)
135146

136-
async def get_operational_service(self, service_name: str = None,
137-
service_type: str = None,
147+
async def get_operational_service(self,
148+
node_id: Optional[int] = None,
149+
compressed_fabric_id: Optional[int] = None,
138150
discovery_timeout_sec: float = DISCOVERY_TIMEOUT_SEC,
139151
log_output: bool = False
140152
) -> Optional[MdnsServiceInfo]:
@@ -144,35 +156,16 @@ async def get_operational_service(self, service_name: str = None,
144156
Args:
145157
log_output (bool): Logs the discovered services to the console. Defaults to False.
146158
discovery_timeout_sec (float): Defaults to 15 seconds.
147-
service_name (str): The unique name of the mDNS service. Defaults to None.
148-
service_type (str): The service type of the service. Defaults to None.
159+
node_id: the node id to create the service name from
160+
compressed_fabric_id: the fabric id to create the service name from
149161
150162
Returns:
151163
Optional[MdnsServiceInfo]: An instance of MdnsServiceInfo or None if timeout reached.
152164
"""
153165
# Validation to ensure both or none of the parameters are provided
154-
if (service_name is None) != (service_type is None):
155-
raise ValueError("Both service_name and service_type must be provided together or not at all.")
156-
157-
mdns_service_info = None
158-
159-
if service_name is None and service_type is None:
160-
mdns_service_info = await self._get_service(MdnsServiceType.OPERATIONAL, log_output, discovery_timeout_sec)
161-
else:
162-
print(f"Looking for MDNS service type '{service_type}', service name '{service_name}'")
163166

164-
# Get service info
165-
service_info = AsyncServiceInfo(service_type, service_name)
166-
is_discovered = await service_info.async_request(self._zc, 3000)
167-
if is_discovered:
168-
mdns_service_info = self._to_mdns_service_info_class(service_info)
169-
self._discovered_services = {}
170-
self._discovered_services[service_type] = [mdns_service_info]
171-
172-
if log_output:
173-
self._log_output()
174-
175-
return mdns_service_info
167+
self._name_filter = f'{compressed_fabric_id:016x}-{node_id:016x}.{MdnsServiceType.OPERATIONAL.value}'.upper()
168+
return await self._get_service(MdnsServiceType.OPERATIONAL, log_output, discovery_timeout_sec)
176169

177170
async def get_border_router_service(self, log_output: bool = False,
178171
discovery_timeout_sec: float = DISCOVERY_TIMEOUT_SEC
@@ -237,7 +230,7 @@ async def _discover(self,
237230
if all_services:
238231
self._service_types = list(await AsyncZeroconfServiceTypes.async_find())
239232

240-
print(f"Browsing for MDNS service(s) of type: {self._service_types}")
233+
logger.info(f"Browsing for MDNS service(s) of type: {self._service_types}")
241234

242235
aiobrowser = AsyncServiceBrowser(zeroconf=self._zc,
243236
type_=self._service_types,
@@ -247,7 +240,7 @@ async def _discover(self,
247240
try:
248241
await asyncio.wait_for(self._event.wait(), timeout=discovery_timeout_sec)
249242
except asyncio.TimeoutError:
250-
print(f"MDNS service discovery timed out after {discovery_timeout_sec} seconds.")
243+
logger.error("MDNS service discovery timed out after %d seconds.", discovery_timeout_sec)
251244
finally:
252245
await aiobrowser.async_cancel()
253246

@@ -276,13 +269,25 @@ def _on_service_state_change(
276269
Returns:
277270
None: This method does not return any value.
278271
"""
279-
if state_change.value == ServiceStateChange.Added.value:
280-
self._event.set()
281-
asyncio.ensure_future(self._query_service_info(
282-
zeroconf,
283-
service_type,
284-
name)
285-
)
272+
if self._verbose_logging:
273+
logger.info("Service state change: %s on %s/%s", state_change, name, service_type)
274+
275+
if state_change.value == ServiceStateChange.Removed.value:
276+
return
277+
278+
if self._name_filter is not None and name.upper() != self._name_filter:
279+
if self._verbose_logging:
280+
logger.info(" Name does NOT match %s", self._name_filter)
281+
return
282+
283+
if self._verbose_logging:
284+
logger.info("Received service data. Unlocking service information")
285+
286+
asyncio.ensure_future(self._query_service_info(
287+
zeroconf,
288+
service_type,
289+
name)
290+
)
286291

287292
async def _query_service_info(self, zeroconf: Zeroconf, service_type: str, service_name: str) -> None:
288293
"""
@@ -304,12 +309,19 @@ async def _query_service_info(self, zeroconf: Zeroconf, service_type: str, servi
304309
service_info.async_clear_cache()
305310

306311
if is_service_discovered:
312+
if self._verbose_logging:
313+
logger.warning("Service discovered for %s/%s.", service_name, service_type)
314+
307315
mdns_service_info = self._to_mdns_service_info_class(service_info)
308316

309317
if service_type not in self._discovered_services:
310318
self._discovered_services[service_type] = [mdns_service_info]
311319
else:
312320
self._discovered_services[service_type].append(mdns_service_info)
321+
elif self._verbose_logging:
322+
logger.warning("Service information not found.")
323+
324+
self._event.set()
313325

314326
def _to_mdns_service_info_class(self, service_info: AsyncServiceInfo) -> MdnsServiceInfo:
315327
"""
@@ -355,21 +367,24 @@ async def _get_service(self, service_type: MdnsServiceType,
355367
any. Returns None if no service of the specified type is discovered within
356368
the timeout period.
357369
"""
358-
mdns_service_info = None
359370
self._service_types = [service_type.value]
360371
await self._discover(discovery_timeout_sec, log_output)
361-
if service_type.value in self._discovered_services:
362-
mdns_service_info = self._discovered_services[service_type.value][0]
363372

364-
return mdns_service_info
373+
if self._verbose_logging:
374+
logger.info("Getting service from discovered services: %s", self._discovered_services)
375+
376+
if service_type.value in self._discovered_services:
377+
return self._discovered_services[service_type.value][0]
378+
else:
379+
return None
365380

366381
def _log_output(self) -> str:
367382
"""
368-
Converts the discovered services to a JSON string and prints it.
383+
Converts the discovered services to a JSON string and log it.
369384
370385
The method is intended to be used for debugging or informational purposes, providing a clear and
371386
comprehensive view of all services discovered during the mDNS service discovery process.
372387
"""
373388
converted_services = {key: [asdict(item) for item in value] for key, value in self._discovered_services.items()}
374389
json_str = json.dumps(converted_services, indent=4)
375-
print(json_str)
390+
logger.info("Discovery data:\n%s", json_str)

0 commit comments

Comments
 (0)
Please sign in to comment.