Skip to content

Commit 84c0cf9

Browse files
authored
PICS checker test implementation (project-chip#30970)
* PICS checker test implementation * Address review comments * Address review comments * Remove tests for case-insensitive pics - we don't want this * Fix case-insensitive pics and add test * Fix pics case in test
1 parent 642aa4e commit 84c0cf9

7 files changed

+194
-27
lines changed

src/python_testing/TC_TIMESYNC_2_1.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -164,15 +164,15 @@ async def test_TC_TIMESYNC_2_1(self):
164164
asserts.assert_true(False, "NTPServerAvailable is mandatory if the NTPS (TIMESYNC.S.F02) feature is supported")
165165

166166
self.print_step(12, "Read TimeZoneListMaxSize")
167-
if self.check_pics("TIMESYNC.S.A000A"):
167+
if self.check_pics("TIMESYNC.S.A000a"):
168168
size = await self.read_ts_attribute_expect_success(endpoint=endpoint, attribute=attributes.TimeZoneListMaxSize)
169169
asserts.assert_greater_equal(size, 1, "TimeZoneListMaxSize must be at least 1")
170170
asserts.assert_less_equal(size, 2, "TimeZoneListMaxSize must be max 2")
171171
elif self.check_pics("TIMESYNC.S.F00"):
172172
asserts.assert_true(False, "TimeZoneListMaxSize is mandatory if the TZ (TIMESYNC.S.F00) feature is supported")
173173

174174
self.print_step(13, "Read DSTOffsetListMaxSize")
175-
if self.check_pics("TIMESYNC.S.A000B"):
175+
if self.check_pics("TIMESYNC.S.A000b"):
176176
size = await self.read_ts_attribute_expect_success(endpoint=endpoint, attribute=attributes.DSTOffsetListMaxSize)
177177
asserts.assert_greater_equal(size, 1, "DSTOffsetListMaxSize must be at least 1")
178178
elif self.check_pics("TIMESYNC.S.F00"):

src/python_testing/TC_pics_checker.py

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
#
2+
# Copyright (c) 2023 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 math
18+
19+
import chip.clusters as Clusters
20+
from basic_composition_support import BasicCompositionTests
21+
from global_attribute_ids import GlobalAttributeIds
22+
from matter_testing_support import (AttributePathLocation, ClusterPathLocation, CommandPathLocation, FeaturePathLocation,
23+
MatterBaseTest, async_test_body, default_matter_test_main)
24+
from mobly import asserts
25+
from spec_parsing_support import build_xml_clusters
26+
27+
28+
def attribute_pics(pics_base: str, id: int) -> str:
29+
return f'{pics_base}.S.A{id:04x}'
30+
31+
32+
def accepted_cmd_pics(pics_base: str, id: int) -> str:
33+
return f'{pics_base}.S.C{id:02x}.Rsp'
34+
35+
36+
def generated_cmd_pics(pics_base: str, id: int) -> str:
37+
return f'{pics_base}.S.C{id:02x}.Tx'
38+
39+
40+
def feature_pics(pics_base: str, bit: int) -> str:
41+
return f'{pics_base}.S.F{bit:02x}'
42+
43+
44+
class TC_PICS_Checker(MatterBaseTest, BasicCompositionTests):
45+
@async_test_body
46+
async def setup_class(self):
47+
super().setup_class()
48+
await self.setup_class_helper(False)
49+
# build_xml_cluster returns a list of issues found when paring the XML
50+
# Problems in the XML shouldn't cause test failure, but we want them recorded
51+
# so they are added to the list of problems that get output when the test set completes.
52+
self.xml_clusters, self.problems = build_xml_clusters()
53+
54+
def _check_and_record_errors(self, location, required, pics):
55+
if required and not self.check_pics(pics):
56+
self.record_error("PICS check", location=location,
57+
problem=f"An element found on the device, but the corresponding PICS {pics} was not found in pics list")
58+
self.success = False
59+
elif not required and self.check_pics(pics):
60+
self.record_error("PICS check", location=location, problem=f"PICS {pics} found in PICS list, but not on device")
61+
self.success = False
62+
63+
def _add_pics_for_lists(self, cluster_id: int, attribute_id_of_element_list: GlobalAttributeIds) -> None:
64+
try:
65+
if attribute_id_of_element_list == GlobalAttributeIds.ATTRIBUTE_LIST_ID:
66+
all_spec_elements_to_check = Clusters.ClusterObjects.ALL_ATTRIBUTES[cluster_id]
67+
pics_mapper = attribute_pics
68+
elif attribute_id_of_element_list == GlobalAttributeIds.ACCEPTED_COMMAND_LIST_ID:
69+
all_spec_elements_to_check = Clusters.ClusterObjects.ALL_ACCEPTED_COMMANDS[cluster_id]
70+
pics_mapper = accepted_cmd_pics
71+
72+
elif attribute_id_of_element_list == GlobalAttributeIds.GENERATED_COMMAND_LIST_ID:
73+
all_spec_elements_to_check = Clusters.ClusterObjects.ALL_GENERATED_COMMANDS[cluster_id]
74+
pics_mapper = generated_cmd_pics
75+
else:
76+
asserts.fail("add_pics_for_list function called for non-list attribute")
77+
except KeyError:
78+
# This cluster does not have any of this element type
79+
return
80+
81+
for element_id in all_spec_elements_to_check:
82+
if element_id > 0xF000:
83+
# No pics for global elements
84+
continue
85+
pics = pics_mapper(self.xml_clusters[cluster_id].pics, element_id)
86+
87+
if cluster_id not in self.endpoint.keys():
88+
# This cluster is not on this endpoint
89+
required = False
90+
elif element_id in self.endpoint[cluster_id][attribute_id_of_element_list]:
91+
# Cluster and element are on the endpoint
92+
required = True
93+
else:
94+
# Cluster is on the endpoint but the element is not in the list
95+
required = False
96+
97+
if attribute_id_of_element_list == GlobalAttributeIds.ATTRIBUTE_LIST_ID:
98+
location = AttributePathLocation(endpoint_id=self.endpoint_id, cluster_id=cluster_id, attribute_id=element_id)
99+
else:
100+
location = CommandPathLocation(endpoint_id=self.endpoint_id, cluster_id=cluster_id, command_id=element_id)
101+
102+
self._check_and_record_errors(location, required, pics)
103+
104+
def test_TC_pics_checker(self):
105+
self.endpoint_id = self.matter_test_config.endpoint
106+
self.endpoint = self.endpoints_tlv[self.endpoint_id]
107+
self.success = True
108+
109+
for cluster_id, cluster in Clusters.ClusterObjects.ALL_CLUSTERS.items():
110+
# Data model XML is used to get the PICS code for this cluster. If we don't know the PICS
111+
# code, we can't evaluate the PICS list. Clusters that are present on the device but are
112+
# not present in the spec are checked in the IDM tests.
113+
if cluster_id not in self.xml_clusters or self.xml_clusters[cluster_id].pics is None:
114+
continue
115+
116+
# Ensure the PICS.S code is correctly marked
117+
pics_cluster = f'{self.xml_clusters[cluster_id].pics}.S'
118+
location = ClusterPathLocation(endpoint_id=self.endpoint_id, cluster_id=cluster_id)
119+
self._check_and_record_errors(location, cluster_id in self.endpoint, pics_cluster)
120+
121+
self._add_pics_for_lists(cluster_id, GlobalAttributeIds.ATTRIBUTE_LIST_ID)
122+
self._add_pics_for_lists(cluster_id, GlobalAttributeIds.ACCEPTED_COMMAND_LIST_ID)
123+
self._add_pics_for_lists(cluster_id, GlobalAttributeIds.GENERATED_COMMAND_LIST_ID)
124+
125+
try:
126+
cluster_features = cluster.Bitmaps.Feature
127+
except AttributeError:
128+
# cluster has no features
129+
continue
130+
131+
pics_base = self.xml_clusters[cluster_id].pics
132+
try:
133+
feature_map = self.endpoint[cluster_id][GlobalAttributeIds.FEATURE_MAP_ID]
134+
except KeyError:
135+
feature_map = 0
136+
137+
for feature_mask in cluster_features:
138+
# Codegen in python uses feature masks (0x01, 0x02, 0x04 etc.)
139+
# PICS uses the mask bit number (1, 2, 3)
140+
# Convert the mask to a bit number so we can check the PICS.
141+
feature_bit = int(math.log2(feature_mask))
142+
pics = feature_pics(pics_base, feature_bit)
143+
if feature_mask & feature_map:
144+
required = True
145+
else:
146+
required = False
147+
148+
try:
149+
location = FeaturePathLocation(endpoint_id=self.endpoint_id, cluster_id=cluster_id,
150+
feature_code=self.xml_clusters[cluster_id].features[feature_mask].code)
151+
except KeyError:
152+
location = ClusterPathLocation(endpoint_id=self.endpoint_id, cluster_id=cluster_id)
153+
self._check_and_record_errors(location, required, pics)
154+
155+
if not self.success:
156+
self.fail_current_test("At least one PICS error was found for this endpoint")
157+
158+
159+
if __name__ == "__main__":
160+
default_matter_test_main()

src/python_testing/TestMatterTestingSupport.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ async def test_type_checking(self):
136136
async def test_pics_support(self):
137137
pics_list = ['TEST.S.A0000=1',
138138
'TEST.S.A0001=0',
139-
'lower.s.a0000=1',
139+
'TEST.S.A000a=1'
140140
'',
141141
' ',
142142
'# comment',
@@ -148,10 +148,9 @@ async def test_pics_support(self):
148148

149149
asserts.assert_true(self.check_pics("TEST.S.A0000"), "PICS parsed incorrectly for TEST.S.A0000")
150150
asserts.assert_false(self.check_pics("TEST.S.A0001"), "PICS parsed incorrectly for TEST.S.A0001")
151-
asserts.assert_true(self.check_pics("LOWER.S.A0000"), "PICS pased incorrectly for LOWER.S.A0000")
151+
asserts.assert_true(self.check_pics("TEST.S.A000a"), "PICS parsed incorrectly for TEST.S.A000a")
152152
asserts.assert_true(self.check_pics("SPACE.S.A0000"), "PICS parsed incorrectly for SPACE.S.A0000")
153153
asserts.assert_false(self.check_pics("NOT.S.A0000"), "PICS parsed incorrectly for NOT.S.A0000")
154-
asserts.assert_true(self.check_pics(" test.s.a0000"), "PICS checker lowercase handled incorrectly")
155154

156155
# invalid pics file should throw a value error
157156
pics_list.append("BAD.S.A000=5")

src/python_testing/basic_composition_support.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,11 @@ def ConvertValue(value) -> Any:
9797

9898

9999
class BasicCompositionTests:
100-
async def setup_class_helper(self):
100+
async def setup_class_helper(self, default_to_pase: bool = True):
101101
dev_ctrl = self.default_controller
102102
self.problems = []
103103

104-
do_test_over_pase = self.user_params.get("use_pase_only", True)
104+
do_test_over_pase = self.user_params.get("use_pase_only", default_to_pase)
105105
dump_device_composition_path: Optional[str] = self.user_params.get("dump_device_composition_path", None)
106106

107107
if do_test_over_pase:

src/python_testing/drlk_2_x_common.py

+2
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,10 @@ async def run_drlk_test_common(self, lockUnlockCommand, lockUnlockCmdRspPICS, lo
149149
self.print_step("1", "TH writes the RequirePINforRemoteOperation attribute value as false on the DUT")
150150
attribute = attributes.RequirePINforRemoteOperation(False)
151151
if self.check_pics("DRLK.S.M.RequirePINForRemoteOperationAttributeWritable"):
152+
print("---------------------- PICS is true")
152153
await self.write_drlk_attribute_expect_success(attribute=attribute)
153154
else:
155+
print("---------------------- PICS is false")
154156
await self.write_drlk_attribute_expect_error(attribute=attribute, error=Status.UnsupportedWrite)
155157

156158
if self.check_pics("DRLK.S.A0033"):

src/python_testing/matter_testing_support.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def parse_pics(lines=typing.List[str]) -> dict[str, bool]:
150150
if val not in ["1", "0"]:
151151
raise ValueError('PICS {} must have a value of 0 or 1'.format(key))
152152

153-
pics[key.strip().upper()] = (val == "1")
153+
pics[key.strip()] = (val == "1")
154154
return pics
155155

156156

@@ -725,7 +725,7 @@ def teardown_class(self):
725725

726726
def check_pics(self, pics_key: str) -> bool:
727727
picsd = self.matter_test_config.pics
728-
pics_key = pics_key.strip().upper()
728+
pics_key = pics_key.strip()
729729
return pics_key in picsd and picsd[pics_key]
730730

731731
async def read_single_attribute(

src/python_testing/spec_parsing_support.py

+24-18
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ class XmlCluster:
100100
accepted_commands: dict[uint, XmlCommand]
101101
generated_commands: dict[uint, XmlCommand]
102102
events: dict[uint, XmlEvent]
103+
pics: str
103104

104105

105106
class CommandType(Enum):
@@ -124,6 +125,12 @@ def __init__(self, cluster, cluster_id, name, is_alias):
124125
except (KeyError, StopIteration):
125126
self._derived = None
126127

128+
try:
129+
classification = next(cluster.iter('classification'))
130+
self._pics = classification.attrib['picsCode']
131+
except (KeyError, StopIteration):
132+
self._pics = None
133+
127134
self.feature_elements = self.get_all_feature_elements()
128135
self.attribute_elements = self.get_all_attribute_elements()
129136
self.command_elements = self.get_all_command_elements()
@@ -348,31 +355,29 @@ def create_cluster(self) -> XmlCluster:
348355
attributes=self.parse_attributes(),
349356
accepted_commands=self.parse_commands(CommandType.ACCEPTED),
350357
generated_commands=self.parse_commands(CommandType.GENERATED),
351-
events=self.parse_events())
358+
events=self.parse_events(), pics=self._pics)
352359

353360
def get_problems(self) -> list[ProblemNotice]:
354361
return self._problems
355362

356363

357364
def build_xml_clusters() -> tuple[list[XmlCluster], list[ProblemNotice]]:
358365
# workaround for aliased clusters not appearing in the xml. Remove this once https://github.com/csa-data-model/projects/issues/373 is addressed
359-
conc_clusters = {0x040C: 'Carbon Monoxide Concentration Measurement',
360-
0x040D: 'Carbon Dioxide Concentration Measurement',
361-
0x0413: 'Nitrogen Dioxide Concentration Measurement',
362-
0x0415: 'Ozone Concentration Measurement',
363-
0x042A: 'PM2.5 Concentration Measurement',
364-
0x042B: 'Formaldehyde Concentration Measurement',
365-
0x042C: 'PM1 Concentration Measurement',
366-
0x042D: 'PM10 Concentration Measurement',
367-
0x042E: 'Total Volatile Organic Compounds Concentration Measurement',
368-
0x042F: 'Radon Concentration Measurement'}
366+
conc_clusters = {0x040C: ('Carbon Monoxide Concentration Measurement', 'CMOCONC'),
367+
0x040D: ('Carbon Dioxide Concentration Measurement', 'CDOCONC'),
368+
0x0413: ('Nitrogen Dioxide Concentration Measurement', 'NDOCONC'),
369+
0x0415: ('Ozone Concentration Measurement', 'OZCONC'),
370+
0x042A: ('PM2.5 Concentration Measurement', 'PMICONC'),
371+
0x042B: ('Formaldehyde Concentration Measurement', 'FLDCONC'),
372+
0x042C: ('PM1 Concentration Measurement', 'PMHCONC'),
373+
0x042D: ('PM10 Concentration Measurement', 'PMKCONC'),
374+
0x042E: ('Total Volatile Organic Compounds Concentration Measurement', 'TVOCCONC'),
375+
0x042F: ('Radon Concentration Measurement', 'RNCONC')}
369376
conc_base_name = 'Concentration Measurement Clusters'
370-
resource_clusters = {0x0071: 'HEPA Filter Monitoring',
371-
0x0072: 'Activated Carbon Filter Monitoring'}
377+
resource_clusters = {0x0071: ('HEPA Filter Monitoring', 'HEPAFREMON'),
378+
0x0072: ('Activated Carbon Filter Monitoring', 'ACFREMON')}
372379
resource_base_name = 'Resource Monitoring Clusters'
373-
water_clusters = {0x0405: 'Relative Humidity Measurement',
374-
0x0407: 'Leaf Wetness Measurement',
375-
0x0408: 'Soil Moisture Measurement'}
380+
water_clusters = {0x0405: ('Relative Humidity Measurement', 'RH')}
376381
water_base_name = 'Water Content Measurement Clusters'
377382
aliases = {conc_base_name: conc_clusters, resource_base_name: resource_clusters, water_base_name: water_clusters}
378383

@@ -482,15 +487,16 @@ def remove_problem(location: typing.Union[CommandPathLocation, FeaturePathLocati
482487
new = XmlCluster(revision=c.revision, derived=c.derived, name=c.name,
483488
feature_map=feature_map, attribute_map=attribute_map, command_map=command_map,
484489
features=features, attributes=attributes, accepted_commands=accepted_commands,
485-
generated_commands=generated_commands, events=events)
490+
generated_commands=generated_commands, events=events, pics=c.pics)
486491
clusters[id] = new
487492

488493
for alias_base_name, aliased_clusters in aliases.items():
489-
for id, alias_name in aliased_clusters.items():
494+
for id, (alias_name, pics) in aliased_clusters.items():
490495
base = derived_clusters[alias_base_name]
491496
new = deepcopy(base)
492497
new.derived = alias_base_name
493498
new.name = alias_name
499+
new.pics = pics
494500
clusters[id] = new
495501

496502
# TODO: All these fixups should be removed BEFORE SVE if at all possible

0 commit comments

Comments
 (0)