Skip to content

Commit a5fdd43

Browse files
authored
Merge branch 'master' into feature/add-validation-logic
2 parents a3ffe06 + 95bf668 commit a5fdd43

14 files changed

+253
-16
lines changed

BUILD.gn

+3-1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ if (current_toolchain != "${dir_pw_toolchain}/default:default") {
6767
"//examples/common/pigweed/rpc_console/py:chip_rpc",
6868
"//integrations/mobly:chip_mobly",
6969
"//scripts/py_matter_yamltests:matter_yamltests",
70+
"//src/python_testing/matter_testing_infrastructure:metadata_parser",
7071
]
7172

7273
pw_python_venv("matter_build_venv") {
@@ -107,6 +108,7 @@ if (current_toolchain != "${dir_pw_toolchain}/default:default") {
107108
deps = [
108109
"${chip_root}/scripts:matter_yamltests_distribution.wheel",
109110
"${chip_root}/src/controller/python:chip-repl",
111+
"${chip_root}/src/python_testing/matter_testing_infrastructure:metadata_parser.wheel",
110112
]
111113
if (enable_pylib) {
112114
deps += [ "${chip_root}/src/pybindings/pycontroller" ]
@@ -234,8 +236,8 @@ if (current_toolchain != "${dir_pw_toolchain}/default:default") {
234236
"//scripts/build:build_examples.tests",
235237
"//scripts/py_matter_idl:matter_idl.tests",
236238
"//scripts/py_matter_yamltests:matter_yamltests.tests",
237-
"//scripts/tests/py:metadata_parser.tests",
238239
"//src:tests_run",
240+
"//src/python_testing/matter_testing_infrastructure:metadata_parser.tests",
239241
]
240242

241243
if (current_os == "linux" || current_os == "mac") {

scripts/build_python.sh

+3
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,9 @@ else
212212
WHEEL=("$OUTPUT_ROOT"/controller/python/chip*.whl)
213213
fi
214214

215+
# Add the matter_testing_infrastructure wheel
216+
WHEEL+=("$OUTPUT_ROOT"/python/obj/src/python_testing/matter_testing_infrastructure/metadata_parser._build_wheel/metadata_parser-*.whl)
217+
215218
if [ -n "$extra_packages" ]; then
216219
WHEEL+=("$extra_packages")
217220
fi

scripts/tests/run_python_test.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
import click
3333
import coloredlogs
3434
from colorama import Fore, Style
35-
from py.metadata import Metadata, MetadataReader
35+
from metadata_parser.metadata import Metadata, MetadataReader
3636

3737
DEFAULT_CHIP_ROOT = os.path.abspath(
3838
os.path.join(os.path.dirname(__file__), '..', '..'))

src/python_testing/TC_DeviceConformance.py

+95-4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
# test-runner-run/run1/script-args: --storage-path admin_storage.json --manual-code 10054912339 --bool-arg ignore_in_progress:True allow_provisional:True --PICS src/app/tests/suites/certification/ci-pics-values --trace-to json:${TRACE_TEST_JSON}.json --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto --tests test_TC_IDM_10_2
2828
# === END CI TEST ARGUMENTS ===
2929

30+
# TODO: Enable 10.5 in CI once the door lock OTA requestor problem is sorted.
3031
from typing import Callable
3132

3233
import chip.clusters as Clusters
@@ -35,16 +36,19 @@
3536
from choice_conformance_support import (evaluate_attribute_choice_conformance, evaluate_command_choice_conformance,
3637
evaluate_feature_choice_conformance)
3738
from conformance_support import ConformanceDecision, conformance_allowed
38-
from global_attribute_ids import GlobalAttributeIds
39-
from matter_testing_support import (AttributePathLocation, ClusterPathLocation, CommandPathLocation, MatterBaseTest, ProblemNotice,
40-
ProblemSeverity, async_test_body, default_matter_test_main)
41-
from spec_parsing_support import CommandType, build_xml_clusters
39+
from global_attribute_ids import (ClusterIdType, DeviceTypeIdType, GlobalAttributeIds, cluster_id_type, device_type_id_type,
40+
is_valid_device_type_id)
41+
from matter_testing_support import (AttributePathLocation, ClusterPathLocation, CommandPathLocation, DeviceTypePathLocation,
42+
MatterBaseTest, ProblemNotice, ProblemSeverity, async_test_body, default_matter_test_main)
43+
from spec_parsing_support import CommandType, build_xml_clusters, build_xml_device_types
4244

4345

4446
class DeviceConformanceTests(BasicCompositionTests):
4547
async def setup_class_helper(self):
4648
await super().setup_class_helper()
4749
self.xml_clusters, self.problems = build_xml_clusters()
50+
self.xml_device_types, problems = build_xml_device_types()
51+
self.problems.extend(problems)
4852

4953
def check_conformance(self, ignore_in_progress: bool, is_ci: bool):
5054
problems = []
@@ -245,6 +249,86 @@ def record_warning(location, problem):
245249

246250
return success, problems
247251

252+
def check_device_type(self, fail_on_extra_clusters: bool = True, allow_provisional: bool = False) -> tuple[bool, list[ProblemNotice]]:
253+
success = True
254+
problems = []
255+
256+
def record_problem(location, problem, severity):
257+
problems.append(ProblemNotice("IDM-10.5", location, severity, problem, ""))
258+
259+
def record_error(location, problem):
260+
nonlocal success
261+
record_problem(location, problem, ProblemSeverity.ERROR)
262+
success = False
263+
264+
def record_warning(location, problem):
265+
record_problem(location, problem, ProblemSeverity.WARNING)
266+
267+
for endpoint_id, endpoint in self.endpoints.items():
268+
if Clusters.Descriptor not in endpoint:
269+
location = ClusterPathLocation(endpoint_id=endpoint_id, cluster_id=Clusters.Descriptor.id)
270+
record_error(location=location, problem='No descriptor cluster found on endpoint')
271+
continue
272+
273+
device_type_list = endpoint[Clusters.Descriptor][Clusters.Descriptor.Attributes.DeviceTypeList]
274+
invalid_device_types = [x for x in device_type_list if not is_valid_device_type_id(device_type_id_type(x.deviceType))]
275+
standard_device_types = [x for x in endpoint[Clusters.Descriptor]
276+
[Clusters.Descriptor.Attributes.DeviceTypeList] if device_type_id_type(x.deviceType) == DeviceTypeIdType.kStandard]
277+
endpoint_clusters = []
278+
server_clusters = []
279+
for device_type in invalid_device_types:
280+
location = DeviceTypePathLocation(device_type_id=device_type.deviceType)
281+
record_error(location=location, problem='Invalid device type ID (out of valid range)')
282+
283+
for device_type in standard_device_types:
284+
device_type_id = device_type.deviceType
285+
location = DeviceTypePathLocation(device_type_id=device_type_id)
286+
if device_type_id not in self.xml_device_types.keys():
287+
record_error(location=location, problem='Unknown device type ID in standard range')
288+
continue
289+
290+
if device_type_id not in self.xml_device_types.keys():
291+
location = DeviceTypePathLocation(device_type_id=device_type_id)
292+
record_error(location=location, problem='Unknown device type')
293+
continue
294+
295+
# TODO: check revision. Possibly in another test?
296+
297+
xml_device = self.xml_device_types[device_type_id]
298+
# IDM 10.1 checks individual clusters for validity,
299+
# so here we can ignore checks for invalid and manufacturer clusters.
300+
server_clusters = [x for x in endpoint[Clusters.Descriptor]
301+
[Clusters.Descriptor.Attributes.ServerList] if cluster_id_type(x) == ClusterIdType.kStandard]
302+
303+
# As a start, we are only checking server clusters
304+
# TODO: check client clusters too?
305+
for cluster_id, cluster_requirement in xml_device.server_clusters.items():
306+
# Device type cluster conformances do not include any conformances based on cluster elements
307+
conformance_decision_with_choice = cluster_requirement.conformance(0, [], [])
308+
location = DeviceTypePathLocation(device_type_id=device_type_id, cluster_id=cluster_id)
309+
if conformance_decision_with_choice.decision == ConformanceDecision.MANDATORY and cluster_id not in server_clusters:
310+
record_error(location=location,
311+
problem=f"Mandatory cluster {cluster_requirement.name} for device type {xml_device.name} is not present in the server list")
312+
success = False
313+
314+
if cluster_id in server_clusters and not conformance_allowed(conformance_decision_with_choice, allow_provisional):
315+
record_error(location=location,
316+
problem=f"Disallowed cluster {cluster_requirement.name} found in server list for device type {xml_device.name}")
317+
success = False
318+
# If we want to check for extra clusters on the endpoint, we need to know the entire set of clusters in all the device type
319+
# lists across all the device types on the endpoint.
320+
endpoint_clusters += xml_device.server_clusters.keys()
321+
if fail_on_extra_clusters:
322+
fn = record_error
323+
else:
324+
fn = record_warning
325+
extra_clusters = set(server_clusters) - set(endpoint_clusters)
326+
for extra in extra_clusters:
327+
location = ClusterPathLocation(endpoint_id=endpoint_id, cluster_id=extra)
328+
fn(location=location, problem=f"Extra cluster found on endpoint with device types {device_type_list}")
329+
330+
return success, problems
331+
248332

249333
class TC_DeviceConformance(MatterBaseTest, DeviceConformanceTests):
250334
@async_test_body
@@ -267,6 +351,13 @@ def test_TC_IDM_10_3(self):
267351
if not success:
268352
self.fail_current_test("Problems with cluster revision on at least one cluster")
269353

354+
def test_TC_IDM_10_5(self):
355+
fail_on_extra_clusters = self.user_params.get("fail_on_extra_clusters", True)
356+
success, problems = self.check_device_type(fail_on_extra_clusters)
357+
self.problems.extend(problems)
358+
if not success:
359+
self.fail_current_test("Problems with Device type conformance on one or more endpoints")
360+
270361

271362
if __name__ == "__main__":
272363
default_matter_test_main()

src/python_testing/TestSpecParsingDeviceType.py

+144-5
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,27 @@
1616
#
1717
import xml.etree.ElementTree as ElementTree
1818

19+
import chip.clusters as Clusters
20+
from chip.clusters import Attribute
21+
from chip.tlv import uint
22+
from conformance_support import conformance_allowed
1923
from jinja2 import Template
2024
from matter_testing_support import MatterBaseTest, default_matter_test_main
2125
from mobly import asserts
22-
from spec_parsing_support import build_xml_device_types, parse_single_device_type
26+
from spec_parsing_support import build_xml_clusters, build_xml_device_types, parse_single_device_type
27+
from TC_DeviceConformance import DeviceConformanceTests
2328

2429

2530
class TestSpecParsingDeviceType(MatterBaseTest):
26-
2731
# This just tests that the current spec can be parsed without failures
2832
def test_spec_device_parsing(self):
29-
device_types, problems = build_xml_device_types()
30-
self.problems += problems
31-
for id, d in device_types.items():
33+
for id, d in self.xml_device_types.items():
3234
print(str(d))
3335

3436
def setup_class(self):
37+
self.xml_clusters, self.xml_cluster_problems = build_xml_clusters()
38+
self.xml_device_types, self.xml_device_types_problems = build_xml_device_types()
39+
3540
self.device_type_id = 0xBBEF
3641
self.revision = 2
3742
self.classification_class = "simple"
@@ -106,6 +111,140 @@ def test_bad_scope(self):
106111
device_type, problems = parse_single_device_type(et)
107112
asserts.assert_equal(len(problems), 1, "Device with no scope did not generate a problem notice")
108113

114+
# All these tests are based on the temp sensor device type because it is very simple
115+
# it requires temperature measurement, identify and the base devices.
116+
# Right now I'm not testing for binding condition.
117+
# The test is entirely based on the descriptor cluster so that's all I'm populating here
118+
# because it makes the test less complex to write.
119+
def create_test(self, server_list: list[uint], no_descriptor: bool = False, bad_device_id: bool = False) -> DeviceConformanceTests:
120+
self.test = DeviceConformanceTests()
121+
self.test.xml_device_types = self.xml_device_types
122+
self.test.xml_clusters = self.xml_clusters
123+
124+
if bad_device_id:
125+
known_ids = list(self.test.xml_device_types.keys())
126+
device_type_id = [a for a in range(min(known_ids), max(known_ids)) if a not in known_ids][0]
127+
else:
128+
device_type_id = 0x0302
129+
130+
resp = Attribute.AsyncReadTransaction.ReadResponse({}, [], {})
131+
if no_descriptor:
132+
resp.attributes = {1: {}}
133+
else:
134+
desc = Clusters.Descriptor
135+
server_list_attr = Clusters.Descriptor.Attributes.ServerList
136+
device_type_list_attr = Clusters.Descriptor.Attributes.DeviceTypeList
137+
device_type_list = [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=device_type_id, revision=2)]
138+
resp.attributes = {1: {desc: {device_type_list_attr: device_type_list, server_list_attr: server_list}}}
139+
self.test.endpoints = resp.attributes
140+
141+
def create_good_device(self, device_type_id: int) -> DeviceConformanceTests:
142+
self.test = DeviceConformanceTests()
143+
self.test.xml_device_types = self.xml_device_types
144+
self.test.xml_clusters = self.xml_clusters
145+
146+
resp = Attribute.AsyncReadTransaction.ReadResponse({}, [], {})
147+
desc = Clusters.Descriptor
148+
server_list_attr = Clusters.Descriptor.Attributes.ServerList
149+
device_type_list_attr = Clusters.Descriptor.Attributes.DeviceTypeList
150+
device_type_list = [Clusters.Descriptor.Structs.DeviceTypeStruct(
151+
deviceType=device_type_id, revision=self.xml_device_types[device_type_id].revision)]
152+
server_list = [k for k, v in self.xml_device_types[device_type_id].server_clusters.items(
153+
) if conformance_allowed(v.conformance(0, [], []), False)]
154+
resp.attributes = {1: {desc: {device_type_list_attr: device_type_list, server_list_attr: server_list}}}
155+
156+
self.test.endpoints = resp.attributes
157+
158+
# Test with temp sensor with temp sensor, identify and descriptor
159+
def test_ts_minimal_clusters(self):
160+
self.create_test([Clusters.TemperatureMeasurement.id, Clusters.Identify.id, Clusters.Descriptor.id])
161+
success, problems = self.test.check_device_type(fail_on_extra_clusters=True)
162+
if problems:
163+
print(problems)
164+
asserts.assert_true(success, "Failure on Temperature Sensor device type test")
165+
166+
# Temp sensor with temp sensor, identify, descriptor, binding
167+
def test_ts_minimal_with_binding(self):
168+
self.create_test([Clusters.TemperatureMeasurement.id, Clusters.Identify.id, Clusters.Binding.id, Clusters.Descriptor.id])
169+
success, problems = self.test.check_device_type(fail_on_extra_clusters=True)
170+
if problems:
171+
print(problems)
172+
asserts.assert_true(success, "Failure on Temperature Sensor device type test")
173+
asserts.assert_false(problems, "Found problems on Temperature sensor device type test")
174+
175+
# Temp sensor with temp sensor, identify, descriptor, fixed label
176+
def test_ts_minimal_with_label(self):
177+
self.create_test([Clusters.TemperatureMeasurement.id, Clusters.Identify.id, Clusters.FixedLabel.id, Clusters.Descriptor.id])
178+
success, problems = self.test.check_device_type(fail_on_extra_clusters=True)
179+
if problems:
180+
print(problems)
181+
asserts.assert_true(success, "Failure on Temperature Sensor device type test")
182+
asserts.assert_false(problems, "Found problems on Temperature sensor device type test")
183+
184+
# Temp sensor with temp sensor, descriptor
185+
def test_ts_missing_identify(self):
186+
self.create_test([Clusters.TemperatureMeasurement.id, Clusters.Descriptor.id])
187+
success, problems = self.test.check_device_type(fail_on_extra_clusters=True)
188+
if problems:
189+
print(problems)
190+
asserts.assert_equal(len(problems), 1, "Unexpected number of problems")
191+
asserts.assert_false(success, "Unexpected success running test that should fail")
192+
193+
# endpoint 1 empty
194+
def test_endpoint_missing_descriptor(self):
195+
self.create_test([], no_descriptor=True)
196+
success, problems = self.test.check_device_type(fail_on_extra_clusters=True)
197+
if problems:
198+
print(problems)
199+
asserts.assert_equal(len(problems), 1, "Unexpected number of problems")
200+
asserts.assert_false(success, "Unexpected success running test that should fail")
201+
202+
# Temp sensor with temp sensor, descriptor, identify, onoff
203+
def test_ts_extra_cluster(self):
204+
self.create_test([Clusters.TemperatureMeasurement.id, Clusters.Identify.id, Clusters.Descriptor.id, Clusters.OnOff.id])
205+
success, problems = self.test.check_device_type(fail_on_extra_clusters=True)
206+
if problems:
207+
print(problems)
208+
asserts.assert_equal(len(problems), 1, "Unexpected number of problems")
209+
asserts.assert_false(success, "Unexpected success running test that should fail")
210+
211+
success, problems = self.test.check_device_type(fail_on_extra_clusters=False)
212+
asserts.assert_equal(len(problems), 1, "Did not receive expected warning for extra clusters")
213+
asserts.assert_true(success, "Unexpected failure")
214+
215+
def test_bad_device_type_id_device_type_test(self):
216+
self.create_test([], bad_device_id=True)
217+
success, problems = self.test.check_device_type(fail_on_extra_clusters=True)
218+
if problems:
219+
print(problems)
220+
asserts.assert_equal(len(problems), 1, "Unexpected number of problems")
221+
asserts.assert_false(success, "Unexpected success running test that should fail")
222+
223+
def test_all_device_types(self):
224+
for id in self.xml_device_types.keys():
225+
self.create_good_device(id)
226+
success, problems = self.test.check_device_type(fail_on_extra_clusters=True)
227+
if problems:
228+
print(problems)
229+
asserts.assert_false(problems, f"Unexpected problems on device type {id}")
230+
asserts.assert_true(success, f"Unexpected failure on device type {id}")
231+
232+
def test_disallowed_cluster(self):
233+
for id, dt in self.xml_device_types.items():
234+
expected_problems = 0
235+
self.create_good_device(id)
236+
for cluster_id, cluster in dt.server_clusters.items():
237+
if not conformance_allowed(cluster.conformance(0, [], []), False):
238+
self.test.endpoints[1][Clusters.Descriptor][Clusters.Descriptor.Attributes.ServerList].append(cluster_id)
239+
expected_problems += 1
240+
if expected_problems == 0:
241+
continue
242+
success, problems = self.test.check_device_type(fail_on_extra_clusters=True)
243+
if problems:
244+
print(problems)
245+
asserts.assert_equal(len(problems), expected_problems, "Unexpected number of problems")
246+
asserts.assert_false(success, "Unexpected success running test that should fail")
247+
109248

110249
if __name__ == "__main__":
111250
default_matter_test_main()

src/python_testing/conformance_support.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,9 @@ def __call__(self, feature_map: uint, attribute_list: list[uint], all_command_li
331331
for op in self.op_list:
332332
decision_with_choice = op(feature_map, attribute_list, all_command_list)
333333
# and operations can't happen on optional or disallowed
334-
if decision_with_choice.decision in [ConformanceDecision.OPTIONAL, ConformanceDecision.DISALLOWED, ConformanceDecision.PROVISIONAL]:
334+
if decision_with_choice.decision == ConformanceDecision.OPTIONAL and all([type(op) == device_feature for op in self.op_list]):
335+
return decision_with_choice
336+
elif decision_with_choice.decision in [ConformanceDecision.OPTIONAL, ConformanceDecision.DISALLOWED, ConformanceDecision.PROVISIONAL]:
335337
raise ConformanceException('AND operation on optional or disallowed item')
336338
elif decision_with_choice.decision == ConformanceDecision.NOT_APPLICABLE:
337339
return decision_with_choice

scripts/tests/py/BUILD.gn src/python_testing/matter_testing_infrastructure/BUILD.gn

+3-3
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ pw_python_package("metadata_parser") {
2828
inputs = [ "env_test.yaml" ]
2929

3030
sources = [
31-
"__init__.py",
32-
"metadata.py",
31+
"metadata_parser/__init__.py",
32+
"metadata_parser/metadata.py",
3333
]
3434

35-
tests = [ "test_metadata.py" ]
35+
tests = [ "metadata_parser/test_metadata.py" ]
3636
}

0 commit comments

Comments
 (0)