From 03b39321ec9dc53b3e63061ff16f4b67999c0923 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Thu, 22 Aug 2024 15:45:14 +0200 Subject: [PATCH 01/33] [TC_MCORE_FS_1_3] Fix test script according to test plan update --- src/python_testing/TC_MCORE_FS_1_3.py | 349 ++++++++++++++++++++++---- 1 file changed, 305 insertions(+), 44 deletions(-) diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index e707b500a6fba8..22bd962317ff54 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -15,15 +15,19 @@ # limitations under the License. # -# This test requires a TH_SERVER application that returns UnsupportedAttribute when reading UniqueID from BasicInformation Cluster. Please specify with --string-arg th_server_app_path: +# This test requires a TH_SERVER application that returns UnsupportedAttribute +# when reading UniqueID from BasicInformation Cluster. Please specify the app +# location with --string-arg th_server_app_path: import logging import os import random -import signal import subprocess +import sys import time import uuid +from subprocess import Popen, PIPE +from threading import Event, Thread import chip.clusters as Clusters from chip import ChipDeviceCtrl @@ -32,53 +36,223 @@ from mobly import asserts +class ThreadWithStop(Thread): + + def __init__(self, args: list = [], stdout_cb=None, tag="", **kw): + super().__init__(**kw) + self.tag = f"[{tag}] " if tag else "" + self.start_event = Event() + self.stop_event = Event() + self.stdout_cb = stdout_cb + self.args = args + + def forward_stdout(self, f): + while True: + line = f.readline() + if not line: + break + sys.stdout.write(f"{self.tag}{line}") + if self.stdout_cb is not None: + self.stdout_cb(line) + + def forward_stderr(self, f): + while True: + line = f.readline() + if not line: + break + sys.stderr.write(f"{self.tag}{line}") + + def run(self): + logging.info("RUN: %s", " ".join(self.args)) + self.p = Popen(self.args, errors="ignore", stdin=PIPE, stdout=PIPE, stderr=PIPE) + self.start_event.set() + # Feed stdout and stderr to console and given callback. + t1 = Thread(target=self.forward_stdout, args=[self.p.stdout]) + t1.start() + t2 = Thread(target=self.forward_stderr, args=[self.p.stderr]) + t2.start() + # Wait for the stop event. + self.stop_event.wait() + self.p.terminate() + t1.join() + t2.join() + + def start(self): + super().start() + self.start_event.wait() + + def stop(self): + self.stop_event.set() + self.join() + + +class FabricAdminController: + + ADMIN_APP_PATH = "out/linux-x64-fabric-admin-rpc/fabric-admin" + BRIDGE_APP_PATH = "out/linux-x64-fabric-bridge-rpc/fabric-bridge-app" + + def _process_admin_output(self, line): + if self.wait_for_text_text is not None and self.wait_for_text_text in line: + self.wait_for_text_event.set() + + def wait_for_text(self): + self.wait_for_text_event.wait(timeout=30) + self.wait_for_text_event.clear() + self.wait_for_text_text = None + + def __init__(self, fabricName=None, nodeId=None, vendorId=None, paaTrustStorePath=None, + bridgePort=None, bridgeDiscriminator=None, bridgePasscode=None): + + self.wait_for_text_event = Event() + self.wait_for_text_text = None + + args = [self.BRIDGE_APP_PATH] + args.extend(['--secured-device-port', str(bridgePort)]) + args.extend(["--discriminator", str(bridgeDiscriminator)]) + args.extend(["--passcode", str(bridgePasscode)]) + # Start the bridge app which will connect to admin via RPC. + self.bridge = ThreadWithStop(args, tag="BRIDGE") + self.bridge.start() + + args = [self.ADMIN_APP_PATH, "interactive", "start"] + args.extend(["--commissioner-name", str(fabricName)]) + args.extend(["--commissioner-nodeid", str(nodeId)]) + args.extend(["--commissioner-vendor-id", str(vendorId)]) + # FIXME: Passing custom PAA store breaks something + # if paaTrustStorePath is not None: + # args.extend(["--paa-trust-store-path", str(paaTrustStorePath)]) + + self.admin = ThreadWithStop(args, self._process_admin_output, tag="ADMIN") + self.wait_for_text_text = "Connected to Fabric-Bridge" + self.admin.start() + + # Wait for the bridge to connect to the admin. + self.wait_for_text() + + def CommissionOnNetwork(self, nodeId, setupPinCode=None, filterType=None, filter=None): + + self.wait_for_text_text = f"Commissioning complete for node ID 0x{nodeId:016x}: success" + + self.admin.p.stdin.write(f"pairing onnetwork {nodeId} {setupPinCode}\n") + self.admin.p.stdin.flush() + + # Wait for success message. + self.wait_for_text() + + def stop(self): + self.admin.stop() + self.bridge.stop() + + +class AppServer: + + def __init__(self, app, port=None, discriminator=None, passcode=None): + + args = [app] + args.extend(['--secured-device-port', str(port)]) + args.extend(["--discriminator", str(discriminator)]) + args.extend(["--passcode", str(passcode)]) + self.app = ThreadWithStop(args, tag="APP") + self.app.start() + + def stop(self): + self.app.stop() + + class TC_MCORE_FS_1_3(MatterBaseTest): - @async_test_body - async def setup_class(self): + + def setup_class(self): super().setup_class() - self.device_for_th_eco_nodeid = 1111 - self.device_for_th_eco_kvs = None - self.device_for_th_eco_port = 5543 - self.app_process_for_th_eco = None - self.device_for_dut_eco_nodeid = 1112 - self.device_for_dut_eco_kvs = None - self.device_for_dut_eco_port = 5544 - self.app_process_for_dut_eco = None + # Get the path to the Fabric Admin and Bridge apps from the user + # params or use the default paths. + FabricAdminController.ADMIN_APP_PATH = self.user_params.get( + "th_fabric_admin_app_path", FabricAdminController.ADMIN_APP_PATH) + FabricAdminController.BRIDGE_APP_PATH = self.user_params.get( + "th_fabric_bridge_app_path", FabricAdminController.BRIDGE_APP_PATH) + if not os.path.exists(FabricAdminController.ADMIN_APP_PATH): + asserts.fail("This test requires a TH_FABRIC_ADMIN app. Specify app path with --string-arg th_fabric_admin_app_path:") + if not os.path.exists(FabricAdminController.BRIDGE_APP_PATH): + asserts.fail("This test requires a TH_FABRIC_BRIDGE app. Specify app path with --string-arg th_fabric_bridge_app_path:") + + # Get the path to the TH_SERVER app from the user params. + app = self.user_params.get("th_server_app_path", None) + if not app: + asserts.fail("This test requires a TH_SERVER app. Specify app path with --string-arg th_server_app_path:") + if not os.path.exists(app): + asserts.fail(f"The path {app} does not exist") + + self.fsa_bridge_port = 5543 + self.fsa_bridge_discriminator = random.randint(0, 4095) + self.fsa_bridge_passcode = 20202021 + + self.th_fsa_controller = FabricAdminController( + paaTrustStorePath=self.matter_test_config.paa_trust_store_path, + bridgePort=self.fsa_bridge_port, + bridgeDiscriminator=self.fsa_bridge_discriminator, + bridgePasscode=self.fsa_bridge_passcode, + vendorId=0xFFF1, + fabricName="beta", + nodeId=1) + + self.server_for_dut_port = 5544 + self.server_for_dut_discriminator = random.randint(0, 4095) + self.server_for_dut_passcode = 20202021 + + self.server_for_th_port = 5545 + self.server_for_th_discriminator = random.randint(0, 4095) + self.server_for_th_passcode = 20202021 + + # Start the TH_SERVER_FOR_TH_FSA app. + self.server_for_th = AppServer( + app, + port=self.server_for_th_port, + discriminator=self.server_for_th_discriminator, + passcode=self.server_for_th_passcode) + + # self.device_for_th_eco_nodeid = 1111 + # self.device_for_th_eco_kvs = None + # self.device_for_th_eco_port = 5543 + # self.app_process_for_th_eco = None + + # self.device_for_dut_eco_nodeid = 1112 + # self.device_for_dut_eco_kvs = None + # self.device_for_dut_eco_port = 5544 + # self.app_process_for_dut_eco = None # Create a second controller on a new fabric to communicate to the server - new_certificate_authority = self.certificate_authority_manager.NewCertificateAuthority() - new_fabric_admin = new_certificate_authority.NewFabricAdmin(vendorId=0xFFF1, fabricId=2) - paa_path = str(self.matter_test_config.paa_trust_store_path) - self.TH_server_controller = new_fabric_admin.NewController(nodeId=112233, paaTrustStorePath=paa_path) + # new_certificate_authority = self.certificate_authority_manager.NewCertificateAuthority() + # new_fabric_admin = new_certificate_authority.NewFabricAdmin(vendorId=0xFFF1, fabricId=2) + # paa_path = str(self.matter_test_config.paa_trust_store_path) + # self.TH_server_controller = new_fabric_admin.NewController(nodeId=112233, paaTrustStorePath=paa_path) def teardown_class(self): - if self.app_process_for_dut_eco is not None: - logging.warning("Stopping app with SIGTERM") - self.app_process_for_dut_eco.send_signal(signal.SIGTERM.value) - self.app_process_for_dut_eco.wait() - if self.app_process_for_th_eco is not None: - logging.warning("Stopping app with SIGTERM") - self.app_process_for_th_eco.send_signal(signal.SIGTERM.value) - self.app_process_for_th_eco.wait() - - os.remove(self.device_for_dut_eco_kvs) - if self.device_for_th_eco_kvs is not None: - os.remove(self.device_for_th_eco_kvs) + + self.th_fsa_controller.stop() + self.server_for_th.stop() + + # os.remove(self.device_for_dut_eco_kvs) + # if self.device_for_th_eco_kvs is not None: + # os.remove(self.device_for_th_eco_kvs) super().teardown_class() - async def create_device_and_commission_to_th_fabric(self, kvs, port, node_id_for_th, device_info): - app = self.user_params.get("th_server_app_path", None) - if not app: - asserts.fail('This test requires a TH_SERVER app. Specify app path with --string-arg th_server_app_path:') + def steps_TC_MCORE_FS_1_3(self) -> list[TestStep]: + return [ + TestStep(0, "Commission DUT if not done", is_commissioning=True), + TestStep(1, "DUT_FSA commissions TH_SERVER_FOR_DUT_FSA to DUT_FSA's fabric and generates a UniqueID"), + TestStep(2, "TH instructs TH_FSA to commission TH_SERVER_FOR_TH_FSA to TH_FSA's fabric"), + TestStep(3, "TH instructs TH_FSA to open up commissioning window on it's aggregator"), + TestStep(4, "Follow manufacturer provided instructions to have DUT_FSA commission TH_FSA's aggregator"), + TestStep(5, "Follow manufacturer provided instructions to enable DUT_FSA to synchronize TH_SERVER_FOR_TH_FSA from TH_FSA onto DUT_FSA's fabric. TH to provide endpoint saved from step 2 in user prompt"), + TestStep(6, "DUT_FSA synchronizes TH_SERVER_FOR_TH_FSA onto DUT_FSA's fabric and copies the UniqueID presented by TH_FSA's Bridged Device Basic Information Cluster"), + ] - if not os.path.exists(app): - asserts.fail(f'The path {app} does not exist') + async def create_device_and_commission_to_th_fabric(self, kvs, port, node_id_for_th, device_info): discriminator = random.randint(0, 4095) passcode = 20202021 - cmd = [app] + cmd = [self.app] cmd.extend(['--secured-device-port', str(port)]) cmd.extend(['--discriminator', str(discriminator)]) cmd.extend(['--passcode', str(passcode)]) @@ -94,18 +268,105 @@ async def create_device_and_commission_to_th_fabric(self, kvs, port, node_id_for await self.TH_server_controller.CommissionOnNetwork(nodeId=node_id_for_th, setupPinCode=passcode, filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, filter=discriminator) logging.info("Commissioning device for DUT ecosystem onto TH for managing") - def steps_TC_MCORE_FS_1_3(self) -> list[TestStep]: - steps = [TestStep(1, "DUT_FSA commissions TH_SED_DUT to DUT_FSAs fabric and generates a UniqueID", is_commissioning=True), - TestStep(2, "TH_FSA commissions TH_SED_TH onto TH_FSAs fabric and generates a UniqueID."), - TestStep(3, "Follow manufacturer provided instructions to enable DUT_FSA to synchronize TH_SED_TH onto DUT_FSAs fabric."), - TestStep(4, "DUT_FSA synchronizes TH_SED_TH onto DUT_FSAs fabric and copies the UniqueID presented by TH_FSAs Bridged Device Basic Information Cluster.")] - return steps - @async_test_body async def test_TC_MCORE_FS_1_3(self): self.is_ci = self.check_pics('PICS_SDK_CI_ONLY') - self.print_step(0, "Commissioning DUT to TH, already done") - self.step(1) + + # Commissioning - done + self.step(0) + + th_fsa_bridge_th_node_id = 2 + self.print_step(1, "Commissioning TH_FSA_BRIDGE to TH fabric") + await self.default_controller.CommissionOnNetwork( + nodeId=th_fsa_bridge_th_node_id, + setupPinCode=self.fsa_bridge_passcode, + filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, + filter=self.fsa_bridge_discriminator, + ) + + # Get the list of endpoints on the TH_FSA_BRIDGE before adding the TH_SERVER_FOR_TH_FSA. + th_fsa_bridge_endpoints = set(await self.read_single_attribute_check_success( + cluster=Clusters.Descriptor, + attribute=Clusters.Descriptor.Attributes.PartsList, + node_id=th_fsa_bridge_th_node_id, + endpoint=0, + )) + + th_server_for_th_fsa_th_node_id = 3 + self.print_step(2, "Commissioning TH_SERVER_FOR_TH_FSA to TH fabric") + await self.default_controller.CommissionOnNetwork( + nodeId=th_server_for_th_fsa_th_node_id, + setupPinCode=self.server_for_th_passcode, + filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, + filter=self.server_for_th_discriminator, + ) + + self.print_step(3, "Verify that TH_SERVER_FOR_TH_FSA does not have a UniqueID") + th_server_unique_id = await self.read_single_attribute_check_success( + cluster=Clusters.BasicInformation, + attribute=Clusters.BasicInformation.Attributes.UniqueID, + node_id=th_server_for_th_fsa_th_node_id) + # TODO: Use app without UniqueID + print("TH_SERVER UniqueID", th_server_unique_id) + # asserts.assert_true(type_matches(th_server_unique_id, str), "UniqueID should be a string") + # asserts.assert_true(th_server_unique_id, "UniqueID should not be an empty string") + + # TODO: Figure out why this is needed (sometimes) + # time.sleep(1) + + discriminator = random.randint(0, 4095) + self.print_step(4, "Open commissioning window on TH_SERVER_FOR_TH_FSA") + params = await self.default_controller.OpenCommissioningWindow( + nodeid=th_server_for_th_fsa_th_node_id, + timeout=600, + iteration=10000, + discriminator=discriminator, + option=1) + + # FIXME: Sometimes the pincode reported by APP and returned here is different... + print("PINCODEEEEEEEEEEEE", params.setupPinCode) + + time.sleep(1) + + th_server_for_th_fsa_th_fsa_node_id = 3 + self.print_step(5, "Commissioning TH_SERVER_FOR_TH_FSA to TH_FSA") + self.th_fsa_controller.CommissionOnNetwork( + nodeId=th_server_for_th_fsa_th_fsa_node_id, + setupPinCode=params.setupPinCode, + filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, + filter=discriminator, + ) + + time.sleep(3) + + # Get the list of endpoints on the TH_FSA_BRIDGE after adding the TH_SERVER_FOR_TH_FSA. + th_fsa_bridge_endpoints_new = set(await self.read_single_attribute_check_success( + cluster=Clusters.Descriptor, + attribute=Clusters.Descriptor.Attributes.PartsList, + node_id=th_fsa_bridge_th_node_id, + endpoint=0, + )) + + # Get the endpoint number for just added TH_SERVER_FOR_TH_FSA. + logging.info("Endpoints on TH_FSA_BRIDGE: old=%s, new=%s", th_fsa_bridge_endpoints, th_fsa_bridge_endpoints_new) + asserts.assert_true(th_fsa_bridge_endpoints_new.issuperset(th_fsa_bridge_endpoints), + "Expected only new endpoints to be added") + unique_endpoints_set = th_fsa_bridge_endpoints_new - th_fsa_bridge_endpoints + asserts.assert_equal(len(unique_endpoints_set), 1, "Expected only one new endpoint") + th_fsa_bridge_th_server_endpoint = list(unique_endpoints_set)[0] + + self.print_step(6, "Verify that TH_FSA created a UniqueID for TH_SERVER_FOR_TH_FSA") + th_fsa_bridge_th_server_unique_id = await self.read_single_attribute_check_success( + cluster=Clusters.BridgedDeviceBasicInformation, + attribute=Clusters.BridgedDeviceBasicInformation.Attributes.UniqueID, + node_id=th_fsa_bridge_th_node_id, + endpoint=th_fsa_bridge_th_server_endpoint) + asserts.assert_true(type_matches(th_fsa_bridge_th_server_unique_id, str), "UniqueID should be a string") + asserts.assert_true(th_fsa_bridge_th_server_unique_id, "UniqueID should not be an empty string") + logging.info("TH_SERVER_FOR_TH_FSA on TH_SERVER_BRIDGE UniqueID: %s", th_fsa_bridge_th_server_unique_id) + + return + # These steps are not explicitly in step 1, but they help identify the dynamically added endpoint in step 1. root_node_endpoint = 0 root_part_list = await self.read_single_attribute_check_success(cluster=Clusters.Descriptor, attribute=Clusters.Descriptor.Attributes.PartsList, endpoint=root_node_endpoint) From b6dc236ec6b0f754aa4dfbff0fc4428236c13826 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Fri, 23 Aug 2024 10:47:48 +0200 Subject: [PATCH 02/33] Separate storage for all used components --- src/python_testing/TC_MCORE_FS_1_3.py | 105 ++++++++------------------ 1 file changed, 33 insertions(+), 72 deletions(-) diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index 22bd962317ff54..8b736a24448ea3 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -24,10 +24,10 @@ import random import subprocess import sys +import tempfile +import threading import time import uuid -from subprocess import Popen, PIPE -from threading import Event, Thread import chip.clusters as Clusters from chip import ChipDeviceCtrl @@ -36,13 +36,13 @@ from mobly import asserts -class ThreadWithStop(Thread): +class ThreadWithStop(threading.Thread): def __init__(self, args: list = [], stdout_cb=None, tag="", **kw): super().__init__(**kw) self.tag = f"[{tag}] " if tag else "" - self.start_event = Event() - self.stop_event = Event() + self.start_event = threading.Event() + self.stop_event = threading.Event() self.stdout_cb = stdout_cb self.args = args @@ -64,12 +64,13 @@ def forward_stderr(self, f): def run(self): logging.info("RUN: %s", " ".join(self.args)) - self.p = Popen(self.args, errors="ignore", stdin=PIPE, stdout=PIPE, stderr=PIPE) + self.p = subprocess.Popen(self.args, errors="ignore", stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.start_event.set() # Feed stdout and stderr to console and given callback. - t1 = Thread(target=self.forward_stdout, args=[self.p.stdout]) + t1 = threading.Thread(target=self.forward_stdout, args=[self.p.stdout]) t1.start() - t2 = Thread(target=self.forward_stderr, args=[self.p.stderr]) + t2 = threading.Thread(target=self.forward_stderr, args=[self.p.stderr]) t2.start() # Wait for the stop event. self.stop_event.wait() @@ -95,18 +96,20 @@ def _process_admin_output(self, line): if self.wait_for_text_text is not None and self.wait_for_text_text in line: self.wait_for_text_event.set() - def wait_for_text(self): - self.wait_for_text_event.wait(timeout=30) + def wait_for_text(self, timeout=30): + if not self.wait_for_text_event.wait(timeout=timeout): + raise Exception(f"Timeout waiting for text: {self.wait_for_text_text}") self.wait_for_text_event.clear() self.wait_for_text_text = None - def __init__(self, fabricName=None, nodeId=None, vendorId=None, paaTrustStorePath=None, + def __init__(self, storageDir, fabricName=None, nodeId=None, vendorId=None, paaTrustStorePath=None, bridgePort=None, bridgeDiscriminator=None, bridgePasscode=None): - self.wait_for_text_event = Event() + self.wait_for_text_event = threading.Event() self.wait_for_text_text = None args = [self.BRIDGE_APP_PATH] + args.extend(["--KVS", tempfile.mkstemp(dir=storageDir, prefix="kvs-bridge-")[1]]) args.extend(['--secured-device-port', str(bridgePort)]) args.extend(["--discriminator", str(bridgeDiscriminator)]) args.extend(["--passcode", str(bridgePasscode)]) @@ -115,6 +118,7 @@ def __init__(self, fabricName=None, nodeId=None, vendorId=None, paaTrustStorePat self.bridge.start() args = [self.ADMIN_APP_PATH, "interactive", "start"] + args.extend(["--storage-directory", storageDir]) args.extend(["--commissioner-name", str(fabricName)]) args.extend(["--commissioner-nodeid", str(nodeId)]) args.extend(["--commissioner-vendor-id", str(vendorId)]) @@ -130,12 +134,10 @@ def __init__(self, fabricName=None, nodeId=None, vendorId=None, paaTrustStorePat self.wait_for_text() def CommissionOnNetwork(self, nodeId, setupPinCode=None, filterType=None, filter=None): - self.wait_for_text_text = f"Commissioning complete for node ID 0x{nodeId:016x}: success" - + # Send the commissioning command to the admin. self.admin.p.stdin.write(f"pairing onnetwork {nodeId} {setupPinCode}\n") self.admin.p.stdin.flush() - # Wait for success message. self.wait_for_text() @@ -146,9 +148,10 @@ def stop(self): class AppServer: - def __init__(self, app, port=None, discriminator=None, passcode=None): + def __init__(self, app, storageDir, port=None, discriminator=None, passcode=None): args = [app] + args.extend(["--KVS", tempfile.mkstemp(dir=storageDir, prefix="kvs-app-")[1]]) args.extend(['--secured-device-port', str(port)]) args.extend(["--discriminator", str(discriminator)]) args.extend(["--passcode", str(passcode)]) @@ -182,11 +185,16 @@ def setup_class(self): if not os.path.exists(app): asserts.fail(f"The path {app} does not exist") + # Create a temporary storage directory for keeping KVS files. + self.storage = tempfile.TemporaryDirectory(prefix=self.__class__.__name__) + logging.info("Temporary storage directory: %s", self.storage.name) + self.fsa_bridge_port = 5543 self.fsa_bridge_discriminator = random.randint(0, 4095) self.fsa_bridge_passcode = 20202021 self.th_fsa_controller = FabricAdminController( + storageDir=self.storage.name, paaTrustStorePath=self.matter_test_config.paa_trust_store_path, bridgePort=self.fsa_bridge_port, bridgeDiscriminator=self.fsa_bridge_discriminator, @@ -206,34 +214,15 @@ def setup_class(self): # Start the TH_SERVER_FOR_TH_FSA app. self.server_for_th = AppServer( app, + storageDir=self.storage.name, port=self.server_for_th_port, discriminator=self.server_for_th_discriminator, passcode=self.server_for_th_passcode) - # self.device_for_th_eco_nodeid = 1111 - # self.device_for_th_eco_kvs = None - # self.device_for_th_eco_port = 5543 - # self.app_process_for_th_eco = None - - # self.device_for_dut_eco_nodeid = 1112 - # self.device_for_dut_eco_kvs = None - # self.device_for_dut_eco_port = 5544 - # self.app_process_for_dut_eco = None - - # Create a second controller on a new fabric to communicate to the server - # new_certificate_authority = self.certificate_authority_manager.NewCertificateAuthority() - # new_fabric_admin = new_certificate_authority.NewFabricAdmin(vendorId=0xFFF1, fabricId=2) - # paa_path = str(self.matter_test_config.paa_trust_store_path) - # self.TH_server_controller = new_fabric_admin.NewController(nodeId=112233, paaTrustStorePath=paa_path) - def teardown_class(self): - self.th_fsa_controller.stop() self.server_for_th.stop() - - # os.remove(self.device_for_dut_eco_kvs) - # if self.device_for_th_eco_kvs is not None: - # os.remove(self.device_for_th_eco_kvs) + self.storage.cleanup() super().teardown_class() def steps_TC_MCORE_FS_1_3(self) -> list[TestStep]: @@ -247,27 +236,6 @@ def steps_TC_MCORE_FS_1_3(self) -> list[TestStep]: TestStep(6, "DUT_FSA synchronizes TH_SERVER_FOR_TH_FSA onto DUT_FSA's fabric and copies the UniqueID presented by TH_FSA's Bridged Device Basic Information Cluster"), ] - async def create_device_and_commission_to_th_fabric(self, kvs, port, node_id_for_th, device_info): - - discriminator = random.randint(0, 4095) - passcode = 20202021 - - cmd = [self.app] - cmd.extend(['--secured-device-port', str(port)]) - cmd.extend(['--discriminator', str(discriminator)]) - cmd.extend(['--passcode', str(passcode)]) - cmd.extend(['--KVS', kvs]) - - # TODO: Determine if we want these logs cooked or pushed to somewhere else - logging.info(f"Starting TH device for {device_info}") - self.app_process_for_dut_eco = subprocess.Popen(cmd) - logging.info(f"Started TH device for {device_info}") - time.sleep(3) - - logging.info("Commissioning from separate fabric") - await self.TH_server_controller.CommissionOnNetwork(nodeId=node_id_for_th, setupPinCode=passcode, filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, filter=discriminator) - logging.info("Commissioning device for DUT ecosystem onto TH for managing") - @async_test_body async def test_TC_MCORE_FS_1_3(self): self.is_ci = self.check_pics('PICS_SDK_CI_ONLY') @@ -302,17 +270,11 @@ async def test_TC_MCORE_FS_1_3(self): ) self.print_step(3, "Verify that TH_SERVER_FOR_TH_FSA does not have a UniqueID") - th_server_unique_id = await self.read_single_attribute_check_success( + await self.read_single_attribute_expect_error( cluster=Clusters.BasicInformation, attribute=Clusters.BasicInformation.Attributes.UniqueID, - node_id=th_server_for_th_fsa_th_node_id) - # TODO: Use app without UniqueID - print("TH_SERVER UniqueID", th_server_unique_id) - # asserts.assert_true(type_matches(th_server_unique_id, str), "UniqueID should be a string") - # asserts.assert_true(th_server_unique_id, "UniqueID should not be an empty string") - - # TODO: Figure out why this is needed (sometimes) - # time.sleep(1) + node_id=th_server_for_th_fsa_th_node_id, + error=Status.UnsupportedAttribute) discriminator = random.randint(0, 4095) self.print_step(4, "Open commissioning window on TH_SERVER_FOR_TH_FSA") @@ -323,9 +285,10 @@ async def test_TC_MCORE_FS_1_3(self): discriminator=discriminator, option=1) - # FIXME: Sometimes the pincode reported by APP and returned here is different... - print("PINCODEEEEEEEEEEEE", params.setupPinCode) - + # FIXME: Sometimes the commissioning does not work with the error: + # > Failed to verify peer's MAC. This can happen when setup code is incorrect. + # However, the setup code is correct... so we need to investigate why this is happening. + # The sleep(1) seems to help, though. time.sleep(1) th_server_for_th_fsa_th_fsa_node_id = 3 @@ -337,8 +300,6 @@ async def test_TC_MCORE_FS_1_3(self): filter=discriminator, ) - time.sleep(3) - # Get the list of endpoints on the TH_FSA_BRIDGE after adding the TH_SERVER_FOR_TH_FSA. th_fsa_bridge_endpoints_new = set(await self.read_single_attribute_check_success( cluster=Clusters.Descriptor, From cc5d201f4882f5e697925c517e19ebf9082e88bd Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Fri, 23 Aug 2024 11:35:16 +0200 Subject: [PATCH 03/33] Open commissioning window on TH_FSA_BRIDGE --- src/python_testing/TC_MCORE_FS_1_3.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index 8b736a24448ea3..923f4174050648 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -19,6 +19,7 @@ # when reading UniqueID from BasicInformation Cluster. Please specify the app # location with --string-arg th_server_app_path: +import asyncio import logging import os import random @@ -26,7 +27,6 @@ import sys import tempfile import threading -import time import uuid import chip.clusters as Clusters @@ -288,8 +288,8 @@ async def test_TC_MCORE_FS_1_3(self): # FIXME: Sometimes the commissioning does not work with the error: # > Failed to verify peer's MAC. This can happen when setup code is incorrect. # However, the setup code is correct... so we need to investigate why this is happening. - # The sleep(1) seems to help, though. - time.sleep(1) + # The sleep(2) seems to help, though. + await asyncio.sleep(2) th_server_for_th_fsa_th_fsa_node_id = 3 self.print_step(5, "Commissioning TH_SERVER_FOR_TH_FSA to TH_FSA") @@ -300,6 +300,9 @@ async def test_TC_MCORE_FS_1_3(self): filter=discriminator, ) + # Wait some time, so the dynamic endpoint will appear on the TH_FSA_BRIDGE. + await asyncio.sleep(2) + # Get the list of endpoints on the TH_FSA_BRIDGE after adding the TH_SERVER_FOR_TH_FSA. th_fsa_bridge_endpoints_new = set(await self.read_single_attribute_check_success( cluster=Clusters.Descriptor, @@ -326,6 +329,17 @@ async def test_TC_MCORE_FS_1_3(self): asserts.assert_true(th_fsa_bridge_th_server_unique_id, "UniqueID should not be an empty string") logging.info("TH_SERVER_FOR_TH_FSA on TH_SERVER_BRIDGE UniqueID: %s", th_fsa_bridge_th_server_unique_id) + discriminator = random.randint(0, 4095) + self.print_step(7, "Open commissioning window on TH_FSA_BRIDGE") + params = await self.default_controller.OpenCommissioningWindow( + nodeid=th_fsa_bridge_th_node_id, + timeout=600, + iteration=10000, + discriminator=discriminator, + option=1) + + await asyncio.sleep(10000) + return # These steps are not explicitly in step 1, but they help identify the dynamically added endpoint in step 1. From 8600050bdb5001323d4f369f4111a075cecb0715 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Fri, 23 Aug 2024 14:51:26 +0200 Subject: [PATCH 04/33] Python wrapper for running fabric-admin and fabric-bridge together --- .../fabric-admin/scripts/fabric-sync-app.py | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100755 examples/fabric-admin/scripts/fabric-sync-app.py diff --git a/examples/fabric-admin/scripts/fabric-sync-app.py b/examples/fabric-admin/scripts/fabric-sync-app.py new file mode 100755 index 00000000000000..0c6151ae5adf2e --- /dev/null +++ b/examples/fabric-admin/scripts/fabric-sync-app.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 + +import contextlib +import sys +import os +import asyncio +from argparse import ArgumentParser + + +async def forwarder(f_in, f_out, prefix: str): + """Forward f_in to f_out with a prefix attached.""" + while True: + line = await f_in.readline() + if not line: + break + f_out.buffer.write(prefix.encode()) + f_out.buffer.write(line) + + +async def run(tag, program, *args, stdin=None): + p = await asyncio.create_subprocess_exec(program, *args, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + stdin=stdin) + # Add the stdout and stderr processing to the event loop. + asyncio.create_task(forwarder(p.stderr, sys.stderr, tag)) + asyncio.create_task(forwarder(p.stdout, sys.stdout, tag)) + return p + + +async def run_admin(program, storage_dir=None, paa_trust_store_path=None, + commissioner_name=None, commissioner_node_id=None, + commissioner_vendor_id=None): + args = [] + if storage_dir is not None: + args.extend(["--storage-directory", storage_dir]) + if paa_trust_store_path is not None: + args.extend(["--paa-trust-store-path", paa_trust_store_path]) + if commissioner_name is not None: + args.extend(["--commissioner-name", commissioner_name]) + if commissioner_node_id is not None: + args.extend(["--commissioner-nodeid", commissioner_node_id]) + if commissioner_vendor_id is not None: + args.extend(["--commissioner-vendor-id", commissioner_vendor_id]) + p = await run("[ADMIN]", program, "interactive", "start", *args) + await p.wait() + + +async def run_bridge(program, storage_dir=None, discriminator=None, + passcode=None, secured_device_port=None): + args = [] + if storage_dir is not None: + args.extend(["--KVS", + os.path.join(storage_dir, "chip_fabric_bridge_kvs")]) + if discriminator is not None: + args.extend(["--discriminator", discriminator]) + if passcode is not None: + args.extend(["--passcode", passcode]) + if secured_device_port is not None: + args.extend(["--secured-device-port", secured_device_port]) + p = await run("[BRIDGE]", program, *args, stdin=asyncio.subprocess.DEVNULL) + await p.wait() + + +async def main(args): + await asyncio.gather( + run_admin( + args.app_admin, + storage_dir=args.storage_dir, + paa_trust_store_path=args.paa_trust_store_path, + commissioner_name=args.commissioner_name, + commissioner_node_id=args.commissioner_nodeid, + commissioner_vendor_id=args.commissioner_vendor_id, + ), + run_bridge( + args.app_bridge, + storage_dir=args.storage_dir, + secured_device_port=args.secured_device_port, + discriminator=args.discriminator, + passcode=args.passcode, + )) + + +if __name__ == "__main__": + parser = ArgumentParser(description="Fabric-Sync Example Application") + parser.add_argument("--app-admin", metavar="PATH", + default="out/linux-x64-fabric-admin-rpc/fabric-admin", + help="path to the fabric-admin executable; default=%(default)s") + parser.add_argument("--app-bridge", metavar="PATH", + default="out/linux-x64-fabric-bridge-rpc/fabric-bridge-app", + help="path to the fabric-bridge executable; default=%(default)s") + parser.add_argument("--storage-dir", metavar="PATH", + help="directory to place storage files in") + parser.add_argument("--paa-trust-store-path", metavar="PATH", + help="path to directory holding PAA certificates") + parser.add_argument("--commissioner-name", metavar="NAME", + help="commissioner name to use for the admin") + parser.add_argument("--commissioner-nodeid", metavar="NUM", + help="commissioner node ID to use for the admin") + parser.add_argument("--commissioner-vendor-id", metavar="NUM", + help="commissioner vendor ID to use for the admin") + parser.add_argument("--secured-device-port", metavar="NUM", + help="secure messages listen port to use for the bridge") + parser.add_argument("--discriminator", metavar="NUM", + help="discriminator to use for the bridge") + parser.add_argument("--passcode", metavar="NUM", + help="passcode to use for the bridge") + with contextlib.suppress(KeyboardInterrupt): + asyncio.run(main(parser.parse_args())) From 476af52fb0c07981f0156c7985d47701820d128f Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Mon, 26 Aug 2024 11:48:25 +0200 Subject: [PATCH 05/33] Customize fabric-admin and fabric-bridge RPC ports --- .../fabric-admin/scripts/fabric-sync-app.py | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/examples/fabric-admin/scripts/fabric-sync-app.py b/examples/fabric-admin/scripts/fabric-sync-app.py index 0c6151ae5adf2e..f43589c62050fe 100755 --- a/examples/fabric-admin/scripts/fabric-sync-app.py +++ b/examples/fabric-admin/scripts/fabric-sync-app.py @@ -28,12 +28,16 @@ async def run(tag, program, *args, stdin=None): return p -async def run_admin(program, storage_dir=None, paa_trust_store_path=None, - commissioner_name=None, commissioner_node_id=None, - commissioner_vendor_id=None): +async def run_admin(program, storage_dir=None, rpc_admin_port=None, rpc_bridge_port=None, + paa_trust_store_path=None, commissioner_name=None, + commissioner_node_id=None, commissioner_vendor_id=None): args = [] if storage_dir is not None: args.extend(["--storage-directory", storage_dir]) + if rpc_admin_port is not None: + args.extend(["--local-server-port", str(rpc_admin_port)]) + if rpc_bridge_port is not None: + args.extend(["--fabric-bridge-server-port", str(rpc_bridge_port)]) if paa_trust_store_path is not None: args.extend(["--paa-trust-store-path", paa_trust_store_path]) if commissioner_name is not None: @@ -46,18 +50,22 @@ async def run_admin(program, storage_dir=None, paa_trust_store_path=None, await p.wait() -async def run_bridge(program, storage_dir=None, discriminator=None, - passcode=None, secured_device_port=None): +async def run_bridge(program, storage_dir=None, rpc_admin_port=None, rpc_bridge_port=None, + discriminator=None, passcode=None, secured_device_port=None): args = [] if storage_dir is not None: args.extend(["--KVS", os.path.join(storage_dir, "chip_fabric_bridge_kvs")]) + if rpc_admin_port is not None: + args.extend(["--fabric-admin-server-port", str(rpc_admin_port)]) + if rpc_bridge_port is not None: + args.extend(["--local-server-port", str(rpc_bridge_port)]) if discriminator is not None: args.extend(["--discriminator", discriminator]) if passcode is not None: args.extend(["--passcode", passcode]) if secured_device_port is not None: - args.extend(["--secured-device-port", secured_device_port]) + args.extend(["--secured-device-port", str(secured_device_port)]) p = await run("[BRIDGE]", program, *args, stdin=asyncio.subprocess.DEVNULL) await p.wait() @@ -67,6 +75,8 @@ async def main(args): run_admin( args.app_admin, storage_dir=args.storage_dir, + rpc_admin_port=args.app_admin_rpc_port, + rpc_bridge_port=args.app_bridge_rpc_port, paa_trust_store_path=args.paa_trust_store_path, commissioner_name=args.commissioner_name, commissioner_node_id=args.commissioner_nodeid, @@ -75,6 +85,8 @@ async def main(args): run_bridge( args.app_bridge, storage_dir=args.storage_dir, + rpc_admin_port=args.app_admin_rpc_port, + rpc_bridge_port=args.app_bridge_rpc_port, secured_device_port=args.secured_device_port, discriminator=args.discriminator, passcode=args.passcode, @@ -89,6 +101,10 @@ async def main(args): parser.add_argument("--app-bridge", metavar="PATH", default="out/linux-x64-fabric-bridge-rpc/fabric-bridge-app", help="path to the fabric-bridge executable; default=%(default)s") + parser.add_argument("--app-admin-rpc-port", metavar="PORT", type=int, + help="fabric-admin RPC server port") + parser.add_argument("--app-bridge-rpc-port", metavar="PORT", type=int, + help="fabric-bridge RPC server port") parser.add_argument("--storage-dir", metavar="PATH", help="directory to place storage files in") parser.add_argument("--paa-trust-store-path", metavar="PATH", @@ -99,7 +115,7 @@ async def main(args): help="commissioner node ID to use for the admin") parser.add_argument("--commissioner-vendor-id", metavar="NUM", help="commissioner vendor ID to use for the admin") - parser.add_argument("--secured-device-port", metavar="NUM", + parser.add_argument("--secured-device-port", metavar="NUM", type=int, help="secure messages listen port to use for the bridge") parser.add_argument("--discriminator", metavar="NUM", help="discriminator to use for the bridge") From 9fc539f1212320aff03457b1795bee358f245d7b Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Mon, 26 Aug 2024 11:51:58 +0200 Subject: [PATCH 06/33] Create storage directory --- examples/fabric-admin/scripts/fabric-sync-app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/fabric-admin/scripts/fabric-sync-app.py b/examples/fabric-admin/scripts/fabric-sync-app.py index f43589c62050fe..d0539cbfd317d5 100755 --- a/examples/fabric-admin/scripts/fabric-sync-app.py +++ b/examples/fabric-admin/scripts/fabric-sync-app.py @@ -71,6 +71,8 @@ async def run_bridge(program, storage_dir=None, rpc_admin_port=None, rpc_bridge_ async def main(args): + if args.storage_dir is not None: + os.makedirs(args.storage_dir, exist_ok=True) await asyncio.gather( run_admin( args.app_admin, From 59c99f4aa5fa5ec0d5e4227f72073bcce3a2b676 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Mon, 26 Aug 2024 13:49:52 +0200 Subject: [PATCH 07/33] Use fabric-sync-app in the TC-MCORE-FS-1.3 script --- .../fabric-admin/scripts/fabric-sync-app.py | 9 +-- src/python_testing/TC_MCORE_FS_1_3.py | 66 ++++++++----------- 2 files changed, 34 insertions(+), 41 deletions(-) diff --git a/examples/fabric-admin/scripts/fabric-sync-app.py b/examples/fabric-admin/scripts/fabric-sync-app.py index d0539cbfd317d5..86b209db0d9781 100755 --- a/examples/fabric-admin/scripts/fabric-sync-app.py +++ b/examples/fabric-admin/scripts/fabric-sync-app.py @@ -15,6 +15,7 @@ async def forwarder(f_in, f_out, prefix: str): break f_out.buffer.write(prefix.encode()) f_out.buffer.write(line) + f_out.flush() async def run(tag, program, *args, stdin=None): @@ -46,7 +47,7 @@ async def run_admin(program, storage_dir=None, rpc_admin_port=None, rpc_bridge_p args.extend(["--commissioner-nodeid", commissioner_node_id]) if commissioner_vendor_id is not None: args.extend(["--commissioner-vendor-id", commissioner_vendor_id]) - p = await run("[ADMIN]", program, "interactive", "start", *args) + p = await run("[FS-ADMIN]", program, "interactive", "start", *args) await p.wait() @@ -66,7 +67,7 @@ async def run_bridge(program, storage_dir=None, rpc_admin_port=None, rpc_bridge_ args.extend(["--passcode", passcode]) if secured_device_port is not None: args.extend(["--secured-device-port", str(secured_device_port)]) - p = await run("[BRIDGE]", program, *args, stdin=asyncio.subprocess.DEVNULL) + p = await run("[FS-BRIDGE]", program, *args, stdin=asyncio.subprocess.DEVNULL) await p.wait() @@ -81,7 +82,7 @@ async def main(args): rpc_bridge_port=args.app_bridge_rpc_port, paa_trust_store_path=args.paa_trust_store_path, commissioner_name=args.commissioner_name, - commissioner_node_id=args.commissioner_nodeid, + commissioner_node_id=args.commissioner_node_id, commissioner_vendor_id=args.commissioner_vendor_id, ), run_bridge( @@ -113,7 +114,7 @@ async def main(args): help="path to directory holding PAA certificates") parser.add_argument("--commissioner-name", metavar="NAME", help="commissioner name to use for the admin") - parser.add_argument("--commissioner-nodeid", metavar="NUM", + parser.add_argument("--commissioner-node-id", metavar="NUM", help="commissioner node ID to use for the admin") parser.add_argument("--commissioner-vendor-id", metavar="NUM", help="commissioner vendor ID to use for the admin") diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index 923f4174050648..580dada758c45a 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -87,10 +87,9 @@ def stop(self): self.join() -class FabricAdminController: +class FabricSyncApp: - ADMIN_APP_PATH = "out/linux-x64-fabric-admin-rpc/fabric-admin" - BRIDGE_APP_PATH = "out/linux-x64-fabric-bridge-rpc/fabric-bridge-app" + APP_PATH = "examples/fabric-admin/scripts/fabric-sync-app.py" def _process_admin_output(self, line): if self.wait_for_text_text is not None and self.wait_for_text_text in line: @@ -108,25 +107,24 @@ def __init__(self, storageDir, fabricName=None, nodeId=None, vendorId=None, paaT self.wait_for_text_event = threading.Event() self.wait_for_text_text = None - args = [self.BRIDGE_APP_PATH] - args.extend(["--KVS", tempfile.mkstemp(dir=storageDir, prefix="kvs-bridge-")[1]]) - args.extend(['--secured-device-port', str(bridgePort)]) - args.extend(["--discriminator", str(bridgeDiscriminator)]) - args.extend(["--passcode", str(bridgePasscode)]) - # Start the bridge app which will connect to admin via RPC. - self.bridge = ThreadWithStop(args, tag="BRIDGE") - self.bridge.start() - - args = [self.ADMIN_APP_PATH, "interactive", "start"] - args.extend(["--storage-directory", storageDir]) - args.extend(["--commissioner-name", str(fabricName)]) - args.extend(["--commissioner-nodeid", str(nodeId)]) - args.extend(["--commissioner-vendor-id", str(vendorId)]) + args = [self.APP_PATH] + # Override default ports, so it will be possible to run + # our TH_FSA alongside the DUT_FSA during CI testing. + args.append("--app-admin-rpc-port=44000") + args.append("--app-bridge-rpc-port=44001") + # Keep the storage directory in a temporary location. + args.append(f"--storage-dir={storageDir}") # FIXME: Passing custom PAA store breaks something # if paaTrustStorePath is not None: - # args.extend(["--paa-trust-store-path", str(paaTrustStorePath)]) - - self.admin = ThreadWithStop(args, self._process_admin_output, tag="ADMIN") + # args.append(f"--paa-trust-store-path={paaTrustStorePath}") + args.append(f"--commissioner-name={fabricName}") + args.append(f"--commissioner-node-id={nodeId}") + args.append(f"--commissioner-vendor-id={vendorId}") + args.append(f"--secured-device-port={bridgePort}") + args.append(f"--discriminator={bridgeDiscriminator}") + args.append(f"--passcode={bridgePasscode}") + + self.admin = ThreadWithStop(args, self._process_admin_output) self.wait_for_text_text = "Connected to Fabric-Bridge" self.admin.start() @@ -143,7 +141,6 @@ def CommissionOnNetwork(self, nodeId, setupPinCode=None, filterType=None, filter def stop(self): self.admin.stop() - self.bridge.stop() class AppServer: @@ -167,23 +164,18 @@ class TC_MCORE_FS_1_3(MatterBaseTest): def setup_class(self): super().setup_class() - # Get the path to the Fabric Admin and Bridge apps from the user - # params or use the default paths. - FabricAdminController.ADMIN_APP_PATH = self.user_params.get( - "th_fabric_admin_app_path", FabricAdminController.ADMIN_APP_PATH) - FabricAdminController.BRIDGE_APP_PATH = self.user_params.get( - "th_fabric_bridge_app_path", FabricAdminController.BRIDGE_APP_PATH) - if not os.path.exists(FabricAdminController.ADMIN_APP_PATH): - asserts.fail("This test requires a TH_FABRIC_ADMIN app. Specify app path with --string-arg th_fabric_admin_app_path:") - if not os.path.exists(FabricAdminController.BRIDGE_APP_PATH): - asserts.fail("This test requires a TH_FABRIC_BRIDGE app. Specify app path with --string-arg th_fabric_bridge_app_path:") + # Get the path to the TH_FSA (fabric-admin and fabric-bridge) app from + # the user params or use the default path. + FabricSyncApp.APP_PATH = self.user_params.get("th_fabric_sync_app_path", FabricSyncApp.APP_PATH) + if not os.path.exists(FabricSyncApp.APP_PATH): + asserts.fail("This test requires a TH_FSA app. Specify app path with --string-arg th_fabric_sync_app_path:") # Get the path to the TH_SERVER app from the user params. - app = self.user_params.get("th_server_app_path", None) - if not app: + th_server_app = self.user_params.get("th_server_app_path", None) + if not th_server_app: asserts.fail("This test requires a TH_SERVER app. Specify app path with --string-arg th_server_app_path:") - if not os.path.exists(app): - asserts.fail(f"The path {app} does not exist") + if not os.path.exists(th_server_app): + asserts.fail(f"The path {th_server_app} does not exist") # Create a temporary storage directory for keeping KVS files. self.storage = tempfile.TemporaryDirectory(prefix=self.__class__.__name__) @@ -193,7 +185,7 @@ def setup_class(self): self.fsa_bridge_discriminator = random.randint(0, 4095) self.fsa_bridge_passcode = 20202021 - self.th_fsa_controller = FabricAdminController( + self.th_fsa_controller = FabricSyncApp( storageDir=self.storage.name, paaTrustStorePath=self.matter_test_config.paa_trust_store_path, bridgePort=self.fsa_bridge_port, @@ -213,7 +205,7 @@ def setup_class(self): # Start the TH_SERVER_FOR_TH_FSA app. self.server_for_th = AppServer( - app, + th_server_app, storageDir=self.storage.name, port=self.server_for_th_port, discriminator=self.server_for_th_discriminator, From 9b7073e9dca7de521b999732cf6219237d9b5d86 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Mon, 26 Aug 2024 16:42:04 +0200 Subject: [PATCH 08/33] Use CommissionerControlCluster to commission TH_SERVER onto DUT --- src/python_testing/TC_MCORE_FS_1_3.py | 153 +++++++++++++++++++++----- 1 file changed, 125 insertions(+), 28 deletions(-) diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index 580dada758c45a..3d5c4df8a68c1c 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -228,15 +228,128 @@ def steps_TC_MCORE_FS_1_3(self) -> list[TestStep]: TestStep(6, "DUT_FSA synchronizes TH_SERVER_FOR_TH_FSA onto DUT_FSA's fabric and copies the UniqueID presented by TH_FSA's Bridged Device Basic Information Cluster"), ] + async def commission_via_commissioner_control(self, controller_node_id: int, device_node_id: int): + """Commission device_node_id to controller_node_id using CommissionerControl cluster.""" + + request_id = random.randint(0, 0xFFFFFFFFFFFFFFFF) + + vendor_id = await self.read_single_attribute_check_success( + node_id=device_node_id, + cluster=Clusters.BasicInformation, + attribute=Clusters.BasicInformation.Attributes.VendorID, + ) + + product_id = await self.read_single_attribute_check_success( + node_id=device_node_id, + cluster=Clusters.BasicInformation, + attribute=Clusters.BasicInformation.Attributes.ProductID, + ) + + await self.send_single_cmd( + node_id=controller_node_id, + cmd=Clusters.CommissionerControl.Commands.RequestCommissioningApproval( + requestId=request_id, + vendorId=vendor_id, + productId=product_id, + ), + ) + + if not self.is_ci: + self.wait_for_user_input("Approve Commissioning Approval Request on DUT using manufacturer specified mechanism") + + resp = await self.send_single_cmd( + node_id=controller_node_id, + cmd=Clusters.CommissionerControl.Commands.CommissionNode( + requestId=request_id, + responseTimeoutSeconds=30, + ), + ) + + asserts.assert_equal(type(resp), Clusters.CommissionerControl.Commands.ReverseOpenCommissioningWindow, + "Incorrect response type") + + await self.send_single_cmd( + node_id=device_node_id, + cmd=Clusters.AdministratorCommissioning.Commands.OpenCommissioningWindow( + commissioningTimeout=3*60, + PAKEPasscodeVerifier=resp.PAKEPasscodeVerifier, + discriminator=resp.discriminator, + iterations=resp.iterations, + salt=resp.salt, + ), + timedRequestTimeoutMs=5000, + ) + + if not self.is_ci: + await asyncio.sleep(30) + @async_test_body async def test_TC_MCORE_FS_1_3(self): self.is_ci = self.check_pics('PICS_SDK_CI_ONLY') + self.is_ci = True # Commissioning - done self.step(0) + th_server_for_th_fsa_th_node_id = 1 + self.print_step(1, "Commissioning TH_SERVER_FOR_TH_FSA to TH fabric") + await self.default_controller.CommissionOnNetwork( + nodeId=th_server_for_th_fsa_th_node_id, + setupPinCode=self.server_for_th_passcode, + filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, + filter=self.server_for_th_discriminator, + ) + + self.print_step(2, "Verify that TH_SERVER_FOR_TH_FSA does not have a UniqueID") + await self.read_single_attribute_expect_error( + cluster=Clusters.BasicInformation, + attribute=Clusters.BasicInformation.Attributes.UniqueID, + node_id=th_server_for_th_fsa_th_node_id, + error=Status.UnsupportedAttribute, + ) + + # Get the list of endpoints on the DUT_FSA_BRIDGE before adding the TH_SERVER_FOR_TH_FSA. + dut_fsa_bridge_endpoints = set(await self.read_single_attribute_check_success( + cluster=Clusters.Descriptor, + attribute=Clusters.Descriptor.Attributes.PartsList, + endpoint=0, + )) + + self.print_step(3, "Commissioning TH_SERVER_FOR_TH_FSA to DUT_FSA fabric") + await self.commission_via_commissioner_control( + controller_node_id=self.dut_node_id, + device_node_id=th_server_for_th_fsa_th_node_id) + + # Wait for the device to appear on the DUT_FSA_BRIDGE. + await asyncio.sleep(2) + + # Get the list of endpoints on the DUT_FSA_BRIDGE after adding the TH_SERVER_FOR_TH_FSA. + dut_fsa_bridge_endpoints_new = set(await self.read_single_attribute_check_success( + cluster=Clusters.Descriptor, + attribute=Clusters.Descriptor.Attributes.PartsList, + endpoint=0, + )) + + # Get the endpoint number for just added TH_SERVER_FOR_TH_FSA. + logging.info("Endpoints on DUT_FSA_BRIDGE: old=%s, new=%s", dut_fsa_bridge_endpoints, dut_fsa_bridge_endpoints_new) + asserts.assert_true(dut_fsa_bridge_endpoints_new.issuperset(dut_fsa_bridge_endpoints), + "Expected only new endpoints to be added") + unique_endpoints_set = dut_fsa_bridge_endpoints_new - dut_fsa_bridge_endpoints + asserts.assert_equal(len(unique_endpoints_set), 1, "Expected only one new endpoint on DUT_FSA") + dut_fsa_bridge_th_server_endpoint = list(unique_endpoints_set)[0] + dut_fsa_bridge_endpoints = dut_fsa_bridge_endpoints_new + + self.print_step(4, "Verify that DUT_FSA created a UniqueID for TH_SERVER_FOR_TH_FSA") + dut_fsa_bridge_th_server_unique_id = await self.read_single_attribute_check_success( + cluster=Clusters.BridgedDeviceBasicInformation, + attribute=Clusters.BridgedDeviceBasicInformation.Attributes.UniqueID, + endpoint=dut_fsa_bridge_th_server_endpoint) + asserts.assert_true(type_matches(dut_fsa_bridge_th_server_unique_id, str), "UniqueID should be a string") + asserts.assert_true(dut_fsa_bridge_th_server_unique_id, "UniqueID should not be an empty string") + logging.info("TH_SERVER_FOR_TH_FSA on TH_SERVER_BRIDGE UniqueID: %s", dut_fsa_bridge_th_server_unique_id) + th_fsa_bridge_th_node_id = 2 - self.print_step(1, "Commissioning TH_FSA_BRIDGE to TH fabric") + self.print_step(5, "Commissioning TH_FSA_BRIDGE to TH fabric") await self.default_controller.CommissionOnNetwork( nodeId=th_fsa_bridge_th_node_id, setupPinCode=self.fsa_bridge_passcode, @@ -252,24 +365,8 @@ async def test_TC_MCORE_FS_1_3(self): endpoint=0, )) - th_server_for_th_fsa_th_node_id = 3 - self.print_step(2, "Commissioning TH_SERVER_FOR_TH_FSA to TH fabric") - await self.default_controller.CommissionOnNetwork( - nodeId=th_server_for_th_fsa_th_node_id, - setupPinCode=self.server_for_th_passcode, - filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, - filter=self.server_for_th_discriminator, - ) - - self.print_step(3, "Verify that TH_SERVER_FOR_TH_FSA does not have a UniqueID") - await self.read_single_attribute_expect_error( - cluster=Clusters.BasicInformation, - attribute=Clusters.BasicInformation.Attributes.UniqueID, - node_id=th_server_for_th_fsa_th_node_id, - error=Status.UnsupportedAttribute) - discriminator = random.randint(0, 4095) - self.print_step(4, "Open commissioning window on TH_SERVER_FOR_TH_FSA") + self.print_step(6, "Open commissioning window on TH_SERVER_FOR_TH_FSA") params = await self.default_controller.OpenCommissioningWindow( nodeid=th_server_for_th_fsa_th_node_id, timeout=600, @@ -284,7 +381,7 @@ async def test_TC_MCORE_FS_1_3(self): await asyncio.sleep(2) th_server_for_th_fsa_th_fsa_node_id = 3 - self.print_step(5, "Commissioning TH_SERVER_FOR_TH_FSA to TH_FSA") + self.print_step(7, "Commissioning TH_SERVER_FOR_TH_FSA to TH_FSA") self.th_fsa_controller.CommissionOnNetwork( nodeId=th_server_for_th_fsa_th_fsa_node_id, setupPinCode=params.setupPinCode, @@ -311,7 +408,7 @@ async def test_TC_MCORE_FS_1_3(self): asserts.assert_equal(len(unique_endpoints_set), 1, "Expected only one new endpoint") th_fsa_bridge_th_server_endpoint = list(unique_endpoints_set)[0] - self.print_step(6, "Verify that TH_FSA created a UniqueID for TH_SERVER_FOR_TH_FSA") + self.print_step(8, "Verify that TH_FSA created a UniqueID for TH_SERVER_FOR_TH_FSA") th_fsa_bridge_th_server_unique_id = await self.read_single_attribute_check_success( cluster=Clusters.BridgedDeviceBasicInformation, attribute=Clusters.BridgedDeviceBasicInformation.Attributes.UniqueID, @@ -321,14 +418,14 @@ async def test_TC_MCORE_FS_1_3(self): asserts.assert_true(th_fsa_bridge_th_server_unique_id, "UniqueID should not be an empty string") logging.info("TH_SERVER_FOR_TH_FSA on TH_SERVER_BRIDGE UniqueID: %s", th_fsa_bridge_th_server_unique_id) - discriminator = random.randint(0, 4095) - self.print_step(7, "Open commissioning window on TH_FSA_BRIDGE") - params = await self.default_controller.OpenCommissioningWindow( - nodeid=th_fsa_bridge_th_node_id, - timeout=600, - iteration=10000, - discriminator=discriminator, - option=1) + # Make sure that the UniqueID on the TH_FSA_BRIDGE is different from the one on the DUT_FSA_BRIDGE. + asserts.assert_not_equal(dut_fsa_bridge_th_server_unique_id, th_fsa_bridge_th_server_unique_id, + "UniqueID on DUT_FSA_BRIDGE and TH_FSA_BRIDGE should be different") + + self.print_step(9, "Commissioning TH_FSA_BRIDGE to DUT_FSA fabric") + await self.commission_via_commissioner_control( + controller_node_id=self.dut_node_id, + device_node_id=th_fsa_bridge_th_node_id) await asyncio.sleep(10000) From 4b21fe9b63985b5a7e98797e54310d86fe239744 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Tue, 27 Aug 2024 11:25:48 +0200 Subject: [PATCH 09/33] Auto-link bridge with admin --- .../fabric-admin/scripts/fabric-sync-app.py | 101 ++++++++++++++---- 1 file changed, 80 insertions(+), 21 deletions(-) diff --git a/examples/fabric-admin/scripts/fabric-sync-app.py b/examples/fabric-admin/scripts/fabric-sync-app.py index 86b209db0d9781..41b6851733354c 100755 --- a/examples/fabric-admin/scripts/fabric-sync-app.py +++ b/examples/fabric-admin/scripts/fabric-sync-app.py @@ -7,31 +7,54 @@ from argparse import ArgumentParser -async def forwarder(f_in, f_out, prefix: str): +BRIDGE_COMMISSIONED = asyncio.Event() +# Log message which should appear in the fabric-admin output if +# the bridge is already commissioned. +BRIDGE_COMMISSIONED_MSG = b"Reading attribute: Cluster=0x0000_001D Endpoint=0x1 AttributeId=0x0000_0000" + + +async def forward_f(f_in, f_out, prefix: str): """Forward f_in to f_out with a prefix attached.""" + global BRIDGE_COMMISSIONED while True: line = await f_in.readline() if not line: break + if not BRIDGE_COMMISSIONED.is_set() and BRIDGE_COMMISSIONED_MSG in line: + BRIDGE_COMMISSIONED.set() f_out.buffer.write(prefix.encode()) f_out.buffer.write(line) f_out.flush() +async def forward_stdin(f_out: asyncio.StreamWriter): + """Forward stdin to f_out.""" + loop = asyncio.get_event_loop() + reader = asyncio.StreamReader() + protocol = asyncio.StreamReaderProtocol(reader) + await loop.connect_read_pipe(lambda: protocol, sys.stdin) + while True: + line = await reader.readline() + if not line: + sys.exit(0) + f_out.write(line) + + async def run(tag, program, *args, stdin=None): p = await asyncio.create_subprocess_exec(program, *args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, stdin=stdin) # Add the stdout and stderr processing to the event loop. - asyncio.create_task(forwarder(p.stderr, sys.stderr, tag)) - asyncio.create_task(forwarder(p.stdout, sys.stdout, tag)) + asyncio.create_task(forward_f(p.stderr, sys.stderr, tag)) + asyncio.create_task(forward_f(p.stdout, sys.stdout, tag)) return p -async def run_admin(program, storage_dir=None, rpc_admin_port=None, rpc_bridge_port=None, - paa_trust_store_path=None, commissioner_name=None, - commissioner_node_id=None, commissioner_vendor_id=None): +async def run_admin(program, storage_dir=None, rpc_admin_port=None, + rpc_bridge_port=None, paa_trust_store_path=None, + commissioner_name=None, commissioner_node_id=None, + commissioner_vendor_id=None): args = [] if storage_dir is not None: args.extend(["--storage-directory", storage_dir]) @@ -44,15 +67,16 @@ async def run_admin(program, storage_dir=None, rpc_admin_port=None, rpc_bridge_p if commissioner_name is not None: args.extend(["--commissioner-name", commissioner_name]) if commissioner_node_id is not None: - args.extend(["--commissioner-nodeid", commissioner_node_id]) + args.extend(["--commissioner-nodeid", str(commissioner_node_id)]) if commissioner_vendor_id is not None: - args.extend(["--commissioner-vendor-id", commissioner_vendor_id]) - p = await run("[FS-ADMIN]", program, "interactive", "start", *args) - await p.wait() + args.extend(["--commissioner-vendor-id", str(commissioner_vendor_id)]) + return await run("[FS-ADMIN]", program, "interactive", "start", *args, + stdin=asyncio.subprocess.PIPE) -async def run_bridge(program, storage_dir=None, rpc_admin_port=None, rpc_bridge_port=None, - discriminator=None, passcode=None, secured_device_port=None): +async def run_bridge(program, storage_dir=None, rpc_admin_port=None, + rpc_bridge_port=None, discriminator=None, passcode=None, + secured_device_port=None): args = [] if storage_dir is not None: args.extend(["--KVS", @@ -62,19 +86,24 @@ async def run_bridge(program, storage_dir=None, rpc_admin_port=None, rpc_bridge_ if rpc_bridge_port is not None: args.extend(["--local-server-port", str(rpc_bridge_port)]) if discriminator is not None: - args.extend(["--discriminator", discriminator]) + args.extend(["--discriminator", str(discriminator)]) if passcode is not None: - args.extend(["--passcode", passcode]) + args.extend(["--passcode", str(passcode)]) if secured_device_port is not None: args.extend(["--secured-device-port", str(secured_device_port)]) - p = await run("[FS-BRIDGE]", program, *args, stdin=asyncio.subprocess.DEVNULL) - await p.wait() + return await run("[FS-BRIDGE]", program, *args, + stdin=asyncio.subprocess.DEVNULL) async def main(args): + + if args.commissioner_node_id == 1: + raise ValueError("NodeID=1 is reserved for the local fabric-bridge") + if args.storage_dir is not None: os.makedirs(args.storage_dir, exist_ok=True) - await asyncio.gather( + + admin, bridge = await asyncio.gather( run_admin( args.app_admin, storage_dir=args.storage_dir, @@ -95,6 +124,36 @@ async def main(args): passcode=args.passcode, )) + # Wait a bit for apps to start. + await asyncio.sleep(1) + + try: + # Check whether the bridge is already commissioned. If it is, + # we will get the response, otherwise we will hit timeout. + cmd = "descriptor read device-type-list 1 1 --timeout 1" + admin.stdin.write((cmd + "\n").encode()) + await asyncio.wait_for(BRIDGE_COMMISSIONED.wait(), timeout=1) + except asyncio.TimeoutError: + # Commission the bridge to the admin. + cmd = "fabricsync add-local-bridge 1" + if args.passcode is not None: + cmd += f" --setup-pin-code {args.passcode}" + if args.secured_device_port is not None: + cmd += f" --local-port {args.secured_device_port}" + admin.stdin.write((cmd + "\n").encode()) + + try: + await asyncio.gather( + admin.wait(), bridge.wait(), + forward_stdin(admin.stdin)) + except SystemExit: + admin.terminate() + bridge.terminate() + except Exception: + admin.terminate() + bridge.terminate() + raise + if __name__ == "__main__": parser = ArgumentParser(description="Fabric-Sync Example Application") @@ -114,15 +173,15 @@ async def main(args): help="path to directory holding PAA certificates") parser.add_argument("--commissioner-name", metavar="NAME", help="commissioner name to use for the admin") - parser.add_argument("--commissioner-node-id", metavar="NUM", + parser.add_argument("--commissioner-node-id", metavar="NUM", type=int, help="commissioner node ID to use for the admin") - parser.add_argument("--commissioner-vendor-id", metavar="NUM", + parser.add_argument("--commissioner-vendor-id", metavar="NUM", type=int, help="commissioner vendor ID to use for the admin") parser.add_argument("--secured-device-port", metavar="NUM", type=int, help="secure messages listen port to use for the bridge") - parser.add_argument("--discriminator", metavar="NUM", + parser.add_argument("--discriminator", metavar="NUM", type=int, help="discriminator to use for the bridge") - parser.add_argument("--passcode", metavar="NUM", + parser.add_argument("--passcode", metavar="NUM", type=int, help="passcode to use for the bridge") with contextlib.suppress(KeyboardInterrupt): asyncio.run(main(parser.parse_args())) From 87c92be43b89badafa58af03689a5bb8bfe07442 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Tue, 27 Aug 2024 14:40:01 +0200 Subject: [PATCH 10/33] Test automation setup --- src/python_testing/TC_MCORE_FS_1_3.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index 3d5c4df8a68c1c..f7bd1d7dff695f 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -19,6 +19,19 @@ # when reading UniqueID from BasicInformation Cluster. Please specify the app # location with --string-arg th_server_app_path: +# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments +# for details about the block below. +# +# === BEGIN CI TEST ARGUMENTS === +# test-runner-runs: run1 +# test-runner-run/run1/app: examples/fabric-admin/scripts/fabric-sync-app.py +# test-runner-run/run1/app-args: --storage-dir dut-fsa --discriminator 1234 +# test-runner-run/run1/factoryreset: True +# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --string-arg th_server_app_path:${LIGHTING_APP_NO_UNIQUE_ID} +# test-runner-run/run1/script-start-delay: 10 +# test-runner-run/run1/quiet: false +# === END CI TEST ARGUMENTS === + import asyncio import logging import os From b0e60d3fe2242c2b209e320e8cb3084a838a4d6c Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Tue, 27 Aug 2024 14:41:23 +0200 Subject: [PATCH 11/33] Terminate apps on SIGTERM and SIGINT --- .../fabric-admin/scripts/fabric-sync-app.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/examples/fabric-admin/scripts/fabric-sync-app.py b/examples/fabric-admin/scripts/fabric-sync-app.py index 41b6851733354c..6a4163dc8e8ede 100755 --- a/examples/fabric-admin/scripts/fabric-sync-app.py +++ b/examples/fabric-admin/scripts/fabric-sync-app.py @@ -1,9 +1,24 @@ #!/usr/bin/env python3 +# Copyright (c) 2024 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio import contextlib +import signal import sys import os -import asyncio from argparse import ArgumentParser @@ -103,6 +118,14 @@ async def main(args): if args.storage_dir is not None: os.makedirs(args.storage_dir, exist_ok=True) + def terminate(signum, frame): + admin.terminate() + bridge.terminate() + sys.exit(1) + + signal.signal(signal.SIGINT, terminate) + signal.signal(signal.SIGTERM, terminate) + admin, bridge = await asyncio.gather( run_admin( args.app_admin, From da7f4eb57a99413a9c8febb70f870b5beecfea38 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Tue, 27 Aug 2024 15:09:18 +0200 Subject: [PATCH 12/33] Open commissioning window on fabric-bridge after adding to FSA --- examples/fabric-admin/scripts/fabric-sync-app.py | 11 +++++++++-- src/python_testing/TC_MCORE_FS_1_3.py | 10 +++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/examples/fabric-admin/scripts/fabric-sync-app.py b/examples/fabric-admin/scripts/fabric-sync-app.py index 6a4163dc8e8ede..c2b4c75722781c 100755 --- a/examples/fabric-admin/scripts/fabric-sync-app.py +++ b/examples/fabric-admin/scripts/fabric-sync-app.py @@ -51,7 +51,7 @@ async def forward_stdin(f_out: asyncio.StreamWriter): while True: line = await reader.readline() if not line: - sys.exit(0) + break f_out.write(line) @@ -155,7 +155,7 @@ def terminate(signum, frame): # we will get the response, otherwise we will hit timeout. cmd = "descriptor read device-type-list 1 1 --timeout 1" admin.stdin.write((cmd + "\n").encode()) - await asyncio.wait_for(BRIDGE_COMMISSIONED.wait(), timeout=1) + await asyncio.wait_for(BRIDGE_COMMISSIONED.wait(), timeout=1.5) except asyncio.TimeoutError: # Commission the bridge to the admin. cmd = "fabricsync add-local-bridge 1" @@ -164,6 +164,13 @@ def terminate(signum, frame): if args.secured_device_port is not None: cmd += f" --local-port {args.secured_device_port}" admin.stdin.write((cmd + "\n").encode()) + # Wait for the bridge to be commissioned. + await asyncio.sleep(5) + + # Open commissioning window with original setup code for the bridge, + # so it can be added by the TH fabric. + cmd = "pairing open-commissioning-window 1 0 0 600 1000 0" + admin.stdin.write((cmd + "\n").encode()) try: await asyncio.gather( diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index f7bd1d7dff695f..2a3b01af1f0a7c 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -130,8 +130,10 @@ def __init__(self, storageDir, fabricName=None, nodeId=None, vendorId=None, paaT # FIXME: Passing custom PAA store breaks something # if paaTrustStorePath is not None: # args.append(f"--paa-trust-store-path={paaTrustStorePath}") - args.append(f"--commissioner-name={fabricName}") - args.append(f"--commissioner-node-id={nodeId}") + if fabricName is not None: + args.append(f"--commissioner-name={fabricName}") + if nodeId is not None: + args.append(f"--commissioner-node-id={nodeId}") args.append(f"--commissioner-vendor-id={vendorId}") args.append(f"--secured-device-port={bridgePort}") args.append(f"--discriminator={bridgeDiscriminator}") @@ -204,9 +206,7 @@ def setup_class(self): bridgePort=self.fsa_bridge_port, bridgeDiscriminator=self.fsa_bridge_discriminator, bridgePasscode=self.fsa_bridge_passcode, - vendorId=0xFFF1, - fabricName="beta", - nodeId=1) + vendorId=0xFFF1) self.server_for_dut_port = 5544 self.server_for_dut_discriminator = random.randint(0, 4095) From d24b1a001122dca104add2f12ea751b605371f36 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Wed, 28 Aug 2024 10:09:21 +0200 Subject: [PATCH 13/33] Commissioning TH_FSA_BRIDGE to DUT_FSA fabric --- .../fabric-admin/scripts/fabric-sync-app.py | 35 +++++++++- src/python_testing/TC_MCORE_FS_1_3.py | 69 +++++++++++++------ 2 files changed, 81 insertions(+), 23 deletions(-) diff --git a/examples/fabric-admin/scripts/fabric-sync-app.py b/examples/fabric-admin/scripts/fabric-sync-app.py index c2b4c75722781c..4fb252dda759ae 100755 --- a/examples/fabric-admin/scripts/fabric-sync-app.py +++ b/examples/fabric-admin/scripts/fabric-sync-app.py @@ -42,6 +42,25 @@ async def forward_f(f_in, f_out, prefix: str): f_out.flush() +async def forward_pipe(pipe_path: str, f_out: asyncio.StreamWriter): + """Forward named pipe to f_out. + + Unfortunately, Python does not support async file I/O on named pipes. This + function performs busy waiting with a short asyncio-friendly sleep to read + from the pipe. + """ + fd = os.open(pipe_path, os.O_RDONLY | os.O_NONBLOCK) + while True: + try: + data = os.read(fd, 1024) + if data: + f_out.write(data) + if not data: + await asyncio.sleep(1) + except BlockingIOError: + await asyncio.sleep(1) + + async def forward_stdin(f_out: asyncio.StreamWriter): """Forward stdin to f_out.""" loop = asyncio.get_event_loop() @@ -51,7 +70,7 @@ async def forward_stdin(f_out: asyncio.StreamWriter): while True: line = await reader.readline() if not line: - break + sys.exit(0) f_out.write(line) @@ -118,6 +137,10 @@ async def main(args): if args.storage_dir is not None: os.makedirs(args.storage_dir, exist_ok=True) + pipe = args.stdin_pipe + if pipe and not os.path.exists(pipe): + os.mkfifo(pipe) + def terminate(signum, frame): admin.terminate() bridge.terminate() @@ -171,11 +194,15 @@ def terminate(signum, frame): # so it can be added by the TH fabric. cmd = "pairing open-commissioning-window 1 0 0 600 1000 0" admin.stdin.write((cmd + "\n").encode()) + # Wait some time for the bridge to open the commissioning window. + await asyncio.sleep(1) try: await asyncio.gather( - admin.wait(), bridge.wait(), - forward_stdin(admin.stdin)) + forward_pipe(pipe, admin.stdin) if pipe else forward_stdin(admin.stdin), + admin.wait(), + bridge.wait(), + ) except SystemExit: admin.terminate() bridge.terminate() @@ -197,6 +224,8 @@ def terminate(signum, frame): help="fabric-admin RPC server port") parser.add_argument("--app-bridge-rpc-port", metavar="PORT", type=int, help="fabric-bridge RPC server port") + parser.add_argument("--stdin-pipe", metavar="PATH", + help="read input from a named pipe instead of stdin") parser.add_argument("--storage-dir", metavar="PATH", help="directory to place storage files in") parser.add_argument("--paa-trust-store-path", metavar="PATH", diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index 2a3b01af1f0a7c..5978bf52f21007 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -25,9 +25,9 @@ # === BEGIN CI TEST ARGUMENTS === # test-runner-runs: run1 # test-runner-run/run1/app: examples/fabric-admin/scripts/fabric-sync-app.py -# test-runner-run/run1/app-args: --storage-dir dut-fsa --discriminator 1234 +# test-runner-run/run1/app-args: --stdin-pipe=dut-fsa/stdin --storage-dir=dut-fsa --discriminator=1234 # test-runner-run/run1/factoryreset: True -# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --string-arg th_server_app_path:${LIGHTING_APP_NO_UNIQUE_ID} +# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --string-arg th_server_app_path:${LIGHTING_APP_NO_UNIQUE_ID} dut_fsa_stdin_pipe:dut-fsa/stdin # test-runner-run/run1/script-start-delay: 10 # test-runner-run/run1/quiet: false # === END CI TEST ARGUMENTS === @@ -181,9 +181,9 @@ def setup_class(self): # Get the path to the TH_FSA (fabric-admin and fabric-bridge) app from # the user params or use the default path. - FabricSyncApp.APP_PATH = self.user_params.get("th_fabric_sync_app_path", FabricSyncApp.APP_PATH) + FabricSyncApp.APP_PATH = self.user_params.get("th_fsa_app_path", FabricSyncApp.APP_PATH) if not os.path.exists(FabricSyncApp.APP_PATH): - asserts.fail("This test requires a TH_FSA app. Specify app path with --string-arg th_fabric_sync_app_path:") + asserts.fail("This test requires a TH_FSA app. Specify app path with --string-arg th_fsa_app_path:") # Get the path to the TH_SERVER app from the user params. th_server_app = self.user_params.get("th_server_app_path", None) @@ -196,18 +196,24 @@ def setup_class(self): self.storage = tempfile.TemporaryDirectory(prefix=self.__class__.__name__) logging.info("Temporary storage directory: %s", self.storage.name) - self.fsa_bridge_port = 5543 - self.fsa_bridge_discriminator = random.randint(0, 4095) - self.fsa_bridge_passcode = 20202021 + self.th_fsa_bridge_address = "::1" + self.th_fsa_bridge_port = 5543 + self.th_fsa_bridge_discriminator = random.randint(0, 4095) + self.th_fsa_bridge_passcode = 20202021 self.th_fsa_controller = FabricSyncApp( storageDir=self.storage.name, paaTrustStorePath=self.matter_test_config.paa_trust_store_path, - bridgePort=self.fsa_bridge_port, - bridgeDiscriminator=self.fsa_bridge_discriminator, - bridgePasscode=self.fsa_bridge_passcode, + bridgePort=self.th_fsa_bridge_port, + bridgeDiscriminator=self.th_fsa_bridge_discriminator, + bridgePasscode=self.th_fsa_bridge_passcode, vendorId=0xFFF1) + # Get the named pipe path for the DUT_FSA app input from the user params. + dut_fsa_stdin_pipe = self.user_params.get("dut_fsa_stdin_pipe", None) + if dut_fsa_stdin_pipe is not None: + self.dut_fsa_stdin = open(dut_fsa_stdin_pipe, "w") + self.server_for_dut_port = 5544 self.server_for_dut_discriminator = random.randint(0, 4095) self.server_for_dut_passcode = 20202021 @@ -314,6 +320,7 @@ async def test_TC_MCORE_FS_1_3(self): ) self.print_step(2, "Verify that TH_SERVER_FOR_TH_FSA does not have a UniqueID") + # FIXME: Sometimes reading the UniqueID fails... await self.read_single_attribute_expect_error( cluster=Clusters.BasicInformation, attribute=Clusters.BasicInformation.Attributes.UniqueID, @@ -365,9 +372,9 @@ async def test_TC_MCORE_FS_1_3(self): self.print_step(5, "Commissioning TH_FSA_BRIDGE to TH fabric") await self.default_controller.CommissionOnNetwork( nodeId=th_fsa_bridge_th_node_id, - setupPinCode=self.fsa_bridge_passcode, + setupPinCode=self.th_fsa_bridge_passcode, filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, - filter=self.fsa_bridge_discriminator, + filter=self.th_fsa_bridge_discriminator, ) # Get the list of endpoints on the TH_FSA_BRIDGE before adding the TH_SERVER_FOR_TH_FSA. @@ -382,10 +389,10 @@ async def test_TC_MCORE_FS_1_3(self): self.print_step(6, "Open commissioning window on TH_SERVER_FOR_TH_FSA") params = await self.default_controller.OpenCommissioningWindow( nodeid=th_server_for_th_fsa_th_node_id, - timeout=600, - iteration=10000, + option=self.default_controller.CommissioningWindowPasscode.kTokenWithRandomPin, discriminator=discriminator, - option=1) + iteration=10000, + timeout=600) # FIXME: Sometimes the commissioning does not work with the error: # > Failed to verify peer's MAC. This can happen when setup code is incorrect. @@ -435,12 +442,34 @@ async def test_TC_MCORE_FS_1_3(self): asserts.assert_not_equal(dut_fsa_bridge_th_server_unique_id, th_fsa_bridge_th_server_unique_id, "UniqueID on DUT_FSA_BRIDGE and TH_FSA_BRIDGE should be different") - self.print_step(9, "Commissioning TH_FSA_BRIDGE to DUT_FSA fabric") - await self.commission_via_commissioner_control( - controller_node_id=self.dut_node_id, - device_node_id=th_fsa_bridge_th_node_id) + discriminator = random.randint(0, 4095) + self.print_step(9, "Open commissioning window on TH_FSA_BRIDGE") + params = await self.default_controller.OpenCommissioningWindow( + nodeid=th_fsa_bridge_th_node_id, + option=self.default_controller.CommissioningWindowPasscode.kTokenWithRandomPin, + discriminator=discriminator, + iteration=10000, + timeout=600) - await asyncio.sleep(10000) + self.print_step(9, "Commissioning TH_FSA_BRIDGE to DUT_FSA fabric") + if not self.is_ci: + self.wait_for_user_input( + f"Commission TH Fabric-Sync Bridge on DUT using manufacturer specified mechanism.\n" + f"Use the following parameters:\n" + f"- discriminator: {discriminator}\n" + f"- setupPinCode: {params.setupPinCode}\n" + f"- setupQRCode: {params.setupQRCode}\n" + f"- setupManualCode: {params.setupManualCode}\n" + f"If using FabricSync Admin, you may type:\n" + f">>> fabricsync add-bridge {params.setupPinCode} {self.th_fsa_bridge_port}") + else: + self.dut_fsa_stdin.write( + f"fabricsync add-bridge 10 {params.setupPinCode} {self.th_fsa_bridge_address} {self.th_fsa_bridge_port}\n") + self.dut_fsa_stdin.flush() + # Wait for the commissioning to complete. + await asyncio.sleep(5) + + await asyncio.sleep(10) return From 9509672c2966c57a8dca8ab48711936428882ae4 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Wed, 28 Aug 2024 10:23:35 +0200 Subject: [PATCH 14/33] Synchronize server from TH to DUT --- src/python_testing/TC_MCORE_FS_1_3.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index 5978bf52f21007..919447df954f7a 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -469,6 +469,20 @@ async def test_TC_MCORE_FS_1_3(self): # Wait for the commissioning to complete. await asyncio.sleep(5) + self.print_step(9, "Synchronize TH_SERVER_FOR_TH_FSA from TH_FSA to DUT_FSA fabric") + if not self.is_ci: + self.wait_for_user_input( + f"Synchronize endpoint from TH Fabric-Sync Bridge to DUT using manufacturer specified mechanism.\n" + f"Use the following parameters:\n" + f"- bridgeDynamicEndpoint: {th_fsa_bridge_th_server_endpoint}\n" + f"If using FabricSync Admin, you may type:\n" + f">>> fabricsync sync-device {th_fsa_bridge_th_server_endpoint}") + else: + self.dut_fsa_stdin.write(f"fabricsync sync-device {th_fsa_bridge_th_server_endpoint}\n") + self.dut_fsa_stdin.flush() + # Wait for the synchronization to complete. + await asyncio.sleep(5) + await asyncio.sleep(10) return From 844b5b9eefa84cb15b4cd8f79648d031eb3168cd Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Wed, 28 Aug 2024 11:40:36 +0200 Subject: [PATCH 15/33] Start another instance of app server --- src/python_testing/TC_MCORE_FS_1_3.py | 72 +++++++++++++-------------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index 919447df954f7a..e3a1e1eb935f24 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -139,7 +139,7 @@ def __init__(self, storageDir, fabricName=None, nodeId=None, vendorId=None, paaT args.append(f"--discriminator={bridgeDiscriminator}") args.append(f"--passcode={bridgePasscode}") - self.admin = ThreadWithStop(args, self._process_admin_output) + self.admin = ThreadWithStop(args, stdout_cb=self._process_admin_output) self.wait_for_text_text = "Connected to Fabric-Bridge" self.admin.start() @@ -160,14 +160,14 @@ def stop(self): class AppServer: - def __init__(self, app, storageDir, port=None, discriminator=None, passcode=None): + def __init__(self, tag, app, storageDir, port=None, discriminator=None, passcode=None): args = [app] args.extend(["--KVS", tempfile.mkstemp(dir=storageDir, prefix="kvs-app-")[1]]) args.extend(['--secured-device-port', str(port)]) args.extend(["--discriminator", str(discriminator)]) args.extend(["--passcode", str(passcode)]) - self.app = ThreadWithStop(args, tag="APP") + self.app = ThreadWithStop(args, tag=tag) self.app.start() def stop(self): @@ -218,12 +218,22 @@ def setup_class(self): self.server_for_dut_discriminator = random.randint(0, 4095) self.server_for_dut_passcode = 20202021 + # Start the TH_SERVER_FOR_DUT_FSA app. + self.server_for_dut = AppServer( + "APP1", + th_server_app, + storageDir=self.storage.name, + port=self.server_for_dut_port, + discriminator=self.server_for_dut_discriminator, + passcode=self.server_for_dut_passcode) + self.server_for_th_port = 5545 self.server_for_th_discriminator = random.randint(0, 4095) self.server_for_th_passcode = 20202021 # Start the TH_SERVER_FOR_TH_FSA app. self.server_for_th = AppServer( + "APP2", th_server_app, storageDir=self.storage.name, port=self.server_for_th_port, @@ -232,6 +242,7 @@ def setup_class(self): def teardown_class(self): self.th_fsa_controller.stop() + self.server_for_dut.stop() self.server_for_th.stop() self.storage.cleanup() super().teardown_class() @@ -310,43 +321,45 @@ async def test_TC_MCORE_FS_1_3(self): # Commissioning - done self.step(0) - th_server_for_th_fsa_th_node_id = 1 - self.print_step(1, "Commissioning TH_SERVER_FOR_TH_FSA to TH fabric") + th_server_for_dut_fsa_th_node_id = 1 + self.print_step(1, "Commissioning TH_SERVER_FOR_DUT_FSA to TH fabric") await self.default_controller.CommissionOnNetwork( - nodeId=th_server_for_th_fsa_th_node_id, - setupPinCode=self.server_for_th_passcode, + nodeId=th_server_for_dut_fsa_th_node_id, + setupPinCode=self.server_for_dut_passcode, filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, - filter=self.server_for_th_discriminator, + filter=self.server_for_dut_discriminator, ) - self.print_step(2, "Verify that TH_SERVER_FOR_TH_FSA does not have a UniqueID") + self.print_step(2, "Verify that TH_SERVER_FOR_DUT_FSA does not have a UniqueID") # FIXME: Sometimes reading the UniqueID fails... await self.read_single_attribute_expect_error( cluster=Clusters.BasicInformation, attribute=Clusters.BasicInformation.Attributes.UniqueID, - node_id=th_server_for_th_fsa_th_node_id, + node_id=th_server_for_dut_fsa_th_node_id, error=Status.UnsupportedAttribute, ) - # Get the list of endpoints on the DUT_FSA_BRIDGE before adding the TH_SERVER_FOR_TH_FSA. + # Get the list of endpoints on the DUT_FSA_BRIDGE before adding the TH_SERVER_FOR_DUT_FSA. dut_fsa_bridge_endpoints = set(await self.read_single_attribute_check_success( cluster=Clusters.Descriptor, attribute=Clusters.Descriptor.Attributes.PartsList, + node_id=self.dut_node_id, endpoint=0, )) - self.print_step(3, "Commissioning TH_SERVER_FOR_TH_FSA to DUT_FSA fabric") + self.print_step(3, "Commissioning TH_SERVER_FOR_DUT_FSA to DUT_FSA fabric") await self.commission_via_commissioner_control( controller_node_id=self.dut_node_id, - device_node_id=th_server_for_th_fsa_th_node_id) + device_node_id=th_server_for_dut_fsa_th_node_id) # Wait for the device to appear on the DUT_FSA_BRIDGE. await asyncio.sleep(2) - # Get the list of endpoints on the DUT_FSA_BRIDGE after adding the TH_SERVER_FOR_TH_FSA. + # Get the list of endpoints on the DUT_FSA_BRIDGE after adding the TH_SERVER_FOR_DUT_FSA. dut_fsa_bridge_endpoints_new = set(await self.read_single_attribute_check_success( cluster=Clusters.Descriptor, attribute=Clusters.Descriptor.Attributes.PartsList, + node_id=self.dut_node_id, endpoint=0, )) @@ -359,7 +372,7 @@ async def test_TC_MCORE_FS_1_3(self): dut_fsa_bridge_th_server_endpoint = list(unique_endpoints_set)[0] dut_fsa_bridge_endpoints = dut_fsa_bridge_endpoints_new - self.print_step(4, "Verify that DUT_FSA created a UniqueID for TH_SERVER_FOR_TH_FSA") + self.print_step(4, "Verify that DUT_FSA created a UniqueID for TH_SERVER_FOR_DUT_FSA") dut_fsa_bridge_th_server_unique_id = await self.read_single_attribute_check_success( cluster=Clusters.BridgedDeviceBasicInformation, attribute=Clusters.BridgedDeviceBasicInformation.Attributes.UniqueID, @@ -385,28 +398,13 @@ async def test_TC_MCORE_FS_1_3(self): endpoint=0, )) - discriminator = random.randint(0, 4095) - self.print_step(6, "Open commissioning window on TH_SERVER_FOR_TH_FSA") - params = await self.default_controller.OpenCommissioningWindow( - nodeid=th_server_for_th_fsa_th_node_id, - option=self.default_controller.CommissioningWindowPasscode.kTokenWithRandomPin, - discriminator=discriminator, - iteration=10000, - timeout=600) - - # FIXME: Sometimes the commissioning does not work with the error: - # > Failed to verify peer's MAC. This can happen when setup code is incorrect. - # However, the setup code is correct... so we need to investigate why this is happening. - # The sleep(2) seems to help, though. - await asyncio.sleep(2) - th_server_for_th_fsa_th_fsa_node_id = 3 - self.print_step(7, "Commissioning TH_SERVER_FOR_TH_FSA to TH_FSA") + self.print_step(6, "Commissioning TH_SERVER_FOR_TH_FSA to TH_FSA") self.th_fsa_controller.CommissionOnNetwork( nodeId=th_server_for_th_fsa_th_fsa_node_id, - setupPinCode=params.setupPinCode, + setupPinCode=self.server_for_th_passcode, filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, - filter=discriminator, + filter=self.server_for_th_discriminator, ) # Wait some time, so the dynamic endpoint will appear on the TH_FSA_BRIDGE. @@ -428,7 +426,7 @@ async def test_TC_MCORE_FS_1_3(self): asserts.assert_equal(len(unique_endpoints_set), 1, "Expected only one new endpoint") th_fsa_bridge_th_server_endpoint = list(unique_endpoints_set)[0] - self.print_step(8, "Verify that TH_FSA created a UniqueID for TH_SERVER_FOR_TH_FSA") + self.print_step(7, "Verify that TH_FSA created a UniqueID for TH_SERVER_FOR_TH_FSA") th_fsa_bridge_th_server_unique_id = await self.read_single_attribute_check_success( cluster=Clusters.BridgedDeviceBasicInformation, attribute=Clusters.BridgedDeviceBasicInformation.Attributes.UniqueID, @@ -443,7 +441,7 @@ async def test_TC_MCORE_FS_1_3(self): "UniqueID on DUT_FSA_BRIDGE and TH_FSA_BRIDGE should be different") discriminator = random.randint(0, 4095) - self.print_step(9, "Open commissioning window on TH_FSA_BRIDGE") + self.print_step(8, "Open commissioning window on TH_FSA_BRIDGE") params = await self.default_controller.OpenCommissioningWindow( nodeid=th_fsa_bridge_th_node_id, option=self.default_controller.CommissioningWindowPasscode.kTokenWithRandomPin, @@ -469,7 +467,7 @@ async def test_TC_MCORE_FS_1_3(self): # Wait for the commissioning to complete. await asyncio.sleep(5) - self.print_step(9, "Synchronize TH_SERVER_FOR_TH_FSA from TH_FSA to DUT_FSA fabric") + self.print_step(10, "Synchronize TH_SERVER_FOR_TH_FSA from TH_FSA to DUT_FSA fabric") if not self.is_ci: self.wait_for_user_input( f"Synchronize endpoint from TH Fabric-Sync Bridge to DUT using manufacturer specified mechanism.\n" @@ -483,7 +481,7 @@ async def test_TC_MCORE_FS_1_3(self): # Wait for the synchronization to complete. await asyncio.sleep(5) - await asyncio.sleep(10) + await asyncio.sleep(1000) return From 262e08055a014c077c062bdeeda711b845887eb0 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Wed, 28 Aug 2024 13:02:08 +0200 Subject: [PATCH 16/33] Test if unique ID was synced --- src/python_testing/TC_MCORE_FS_1_3.py | 51 ++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index e3a1e1eb935f24..d40d635ef9e1ab 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -176,6 +176,11 @@ def stop(self): class TC_MCORE_FS_1_3(MatterBaseTest): + @property + def default_timeout(self) -> int: + # This test has some manual steps, so we need a longer timeout. + return 200 + def setup_class(self): super().setup_class() @@ -363,23 +368,23 @@ async def test_TC_MCORE_FS_1_3(self): endpoint=0, )) - # Get the endpoint number for just added TH_SERVER_FOR_TH_FSA. + # Get the endpoint number for just added TH_SERVER_FOR_DUT_FSA. logging.info("Endpoints on DUT_FSA_BRIDGE: old=%s, new=%s", dut_fsa_bridge_endpoints, dut_fsa_bridge_endpoints_new) asserts.assert_true(dut_fsa_bridge_endpoints_new.issuperset(dut_fsa_bridge_endpoints), "Expected only new endpoints to be added") unique_endpoints_set = dut_fsa_bridge_endpoints_new - dut_fsa_bridge_endpoints asserts.assert_equal(len(unique_endpoints_set), 1, "Expected only one new endpoint on DUT_FSA") - dut_fsa_bridge_th_server_endpoint = list(unique_endpoints_set)[0] + dut_fsa_bridge_dut_server_endpoint = list(unique_endpoints_set)[0] dut_fsa_bridge_endpoints = dut_fsa_bridge_endpoints_new self.print_step(4, "Verify that DUT_FSA created a UniqueID for TH_SERVER_FOR_DUT_FSA") - dut_fsa_bridge_th_server_unique_id = await self.read_single_attribute_check_success( + dut_fsa_bridge_dut_server_unique_id = await self.read_single_attribute_check_success( cluster=Clusters.BridgedDeviceBasicInformation, attribute=Clusters.BridgedDeviceBasicInformation.Attributes.UniqueID, - endpoint=dut_fsa_bridge_th_server_endpoint) - asserts.assert_true(type_matches(dut_fsa_bridge_th_server_unique_id, str), "UniqueID should be a string") - asserts.assert_true(dut_fsa_bridge_th_server_unique_id, "UniqueID should not be an empty string") - logging.info("TH_SERVER_FOR_TH_FSA on TH_SERVER_BRIDGE UniqueID: %s", dut_fsa_bridge_th_server_unique_id) + endpoint=dut_fsa_bridge_dut_server_endpoint) + asserts.assert_true(type_matches(dut_fsa_bridge_dut_server_unique_id, str), "UniqueID should be a string") + asserts.assert_true(dut_fsa_bridge_dut_server_unique_id, "UniqueID should not be an empty string") + logging.info("TH_SERVER_FOR_DUT_FSA on DUT_SERVER_BRIDGE UniqueID: %s", dut_fsa_bridge_dut_server_unique_id) th_fsa_bridge_th_node_id = 2 self.print_step(5, "Commissioning TH_FSA_BRIDGE to TH fabric") @@ -437,7 +442,7 @@ async def test_TC_MCORE_FS_1_3(self): logging.info("TH_SERVER_FOR_TH_FSA on TH_SERVER_BRIDGE UniqueID: %s", th_fsa_bridge_th_server_unique_id) # Make sure that the UniqueID on the TH_FSA_BRIDGE is different from the one on the DUT_FSA_BRIDGE. - asserts.assert_not_equal(dut_fsa_bridge_th_server_unique_id, th_fsa_bridge_th_server_unique_id, + asserts.assert_not_equal(dut_fsa_bridge_dut_server_unique_id, th_fsa_bridge_th_server_unique_id, "UniqueID on DUT_FSA_BRIDGE and TH_FSA_BRIDGE should be different") discriminator = random.randint(0, 4095) @@ -481,6 +486,36 @@ async def test_TC_MCORE_FS_1_3(self): # Wait for the synchronization to complete. await asyncio.sleep(5) + # Get the list of endpoints on the DUT_FSA_BRIDGE after synchronization + dut_fsa_bridge_endpoints_new = set(await self.read_single_attribute_check_success( + cluster=Clusters.Descriptor, + attribute=Clusters.Descriptor.Attributes.PartsList, + node_id=self.dut_node_id, + endpoint=0, + )) + + # Get the endpoint number for just synced TH_SERVER_FOR_TH_FSA. + logging.info("Endpoints on DUT_FSA_BRIDGE: old=%s, new=%s", dut_fsa_bridge_endpoints, dut_fsa_bridge_endpoints_new) + asserts.assert_true(dut_fsa_bridge_endpoints_new.issuperset(dut_fsa_bridge_endpoints), + "Expected only new endpoints to be added") + unique_endpoints_set = dut_fsa_bridge_endpoints_new - dut_fsa_bridge_endpoints + asserts.assert_equal(len(unique_endpoints_set), 1, "Expected only one new endpoint on DUT_FSA") + dut_fsa_bridge_th_server_endpoint = list(unique_endpoints_set)[0] + dut_fsa_bridge_endpoints = dut_fsa_bridge_endpoints_new + + self.print_step(11, "Verify that DUT_FSA copied the TH_SERVER_FOR_TH_FSA UniqueID from TH_FSA") + dut_fsa_bridge_th_server_unique_id = await self.read_single_attribute_check_success( + cluster=Clusters.BridgedDeviceBasicInformation, + attribute=Clusters.BridgedDeviceBasicInformation.Attributes.UniqueID, + endpoint=dut_fsa_bridge_th_server_endpoint) + asserts.assert_true(type_matches(dut_fsa_bridge_th_server_unique_id, str), "UniqueID should be a string") + asserts.assert_true(dut_fsa_bridge_th_server_unique_id, "UniqueID should not be an empty string") + logging.info("TH_SERVER_FOR_TH_FSA on DUT_SERVER_BRIDGE UniqueID: %s", dut_fsa_bridge_th_server_unique_id) + + # Make sure that the UniqueID on the DUT_FSA_BRIDGE is the same as the one on the DUT_FSA_BRIDGE. + asserts.assert_equal(dut_fsa_bridge_th_server_unique_id, th_fsa_bridge_th_server_unique_id, + "UniqueID on DUT_FSA_BRIDGE and TH_FSA_BRIDGE should be the same") + await asyncio.sleep(1000) return From 804ef519b4d00b7a4c1e3ae4a66f818270289709 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Wed, 28 Aug 2024 13:38:44 +0200 Subject: [PATCH 17/33] Allow customization for fabric-sync app components --- src/python_testing/TC_MCORE_FS_1_3.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index d40d635ef9e1ab..2bd4745916a87d 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -25,9 +25,9 @@ # === BEGIN CI TEST ARGUMENTS === # test-runner-runs: run1 # test-runner-run/run1/app: examples/fabric-admin/scripts/fabric-sync-app.py -# test-runner-run/run1/app-args: --stdin-pipe=dut-fsa/stdin --storage-dir=dut-fsa --discriminator=1234 +# test-runner-run/run1/app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa/stdin --storage-dir=dut-fsa --discriminator=1234 # test-runner-run/run1/factoryreset: True -# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --string-arg th_server_app_path:${LIGHTING_APP_NO_UNIQUE_ID} dut_fsa_stdin_pipe:dut-fsa/stdin +# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --string-arg th_fsa_admin_path:${FABRIC_ADMIN_APP} th_fsa_bridge_path:${FABRIC_BRIDGE_APP} th_server_app_path:${LIGHTING_APP_NO_UNIQUE_ID} dut_fsa_stdin_pipe:dut-fsa/stdin # test-runner-run/run1/script-start-delay: 10 # test-runner-run/run1/quiet: false # === END CI TEST ARGUMENTS === @@ -103,6 +103,8 @@ def stop(self): class FabricSyncApp: APP_PATH = "examples/fabric-admin/scripts/fabric-sync-app.py" + FABRIC_ADMIN_PATH = "out/linux-x64-fabric-admin-rpc/fabric-admin" + FABRIC_BRIDGE_PATH = "out/linux-x64-fabric-bridge-rpc/fabric-bridge-app" def _process_admin_output(self, line): if self.wait_for_text_text is not None and self.wait_for_text_text in line: @@ -121,6 +123,8 @@ def __init__(self, storageDir, fabricName=None, nodeId=None, vendorId=None, paaT self.wait_for_text_text = None args = [self.APP_PATH] + args.append(f"--app-admin={self.FABRIC_ADMIN_PATH}") + args.append(f"--app-bridge={self.FABRIC_BRIDGE_PATH}") # Override default ports, so it will be possible to run # our TH_FSA alongside the DUT_FSA during CI testing. args.append("--app-admin-rpc-port=44000") @@ -189,6 +193,12 @@ def setup_class(self): FabricSyncApp.APP_PATH = self.user_params.get("th_fsa_app_path", FabricSyncApp.APP_PATH) if not os.path.exists(FabricSyncApp.APP_PATH): asserts.fail("This test requires a TH_FSA app. Specify app path with --string-arg th_fsa_app_path:") + FabricSyncApp.FABRIC_ADMIN_PATH = self.user_params.get("th_fsa_admin_path", FabricSyncApp.FABRIC_ADMIN_PATH) + if not os.path.exists(FabricSyncApp.FABRIC_ADMIN_PATH): + asserts.fail("This test requires a TH_FSA_ADMIN app. Specify app path with --string-arg th_fsa_admin_path:") + FabricSyncApp.FABRIC_BRIDGE_PATH = self.user_params.get("th_fsa_bridge_path", FabricSyncApp.FABRIC_BRIDGE_PATH) + if not os.path.exists(FabricSyncApp.FABRIC_BRIDGE_PATH): + asserts.fail("This test requires a TH_FSA_BRIDGE app. Specify app path with --string-arg th_fsa_bridge_path:") # Get the path to the TH_SERVER app from the user params. th_server_app = self.user_params.get("th_server_app_path", None) From 61c3e496963eaee893e1b8c39bbfa2e09b45d411 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Wed, 28 Aug 2024 15:10:42 +0200 Subject: [PATCH 18/33] Final cleanup --- src/python_testing/TC_MCORE_FS_1_3.py | 59 +-------------------------- 1 file changed, 1 insertion(+), 58 deletions(-) diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index 2bd4745916a87d..4c641920658265 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -40,7 +40,6 @@ import sys import tempfile import threading -import uuid import chip.clusters as Clusters from chip import ChipDeviceCtrl @@ -423,7 +422,7 @@ async def test_TC_MCORE_FS_1_3(self): ) # Wait some time, so the dynamic endpoint will appear on the TH_FSA_BRIDGE. - await asyncio.sleep(2) + await asyncio.sleep(5) # Get the list of endpoints on the TH_FSA_BRIDGE after adding the TH_SERVER_FOR_TH_FSA. th_fsa_bridge_endpoints_new = set(await self.read_single_attribute_check_success( @@ -526,62 +525,6 @@ async def test_TC_MCORE_FS_1_3(self): asserts.assert_equal(dut_fsa_bridge_th_server_unique_id, th_fsa_bridge_th_server_unique_id, "UniqueID on DUT_FSA_BRIDGE and TH_FSA_BRIDGE should be the same") - await asyncio.sleep(1000) - - return - - # These steps are not explicitly in step 1, but they help identify the dynamically added endpoint in step 1. - root_node_endpoint = 0 - root_part_list = await self.read_single_attribute_check_success(cluster=Clusters.Descriptor, attribute=Clusters.Descriptor.Attributes.PartsList, endpoint=root_node_endpoint) - set_of_endpoints_before_adding_device = set(root_part_list) - - kvs = f'kvs_{str(uuid.uuid4())}' - device_info = "for DUT ecosystem" - await self.create_device_and_commission_to_th_fabric(kvs, self.device_for_dut_eco_port, self.device_for_dut_eco_nodeid, device_info) - - self.device_for_dut_eco_kvs = kvs - read_result = await self.TH_server_controller.ReadAttribute(self.device_for_dut_eco_nodeid, [(root_node_endpoint, Clusters.BasicInformation.Attributes.UniqueID)]) - result = read_result[root_node_endpoint][Clusters.BasicInformation][Clusters.BasicInformation.Attributes.UniqueID] - asserts.assert_true(type_matches(result, Clusters.Attribute.ValueDecodeFailure), "We were expecting a value decode failure") - asserts.assert_equal(result.Reason.status, Status.UnsupportedAttribute, "Incorrect error returned from reading UniqueID") - - params = await self.openCommissioningWindow(dev_ctrl=self.TH_server_controller, node_id=self.device_for_dut_eco_nodeid) - - self.wait_for_user_input( - prompt_msg=f"Using the DUT vendor's provided interface, commission the device using the following parameters:\n" - f"- discriminator: {params.randomDiscriminator}\n" - f"- setupPinCode: {params.commissioningParameters.setupPinCode}\n" - f"- setupQRCode: {params.commissioningParameters.setupQRCode}\n" - f"- setupManualcode: {params.commissioningParameters.setupManualCode}\n" - f"If using FabricSync Admin, you may type:\n" - f">>> pairing onnetwork {params.commissioningParameters.setupPinCode}") - - root_part_list = await self.read_single_attribute_check_success(cluster=Clusters.Descriptor, attribute=Clusters.Descriptor.Attributes.PartsList, endpoint=root_node_endpoint) - set_of_endpoints_after_adding_device = set(root_part_list) - - asserts.assert_true(set_of_endpoints_after_adding_device.issuperset( - set_of_endpoints_before_adding_device), "Expected only new endpoints to be added") - unique_endpoints_set = set_of_endpoints_after_adding_device - set_of_endpoints_before_adding_device - asserts.assert_equal(len(unique_endpoints_set), 1, "Expected only one new endpoint") - newly_added_endpoint = list(unique_endpoints_set)[0] - - th_sed_dut_unique_id = await self.read_single_attribute_check_success(cluster=Clusters.BridgedDeviceBasicInformation, attribute=Clusters.BridgedDeviceBasicInformation.Attributes.UniqueID, endpoint=newly_added_endpoint) - asserts.assert_true(type_matches(th_sed_dut_unique_id, str), "UniqueID should be a string") - asserts.assert_true(th_sed_dut_unique_id, "UniqueID should not be an empty string") - - self.step(2) - kvs = f'kvs_{str(uuid.uuid4())}' - device_info = "for TH_FSA ecosystem" - await self.create_device_and_commission_to_th_fabric(kvs, self.device_for_th_eco_port, self.device_for_th_eco_nodeid, device_info) - self.device_for_th_eco_kvs = kvs - # TODO(https://github.com/CHIP-Specifications/chip-test-plans/issues/4375) During setup we need to create the TH_FSA device - # where we would commission device created in create_device_and_commission_to_th_fabric to be commissioned into TH_FSA. - - # TODO(https://github.com/CHIP-Specifications/chip-test-plans/issues/4375) Because we cannot create a TH_FSA and there is - # no way to mock it the following 2 test steps are skipped for now. - self.skip_step(3) - self.skip_step(4) - if __name__ == "__main__": default_matter_test_main() From bf40da45b3683f65c227113f2d716dcbd32afb85 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Wed, 28 Aug 2024 16:40:59 +0200 Subject: [PATCH 19/33] Split test case into two test cases --- src/python_testing/TC_MCORE_FS_1_3.py | 292 ++------------ src/python_testing/TC_MCORE_FS_1_4.py | 530 ++++++++++++++++++++++++++ 2 files changed, 556 insertions(+), 266 deletions(-) create mode 100644 src/python_testing/TC_MCORE_FS_1_4.py diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index 4c641920658265..4a5cd4a1a738de 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -27,7 +27,7 @@ # test-runner-run/run1/app: examples/fabric-admin/scripts/fabric-sync-app.py # test-runner-run/run1/app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa/stdin --storage-dir=dut-fsa --discriminator=1234 # test-runner-run/run1/factoryreset: True -# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --string-arg th_fsa_admin_path:${FABRIC_ADMIN_APP} th_fsa_bridge_path:${FABRIC_BRIDGE_APP} th_server_app_path:${LIGHTING_APP_NO_UNIQUE_ID} dut_fsa_stdin_pipe:dut-fsa/stdin +# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --string-arg th_server_app_path:${LIGHTING_APP_NO_UNIQUE_ID} # test-runner-run/run1/script-start-delay: 10 # test-runner-run/run1/quiet: false # === END CI TEST ARGUMENTS === @@ -99,78 +99,16 @@ def stop(self): self.join() -class FabricSyncApp: - - APP_PATH = "examples/fabric-admin/scripts/fabric-sync-app.py" - FABRIC_ADMIN_PATH = "out/linux-x64-fabric-admin-rpc/fabric-admin" - FABRIC_BRIDGE_PATH = "out/linux-x64-fabric-bridge-rpc/fabric-bridge-app" - - def _process_admin_output(self, line): - if self.wait_for_text_text is not None and self.wait_for_text_text in line: - self.wait_for_text_event.set() - - def wait_for_text(self, timeout=30): - if not self.wait_for_text_event.wait(timeout=timeout): - raise Exception(f"Timeout waiting for text: {self.wait_for_text_text}") - self.wait_for_text_event.clear() - self.wait_for_text_text = None - - def __init__(self, storageDir, fabricName=None, nodeId=None, vendorId=None, paaTrustStorePath=None, - bridgePort=None, bridgeDiscriminator=None, bridgePasscode=None): - - self.wait_for_text_event = threading.Event() - self.wait_for_text_text = None - - args = [self.APP_PATH] - args.append(f"--app-admin={self.FABRIC_ADMIN_PATH}") - args.append(f"--app-bridge={self.FABRIC_BRIDGE_PATH}") - # Override default ports, so it will be possible to run - # our TH_FSA alongside the DUT_FSA during CI testing. - args.append("--app-admin-rpc-port=44000") - args.append("--app-bridge-rpc-port=44001") - # Keep the storage directory in a temporary location. - args.append(f"--storage-dir={storageDir}") - # FIXME: Passing custom PAA store breaks something - # if paaTrustStorePath is not None: - # args.append(f"--paa-trust-store-path={paaTrustStorePath}") - if fabricName is not None: - args.append(f"--commissioner-name={fabricName}") - if nodeId is not None: - args.append(f"--commissioner-node-id={nodeId}") - args.append(f"--commissioner-vendor-id={vendorId}") - args.append(f"--secured-device-port={bridgePort}") - args.append(f"--discriminator={bridgeDiscriminator}") - args.append(f"--passcode={bridgePasscode}") - - self.admin = ThreadWithStop(args, stdout_cb=self._process_admin_output) - self.wait_for_text_text = "Connected to Fabric-Bridge" - self.admin.start() - - # Wait for the bridge to connect to the admin. - self.wait_for_text() - - def CommissionOnNetwork(self, nodeId, setupPinCode=None, filterType=None, filter=None): - self.wait_for_text_text = f"Commissioning complete for node ID 0x{nodeId:016x}: success" - # Send the commissioning command to the admin. - self.admin.p.stdin.write(f"pairing onnetwork {nodeId} {setupPinCode}\n") - self.admin.p.stdin.flush() - # Wait for success message. - self.wait_for_text() - - def stop(self): - self.admin.stop() - - class AppServer: - def __init__(self, tag, app, storageDir, port=None, discriminator=None, passcode=None): + def __init__(self, app, storageDir, port=None, discriminator=None, passcode=None): args = [app] args.extend(["--KVS", tempfile.mkstemp(dir=storageDir, prefix="kvs-app-")[1]]) args.extend(['--secured-device-port', str(port)]) args.extend(["--discriminator", str(discriminator)]) args.extend(["--passcode", str(passcode)]) - self.app = ThreadWithStop(args, tag=tag) + self.app = ThreadWithStop(args, tag="APP") self.app.start() def stop(self): @@ -187,18 +125,6 @@ def default_timeout(self) -> int: def setup_class(self): super().setup_class() - # Get the path to the TH_FSA (fabric-admin and fabric-bridge) app from - # the user params or use the default path. - FabricSyncApp.APP_PATH = self.user_params.get("th_fsa_app_path", FabricSyncApp.APP_PATH) - if not os.path.exists(FabricSyncApp.APP_PATH): - asserts.fail("This test requires a TH_FSA app. Specify app path with --string-arg th_fsa_app_path:") - FabricSyncApp.FABRIC_ADMIN_PATH = self.user_params.get("th_fsa_admin_path", FabricSyncApp.FABRIC_ADMIN_PATH) - if not os.path.exists(FabricSyncApp.FABRIC_ADMIN_PATH): - asserts.fail("This test requires a TH_FSA_ADMIN app. Specify app path with --string-arg th_fsa_admin_path:") - FabricSyncApp.FABRIC_BRIDGE_PATH = self.user_params.get("th_fsa_bridge_path", FabricSyncApp.FABRIC_BRIDGE_PATH) - if not os.path.exists(FabricSyncApp.FABRIC_BRIDGE_PATH): - asserts.fail("This test requires a TH_FSA_BRIDGE app. Specify app path with --string-arg th_fsa_bridge_path:") - # Get the path to the TH_SERVER app from the user params. th_server_app = self.user_params.get("th_server_app_path", None) if not th_server_app: @@ -210,54 +136,20 @@ def setup_class(self): self.storage = tempfile.TemporaryDirectory(prefix=self.__class__.__name__) logging.info("Temporary storage directory: %s", self.storage.name) - self.th_fsa_bridge_address = "::1" - self.th_fsa_bridge_port = 5543 - self.th_fsa_bridge_discriminator = random.randint(0, 4095) - self.th_fsa_bridge_passcode = 20202021 + self.th_server_port = 5544 + self.th_server_discriminator = random.randint(0, 4095) + self.th_server_passcode = 20202021 - self.th_fsa_controller = FabricSyncApp( - storageDir=self.storage.name, - paaTrustStorePath=self.matter_test_config.paa_trust_store_path, - bridgePort=self.th_fsa_bridge_port, - bridgeDiscriminator=self.th_fsa_bridge_discriminator, - bridgePasscode=self.th_fsa_bridge_passcode, - vendorId=0xFFF1) - - # Get the named pipe path for the DUT_FSA app input from the user params. - dut_fsa_stdin_pipe = self.user_params.get("dut_fsa_stdin_pipe", None) - if dut_fsa_stdin_pipe is not None: - self.dut_fsa_stdin = open(dut_fsa_stdin_pipe, "w") - - self.server_for_dut_port = 5544 - self.server_for_dut_discriminator = random.randint(0, 4095) - self.server_for_dut_passcode = 20202021 - - # Start the TH_SERVER_FOR_DUT_FSA app. - self.server_for_dut = AppServer( - "APP1", + # Start the TH_SERVER app. + self.th_server = AppServer( th_server_app, storageDir=self.storage.name, - port=self.server_for_dut_port, - discriminator=self.server_for_dut_discriminator, - passcode=self.server_for_dut_passcode) - - self.server_for_th_port = 5545 - self.server_for_th_discriminator = random.randint(0, 4095) - self.server_for_th_passcode = 20202021 - - # Start the TH_SERVER_FOR_TH_FSA app. - self.server_for_th = AppServer( - "APP2", - th_server_app, - storageDir=self.storage.name, - port=self.server_for_th_port, - discriminator=self.server_for_th_discriminator, - passcode=self.server_for_th_passcode) + port=self.th_server_port, + discriminator=self.th_server_discriminator, + passcode=self.th_server_passcode) def teardown_class(self): - self.th_fsa_controller.stop() - self.server_for_dut.stop() - self.server_for_th.stop() + self.th_server.stop() self.storage.cleanup() super().teardown_class() @@ -330,30 +222,29 @@ async def commission_via_commissioner_control(self, controller_node_id: int, dev @async_test_body async def test_TC_MCORE_FS_1_3(self): self.is_ci = self.check_pics('PICS_SDK_CI_ONLY') - self.is_ci = True # Commissioning - done self.step(0) - th_server_for_dut_fsa_th_node_id = 1 - self.print_step(1, "Commissioning TH_SERVER_FOR_DUT_FSA to TH fabric") + th_server_th_node_id = 1 + self.print_step(1, "Commissioning TH_SERVER to TH fabric") await self.default_controller.CommissionOnNetwork( - nodeId=th_server_for_dut_fsa_th_node_id, - setupPinCode=self.server_for_dut_passcode, + nodeId=th_server_th_node_id, + setupPinCode=self.th_server_passcode, filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, - filter=self.server_for_dut_discriminator, + filter=self.th_server_discriminator, ) - self.print_step(2, "Verify that TH_SERVER_FOR_DUT_FSA does not have a UniqueID") + self.print_step(2, "Verify that TH_SERVER does not have a UniqueID") # FIXME: Sometimes reading the UniqueID fails... await self.read_single_attribute_expect_error( cluster=Clusters.BasicInformation, attribute=Clusters.BasicInformation.Attributes.UniqueID, - node_id=th_server_for_dut_fsa_th_node_id, + node_id=th_server_th_node_id, error=Status.UnsupportedAttribute, ) - # Get the list of endpoints on the DUT_FSA_BRIDGE before adding the TH_SERVER_FOR_DUT_FSA. + # Get the list of endpoints on the DUT_FSA_BRIDGE before adding the TH_SERVER. dut_fsa_bridge_endpoints = set(await self.read_single_attribute_check_success( cluster=Clusters.Descriptor, attribute=Clusters.Descriptor.Attributes.PartsList, @@ -361,15 +252,15 @@ async def test_TC_MCORE_FS_1_3(self): endpoint=0, )) - self.print_step(3, "Commissioning TH_SERVER_FOR_DUT_FSA to DUT_FSA fabric") + self.print_step(3, "Commissioning TH_SERVER to DUT_FSA fabric") await self.commission_via_commissioner_control( controller_node_id=self.dut_node_id, - device_node_id=th_server_for_dut_fsa_th_node_id) + device_node_id=th_server_th_node_id) # Wait for the device to appear on the DUT_FSA_BRIDGE. await asyncio.sleep(2) - # Get the list of endpoints on the DUT_FSA_BRIDGE after adding the TH_SERVER_FOR_DUT_FSA. + # Get the list of endpoints on the DUT_FSA_BRIDGE after adding the TH_SERVER. dut_fsa_bridge_endpoints_new = set(await self.read_single_attribute_check_success( cluster=Clusters.Descriptor, attribute=Clusters.Descriptor.Attributes.PartsList, @@ -377,153 +268,22 @@ async def test_TC_MCORE_FS_1_3(self): endpoint=0, )) - # Get the endpoint number for just added TH_SERVER_FOR_DUT_FSA. - logging.info("Endpoints on DUT_FSA_BRIDGE: old=%s, new=%s", dut_fsa_bridge_endpoints, dut_fsa_bridge_endpoints_new) - asserts.assert_true(dut_fsa_bridge_endpoints_new.issuperset(dut_fsa_bridge_endpoints), - "Expected only new endpoints to be added") - unique_endpoints_set = dut_fsa_bridge_endpoints_new - dut_fsa_bridge_endpoints - asserts.assert_equal(len(unique_endpoints_set), 1, "Expected only one new endpoint on DUT_FSA") - dut_fsa_bridge_dut_server_endpoint = list(unique_endpoints_set)[0] - dut_fsa_bridge_endpoints = dut_fsa_bridge_endpoints_new - - self.print_step(4, "Verify that DUT_FSA created a UniqueID for TH_SERVER_FOR_DUT_FSA") - dut_fsa_bridge_dut_server_unique_id = await self.read_single_attribute_check_success( - cluster=Clusters.BridgedDeviceBasicInformation, - attribute=Clusters.BridgedDeviceBasicInformation.Attributes.UniqueID, - endpoint=dut_fsa_bridge_dut_server_endpoint) - asserts.assert_true(type_matches(dut_fsa_bridge_dut_server_unique_id, str), "UniqueID should be a string") - asserts.assert_true(dut_fsa_bridge_dut_server_unique_id, "UniqueID should not be an empty string") - logging.info("TH_SERVER_FOR_DUT_FSA on DUT_SERVER_BRIDGE UniqueID: %s", dut_fsa_bridge_dut_server_unique_id) - - th_fsa_bridge_th_node_id = 2 - self.print_step(5, "Commissioning TH_FSA_BRIDGE to TH fabric") - await self.default_controller.CommissionOnNetwork( - nodeId=th_fsa_bridge_th_node_id, - setupPinCode=self.th_fsa_bridge_passcode, - filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, - filter=self.th_fsa_bridge_discriminator, - ) - - # Get the list of endpoints on the TH_FSA_BRIDGE before adding the TH_SERVER_FOR_TH_FSA. - th_fsa_bridge_endpoints = set(await self.read_single_attribute_check_success( - cluster=Clusters.Descriptor, - attribute=Clusters.Descriptor.Attributes.PartsList, - node_id=th_fsa_bridge_th_node_id, - endpoint=0, - )) - - th_server_for_th_fsa_th_fsa_node_id = 3 - self.print_step(6, "Commissioning TH_SERVER_FOR_TH_FSA to TH_FSA") - self.th_fsa_controller.CommissionOnNetwork( - nodeId=th_server_for_th_fsa_th_fsa_node_id, - setupPinCode=self.server_for_th_passcode, - filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, - filter=self.server_for_th_discriminator, - ) - - # Wait some time, so the dynamic endpoint will appear on the TH_FSA_BRIDGE. - await asyncio.sleep(5) - - # Get the list of endpoints on the TH_FSA_BRIDGE after adding the TH_SERVER_FOR_TH_FSA. - th_fsa_bridge_endpoints_new = set(await self.read_single_attribute_check_success( - cluster=Clusters.Descriptor, - attribute=Clusters.Descriptor.Attributes.PartsList, - node_id=th_fsa_bridge_th_node_id, - endpoint=0, - )) - - # Get the endpoint number for just added TH_SERVER_FOR_TH_FSA. - logging.info("Endpoints on TH_FSA_BRIDGE: old=%s, new=%s", th_fsa_bridge_endpoints, th_fsa_bridge_endpoints_new) - asserts.assert_true(th_fsa_bridge_endpoints_new.issuperset(th_fsa_bridge_endpoints), - "Expected only new endpoints to be added") - unique_endpoints_set = th_fsa_bridge_endpoints_new - th_fsa_bridge_endpoints - asserts.assert_equal(len(unique_endpoints_set), 1, "Expected only one new endpoint") - th_fsa_bridge_th_server_endpoint = list(unique_endpoints_set)[0] - - self.print_step(7, "Verify that TH_FSA created a UniqueID for TH_SERVER_FOR_TH_FSA") - th_fsa_bridge_th_server_unique_id = await self.read_single_attribute_check_success( - cluster=Clusters.BridgedDeviceBasicInformation, - attribute=Clusters.BridgedDeviceBasicInformation.Attributes.UniqueID, - node_id=th_fsa_bridge_th_node_id, - endpoint=th_fsa_bridge_th_server_endpoint) - asserts.assert_true(type_matches(th_fsa_bridge_th_server_unique_id, str), "UniqueID should be a string") - asserts.assert_true(th_fsa_bridge_th_server_unique_id, "UniqueID should not be an empty string") - logging.info("TH_SERVER_FOR_TH_FSA on TH_SERVER_BRIDGE UniqueID: %s", th_fsa_bridge_th_server_unique_id) - - # Make sure that the UniqueID on the TH_FSA_BRIDGE is different from the one on the DUT_FSA_BRIDGE. - asserts.assert_not_equal(dut_fsa_bridge_dut_server_unique_id, th_fsa_bridge_th_server_unique_id, - "UniqueID on DUT_FSA_BRIDGE and TH_FSA_BRIDGE should be different") - - discriminator = random.randint(0, 4095) - self.print_step(8, "Open commissioning window on TH_FSA_BRIDGE") - params = await self.default_controller.OpenCommissioningWindow( - nodeid=th_fsa_bridge_th_node_id, - option=self.default_controller.CommissioningWindowPasscode.kTokenWithRandomPin, - discriminator=discriminator, - iteration=10000, - timeout=600) - - self.print_step(9, "Commissioning TH_FSA_BRIDGE to DUT_FSA fabric") - if not self.is_ci: - self.wait_for_user_input( - f"Commission TH Fabric-Sync Bridge on DUT using manufacturer specified mechanism.\n" - f"Use the following parameters:\n" - f"- discriminator: {discriminator}\n" - f"- setupPinCode: {params.setupPinCode}\n" - f"- setupQRCode: {params.setupQRCode}\n" - f"- setupManualCode: {params.setupManualCode}\n" - f"If using FabricSync Admin, you may type:\n" - f">>> fabricsync add-bridge {params.setupPinCode} {self.th_fsa_bridge_port}") - else: - self.dut_fsa_stdin.write( - f"fabricsync add-bridge 10 {params.setupPinCode} {self.th_fsa_bridge_address} {self.th_fsa_bridge_port}\n") - self.dut_fsa_stdin.flush() - # Wait for the commissioning to complete. - await asyncio.sleep(5) - - self.print_step(10, "Synchronize TH_SERVER_FOR_TH_FSA from TH_FSA to DUT_FSA fabric") - if not self.is_ci: - self.wait_for_user_input( - f"Synchronize endpoint from TH Fabric-Sync Bridge to DUT using manufacturer specified mechanism.\n" - f"Use the following parameters:\n" - f"- bridgeDynamicEndpoint: {th_fsa_bridge_th_server_endpoint}\n" - f"If using FabricSync Admin, you may type:\n" - f">>> fabricsync sync-device {th_fsa_bridge_th_server_endpoint}") - else: - self.dut_fsa_stdin.write(f"fabricsync sync-device {th_fsa_bridge_th_server_endpoint}\n") - self.dut_fsa_stdin.flush() - # Wait for the synchronization to complete. - await asyncio.sleep(5) - - # Get the list of endpoints on the DUT_FSA_BRIDGE after synchronization - dut_fsa_bridge_endpoints_new = set(await self.read_single_attribute_check_success( - cluster=Clusters.Descriptor, - attribute=Clusters.Descriptor.Attributes.PartsList, - node_id=self.dut_node_id, - endpoint=0, - )) - - # Get the endpoint number for just synced TH_SERVER_FOR_TH_FSA. + # Get the endpoint number for just added TH_SERVER. logging.info("Endpoints on DUT_FSA_BRIDGE: old=%s, new=%s", dut_fsa_bridge_endpoints, dut_fsa_bridge_endpoints_new) asserts.assert_true(dut_fsa_bridge_endpoints_new.issuperset(dut_fsa_bridge_endpoints), "Expected only new endpoints to be added") unique_endpoints_set = dut_fsa_bridge_endpoints_new - dut_fsa_bridge_endpoints asserts.assert_equal(len(unique_endpoints_set), 1, "Expected only one new endpoint on DUT_FSA") dut_fsa_bridge_th_server_endpoint = list(unique_endpoints_set)[0] - dut_fsa_bridge_endpoints = dut_fsa_bridge_endpoints_new - self.print_step(11, "Verify that DUT_FSA copied the TH_SERVER_FOR_TH_FSA UniqueID from TH_FSA") + self.print_step(4, "Verify that DUT_FSA created a UniqueID for TH_SERVER") dut_fsa_bridge_th_server_unique_id = await self.read_single_attribute_check_success( cluster=Clusters.BridgedDeviceBasicInformation, attribute=Clusters.BridgedDeviceBasicInformation.Attributes.UniqueID, endpoint=dut_fsa_bridge_th_server_endpoint) asserts.assert_true(type_matches(dut_fsa_bridge_th_server_unique_id, str), "UniqueID should be a string") asserts.assert_true(dut_fsa_bridge_th_server_unique_id, "UniqueID should not be an empty string") - logging.info("TH_SERVER_FOR_TH_FSA on DUT_SERVER_BRIDGE UniqueID: %s", dut_fsa_bridge_th_server_unique_id) - - # Make sure that the UniqueID on the DUT_FSA_BRIDGE is the same as the one on the DUT_FSA_BRIDGE. - asserts.assert_equal(dut_fsa_bridge_th_server_unique_id, th_fsa_bridge_th_server_unique_id, - "UniqueID on DUT_FSA_BRIDGE and TH_FSA_BRIDGE should be the same") + logging.info("UniqueID generated for TH_SERVER: %s", dut_fsa_bridge_th_server_unique_id) if __name__ == "__main__": diff --git a/src/python_testing/TC_MCORE_FS_1_4.py b/src/python_testing/TC_MCORE_FS_1_4.py new file mode 100644 index 00000000000000..6036714966fbc2 --- /dev/null +++ b/src/python_testing/TC_MCORE_FS_1_4.py @@ -0,0 +1,530 @@ +# +# Copyright (c) 2024 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# This test requires a TH_SERVER application that returns UnsupportedAttribute +# when reading UniqueID from BasicInformation Cluster. Please specify the app +# location with --string-arg th_server_app_path: + +# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments +# for details about the block below. +# +# === BEGIN CI TEST ARGUMENTS === +# test-runner-runs: run1 +# test-runner-run/run1/app: examples/fabric-admin/scripts/fabric-sync-app.py +# test-runner-run/run1/app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa/stdin --storage-dir=dut-fsa --discriminator=1234 +# test-runner-run/run1/factoryreset: True +# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --string-arg th_fsa_admin_path:${FABRIC_ADMIN_APP} th_fsa_bridge_path:${FABRIC_BRIDGE_APP} th_server_app_path:${LIGHTING_APP_NO_UNIQUE_ID} dut_fsa_stdin_pipe:dut-fsa/stdin +# test-runner-run/run1/script-start-delay: 10 +# test-runner-run/run1/quiet: false +# === END CI TEST ARGUMENTS === + +import asyncio +import logging +import os +import random +import subprocess +import sys +import tempfile +import threading + +import chip.clusters as Clusters +from chip import ChipDeviceCtrl +from chip.interaction_model import Status +from matter_testing_support import MatterBaseTest, TestStep, async_test_body, default_matter_test_main, type_matches +from mobly import asserts + + +class ThreadWithStop(threading.Thread): + + def __init__(self, args: list = [], stdout_cb=None, tag="", **kw): + super().__init__(**kw) + self.tag = f"[{tag}] " if tag else "" + self.start_event = threading.Event() + self.stop_event = threading.Event() + self.stdout_cb = stdout_cb + self.args = args + + def forward_stdout(self, f): + while True: + line = f.readline() + if not line: + break + sys.stdout.write(f"{self.tag}{line}") + if self.stdout_cb is not None: + self.stdout_cb(line) + + def forward_stderr(self, f): + while True: + line = f.readline() + if not line: + break + sys.stderr.write(f"{self.tag}{line}") + + def run(self): + logging.info("RUN: %s", " ".join(self.args)) + self.p = subprocess.Popen(self.args, errors="ignore", stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + self.start_event.set() + # Feed stdout and stderr to console and given callback. + t1 = threading.Thread(target=self.forward_stdout, args=[self.p.stdout]) + t1.start() + t2 = threading.Thread(target=self.forward_stderr, args=[self.p.stderr]) + t2.start() + # Wait for the stop event. + self.stop_event.wait() + self.p.terminate() + t1.join() + t2.join() + + def start(self): + super().start() + self.start_event.wait() + + def stop(self): + self.stop_event.set() + self.join() + + +class FabricSyncApp: + + APP_PATH = "examples/fabric-admin/scripts/fabric-sync-app.py" + FABRIC_ADMIN_PATH = "out/linux-x64-fabric-admin-rpc/fabric-admin" + FABRIC_BRIDGE_PATH = "out/linux-x64-fabric-bridge-rpc/fabric-bridge-app" + + def _process_admin_output(self, line): + if self.wait_for_text_text is not None and self.wait_for_text_text in line: + self.wait_for_text_event.set() + + def wait_for_text(self, timeout=30): + if not self.wait_for_text_event.wait(timeout=timeout): + raise Exception(f"Timeout waiting for text: {self.wait_for_text_text}") + self.wait_for_text_event.clear() + self.wait_for_text_text = None + + def __init__(self, storageDir, fabricName=None, nodeId=None, vendorId=None, paaTrustStorePath=None, + bridgePort=None, bridgeDiscriminator=None, bridgePasscode=None): + + self.wait_for_text_event = threading.Event() + self.wait_for_text_text = None + + args = [self.APP_PATH] + args.append(f"--app-admin={self.FABRIC_ADMIN_PATH}") + args.append(f"--app-bridge={self.FABRIC_BRIDGE_PATH}") + # Override default ports, so it will be possible to run + # our TH_FSA alongside the DUT_FSA during CI testing. + args.append("--app-admin-rpc-port=44000") + args.append("--app-bridge-rpc-port=44001") + # Keep the storage directory in a temporary location. + args.append(f"--storage-dir={storageDir}") + # FIXME: Passing custom PAA store breaks something + # if paaTrustStorePath is not None: + # args.append(f"--paa-trust-store-path={paaTrustStorePath}") + if fabricName is not None: + args.append(f"--commissioner-name={fabricName}") + if nodeId is not None: + args.append(f"--commissioner-node-id={nodeId}") + args.append(f"--commissioner-vendor-id={vendorId}") + args.append(f"--secured-device-port={bridgePort}") + args.append(f"--discriminator={bridgeDiscriminator}") + args.append(f"--passcode={bridgePasscode}") + + self.admin = ThreadWithStop(args, stdout_cb=self._process_admin_output) + self.wait_for_text_text = "Connected to Fabric-Bridge" + self.admin.start() + + # Wait for the bridge to connect to the admin. + self.wait_for_text() + + def CommissionOnNetwork(self, nodeId, setupPinCode=None, filterType=None, filter=None): + self.wait_for_text_text = f"Commissioning complete for node ID 0x{nodeId:016x}: success" + # Send the commissioning command to the admin. + self.admin.p.stdin.write(f"pairing onnetwork {nodeId} {setupPinCode}\n") + self.admin.p.stdin.flush() + # Wait for success message. + self.wait_for_text() + + def stop(self): + self.admin.stop() + + +class AppServer: + + def __init__(self, tag, app, storageDir, port=None, discriminator=None, passcode=None): + + args = [app] + args.extend(["--KVS", tempfile.mkstemp(dir=storageDir, prefix="kvs-app-")[1]]) + args.extend(['--secured-device-port', str(port)]) + args.extend(["--discriminator", str(discriminator)]) + args.extend(["--passcode", str(passcode)]) + self.app = ThreadWithStop(args, tag=tag) + self.app.start() + + def stop(self): + self.app.stop() + + +class TC_MCORE_FS_1_4(MatterBaseTest): + + @property + def default_timeout(self) -> int: + # This test has some manual steps, so we need a longer timeout. + return 200 + + def setup_class(self): + super().setup_class() + + # Get the path to the TH_FSA (fabric-admin and fabric-bridge) app from + # the user params or use the default path. + FabricSyncApp.APP_PATH = self.user_params.get("th_fsa_app_path", FabricSyncApp.APP_PATH) + if not os.path.exists(FabricSyncApp.APP_PATH): + asserts.fail("This test requires a TH_FSA app. Specify app path with --string-arg th_fsa_app_path:") + FabricSyncApp.FABRIC_ADMIN_PATH = self.user_params.get("th_fsa_admin_path", FabricSyncApp.FABRIC_ADMIN_PATH) + if not os.path.exists(FabricSyncApp.FABRIC_ADMIN_PATH): + asserts.fail("This test requires a TH_FSA_ADMIN app. Specify app path with --string-arg th_fsa_admin_path:") + FabricSyncApp.FABRIC_BRIDGE_PATH = self.user_params.get("th_fsa_bridge_path", FabricSyncApp.FABRIC_BRIDGE_PATH) + if not os.path.exists(FabricSyncApp.FABRIC_BRIDGE_PATH): + asserts.fail("This test requires a TH_FSA_BRIDGE app. Specify app path with --string-arg th_fsa_bridge_path:") + + # Get the path to the TH_SERVER app from the user params. + th_server_app = self.user_params.get("th_server_app_path", None) + if not th_server_app: + asserts.fail("This test requires a TH_SERVER app. Specify app path with --string-arg th_server_app_path:") + if not os.path.exists(th_server_app): + asserts.fail(f"The path {th_server_app} does not exist") + + # Create a temporary storage directory for keeping KVS files. + self.storage = tempfile.TemporaryDirectory(prefix=self.__class__.__name__) + logging.info("Temporary storage directory: %s", self.storage.name) + + self.th_fsa_bridge_address = "::1" + self.th_fsa_bridge_port = 5543 + self.th_fsa_bridge_discriminator = random.randint(0, 4095) + self.th_fsa_bridge_passcode = 20202021 + + self.th_fsa_controller = FabricSyncApp( + storageDir=self.storage.name, + paaTrustStorePath=self.matter_test_config.paa_trust_store_path, + bridgePort=self.th_fsa_bridge_port, + bridgeDiscriminator=self.th_fsa_bridge_discriminator, + bridgePasscode=self.th_fsa_bridge_passcode, + vendorId=0xFFF1) + + # Get the named pipe path for the DUT_FSA app input from the user params. + dut_fsa_stdin_pipe = self.user_params.get("dut_fsa_stdin_pipe", None) + if dut_fsa_stdin_pipe is not None: + self.dut_fsa_stdin = open(dut_fsa_stdin_pipe, "w") + + self.server_for_dut_port = 5544 + self.server_for_dut_discriminator = random.randint(0, 4095) + self.server_for_dut_passcode = 20202021 + + # Start the TH_SERVER_FOR_DUT_FSA app. + self.server_for_dut = AppServer( + "APP1", + th_server_app, + storageDir=self.storage.name, + port=self.server_for_dut_port, + discriminator=self.server_for_dut_discriminator, + passcode=self.server_for_dut_passcode) + + self.server_for_th_port = 5545 + self.server_for_th_discriminator = random.randint(0, 4095) + self.server_for_th_passcode = 20202021 + + # Start the TH_SERVER_FOR_TH_FSA app. + self.server_for_th = AppServer( + "APP2", + th_server_app, + storageDir=self.storage.name, + port=self.server_for_th_port, + discriminator=self.server_for_th_discriminator, + passcode=self.server_for_th_passcode) + + def teardown_class(self): + self.th_fsa_controller.stop() + self.server_for_dut.stop() + self.server_for_th.stop() + self.storage.cleanup() + super().teardown_class() + + def steps_TC_MCORE_FS_1_4(self) -> list[TestStep]: + return [ + TestStep(0, "Commission DUT if not done", is_commissioning=True), + TestStep(1, "DUT_FSA commissions TH_SERVER_FOR_DUT_FSA to DUT_FSA's fabric and generates a UniqueID"), + TestStep(2, "TH instructs TH_FSA to commission TH_SERVER_FOR_TH_FSA to TH_FSA's fabric"), + TestStep(3, "TH instructs TH_FSA to open up commissioning window on it's aggregator"), + TestStep(4, "Follow manufacturer provided instructions to have DUT_FSA commission TH_FSA's aggregator"), + TestStep(5, "Follow manufacturer provided instructions to enable DUT_FSA to synchronize TH_SERVER_FOR_TH_FSA from TH_FSA onto DUT_FSA's fabric. TH to provide endpoint saved from step 2 in user prompt"), + TestStep(6, "DUT_FSA synchronizes TH_SERVER_FOR_TH_FSA onto DUT_FSA's fabric and copies the UniqueID presented by TH_FSA's Bridged Device Basic Information Cluster"), + ] + + async def commission_via_commissioner_control(self, controller_node_id: int, device_node_id: int): + """Commission device_node_id to controller_node_id using CommissionerControl cluster.""" + + request_id = random.randint(0, 0xFFFFFFFFFFFFFFFF) + + vendor_id = await self.read_single_attribute_check_success( + node_id=device_node_id, + cluster=Clusters.BasicInformation, + attribute=Clusters.BasicInformation.Attributes.VendorID, + ) + + product_id = await self.read_single_attribute_check_success( + node_id=device_node_id, + cluster=Clusters.BasicInformation, + attribute=Clusters.BasicInformation.Attributes.ProductID, + ) + + await self.send_single_cmd( + node_id=controller_node_id, + cmd=Clusters.CommissionerControl.Commands.RequestCommissioningApproval( + requestId=request_id, + vendorId=vendor_id, + productId=product_id, + ), + ) + + if not self.is_ci: + self.wait_for_user_input("Approve Commissioning Approval Request on DUT using manufacturer specified mechanism") + + resp = await self.send_single_cmd( + node_id=controller_node_id, + cmd=Clusters.CommissionerControl.Commands.CommissionNode( + requestId=request_id, + responseTimeoutSeconds=30, + ), + ) + + asserts.assert_equal(type(resp), Clusters.CommissionerControl.Commands.ReverseOpenCommissioningWindow, + "Incorrect response type") + + await self.send_single_cmd( + node_id=device_node_id, + cmd=Clusters.AdministratorCommissioning.Commands.OpenCommissioningWindow( + commissioningTimeout=3*60, + PAKEPasscodeVerifier=resp.PAKEPasscodeVerifier, + discriminator=resp.discriminator, + iterations=resp.iterations, + salt=resp.salt, + ), + timedRequestTimeoutMs=5000, + ) + + if not self.is_ci: + await asyncio.sleep(30) + + @async_test_body + async def test_TC_MCORE_FS_1_4(self): + self.is_ci = self.check_pics('PICS_SDK_CI_ONLY') + self.is_ci = True + + # Commissioning - done + self.step(0) + + th_server_for_dut_fsa_th_node_id = 1 + self.print_step(1, "Commissioning TH_SERVER_FOR_DUT_FSA to TH fabric") + await self.default_controller.CommissionOnNetwork( + nodeId=th_server_for_dut_fsa_th_node_id, + setupPinCode=self.server_for_dut_passcode, + filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, + filter=self.server_for_dut_discriminator, + ) + + self.print_step(2, "Verify that TH_SERVER_FOR_DUT_FSA does not have a UniqueID") + # FIXME: Sometimes reading the UniqueID fails... + await self.read_single_attribute_expect_error( + cluster=Clusters.BasicInformation, + attribute=Clusters.BasicInformation.Attributes.UniqueID, + node_id=th_server_for_dut_fsa_th_node_id, + error=Status.UnsupportedAttribute, + ) + + # Get the list of endpoints on the DUT_FSA_BRIDGE before adding the TH_SERVER_FOR_DUT_FSA. + dut_fsa_bridge_endpoints = set(await self.read_single_attribute_check_success( + cluster=Clusters.Descriptor, + attribute=Clusters.Descriptor.Attributes.PartsList, + node_id=self.dut_node_id, + endpoint=0, + )) + + self.print_step(3, "Commissioning TH_SERVER_FOR_DUT_FSA to DUT_FSA fabric") + await self.commission_via_commissioner_control( + controller_node_id=self.dut_node_id, + device_node_id=th_server_for_dut_fsa_th_node_id) + + # Wait for the device to appear on the DUT_FSA_BRIDGE. + await asyncio.sleep(2) + + # Get the list of endpoints on the DUT_FSA_BRIDGE after adding the TH_SERVER_FOR_DUT_FSA. + dut_fsa_bridge_endpoints_new = set(await self.read_single_attribute_check_success( + cluster=Clusters.Descriptor, + attribute=Clusters.Descriptor.Attributes.PartsList, + node_id=self.dut_node_id, + endpoint=0, + )) + + # Get the endpoint number for just added TH_SERVER_FOR_DUT_FSA. + logging.info("Endpoints on DUT_FSA_BRIDGE: old=%s, new=%s", dut_fsa_bridge_endpoints, dut_fsa_bridge_endpoints_new) + asserts.assert_true(dut_fsa_bridge_endpoints_new.issuperset(dut_fsa_bridge_endpoints), + "Expected only new endpoints to be added") + unique_endpoints_set = dut_fsa_bridge_endpoints_new - dut_fsa_bridge_endpoints + asserts.assert_equal(len(unique_endpoints_set), 1, "Expected only one new endpoint on DUT_FSA") + dut_fsa_bridge_dut_server_endpoint = list(unique_endpoints_set)[0] + dut_fsa_bridge_endpoints = dut_fsa_bridge_endpoints_new + + self.print_step(4, "Verify that DUT_FSA created a UniqueID for TH_SERVER_FOR_DUT_FSA") + dut_fsa_bridge_dut_server_unique_id = await self.read_single_attribute_check_success( + cluster=Clusters.BridgedDeviceBasicInformation, + attribute=Clusters.BridgedDeviceBasicInformation.Attributes.UniqueID, + endpoint=dut_fsa_bridge_dut_server_endpoint) + asserts.assert_true(type_matches(dut_fsa_bridge_dut_server_unique_id, str), "UniqueID should be a string") + asserts.assert_true(dut_fsa_bridge_dut_server_unique_id, "UniqueID should not be an empty string") + logging.info("TH_SERVER_FOR_DUT_FSA on DUT_SERVER_BRIDGE UniqueID: %s", dut_fsa_bridge_dut_server_unique_id) + + th_fsa_bridge_th_node_id = 2 + self.print_step(5, "Commissioning TH_FSA_BRIDGE to TH fabric") + await self.default_controller.CommissionOnNetwork( + nodeId=th_fsa_bridge_th_node_id, + setupPinCode=self.th_fsa_bridge_passcode, + filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, + filter=self.th_fsa_bridge_discriminator, + ) + + # Get the list of endpoints on the TH_FSA_BRIDGE before adding the TH_SERVER_FOR_TH_FSA. + th_fsa_bridge_endpoints = set(await self.read_single_attribute_check_success( + cluster=Clusters.Descriptor, + attribute=Clusters.Descriptor.Attributes.PartsList, + node_id=th_fsa_bridge_th_node_id, + endpoint=0, + )) + + th_server_for_th_fsa_th_fsa_node_id = 3 + self.print_step(6, "Commissioning TH_SERVER_FOR_TH_FSA to TH_FSA") + self.th_fsa_controller.CommissionOnNetwork( + nodeId=th_server_for_th_fsa_th_fsa_node_id, + setupPinCode=self.server_for_th_passcode, + filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, + filter=self.server_for_th_discriminator, + ) + + # Wait some time, so the dynamic endpoint will appear on the TH_FSA_BRIDGE. + await asyncio.sleep(5) + + # Get the list of endpoints on the TH_FSA_BRIDGE after adding the TH_SERVER_FOR_TH_FSA. + th_fsa_bridge_endpoints_new = set(await self.read_single_attribute_check_success( + cluster=Clusters.Descriptor, + attribute=Clusters.Descriptor.Attributes.PartsList, + node_id=th_fsa_bridge_th_node_id, + endpoint=0, + )) + + # Get the endpoint number for just added TH_SERVER_FOR_TH_FSA. + logging.info("Endpoints on TH_FSA_BRIDGE: old=%s, new=%s", th_fsa_bridge_endpoints, th_fsa_bridge_endpoints_new) + asserts.assert_true(th_fsa_bridge_endpoints_new.issuperset(th_fsa_bridge_endpoints), + "Expected only new endpoints to be added") + unique_endpoints_set = th_fsa_bridge_endpoints_new - th_fsa_bridge_endpoints + asserts.assert_equal(len(unique_endpoints_set), 1, "Expected only one new endpoint") + th_fsa_bridge_th_server_endpoint = list(unique_endpoints_set)[0] + + self.print_step(7, "Verify that TH_FSA created a UniqueID for TH_SERVER_FOR_TH_FSA") + th_fsa_bridge_th_server_unique_id = await self.read_single_attribute_check_success( + cluster=Clusters.BridgedDeviceBasicInformation, + attribute=Clusters.BridgedDeviceBasicInformation.Attributes.UniqueID, + node_id=th_fsa_bridge_th_node_id, + endpoint=th_fsa_bridge_th_server_endpoint) + asserts.assert_true(type_matches(th_fsa_bridge_th_server_unique_id, str), "UniqueID should be a string") + asserts.assert_true(th_fsa_bridge_th_server_unique_id, "UniqueID should not be an empty string") + logging.info("TH_SERVER_FOR_TH_FSA on TH_SERVER_BRIDGE UniqueID: %s", th_fsa_bridge_th_server_unique_id) + + # Make sure that the UniqueID on the TH_FSA_BRIDGE is different from the one on the DUT_FSA_BRIDGE. + asserts.assert_not_equal(dut_fsa_bridge_dut_server_unique_id, th_fsa_bridge_th_server_unique_id, + "UniqueID on DUT_FSA_BRIDGE and TH_FSA_BRIDGE should be different") + + discriminator = random.randint(0, 4095) + self.print_step(8, "Open commissioning window on TH_FSA_BRIDGE") + params = await self.default_controller.OpenCommissioningWindow( + nodeid=th_fsa_bridge_th_node_id, + option=self.default_controller.CommissioningWindowPasscode.kTokenWithRandomPin, + discriminator=discriminator, + iteration=10000, + timeout=600) + + self.print_step(9, "Commissioning TH_FSA_BRIDGE to DUT_FSA fabric") + if not self.is_ci: + self.wait_for_user_input( + f"Commission TH Fabric-Sync Bridge on DUT using manufacturer specified mechanism.\n" + f"Use the following parameters:\n" + f"- discriminator: {discriminator}\n" + f"- setupPinCode: {params.setupPinCode}\n" + f"- setupQRCode: {params.setupQRCode}\n" + f"- setupManualCode: {params.setupManualCode}\n" + f"If using FabricSync Admin, you may type:\n" + f">>> fabricsync add-bridge {params.setupPinCode} {self.th_fsa_bridge_port}") + else: + self.dut_fsa_stdin.write( + f"fabricsync add-bridge 10 {params.setupPinCode} {self.th_fsa_bridge_address} {self.th_fsa_bridge_port}\n") + self.dut_fsa_stdin.flush() + # Wait for the commissioning to complete. + await asyncio.sleep(5) + + self.print_step(10, "Synchronize TH_SERVER_FOR_TH_FSA from TH_FSA to DUT_FSA fabric") + if not self.is_ci: + self.wait_for_user_input( + f"Synchronize endpoint from TH Fabric-Sync Bridge to DUT using manufacturer specified mechanism.\n" + f"Use the following parameters:\n" + f"- bridgeDynamicEndpoint: {th_fsa_bridge_th_server_endpoint}\n" + f"If using FabricSync Admin, you may type:\n" + f">>> fabricsync sync-device {th_fsa_bridge_th_server_endpoint}") + else: + self.dut_fsa_stdin.write(f"fabricsync sync-device {th_fsa_bridge_th_server_endpoint}\n") + self.dut_fsa_stdin.flush() + # Wait for the synchronization to complete. + await asyncio.sleep(5) + + # Get the list of endpoints on the DUT_FSA_BRIDGE after synchronization + dut_fsa_bridge_endpoints_new = set(await self.read_single_attribute_check_success( + cluster=Clusters.Descriptor, + attribute=Clusters.Descriptor.Attributes.PartsList, + node_id=self.dut_node_id, + endpoint=0, + )) + + # Get the endpoint number for just synced TH_SERVER_FOR_TH_FSA. + logging.info("Endpoints on DUT_FSA_BRIDGE: old=%s, new=%s", dut_fsa_bridge_endpoints, dut_fsa_bridge_endpoints_new) + asserts.assert_true(dut_fsa_bridge_endpoints_new.issuperset(dut_fsa_bridge_endpoints), + "Expected only new endpoints to be added") + unique_endpoints_set = dut_fsa_bridge_endpoints_new - dut_fsa_bridge_endpoints + asserts.assert_equal(len(unique_endpoints_set), 1, "Expected only one new endpoint on DUT_FSA") + dut_fsa_bridge_th_server_endpoint = list(unique_endpoints_set)[0] + dut_fsa_bridge_endpoints = dut_fsa_bridge_endpoints_new + + self.print_step(11, "Verify that DUT_FSA copied the TH_SERVER_FOR_TH_FSA UniqueID from TH_FSA") + dut_fsa_bridge_th_server_unique_id = await self.read_single_attribute_check_success( + cluster=Clusters.BridgedDeviceBasicInformation, + attribute=Clusters.BridgedDeviceBasicInformation.Attributes.UniqueID, + endpoint=dut_fsa_bridge_th_server_endpoint) + asserts.assert_true(type_matches(dut_fsa_bridge_th_server_unique_id, str), "UniqueID should be a string") + asserts.assert_true(dut_fsa_bridge_th_server_unique_id, "UniqueID should not be an empty string") + logging.info("TH_SERVER_FOR_TH_FSA on DUT_SERVER_BRIDGE UniqueID: %s", dut_fsa_bridge_th_server_unique_id) + + # Make sure that the UniqueID on the DUT_FSA_BRIDGE is the same as the one on the DUT_FSA_BRIDGE. + asserts.assert_equal(dut_fsa_bridge_th_server_unique_id, th_fsa_bridge_th_server_unique_id, + "UniqueID on DUT_FSA_BRIDGE and TH_FSA_BRIDGE should be the same") + + +if __name__ == "__main__": + default_matter_test_main() From 1e93d9e04a980fe63188e0c0f00156c99d91cf08 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Thu, 29 Aug 2024 09:34:04 +0200 Subject: [PATCH 20/33] Simplify TC_MCORE_FS_1_3 script --- .../fabric-admin/scripts/fabric-sync-app.py | 2 +- src/python_testing/TC_MCORE_FS_1_3.py | 82 +++++++------------ 2 files changed, 31 insertions(+), 53 deletions(-) diff --git a/examples/fabric-admin/scripts/fabric-sync-app.py b/examples/fabric-admin/scripts/fabric-sync-app.py index 4fb252dda759ae..9459eb1728062c 100755 --- a/examples/fabric-admin/scripts/fabric-sync-app.py +++ b/examples/fabric-admin/scripts/fabric-sync-app.py @@ -144,7 +144,7 @@ async def main(args): def terminate(signum, frame): admin.terminate() bridge.terminate() - sys.exit(1) + sys.exit(0) signal.signal(signal.SIGINT, terminate) signal.signal(signal.SIGTERM, terminate) diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index 4a5cd4a1a738de..fabbc5f7d50d99 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -15,9 +15,9 @@ # limitations under the License. # -# This test requires a TH_SERVER application that returns UnsupportedAttribute +# This test requires a TH_SERVER_NO_UID application that returns UnsupportedAttribute # when reading UniqueID from BasicInformation Cluster. Please specify the app -# location with --string-arg th_server_app_path: +# location with --string-arg th_server_no_uid_app_path: # See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments # for details about the block below. @@ -27,7 +27,7 @@ # test-runner-run/run1/app: examples/fabric-admin/scripts/fabric-sync-app.py # test-runner-run/run1/app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa/stdin --storage-dir=dut-fsa --discriminator=1234 # test-runner-run/run1/factoryreset: True -# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --string-arg th_server_app_path:${LIGHTING_APP_NO_UNIQUE_ID} +# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --string-arg th_server_no_uid_app_path:${LIGHTING_APP_NO_UNIQUE_ID} # test-runner-run/run1/script-start-delay: 10 # test-runner-run/run1/quiet: false # === END CI TEST ARGUMENTS === @@ -48,55 +48,36 @@ from mobly import asserts -class ThreadWithStop(threading.Thread): +class Subprocess(threading.Thread): - def __init__(self, args: list = [], stdout_cb=None, tag="", **kw): + def __init__(self, args: list = [], tag="", **kw): super().__init__(**kw) self.tag = f"[{tag}] " if tag else "" - self.start_event = threading.Event() - self.stop_event = threading.Event() - self.stdout_cb = stdout_cb self.args = args - def forward_stdout(self, f): + def forward_f(self, f_in, f_out): while True: - line = f.readline() + line = f_in.readline() if not line: break - sys.stdout.write(f"{self.tag}{line}") - if self.stdout_cb is not None: - self.stdout_cb(line) - - def forward_stderr(self, f): - while True: - line = f.readline() - if not line: - break - sys.stderr.write(f"{self.tag}{line}") + f_out.write(f"{self.tag}{line}") + f_out.flush() def run(self): logging.info("RUN: %s", " ".join(self.args)) self.p = subprocess.Popen(self.args, errors="ignore", stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - self.start_event.set() - # Feed stdout and stderr to console and given callback. - t1 = threading.Thread(target=self.forward_stdout, args=[self.p.stdout]) + # Forward stdout and stderr with a tag attached. + t1 = threading.Thread(target=self.forward_f, args=[self.p.stdout, sys.stdout]) t1.start() - t2 = threading.Thread(target=self.forward_stderr, args=[self.p.stderr]) + t2 = threading.Thread(target=self.forward_f, args=[self.p.stderr, sys.stderr]) t2.start() - # Wait for the stop event. - self.stop_event.wait() - self.p.terminate() + self.p.wait() t1.join() t2.join() - def start(self): - super().start() - self.start_event.wait() - def stop(self): - self.stop_event.set() - self.join() + self.p.terminate() class AppServer: @@ -108,7 +89,7 @@ def __init__(self, app, storageDir, port=None, discriminator=None, passcode=None args.extend(['--secured-device-port', str(port)]) args.extend(["--discriminator", str(discriminator)]) args.extend(["--passcode", str(passcode)]) - self.app = ThreadWithStop(args, tag="APP") + self.app = Subprocess(args, tag="SERVER") self.app.start() def stop(self): @@ -125,10 +106,10 @@ def default_timeout(self) -> int: def setup_class(self): super().setup_class() - # Get the path to the TH_SERVER app from the user params. - th_server_app = self.user_params.get("th_server_app_path", None) + # Get the path to the TH_SERVER_NO_UID app from the user params. + th_server_app = self.user_params.get("th_server_no_uid_app_path", None) if not th_server_app: - asserts.fail("This test requires a TH_SERVER app. Specify app path with --string-arg th_server_app_path:") + asserts.fail("This test requires a TH_SERVER_NO_UID app. Specify app path with --string-arg th_server_no_uid_app_path:") if not os.path.exists(th_server_app): asserts.fail(f"The path {th_server_app} does not exist") @@ -140,7 +121,7 @@ def setup_class(self): self.th_server_discriminator = random.randint(0, 4095) self.th_server_passcode = 20202021 - # Start the TH_SERVER app. + # Start the TH_SERVER_NO_UID app. self.th_server = AppServer( th_server_app, storageDir=self.storage.name, @@ -156,12 +137,9 @@ def teardown_class(self): def steps_TC_MCORE_FS_1_3(self) -> list[TestStep]: return [ TestStep(0, "Commission DUT if not done", is_commissioning=True), - TestStep(1, "DUT_FSA commissions TH_SERVER_FOR_DUT_FSA to DUT_FSA's fabric and generates a UniqueID"), - TestStep(2, "TH instructs TH_FSA to commission TH_SERVER_FOR_TH_FSA to TH_FSA's fabric"), - TestStep(3, "TH instructs TH_FSA to open up commissioning window on it's aggregator"), - TestStep(4, "Follow manufacturer provided instructions to have DUT_FSA commission TH_FSA's aggregator"), - TestStep(5, "Follow manufacturer provided instructions to enable DUT_FSA to synchronize TH_SERVER_FOR_TH_FSA from TH_FSA onto DUT_FSA's fabric. TH to provide endpoint saved from step 2 in user prompt"), - TestStep(6, "DUT_FSA synchronizes TH_SERVER_FOR_TH_FSA onto DUT_FSA's fabric and copies the UniqueID presented by TH_FSA's Bridged Device Basic Information Cluster"), + TestStep(1, "TH commissions TH_SERVER_NO_UID to TH's fabric"), + TestStep(2, "DUT_FSA commissions TH_SERVER_NO_UID to DUT_FSA's fabric and generates a UniqueID.", + "TH verifies a value is visible for the UniqueID from the DUT_FSA's Bridged Device Basic Information Cluster."), ] async def commission_via_commissioner_control(self, controller_node_id: int, device_node_id: int): @@ -226,8 +204,9 @@ async def test_TC_MCORE_FS_1_3(self): # Commissioning - done self.step(0) + self.step(1) + th_server_th_node_id = 1 - self.print_step(1, "Commissioning TH_SERVER to TH fabric") await self.default_controller.CommissionOnNetwork( nodeId=th_server_th_node_id, setupPinCode=self.th_server_passcode, @@ -235,7 +214,6 @@ async def test_TC_MCORE_FS_1_3(self): filter=self.th_server_discriminator, ) - self.print_step(2, "Verify that TH_SERVER does not have a UniqueID") # FIXME: Sometimes reading the UniqueID fails... await self.read_single_attribute_expect_error( cluster=Clusters.BasicInformation, @@ -244,7 +222,7 @@ async def test_TC_MCORE_FS_1_3(self): error=Status.UnsupportedAttribute, ) - # Get the list of endpoints on the DUT_FSA_BRIDGE before adding the TH_SERVER. + # Get the list of endpoints on the DUT_FSA_BRIDGE before adding the TH_SERVER_NO_UID. dut_fsa_bridge_endpoints = set(await self.read_single_attribute_check_success( cluster=Clusters.Descriptor, attribute=Clusters.Descriptor.Attributes.PartsList, @@ -252,7 +230,8 @@ async def test_TC_MCORE_FS_1_3(self): endpoint=0, )) - self.print_step(3, "Commissioning TH_SERVER to DUT_FSA fabric") + self.step(2) + await self.commission_via_commissioner_control( controller_node_id=self.dut_node_id, device_node_id=th_server_th_node_id) @@ -260,7 +239,7 @@ async def test_TC_MCORE_FS_1_3(self): # Wait for the device to appear on the DUT_FSA_BRIDGE. await asyncio.sleep(2) - # Get the list of endpoints on the DUT_FSA_BRIDGE after adding the TH_SERVER. + # Get the list of endpoints on the DUT_FSA_BRIDGE after adding the TH_SERVER_NO_UID. dut_fsa_bridge_endpoints_new = set(await self.read_single_attribute_check_success( cluster=Clusters.Descriptor, attribute=Clusters.Descriptor.Attributes.PartsList, @@ -268,7 +247,7 @@ async def test_TC_MCORE_FS_1_3(self): endpoint=0, )) - # Get the endpoint number for just added TH_SERVER. + # Get the endpoint number for just added TH_SERVER_NO_UID. logging.info("Endpoints on DUT_FSA_BRIDGE: old=%s, new=%s", dut_fsa_bridge_endpoints, dut_fsa_bridge_endpoints_new) asserts.assert_true(dut_fsa_bridge_endpoints_new.issuperset(dut_fsa_bridge_endpoints), "Expected only new endpoints to be added") @@ -276,14 +255,13 @@ async def test_TC_MCORE_FS_1_3(self): asserts.assert_equal(len(unique_endpoints_set), 1, "Expected only one new endpoint on DUT_FSA") dut_fsa_bridge_th_server_endpoint = list(unique_endpoints_set)[0] - self.print_step(4, "Verify that DUT_FSA created a UniqueID for TH_SERVER") dut_fsa_bridge_th_server_unique_id = await self.read_single_attribute_check_success( cluster=Clusters.BridgedDeviceBasicInformation, attribute=Clusters.BridgedDeviceBasicInformation.Attributes.UniqueID, endpoint=dut_fsa_bridge_th_server_endpoint) asserts.assert_true(type_matches(dut_fsa_bridge_th_server_unique_id, str), "UniqueID should be a string") asserts.assert_true(dut_fsa_bridge_th_server_unique_id, "UniqueID should not be an empty string") - logging.info("UniqueID generated for TH_SERVER: %s", dut_fsa_bridge_th_server_unique_id) + logging.info("UniqueID generated for TH_SERVER_NO_UID: %s", dut_fsa_bridge_th_server_unique_id) if __name__ == "__main__": From cc9fb36cf12692988f1e1f6afdc14d17b0f55ef0 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Thu, 29 Aug 2024 10:28:42 +0200 Subject: [PATCH 21/33] Simplify TC_MCORE_FS_1_4 steps --- .../fabric-admin/scripts/fabric-sync-app.py | 3 +- src/python_testing/TC_MCORE_FS_1_3.py | 6 +- src/python_testing/TC_MCORE_FS_1_4.py | 229 +++++++----------- 3 files changed, 96 insertions(+), 142 deletions(-) diff --git a/examples/fabric-admin/scripts/fabric-sync-app.py b/examples/fabric-admin/scripts/fabric-sync-app.py index 9459eb1728062c..452493e8e4977e 100755 --- a/examples/fabric-admin/scripts/fabric-sync-app.py +++ b/examples/fabric-admin/scripts/fabric-sync-app.py @@ -16,12 +16,11 @@ import asyncio import contextlib +import os import signal import sys -import os from argparse import ArgumentParser - BRIDGE_COMMISSIONED = asyncio.Event() # Log message which should appear in the fabric-admin output if # the bridge is already commissioned. diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index fabbc5f7d50d99..1208567b643aa4 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -72,12 +72,14 @@ def run(self): t1.start() t2 = threading.Thread(target=self.forward_f, args=[self.p.stderr, sys.stderr]) t2.start() + # Wait for the process to finish. self.p.wait() t1.join() t2.join() def stop(self): self.p.terminate() + self.join() class AppServer: @@ -222,6 +224,8 @@ async def test_TC_MCORE_FS_1_3(self): error=Status.UnsupportedAttribute, ) + self.step(2) + # Get the list of endpoints on the DUT_FSA_BRIDGE before adding the TH_SERVER_NO_UID. dut_fsa_bridge_endpoints = set(await self.read_single_attribute_check_success( cluster=Clusters.Descriptor, @@ -230,8 +234,6 @@ async def test_TC_MCORE_FS_1_3(self): endpoint=0, )) - self.step(2) - await self.commission_via_commissioner_control( controller_node_id=self.dut_node_id, device_node_id=th_server_th_node_id) diff --git a/src/python_testing/TC_MCORE_FS_1_4.py b/src/python_testing/TC_MCORE_FS_1_4.py index 6036714966fbc2..a9fc7d2e6570ca 100644 --- a/src/python_testing/TC_MCORE_FS_1_4.py +++ b/src/python_testing/TC_MCORE_FS_1_4.py @@ -15,9 +15,9 @@ # limitations under the License. # -# This test requires a TH_SERVER application that returns UnsupportedAttribute +# This test requires a TH_SERVER_NO_UID application that returns UnsupportedAttribute # when reading UniqueID from BasicInformation Cluster. Please specify the app -# location with --string-arg th_server_app_path: +# location with --string-arg th_server_no_uid_app_path: # See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments # for details about the block below. @@ -27,7 +27,7 @@ # test-runner-run/run1/app: examples/fabric-admin/scripts/fabric-sync-app.py # test-runner-run/run1/app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa/stdin --storage-dir=dut-fsa --discriminator=1234 # test-runner-run/run1/factoryreset: True -# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --string-arg th_fsa_admin_path:${FABRIC_ADMIN_APP} th_fsa_bridge_path:${FABRIC_BRIDGE_APP} th_server_app_path:${LIGHTING_APP_NO_UNIQUE_ID} dut_fsa_stdin_pipe:dut-fsa/stdin +# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --string-arg th_fsa_admin_path:${FABRIC_ADMIN_APP} th_fsa_bridge_path:${FABRIC_BRIDGE_APP} th_server_no_uid_app_path:${LIGHTING_APP_NO_UNIQUE_ID} dut_fsa_stdin_pipe:dut-fsa/stdin # test-runner-run/run1/script-start-delay: 10 # test-runner-run/run1/quiet: false # === END CI TEST ARGUMENTS === @@ -48,54 +48,40 @@ from mobly import asserts -class ThreadWithStop(threading.Thread): +class Subprocess(threading.Thread): def __init__(self, args: list = [], stdout_cb=None, tag="", **kw): super().__init__(**kw) self.tag = f"[{tag}] " if tag else "" - self.start_event = threading.Event() - self.stop_event = threading.Event() self.stdout_cb = stdout_cb self.args = args - def forward_stdout(self, f): + def forward_f(self, f_in, f_out): while True: - line = f.readline() + line = f_in.readline() if not line: break - sys.stdout.write(f"{self.tag}{line}") + f_out.write(f"{self.tag}{line}") + f_out.flush() if self.stdout_cb is not None: self.stdout_cb(line) - def forward_stderr(self, f): - while True: - line = f.readline() - if not line: - break - sys.stderr.write(f"{self.tag}{line}") - def run(self): logging.info("RUN: %s", " ".join(self.args)) self.p = subprocess.Popen(self.args, errors="ignore", stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - self.start_event.set() - # Feed stdout and stderr to console and given callback. - t1 = threading.Thread(target=self.forward_stdout, args=[self.p.stdout]) + # Forward stdout and stderr with a tag attached. + t1 = threading.Thread(target=self.forward_f, args=[self.p.stdout, sys.stdout]) t1.start() - t2 = threading.Thread(target=self.forward_stderr, args=[self.p.stderr]) + t2 = threading.Thread(target=self.forward_f, args=[self.p.stderr, sys.stderr]) t2.start() - # Wait for the stop event. - self.stop_event.wait() - self.p.terminate() + # Wait for the process to finish. + self.p.wait() t1.join() t2.join() - def start(self): - super().start() - self.start_event.wait() - def stop(self): - self.stop_event.set() + self.p.terminate() self.join() @@ -142,7 +128,7 @@ def __init__(self, storageDir, fabricName=None, nodeId=None, vendorId=None, paaT args.append(f"--discriminator={bridgeDiscriminator}") args.append(f"--passcode={bridgePasscode}") - self.admin = ThreadWithStop(args, stdout_cb=self._process_admin_output) + self.admin = Subprocess(args, stdout_cb=self._process_admin_output) self.wait_for_text_text = "Connected to Fabric-Bridge" self.admin.start() @@ -163,14 +149,14 @@ def stop(self): class AppServer: - def __init__(self, tag, app, storageDir, port=None, discriminator=None, passcode=None): + def __init__(self, app, storageDir, port=None, discriminator=None, passcode=None): args = [app] args.extend(["--KVS", tempfile.mkstemp(dir=storageDir, prefix="kvs-app-")[1]]) args.extend(['--secured-device-port', str(port)]) args.extend(["--discriminator", str(discriminator)]) args.extend(["--passcode", str(passcode)]) - self.app = ThreadWithStop(args, tag=tag) + self.app = Subprocess(args, tag="SERVER") self.app.start() def stop(self): @@ -199,10 +185,10 @@ def setup_class(self): if not os.path.exists(FabricSyncApp.FABRIC_BRIDGE_PATH): asserts.fail("This test requires a TH_FSA_BRIDGE app. Specify app path with --string-arg th_fsa_bridge_path:") - # Get the path to the TH_SERVER app from the user params. - th_server_app = self.user_params.get("th_server_app_path", None) + # Get the path to the TH_SERVER_NO_UID app from the user params. + th_server_app = self.user_params.get("th_server_no_uid_app_path", None) if not th_server_app: - asserts.fail("This test requires a TH_SERVER app. Specify app path with --string-arg th_server_app_path:") + asserts.fail("This test requires a TH_SERVER_NO_UID app. Specify app path with --string-arg th_server_no_uid_app_path:") if not os.path.exists(th_server_app): asserts.fail(f"The path {th_server_app} does not exist") @@ -228,48 +214,36 @@ def setup_class(self): if dut_fsa_stdin_pipe is not None: self.dut_fsa_stdin = open(dut_fsa_stdin_pipe, "w") - self.server_for_dut_port = 5544 - self.server_for_dut_discriminator = random.randint(0, 4095) - self.server_for_dut_passcode = 20202021 + self.th_server_port = 5544 + self.th_server_discriminator = random.randint(0, 4095) + self.th_server_passcode = 20202021 - # Start the TH_SERVER_FOR_DUT_FSA app. - self.server_for_dut = AppServer( - "APP1", + # Start the TH_SERVER_NO_UID app. + self.th_server = AppServer( th_server_app, storageDir=self.storage.name, - port=self.server_for_dut_port, - discriminator=self.server_for_dut_discriminator, - passcode=self.server_for_dut_passcode) - - self.server_for_th_port = 5545 - self.server_for_th_discriminator = random.randint(0, 4095) - self.server_for_th_passcode = 20202021 - - # Start the TH_SERVER_FOR_TH_FSA app. - self.server_for_th = AppServer( - "APP2", - th_server_app, - storageDir=self.storage.name, - port=self.server_for_th_port, - discriminator=self.server_for_th_discriminator, - passcode=self.server_for_th_passcode) + port=self.th_server_port, + discriminator=self.th_server_discriminator, + passcode=self.th_server_passcode) def teardown_class(self): self.th_fsa_controller.stop() - self.server_for_dut.stop() - self.server_for_th.stop() + self.th_server.stop() self.storage.cleanup() super().teardown_class() def steps_TC_MCORE_FS_1_4(self) -> list[TestStep]: return [ TestStep(0, "Commission DUT if not done", is_commissioning=True), - TestStep(1, "DUT_FSA commissions TH_SERVER_FOR_DUT_FSA to DUT_FSA's fabric and generates a UniqueID"), - TestStep(2, "TH instructs TH_FSA to commission TH_SERVER_FOR_TH_FSA to TH_FSA's fabric"), - TestStep(3, "TH instructs TH_FSA to open up commissioning window on it's aggregator"), - TestStep(4, "Follow manufacturer provided instructions to have DUT_FSA commission TH_FSA's aggregator"), - TestStep(5, "Follow manufacturer provided instructions to enable DUT_FSA to synchronize TH_SERVER_FOR_TH_FSA from TH_FSA onto DUT_FSA's fabric. TH to provide endpoint saved from step 2 in user prompt"), - TestStep(6, "DUT_FSA synchronizes TH_SERVER_FOR_TH_FSA onto DUT_FSA's fabric and copies the UniqueID presented by TH_FSA's Bridged Device Basic Information Cluster"), + TestStep(1, "TH commissions TH_SERVER_NO_UID to TH's fabric.", + "TH verifies that the TH_SERVER_NO_UID does not provide a UniqueID."), + TestStep(2, "TH instructs TH_FSA to commission TH_SERVER_NO_UID to TH_FSA's fabric."), + TestStep(3, "TH instructs TH_FSA to open up commissioning window on it's aggregator."), + TestStep(4, "Follow manufacturer provided instructions to have DUT_FSA commission TH_FSA's aggregator."), + TestStep(5, "Follow manufacturer provided instructions to enable DUT_FSA to synchronize TH_SERVER_NO_UID" + " from TH_FSA onto DUT_FSA's fabric. TH to provide endpoint saved from step 2 in user prompt."), + TestStep(6, "DUT_FSA synchronizes TH_SERVER_NO_UID onto DUT_FSA's fabric and copies the UniqueID presented" + " by TH_FSA's Bridged Device Basic Information Cluster."), ] async def commission_via_commissioner_control(self, controller_node_id: int, device_node_id: int): @@ -330,73 +304,32 @@ async def commission_via_commissioner_control(self, controller_node_id: int, dev @async_test_body async def test_TC_MCORE_FS_1_4(self): self.is_ci = self.check_pics('PICS_SDK_CI_ONLY') - self.is_ci = True # Commissioning - done self.step(0) - th_server_for_dut_fsa_th_node_id = 1 - self.print_step(1, "Commissioning TH_SERVER_FOR_DUT_FSA to TH fabric") + self.step(1) + + th_server_th_node_id = 1 await self.default_controller.CommissionOnNetwork( - nodeId=th_server_for_dut_fsa_th_node_id, - setupPinCode=self.server_for_dut_passcode, + nodeId=th_server_th_node_id, + setupPinCode=self.th_server_passcode, filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, - filter=self.server_for_dut_discriminator, + filter=self.th_server_discriminator, ) - self.print_step(2, "Verify that TH_SERVER_FOR_DUT_FSA does not have a UniqueID") # FIXME: Sometimes reading the UniqueID fails... await self.read_single_attribute_expect_error( cluster=Clusters.BasicInformation, attribute=Clusters.BasicInformation.Attributes.UniqueID, - node_id=th_server_for_dut_fsa_th_node_id, + node_id=th_server_th_node_id, error=Status.UnsupportedAttribute, ) - # Get the list of endpoints on the DUT_FSA_BRIDGE before adding the TH_SERVER_FOR_DUT_FSA. - dut_fsa_bridge_endpoints = set(await self.read_single_attribute_check_success( - cluster=Clusters.Descriptor, - attribute=Clusters.Descriptor.Attributes.PartsList, - node_id=self.dut_node_id, - endpoint=0, - )) - - self.print_step(3, "Commissioning TH_SERVER_FOR_DUT_FSA to DUT_FSA fabric") - await self.commission_via_commissioner_control( - controller_node_id=self.dut_node_id, - device_node_id=th_server_for_dut_fsa_th_node_id) - - # Wait for the device to appear on the DUT_FSA_BRIDGE. - await asyncio.sleep(2) - - # Get the list of endpoints on the DUT_FSA_BRIDGE after adding the TH_SERVER_FOR_DUT_FSA. - dut_fsa_bridge_endpoints_new = set(await self.read_single_attribute_check_success( - cluster=Clusters.Descriptor, - attribute=Clusters.Descriptor.Attributes.PartsList, - node_id=self.dut_node_id, - endpoint=0, - )) - - # Get the endpoint number for just added TH_SERVER_FOR_DUT_FSA. - logging.info("Endpoints on DUT_FSA_BRIDGE: old=%s, new=%s", dut_fsa_bridge_endpoints, dut_fsa_bridge_endpoints_new) - asserts.assert_true(dut_fsa_bridge_endpoints_new.issuperset(dut_fsa_bridge_endpoints), - "Expected only new endpoints to be added") - unique_endpoints_set = dut_fsa_bridge_endpoints_new - dut_fsa_bridge_endpoints - asserts.assert_equal(len(unique_endpoints_set), 1, "Expected only one new endpoint on DUT_FSA") - dut_fsa_bridge_dut_server_endpoint = list(unique_endpoints_set)[0] - dut_fsa_bridge_endpoints = dut_fsa_bridge_endpoints_new - - self.print_step(4, "Verify that DUT_FSA created a UniqueID for TH_SERVER_FOR_DUT_FSA") - dut_fsa_bridge_dut_server_unique_id = await self.read_single_attribute_check_success( - cluster=Clusters.BridgedDeviceBasicInformation, - attribute=Clusters.BridgedDeviceBasicInformation.Attributes.UniqueID, - endpoint=dut_fsa_bridge_dut_server_endpoint) - asserts.assert_true(type_matches(dut_fsa_bridge_dut_server_unique_id, str), "UniqueID should be a string") - asserts.assert_true(dut_fsa_bridge_dut_server_unique_id, "UniqueID should not be an empty string") - logging.info("TH_SERVER_FOR_DUT_FSA on DUT_SERVER_BRIDGE UniqueID: %s", dut_fsa_bridge_dut_server_unique_id) + self.step(2) th_fsa_bridge_th_node_id = 2 - self.print_step(5, "Commissioning TH_FSA_BRIDGE to TH fabric") + # Commissioning TH_FSA_BRIDGE to TH fabric. await self.default_controller.CommissionOnNetwork( nodeId=th_fsa_bridge_th_node_id, setupPinCode=self.th_fsa_bridge_passcode, @@ -404,7 +337,7 @@ async def test_TC_MCORE_FS_1_4(self): filter=self.th_fsa_bridge_discriminator, ) - # Get the list of endpoints on the TH_FSA_BRIDGE before adding the TH_SERVER_FOR_TH_FSA. + # Get the list of endpoints on the TH_FSA_BRIDGE before adding the TH_SERVER_NO_UID. th_fsa_bridge_endpoints = set(await self.read_single_attribute_check_success( cluster=Clusters.Descriptor, attribute=Clusters.Descriptor.Attributes.PartsList, @@ -412,19 +345,28 @@ async def test_TC_MCORE_FS_1_4(self): endpoint=0, )) - th_server_for_th_fsa_th_fsa_node_id = 3 - self.print_step(6, "Commissioning TH_SERVER_FOR_TH_FSA to TH_FSA") + discriminator = random.randint(0, 4095) + # Open commissioning window on TH_SERVER_NO_UID. + params = await self.default_controller.OpenCommissioningWindow( + nodeid=th_server_th_node_id, + option=self.default_controller.CommissioningWindowPasscode.kTokenWithRandomPin, + discriminator=discriminator, + iteration=10000, + timeout=600) + + th_server_th_fsa_node_id = 3 + # Commissioning TH_SERVER_NO_UID to TH_FSA. self.th_fsa_controller.CommissionOnNetwork( - nodeId=th_server_for_th_fsa_th_fsa_node_id, - setupPinCode=self.server_for_th_passcode, + nodeId=th_server_th_fsa_node_id, + setupPinCode=params.setupPinCode, filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, - filter=self.server_for_th_discriminator, + filter=discriminator, ) # Wait some time, so the dynamic endpoint will appear on the TH_FSA_BRIDGE. await asyncio.sleep(5) - # Get the list of endpoints on the TH_FSA_BRIDGE after adding the TH_SERVER_FOR_TH_FSA. + # Get the list of endpoints on the TH_FSA_BRIDGE after adding the TH_SERVER_NO_UID. th_fsa_bridge_endpoints_new = set(await self.read_single_attribute_check_success( cluster=Clusters.Descriptor, attribute=Clusters.Descriptor.Attributes.PartsList, @@ -432,7 +374,7 @@ async def test_TC_MCORE_FS_1_4(self): endpoint=0, )) - # Get the endpoint number for just added TH_SERVER_FOR_TH_FSA. + # Get the endpoint number for just added TH_SERVER_NO_UID. logging.info("Endpoints on TH_FSA_BRIDGE: old=%s, new=%s", th_fsa_bridge_endpoints, th_fsa_bridge_endpoints_new) asserts.assert_true(th_fsa_bridge_endpoints_new.issuperset(th_fsa_bridge_endpoints), "Expected only new endpoints to be added") @@ -440,7 +382,7 @@ async def test_TC_MCORE_FS_1_4(self): asserts.assert_equal(len(unique_endpoints_set), 1, "Expected only one new endpoint") th_fsa_bridge_th_server_endpoint = list(unique_endpoints_set)[0] - self.print_step(7, "Verify that TH_FSA created a UniqueID for TH_SERVER_FOR_TH_FSA") + # Verify that TH_FSA created a UniqueID for TH_SERVER_NO_UID. th_fsa_bridge_th_server_unique_id = await self.read_single_attribute_check_success( cluster=Clusters.BridgedDeviceBasicInformation, attribute=Clusters.BridgedDeviceBasicInformation.Attributes.UniqueID, @@ -448,14 +390,12 @@ async def test_TC_MCORE_FS_1_4(self): endpoint=th_fsa_bridge_th_server_endpoint) asserts.assert_true(type_matches(th_fsa_bridge_th_server_unique_id, str), "UniqueID should be a string") asserts.assert_true(th_fsa_bridge_th_server_unique_id, "UniqueID should not be an empty string") - logging.info("TH_SERVER_FOR_TH_FSA on TH_SERVER_BRIDGE UniqueID: %s", th_fsa_bridge_th_server_unique_id) + logging.info("UniqueID generated for TH_SERVER_NO_UID: %s", th_fsa_bridge_th_server_unique_id) - # Make sure that the UniqueID on the TH_FSA_BRIDGE is different from the one on the DUT_FSA_BRIDGE. - asserts.assert_not_equal(dut_fsa_bridge_dut_server_unique_id, th_fsa_bridge_th_server_unique_id, - "UniqueID on DUT_FSA_BRIDGE and TH_FSA_BRIDGE should be different") + self.step(3) discriminator = random.randint(0, 4095) - self.print_step(8, "Open commissioning window on TH_FSA_BRIDGE") + # Open commissioning window on TH_FSA_BRIDGE. params = await self.default_controller.OpenCommissioningWindow( nodeid=th_fsa_bridge_th_node_id, option=self.default_controller.CommissioningWindowPasscode.kTokenWithRandomPin, @@ -463,10 +403,12 @@ async def test_TC_MCORE_FS_1_4(self): iteration=10000, timeout=600) - self.print_step(9, "Commissioning TH_FSA_BRIDGE to DUT_FSA fabric") + self.step(4) + + # Commissioning TH_FSA_BRIDGE to DUT_FSA fabric. if not self.is_ci: self.wait_for_user_input( - f"Commission TH Fabric-Sync Bridge on DUT using manufacturer specified mechanism.\n" + f"Commission TH_FSA's aggregator on DUT using manufacturer specified mechanism.\n" f"Use the following parameters:\n" f"- discriminator: {discriminator}\n" f"- setupPinCode: {params.setupPinCode}\n" @@ -481,12 +423,22 @@ async def test_TC_MCORE_FS_1_4(self): # Wait for the commissioning to complete. await asyncio.sleep(5) - self.print_step(10, "Synchronize TH_SERVER_FOR_TH_FSA from TH_FSA to DUT_FSA fabric") + self.step(5) + + # Get the list of endpoints on the DUT_FSA_BRIDGE before synchronization. + dut_fsa_bridge_endpoints = set(await self.read_single_attribute_check_success( + cluster=Clusters.Descriptor, + attribute=Clusters.Descriptor.Attributes.PartsList, + node_id=self.dut_node_id, + endpoint=0, + )) + + # Synchronize TH_SERVER_NO_UID from TH_FSA to DUT_FSA fabric. if not self.is_ci: self.wait_for_user_input( - f"Synchronize endpoint from TH Fabric-Sync Bridge to DUT using manufacturer specified mechanism.\n" + f"Synchronize endpoint from TH_FSA's aggregator to DUT using manufacturer specified mechanism.\n" f"Use the following parameters:\n" - f"- bridgeDynamicEndpoint: {th_fsa_bridge_th_server_endpoint}\n" + f"- endpointID: {th_fsa_bridge_th_server_endpoint}\n" f"If using FabricSync Admin, you may type:\n" f">>> fabricsync sync-device {th_fsa_bridge_th_server_endpoint}") else: @@ -495,6 +447,8 @@ async def test_TC_MCORE_FS_1_4(self): # Wait for the synchronization to complete. await asyncio.sleep(5) + self.step(6) + # Get the list of endpoints on the DUT_FSA_BRIDGE after synchronization dut_fsa_bridge_endpoints_new = set(await self.read_single_attribute_check_success( cluster=Clusters.Descriptor, @@ -503,27 +457,26 @@ async def test_TC_MCORE_FS_1_4(self): endpoint=0, )) - # Get the endpoint number for just synced TH_SERVER_FOR_TH_FSA. + # Get the endpoint number for just synced TH_SERVER_NO_UID. logging.info("Endpoints on DUT_FSA_BRIDGE: old=%s, new=%s", dut_fsa_bridge_endpoints, dut_fsa_bridge_endpoints_new) asserts.assert_true(dut_fsa_bridge_endpoints_new.issuperset(dut_fsa_bridge_endpoints), "Expected only new endpoints to be added") unique_endpoints_set = dut_fsa_bridge_endpoints_new - dut_fsa_bridge_endpoints asserts.assert_equal(len(unique_endpoints_set), 1, "Expected only one new endpoint on DUT_FSA") dut_fsa_bridge_th_server_endpoint = list(unique_endpoints_set)[0] - dut_fsa_bridge_endpoints = dut_fsa_bridge_endpoints_new - self.print_step(11, "Verify that DUT_FSA copied the TH_SERVER_FOR_TH_FSA UniqueID from TH_FSA") + # Verify that DUT_FSA copied the TH_SERVER_NO_UID UniqueID from TH_FSA. dut_fsa_bridge_th_server_unique_id = await self.read_single_attribute_check_success( cluster=Clusters.BridgedDeviceBasicInformation, attribute=Clusters.BridgedDeviceBasicInformation.Attributes.UniqueID, endpoint=dut_fsa_bridge_th_server_endpoint) asserts.assert_true(type_matches(dut_fsa_bridge_th_server_unique_id, str), "UniqueID should be a string") asserts.assert_true(dut_fsa_bridge_th_server_unique_id, "UniqueID should not be an empty string") - logging.info("TH_SERVER_FOR_TH_FSA on DUT_SERVER_BRIDGE UniqueID: %s", dut_fsa_bridge_th_server_unique_id) + logging.info("UniqueID for TH_SERVER_NO_UID on DUT_FSA: %s", th_fsa_bridge_th_server_unique_id) # Make sure that the UniqueID on the DUT_FSA_BRIDGE is the same as the one on the DUT_FSA_BRIDGE. asserts.assert_equal(dut_fsa_bridge_th_server_unique_id, th_fsa_bridge_th_server_unique_id, - "UniqueID on DUT_FSA_BRIDGE and TH_FSA_BRIDGE should be the same") + "UniqueID on DUT_FSA and TH_FSA should be the same") if __name__ == "__main__": From 04bf44164ddbd690988deaffac41170f135ade79 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Thu, 29 Aug 2024 13:17:48 +0200 Subject: [PATCH 22/33] Use volatile storage for fabric-sync-app by default --- examples/fabric-admin/scripts/fabric-sync-app.py | 16 +++++++++++----- src/python_testing/TC_MCORE_FS_1_3.py | 9 ++++----- src/python_testing/TC_MCORE_FS_1_4.py | 7 +++---- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/examples/fabric-admin/scripts/fabric-sync-app.py b/examples/fabric-admin/scripts/fabric-sync-app.py index 452493e8e4977e..757d8b4dfa154d 100755 --- a/examples/fabric-admin/scripts/fabric-sync-app.py +++ b/examples/fabric-admin/scripts/fabric-sync-app.py @@ -20,6 +20,7 @@ import signal import sys from argparse import ArgumentParser +from tempfile import TemporaryDirectory BRIDGE_COMMISSIONED = asyncio.Event() # Log message which should appear in the fabric-admin output if @@ -133,8 +134,12 @@ async def main(args): if args.commissioner_node_id == 1: raise ValueError("NodeID=1 is reserved for the local fabric-bridge") - if args.storage_dir is not None: - os.makedirs(args.storage_dir, exist_ok=True) + storage_dir = args.storage_dir + if storage_dir is not None: + os.makedirs(storage_dir, exist_ok=True) + else: + storage = TemporaryDirectory(prefix="fabric-sync-app") + storage_dir = storage.name pipe = args.stdin_pipe if pipe and not os.path.exists(pipe): @@ -151,7 +156,7 @@ def terminate(signum, frame): admin, bridge = await asyncio.gather( run_admin( args.app_admin, - storage_dir=args.storage_dir, + storage_dir=storage_dir, rpc_admin_port=args.app_admin_rpc_port, rpc_bridge_port=args.app_bridge_rpc_port, paa_trust_store_path=args.paa_trust_store_path, @@ -161,7 +166,7 @@ def terminate(signum, frame): ), run_bridge( args.app_bridge, - storage_dir=args.storage_dir, + storage_dir=storage_dir, rpc_admin_port=args.app_admin_rpc_port, rpc_bridge_port=args.app_bridge_rpc_port, secured_device_port=args.secured_device_port, @@ -226,7 +231,8 @@ def terminate(signum, frame): parser.add_argument("--stdin-pipe", metavar="PATH", help="read input from a named pipe instead of stdin") parser.add_argument("--storage-dir", metavar="PATH", - help="directory to place storage files in") + help=("directory to place storage files in; by default " + "volatile storage is used")) parser.add_argument("--paa-trust-store-path", metavar="PATH", help="path to directory holding PAA certificates") parser.add_argument("--commissioner-name", metavar="NAME", diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index 1208567b643aa4..3e318915ff4ccb 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -25,10 +25,10 @@ # === BEGIN CI TEST ARGUMENTS === # test-runner-runs: run1 # test-runner-run/run1/app: examples/fabric-admin/scripts/fabric-sync-app.py -# test-runner-run/run1/app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa/stdin --storage-dir=dut-fsa --discriminator=1234 +# test-runner-run/run1/app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa-stdin --discriminator=1234 # test-runner-run/run1/factoryreset: True # test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --string-arg th_server_no_uid_app_path:${LIGHTING_APP_NO_UNIQUE_ID} -# test-runner-run/run1/script-start-delay: 10 +# test-runner-run/run1/script-start-delay: 5 # test-runner-run/run1/quiet: false # === END CI TEST ARGUMENTS === @@ -140,8 +140,8 @@ def steps_TC_MCORE_FS_1_3(self) -> list[TestStep]: return [ TestStep(0, "Commission DUT if not done", is_commissioning=True), TestStep(1, "TH commissions TH_SERVER_NO_UID to TH's fabric"), - TestStep(2, "DUT_FSA commissions TH_SERVER_NO_UID to DUT_FSA's fabric and generates a UniqueID.", - "TH verifies a value is visible for the UniqueID from the DUT_FSA's Bridged Device Basic Information Cluster."), + # TestStep(2, "DUT_FSA commissions TH_SERVER_NO_UID to DUT_FSA's fabric and generates a UniqueID.", + # "TH verifies a value is visible for the UniqueID from the DUT_FSA's Bridged Device Basic Information Cluster."), ] async def commission_via_commissioner_control(self, controller_node_id: int, device_node_id: int): @@ -216,7 +216,6 @@ async def test_TC_MCORE_FS_1_3(self): filter=self.th_server_discriminator, ) - # FIXME: Sometimes reading the UniqueID fails... await self.read_single_attribute_expect_error( cluster=Clusters.BasicInformation, attribute=Clusters.BasicInformation.Attributes.UniqueID, diff --git a/src/python_testing/TC_MCORE_FS_1_4.py b/src/python_testing/TC_MCORE_FS_1_4.py index a9fc7d2e6570ca..187aa2691b0209 100644 --- a/src/python_testing/TC_MCORE_FS_1_4.py +++ b/src/python_testing/TC_MCORE_FS_1_4.py @@ -25,10 +25,10 @@ # === BEGIN CI TEST ARGUMENTS === # test-runner-runs: run1 # test-runner-run/run1/app: examples/fabric-admin/scripts/fabric-sync-app.py -# test-runner-run/run1/app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa/stdin --storage-dir=dut-fsa --discriminator=1234 +# test-runner-run/run1/app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa-stdin --discriminator=1234 # test-runner-run/run1/factoryreset: True -# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --string-arg th_fsa_admin_path:${FABRIC_ADMIN_APP} th_fsa_bridge_path:${FABRIC_BRIDGE_APP} th_server_no_uid_app_path:${LIGHTING_APP_NO_UNIQUE_ID} dut_fsa_stdin_pipe:dut-fsa/stdin -# test-runner-run/run1/script-start-delay: 10 +# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --string-arg th_fsa_admin_path:${FABRIC_ADMIN_APP} th_fsa_bridge_path:${FABRIC_BRIDGE_APP} th_server_no_uid_app_path:${LIGHTING_APP_NO_UNIQUE_ID} dut_fsa_stdin_pipe:dut-fsa-stdin +# test-runner-run/run1/script-start-delay: 5 # test-runner-run/run1/quiet: false # === END CI TEST ARGUMENTS === @@ -318,7 +318,6 @@ async def test_TC_MCORE_FS_1_4(self): filter=self.th_server_discriminator, ) - # FIXME: Sometimes reading the UniqueID fails... await self.read_single_attribute_expect_error( cluster=Clusters.BasicInformation, attribute=Clusters.BasicInformation.Attributes.UniqueID, From 76250fc9f7078f9aa2ee0b852384f3318e1e4dba Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Thu, 29 Aug 2024 13:18:56 +0200 Subject: [PATCH 23/33] Add TC_MCORE_FS_1_4 to exceptions --- src/python_testing/execute_python_tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/python_testing/execute_python_tests.py b/src/python_testing/execute_python_tests.py index 91db2b2614e56f..db6406a0ef81c4 100644 --- a/src/python_testing/execute_python_tests.py +++ b/src/python_testing/execute_python_tests.py @@ -75,6 +75,7 @@ def main(search_directory, env_file): "TC_MCORE_FS_1_1.py", "TC_MCORE_FS_1_2.py", "TC_MCORE_FS_1_3.py", + "TC_MCORE_FS_1_4.py", "TC_OCC_3_1.py", "TC_OCC_3_2.py", "TC_BRBINFO_4_1.py", From 31dc93fca38b33048b7764da757cc1cc4f1f5926 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Mon, 2 Sep 2024 09:35:42 +0200 Subject: [PATCH 24/33] Get rid of defaults --- .../fabric-admin/scripts/fabric-sync-app.py | 11 +++--- src/python_testing/TC_MCORE_FS_1_3.py | 4 +- src/python_testing/TC_MCORE_FS_1_4.py | 37 +++++++++++-------- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/examples/fabric-admin/scripts/fabric-sync-app.py b/examples/fabric-admin/scripts/fabric-sync-app.py index 757d8b4dfa154d..7cb2e150b8c868 100755 --- a/examples/fabric-admin/scripts/fabric-sync-app.py +++ b/examples/fabric-admin/scripts/fabric-sync-app.py @@ -22,21 +22,20 @@ from argparse import ArgumentParser from tempfile import TemporaryDirectory -BRIDGE_COMMISSIONED = asyncio.Event() +_bridge_commissioned_event = asyncio.Event() # Log message which should appear in the fabric-admin output if # the bridge is already commissioned. -BRIDGE_COMMISSIONED_MSG = b"Reading attribute: Cluster=0x0000_001D Endpoint=0x1 AttributeId=0x0000_0000" +_BRIDGE_COMMISSIONED_MSG = b"Reading attribute: Cluster=0x0000_001D Endpoint=0x1 AttributeId=0x0000_0000" async def forward_f(f_in, f_out, prefix: str): """Forward f_in to f_out with a prefix attached.""" - global BRIDGE_COMMISSIONED while True: line = await f_in.readline() if not line: break - if not BRIDGE_COMMISSIONED.is_set() and BRIDGE_COMMISSIONED_MSG in line: - BRIDGE_COMMISSIONED.set() + if not _bridge_commissioned_event.is_set() and _BRIDGE_COMMISSIONED_MSG in line: + _bridge_commissioned_event.set() f_out.buffer.write(prefix.encode()) f_out.buffer.write(line) f_out.flush() @@ -182,7 +181,7 @@ def terminate(signum, frame): # we will get the response, otherwise we will hit timeout. cmd = "descriptor read device-type-list 1 1 --timeout 1" admin.stdin.write((cmd + "\n").encode()) - await asyncio.wait_for(BRIDGE_COMMISSIONED.wait(), timeout=1.5) + await asyncio.wait_for(_bridge_commissioned_event.wait(), timeout=1.5) except asyncio.TimeoutError: # Commission the bridge to the admin. cmd = "fabricsync add-local-bridge 1" diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index 3e318915ff4ccb..359187096e6b1a 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -140,8 +140,8 @@ def steps_TC_MCORE_FS_1_3(self) -> list[TestStep]: return [ TestStep(0, "Commission DUT if not done", is_commissioning=True), TestStep(1, "TH commissions TH_SERVER_NO_UID to TH's fabric"), - # TestStep(2, "DUT_FSA commissions TH_SERVER_NO_UID to DUT_FSA's fabric and generates a UniqueID.", - # "TH verifies a value is visible for the UniqueID from the DUT_FSA's Bridged Device Basic Information Cluster."), + TestStep(2, "DUT_FSA commissions TH_SERVER_NO_UID to DUT_FSA's fabric and generates a UniqueID.", + "TH verifies a value is visible for the UniqueID from the DUT_FSA's Bridged Device Basic Information Cluster."), ] async def commission_via_commissioner_control(self, controller_node_id: int, device_node_id: int): diff --git a/src/python_testing/TC_MCORE_FS_1_4.py b/src/python_testing/TC_MCORE_FS_1_4.py index 187aa2691b0209..8ec7b02025d784 100644 --- a/src/python_testing/TC_MCORE_FS_1_4.py +++ b/src/python_testing/TC_MCORE_FS_1_4.py @@ -87,10 +87,6 @@ def stop(self): class FabricSyncApp: - APP_PATH = "examples/fabric-admin/scripts/fabric-sync-app.py" - FABRIC_ADMIN_PATH = "out/linux-x64-fabric-admin-rpc/fabric-admin" - FABRIC_BRIDGE_PATH = "out/linux-x64-fabric-bridge-rpc/fabric-bridge-app" - def _process_admin_output(self, line): if self.wait_for_text_text is not None and self.wait_for_text_text in line: self.wait_for_text_event.set() @@ -101,15 +97,16 @@ def wait_for_text(self, timeout=30): self.wait_for_text_event.clear() self.wait_for_text_text = None - def __init__(self, storageDir, fabricName=None, nodeId=None, vendorId=None, paaTrustStorePath=None, + def __init__(self, fabricSyncAppPath, fabricAdminAppPath, fabricBridgeAppPath, + storageDir, fabricName=None, nodeId=None, vendorId=None, paaTrustStorePath=None, bridgePort=None, bridgeDiscriminator=None, bridgePasscode=None): self.wait_for_text_event = threading.Event() self.wait_for_text_text = None - args = [self.APP_PATH] - args.append(f"--app-admin={self.FABRIC_ADMIN_PATH}") - args.append(f"--app-bridge={self.FABRIC_BRIDGE_PATH}") + args = [fabricSyncAppPath] + args.append(f"--app-admin={fabricAdminAppPath}") + args.append(f"--app-bridge={fabricBridgeAppPath}") # Override default ports, so it will be possible to run # our TH_FSA alongside the DUT_FSA during CI testing. args.append("--app-admin-rpc-port=44000") @@ -173,17 +170,22 @@ def default_timeout(self) -> int: def setup_class(self): super().setup_class() - # Get the path to the TH_FSA (fabric-admin and fabric-bridge) app from - # the user params or use the default path. - FabricSyncApp.APP_PATH = self.user_params.get("th_fsa_app_path", FabricSyncApp.APP_PATH) - if not os.path.exists(FabricSyncApp.APP_PATH): + # Get the path to the TH_FSA (fabric-admin and fabric-bridge) app from the user params. + th_fsa_app_path = self.user_params.get("th_fsa_app_path") + if not th_fsa_app_path: asserts.fail("This test requires a TH_FSA app. Specify app path with --string-arg th_fsa_app_path:") - FabricSyncApp.FABRIC_ADMIN_PATH = self.user_params.get("th_fsa_admin_path", FabricSyncApp.FABRIC_ADMIN_PATH) - if not os.path.exists(FabricSyncApp.FABRIC_ADMIN_PATH): + if not os.path.exists(th_fsa_app_path): + asserts.fail(f"The path {th_fsa_app_path} does not exist") + th_fsa_admin_path = self.user_params.get("th_fsa_admin_path") + if not th_fsa_admin_path: asserts.fail("This test requires a TH_FSA_ADMIN app. Specify app path with --string-arg th_fsa_admin_path:") - FabricSyncApp.FABRIC_BRIDGE_PATH = self.user_params.get("th_fsa_bridge_path", FabricSyncApp.FABRIC_BRIDGE_PATH) - if not os.path.exists(FabricSyncApp.FABRIC_BRIDGE_PATH): + if not os.path.exists(th_fsa_admin_path): + asserts.fail(f"The path {th_fsa_admin_path} does not exist") + th_fsa_bridge_path = self.user_params.get("th_fsa_bridge_path") + if not th_fsa_bridge_path: asserts.fail("This test requires a TH_FSA_BRIDGE app. Specify app path with --string-arg th_fsa_bridge_path:") + if not os.path.exists(th_fsa_bridge_path): + asserts.fail(f"The path {th_fsa_bridge_path} does not exist") # Get the path to the TH_SERVER_NO_UID app from the user params. th_server_app = self.user_params.get("th_server_no_uid_app_path", None) @@ -202,6 +204,9 @@ def setup_class(self): self.th_fsa_bridge_passcode = 20202021 self.th_fsa_controller = FabricSyncApp( + th_fsa_app_path, + th_fsa_admin_path, + th_fsa_bridge_path, storageDir=self.storage.name, paaTrustStorePath=self.matter_test_config.paa_trust_store_path, bridgePort=self.th_fsa_bridge_port, From f6cee46316955c975ad8ac314d44053f2bc54f11 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Mon, 2 Sep 2024 09:53:46 +0200 Subject: [PATCH 25/33] Document used options in open commissioning window --- examples/fabric-admin/scripts/fabric-sync-app.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/examples/fabric-admin/scripts/fabric-sync-app.py b/examples/fabric-admin/scripts/fabric-sync-app.py index 7cb2e150b8c868..d221f456901993 100755 --- a/examples/fabric-admin/scripts/fabric-sync-app.py +++ b/examples/fabric-admin/scripts/fabric-sync-app.py @@ -195,7 +195,14 @@ def terminate(signum, frame): # Open commissioning window with original setup code for the bridge, # so it can be added by the TH fabric. - cmd = "pairing open-commissioning-window 1 0 0 600 1000 0" + cwNodeId = 1 + cwEndpointId = 0 + cwOption = 0 # 0: Original setup code, 1: New setup code + cwTimeout = 600 + cwIteration = 1000 + cwDiscriminator = 0 + cmd = (f"pairing open-commissioning-window {cwNodeId} {cwEndpointId}" + f" {cwOption} {cwTimeout} {cwIteration} {cwDiscriminator}") admin.stdin.write((cmd + "\n").encode()) # Wait some time for the bridge to open the commissioning window. await asyncio.sleep(1) From 77a4b9e36f0834e67aaaaec5b1094782028291cb Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Mon, 2 Sep 2024 09:55:06 +0200 Subject: [PATCH 26/33] Speed up the pipe read busy loop --- examples/fabric-admin/scripts/fabric-sync-app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/fabric-admin/scripts/fabric-sync-app.py b/examples/fabric-admin/scripts/fabric-sync-app.py index d221f456901993..5580dd0310e620 100755 --- a/examples/fabric-admin/scripts/fabric-sync-app.py +++ b/examples/fabric-admin/scripts/fabric-sync-app.py @@ -55,9 +55,9 @@ async def forward_pipe(pipe_path: str, f_out: asyncio.StreamWriter): if data: f_out.write(data) if not data: - await asyncio.sleep(1) + await asyncio.sleep(0.1) except BlockingIOError: - await asyncio.sleep(1) + await asyncio.sleep(0.1) async def forward_stdin(f_out: asyncio.StreamWriter): From 88020ad3ef4e226c0a1a8dca54aefcb79fb3ccba Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Mon, 2 Sep 2024 10:15:25 +0200 Subject: [PATCH 27/33] Refactor local output processing --- .../fabric-admin/scripts/fabric-sync-app.py | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/examples/fabric-admin/scripts/fabric-sync-app.py b/examples/fabric-admin/scripts/fabric-sync-app.py index 5580dd0310e620..a03e198efdc14b 100755 --- a/examples/fabric-admin/scripts/fabric-sync-app.py +++ b/examples/fabric-admin/scripts/fabric-sync-app.py @@ -19,24 +19,23 @@ import os import signal import sys +import typing from argparse import ArgumentParser from tempfile import TemporaryDirectory -_bridge_commissioned_event = asyncio.Event() -# Log message which should appear in the fabric-admin output if -# the bridge is already commissioned. -_BRIDGE_COMMISSIONED_MSG = b"Reading attribute: Cluster=0x0000_001D Endpoint=0x1 AttributeId=0x0000_0000" +async def forward_f(f_in, f_out: typing.BinaryIO, prefix: bytes, cb=None): + """Forward f_in to f_out with a prefix attached. -async def forward_f(f_in, f_out, prefix: str): - """Forward f_in to f_out with a prefix attached.""" + This function can optionally feed received lines to a callback function. + """ while True: line = await f_in.readline() if not line: break - if not _bridge_commissioned_event.is_set() and _BRIDGE_COMMISSIONED_MSG in line: - _bridge_commissioned_event.set() - f_out.buffer.write(prefix.encode()) + if cb is not None: + cb(line) + f_out.buffer.write(prefix) f_out.buffer.write(line) f_out.flush() @@ -69,25 +68,26 @@ async def forward_stdin(f_out: asyncio.StreamWriter): while True: line = await reader.readline() if not line: + # Exit on Ctrl-D (EOF). sys.exit(0) f_out.write(line) -async def run(tag, program, *args, stdin=None): +async def run(tag, program, *args, stdin=None, stdout_cb=None): p = await asyncio.create_subprocess_exec(program, *args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, stdin=stdin) # Add the stdout and stderr processing to the event loop. - asyncio.create_task(forward_f(p.stderr, sys.stderr, tag)) - asyncio.create_task(forward_f(p.stdout, sys.stdout, tag)) + asyncio.create_task(forward_f(p.stderr, sys.stderr, tag.encode())) + asyncio.create_task(forward_f(p.stdout, sys.stdout, tag.encode(), cb=stdout_cb)) return p -async def run_admin(program, storage_dir=None, rpc_admin_port=None, - rpc_bridge_port=None, paa_trust_store_path=None, - commissioner_name=None, commissioner_node_id=None, - commissioner_vendor_id=None): +async def run_admin(program, stdout_cb=None, storage_dir=None, + rpc_admin_port=None, rpc_bridge_port=None, + paa_trust_store_path=None, commissioner_name=None, + commissioner_node_id=None, commissioner_vendor_id=None): args = [] if storage_dir is not None: args.extend(["--storage-directory", storage_dir]) @@ -104,7 +104,8 @@ async def run_admin(program, storage_dir=None, rpc_admin_port=None, if commissioner_vendor_id is not None: args.extend(["--commissioner-vendor-id", str(commissioner_vendor_id)]) return await run("[FS-ADMIN]", program, "interactive", "start", *args, - stdin=asyncio.subprocess.PIPE) + stdin=asyncio.subprocess.PIPE, + stdout_cb=stdout_cb) async def run_bridge(program, storage_dir=None, rpc_admin_port=None, @@ -152,9 +153,19 @@ def terminate(signum, frame): signal.signal(signal.SIGINT, terminate) signal.signal(signal.SIGTERM, terminate) + bridge_commissioned_event = asyncio.Event() + # Log message which should appear in the fabric-admin output if + # the bridge is already commissioned. + _BRIDGE_COMMISSIONED_MSG = b"Reading attribute: Cluster=0x0000_001D Endpoint=0x1 AttributeId=0x0000_0000" + + def check_for_commissioned_bridge(line: bytes): + if not bridge_commissioned_event.is_set() and _BRIDGE_COMMISSIONED_MSG in line: + bridge_commissioned_event.set() + admin, bridge = await asyncio.gather( run_admin( args.app_admin, + stdout_cb=check_for_commissioned_bridge, storage_dir=storage_dir, rpc_admin_port=args.app_admin_rpc_port, rpc_bridge_port=args.app_bridge_rpc_port, @@ -181,7 +192,7 @@ def terminate(signum, frame): # we will get the response, otherwise we will hit timeout. cmd = "descriptor read device-type-list 1 1 --timeout 1" admin.stdin.write((cmd + "\n").encode()) - await asyncio.wait_for(_bridge_commissioned_event.wait(), timeout=1.5) + await asyncio.wait_for(bridge_commissioned_event.wait(), timeout=1.5) except asyncio.TimeoutError: # Commission the bridge to the admin. cmd = "fabricsync add-local-bridge 1" @@ -204,8 +215,6 @@ def terminate(signum, frame): cmd = (f"pairing open-commissioning-window {cwNodeId} {cwEndpointId}" f" {cwOption} {cwTimeout} {cwIteration} {cwDiscriminator}") admin.stdin.write((cmd + "\n").encode()) - # Wait some time for the bridge to open the commissioning window. - await asyncio.sleep(1) try: await asyncio.gather( From 908a265caac0193d14f3b09b73d7d0f3af944148 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Mon, 2 Sep 2024 11:38:36 +0200 Subject: [PATCH 28/33] Improve wait for output --- .../fabric-admin/scripts/fabric-sync-app.py | 122 +++++++++++------- src/python_testing/TC_MCORE_FS_1_3.py | 3 - src/python_testing/TC_MCORE_FS_1_4.py | 5 +- 3 files changed, 79 insertions(+), 51 deletions(-) diff --git a/examples/fabric-admin/scripts/fabric-sync-app.py b/examples/fabric-admin/scripts/fabric-sync-app.py index a03e198efdc14b..8005346c08c8f9 100755 --- a/examples/fabric-admin/scripts/fabric-sync-app.py +++ b/examples/fabric-admin/scripts/fabric-sync-app.py @@ -24,7 +24,8 @@ from tempfile import TemporaryDirectory -async def forward_f(f_in, f_out: typing.BinaryIO, prefix: bytes, cb=None): +async def forward_f(f_in: asyncio.StreamReader, f_out: typing.BinaryIO, + prefix: bytes, cb=None): """Forward f_in to f_out with a prefix attached. This function can optionally feed received lines to a callback function. @@ -73,15 +74,49 @@ async def forward_stdin(f_out: asyncio.StreamWriter): f_out.write(line) -async def run(tag, program, *args, stdin=None, stdout_cb=None): - p = await asyncio.create_subprocess_exec(program, *args, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - stdin=stdin) - # Add the stdout and stderr processing to the event loop. - asyncio.create_task(forward_f(p.stderr, sys.stderr, tag.encode())) - asyncio.create_task(forward_f(p.stdout, sys.stdout, tag.encode(), cb=stdout_cb)) - return p +class Subprocess: + + def __init__(self, tag: str, program: str, *args, stdout_cb=None): + self.event = asyncio.Event() + self.tag = tag.encode() + self.program = program + self.args = args + self.stdout_cb = stdout_cb + self.expected_output = None + + def _check_output(self, line: bytes): + if self.expected_output is not None and self.expected_output in line: + self.event.set() + + async def run(self): + self.p = await asyncio.create_subprocess_exec(self.program, *self.args, + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE) + # Add the stdout and stderr processing to the event loop. + asyncio.create_task(forward_f(self.p.stderr, sys.stderr, self.tag)) + asyncio.create_task(forward_f(self.p.stdout, sys.stdout, self.tag, + cb=self._check_output)) + + async def send(self, message: str, expected_output: str = None, timeout: float = None): + """Send a message to a process and optionally wait for a response.""" + + if expected_output is not None: + self.expected_output = expected_output.encode() + self.event.clear() + + self.p.stdin.write((message + "\n").encode()) + await self.p.stdin.drain() + + if expected_output is not None: + await asyncio.wait_for(self.event.wait(), timeout=timeout) + self.expected_output = None + + async def wait(self): + await self.p.wait() + + def terminate(self): + self.p.terminate() async def run_admin(program, stdout_cb=None, storage_dir=None, @@ -103,9 +138,10 @@ async def run_admin(program, stdout_cb=None, storage_dir=None, args.extend(["--commissioner-nodeid", str(commissioner_node_id)]) if commissioner_vendor_id is not None: args.extend(["--commissioner-vendor-id", str(commissioner_vendor_id)]) - return await run("[FS-ADMIN]", program, "interactive", "start", *args, - stdin=asyncio.subprocess.PIPE, - stdout_cb=stdout_cb) + p = Subprocess("[FS-ADMIN]", program, "interactive", "start", *args, + stdout_cb=stdout_cb) + await p.run() + return p async def run_bridge(program, storage_dir=None, rpc_admin_port=None, @@ -125,14 +161,18 @@ async def run_bridge(program, storage_dir=None, rpc_admin_port=None, args.extend(["--passcode", str(passcode)]) if secured_device_port is not None: args.extend(["--secured-device-port", str(secured_device_port)]) - return await run("[FS-BRIDGE]", program, *args, - stdin=asyncio.subprocess.DEVNULL) + p = Subprocess("[FS-BRIDGE]", program, *args) + await p.run() + return p async def main(args): - if args.commissioner_node_id == 1: - raise ValueError("NodeID=1 is reserved for the local fabric-bridge") + # Node ID of the bridge on the fabric. + bridge_node_id = 1 + + if args.commissioner_node_id == bridge_node_id: + raise ValueError(f"NodeID={bridge_node_id} is reserved for the local fabric-bridge") storage_dir = args.storage_dir if storage_dir is not None: @@ -153,19 +193,9 @@ def terminate(signum, frame): signal.signal(signal.SIGINT, terminate) signal.signal(signal.SIGTERM, terminate) - bridge_commissioned_event = asyncio.Event() - # Log message which should appear in the fabric-admin output if - # the bridge is already commissioned. - _BRIDGE_COMMISSIONED_MSG = b"Reading attribute: Cluster=0x0000_001D Endpoint=0x1 AttributeId=0x0000_0000" - - def check_for_commissioned_bridge(line: bytes): - if not bridge_commissioned_event.is_set() and _BRIDGE_COMMISSIONED_MSG in line: - bridge_commissioned_event.set() - admin, bridge = await asyncio.gather( run_admin( args.app_admin, - stdout_cb=check_for_commissioned_bridge, storage_dir=storage_dir, rpc_admin_port=args.app_admin_rpc_port, rpc_bridge_port=args.app_bridge_rpc_port, @@ -190,35 +220,39 @@ def check_for_commissioned_bridge(line: bytes): try: # Check whether the bridge is already commissioned. If it is, # we will get the response, otherwise we will hit timeout. - cmd = "descriptor read device-type-list 1 1 --timeout 1" - admin.stdin.write((cmd + "\n").encode()) - await asyncio.wait_for(bridge_commissioned_event.wait(), timeout=1.5) + await admin.send( + f"descriptor read device-type-list {bridge_node_id} 1 --timeout 1", + # Log message which should appear in the fabric-admin output if + # the bridge is already commissioned. + expected_output="Reading attribute: Cluster=0x0000_001D Endpoint=0x1 AttributeId=0x0000_0000", + timeout=1.5) except asyncio.TimeoutError: # Commission the bridge to the admin. - cmd = "fabricsync add-local-bridge 1" + cmd = f"fabricsync add-local-bridge {bridge_node_id}" if args.passcode is not None: cmd += f" --setup-pin-code {args.passcode}" if args.secured_device_port is not None: cmd += f" --local-port {args.secured_device_port}" - admin.stdin.write((cmd + "\n").encode()) - # Wait for the bridge to be commissioned. - await asyncio.sleep(5) + await admin.send( + cmd, + # Wait for the log message indicating that the bridge has been + # added to the fabric. + f"Commissioning complete for node ID {bridge_node_id:#018x}: success") # Open commissioning window with original setup code for the bridge, # so it can be added by the TH fabric. - cwNodeId = 1 - cwEndpointId = 0 - cwOption = 0 # 0: Original setup code, 1: New setup code - cwTimeout = 600 - cwIteration = 1000 - cwDiscriminator = 0 - cmd = (f"pairing open-commissioning-window {cwNodeId} {cwEndpointId}" - f" {cwOption} {cwTimeout} {cwIteration} {cwDiscriminator}") - admin.stdin.write((cmd + "\n").encode()) + cw_endpoint_id = 0 + cw_option = 0 # 0: Original setup code, 1: New setup code + cw_timeout = 600 + cw_iteration = 1000 + cw_discriminator = 0 + cmd = (f"pairing open-commissioning-window {bridge_node_id} {cw_endpoint_id}" + f" {cw_option} {cw_timeout} {cw_iteration} {cw_discriminator}") + await admin.send(cmd) try: await asyncio.gather( - forward_pipe(pipe, admin.stdin) if pipe else forward_stdin(admin.stdin), + forward_pipe(pipe, admin.p.stdin) if pipe else forward_stdin(admin.p.stdin), admin.wait(), bridge.wait(), ) diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index 359187096e6b1a..c70f6fe6bccde7 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -196,9 +196,6 @@ async def commission_via_commissioner_control(self, controller_node_id: int, dev timedRequestTimeoutMs=5000, ) - if not self.is_ci: - await asyncio.sleep(30) - @async_test_body async def test_TC_MCORE_FS_1_3(self): self.is_ci = self.check_pics('PICS_SDK_CI_ONLY') diff --git a/src/python_testing/TC_MCORE_FS_1_4.py b/src/python_testing/TC_MCORE_FS_1_4.py index 8ec7b02025d784..56ff2eb8652619 100644 --- a/src/python_testing/TC_MCORE_FS_1_4.py +++ b/src/python_testing/TC_MCORE_FS_1_4.py @@ -27,7 +27,7 @@ # test-runner-run/run1/app: examples/fabric-admin/scripts/fabric-sync-app.py # test-runner-run/run1/app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa-stdin --discriminator=1234 # test-runner-run/run1/factoryreset: True -# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --string-arg th_fsa_admin_path:${FABRIC_ADMIN_APP} th_fsa_bridge_path:${FABRIC_BRIDGE_APP} th_server_no_uid_app_path:${LIGHTING_APP_NO_UNIQUE_ID} dut_fsa_stdin_pipe:dut-fsa-stdin +# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --string-arg th_fsa_app_path:examples/fabric-admin/scripts/fabric-sync-app.py th_fsa_admin_path:${FABRIC_ADMIN_APP} th_fsa_bridge_path:${FABRIC_BRIDGE_APP} th_server_no_uid_app_path:${LIGHTING_APP_NO_UNIQUE_ID} dut_fsa_stdin_pipe:dut-fsa-stdin # test-runner-run/run1/script-start-delay: 5 # test-runner-run/run1/quiet: false # === END CI TEST ARGUMENTS === @@ -303,9 +303,6 @@ async def commission_via_commissioner_control(self, controller_node_id: int, dev timedRequestTimeoutMs=5000, ) - if not self.is_ci: - await asyncio.sleep(30) - @async_test_body async def test_TC_MCORE_FS_1_4(self): self.is_ci = self.check_pics('PICS_SDK_CI_ONLY') From 6d2ea6c061ad8eecd9c4fd59052c9c8506f0b715 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Mon, 2 Sep 2024 12:04:21 +0200 Subject: [PATCH 29/33] Add FS-sync tests to CI --- .github/workflows/tests.yaml | 6 ++++++ src/python_testing/TC_MCORE_FS_1_3.py | 2 +- src/python_testing/TC_MCORE_FS_1_4.py | 2 +- src/python_testing/execute_python_tests.py | 2 -- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b37ed3c6ca4370..5265f82b09454c 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -486,6 +486,9 @@ jobs: --target linux-x64-microwave-oven-ipv6only-no-ble-no-wifi-tsan-clang-test \ --target linux-x64-rvc-ipv6only-no-ble-no-wifi-tsan-clang-test \ --target linux-x64-network-manager-ipv6only-no-ble-no-wifi-tsan-clang-test \ + --target linux-x64-fabric-admin-rpc-ipv6only-clang \ + --target linux-x64-fabric-bridge-rpc-ipv6only-no-ble-no-wifi-clang \ + --target linux-x64-light-data-model-no-unique-id-ipv6only-no-ble-no-wifi-clang \ --target linux-x64-python-bindings \ build \ --copy-artifacts-to objdir-clone \ @@ -500,6 +503,9 @@ jobs: echo "CHIP_MICROWAVE_OVEN_APP: out/linux-x64-microwave-oven-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-microwave-oven-app" >> /tmp/test_env.yaml echo "CHIP_RVC_APP: out/linux-x64-rvc-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-rvc-app" >> /tmp/test_env.yaml echo "NETWORK_MANAGEMENT_APP: out/linux-x64-network-manager-ipv6only-no-ble-no-wifi-tsan-clang-test/matter-network-manager-app" >> /tmp/test_env.yaml + echo "FABRIC_ADMIN_APP: out/linux-x64-fabric-admin-rpc-ipv6only-clang/fabric-admin" >> /tmp/test_env.yaml + echo "FABRIC_BRIDGE_APP: out/linux-x64-fabric-bridge-rpc-ipv6only-no-ble-no-wifi-clang/fabric-bridge-app" >> /tmp/test_env.yaml + echo "LIGHTING_APP_NO_UNIQUE_ID: out/linux-x64-light-data-model-no-unique-id-ipv6only-no-ble-no-wifi-clang/chip-lighting-app" >> /tmp/test_env.yaml echo "TRACE_APP: out/trace_data/app-{SCRIPT_BASE_NAME}" >> /tmp/test_env.yaml echo "TRACE_TEST_JSON: out/trace_data/test-{SCRIPT_BASE_NAME}" >> /tmp/test_env.yaml echo "TRACE_TEST_PERFETTO: out/trace_data/test-{SCRIPT_BASE_NAME}" >> /tmp/test_env.yaml diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index c70f6fe6bccde7..c4c0a6a1cc877c 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -27,7 +27,7 @@ # test-runner-run/run1/app: examples/fabric-admin/scripts/fabric-sync-app.py # test-runner-run/run1/app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa-stdin --discriminator=1234 # test-runner-run/run1/factoryreset: True -# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --string-arg th_server_no_uid_app_path:${LIGHTING_APP_NO_UNIQUE_ID} +# test-runner-run/run1/script-args: --PICS src/app/tests/suites/certification/ci-pics-values --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --string-arg th_server_no_uid_app_path:${LIGHTING_APP_NO_UNIQUE_ID} # test-runner-run/run1/script-start-delay: 5 # test-runner-run/run1/quiet: false # === END CI TEST ARGUMENTS === diff --git a/src/python_testing/TC_MCORE_FS_1_4.py b/src/python_testing/TC_MCORE_FS_1_4.py index 56ff2eb8652619..28df32d8414944 100644 --- a/src/python_testing/TC_MCORE_FS_1_4.py +++ b/src/python_testing/TC_MCORE_FS_1_4.py @@ -27,7 +27,7 @@ # test-runner-run/run1/app: examples/fabric-admin/scripts/fabric-sync-app.py # test-runner-run/run1/app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa-stdin --discriminator=1234 # test-runner-run/run1/factoryreset: True -# test-runner-run/run1/script-args: --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --string-arg th_fsa_app_path:examples/fabric-admin/scripts/fabric-sync-app.py th_fsa_admin_path:${FABRIC_ADMIN_APP} th_fsa_bridge_path:${FABRIC_BRIDGE_APP} th_server_no_uid_app_path:${LIGHTING_APP_NO_UNIQUE_ID} dut_fsa_stdin_pipe:dut-fsa-stdin +# test-runner-run/run1/script-args: --PICS src/app/tests/suites/certification/ci-pics-values --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --string-arg th_fsa_app_path:examples/fabric-admin/scripts/fabric-sync-app.py th_fsa_admin_path:${FABRIC_ADMIN_APP} th_fsa_bridge_path:${FABRIC_BRIDGE_APP} th_server_no_uid_app_path:${LIGHTING_APP_NO_UNIQUE_ID} dut_fsa_stdin_pipe:dut-fsa-stdin # test-runner-run/run1/script-start-delay: 5 # test-runner-run/run1/quiet: false # === END CI TEST ARGUMENTS === diff --git a/src/python_testing/execute_python_tests.py b/src/python_testing/execute_python_tests.py index 1b56432b447b5b..d508692a842846 100644 --- a/src/python_testing/execute_python_tests.py +++ b/src/python_testing/execute_python_tests.py @@ -74,8 +74,6 @@ def main(search_directory, env_file): "TC_TMP_2_1.py", "TC_MCORE_FS_1_1.py", "TC_MCORE_FS_1_2.py", - "TC_MCORE_FS_1_3.py", - "TC_MCORE_FS_1_4.py", "TC_MCORE_FS_1_5.py", "TC_OCC_3_1.py", "TC_OCC_3_2.py", From 34befd4185fdbeada13316c3692013697acbb963 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Mon, 2 Sep 2024 12:34:44 +0200 Subject: [PATCH 30/33] Improve Python code style --- src/python_testing/TC_MCORE_FS_1_3.py | 6 +- src/python_testing/TC_MCORE_FS_1_4.py | 89 ++++++++++++++------------- 2 files changed, 48 insertions(+), 47 deletions(-) diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index c4c0a6a1cc877c..3c75788032b3bd 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -84,10 +84,10 @@ def stop(self): class AppServer: - def __init__(self, app, storageDir, port=None, discriminator=None, passcode=None): + def __init__(self, app, storage_dir, port=None, discriminator=None, passcode=None): args = [app] - args.extend(["--KVS", tempfile.mkstemp(dir=storageDir, prefix="kvs-app-")[1]]) + args.extend(["--KVS", tempfile.mkstemp(dir=storage_dir, prefix="kvs-app-")[1]]) args.extend(['--secured-device-port', str(port)]) args.extend(["--discriminator", str(discriminator)]) args.extend(["--passcode", str(passcode)]) @@ -126,7 +126,7 @@ def setup_class(self): # Start the TH_SERVER_NO_UID app. self.th_server = AppServer( th_server_app, - storageDir=self.storage.name, + storage_dir=self.storage.name, port=self.th_server_port, discriminator=self.th_server_discriminator, passcode=self.th_server_passcode) diff --git a/src/python_testing/TC_MCORE_FS_1_4.py b/src/python_testing/TC_MCORE_FS_1_4.py index 28df32d8414944..4f217e1717a756 100644 --- a/src/python_testing/TC_MCORE_FS_1_4.py +++ b/src/python_testing/TC_MCORE_FS_1_4.py @@ -71,14 +71,14 @@ def run(self): self.p = subprocess.Popen(self.args, errors="ignore", stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Forward stdout and stderr with a tag attached. - t1 = threading.Thread(target=self.forward_f, args=[self.p.stdout, sys.stdout]) - t1.start() - t2 = threading.Thread(target=self.forward_f, args=[self.p.stderr, sys.stderr]) - t2.start() + forwarding_stdout_thread = threading.Thread(target=self.forward_f, args=[self.p.stdout, sys.stdout]) + forwarding_stdout_thread.start() + forwarding_stderr_thread = threading.Thread(target=self.forward_f, args=[self.p.stderr, sys.stderr]) + forwarding_stderr_thread.start() # Wait for the process to finish. self.p.wait() - t1.join() - t2.join() + forwarding_stdout_thread.join() + forwarding_stderr_thread.join() def stop(self): self.p.terminate() @@ -97,59 +97,60 @@ def wait_for_text(self, timeout=30): self.wait_for_text_event.clear() self.wait_for_text_text = None - def __init__(self, fabricSyncAppPath, fabricAdminAppPath, fabricBridgeAppPath, - storageDir, fabricName=None, nodeId=None, vendorId=None, paaTrustStorePath=None, - bridgePort=None, bridgeDiscriminator=None, bridgePasscode=None): + def __init__(self, fabric_sync_app_path, fabric_admin_app_path, fabric_bridge_app_path, + storage_dir, fabric_name=None, node_id=None, vendor_id=None, + paa_trust_store_path=None, bridge_port=None, bridge_discriminator=None, + bridge_passcode=None): self.wait_for_text_event = threading.Event() self.wait_for_text_text = None - args = [fabricSyncAppPath] - args.append(f"--app-admin={fabricAdminAppPath}") - args.append(f"--app-bridge={fabricBridgeAppPath}") + args = [fabric_sync_app_path] + args.append(f"--app-admin={fabric_admin_app_path}") + args.append(f"--app-bridge={fabric_bridge_app_path}") # Override default ports, so it will be possible to run # our TH_FSA alongside the DUT_FSA during CI testing. args.append("--app-admin-rpc-port=44000") args.append("--app-bridge-rpc-port=44001") # Keep the storage directory in a temporary location. - args.append(f"--storage-dir={storageDir}") + args.append(f"--storage-dir={storage_dir}") # FIXME: Passing custom PAA store breaks something - # if paaTrustStorePath is not None: - # args.append(f"--paa-trust-store-path={paaTrustStorePath}") - if fabricName is not None: - args.append(f"--commissioner-name={fabricName}") - if nodeId is not None: - args.append(f"--commissioner-node-id={nodeId}") - args.append(f"--commissioner-vendor-id={vendorId}") - args.append(f"--secured-device-port={bridgePort}") - args.append(f"--discriminator={bridgeDiscriminator}") - args.append(f"--passcode={bridgePasscode}") - - self.admin = Subprocess(args, stdout_cb=self._process_admin_output) + # if paa_trust_store_path is not None: + # args.append(f"--paa-trust-store-path={paa_trust_store_path}") + if fabric_name is not None: + args.append(f"--commissioner-name={fabric_name}") + if node_id is not None: + args.append(f"--commissioner-node-id={node_id}") + args.append(f"--commissioner-vendor-id={vendor_id}") + args.append(f"--secured-device-port={bridge_port}") + args.append(f"--discriminator={bridge_discriminator}") + args.append(f"--passcode={bridge_passcode}") + + self.fabric_sync_app = Subprocess(args, stdout_cb=self._process_admin_output) self.wait_for_text_text = "Connected to Fabric-Bridge" - self.admin.start() + self.fabric_sync_app.start() # Wait for the bridge to connect to the admin. self.wait_for_text() - def CommissionOnNetwork(self, nodeId, setupPinCode=None, filterType=None, filter=None): - self.wait_for_text_text = f"Commissioning complete for node ID 0x{nodeId:016x}: success" + def commission_on_network(self, node_id, setup_pin_code=None, filter_type=None, filter=None): + self.wait_for_text_text = f"Commissioning complete for node ID {node_id:#018x}: success" # Send the commissioning command to the admin. - self.admin.p.stdin.write(f"pairing onnetwork {nodeId} {setupPinCode}\n") - self.admin.p.stdin.flush() + self.fabric_sync_app.p.stdin.write(f"pairing onnetwork {node_id} {setup_pin_code}\n") + self.fabric_sync_app.p.stdin.flush() # Wait for success message. self.wait_for_text() def stop(self): - self.admin.stop() + self.fabric_sync_app.stop() class AppServer: - def __init__(self, app, storageDir, port=None, discriminator=None, passcode=None): + def __init__(self, app, storage_dir, port=None, discriminator=None, passcode=None): args = [app] - args.extend(["--KVS", tempfile.mkstemp(dir=storageDir, prefix="kvs-app-")[1]]) + args.extend(["--KVS", tempfile.mkstemp(dir=storage_dir, prefix="kvs-app-")[1]]) args.extend(['--secured-device-port', str(port)]) args.extend(["--discriminator", str(discriminator)]) args.extend(["--passcode", str(passcode)]) @@ -207,12 +208,12 @@ def setup_class(self): th_fsa_app_path, th_fsa_admin_path, th_fsa_bridge_path, - storageDir=self.storage.name, - paaTrustStorePath=self.matter_test_config.paa_trust_store_path, - bridgePort=self.th_fsa_bridge_port, - bridgeDiscriminator=self.th_fsa_bridge_discriminator, - bridgePasscode=self.th_fsa_bridge_passcode, - vendorId=0xFFF1) + storage_dir=self.storage.name, + paa_trust_store_path=self.matter_test_config.paa_trust_store_path, + bridge_port=self.th_fsa_bridge_port, + bridge_discriminator=self.th_fsa_bridge_discriminator, + bridge_passcode=self.th_fsa_bridge_passcode, + vendor_id=0xFFF1) # Get the named pipe path for the DUT_FSA app input from the user params. dut_fsa_stdin_pipe = self.user_params.get("dut_fsa_stdin_pipe", None) @@ -226,7 +227,7 @@ def setup_class(self): # Start the TH_SERVER_NO_UID app. self.th_server = AppServer( th_server_app, - storageDir=self.storage.name, + storage_dir=self.storage.name, port=self.th_server_port, discriminator=self.th_server_discriminator, passcode=self.th_server_passcode) @@ -357,10 +358,10 @@ async def test_TC_MCORE_FS_1_4(self): th_server_th_fsa_node_id = 3 # Commissioning TH_SERVER_NO_UID to TH_FSA. - self.th_fsa_controller.CommissionOnNetwork( - nodeId=th_server_th_fsa_node_id, - setupPinCode=params.setupPinCode, - filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, + self.th_fsa_controller.commission_on_network( + node_id=th_server_th_fsa_node_id, + setup_pin_code=params.setupPinCode, + filter_type=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, filter=discriminator, ) From a7ff55e3936bdcf349b1a9cc9a66775acc4f4ff8 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Mon, 2 Sep 2024 12:45:27 +0200 Subject: [PATCH 31/33] Fix wait for fabric-sync-app start --- src/python_testing/TC_MCORE_FS_1_4.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/python_testing/TC_MCORE_FS_1_4.py b/src/python_testing/TC_MCORE_FS_1_4.py index 4f217e1717a756..6b571f876c27f0 100644 --- a/src/python_testing/TC_MCORE_FS_1_4.py +++ b/src/python_testing/TC_MCORE_FS_1_4.py @@ -127,10 +127,10 @@ def __init__(self, fabric_sync_app_path, fabric_admin_app_path, fabric_bridge_ap args.append(f"--passcode={bridge_passcode}") self.fabric_sync_app = Subprocess(args, stdout_cb=self._process_admin_output) - self.wait_for_text_text = "Connected to Fabric-Bridge" + self.wait_for_text_text = "Successfully opened pairing window on the device" self.fabric_sync_app.start() - # Wait for the bridge to connect to the admin. + # Wait for the fabric-sync-app to be ready. self.wait_for_text() def commission_on_network(self, node_id, setup_pin_code=None, filter_type=None, filter=None): From 356328e223f9711078e6b31cd281713a57c1b55a Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Mon, 2 Sep 2024 14:58:53 +0200 Subject: [PATCH 32/33] Fix asyncio forwarder --- .../fabric-admin/scripts/fabric-sync-app.py | 46 +++++++++++++------ src/python_testing/execute_python_tests.py | 2 + 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/examples/fabric-admin/scripts/fabric-sync-app.py b/examples/fabric-admin/scripts/fabric-sync-app.py index 8005346c08c8f9..0f6385fa5aec53 100755 --- a/examples/fabric-admin/scripts/fabric-sync-app.py +++ b/examples/fabric-admin/scripts/fabric-sync-app.py @@ -19,13 +19,30 @@ import os import signal import sys -import typing from argparse import ArgumentParser from tempfile import TemporaryDirectory -async def forward_f(f_in: asyncio.StreamReader, f_out: typing.BinaryIO, - prefix: bytes, cb=None): +async def asyncio_stdin() -> asyncio.StreamReader: + """Wrap sys.stdin in an asyncio StreamReader.""" + loop = asyncio.get_event_loop() + reader = asyncio.StreamReader() + protocol = asyncio.StreamReaderProtocol(reader) + await loop.connect_read_pipe(lambda: protocol, sys.stdin) + return reader + + +async def asyncio_stdout(file=sys.stdout) -> asyncio.StreamWriter: + """Wrap an IO stream in an asyncio StreamWriter.""" + loop = asyncio.get_event_loop() + transport, protocol = await loop.connect_write_pipe( + lambda: asyncio.streams.FlowControlMixin(loop=loop), + os.fdopen(file.fileno(), 'wb')) + return asyncio.streams.StreamWriter(transport, protocol, None, loop) + + +async def forward_f(prefix: bytes, f_in: asyncio.StreamReader, + f_out: asyncio.StreamWriter, cb=None): """Forward f_in to f_out with a prefix attached. This function can optionally feed received lines to a callback function. @@ -36,9 +53,9 @@ async def forward_f(f_in: asyncio.StreamReader, f_out: typing.BinaryIO, break if cb is not None: cb(line) - f_out.buffer.write(prefix) - f_out.buffer.write(line) - f_out.flush() + f_out.write(prefix) + f_out.write(line) + await f_out.drain() async def forward_pipe(pipe_path: str, f_out: asyncio.StreamWriter): @@ -62,10 +79,7 @@ async def forward_pipe(pipe_path: str, f_out: asyncio.StreamWriter): async def forward_stdin(f_out: asyncio.StreamWriter): """Forward stdin to f_out.""" - loop = asyncio.get_event_loop() - reader = asyncio.StreamReader() - protocol = asyncio.StreamReaderProtocol(reader) - await loop.connect_read_pipe(lambda: protocol, sys.stdin) + reader = await asyncio_stdin() while True: line = await reader.readline() if not line: @@ -94,9 +108,15 @@ async def run(self): stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) # Add the stdout and stderr processing to the event loop. - asyncio.create_task(forward_f(self.p.stderr, sys.stderr, self.tag)) - asyncio.create_task(forward_f(self.p.stdout, sys.stdout, self.tag, - cb=self._check_output)) + asyncio.create_task(forward_f( + self.tag, + self.p.stderr, + await asyncio_stdout(sys.stderr))) + asyncio.create_task(forward_f( + self.tag, + self.p.stdout, + await asyncio_stdout(sys.stdout), + cb=self._check_output)) async def send(self, message: str, expected_output: str = None, timeout: float = None): """Send a message to a process and optionally wait for a response.""" diff --git a/src/python_testing/execute_python_tests.py b/src/python_testing/execute_python_tests.py index d508692a842846..1b56432b447b5b 100644 --- a/src/python_testing/execute_python_tests.py +++ b/src/python_testing/execute_python_tests.py @@ -74,6 +74,8 @@ def main(search_directory, env_file): "TC_TMP_2_1.py", "TC_MCORE_FS_1_1.py", "TC_MCORE_FS_1_2.py", + "TC_MCORE_FS_1_3.py", + "TC_MCORE_FS_1_4.py", "TC_MCORE_FS_1_5.py", "TC_OCC_3_1.py", "TC_OCC_3_2.py", From 1115a406d73dea0202545e9ebbb5dd452be9210a Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Mon, 2 Sep 2024 16:23:24 +0200 Subject: [PATCH 33/33] Fixes for review comments --- .../fabric-admin/scripts/fabric-sync-app.py | 8 +- src/python_testing/TC_MCORE_FS_1_3.py | 10 ++- src/python_testing/TC_MCORE_FS_1_4.py | 77 ++++--------------- 3 files changed, 28 insertions(+), 67 deletions(-) diff --git a/examples/fabric-admin/scripts/fabric-sync-app.py b/examples/fabric-admin/scripts/fabric-sync-app.py index 0f6385fa5aec53..c6faed8b67ac00 100755 --- a/examples/fabric-admin/scripts/fabric-sync-app.py +++ b/examples/fabric-admin/scripts/fabric-sync-app.py @@ -259,16 +259,14 @@ def terminate(signum, frame): # added to the fabric. f"Commissioning complete for node ID {bridge_node_id:#018x}: success") - # Open commissioning window with original setup code for the bridge, - # so it can be added by the TH fabric. + # Open commissioning window with original setup code for the bridge. cw_endpoint_id = 0 cw_option = 0 # 0: Original setup code, 1: New setup code cw_timeout = 600 cw_iteration = 1000 cw_discriminator = 0 - cmd = (f"pairing open-commissioning-window {bridge_node_id} {cw_endpoint_id}" - f" {cw_option} {cw_timeout} {cw_iteration} {cw_discriminator}") - await admin.send(cmd) + await admin.send(f"pairing open-commissioning-window {bridge_node_id} {cw_endpoint_id}" + f" {cw_option} {cw_timeout} {cw_iteration} {cw_discriminator}") try: await asyncio.gather( diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py index 3c75788032b3bd..1a18896c055952 100644 --- a/src/python_testing/TC_MCORE_FS_1_3.py +++ b/src/python_testing/TC_MCORE_FS_1_3.py @@ -48,6 +48,7 @@ from mobly import asserts +# TODO: Make this class more generic. Issue #35348 class Subprocess(threading.Thread): def __init__(self, args: list = [], tag="", **kw): @@ -108,6 +109,9 @@ def default_timeout(self) -> int: def setup_class(self): super().setup_class() + self.th_server = None + self.storage = None + # Get the path to the TH_SERVER_NO_UID app from the user params. th_server_app = self.user_params.get("th_server_no_uid_app_path", None) if not th_server_app: @@ -132,8 +136,10 @@ def setup_class(self): passcode=self.th_server_passcode) def teardown_class(self): - self.th_server.stop() - self.storage.cleanup() + if self.th_server is not None: + self.th_server.stop() + if self.storage is not None: + self.storage.cleanup() super().teardown_class() def steps_TC_MCORE_FS_1_3(self) -> list[TestStep]: diff --git a/src/python_testing/TC_MCORE_FS_1_4.py b/src/python_testing/TC_MCORE_FS_1_4.py index 6b571f876c27f0..8e05c2dd7e9c3d 100644 --- a/src/python_testing/TC_MCORE_FS_1_4.py +++ b/src/python_testing/TC_MCORE_FS_1_4.py @@ -48,6 +48,7 @@ from mobly import asserts +# TODO: Make this class more generic. Issue #35348 class Subprocess(threading.Thread): def __init__(self, args: list = [], stdout_cb=None, tag="", **kw): @@ -114,9 +115,8 @@ def __init__(self, fabric_sync_app_path, fabric_admin_app_path, fabric_bridge_ap args.append("--app-bridge-rpc-port=44001") # Keep the storage directory in a temporary location. args.append(f"--storage-dir={storage_dir}") - # FIXME: Passing custom PAA store breaks something - # if paa_trust_store_path is not None: - # args.append(f"--paa-trust-store-path={paa_trust_store_path}") + if paa_trust_store_path is not None: + args.append(f"--paa-trust-store-path={paa_trust_store_path}") if fabric_name is not None: args.append(f"--commissioner-name={fabric_name}") if node_id is not None: @@ -171,6 +171,10 @@ def default_timeout(self) -> int: def setup_class(self): super().setup_class() + self.th_fsa_controller = None + self.th_server = None + self.storage = None + # Get the path to the TH_FSA (fabric-admin and fabric-bridge) app from the user params. th_fsa_app_path = self.user_params.get("th_fsa_app_path") if not th_fsa_app_path: @@ -201,7 +205,9 @@ def setup_class(self): self.th_fsa_bridge_address = "::1" self.th_fsa_bridge_port = 5543 - self.th_fsa_bridge_discriminator = random.randint(0, 4095) + # Random discriminator between 0 and MAX - 1. The one-less is to save + # a room for the TH_SERVER_NO_UID discriminator. + self.th_fsa_bridge_discriminator = random.randint(0, 4094) self.th_fsa_bridge_passcode = 20202021 self.th_fsa_controller = FabricSyncApp( @@ -221,7 +227,7 @@ def setup_class(self): self.dut_fsa_stdin = open(dut_fsa_stdin_pipe, "w") self.th_server_port = 5544 - self.th_server_discriminator = random.randint(0, 4095) + self.th_server_discriminator = self.th_fsa_bridge_discriminator + 1 self.th_server_passcode = 20202021 # Start the TH_SERVER_NO_UID app. @@ -233,9 +239,12 @@ def setup_class(self): passcode=self.th_server_passcode) def teardown_class(self): - self.th_fsa_controller.stop() - self.th_server.stop() - self.storage.cleanup() + if self.th_fsa_controller is not None: + self.th_fsa_controller.stop() + if self.th_server is not None: + self.th_server.stop() + if self.storage is not None: + self.storage.cleanup() super().teardown_class() def steps_TC_MCORE_FS_1_4(self) -> list[TestStep]: @@ -252,58 +261,6 @@ def steps_TC_MCORE_FS_1_4(self) -> list[TestStep]: " by TH_FSA's Bridged Device Basic Information Cluster."), ] - async def commission_via_commissioner_control(self, controller_node_id: int, device_node_id: int): - """Commission device_node_id to controller_node_id using CommissionerControl cluster.""" - - request_id = random.randint(0, 0xFFFFFFFFFFFFFFFF) - - vendor_id = await self.read_single_attribute_check_success( - node_id=device_node_id, - cluster=Clusters.BasicInformation, - attribute=Clusters.BasicInformation.Attributes.VendorID, - ) - - product_id = await self.read_single_attribute_check_success( - node_id=device_node_id, - cluster=Clusters.BasicInformation, - attribute=Clusters.BasicInformation.Attributes.ProductID, - ) - - await self.send_single_cmd( - node_id=controller_node_id, - cmd=Clusters.CommissionerControl.Commands.RequestCommissioningApproval( - requestId=request_id, - vendorId=vendor_id, - productId=product_id, - ), - ) - - if not self.is_ci: - self.wait_for_user_input("Approve Commissioning Approval Request on DUT using manufacturer specified mechanism") - - resp = await self.send_single_cmd( - node_id=controller_node_id, - cmd=Clusters.CommissionerControl.Commands.CommissionNode( - requestId=request_id, - responseTimeoutSeconds=30, - ), - ) - - asserts.assert_equal(type(resp), Clusters.CommissionerControl.Commands.ReverseOpenCommissioningWindow, - "Incorrect response type") - - await self.send_single_cmd( - node_id=device_node_id, - cmd=Clusters.AdministratorCommissioning.Commands.OpenCommissioningWindow( - commissioningTimeout=3*60, - PAKEPasscodeVerifier=resp.PAKEPasscodeVerifier, - discriminator=resp.discriminator, - iterations=resp.iterations, - salt=resp.salt, - ), - timedRequestTimeoutMs=5000, - ) - @async_test_body async def test_TC_MCORE_FS_1_4(self): self.is_ci = self.check_pics('PICS_SDK_CI_ONLY')