Skip to content

Commit 077d4fc

Browse files
TC-IDM-10.1: Support write-only attributes (#32049)
* 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. * remove random comment * Establish PASE session from code * fix node id * Catch KeyError on missing global This would have already been validated in the last step, so catch and release this error and move on. * Fix global attribute names * Fix attribute string * Restyled by clang-format * Update src/python_testing/TC_DeviceBasicComposition.py * Restyled by autopep8 * Make comment more verbose. --------- Co-authored-by: Restyled.io <commits@restyled.io>
1 parent a71f814 commit 077d4fc

6 files changed

+89
-28
lines changed

src/controller/python/ChipDeviceController-ScriptBinding.cpp

+9
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ PyChipError pychip_DeviceController_EstablishPASESessionIP(chip::Controller::Dev
152152
uint32_t setupPINCode, chip::NodeId nodeid, uint16_t port);
153153
PyChipError pychip_DeviceController_EstablishPASESessionBLE(chip::Controller::DeviceCommissioner * devCtrl, uint32_t setupPINCode,
154154
uint16_t discriminator, chip::NodeId nodeid);
155+
PyChipError pychip_DeviceController_EstablishPASESession(chip::Controller::DeviceCommissioner * devCtrl, const char * setUpCode,
156+
chip::NodeId nodeid);
155157
PyChipError pychip_DeviceController_Commission(chip::Controller::DeviceCommissioner * devCtrl, chip::NodeId nodeid);
156158

157159
PyChipError pychip_DeviceController_DiscoverCommissionableNodesLongDiscriminator(chip::Controller::DeviceCommissioner * devCtrl,
@@ -600,6 +602,13 @@ PyChipError pychip_DeviceController_EstablishPASESessionBLE(chip::Controller::De
600602
return ToPyChipError(devCtrl->EstablishPASEConnection(nodeid, params));
601603
}
602604

605+
PyChipError pychip_DeviceController_EstablishPASESession(chip::Controller::DeviceCommissioner * devCtrl, const char * setUpCode,
606+
chip::NodeId nodeid)
607+
{
608+
sPairingDelegate.SetExpectingPairingComplete(true);
609+
return ToPyChipError(devCtrl->EstablishPASEConnection(nodeid, setUpCode));
610+
}
611+
603612
PyChipError pychip_DeviceController_Commission(chip::Controller::DeviceCommissioner * devCtrl, chip::NodeId nodeid)
604613
{
605614
CommissioningParameters params;

src/controller/python/chip/ChipDeviceCtrl.py

+12
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,15 @@ def EstablishPASESessionIP(self, ipaddr: str, setupPinCode: int, nodeid: int, po
500500
self.devCtrl, ipaddr.encode("utf-8"), setupPinCode, nodeid, port)
501501
)
502502

503+
def EstablishPASESession(self, setUpCode: str, nodeid: int):
504+
self.CheckIsActive()
505+
506+
self.state = DCState.RENDEZVOUS_ONGOING
507+
return self._ChipStack.CallAsync(
508+
lambda: self._dmLib.pychip_DeviceController_EstablishPASESession(
509+
self.devCtrl, setUpCode.encode("utf-8"), nodeid)
510+
)
511+
503512
def GetTestCommissionerUsed(self):
504513
return self._ChipStack.Call(
505514
lambda: self._dmLib.pychip_TestCommissionerUsed()
@@ -1588,6 +1597,9 @@ def _InitLib(self):
15881597
self._dmLib.pychip_DeviceController_EstablishPASESessionBLE.argtypes = [
15891598
c_void_p, c_uint32, c_uint16, c_uint64]
15901599
self._dmLib.pychip_DeviceController_EstablishPASESessionBLE.restype = PyChipError
1600+
self._dmLib.pychip_DeviceController_EstablishPASESession.argtypes = [
1601+
c_void_p, c_char_p, c_uint64]
1602+
self._dmLib.pychip_DeviceController_EstablishPASESession.restype = PyChipError
15911603

15921604
self._dmLib.pychip_DeviceController_DiscoverAllCommissionableNodes.argtypes = [
15931605
c_void_p]

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

+55-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)
@@ -165,7 +169,41 @@ def test_TC_DT_1_1(self):
165169
if not success:
166170
self.fail_current_test("At least one endpoint was missing the descriptor cluster.")
167171

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

171209
@dataclass
@@ -222,6 +260,10 @@ class RequiredMandatoryAttribute:
222260
problem=f"Failed validation of value on {location.as_string(self.cluster_mapper)}: {str(e)}", spec_location="Global Elements")
223261
success = False
224262
continue
263+
except KeyError:
264+
# A KeyError here means the attribute does not exist. This problem was already recorded in step 2,
265+
# but we don't assert until the end of the test, so ignore this and don't re-record the error.
266+
continue
225267

226268
self.print_step(4, "Validate the attribute list exactly matches the set of reported attributes")
227269
if success:
@@ -236,15 +278,19 @@ class RequiredMandatoryAttribute:
236278
logging.debug(
237279
f"Checking presence of claimed supported {attribute_string} on {location.as_cluster_string(self.cluster_mapper)}: {'found' if has_attribute else 'not_found'}")
238280

239-
# Check attribute is actually present.
240281
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
282+
# Check if this is a write-only attribute by trying to read it.
283+
# If it's present and write-only it should return an UNSUPPORTED_READ error. All other errors are a failure.
284+
# Because these can be MEI attributes, we need to build the ClusterAttributeDescriptor manually since it's
285+
# not guaranteed to be generated. Since we expect an error back anyway, the type doesn't matter.
286+
287+
write_only_attribute = await self._read_non_standard_attribute_check_unsupported_read(
288+
endpoint_id=endpoint_id, cluster_id=cluster_id, attribute_id=attribute_id)
289+
290+
if not write_only_attribute:
291+
self.record_error(self.get_test_name(), location=location,
292+
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")
293+
success = False
248294
continue
249295

250296
attribute_value = cluster[attribute_id]

src/python_testing/basic_composition_support.py

+4-18
Original file line numberDiff line numberDiff line change
@@ -105,24 +105,10 @@ async def setup_class_helper(self, default_to_pase: bool = True):
105105
dump_device_composition_path: Optional[str] = self.user_params.get("dump_device_composition_path", None)
106106

107107
if do_test_over_pase:
108-
info = self.get_setup_payload_info()
109-
110-
commissionable_nodes = dev_ctrl.DiscoverCommissionableNodes(
111-
info.filter_type, info.filter_value, stopOnFirst=True, timeoutSecond=15)
112-
logging.info(f"Commissionable nodes: {commissionable_nodes}")
113-
# TODO: Support BLE
114-
if commissionable_nodes is not None and len(commissionable_nodes) > 0:
115-
commissionable_node = commissionable_nodes[0]
116-
instance_name = f"{commissionable_node.instanceName}._matterc._udp.local"
117-
vid = f"{commissionable_node.vendorId}"
118-
pid = f"{commissionable_node.productId}"
119-
address = f"{commissionable_node.addresses[0]}"
120-
logging.info(f"Found instance {instance_name}, VID={vid}, PID={pid}, Address={address}")
121-
122-
node_id = 1
123-
dev_ctrl.EstablishPASESessionIP(address, info.passcode, node_id)
124-
else:
125-
asserts.fail("Failed to find the DUT according to command line arguments.")
108+
setupCode = self.matter_test_config.qr_code_content if self.matter_test_config.qr_code_content is not None else self.matter_test_config.manual_code
109+
asserts.assert_true(setupCode, "Require either --qr-code or --manual-code.")
110+
node_id = self.dut_node_id
111+
dev_ctrl.EstablishPASESession(setupCode, node_id)
126112
else:
127113
# Using the already commissioned node
128114
node_id = self.dut_node_id

src/python_testing/matter_testing_support.py

+4
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
from chip.setup_payload import SetupPayload
6161
from chip.storage import PersistentStorage
6262
from chip.tracing import TracingContext
63+
from global_attribute_ids import GlobalAttributeIds
6364
from mobly import asserts, base_test, signals, utils
6465
from mobly.config_parser import ENV_MOBLY_LOGPATH, TestRunConfig
6566
from mobly.test_runner import TestRunner
@@ -412,6 +413,9 @@ def get_cluster_string(self, cluster_id: int) -> str:
412413
return f"Cluster {name} ({cluster_id}, 0x{cluster_id:04X})"
413414

414415
def get_attribute_string(self, cluster_id: int, attribute_id) -> str:
416+
global_attrs = [item.value for item in GlobalAttributeIds]
417+
if attribute_id in global_attrs:
418+
return f"Attribute {GlobalAttributeIds(attribute_id).to_name()} {attribute_id}, 0x{attribute_id:04X}"
415419
mapping = self._mapping._CLUSTER_ID_DICT.get(cluster_id, None)
416420
if not mapping:
417421
return f"Attribute Unknown ({attribute_id}, 0x{attribute_id:08X})"

0 commit comments

Comments
 (0)