From 3e6657c43049a9003c81c6e281629ab119693559 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 16 May 2024 09:13:00 +0200 Subject: [PATCH] [Python] Eliminate ZCLReadAttribute/ZCLSend (#33428) * Convert TestLevelControlCluster to asyncio Remove ZCLReadAttribute and ZCLSend API use from the level control test TestLevelControlCluster and convert to asyncio. * Convert TestReadBasicAttributes to asyncio Remove ZCLReadAttribute API use from basic information cluster test and convert to use asyncio. * Use SendCommand directly in send_zcl_command Avoid using ZCLSend API instead use SendCommand directly in the send_zcl_command helper function. * Convert TestFailsafe to use asyncio/SendCommand Remove ZCLSend API usage and call SendCommand directly. Also convert the test to a test using asyncio. * Convert TestOnOffCluster to use asyncio/SendCommand Remove ZCLSend API usage and call SendCommand directly. Also convert the test to a test using asyncio. * Drop TestResult helper class The class is no longer required. Test results are tested directly. * Fix send_zcl_command argument formatting * Catch exception more specifically * Fix TestWriteBasicAttributes for all cases It seems TestWriteBasicAttributes did not correctly write the attributes. The broad exception handling seems to have hidden this issue even. Make sure the attributes with the correct value get written, and check for unexpected and expected IM errors in the per-attribute results specifically. * Fix TestFailsafe by catching correct exception * Drop unused import --- .../python/test/test_scripts/base.py | 189 ++++++++---------- .../commissioning_failure_test.py | 6 +- .../test/test_scripts/commissioning_test.py | 6 +- .../test/test_scripts/failsafe_tests.py | 3 +- .../test/test_scripts/mobile-device-test.py | 30 ++- .../test_scripts/split_commissioning_test.py | 11 +- .../mbed/integration_tests/common/utils.py | 41 ++-- 7 files changed, 134 insertions(+), 152 deletions(-) diff --git a/src/controller/python/test/test_scripts/base.py b/src/controller/python/test/test_scripts/base.py index 9fc9300f4c8116..3f9f76d9101874 100644 --- a/src/controller/python/test/test_scripts/base.py +++ b/src/controller/python/test/test_scripts/base.py @@ -178,29 +178,6 @@ def run(self): TestFail("Timeout", doCrash=True) -class TestResult: - def __init__(self, operationName, result): - self.operationName = operationName - self.result = result - - def assertStatusEqual(self, expected): - if self.result is None: - raise Exception(f"{self.operationName}: no result got") - if self.result.status != expected: - raise Exception( - f"{self.operationName}: expected status {expected}, got {self.result.status}") - return self - - def assertValueEqual(self, expected): - self.assertStatusEqual(0) - if self.result is None: - raise Exception(f"{self.operationName}: no result got") - if self.result.value != expected: - raise Exception( - f"{self.operationName}: expected value {expected}, got {self.result.value}") - return self - - class BaseTestHelper: def __init__(self, nodeid: int, paaTrustStorePath: str, testCommissioner: bool = False, keypair: p256keypair.P256Keypair = None): @@ -368,15 +345,16 @@ def TestOnNetworkCommissioning(self, discriminator: int, setuppin: int, nodeid: def TestUsedTestCommissioner(self): return self.devCtrl.GetTestCommissionerUsed() - def TestFailsafe(self, nodeid: int): + async def TestFailsafe(self, nodeid: int): self.logger.info("Testing arm failsafe") self.logger.info("Setting failsafe on CASE connection") - err, resp = self.devCtrl.ZCLSend("GeneralCommissioning", "ArmFailSafe", nodeid, - 0, 0, dict(expiryLengthSeconds=60, breadcrumb=1), blocking=True) - if err != 0: + try: + resp = await self.devCtrl.SendCommand(nodeid, 0, + Clusters.GeneralCommissioning.Commands.ArmFailSafe(expiryLengthSeconds=60, breadcrumb=1)) + except IM.InteractionModelError as ex: self.logger.error( - "Failed to send arm failsafe command error is {} with im response{}".format(err, resp)) + "Failed to send arm failsafe command error is {}".format(ex.status)) return False if resp.errorCode is not Clusters.GeneralCommissioning.Enums.CommissioningErrorEnum.kOk: @@ -387,17 +365,17 @@ def TestFailsafe(self, nodeid: int): self.logger.info( "Attempting to open basic commissioning window - this should fail since the failsafe is armed") try: - asyncio.run(self.devCtrl.SendCommand( + await self.devCtrl.SendCommand( nodeid, 0, Clusters.AdministratorCommissioning.Commands.OpenBasicCommissioningWindow(180), timedRequestTimeoutMs=10000 - )) + ) # we actually want the exception here because we want to see a failure, so return False here self.logger.error( 'Incorrectly succeeded in opening basic commissioning window') return False - except Exception: + except IM.InteractionModelError: pass # TODO: @@ -413,39 +391,39 @@ def TestFailsafe(self, nodeid: int): self.logger.info( "Attempting to open enhanced commissioning window - this should fail since the failsafe is armed") try: - asyncio.run(self.devCtrl.SendCommand( + await self.devCtrl.SendCommand( nodeid, 0, Clusters.AdministratorCommissioning.Commands.OpenCommissioningWindow( commissioningTimeout=180, PAKEPasscodeVerifier=verifier, discriminator=discriminator, iterations=iterations, - salt=salt), timedRequestTimeoutMs=10000)) + salt=salt), timedRequestTimeoutMs=10000) # we actually want the exception here because we want to see a failure, so return False here self.logger.error( 'Incorrectly succeeded in opening enhanced commissioning window') return False - except Exception: + except IM.InteractionModelError: pass self.logger.info("Disarming failsafe on CASE connection") - err, resp = self.devCtrl.ZCLSend("GeneralCommissioning", "ArmFailSafe", nodeid, - 0, 0, dict(expiryLengthSeconds=0, breadcrumb=1), blocking=True) - if err != 0: + try: + resp = await self.devCtrl.SendCommand(nodeid, 0, + Clusters.GeneralCommissioning.Commands.ArmFailSafe(expiryLengthSeconds=0, breadcrumb=1)) + except IM.InteractionModelError as ex: self.logger.error( - "Failed to send arm failsafe command error is {} with im response{}".format(err, resp)) + "Failed to send arm failsafe command error is {}".format(ex.status)) return False self.logger.info( "Opening Commissioning Window - this should succeed since the failsafe was just disarmed") try: - asyncio.run( - self.devCtrl.SendCommand( - nodeid, - 0, - Clusters.AdministratorCommissioning.Commands.OpenBasicCommissioningWindow(180), - timedRequestTimeoutMs=10000 - )) + await self.devCtrl.SendCommand( + nodeid, + 0, + Clusters.AdministratorCommissioning.Commands.OpenBasicCommissioningWindow(180), + timedRequestTimeoutMs=10000 + ) except Exception: self.logger.error( 'Failed to open commissioning window after disarming failsafe') @@ -453,11 +431,12 @@ def TestFailsafe(self, nodeid: int): self.logger.info( "Attempting to arm failsafe over CASE - this should fail since the commissioning window is open") - err, resp = self.devCtrl.ZCLSend("GeneralCommissioning", "ArmFailSafe", nodeid, - 0, 0, dict(expiryLengthSeconds=60, breadcrumb=1), blocking=True) - if err != 0: + try: + resp = await self.devCtrl.SendCommand(nodeid, 0, + Clusters.GeneralCommissioning.Commands.ArmFailSafe(expiryLengthSeconds=60, breadcrumb=1)) + except IM.InteractionModelError as ex: self.logger.error( - "Failed to send arm failsafe command error is {} with im response{}".format(err, resp)) + "Failed to send arm failsafe command error is {}".format(ex.status)) return False if resp.errorCode is Clusters.GeneralCommissioning.Enums.CommissioningErrorEnum.kBusyWithOtherAdmin: return True @@ -1095,50 +1074,48 @@ def SetNetworkCommissioningParameters(self, dataset: str): self.devCtrl.SetThreadOperationalDataset(bytes.fromhex(dataset)) return True - def TestOnOffCluster(self, nodeid: int, endpoint: int, group: int): + async def TestOnOffCluster(self, nodeid: int, endpoint: int): self.logger.info( "Sending On/Off commands to device {} endpoint {}".format(nodeid, endpoint)) - err, resp = self.devCtrl.ZCLSend("OnOff", "On", nodeid, - endpoint, group, {}, blocking=True) - if err != 0: + + try: + await self.devCtrl.SendCommand(nodeid, endpoint, + Clusters.OnOff.Commands.On()) + except IM.InteractionModelError as ex: self.logger.error( - "failed to send OnOff.On: error is {} with im response{}".format(err, resp)) + "failed to send OnOff.On: error is {}".format(ex.status)) return False - err, resp = self.devCtrl.ZCLSend("OnOff", "Off", nodeid, - endpoint, group, {}, blocking=True) - if err != 0: + + try: + await self.devCtrl.SendCommand(nodeid, endpoint, + Clusters.OnOff.Commands.Off()) + except IM.InteractionModelError as ex: self.logger.error( - "failed to send OnOff.Off: error is {} with im response {}".format(err, resp)) + "failed to send OnOff.Off: error is {}".format(ex.status)) return False return True - def TestLevelControlCluster(self, nodeid: int, endpoint: int, group: int): + async def TestLevelControlCluster(self, nodeid: int, endpoint: int): self.logger.info( f"Sending MoveToLevel command to device {nodeid} endpoint {endpoint}") - try: - commonArgs = dict(transitionTime=0, optionsMask=1, optionsOverride=1) + commonArgs = dict(transitionTime=0, optionsMask=1, optionsOverride=1) + + async def _moveClusterLevel(setLevel): + await self.devCtrl.SendCommand(nodeid, + endpoint, + Clusters.LevelControl.Commands.MoveToLevel(**commonArgs, level=setLevel)) + res = await self.devCtrl.ReadAttribute(nodeid, [(endpoint, Clusters.LevelControl.Attributes.CurrentLevel)]) + readVal = res[endpoint][Clusters.LevelControl][Clusters.LevelControl.Attributes.CurrentLevel] + if readVal != setLevel: + raise Exception(f"Read attribute LevelControl.CurrentLevel: expected value {setLevel}, got {readVal}") + + try: # Move to 1 - self.devCtrl.ZCLSend("LevelControl", "MoveToLevel", nodeid, - endpoint, group, dict(**commonArgs, level=1), blocking=True) - res = self.devCtrl.ZCLReadAttribute(cluster="LevelControl", - attribute="CurrentLevel", - nodeid=nodeid, - endpoint=endpoint, - groupid=group) - TestResult("Read attribute LevelControl.CurrentLevel", - res).assertValueEqual(1) + await _moveClusterLevel(1) # Move to 254 - self.devCtrl.ZCLSend("LevelControl", "MoveToLevel", nodeid, - endpoint, group, dict(**commonArgs, level=254), blocking=True) - res = self.devCtrl.ZCLReadAttribute(cluster="LevelControl", - attribute="CurrentLevel", - nodeid=nodeid, - endpoint=endpoint, - groupid=group) - TestResult("Read attribute LevelControl.CurrentLevel", - res).assertValueEqual(254) + await _moveClusterLevel(254) return True except Exception as ex: @@ -1171,29 +1148,27 @@ def TestResolve(self, nodeid): self.logger.exception("Failed to resolve. {}".format(ex)) return False - def TestReadBasicAttributes(self, nodeid: int, endpoint: int, group: int): + async def TestReadBasicAttributes(self, nodeid: int, endpoint: int): + attrs = Clusters.BasicInformation.Attributes basic_cluster_attrs = { - "VendorName": "TEST_VENDOR", - "VendorID": 0xFFF1, - "ProductName": "TEST_PRODUCT", - "ProductID": 0x8001, - "NodeLabel": "Test", - "Location": "XX", - "HardwareVersion": 0, - "HardwareVersionString": "TEST_VERSION", - "SoftwareVersion": 1, - "SoftwareVersionString": "1.0", + attrs.VendorName: "TEST_VENDOR", + attrs.VendorID: 0xFFF1, + attrs.ProductName: "TEST_PRODUCT", + attrs.ProductID: 0x8001, + attrs.NodeLabel: "Test", + attrs.Location: "XX", + attrs.HardwareVersion: 0, + attrs.HardwareVersionString: "TEST_VERSION", + attrs.SoftwareVersion: 1, + attrs.SoftwareVersionString: "1.0", } failed_zcl = {} for basic_attr, expected_value in basic_cluster_attrs.items(): try: - res = self.devCtrl.ZCLReadAttribute(cluster="BasicInformation", - attribute=basic_attr, - nodeid=nodeid, - endpoint=endpoint, - groupid=group) - TestResult(f"Read attribute {basic_attr}", res).assertValueEqual( - expected_value) + res = await self.devCtrl.ReadAttribute(nodeid, [(endpoint, basic_attr)]) + readVal = res[endpoint][Clusters.BasicInformation][basic_attr] + if readVal != expected_value: + raise Exception(f"Read attribute: expected value {expected_value}, got {readVal}") except Exception as ex: failed_zcl[basic_attr] = str(ex) if failed_zcl: @@ -1217,16 +1192,16 @@ class AttributeWriteRequest: failed_attribute_write = [] for req in requests: try: - try: - await self.devCtrl.WriteAttribute(nodeid, [(endpoint, req.attribute, 0)]) - if req.expected_status != IM.Status.Success: - raise AssertionError( - f"Write attribute {req.attribute.__qualname__} expects failure but got success response") - except Exception as ex: - if req.expected_status != IM.Status.Success: - continue - else: - raise ex + # Errors tested here is in the per-attribute result list (type AttributeStatus) + write_res = await self.devCtrl.WriteAttribute(nodeid, [(endpoint, req.attribute(req.value))]) + status = write_res[0].Status + if req.expected_status != status: + raise AssertionError( + f"Write attribute {req.attribute.__qualname__} expects {req.expected_status} but got {status}") + + # Only execute read tests where write is successful. + if req.expected_status != IM.Status.Success: + continue res = await self.devCtrl.ReadAttribute(nodeid, [(endpoint, req.attribute)]) val = res[endpoint][req.cluster][req.attribute] diff --git a/src/controller/python/test/test_scripts/commissioning_failure_test.py b/src/controller/python/test/test_scripts/commissioning_failure_test.py index eca170601c7f29..d680682d567491 100755 --- a/src/controller/python/test/test_scripts/commissioning_failure_test.py +++ b/src/controller/python/test/test_scripts/commissioning_failure_test.py @@ -19,6 +19,7 @@ # Commissioning test. +import asyncio import os import sys from optparse import OptionParser @@ -121,9 +122,8 @@ def main(): FailIfNot(test.TestCommissionFailure(1, 0), "Failed to commission device") logger.info("Testing on off cluster") - FailIfNot(test.TestOnOffCluster(nodeid=1, - endpoint=LIGHTING_ENDPOINT_ID, - group=GROUP_ID), "Failed to test on off cluster") + FailIfNot(asyncio.run(test.TestOnOffCluster(nodeid=1, + endpoint=LIGHTING_ENDPOINT_ID)), "Failed to test on off cluster") timeoutTicker.stop() diff --git a/src/controller/python/test/test_scripts/commissioning_test.py b/src/controller/python/test/test_scripts/commissioning_test.py index b6adc0f477884d..4a7f15d6c3b085 100755 --- a/src/controller/python/test/test_scripts/commissioning_test.py +++ b/src/controller/python/test/test_scripts/commissioning_test.py @@ -19,6 +19,7 @@ # Commissioning test. +import asyncio import os import sys from optparse import OptionParser @@ -146,9 +147,8 @@ def main(): TestFail("Must provide device address or setup payload to commissioning the device") logger.info("Testing on off cluster") - FailIfNot(test.TestOnOffCluster(nodeid=options.nodeid, - endpoint=LIGHTING_ENDPOINT_ID, - group=GROUP_ID), "Failed to test on off cluster") + FailIfNot(asyncio.run(test.TestOnOffCluster(nodeid=options.nodeid, + endpoint=LIGHTING_ENDPOINT_ID)), "Failed to test on off cluster") FailIfNot(test.TestUsedTestCommissioner(), "Test commissioner check failed") diff --git a/src/controller/python/test/test_scripts/failsafe_tests.py b/src/controller/python/test/test_scripts/failsafe_tests.py index 4b3838430ca213..d1a2034e7359d5 100755 --- a/src/controller/python/test/test_scripts/failsafe_tests.py +++ b/src/controller/python/test/test_scripts/failsafe_tests.py @@ -19,6 +19,7 @@ # Commissioning test. +import asyncio import os import sys from optparse import OptionParser @@ -99,7 +100,7 @@ def main(): nodeid=1), "Failed to finish key exchange") - FailIfNot(test.TestFailsafe(nodeid=1), "Failed failsafe test") + FailIfNot(asyncio.run(test.TestFailsafe(nodeid=1)), "Failed failsafe test") timeoutTicker.stop() diff --git a/src/controller/python/test/test_scripts/mobile-device-test.py b/src/controller/python/test/test_scripts/mobile-device-test.py index 33ae713fe02cb2..8f6f534dcefb96 100755 --- a/src/controller/python/test/test_scripts/mobile-device-test.py +++ b/src/controller/python/test/test_scripts/mobile-device-test.py @@ -102,20 +102,17 @@ def TestDatamodel(test: BaseTestHelper, device_nodeid: int): logger.info("Testing datamodel functions") logger.info("Testing on off cluster") - FailIfNot(test.TestOnOffCluster(nodeid=device_nodeid, - endpoint=LIGHTING_ENDPOINT_ID, - group=GROUP_ID), "Failed to test on off cluster") + FailIfNot(asyncio.run(test.TestOnOffCluster(nodeid=device_nodeid, + endpoint=LIGHTING_ENDPOINT_ID)), "Failed to test on off cluster") logger.info("Testing level control cluster") - FailIfNot(test.TestLevelControlCluster(nodeid=device_nodeid, - endpoint=LIGHTING_ENDPOINT_ID, - group=GROUP_ID), + FailIfNot(asyncio.run(test.TestLevelControlCluster(nodeid=device_nodeid, + endpoint=LIGHTING_ENDPOINT_ID)), "Failed to test level control cluster") logger.info("Testing sending commands to non exist endpoint") - FailIfNot(not test.TestOnOffCluster(nodeid=device_nodeid, - endpoint=233, - group=GROUP_ID), "Failed to test on off cluster on non-exist endpoint") + FailIfNot(not asyncio.run(test.TestOnOffCluster(nodeid=device_nodeid, + endpoint=233)), "Failed to test on off cluster on non-exist endpoint") # Test experimental Python cluster objects API logger.info("Testing cluster objects API") @@ -123,9 +120,8 @@ def TestDatamodel(test: BaseTestHelper, device_nodeid: int): "Failed when testing Python Cluster Object APIs") logger.info("Testing attribute reading") - FailIfNot(test.TestReadBasicAttributes(nodeid=device_nodeid, - endpoint=ENDPOINT_ID, - group=GROUP_ID), + FailIfNot(asyncio.run(test.TestReadBasicAttributes(nodeid=device_nodeid, + endpoint=ENDPOINT_ID)), "Failed to test Read Basic Attributes") logger.info("Testing attribute writing") @@ -134,9 +130,8 @@ def TestDatamodel(test: BaseTestHelper, device_nodeid: int): "Failed to test Write Basic Attributes") logger.info("Testing attribute reading basic again") - FailIfNot(test.TestReadBasicAttributes(nodeid=1, - endpoint=ENDPOINT_ID, - group=GROUP_ID), + FailIfNot(asyncio.run(test.TestReadBasicAttributes(nodeid=1, + endpoint=ENDPOINT_ID)), "Failed to test Read Basic Attributes") logger.info("Testing subscription") @@ -152,9 +147,8 @@ def TestDatamodel(test: BaseTestHelper, device_nodeid: int): "Failed to validated re-subscription") logger.info("Testing on off cluster over resolved connection") - FailIfNot(test.TestOnOffCluster(nodeid=device_nodeid, - endpoint=LIGHTING_ENDPOINT_ID, - group=GROUP_ID), "Failed to test on off cluster") + FailIfNot(asyncio.run(test.TestOnOffCluster(nodeid=device_nodeid, + endpoint=LIGHTING_ENDPOINT_ID)), "Failed to test on off cluster") logger.info("Testing writing/reading fabric sensitive data") asyncio.run(test.TestFabricSensitive(nodeid=device_nodeid)) diff --git a/src/controller/python/test/test_scripts/split_commissioning_test.py b/src/controller/python/test/test_scripts/split_commissioning_test.py index 47fedb3aadee8f..9233d58b90377d 100755 --- a/src/controller/python/test/test_scripts/split_commissioning_test.py +++ b/src/controller/python/test/test_scripts/split_commissioning_test.py @@ -19,6 +19,7 @@ # Commissioning test. +import asyncio import os import sys from optparse import OptionParser @@ -118,14 +119,12 @@ def main(): "Failed to commission device 2") logger.info("Testing on off cluster on device 1") - FailIfNot(test.TestOnOffCluster(nodeid=1, - endpoint=LIGHTING_ENDPOINT_ID, - group=GROUP_ID), "Failed to test on off cluster on device 1") + FailIfNot(asyncio.run(test.TestOnOffCluster(nodeid=1, + endpoint=LIGHTING_ENDPOINT_ID)), "Failed to test on off cluster on device 1") logger.info("Testing on off cluster on device 2") - FailIfNot(test.TestOnOffCluster(nodeid=2, - endpoint=LIGHTING_ENDPOINT_ID, - group=GROUP_ID), "Failed to test on off cluster on device 2") + FailIfNot(asyncio.run(test.TestOnOffCluster(nodeid=2, + endpoint=LIGHTING_ENDPOINT_ID)), "Failed to test on off cluster on device 2") timeoutTicker.stop() diff --git a/src/test_driver/mbed/integration_tests/common/utils.py b/src/test_driver/mbed/integration_tests/common/utils.py index 036b612d7ba18c..2b1db4da30ab8e 100644 --- a/src/test_driver/mbed/integration_tests/common/utils.py +++ b/src/test_driver/mbed/integration_tests/common/utils.py @@ -14,6 +14,7 @@ # limitations under the License. +import asyncio import logging import platform import random @@ -114,21 +115,33 @@ def send_zcl_command(devCtrl, line): if len(args) < 5: raise exceptions.InvalidArgumentCount(5, len(args)) - if args[0] not in all_commands: - raise exceptions.UnknownCluster(args[0]) - command = all_commands.get(args[0]).get(args[1], None) + cluster = args[0] + command = args[1] + if cluster not in all_commands: + raise exceptions.UnknownCluster(cluster) + commandObj = all_commands.get(cluster).get(command, None) # When command takes no arguments, (not command) is True - if command is None: - raise exceptions.UnknownCommand(args[0], args[1]) - err, res = devCtrl.ZCLSend(args[0], args[1], int( - args[2]), int(args[3]), int(args[4]), FormatZCLArguments(args[5:], command), blocking=True) - if err != 0: - log.error("Failed to send ZCL command [{}] {}.".format(err, res)) - elif res is not None: - log.info("Success, received command response:") - log.info(res) - else: - log.info("Success, no command response.") + if commandObj is None: + raise exceptions.UnknownCommand(cluster, command) + + try: + req = commandObj(**FormatZCLArguments(args[5:], commandObj)) + except BaseException: + raise exceptions.UnknownCommand(cluster, command) + + nodeid = int(args[2]) + endpoint = int(args[3]) + try: + res = asyncio.run(devCtrl.SendCommand(nodeid, endpoint, req)) + logging.debug(f"CommandResponse {res}") + if res is not None: + log.info("Success, received command response:") + log.info(res) + else: + log.info("Success, no command response.") + except exceptions.InteractionModelError as ex: + return (int(ex.status), None) + log.error("Failed to send ZCL command [{}] {}.".format(int(ex.status), None)) except exceptions.ChipStackException as ex: log.error("An exception occurred during processing ZCL command:") log.error(str(ex))