Skip to content

Commit 4fd7215

Browse files
authored
Fix running async functions without event loop during testing (#36859)
* Keep device commissioning method in CommissionDeviceTest class * Improve readability * Run matter testing on a single even loop context * Update all run_tests_no_exit() usages
1 parent 594ffe2 commit 4fd7215

File tree

5 files changed

+72
-51
lines changed

5 files changed

+72
-51
lines changed

src/python_testing/matter_testing_infrastructure/chip/testing/matter_testing.py

+56-45
Original file line numberDiff line numberDiff line change
@@ -972,18 +972,6 @@ def __init__(self, *args):
972972
# The named pipe name must be set in the derived classes
973973
self.app_pipe = None
974974

975-
async def commission_devices(self) -> bool:
976-
conf = self.matter_test_config
977-
978-
for commission_idx, node_id in enumerate(conf.dut_node_ids):
979-
logging.info(
980-
f"Starting commissioning for root index {conf.root_of_trust_index}, fabric ID 0x{conf.fabric_id:016X}, node ID 0x{node_id:016X}")
981-
logging.info(f"Commissioning method: {conf.commissioning_method}")
982-
983-
await CommissionDeviceTest.commission_device(self, commission_idx)
984-
985-
return True
986-
987975
def get_test_steps(self, test: str) -> list[TestStep]:
988976
''' Retrieves the test step list for the given test
989977
@@ -1168,17 +1156,14 @@ def setup_test(self):
11681156
self.step(1)
11691157

11701158
def teardown_class(self):
1171-
"""Final teardown after all tests: log all problems"""
1172-
if len(self.problems) == 0:
1173-
return
1174-
1175-
logging.info("###########################################################")
1176-
logging.info("Problems found:")
1177-
logging.info("===============")
1178-
for problem in self.problems:
1179-
logging.info(str(problem))
1180-
logging.info("###########################################################")
1181-
1159+
"""Final teardown after all tests: log all problems."""
1160+
if len(self.problems) > 0:
1161+
logging.info("###########################################################")
1162+
logging.info("Problems found:")
1163+
logging.info("===============")
1164+
for problem in self.problems:
1165+
logging.info(str(problem))
1166+
logging.info("###########################################################")
11821167
super().teardown_class()
11831168

11841169
def check_pics(self, pics_key: str) -> bool:
@@ -2107,8 +2092,7 @@ def parse_matter_test_args(argv: Optional[List[str]] = None) -> MatterTestConfig
21072092

21082093
def _async_runner(body, self: MatterBaseTest, *args, **kwargs):
21092094
timeout = self.matter_test_config.timeout if self.matter_test_config.timeout is not None else self.default_timeout
2110-
runner_with_timeout = asyncio.wait_for(body(self, *args, **kwargs), timeout=timeout)
2111-
return asyncio.run(runner_with_timeout)
2095+
return self.event_loop.run_until_complete(asyncio.wait_for(body(self, *args, **kwargs), timeout=timeout))
21122096

21132097

21142098
def async_test_body(body):
@@ -2301,7 +2285,7 @@ def run_on_singleton_matching_endpoint(accept_function: EndpointCheckFunction):
23012285
def run_on_singleton_matching_endpoint_internal(body):
23022286
def matching_runner(self: MatterBaseTest, *args, **kwargs):
23032287
runner_with_timeout = asyncio.wait_for(_get_all_matching_endpoints(self, accept_function), timeout=30)
2304-
matching = asyncio.run(runner_with_timeout)
2288+
matching = self.event_loop.run_until_complete(runner_with_timeout)
23052289
asserts.assert_less_equal(len(matching), 1, "More than one matching endpoint found for singleton test.")
23062290
if not matching:
23072291
logging.info("Test is not applicable to any endpoint - skipping test")
@@ -2348,7 +2332,7 @@ def run_if_endpoint_matches(accept_function: EndpointCheckFunction):
23482332
def run_if_endpoint_matches_internal(body):
23492333
def per_endpoint_runner(self: MatterBaseTest, *args, **kwargs):
23502334
runner_with_timeout = asyncio.wait_for(should_run_test_on_endpoint(self, accept_function), timeout=60)
2351-
should_run_test = asyncio.run(runner_with_timeout)
2335+
should_run_test = self.event_loop.run_until_complete(runner_with_timeout)
23522336
if not should_run_test:
23532337
logging.info("Test is not applicable to this endpoint - skipping test")
23542338
asserts.skip('Endpoint does not match test requirements')
@@ -2367,14 +2351,25 @@ def __init__(self, *args):
23672351
self.is_commissioning = True
23682352

23692353
def test_run_commissioning(self):
2370-
if not asyncio.run(self.commission_devices()):
2371-
raise signals.TestAbortAll("Failed to commission node")
2354+
if not self.event_loop.run_until_complete(self.commission_devices()):
2355+
raise signals.TestAbortAll("Failed to commission node(s)")
2356+
2357+
async def commission_devices(self) -> bool:
2358+
conf = self.matter_test_config
23722359

2373-
async def commission_device(instance: MatterBaseTest, i) -> bool:
2374-
dev_ctrl = instance.default_controller
2375-
conf = instance.matter_test_config
2360+
commissioned = []
2361+
setup_payloads = self.get_setup_payload_info()
2362+
for node_id, setup_payload in zip(conf.dut_node_ids, setup_payloads):
2363+
logging.info(f"Starting commissioning for root index {conf.root_of_trust_index}, "
2364+
f"fabric ID 0x{conf.fabric_id:016X}, node ID 0x{node_id:016X}")
2365+
logging.info(f"Commissioning method: {conf.commissioning_method}")
2366+
commissioned.append(await self.commission_device(node_id, setup_payload))
23762367

2377-
info = instance.get_setup_payload_info()[i]
2368+
return all(commissioned)
2369+
2370+
async def commission_device(self, node_id: int, info: SetupPayloadInfo) -> bool:
2371+
dev_ctrl = self.default_controller
2372+
conf = self.matter_test_config
23782373

23792374
if conf.tc_version_to_simulate is not None and conf.tc_user_response_to_simulate is not None:
23802375
logging.debug(
@@ -2384,7 +2379,7 @@ async def commission_device(instance: MatterBaseTest, i) -> bool:
23842379
if conf.commissioning_method == "on-network":
23852380
try:
23862381
await dev_ctrl.CommissionOnNetwork(
2387-
nodeId=conf.dut_node_ids[i],
2382+
nodeId=node_id,
23882383
setupPinCode=info.passcode,
23892384
filterType=info.filter_type,
23902385
filter=info.filter_value
@@ -2398,7 +2393,7 @@ async def commission_device(instance: MatterBaseTest, i) -> bool:
23982393
await dev_ctrl.CommissionWiFi(
23992394
info.filter_value,
24002395
info.passcode,
2401-
conf.dut_node_ids[i],
2396+
node_id,
24022397
conf.wifi_ssid,
24032398
conf.wifi_passphrase,
24042399
isShortDiscriminator=(info.filter_type == DiscoveryFilterType.SHORT_DISCRIMINATOR)
@@ -2412,7 +2407,7 @@ async def commission_device(instance: MatterBaseTest, i) -> bool:
24122407
await dev_ctrl.CommissionThread(
24132408
info.filter_value,
24142409
info.passcode,
2415-
conf.dut_node_ids[i],
2410+
node_id,
24162411
conf.thread_operational_dataset,
24172412
isShortDiscriminator=(info.filter_type == DiscoveryFilterType.SHORT_DISCRIMINATOR)
24182413
)
@@ -2425,7 +2420,8 @@ async def commission_device(instance: MatterBaseTest, i) -> bool:
24252420
logging.warning("==== USING A DIRECT IP COMMISSIONING METHOD NOT SUPPORTED IN THE LONG TERM ====")
24262421
await dev_ctrl.CommissionIP(
24272422
ipaddr=conf.commissionee_ip_address_just_for_testing,
2428-
setupPinCode=info.passcode, nodeid=conf.dut_node_ids[i]
2423+
setupPinCode=info.passcode,
2424+
nodeid=node_id,
24292425
)
24302426
return True
24312427
except ChipStackError as e:
@@ -2441,10 +2437,10 @@ def default_matter_test_main():
24412437
In this case, only one test class in a test script is allowed.
24422438
To make your test script executable, add the following to your file:
24432439
.. code-block:: python
2444-
from chip.testing.matter_testing.py import default_matter_test_main
2440+
from chip.testing.matter_testing import default_matter_test_main
24452441
...
24462442
if __name__ == '__main__':
2447-
default_matter_test_main.main()
2443+
default_matter_test_main()
24482444
"""
24492445

24502446
matter_test_config = parse_matter_test_args()
@@ -2473,7 +2469,15 @@ def get_test_info(test_class: MatterBaseTest, matter_test_config: MatterTestConf
24732469
return info
24742470

24752471

2476-
def run_tests_no_exit(test_class: MatterBaseTest, matter_test_config: MatterTestConfig, hooks: TestRunnerHooks, default_controller=None, external_stack=None) -> bool:
2472+
def run_tests_no_exit(test_class: MatterBaseTest, matter_test_config: MatterTestConfig,
2473+
event_loop: asyncio.AbstractEventLoop, hooks: TestRunnerHooks,
2474+
default_controller=None, external_stack=None) -> bool:
2475+
2476+
# NOTE: It's not possible to pass event loop via Mobly TestRunConfig user params, because the
2477+
# Mobly deep copies the user params before passing them to the test class and the event
2478+
# loop is not serializable. So, we are setting the event loop as a test class member.
2479+
CommissionDeviceTest.event_loop = event_loop
2480+
test_class.event_loop = event_loop
24772481

24782482
get_test_info(test_class, matter_test_config)
24792483

@@ -2553,9 +2557,13 @@ def run_tests_no_exit(test_class: MatterBaseTest, matter_test_config: MatterTest
25532557
duration = (datetime.now(timezone.utc) - runner_start_time) / timedelta(microseconds=1)
25542558
hooks.stop(duration=duration)
25552559

2556-
# Shutdown the stack when all done
25572560
if not external_stack:
2558-
stack.Shutdown()
2561+
async def shutdown():
2562+
stack.Shutdown()
2563+
# Shutdown the stack when all done. Use the async runner to ensure that
2564+
# during the shutdown callbacks can use tha same async context which was used
2565+
# during the initialization.
2566+
event_loop.run_until_complete(shutdown())
25592567

25602568
if ok:
25612569
logging.info("Final result: PASS !")
@@ -2564,6 +2572,9 @@ def run_tests_no_exit(test_class: MatterBaseTest, matter_test_config: MatterTest
25642572
return ok
25652573

25662574

2567-
def run_tests(test_class: MatterBaseTest, matter_test_config: MatterTestConfig, hooks: TestRunnerHooks, default_controller=None, external_stack=None) -> None:
2568-
if not run_tests_no_exit(test_class, matter_test_config, hooks, default_controller, external_stack):
2569-
sys.exit(1)
2575+
def run_tests(test_class: MatterBaseTest, matter_test_config: MatterTestConfig,
2576+
hooks: TestRunnerHooks, default_controller=None, external_stack=None) -> None:
2577+
with asyncio.Runner() as runner:
2578+
if not run_tests_no_exit(test_class, matter_test_config, runner.get_loop(),
2579+
hooks, default_controller, external_stack):
2580+
sys.exit(1)

src/python_testing/post_certification_tests/production_device_checks.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
# pip install opencv-python requests click_option_group
3737
# python src/python_testing/post_certification_tests/production_device_checks.py
3838

39+
import asyncio
3940
import base64
4041
import hashlib
4142
import importlib
@@ -390,9 +391,9 @@ def run_test(test_class: MatterBaseTest, tests: typing.List[str], test_config: T
390391
stack = test_config.get_stack()
391392
controller = test_config.get_controller()
392393
matter_config = test_config.get_config(tests)
393-
ok = run_tests_no_exit(test_class, matter_config, hooks, controller, stack)
394-
if not ok:
395-
print(f"Test failure. Failed on step: {hooks.get_failures()}")
394+
with asyncio.Runner() as runner:
395+
if not run_tests_no_exit(test_class, matter_config, runner.get_loop(), hooks, controller, stack):
396+
print(f"Test failure. Failed on step: {hooks.get_failures()}")
396397
return hooks.get_failures()
397398

398399

src/python_testing/test_testing/MockTestRunner.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# limitations under the License.
1616
#
1717

18+
import asyncio
1819
import importlib
1920
import os
2021
import sys
@@ -75,4 +76,6 @@ def run_test_with_mock_read(self, read_cache: Attribute.AsyncReadTransaction.Re
7576
self.default_controller.Read = AsyncMock(return_value=read_cache)
7677
# This doesn't need to do anything since we are overriding the read anyway
7778
self.default_controller.FindOrEstablishPASESession = AsyncMock(return_value=None)
78-
return run_tests_no_exit(self.test_class, self.config, hooks, self.default_controller, self.stack)
79+
with asyncio.Runner() as runner:
80+
return run_tests_no_exit(self.test_class, self.config, runner.get_loop(),
81+
hooks, self.default_controller, self.stack)

src/python_testing/test_testing/test_TC_CCNTL_2_2.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# limitations under the License.
1717
#
1818

19+
import asyncio
1920
import base64
2021
import os
2122
import pathlib
@@ -166,7 +167,9 @@ def run_test_with_mock(self, dynamic_invoke_return: typing.Callable, dynamic_eve
166167
self.default_controller.FindOrEstablishPASESession = AsyncMock(return_value=None)
167168
self.default_controller.ReadEvent = AsyncMock(return_value=[], side_effect=dynamic_event_return)
168169

169-
return run_tests_no_exit(self.test_class, self.config, hooks, self.default_controller, self.stack)
170+
with asyncio.Runner() as runner:
171+
return run_tests_no_exit(self.test_class, self.config, runner.get_loop(),
172+
hooks, self.default_controller, self.stack)
170173

171174

172175
@click.command()

src/python_testing/test_testing/test_TC_MCORE_FS_1_1.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# limitations under the License.
1717
#
1818

19+
import asyncio
1920
import base64
2021
import os
2122
import pathlib
@@ -137,7 +138,9 @@ def run_test_with_mock(self, dynamic_invoke_return: typing.Callable, dynamic_eve
137138
self.default_controller.FindOrEstablishPASESession = AsyncMock(return_value=None)
138139
self.default_controller.ReadEvent = AsyncMock(return_value=[], side_effect=dynamic_event_return)
139140

140-
return run_tests_no_exit(self.test_class, self.config, hooks, self.default_controller, self.stack)
141+
with asyncio.Runner() as runner:
142+
return run_tests_no_exit(self.test_class, self.config, runner.get_loop(),
143+
hooks, self.default_controller, self.stack)
141144

142145

143146
@click.command()

0 commit comments

Comments
 (0)