Skip to content

Commit 08e268e

Browse files
committed
TC-IDM-10.1: Support write-only attributes
Write only attributes are not returned in the wildcard, but will return the UNSUPPORTED_READ error if we attempt to read them in a concrete path. We can detect their presence by probing for this error via a read.
1 parent 1913dba commit 08e268e

File tree

3 files changed

+62
-11
lines changed

3 files changed

+62
-11
lines changed

src/controller/python/chip/clusters/ClusterObjects.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ def __init_subclass__(cls, *args, **kwargs) -> None:
301301
"""Register a subclass."""
302302
super().__init_subclass__(*args, **kwargs)
303303
try:
304-
if cls.cluster_id not in ALL_ATTRIBUTES:
304+
if cls.standard_attribute and cls.cluster_id not in ALL_ATTRIBUTES:
305305
ALL_ATTRIBUTES[cls.cluster_id] = {}
306306
# register this clusterattribute in the ALL_ATTRIBUTES dict for quick lookups
307307
ALL_ATTRIBUTES[cls.cluster_id][cls.attribute_id] = cls
@@ -345,6 +345,10 @@ def attribute_type(cls) -> ClusterObjectFieldDescriptor:
345345
def must_use_timed_write(cls) -> bool:
346346
return False
347347

348+
@ChipUtility.classproperty
349+
def standard_attribute(cls) -> bool:
350+
return True
351+
348352
@ChipUtility.classproperty
349353
def _cluster_object(cls) -> ClusterObject:
350354
return make_dataclass('InternalClass',

src/python_testing/TC_DeviceBasicComposition.py

+56-9
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@
2323
import chip.clusters.ClusterObjects
2424
import chip.tlv
2525
from basic_composition_support import BasicCompositionTests
26+
from chip import ChipUtility
2627
from chip.clusters.Attribute import ValueDecodeFailure
28+
from chip.clusters.ClusterObjects import ClusterAttributeDescriptor, ClusterObjectFieldDescriptor
29+
from chip.interaction_model import InteractionModelError, Status
30+
from chip.tlv import uint
2731
from global_attribute_ids import GlobalAttributeIds
2832
from matter_testing_support import (AttributePathLocation, ClusterPathLocation, CommandPathLocation, MatterBaseTest,
2933
async_test_body, default_matter_test_main)
@@ -32,6 +36,9 @@
3236
find_tree_roots, get_all_children, get_direct_children_of_root, parts_list_cycles,
3337
separate_endpoint_types)
3438

39+
# The above code is importing various classes from the "ClusterObjects" module and assigning them to
40+
# variables. These classes are likely used for creating and managing clusters of objects in a program.
41+
3542

3643
def check_int_in_range(min_value: int, max_value: int, allow_null: bool = False) -> Callable:
3744
"""Returns a checker for whether `obj` is an int that fits in a range."""
@@ -165,7 +172,43 @@ def test_TC_DT_1_1(self):
165172
if not success:
166173
self.fail_current_test("At least one endpoint was missing the descriptor cluster.")
167174

168-
def test_TC_IDM_10_1(self):
175+
async def _read_non_standard_attribute_check_unsupported_read(self, endpoint_id, cluster_id, attribute_id) -> bool:
176+
@dataclass
177+
class TempAttribute(ClusterAttributeDescriptor):
178+
@ChipUtility.classproperty
179+
def cluster_id(cls) -> int:
180+
return cluster_id
181+
182+
@ChipUtility.classproperty
183+
def attribute_id(cls) -> int:
184+
return attribute_id
185+
186+
@ChipUtility.classproperty
187+
def attribute_type(cls) -> ClusterObjectFieldDescriptor:
188+
return ClusterObjectFieldDescriptor(Type=uint)
189+
190+
@ChipUtility.classproperty
191+
def standard_attribute(cls) -> bool:
192+
return False
193+
194+
value: 'uint' = 0
195+
196+
result = await self.default_controller.Read(nodeid=self.dut_node_id, attributes=[(endpoint_id, TempAttribute)])
197+
try:
198+
attr_ret = result.tlvAttributes[endpoint_id][cluster_id][attribute_id]
199+
except KeyError:
200+
attr_ret = None
201+
202+
print(attr_ret)
203+
204+
error_type_ok = attr_ret is not None and isinstance(
205+
attr_ret, Clusters.Attribute.ValueDecodeFailure) and isinstance(attr_ret.Reason, InteractionModelError)
206+
207+
got_expected_error = error_type_ok and attr_ret.Reason.status == Status.UnsupportedRead
208+
return got_expected_error
209+
210+
@async_test_body
211+
async def test_TC_IDM_10_1(self):
169212
self.print_step(1, "Perform a wildcard read of attributes on all endpoints - already done")
170213

171214
@dataclass
@@ -236,15 +279,19 @@ class RequiredMandatoryAttribute:
236279
logging.debug(
237280
f"Checking presence of claimed supported {attribute_string} on {location.as_cluster_string(self.cluster_mapper)}: {'found' if has_attribute else 'not_found'}")
238281

239-
# Check attribute is actually present.
240282
if not has_attribute:
241-
# TODO: Handle detecting write-only attributes from schema.
242-
if "WriteOnly" in attribute_string:
243-
continue
244-
245-
self.record_error(self.get_test_name(), location=location,
246-
problem=f"Did not find {attribute_string} on {location.as_cluster_string(self.cluster_mapper)} when it was claimed in AttributeList ({attribute_list})", spec_location="AttributeList Attribute")
247-
success = False
283+
# Check if this is a write-only attribute by trying to read it.
284+
# If it's present and write-only it should return an UNSUPPORTED_READ error. All other errors are a failure.
285+
# Because these can be MEI attributes, we need to build the ClusterAttributeDescriptor manually since it's
286+
# not guaranteed to be generated. Since we expect an error back anyway, the type doesn't matter.
287+
288+
write_only_attribute = await self._read_non_standard_attribute_check_unsupported_read(
289+
endpoint_id=endpoint_id, cluster_id=cluster_id, attribute_id=attribute_id)
290+
291+
if not write_only_attribute:
292+
self.record_error(self.get_test_name(), location=location,
293+
problem=f"Did not find {attribute_string} on {location.as_cluster_string(self.cluster_mapper)} when it was claimed in AttributeList ({attribute_list})", spec_location="AttributeList Attribute")
294+
success = False
248295
continue
249296

250297
attribute_value = cluster[attribute_id]

src/python_testing/basic_composition_support.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ async def setup_class_helper(self, default_to_pase: bool = True):
119119
address = f"{commissionable_node.addresses[0]}"
120120
logging.info(f"Found instance {instance_name}, VID={vid}, PID={pid}, Address={address}")
121121

122-
node_id = 1
122+
node_id = self.dut_node_id
123123
dev_ctrl.EstablishPASESessionIP(address, info.passcode, node_id)
124124
else:
125125
asserts.fail("Failed to find the DUT according to command line arguments.")

0 commit comments

Comments
 (0)