blob: 4c4bf3554780aefc3a4e4503e31abcad7a638633 [file]
# 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())