Skip to content

Commit 033cad4

Browse files
committed
Creating TestSpecParsingNamespace test module:
- Creating with data provided by Cecille in PR here: project-chip#37527 - Updated test.yaml to include TestSpecParsingNamespace.py to run in CI - Updated matter_testing support module to include new NamespacePathLocation function for Namespace file locations - Updated spec_parsing to include new functions build_xml_namespaces and parse_namespace, as well as XmlNamespaces and XmlTags dataclasses
1 parent 75bca34 commit 033cad4

File tree

4 files changed

+479
-3
lines changed

4 files changed

+479
-3
lines changed

.github/workflows/tests.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,7 @@ jobs:
539539
scripts/run_in_python_env.sh out/venv 'python3 src/python_testing/TestIdChecks.py'
540540
scripts/run_in_python_env.sh out/venv 'python3 src/python_testing/TestMatterTestingSupport.py'
541541
scripts/run_in_python_env.sh out/venv 'python3 src/python_testing/TestSpecParsingDeviceType.py'
542+
scripts/run_in_python_env.sh out/venv 'python3 src/python_testing/TestSpecParsingNamespace.py'
542543
scripts/run_in_python_env.sh out/venv 'python3 src/python_testing/TestSpecParsingSelection.py'
543544
scripts/run_in_python_env.sh out/venv 'python3 src/python_testing/TestSpecParsingSupport.py'
544545
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
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+
18+
import xml.etree.ElementTree as ElementTree
19+
from jinja2 import Template
20+
import os
21+
22+
from chip.testing.matter_testing import (MatterBaseTest, default_matter_test_main,
23+
ProblemNotice, ProblemSeverity, NamespacePathLocation)
24+
from chip.testing.spec_parsing import (XmlNamespace, parse_namespace,
25+
build_xml_namespaces)
26+
from mobly import asserts
27+
28+
class TestSpecParsingNamespace(MatterBaseTest):
29+
def setup_class(self):
30+
# Get the data model paths
31+
self.dm_1_3 = os.path.join(os.path.dirname(__file__), "..", "..", "data_model", "1.3")
32+
self.dm_1_4 = os.path.join(os.path.dirname(__file__), "..", "..", "data_model", "1.4")
33+
self.dm_1_4_1 = os.path.join(os.path.dirname(__file__), "..", "..", "data_model", "1.4.1")
34+
self.dm_master = os.path.join(os.path.dirname(__file__), "..", "..", "data_model", "master")
35+
36+
# Test data setup
37+
self.namespace_id = 0x0001
38+
self.namespace_name = "Test Namespace"
39+
self.tags = {
40+
0x0000: "Tag1",
41+
0x0001: "Tag2",
42+
0x0002: "Tag3"
43+
}
44+
45+
# Template for generating test XML
46+
self.template = Template("""<?xml version="1.0"?>
47+
<namespace xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
48+
xsi:schemaLocation="types types.xsd namespace namespace.xsd"
49+
id="{{ namespace_id }}"
50+
name="{{ namespace_name }}">
51+
<tags>
52+
{% for id, name in tags.items() %}
53+
<tag id="{{ "0x%04X" % id }}" name="{{ name }}"/>
54+
{% endfor %}
55+
</tags>
56+
</namespace>""")
57+
58+
def test_namespace_parsing(self):
59+
"""Test basic namespace parsing with valid data"""
60+
xml = self.template.render(
61+
namespace_id=f"0x{self.namespace_id:04X}",
62+
namespace_name=self.namespace_name,
63+
tags=self.tags
64+
)
65+
et = ElementTree.fromstring(xml)
66+
namespace, problems = parse_namespace(et)
67+
68+
asserts.assert_equal(len(problems), 0, "Unexpected problems parsing namespace")
69+
asserts.assert_equal(namespace.id, self.namespace_id, "Incorrect namespace ID")
70+
asserts.assert_equal(namespace.name, self.namespace_name, "Incorrect namespace name")
71+
asserts.assert_equal(len(namespace.tags), len(self.tags), "Incorrect number of tags")
72+
73+
for tag_id, tag_name in self.tags.items():
74+
asserts.assert_true(tag_id in namespace.tags, f"Tag ID 0x{tag_id:04X} not found")
75+
asserts.assert_equal(namespace.tags[tag_id].name, tag_name, f"Incorrect name for tag 0x{tag_id:04X}")
76+
77+
def test_bad_namespace_id(self):
78+
"""Test parsing with invalid namespace ID"""
79+
xml = self.template.render(
80+
namespace_id="",
81+
namespace_name=self.namespace_name,
82+
tags=self.tags
83+
)
84+
et = ElementTree.fromstring(xml)
85+
namespace, problems = parse_namespace(et)
86+
asserts.assert_equal(len(problems), 1, "Namespace with blank ID did not generate a problem notice")
87+
88+
def test_missing_namespace_name(self):
89+
"""Test parsing with missing namespace name"""
90+
xml = self.template.render(
91+
namespace_id=f"0x{self.namespace_id:04X}",
92+
namespace_name="",
93+
tags=self.tags
94+
)
95+
et = ElementTree.fromstring(xml)
96+
namespace, problems = parse_namespace(et)
97+
asserts.assert_equal(len(problems), 1, "Namespace with no name did not generate a problem notice")
98+
99+
def test_no_tags(self):
100+
"""Test parsing with no tags"""
101+
xml = self.template.render(
102+
namespace_id=f"0x{self.namespace_id:04X}",
103+
namespace_name=self.namespace_name,
104+
tags={}
105+
)
106+
et = ElementTree.fromstring(xml)
107+
namespace, problems = parse_namespace(et)
108+
asserts.assert_equal(len(problems), 0, "Unexpected problems parsing empty namespace")
109+
asserts.assert_equal(len(namespace.tags), 0, "Empty namespace should have no tags")
110+
111+
def test_spec_files(self):
112+
"""Test parsing actual spec files from different versions"""
113+
one_three, _ = build_xml_namespaces(self.dm_1_3)
114+
one_four, one_four_problems = build_xml_namespaces(self.dm_1_4)
115+
one_four_one, one_four_one_problems = build_xml_namespaces(self.dm_1_4_1)
116+
tot, tot_problems = build_xml_namespaces(self.dm_master)
117+
118+
asserts.assert_equal(len(one_four_problems), 0, "Problems found when parsing 1.4 spec")
119+
asserts.assert_equal(len(one_four_one_problems), 0, "Problems found when parsing 1.4.1 spec")
120+
121+
# Check version relationships
122+
asserts.assert_greater(len(set(tot.keys()) - set(one_three.keys())),
123+
0, "Master dir does not contain any namespaces not in 1.3")
124+
asserts.assert_greater(len(set(tot.keys()) - set(one_four.keys())),
125+
0, "Master dir does not contain any namespaces not in 1.4")
126+
asserts.assert_greater(len(set(one_four.keys()) - set(one_three.keys())),
127+
0, "1.4 dir does not contain any namespaces not in 1.3")
128+
129+
# Check version consistency
130+
asserts.assert_equal(set(one_four.keys()) - set(one_four_one.keys()),
131+
set(), "There are some 1.4 namespaces that are unexpectedly not included in the 1.4.1 files")
132+
asserts.assert_equal(set(one_four.keys()) - set(tot.keys()),
133+
set(), "There are some 1.4 namespaces that are unexpectedly not included in the TOT files")
134+
135+
def validate_namespace_xml(self, xml_file: str) -> list[ProblemNotice]:
136+
# Validating XML namespace files
137+
problems = []
138+
try:
139+
tree = ElementTree.parse(xml_file)
140+
root = tree.getroot()
141+
142+
# Check for namespace ID and validate format
143+
namespace_id = root.get('id')
144+
if not namespace_id:
145+
problems.append(ProblemNotice(
146+
test_name="Validate Namespace XML",
147+
location=NamespacePathLocation(),
148+
severity=ProblemSeverity.WARNING,
149+
problem=f"Missing namespace ID in {xml_file}"
150+
))
151+
else:
152+
# Validate 16-bit hex format (0xNNNN)
153+
try:
154+
# Remove '0x' prefix if present and try to parse
155+
id_value = int(namespace_id.replace('0x', ''), 16)
156+
if id_value < 0 or id_value > 0xFFFF:
157+
problems.append(ProblemNotice(
158+
test_name="Validate Namespace XML",
159+
location=NamespacePathLocation(),
160+
severity=ProblemSeverity.WARNING,
161+
problem=f"Namespace ID {namespace_id} is not a valid 16-bit value in {xml_file}"
162+
))
163+
164+
# Check format is exactly 0xNNNN where N is a hex digit
165+
if not namespace_id.lower().startswith('0x') or len(namespace_id) != 6:
166+
problems.append(ProblemNotice(
167+
test_name="Validate Namespace XML",
168+
location=NamespacePathLocation(),
169+
severity=ProblemSeverity.WARNING,
170+
problem=f"Namespace ID {namespace_id} does not follow required format '0xNNNN' in {xml_file}"
171+
))
172+
except ValueError:
173+
problems.append(ProblemNotice(
174+
test_name="Validate Namespace XML",
175+
location=NamespacePathLocation(),
176+
severity=ProblemSeverity.WARNING,
177+
problem=f"Invalid hex format for namespace ID {namespace_id} in {xml_file}"
178+
))
179+
180+
# Check for namespace name
181+
namespace_name = root.get('name', '').strip()
182+
if not namespace_name:
183+
problems.append(ProblemNotice(
184+
test_name="Validate Namespace XML",
185+
location=NamespacePathLocation(),
186+
severity=ProblemSeverity.WARNING,
187+
problem=f"Missing or empty namespace name in {xml_file}"
188+
))
189+
190+
# Check tags structure
191+
tags_elem = root.find('tags')
192+
if tags_elem is not None:
193+
for tag in tags_elem.findall('tag'):
194+
# Check tag ID and validate format
195+
tag_id = tag.get('id')
196+
if not tag_id:
197+
problems.append(ProblemNotice(
198+
test_name="Validate Namespace XML",
199+
location=NamespacePathLocation(),
200+
severity=ProblemSeverity.WARNING,
201+
problem=f"Missing tag ID in {xml_file}"
202+
))
203+
else:
204+
# Validate 16-bit hex format for tags (0xNNNN)
205+
try:
206+
# Remove '0x' prefix if present and try to parse
207+
id_value = int(tag_id.replace('0x', ''), 16)
208+
if id_value < 0 or id_value > 0xFFFF:
209+
problems.append(ProblemNotice(
210+
test_name="Validate Namespace XML",
211+
location=NamespacePathLocation(),
212+
severity=ProblemSeverity.WARNING,
213+
problem=f"Tag ID {tag_id} is not a valid 16-bit value in {xml_file}"
214+
))
215+
# Check format is exactly 0xNNNN where N is hex digit
216+
if not tag_id.lower().startswith('0x') or len(tag_id) != 6:
217+
problems.append(ProblemNotice(
218+
test_name="Validate Namespace XML",
219+
location=NamespacePathLocation(),
220+
severity=ProblemSeverity.WARNING,
221+
problem=f"Tag ID {tag_id} does not follow required format '0xNNNN' in {xml_file}"
222+
))
223+
except ValueError:
224+
problems.append(ProblemNotice(
225+
test_name="Validate Namespace XML",
226+
location=NamespacePathLocation(),
227+
severity=ProblemSeverity.WARNING,
228+
problem=f"Invalid hex format for tag ID {tag_id} in {xml_file}"
229+
))
230+
231+
# Check tag name
232+
tag_name = tag.get('name', '').strip()
233+
if not tag_name:
234+
problems.append(ProblemNotice(
235+
test_name="Validate Namespace XML",
236+
location=NamespacePathLocation(),
237+
severity=ProblemSeverity.WARNING,
238+
problem=f"Missing or empty tag name in {xml_file}"
239+
))
240+
241+
except Exception as e:
242+
problems.append(ProblemNotice(
243+
test_name="Validate Namespace XML",
244+
location=NamespacePathLocation(),
245+
severity=ProblemSeverity.WARNING,
246+
problem=f"Failed to parse {xml_file}: {str(e)}"
247+
))
248+
249+
return problems
250+
251+
def test_all_namespace_files(self):
252+
"""Test all namespace XML files in the 1.4 and 1.4.1 data model directories"""
253+
data_model_versions = {
254+
"1.4": self.dm_1_4,
255+
"1.4.1": self.dm_1_4_1,
256+
}
257+
258+
for version, dm_path in data_model_versions.items():
259+
namespace_path = os.path.join(dm_path, "namespaces")
260+
if not os.path.exists(namespace_path):
261+
self.print_step("Issue encountered", f"\nSkipping {version} - namespace directory not found")
262+
continue
263+
264+
for filename in os.listdir(namespace_path):
265+
if not filename.endswith('.xml'):
266+
continue
267+
268+
filepath = os.path.join(namespace_path, filename)
269+
problems = self.validate_namespace_xml(filepath)
270+
271+
if problems:
272+
for problem in problems:
273+
self.print_step("problem", problem)
274+
275+
# Run the same validation we did for generated XML
276+
tree = ElementTree.parse(filepath)
277+
namespace, parse_problems = parse_namespace(tree.getroot())
278+
279+
# Verify namespace has required attributes
280+
asserts.assert_true(hasattr(namespace, 'id'), f"Namespace in {filename} missing ID")
281+
asserts.assert_true(hasattr(namespace, 'name'), f"Namespace in {filename} missing name")
282+
asserts.assert_true(hasattr(namespace, 'tags'), f"Namespace in {filename} missing tags dictionary")
283+
284+
# Verify each tag has required attributes
285+
for tag_id, tag in namespace.tags.items():
286+
asserts.assert_true(hasattr(tag, 'id'), f"Tag in {filename} missing ID")
287+
asserts.assert_true(hasattr(tag, 'name'), f"Tag in {filename} missing name")
288+
289+
if __name__ == "__main__":
290+
default_matter_test_main()

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

+16-2
Original file line numberDiff line numberDiff line change
@@ -896,13 +896,27 @@ def __str__(self):
896896
msg += f'\n ClusterID: {self.cluster_id}'
897897
return msg
898898

899+
@dataclass
900+
class NamespacePathLocation:
901+
"""Location in a namespace definition"""
902+
def __init__(self, namespace_id: Optional[int] = None, tag_id: Optional[int] = None):
903+
self.namespace_id = namespace_id
904+
self.tag_id = tag_id
905+
906+
def __str__(self) -> str:
907+
result = "Namespace"
908+
if self.namespace_id is not None:
909+
result += f" 0x{self.namespace_id:04X}"
910+
if self.tag_id is not None:
911+
result += f" Tag 0x{self.tag_id:04X}"
912+
return result
899913

900914
class UnknownProblemLocation:
901915
def __str__(self):
902916
return '\n Unknown Locations - see message for more details'
903917

904-
905-
ProblemLocation = typing.Union[ClusterPathLocation, DeviceTypePathLocation, UnknownProblemLocation]
918+
ProblemLocation = typing.Union[ClusterPathLocation, DeviceTypePathLocation,
919+
UnknownProblemLocation, NamespacePathLocation]
906920

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

0 commit comments

Comments
 (0)