Skip to content

Commit ded3be7

Browse files
Device type DM XML parser (#33845)
* Device type DM XML parser * Restyled by autopep8 * linter * Update src/python_testing/TestSpecParsingDeviceType.py * Apply suggestions from code review * address review comments * Restyled by autopep8 * use new directory structure for device types too * python 3.9 compliance --------- Co-authored-by: Restyled.io <commits@restyled.io>
1 parent b88ac27 commit ded3be7

File tree

4 files changed

+322
-62
lines changed

4 files changed

+322
-62
lines changed

.github/workflows/tests.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,7 @@ jobs:
582582
scripts/run_in_python_env.sh out/venv './scripts/tests/TestTimeSyncTrustedTimeSourceRunner.py'
583583
scripts/run_in_python_env.sh out/venv './src/python_testing/test_testing/test_TC_ICDM_2_1.py'
584584
scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/TestIdChecks.py'
585+
scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/TestSpecParsingDeviceType.py'
585586
scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/TestConformanceSupport.py'
586587
scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/test_testing/test_IDM_10_4.py'
587588
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#
2+
# Copyright (c) 2024 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 xml.etree.ElementTree as ElementTree
18+
19+
from jinja2 import Template
20+
from matter_testing_support import MatterBaseTest, default_matter_test_main
21+
from mobly import asserts
22+
from spec_parsing_support import build_xml_device_types, parse_single_device_type
23+
24+
25+
class TestSpecParsingDeviceType(MatterBaseTest):
26+
27+
# This just tests that the current spec can be parsed without failures
28+
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():
32+
print(str(d))
33+
34+
def setup_class(self):
35+
self.device_type_id = 0xBBEF
36+
self.revision = 2
37+
self.classification_class = "simple"
38+
self.classification_scope = "endpoint"
39+
self.clusters = {0x0003: "Identify", 0x0004: "Groups"}
40+
41+
# Conformance support tests the different types of conformance for clusters, so here we just want to ensure that we're correctly parsing the XML into python
42+
# adds the same attributes and features to every cluster. This is fine for testing.
43+
self.template = Template("""<deviceType xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="types types.xsd devicetype devicetype.xsd" id="{{ device_type_id }}" name="Test Device Type" revision="{{ revision }}">
44+
<revisionHistory>
45+
{% for i in range(revision) %}
46+
<revision revision="{{ i }}" summary="Rev"/>
47+
{% endfor %}
48+
</revisionHistory>
49+
<classification {% if classification_class %} class="{{ classification_class }}" {% endif %} {% if classification_scope %} scope="{{ classification_scope }}" {% endif %}/>
50+
<conditions/>
51+
<clusters>
52+
{% for k,v in clusters.items() %}
53+
<cluster id="{{ k }}" name="{{ v }}" side="server">
54+
<mandatoryConform/>
55+
</cluster>
56+
{% endfor %}
57+
</clusters>
58+
</deviceType>""")
59+
60+
def test_device_type_clusters(self):
61+
xml = self.template.render(device_type_id=self.device_type_id, revision=self.revision, classification_class=self.classification_class,
62+
classification_scope=self.classification_scope, clusters=self.clusters)
63+
et = ElementTree.fromstring(xml)
64+
device_type, problems = parse_single_device_type(et)
65+
asserts.assert_equal(len(problems), 0, "Unexpected problems parsing device type conformance")
66+
asserts.assert_equal(len(device_type.keys()), 1, "Unexpected number of device types returned")
67+
asserts.assert_true(self.device_type_id in device_type.keys(), "device type id not found in returned data")
68+
asserts.assert_equal(device_type[self.device_type_id].revision, self.revision, "Unexpected revision")
69+
asserts.assert_equal(len(device_type[self.device_type_id].server_clusters),
70+
len(self.clusters), "Unexpected number of clusters")
71+
for id, name in self.clusters.items():
72+
asserts.assert_equal(device_type[self.device_type_id].server_clusters[id].name, name, "Incorrect cluster name")
73+
asserts.assert_equal(str(device_type[self.device_type_id].server_clusters[id].conformance),
74+
'M', 'Incorrect cluster conformance')
75+
76+
def test_no_clusters(self):
77+
clusters = {}
78+
xml = self.template.render(device_type_id=self.device_type_id, revision=self.revision, classification_class=self.classification_class,
79+
classification_scope=self.classification_scope, clusters=clusters)
80+
et = ElementTree.fromstring(xml)
81+
device_type, problems = parse_single_device_type(et)
82+
asserts.assert_equal(len(problems), 0, "Unexpected problems parsing device type conformance")
83+
asserts.assert_equal(len(device_type.keys()), 1, "Unexpected number of device types returned")
84+
asserts.assert_true(self.device_type_id in device_type.keys(), "device type id not found in returned data")
85+
asserts.assert_equal(device_type[self.device_type_id].revision, self.revision, "Unexpected revision")
86+
asserts.assert_equal(len(device_type[self.device_type_id].server_clusters), len(clusters), "Unexpected number of clusters")
87+
88+
def test_bad_device_type_id(self):
89+
xml = self.template.render(device_type_id="", revision=self.revision, classification_class=self.classification_class,
90+
classification_scope=self.classification_scope, clusters=self.clusters)
91+
et = ElementTree.fromstring(xml)
92+
device_type, problems = parse_single_device_type(et)
93+
asserts.assert_equal(len(problems), 1, "Device with blank ID did not generate a problem notice")
94+
95+
def test_bad_class(self):
96+
xml = self.template.render(device_type_id=self.device_type_id, revision=self.revision, classification_class="",
97+
classification_scope=self.classification_scope, clusters=self.clusters)
98+
et = ElementTree.fromstring(xml)
99+
device_type, problems = parse_single_device_type(et)
100+
asserts.assert_equal(len(problems), 1, "Device with no class did not generate a problem notice")
101+
102+
def test_bad_scope(self):
103+
xml = self.template.render(device_type_id=self.device_type_id, revision=self.revision, classification_class=self.classification_class,
104+
classification_scope="", clusters=self.clusters)
105+
et = ElementTree.fromstring(xml)
106+
device_type, problems = parse_single_device_type(et)
107+
asserts.assert_equal(len(problems), 1, "Device with no scope did not generate a problem notice")
108+
109+
110+
if __name__ == "__main__":
111+
default_matter_test_main()

src/python_testing/matter_testing_support.py

+29-31
Original file line numberDiff line numberDiff line change
@@ -450,14 +450,17 @@ class CustomCommissioningParameters:
450450

451451

452452
@dataclass
453-
class ProblemLocation:
453+
class ClusterPathLocation:
454+
endpoint_id: int
455+
cluster_id: int
456+
454457
def __str__(self):
455-
return "UNKNOWN"
458+
return (f'\n Endpoint: {self.endpoint_id},'
459+
f'\n Cluster: {cluster_id_str(self.cluster_id)}')
456460

457461

458462
@dataclass
459-
class AttributePathLocation(ProblemLocation):
460-
endpoint_id: int
463+
class AttributePathLocation(ClusterPathLocation):
461464
cluster_id: Optional[int] = None
462465
attribute_id: Optional[int] = None
463466

@@ -475,55 +478,50 @@ def as_string(self, mapper: ClusterMapper):
475478
return desc
476479

477480
def __str__(self):
478-
return (f'\n Endpoint: {self.endpoint_id},'
479-
f'\n Cluster: {cluster_id_str(self.cluster_id)},'
480-
f'\n Attribute:{id_str(self.attribute_id)}')
481+
return (f'{super().__str__()}'
482+
f'\n Attribute:{id_str(self.attribute_id)}')
481483

482484

483485
@dataclass
484-
class EventPathLocation(ProblemLocation):
485-
endpoint_id: int
486-
cluster_id: int
486+
class EventPathLocation(ClusterPathLocation):
487487
event_id: int
488488

489489
def __str__(self):
490-
return (f'\n Endpoint: {self.endpoint_id},'
491-
f'\n Cluster: {cluster_id_str(self.cluster_id)},'
492-
f'\n Event: {id_str(self.event_id)}')
490+
return (f'{super().__str__()}'
491+
f'\n Event: {id_str(self.event_id)}')
493492

494493

495494
@dataclass
496-
class CommandPathLocation(ProblemLocation):
497-
endpoint_id: int
498-
cluster_id: int
495+
class CommandPathLocation(ClusterPathLocation):
499496
command_id: int
500497

501498
def __str__(self):
502-
return (f'\n Endpoint: {self.endpoint_id},'
503-
f'\n Cluster: {cluster_id_str(self.cluster_id)},'
504-
f'\n Command: {id_str(self.command_id)}')
499+
return (f'{super().__str__()}'
500+
f'\n Command: {id_str(self.command_id)}')
505501

506502

507503
@dataclass
508-
class ClusterPathLocation(ProblemLocation):
509-
endpoint_id: int
510-
cluster_id: int
504+
class FeaturePathLocation(ClusterPathLocation):
505+
feature_code: str
511506

512507
def __str__(self):
513-
return (f'\n Endpoint: {self.endpoint_id},'
514-
f'\n Cluster: {cluster_id_str(self.cluster_id)}')
508+
return (f'{super().__str__()}'
509+
f'\n Feature: {self.feature_code}')
515510

516511

517512
@dataclass
518-
class FeaturePathLocation(ProblemLocation):
519-
endpoint_id: int
520-
cluster_id: int
521-
feature_code: str
513+
class DeviceTypePathLocation:
514+
device_type_id: int
515+
cluster_id: Optional[int] = None
522516

523517
def __str__(self):
524-
return (f'\n Endpoint: {self.endpoint_id},'
525-
f'\n Cluster: {cluster_id_str(self.cluster_id)},'
526-
f'\n Feature: {self.feature_code}')
518+
msg = f'\n DeviceType: {self.device_type_id}'
519+
if self.cluster_id:
520+
msg += f'\n ClusterID: {self.cluster_id}'
521+
return msg
522+
523+
524+
ProblemLocation = typing.Union[ClusterPathLocation, DeviceTypePathLocation]
527525

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

0 commit comments

Comments
 (0)