| # Copyright (c) 2026 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 contextlib |
| import dataclasses |
| import logging |
| import sys |
| import tempfile |
| from abc import ABC, abstractmethod |
| from pathlib import Path |
| from typing import ClassVar |
| |
| import chiptest |
| from chiptest.accessories import AppsRegister |
| from chiptest.concurrency.context import StartStopContextMixin |
| from chiptest.log_config import LogConfig |
| from chiptest.results import TestResult |
| from chiptest.runner import Executor |
| from chiptest.test_definition import TestDefinition, TestJobConfig |
| |
| from .process import ProcessConfig, WrappedProcess |
| |
| log = logging.getLogger(__name__) |
| |
| |
| @dataclasses.dataclass |
| class WorkerConfig(ProcessConfig, TestJobConfig): |
| """Configuration for a worker process based on a common test job config.""" |
| |
| WORKER_START_TIMEOUT: ClassVar[float] = 15 |
| WORKER_STOP_TIMEOUT: ClassVar[float] = 10 |
| |
| tmp_dir_clear: bool = False |
| tmp_dir_default: ClassVar[Path] = Path(tempfile.gettempdir()) |
| tmp_dir_worker_base: ClassVar[Path] = Path(tempfile.gettempdir()) / "matter_test_suite" |
| |
| @classmethod |
| def from_test_job_config(cls, log_config: LogConfig, config: TestJobConfig, *, tmp_dir_clear: bool): |
| """Create a worker config from a test job config.""" |
| # Needs to be a shallow copy, so that we don't accidentally create unpicklable generators in the config. |
| return cls(**{field.name: getattr(config, field.name) for field in dataclasses.fields(config)}, |
| name=f"W{{id:0{len(str(config.concurrency))}}}", |
| log_config=log_config, |
| start_timeout_sec=WorkerConfig.WORKER_START_TIMEOUT, |
| stop_timeout_sec=WorkerConfig.WORKER_STOP_TIMEOUT, |
| tmp_dir_clear=tmp_dir_clear) |
| |
| |
| @dataclasses.dataclass |
| class WorkerJob: |
| """A unit of work for a worker process.""" |
| iteration: int |
| test: TestDefinition |
| |
| |
| class WorkerProcess(WrappedProcess[WorkerConfig, WorkerJob, TestResult], StartStopContextMixin, ABC): |
| """ |
| Abstract process class responsible for running tests in a subprocess. |
| |
| Each supported platform should have its own implementation of this class that initializes the platform-specific executor and |
| resources in ``_platform_init()`` and set WorkerProcessCls. |
| """ |
| |
| def _proc_init(self, exit_stack: contextlib.ExitStack) -> None: |
| self.mgmt_ns_wrapper: str | None = None |
| self.thread_ba_host: str | None = None |
| self.thread_ba_port: int | None = None |
| |
| # Initialize platform-specific executor. |
| self.executor = self._platform_init(exit_stack) |
| |
| # Finalize common parts. |
| self.runner = chiptest.runner.Runner(self.executor) |
| self.apps_register: AppsRegister = exit_stack.enter_context(AppsRegister(self.mgmt_ns_wrapper, self._config.log_config)) |
| |
| @abstractmethod |
| def _platform_init(self, exit_stack: contextlib.ExitStack) -> Executor: |
| """Initialize platform-specific executor.""" |
| |
| def _proc_work(self) -> None: |
| """Main loop of the worker process.""" |
| while True: |
| work = self._work_queue.get() |
| |
| with (self.state.working_context(), |
| self._config.log_config.fmt_context(task=work.test.name, level=self._config.log_config.level_tests), |
| TestResult.measure_execution(work.test.name, self._config.id, work.iteration, self._config.dry_run) as result): |
| work.test.Run(self.runner, self.apps_register, self._config, self.thread_ba_host, self.thread_ba_port) |
| |
| self._rsp_queue.put(result) |
| if isinstance(result.exception, KeyboardInterrupt): |
| raise result.exception |
| |
| |
| class GenericWorkerProcess(WorkerProcess): |
| """Generic implementation of the worker process.""" |
| |
| def _platform_init(self, exit_stack: contextlib.ExitStack) -> Executor: |
| log.warning('No platform-specific executor for "%s"', sys.platform) |
| return exit_stack.enter_context(Executor()) |