Skip to content

Commit 93114c7

Browse files
Auto-select data model set based on specification version (#37394)
* Auto-select data model set based on specification version * Remove extra includes * Fix PICS test test to add SoftwareVersion with pics * Restyled by isort * linter * Omit TestSpecParsingSelection test from app testing --------- Co-authored-by: Restyled.io <commits@restyled.io>
1 parent 2a15d46 commit 93114c7

15 files changed

+247
-22
lines changed

.github/workflows/tests.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,7 @@ jobs:
537537
scripts/run_in_python_env.sh out/venv 'python3 src/python_testing/TestIdChecks.py'
538538
scripts/run_in_python_env.sh out/venv 'python3 src/python_testing/TestMatterTestingSupport.py'
539539
scripts/run_in_python_env.sh out/venv 'python3 src/python_testing/TestSpecParsingDeviceType.py'
540+
scripts/run_in_python_env.sh out/venv 'python3 src/python_testing/TestSpecParsingSelection.py'
540541
scripts/run_in_python_env.sh out/venv 'python3 src/python_testing/TestSpecParsingSupport.py'
541542
542543
- name: Run Tests

src/python_testing/TC_AccessChecker.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from chip.testing.global_attribute_ids import GlobalAttributeIds
2929
from chip.testing.matter_testing import (AttributePathLocation, ClusterPathLocation, MatterBaseTest, TestStep, async_test_body,
3030
default_matter_test_main)
31-
from chip.testing.spec_parsing import XmlCluster, build_xml_clusters
31+
from chip.testing.spec_parsing import XmlCluster
3232
from chip.tlv import uint
3333

3434

@@ -72,7 +72,8 @@ async def setup_class(self):
7272
self.user_params["use_pase_only"] = False
7373
super().setup_class()
7474
await self.setup_class_helper()
75-
self.xml_clusters, self.problems = build_xml_clusters()
75+
self.build_spec_xmls()
76+
7677
acl_attr = Clusters.AccessControl.Attributes.Acl
7778
self.default_acl = await self.read_single_attribute_check_success(cluster=Clusters.AccessControl, attribute=acl_attr)
7879
self._record_errors()

src/python_testing/TC_DeviceConformance.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,14 @@
4747
device_type_id_type, is_valid_device_type_id)
4848
from chip.testing.matter_testing import (AttributePathLocation, ClusterPathLocation, CommandPathLocation, DeviceTypePathLocation,
4949
MatterBaseTest, ProblemNotice, ProblemSeverity, async_test_body, default_matter_test_main)
50-
from chip.testing.spec_parsing import CommandType, build_xml_clusters, build_xml_device_types
50+
from chip.testing.spec_parsing import CommandType
5151
from chip.tlv import uint
5252

5353

5454
class DeviceConformanceTests(BasicCompositionTests):
5555
async def setup_class_helper(self):
5656
await super().setup_class_helper()
57-
self.xml_clusters, self.problems = build_xml_clusters()
58-
self.xml_device_types, problems = build_xml_device_types()
59-
self.problems.extend(problems)
57+
self.build_spec_xmls()
6058

6159
def _get_device_type_id(self, device_type_name: str) -> int:
6260
id = [id for id, dt in self.xml_device_types.items() if dt.name.lower() == device_type_name.lower()]

src/python_testing/TC_pics_checker.py

+4-7
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
from chip.testing.basic_composition import BasicCompositionTests
2121
from chip.testing.global_attribute_ids import GlobalAttributeIds
2222
from chip.testing.matter_testing import (AttributePathLocation, ClusterPathLocation, CommandPathLocation, FeaturePathLocation,
23-
MatterBaseTest, ProblemLocation, TestStep, async_test_body, default_matter_test_main)
23+
MatterBaseTest, TestStep, UnknownProblemLocation, async_test_body,
24+
default_matter_test_main)
2425
from chip.testing.pics import accepted_cmd_pics_str, attribute_pics_str, feature_pics_str, generated_cmd_pics_str
25-
from chip.testing.spec_parsing import build_xml_clusters
2626
from mobly import asserts
2727

2828

@@ -31,10 +31,7 @@ class TC_PICS_Checker(MatterBaseTest, BasicCompositionTests):
3131
async def setup_class(self):
3232
super().setup_class()
3333
await self.setup_class_helper(False)
34-
# build_xml_cluster returns a list of issues found when paring the XML
35-
# Problems in the XML shouldn't cause test failure, but we want them recorded
36-
# so they are added to the list of problems that get output when the test set completes.
37-
self.xml_clusters, self.problems = build_xml_clusters()
34+
self.build_spec_xmls()
3835

3936
def _check_and_record_errors(self, location, required, pics):
4037
if required and not self.check_pics(pics):
@@ -178,7 +175,7 @@ def test_TC_IDM_10_4(self):
178175

179176
self.step(7)
180177
if self.is_pics_sdk_ci_only:
181-
self.record_error("PICS check", location=ProblemLocation(),
178+
self.record_error("PICS check", location=UnknownProblemLocation(),
182179
problem="PICS PICS_SDK_CI_ONLY found in PICS list. This PICS is disallowed for certification.")
183180
self.success = False
184181

src/python_testing/TestConformanceTest.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from chip.testing.conformance import ConformanceDecision
2323
from chip.testing.global_attribute_ids import GlobalAttributeIds
2424
from chip.testing.matter_testing import MatterBaseTest, async_test_body, default_matter_test_main
25-
from chip.testing.spec_parsing import build_xml_clusters, build_xml_device_types
25+
from chip.testing.spec_parsing import PrebuiltDataModelDirectory, build_xml_clusters, build_xml_device_types
2626
from mobly import asserts
2727
from TC_DeviceConformance import DeviceConformanceTests
2828

@@ -118,8 +118,10 @@ def is_mandatory(conformance):
118118

119119
class TestConformanceSupport(MatterBaseTest, DeviceConformanceTests):
120120
def setup_class(self):
121-
self.xml_clusters, self.problems = build_xml_clusters()
122-
self.xml_device_types, problems = build_xml_device_types()
121+
# Latest fully qualified version
122+
# TODO: It might be good to find a way to run this against each directory.
123+
self.xml_clusters, self.problems = build_xml_clusters(PrebuiltDataModelDirectory.k1_4)
124+
self.xml_device_types, problems = build_xml_device_types(PrebuiltDataModelDirectory.k1_4)
123125
self.problems.extend(problems)
124126

125127
@async_test_body

src/python_testing/TestSpecParsingDeviceType.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ def test_spec_device_parsing(self):
3535
print(str(d))
3636

3737
def setup_class(self):
38-
self.xml_clusters, self.xml_cluster_problems = build_xml_clusters()
39-
self.xml_device_types, self.xml_device_types_problems = build_xml_device_types()
38+
# Latest fully qualified release
39+
self.xml_clusters, self.xml_cluster_problems = build_xml_clusters(PrebuiltDataModelDirectory.k1_4)
40+
self.xml_device_types, self.xml_device_types_problems = build_xml_device_types(PrebuiltDataModelDirectory.k1_4)
4041

4142
self.device_type_id = 0xBBEF
4243
self.revision = 2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
#
2+
# Copyright (c) 2025 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+
import chip.clusters as Clusters
18+
from chip.testing.conformance import ConformanceDecision, ConformanceException
19+
from chip.testing.global_attribute_ids import is_standard_attribute_id
20+
from chip.testing.matter_testing import MatterBaseTest, default_matter_test_main
21+
from chip.testing.spec_parsing import PrebuiltDataModelDirectory, build_xml_clusters, dm_from_spec_version
22+
from chip.tlv import uint
23+
from mobly import asserts, signals
24+
from TC_DeviceConformance import DeviceConformanceTests
25+
26+
27+
class TestSpecParsingSelection(MatterBaseTest, DeviceConformanceTests):
28+
def setup_class(self):
29+
# Overriding the DeviceConformanceTest setup_class so we don't go out to a real device
30+
pass
31+
32+
def test_dm_from_spec_version(self):
33+
asserts.assert_equal(dm_from_spec_version(0x01030000), PrebuiltDataModelDirectory.k1_3,
34+
"Incorrect directory selected for 1.3 with patch 0")
35+
asserts.assert_equal(dm_from_spec_version(0x01030100), PrebuiltDataModelDirectory.k1_3,
36+
"Incorrect directory selected for 1.3 with patch 1")
37+
asserts.assert_equal(dm_from_spec_version(0x01040100), PrebuiltDataModelDirectory.k1_4_1,
38+
"Incorrect directory selected for 1.4.1")
39+
asserts.assert_equal(dm_from_spec_version(0x01040100), PrebuiltDataModelDirectory.k1_4_1,
40+
"Incorrect directory selected for 1.4.1")
41+
asserts.assert_equal(dm_from_spec_version(0x01050000), PrebuiltDataModelDirectory.kMaster,
42+
"Incorrect directory selected for 1.5")
43+
44+
# We don't have data model files for 1.2, so these should error
45+
with asserts.assert_raises(ConformanceException, "Expected assertion was not raised for spec version 1.2"):
46+
dm_from_spec_version(0x01020000)
47+
48+
# Any dot release besides 0 and 1 for 1.4 should error
49+
with asserts.assert_raises(ConformanceException, "Data model incorrectly identified for 1.4.2"):
50+
dm_from_spec_version(0x01040200)
51+
52+
with asserts.assert_raises(ConformanceException, "Data model incorrectly identified for 1.4.FF"):
53+
dm_from_spec_version(0x0104FF00)
54+
55+
# Any dot release besides 0 for 1.5 should error
56+
with asserts.assert_raises(ConformanceException, "Data model incorrectly identified for 1.5.1"):
57+
dm_from_spec_version(0x01050100)
58+
with asserts.assert_raises(ConformanceException, "Data model incorrectly identified for 1.5.FF"):
59+
dm_from_spec_version(0x0105FF00)
60+
61+
# Any value with stuff in reserved should error
62+
with asserts.assert_raises(ConformanceException, "Error not returned for specification revision with non-zero reserved values"):
63+
dm_from_spec_version(0x01030001)
64+
with asserts.assert_raises(ConformanceException, "Error not returned for specification revision with non-zero reserved values"):
65+
dm_from_spec_version(0x01040001)
66+
with asserts.assert_raises(ConformanceException, "Error not returned for specification revision with non-zero reserved values"):
67+
dm_from_spec_version(0x01040101)
68+
with asserts.assert_raises(ConformanceException, "Error not returned for specification revision with non-zero reserved values"):
69+
dm_from_spec_version(0x01050001)
70+
71+
def _create_device(self, spec_version: uint, tc_enabled: bool):
72+
# Build at 1.4.1 so we can have TC info
73+
xml_clusters, _ = build_xml_clusters(PrebuiltDataModelDirectory.k1_4_1)
74+
75+
gc_feature_map = Clusters.GeneralCommissioning.Bitmaps.Feature.kTermsAndConditions if tc_enabled else 0
76+
77+
def create_cluster_globals(cluster, feature_map):
78+
spec_attributes = xml_clusters[cluster.id].attributes
79+
spec_accepted_commands = xml_clusters[cluster.id].accepted_commands
80+
spec_generated_commands = xml_clusters[cluster.id].generated_commands
81+
# Build just the lists - basic composition checks the wildcard against the lists, conformance just uses lists
82+
attributes = [id for id, a in spec_attributes.items() if a.conformance(
83+
feature_map, [], []).decision == ConformanceDecision.MANDATORY]
84+
accepted_commands = [id for id, c in spec_accepted_commands.items() if c.conformance(
85+
feature_map, [], []).decision == ConformanceDecision.MANDATORY]
86+
generated_commands = [id for id, c in spec_generated_commands.items() if c.conformance(
87+
feature_map, [], []).decision == ConformanceDecision.MANDATORY]
88+
attr = cluster.Attributes
89+
90+
resp = {}
91+
non_global_attrs = [a for a in attributes if is_standard_attribute_id(a)]
92+
for attribute_id in non_global_attrs:
93+
# We don't use the values in these tests, set them all to 0. The types are wrong, but it shouldn't matter
94+
resp[Clusters.ClusterObjects.ALL_ATTRIBUTES[cluster.id][attribute_id]] = 0
95+
96+
resp[attr.AttributeList] = attributes
97+
resp[attr.AcceptedCommandList] = accepted_commands
98+
resp[attr.GeneratedCommandList] = generated_commands
99+
resp[attr.FeatureMap] = feature_map
100+
resp[attr.ClusterRevision] = xml_clusters[cluster.id].revision
101+
102+
return resp
103+
104+
def get_tlv(resp):
105+
# This only works because there are no structs in here.
106+
# structs need special handling. Beware.
107+
return {k.attribute_id: v for k, v in resp.items()}
108+
109+
gc_resp = create_cluster_globals(Clusters.GeneralCommissioning, gc_feature_map)
110+
bi_resp = create_cluster_globals(Clusters.BasicInformation, 0)
111+
bi_resp[Clusters.BasicInformation.Attributes.SpecificationVersion] = spec_version
112+
113+
self.endpoints = {0: {Clusters.GeneralCommissioning: gc_resp, Clusters.BasicInformation: bi_resp}}
114+
self.endpoints_tlv = {0: {Clusters.GeneralCommissioning.id: get_tlv(
115+
gc_resp), Clusters.BasicInformation.id: get_tlv(bi_resp)}}
116+
117+
def _run_conformance_against_device(self, spec_version: uint, tc_enabled: bool, expect_success_conformance: bool, expect_success_revisions: bool):
118+
self._create_device(spec_version, tc_enabled)
119+
# build the spec XMLs for the stated version
120+
self.build_spec_xmls()
121+
success, problems = self.check_conformance(ignore_in_progress=False, is_ci=False, allow_provisional=False)
122+
problem_strs = [str(p) for p in problems]
123+
problem_str = "\n".join(problem_strs)
124+
asserts.assert_equal(success, expect_success_conformance,
125+
f"Improper conformance result for spec version {spec_version:08X}, TC: {tc_enabled} problems: {problem_str}")
126+
127+
success, problems = self.check_revisions(ignore_in_progress=False)
128+
asserts.assert_equal(success, expect_success_revisions,
129+
f"Improper revision result for spec version {spec_version:08X}, TC: {tc_enabled} problems: {problems}")
130+
131+
def test_conformance(self):
132+
133+
# 1.4 is OK if TC is off
134+
self._run_conformance_against_device(0x01040000, False, expect_success_conformance=True, expect_success_revisions=True)
135+
# 1.4.1 is OK if TC is off
136+
self._run_conformance_against_device(0x01040100, False, expect_success_conformance=True, expect_success_revisions=True)
137+
# 1.4.1 is OK if TC is on
138+
self._run_conformance_against_device(0x01040100, True, expect_success_conformance=True, expect_success_revisions=True)
139+
# 1.4 is NOT OK if TC is on
140+
self._run_conformance_against_device(0x01040000, True, expect_success_conformance=False, expect_success_revisions=True)
141+
142+
# Check that we get a test failure on a bad spec revision
143+
self._create_device(0xFFFFFFFF, False)
144+
with asserts.assert_raises(signals.TestFailure, "Exception not properly raised for bad spec type"):
145+
self.build_spec_xmls()
146+
147+
148+
if __name__ == "__main__":
149+
default_matter_test_main()

src/python_testing/TestSpecParsingSupport.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,8 @@ def get_access_enum_from_string(access_str: str) -> Clusters.AccessControl.Enums
253253
class TestSpecParsingSupport(MatterBaseTest):
254254
def setup_class(self):
255255
super().setup_class()
256-
self.spec_xml_clusters, self.spec_problems = build_xml_clusters()
256+
# Latest fully certified build
257+
self.spec_xml_clusters, self.spec_problems = build_xml_clusters(PrebuiltDataModelDirectory.k1_4)
257258
self.all_spec_clusters = set([(id, c.name, c.pics) for id, c in self.spec_xml_clusters.items()])
258259

259260
def test_build_xml_override(self):

src/python_testing/matter_testing_infrastructure/chip/testing/basic_composition.py

+22
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import chip.clusters.ClusterObjects
3232
import chip.tlv
3333
from chip.clusters.Attribute import ValueDecodeFailure
34+
from chip.testing.conformance import ConformanceException
35+
from chip.testing.spec_parsing import PrebuiltDataModelDirectory, build_xml_clusters, build_xml_device_types, dm_from_spec_version
3436
from mobly import asserts
3537

3638

@@ -210,3 +212,23 @@ def fail_current_test(self, msg: Optional[str] = None):
210212
asserts.fail(msg=self.problems[-1].problem)
211213
else:
212214
asserts.fail(msg)
215+
216+
def _get_dm(self) -> PrebuiltDataModelDirectory:
217+
try:
218+
spec_version = self.endpoints[0][Clusters.BasicInformation][Clusters.BasicInformation.Attributes.SpecificationVersion]
219+
except KeyError:
220+
asserts.fail(
221+
"Specification Version not found on device - ensure device bas a basic information cluster on EP0 supporting Specification Version")
222+
try:
223+
return dm_from_spec_version(spec_version)
224+
except ConformanceException as e:
225+
asserts.fail(f"Unable to identify specification version: {e}")
226+
227+
def build_spec_xmls(self):
228+
dm = self._get_dm()
229+
logging.info("----------------------------------------------------------------------------------")
230+
logging.info(f"-- Running tests against Specification version {dm.dirname}")
231+
logging.info("----------------------------------------------------------------------------------")
232+
self.xml_clusters, self.problems = build_xml_clusters(dm)
233+
self.xml_device_types, problems = build_xml_device_types(dm)
234+
self.problems.extend(problems)

src/python_testing/matter_testing_infrastructure/chip/testing/matter_testing.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -803,7 +803,12 @@ def __str__(self):
803803
return msg
804804

805805

806-
ProblemLocation = typing.Union[ClusterPathLocation, DeviceTypePathLocation]
806+
class UnknownProblemLocation:
807+
def __str__(self):
808+
return '\n Unknown Locations - see message for more details'
809+
810+
811+
ProblemLocation = typing.Union[ClusterPathLocation, DeviceTypePathLocation, UnknownProblemLocation]
807812

808813
# ProblemSeverity is not using StrEnum, but rather Enum, since StrEnum only
809814
# appeared in 3.11. To make it JSON serializable easily, multiple inheritance

src/python_testing/matter_testing_infrastructure/chip/testing/runner.py

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ def run_test_with_mock_read(self, read_cache: Attribute.AsyncReadTransaction.Rea
7575
self.default_controller.Read = AsyncMock(return_value=read_cache)
7676
# This doesn't need to do anything since we are overriding the read anyway
7777
self.default_controller.FindOrEstablishPASESession = AsyncMock(return_value=None)
78+
self.default_controller.GetConnectedDevice = AsyncMock(return_value=None)
7879
with asyncio.Runner() as runner:
7980
return run_tests_no_exit(self.test_class, self.config, runner.get_loop(),
8081
hooks, self.default_controller, self.stack)

0 commit comments

Comments
 (0)