Skip to content

Commit 56c0bd8

Browse files
[Fabric-Sync] Run MCORE-FS-1.3 and MCORE-FS-1.4 on CI (#35402)
* [Fabric-Sync] Run MCORE-FS-1.3 and MCORE-FS-1.4 on CI * Adopt TC_MCORE_FS_1_1 to run in CI * Reuse AppServer from TC_MCORE_FS_1_1 * Fix typo * Reuse AppServer from TC_MCORE_FS_1_1 * Restyled by isort * Fix TH server app name * Add json and perfetto tracing * Do not exit fabric-sync-app before apps are terminated * Wait for process termination * Turn off verbose output --------- Co-authored-by: Restyled.io <commits@restyled.io>
1 parent d3428a1 commit 56c0bd8

File tree

8 files changed

+209
-216
lines changed

8 files changed

+209
-216
lines changed

examples/fabric-admin/scripts/fabric-sync-app.py

+29-26
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,7 @@ async def forward_f(prefix: bytes, f_in: asyncio.StreamReader,
3131
3232
This function can optionally feed received lines to a callback function.
3333
"""
34-
while True:
35-
line = await f_in.readline()
36-
if not line:
37-
break
34+
while line := await f_in.readline():
3835
if cb is not None:
3936
cb(line)
4037
f_out.buffer.write(prefix)
@@ -68,11 +65,7 @@ async def forward_stdin(f_out: asyncio.StreamWriter):
6865
reader = asyncio.StreamReader()
6966
protocol = asyncio.StreamReaderProtocol(reader)
7067
await loop.connect_read_pipe(lambda: protocol, sys.stdin)
71-
while True:
72-
line = await reader.readline()
73-
if not line:
74-
# Exit on Ctrl-D (EOF).
75-
sys.exit(0)
68+
while line := await reader.readline():
7669
f_out.write(line)
7770
await f_out.drain()
7871

@@ -206,12 +199,16 @@ async def main(args):
206199
passcode=args.passcode,
207200
))
208201

202+
loop = asyncio.get_event_loop()
203+
209204
def terminate():
210-
admin.terminate()
211-
bridge.terminate()
212-
sys.exit(0)
205+
with contextlib.suppress(ProcessLookupError):
206+
admin.terminate()
207+
with contextlib.suppress(ProcessLookupError):
208+
bridge.terminate()
209+
loop.remove_signal_handler(signal.SIGINT)
210+
loop.remove_signal_handler(signal.SIGTERM)
213211

214-
loop = asyncio.get_event_loop()
215212
loop.add_signal_handler(signal.SIGINT, terminate)
216213
loop.add_signal_handler(signal.SIGTERM, terminate)
217214

@@ -238,7 +235,8 @@ def terminate():
238235
cmd,
239236
# Wait for the log message indicating that the bridge has been
240237
# added to the fabric.
241-
f"Commissioning complete for node ID {bridge_node_id:#018x}: success")
238+
f"Commissioning complete for node ID {bridge_node_id:#018x}: success",
239+
timeout=30)
242240

243241
# Open commissioning window with original setup code for the bridge.
244242
cw_endpoint_id = 0
@@ -250,18 +248,23 @@ def terminate():
250248
f" {cw_option} {cw_timeout} {cw_iteration} {cw_discriminator}")
251249

252250
try:
253-
await asyncio.gather(
254-
forward_pipe(pipe, admin.p.stdin) if pipe else forward_stdin(admin.p.stdin),
255-
admin.wait(),
256-
bridge.wait(),
257-
)
258-
except SystemExit:
259-
admin.terminate()
260-
bridge.terminate()
261-
except Exception:
262-
admin.terminate()
263-
bridge.terminate()
264-
raise
251+
forward = forward_pipe(pipe, admin.p.stdin) if pipe else forward_stdin(admin.p.stdin)
252+
# Wait for any of the tasks to complete.
253+
_, pending = await asyncio.wait([
254+
asyncio.create_task(admin.wait()),
255+
asyncio.create_task(bridge.wait()),
256+
asyncio.create_task(forward),
257+
], return_when=asyncio.FIRST_COMPLETED)
258+
# Cancel the remaining tasks.
259+
for task in pending:
260+
task.cancel()
261+
except Exception as e:
262+
print(e, file=sys.stderr)
263+
264+
terminate()
265+
# Make sure that we will not return until both processes are terminated.
266+
await admin.wait()
267+
await bridge.wait()
265268

266269

267270
if __name__ == "__main__":

src/python_testing/TC_MCORE_FS_1_1.py

+71-33
Original file line numberDiff line numberDiff line change
@@ -17,44 +17,83 @@
1717

1818
# This test requires a TH_SERVER application. Please specify with --string-arg th_server_app_path:<path_to_app>
1919

20+
# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments
21+
# for details about the block below.
22+
#
23+
# === BEGIN CI TEST ARGUMENTS ===
24+
# test-runner-runs: run1
25+
# test-runner-run/run1/app: examples/fabric-admin/scripts/fabric-sync-app.py
26+
# test-runner-run/run1/app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa-stdin --discriminator=1234
27+
# test-runner-run/run1/factoryreset: true
28+
# 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_app_path:${ALL_CLUSTERS_APP} --trace-to json:${TRACE_TEST_JSON}.json --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto
29+
# test-runner-run/run1/script-start-delay: 5
30+
# test-runner-run/run1/quiet: true
31+
# === END CI TEST ARGUMENTS ===
32+
2033
import logging
2134
import os
2235
import random
23-
import signal
24-
import subprocess
36+
import tempfile
2537
import time
26-
import uuid
2738

2839
import chip.clusters as Clusters
2940
from chip import ChipDeviceCtrl
41+
from chip.testing.tasks import Subprocess
3042
from matter_testing_support import MatterBaseTest, TestStep, async_test_body, default_matter_test_main
3143
from mobly import asserts
3244

3345

46+
class AppServer(Subprocess):
47+
"""Wrapper class for starting an application server in a subprocess."""
48+
49+
# Prefix for log messages from the application server.
50+
PREFIX = "[SERVER]"
51+
52+
def __init__(self, app: str, storage_dir: str, discriminator: int, passcode: int, port: int = 5540):
53+
storage_kvs_dir = tempfile.mkstemp(dir=storage_dir, prefix="kvs-app-")[1]
54+
# Start the server application with dedicated KVS storage.
55+
super().__init__(app, "--KVS", storage_kvs_dir,
56+
'--secured-device-port', str(port),
57+
"--discriminator", str(discriminator),
58+
"--passcode", str(passcode),
59+
prefix=self.PREFIX)
60+
61+
def start(self):
62+
# Start process and block until it prints the expected output.
63+
super().start(expected_output="Server initialization complete")
64+
65+
3466
class TC_MCORE_FS_1_1(MatterBaseTest):
3567

3668
@async_test_body
3769
async def setup_class(self):
3870
super().setup_class()
39-
self.app_process = None
40-
app = self.user_params.get("th_server_app_path", None)
41-
if not app:
42-
asserts.fail('This test requires a TH_SERVER app. Specify app path with --string-arg th_server_app_path:<path_to_app>')
43-
44-
self.kvs = f'kvs_{str(uuid.uuid4())}'
45-
self.port = 5543
46-
discriminator = random.randint(0, 4095)
47-
passcode = 20202021
48-
cmd = [app]
49-
cmd.extend(['--secured-device-port', str(5543)])
50-
cmd.extend(['--discriminator', str(discriminator)])
51-
cmd.extend(['--passcode', str(passcode)])
52-
cmd.extend(['--KVS', self.kvs])
53-
# TODO: Determine if we want these logs cooked or pushed to somewhere else
54-
logging.info("Starting application to acts mock a server portion of TH_FSA")
55-
self.app_process = subprocess.Popen(cmd)
56-
logging.info("Started application to acts mock a server portion of TH_FSA")
57-
time.sleep(3)
71+
72+
self.th_server = None
73+
self.storage = None
74+
75+
th_server_app = self.user_params.get("th_server_app_path", None)
76+
if not th_server_app:
77+
asserts.fail("This test requires a TH_SERVER app. Specify app path with --string-arg th_server_app_path:<path_to_app>")
78+
if not os.path.exists(th_server_app):
79+
asserts.fail(f"The path {th_server_app} does not exist")
80+
81+
# Create a temporary storage directory for keeping KVS files.
82+
self.storage = tempfile.TemporaryDirectory(prefix=self.__class__.__name__)
83+
logging.info("Temporary storage directory: %s", self.storage.name)
84+
85+
self.th_server_port = 5543
86+
self.th_server_discriminator = random.randint(0, 4095)
87+
self.th_server_passcode = 20202021
88+
89+
# Start the TH_SERVER_NO_UID app.
90+
self.th_server = AppServer(
91+
th_server_app,
92+
storage_dir=self.storage.name,
93+
port=self.th_server_port,
94+
discriminator=self.th_server_discriminator,
95+
passcode=self.th_server_passcode)
96+
self.th_server.start()
5897

5998
logging.info("Commissioning from separate fabric")
6099
# Create a second controller on a new fabric to communicate to the server
@@ -63,25 +102,24 @@ async def setup_class(self):
63102
paa_path = str(self.matter_test_config.paa_trust_store_path)
64103
self.TH_server_controller = new_fabric_admin.NewController(nodeId=112233, paaTrustStorePath=paa_path)
65104
self.server_nodeid = 1111
66-
await self.TH_server_controller.CommissionOnNetwork(nodeId=self.server_nodeid, setupPinCode=passcode, filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, filter=discriminator)
105+
await self.TH_server_controller.CommissionOnNetwork(
106+
nodeId=self.server_nodeid,
107+
setupPinCode=self.th_server_passcode,
108+
filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR,
109+
filter=self.th_server_discriminator)
67110
logging.info("Commissioning TH_SERVER complete")
68111

69112
def teardown_class(self):
70-
# In case the th_server_app_path does not exist, then we failed the test
71-
# and there is nothing to remove
72-
if self.app_process is not None:
73-
logging.warning("Stopping app with SIGTERM")
74-
self.app_process.send_signal(signal.SIGTERM.value)
75-
self.app_process.wait()
76-
77-
if os.path.exists(self.kvs):
78-
os.remove(self.kvs)
113+
if self.th_server is not None:
114+
self.th_server.terminate()
115+
if self.storage is not None:
116+
self.storage.cleanup()
79117
super().teardown_class()
80118

81119
def steps_TC_MCORE_FS_1_1(self) -> list[TestStep]:
82120
steps = [TestStep(1, "Enable Fabric Synchronization on DUT_FSA using the manufacturer specified mechanism.", is_commissioning=True),
83121
TestStep(2, "Commission DUT_FSA onto TH_FSA fabric."),
84-
TestStep(3, "Reverse Commision Commission TH_FSAs onto DUT_FSA fabric."),
122+
TestStep(3, "Reverse Commission TH_FSAs onto DUT_FSA fabric."),
85123
TestStep("3a", "TH_FSA sends RequestCommissioningApproval"),
86124
TestStep("3b", "TH_FSA sends CommissionNode"),
87125
TestStep("3c", "DUT_FSA commissions TH_FSA")]

src/python_testing/TC_MCORE_FS_1_2.py

+39-44
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,17 @@
2424
import os
2525
import queue
2626
import secrets
27-
import signal
2827
import struct
29-
import subprocess
28+
import tempfile
3029
import time
31-
import uuid
3230
from dataclasses import dataclass
3331

3432
import chip.clusters as Clusters
3533
from chip import ChipDeviceCtrl
3634
from ecdsa.curves import NIST256p
3735
from matter_testing_support import MatterBaseTest, TestStep, async_test_body, default_matter_test_main, type_matches
3836
from mobly import asserts
37+
from TC_MCORE_FS_1_1 import AppServer
3938
from TC_SC_3_6 import AttributeChangeAccumulator
4039

4140
# Length of `w0s` and `w1s` elements
@@ -52,7 +51,7 @@ def _generate_verifier(passcode: int, salt: bytes, iterations: int) -> bytes:
5251

5352

5453
@dataclass
55-
class _SetupParamters:
54+
class _SetupParameters:
5655
setup_qr_code: str
5756
manual_code: int
5857
discriminator: int
@@ -63,45 +62,49 @@ class TC_MCORE_FS_1_2(MatterBaseTest):
6362
@async_test_body
6463
async def setup_class(self):
6564
super().setup_class()
65+
6666
self._partslist_subscription = None
67-
self._app_th_server_process = None
68-
self._th_server_kvs = None
67+
self.th_server = None
68+
self.storage = None
69+
70+
th_server_port = self.user_params.get("th_server_port", 5543)
71+
th_server_app = self.user_params.get("th_server_app_path", None)
72+
if not th_server_app:
73+
asserts.fail('This test requires a TH_SERVER app. Specify app path with --string-arg th_server_app_path:<path_to_app>')
74+
if not os.path.exists(th_server_app):
75+
asserts.fail(f'The path {th_server_app} does not exist')
76+
77+
# Create a temporary storage directory for keeping KVS files.
78+
self.storage = tempfile.TemporaryDirectory(prefix=self.__class__.__name__)
79+
logging.info("Temporary storage directory: %s", self.storage.name)
80+
81+
self.th_server_port = th_server_port
82+
self.th_server_setup_params = _SetupParameters(
83+
setup_qr_code="MT:-24J0AFN00KA0648G00",
84+
manual_code=34970112332,
85+
discriminator=3840,
86+
passcode=20202021)
87+
88+
# Start the TH_SERVER_NO_UID app.
89+
self.th_server = AppServer(
90+
th_server_app,
91+
storage_dir=self.storage.name,
92+
port=self.th_server_port,
93+
discriminator=self.th_server_setup_params.discriminator,
94+
passcode=self.th_server_setup_params.passcode)
95+
self.th_server.start()
6996

7097
def teardown_class(self):
7198
if self._partslist_subscription is not None:
7299
self._partslist_subscription.Shutdown()
73100
self._partslist_subscription = None
74-
75-
if self._app_th_server_process is not None:
76-
logging.warning("Stopping app with SIGTERM")
77-
self._app_th_server_process.send_signal(signal.SIGTERM.value)
78-
self._app_th_server_process.wait()
79-
80-
if self._th_server_kvs is not None:
81-
os.remove(self._th_server_kvs)
101+
if self.th_server is not None:
102+
self.th_server.terminate()
103+
if self.storage is not None:
104+
self.storage.cleanup()
82105
super().teardown_class()
83106

84-
async def _create_th_server(self, port):
85-
# These are default testing values
86-
setup_params = _SetupParamters(setup_qr_code="MT:-24J0AFN00KA0648G00",
87-
manual_code=34970112332, discriminator=3840, passcode=20202021)
88-
kvs = f'kvs_{str(uuid.uuid4())}'
89-
90-
cmd = [self._th_server_app_path]
91-
cmd.extend(['--secured-device-port', str(port)])
92-
cmd.extend(['--discriminator', str(setup_params.discriminator)])
93-
cmd.extend(['--passcode', str(setup_params.passcode)])
94-
cmd.extend(['--KVS', kvs])
95-
96-
# TODO: Determine if we want these logs cooked or pushed to somewhere else
97-
logging.info("Starting TH_SERVER")
98-
self._app_th_server_process = subprocess.Popen(cmd)
99-
self._th_server_kvs = kvs
100-
logging.info("Started TH_SERVER")
101-
time.sleep(3)
102-
return setup_params
103-
104-
def _ask_for_vendor_commissioning_ux_operation(self, setup_params: _SetupParamters):
107+
def _ask_for_vendor_commissioning_ux_operation(self, setup_params: _SetupParameters):
105108
self.wait_for_user_input(
106109
prompt_msg=f"Using the DUT vendor's provided interface, commission the ICD device using the following parameters:\n"
107110
f"- discriminator: {setup_params.discriminator}\n"
@@ -115,7 +118,6 @@ def steps_TC_MCORE_FS_1_2(self) -> list[TestStep]:
115118
steps = [TestStep(1, "TH subscribes to PartsList attribute of the Descriptor cluster of DUT_FSA endpoint 0."),
116119
TestStep(2, "Follow manufacturer provided instructions to have DUT_FSA commission TH_SERVER"),
117120
TestStep(3, "TH waits up to 30 seconds for subscription report from the PartsList attribute of the Descriptor to contain new endpoint"),
118-
119121
TestStep(4, "TH uses DUT to open commissioning window to TH_SERVER"),
120122
TestStep(5, "TH commissions TH_SERVER"),
121123
TestStep(6, "TH reads all attributes in Basic Information cluster from TH_SERVER directly"),
@@ -134,12 +136,6 @@ async def test_TC_MCORE_FS_1_2(self):
134136

135137
min_report_interval_sec = self.user_params.get("min_report_interval_sec", 0)
136138
max_report_interval_sec = self.user_params.get("max_report_interval_sec", 30)
137-
th_server_port = self.user_params.get("th_server_port", 5543)
138-
self._th_server_app_path = self.user_params.get("th_server_app_path", None)
139-
if not self._th_server_app_path:
140-
asserts.fail('This test requires a TH_SERVER app. Specify app path with --string-arg th_server_app_path:<path_to_app>')
141-
if not os.path.exists(self._th_server_app_path):
142-
asserts.fail(f'The path {self._th_server_app_path} does not exist')
143139

144140
self.step(1)
145141
# Subscribe to the PartsList
@@ -164,8 +160,7 @@ async def test_TC_MCORE_FS_1_2(self):
164160
asserts.assert_true(type_matches(step_1_dut_parts_list, list), "PartsList is expected to be a list")
165161

166162
self.step(2)
167-
setup_params = await self._create_th_server(th_server_port)
168-
self._ask_for_vendor_commissioning_ux_operation(setup_params)
163+
self._ask_for_vendor_commissioning_ux_operation(self.th_server_setup_params)
169164

170165
self.step(3)
171166
report_waiting_timeout_delay_sec = 30

0 commit comments

Comments
 (0)