From 10088b538115d9d23136fae98ef764cfc5602104 Mon Sep 17 00:00:00 2001 From: Fiona Klute Date: Fri, 6 Mar 2026 22:55:39 +0100 Subject: [PATCH] support/testing: transfer config in environment instead of class variables Passing runtime configuration to tests in BRConfigTest class variables requires the "fork" start method for multiprocessing. With "forkserver" (default for POSIX platforms since Python 3.14) class variables modified in the parent process are lost, because the child process does not inherit the full Python state. This broke the way support/testing/run-tests sets configuration for BRConfigTest, so a start method override was added in commit 3d2141bceefda32bef06ddda594eaf8665913276. Instead this patch adds a settings dataclass (requires Python >= 3.7) which is serialized into an environment variable from run-tests and read during BRConfigTest.__init__(). Reading is skipped if the environment variable is not set so test discovery (not execution) works without configuration (e.g. utils/get-developers uses this). With that, run-tests no longer relies on a specific multiprocessing start method. Signed-off-by: Fiona Klute Signed-off-by: Thomas Petazzoni --- support/testing/infra/basetest.py | 37 ++++++++++++++++++++++++++++++ support/testing/run-tests | 38 ++++++++++++------------------- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/support/testing/infra/basetest.py b/support/testing/infra/basetest.py index 13f9b2d398..5fa902a77f 100644 --- a/support/testing/infra/basetest.py +++ b/support/testing/infra/basetest.py @@ -1,6 +1,9 @@ import unittest import os +import dataclasses import datetime +import json +from typing import ClassVar from infra.builder import Builder from infra.emulator import Emulator @@ -22,6 +25,33 @@ MINIMAL_CONFIG = \ """ +@dataclasses.dataclass +class BRTestSettings: + """support/testing/run-tests stores runtime settings in this + object, and serializes it into the environment variable named in + CONF_ENV. On instantiation, BRConfigTest loads this + configuration. That way configuration can be transferred into + multiprocessing worker processes without requiring the "fork" + start method. + + """ + CONF_ENV: ClassVar[str] = 'BR_TEST_CONFIG_JSON' + + downloaddir: str + outputdir: str + logtofile: bool = True + keepbuilds: bool = False + jlevel: int = 0 + timeout_multiplier: int = 1 + + def to_env(self) -> None: + os.environ[self.CONF_ENV] = json.dumps(dataclasses.asdict(self)) + + @classmethod + def from_env(cls) -> 'BRTestSettings': + return cls(**json.loads(os.environ[cls.CONF_ENV])) + + class BRConfigTest(unittest.TestCase): """Test up to the configure stage.""" config: str @@ -35,6 +65,8 @@ class BRConfigTest(unittest.TestCase): def __init__(self, names): super(BRConfigTest, self).__init__(names) + if BRTestSettings.CONF_ENV in os.environ: + self.conf_from_env() self.testname = self.__class__.__name__ self.builddir = self.outputdir and os.path.join(self.outputdir, self.testname) self.config += '\nBR2_DL_DIR="{}"\n'.format(self.downloaddir) @@ -44,6 +76,11 @@ class BRConfigTest(unittest.TestCase): print("{} {:40s} {}".format(datetime.datetime.now().strftime("%H:%M:%S"), self.testname, msg)) + def conf_from_env(self) -> None: + conf = BRTestSettings.from_env() + for f in dataclasses.fields(conf): + setattr(self, f.name, getattr(conf, f.name)) + def setUp(self): self.show_msg("Starting") self.b = Builder(self.config, self.builddir, self.logtofile, self.jlevel) diff --git a/support/testing/run-tests b/support/testing/run-tests index 2c7ce87c96..f0c1cc782a 100755 --- a/support/testing/run-tests +++ b/support/testing/run-tests @@ -6,7 +6,7 @@ import sys import nose2 -from infra.basetest import BRConfigTest +from infra.basetest import BRTestSettings import infra @@ -44,9 +44,6 @@ def main(): script_path = os.path.realpath(__file__) test_dir = os.path.dirname(script_path) - if args.stdout: - BRConfigTest.logtofile = False - if args.list: print("List of tests") nose2_args = [ @@ -69,7 +66,7 @@ def main(): parser.print_help() return 1 - BRConfigTest.downloaddir = os.path.abspath(args.download) + args.download = os.path.abspath(args.download) if args.prepare_only: emulator_builtin_binaries = ["kernel-vexpress-5.10.202", @@ -78,7 +75,7 @@ def main(): "versatile-pb-5.10.202.dtb"] print("Downloading emulator builtin binaries") for binary in emulator_builtin_binaries: - infra.download(BRConfigTest.downloaddir, binary) + infra.download(args.download, binary) return 0 if args.output is None: @@ -90,15 +87,13 @@ def main(): if not os.path.exists(args.output): os.mkdir(args.output) - BRConfigTest.outputdir = os.path.abspath(args.output) - if args.all is False and not args.testname: print("No test selected") print("") parser.print_help() return 1 - BRConfigTest.keepbuilds = args.keep + jlevel = 0 if args.testcases != 1: if args.testcases < 1: @@ -109,7 +104,7 @@ def main(): # same default BR2_JLEVEL as package/Makefile.in br2_jlevel = 1 + multiprocessing.cpu_count() each_testcase = int((br2_jlevel + args.testcases) / args.testcases) - BRConfigTest.jlevel = each_testcase + jlevel = each_testcase if args.jlevel: if args.jlevel < 0: @@ -118,14 +113,22 @@ def main(): parser.print_help() return 1 # the user can override the auto calculated value - BRConfigTest.jlevel = args.jlevel + jlevel = args.jlevel if args.timeout_multiplier < 1: print("Invalid multiplier for timeout values") print("") parser.print_help() return 1 - BRConfigTest.timeout_multiplier = args.timeout_multiplier + + BRTestSettings( + logtofile=not args.stdout, + downloaddir=args.download, + outputdir=os.path.abspath(args.output), + keepbuilds=args.keep, + jlevel=jlevel, + timeout_multiplier=args.timeout_multiplier + ).to_env() nose2_args = ["-v", "-N", str(args.testcases), @@ -142,15 +145,4 @@ def main(): if __name__ == "__main__": - # python 3.14 changed default start method from fork to - # fork-server, which is not compatible with the nose2 setup - if multiprocessing.get_start_method(allow_none=True) != "fork": - multiprocessing.set_start_method("fork", force=True) - # set_start_method throws a RuntimeError if called more than - # once. - # Debian python3-nose2 0.15.1-2 includes a patch adding - # another set_start_method() call, so monkey patch it out to - # get rid of this - multiprocessing.set_start_method = lambda *args: None - sys.exit(main())