Skip to content

Commit e7f4ea5

Browse files
committedMar 10, 2025
initial runner decouple
1 parent f4260df commit e7f4ea5

File tree

2 files changed

+293
-244
lines changed

2 files changed

+293
-244
lines changed
 

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

+4-242
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import chip.testing.conversions as conversions
4545
import chip.testing.matchers as matchers
4646
import chip.testing.timeoperations as timeoperations
47+
import chip.testing.runner as runner
4748
from chip.tlv import uint
4849

4950
# isort: off
@@ -69,17 +70,10 @@
6970
from chip.storage import PersistentStorage
7071
from chip.testing.commissioning import CommissioningInfo, CustomCommissioningParameters, SetupPayloadInfo, commission_devices
7172
from chip.testing.global_attribute_ids import GlobalAttributeIds
73+
from chip.testing.runner import get_test_info, run_tests_no_exit, run_tests, TestStep, TestInfo, TestRunnerHooks
7274
from chip.testing.pics import read_pics_from_file
7375
from chip.tracing import TracingContext
7476
from mobly import asserts, base_test, signals, utils
75-
from mobly.config_parser import ENV_MOBLY_LOGPATH, TestRunConfig
76-
from mobly.test_runner import TestRunner
77-
78-
try:
79-
from matter_yamltests.hooks import TestRunnerHooks
80-
except ImportError:
81-
class TestRunnerHooks:
82-
pass
8377

8478

8579
# TODO: Add utility to commission a device if needed
@@ -498,51 +492,6 @@ def flush_reports(self) -> None:
498492
return
499493

500494

501-
class InternalTestRunnerHooks(TestRunnerHooks):
502-
503-
def start(self, count: int):
504-
logging.info(f'Starting test set, running {count} tests')
505-
506-
def stop(self, duration: int):
507-
logging.info(f'Finished test set, ran for {duration}ms')
508-
509-
def test_start(self, filename: str, name: str, count: int, steps: list[str] = []):
510-
logging.info(f'Starting test from {filename}: {name} - {count} steps')
511-
512-
def test_stop(self, exception: Exception, duration: int):
513-
logging.info(f'Finished test in {duration}ms')
514-
515-
def step_skipped(self, name: str, expression: str):
516-
# TODO: Do we really need the expression as a string? We can evaluate this in code very easily
517-
logging.info(f'\t\t**** Skipping: {name}')
518-
519-
def step_start(self, name: str):
520-
# The way I'm calling this, the name is already includes the step number, but it seems like it might be good to separate these
521-
logging.info(f'\t\t***** Test Step {name}')
522-
523-
def step_success(self, logger, logs, duration: int, request):
524-
pass
525-
526-
def step_failure(self, logger, logs, duration: int, request, received):
527-
# TODO: there's supposed to be some kind of error message here, but I have no idea where it's meant to come from in this API
528-
logging.info('\t\t***** Test Failure : ')
529-
530-
def step_unknown(self):
531-
"""
532-
This method is called when the result of running a step is unknown. For example during a dry-run.
533-
"""
534-
pass
535-
536-
def show_prompt(self,
537-
msg: str,
538-
placeholder: Optional[str] = None,
539-
default_value: Optional[str] = None) -> None:
540-
pass
541-
542-
def test_skipped(self, filename: str, name: str):
543-
logging.info(f"Skipping test from {filename}: {name}")
544-
545-
546495
@dataclass
547496
class MatterTestConfig:
548497
storage_path: pathlib.Path = pathlib.Path(".")
@@ -840,25 +789,6 @@ def stack(self) -> ChipStack:
840789
return builtins.chipStack
841790

842791

843-
@dataclass
844-
class TestStep:
845-
test_plan_number: typing.Union[int, str]
846-
description: str
847-
expectation: str = ""
848-
is_commissioning: bool = False
849-
850-
def __str__(self):
851-
return f'{self.test_plan_number}: {self.description}\tExpected outcome: {self.expectation}'
852-
853-
854-
@dataclass
855-
class TestInfo:
856-
function: str
857-
desc: str
858-
steps: list[TestStep]
859-
pics: list[str]
860-
861-
862792
class MatterBaseTest(base_test.BaseTestClass):
863793
def __init__(self, *args):
864794
super().__init__(*args)
@@ -1587,26 +1517,6 @@ def wait_for_user_input(self,
15871517
return None
15881518

15891519

1590-
def generate_mobly_test_config(matter_test_config: MatterTestConfig):
1591-
test_run_config = TestRunConfig()
1592-
# We use a default name. We don't use Mobly YAML configs, so that we can be
1593-
# freestanding without relying
1594-
test_run_config.testbed_name = "MatterTest"
1595-
1596-
log_path = matter_test_config.logs_path
1597-
log_path = _DEFAULT_LOG_PATH if log_path is None else log_path
1598-
if ENV_MOBLY_LOGPATH in os.environ:
1599-
log_path = os.environ[ENV_MOBLY_LOGPATH]
1600-
1601-
test_run_config.log_path = log_path
1602-
# TODO: For later, configure controllers
1603-
test_run_config.controller_configs = {}
1604-
1605-
test_run_config.user_params = matter_test_config.global_test_params
1606-
1607-
return test_run_config
1608-
1609-
16101520
def _find_test_class():
16111521
"""Finds the test class in a test script.
16121522
Walk through module members and find the subclass of MatterBaseTest. Only
@@ -2276,156 +2186,6 @@ def test_run_commissioning(self):
22762186
raise signals.TestAbortAll("Failed to commission node(s)")
22772187

22782188

2279-
def default_matter_test_main():
2280-
"""Execute the test class in a test module.
2281-
This is the default entry point for running a test script file directly.
2282-
In this case, only one test class in a test script is allowed.
2283-
To make your test script executable, add the following to your file:
2284-
.. code-block:: python
2285-
from chip.testing.matter_testing import default_matter_test_main
2286-
...
2287-
if __name__ == '__main__':
2288-
default_matter_test_main()
2289-
"""
2290-
2291-
matter_test_config = parse_matter_test_args()
2292-
2293-
# Find the test class in the test script.
2294-
test_class = _find_test_class()
2295-
2296-
hooks = InternalTestRunnerHooks()
2297-
2298-
run_tests(test_class, matter_test_config, hooks)
2299-
2300-
2301-
def get_test_info(test_class: MatterBaseTest, matter_test_config: MatterTestConfig) -> list[TestInfo]:
2302-
test_config = generate_mobly_test_config(matter_test_config)
2303-
base = test_class(test_config)
2304-
2305-
if len(matter_test_config.tests) > 0:
2306-
tests = matter_test_config.tests
2307-
else:
2308-
tests = base.get_existing_test_names()
2309-
2310-
info = []
2311-
for t in tests:
2312-
info.append(TestInfo(t, steps=base.get_test_steps(t), desc=base.get_test_desc(t), pics=base.get_test_pics(t)))
2313-
2314-
return info
2315-
2316-
2317-
def run_tests_no_exit(test_class: MatterBaseTest, matter_test_config: MatterTestConfig,
2318-
event_loop: asyncio.AbstractEventLoop, hooks: TestRunnerHooks,
2319-
default_controller=None, external_stack=None) -> bool:
2320-
2321-
# NOTE: It's not possible to pass event loop via Mobly TestRunConfig user params, because the
2322-
# Mobly deep copies the user params before passing them to the test class and the event
2323-
# loop is not serializable. So, we are setting the event loop as a test class member.
2324-
CommissionDeviceTest.event_loop = event_loop
2325-
test_class.event_loop = event_loop
2326-
2327-
get_test_info(test_class, matter_test_config)
2328-
2329-
# Load test config file.
2330-
test_config = generate_mobly_test_config(matter_test_config)
2331-
2332-
# Parse test specifiers if exist.
2333-
tests = None
2334-
if len(matter_test_config.tests) > 0:
2335-
tests = matter_test_config.tests
2336-
2337-
if external_stack:
2338-
stack = external_stack
2339-
else:
2340-
stack = MatterStackState(matter_test_config)
2341-
2342-
with TracingContext() as tracing_ctx:
2343-
for destination in matter_test_config.trace_to:
2344-
tracing_ctx.StartFromString(destination)
2345-
2346-
test_config.user_params["matter_stack"] = stash_globally(stack)
2347-
2348-
# TODO: Steer to right FabricAdmin!
2349-
# TODO: If CASE Admin Subject is a CAT tag range, then make sure to issue NOC with that CAT tag
2350-
if not default_controller:
2351-
default_controller = stack.certificate_authorities[0].adminList[0].NewController(
2352-
nodeId=matter_test_config.controller_node_id,
2353-
paaTrustStorePath=str(matter_test_config.paa_trust_store_path),
2354-
catTags=matter_test_config.controller_cat_tags,
2355-
dacRevocationSetPath=str(matter_test_config.dac_revocation_set_path),
2356-
)
2357-
test_config.user_params["default_controller"] = stash_globally(default_controller)
2358-
2359-
test_config.user_params["matter_test_config"] = stash_globally(matter_test_config)
2360-
test_config.user_params["hooks"] = stash_globally(hooks)
2361-
2362-
# Execute the test class with the config
2363-
ok = True
2364-
2365-
test_config.user_params["certificate_authority_manager"] = stash_globally(stack.certificate_authority_manager)
2366-
2367-
# Execute the test class with the config
2368-
ok = True
2369-
2370-
runner = TestRunner(log_dir=test_config.log_path,
2371-
testbed_name=test_config.testbed_name)
2372-
2373-
with runner.mobly_logger():
2374-
if matter_test_config.commissioning_method is not None:
2375-
runner.add_test_class(test_config, CommissionDeviceTest, None)
2376-
2377-
# Add the tests selected unless we have a commission-only request
2378-
if not matter_test_config.commission_only:
2379-
runner.add_test_class(test_config, test_class, tests)
2380-
2381-
if hooks:
2382-
# Right now, we only support running a single test class at once,
2383-
# but it's relatively easy to expand that to make the test process faster
2384-
# TODO: support a list of tests
2385-
hooks.start(count=1)
2386-
# Mobly gives the test run time in seconds, lets be a bit more precise
2387-
runner_start_time = datetime.now(timezone.utc)
2388-
2389-
try:
2390-
runner.run()
2391-
ok = runner.results.is_all_pass and ok
2392-
if matter_test_config.fail_on_skipped_tests and runner.results.skipped:
2393-
ok = False
2394-
except TimeoutError:
2395-
ok = False
2396-
except signals.TestAbortAll:
2397-
ok = False
2398-
except Exception:
2399-
logging.exception('Exception when executing %s.', test_config.testbed_name)
2400-
ok = False
2401-
2402-
if hooks:
2403-
duration = (datetime.now(timezone.utc) - runner_start_time) / timedelta(microseconds=1)
2404-
hooks.stop(duration=duration)
2405-
2406-
if not external_stack:
2407-
async def shutdown():
2408-
stack.Shutdown()
2409-
# Shutdown the stack when all done. Use the async runner to ensure that
2410-
# during the shutdown callbacks can use tha same async context which was used
2411-
# during the initialization.
2412-
event_loop.run_until_complete(shutdown())
2413-
2414-
if ok:
2415-
logging.info("Final result: PASS !")
2416-
else:
2417-
logging.error("Final result: FAIL !")
2418-
return ok
2419-
2420-
2421-
def run_tests(test_class: MatterBaseTest, matter_test_config: MatterTestConfig,
2422-
hooks: TestRunnerHooks, default_controller=None, external_stack=None) -> None:
2423-
with asyncio.Runner() as runner:
2424-
if not run_tests_no_exit(test_class, matter_test_config, runner.get_loop(),
2425-
hooks, default_controller, external_stack):
2426-
sys.exit(1)
2427-
2428-
24292189
# TODO(#37537): Remove these temporary aliases after transition period
24302190
type_matches = matchers.is_type
24312191
utc_time_in_matter_epoch = timeoperations.utc_time_in_matter_epoch
@@ -2437,3 +2197,5 @@ def run_tests(test_class: MatterBaseTest, matter_test_config: MatterTestConfig,
24372197
hex_from_bytes = conversions.hex_from_bytes
24382198
id_str = conversions.format_decimal_and_hex
24392199
cluster_id_str = conversions.cluster_id_with_name
2200+
2201+
default_matter_test_main = runner.default_matter_test_main

0 commit comments

Comments
 (0)