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"\n Skipping { 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 ()
0 commit comments