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
3d2141bcee.

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 <fiona.klute@gmx.de>
Signed-off-by: Thomas Petazzoni <thomas.petazzoni@bootlin.com>
This commit is contained in:
Fiona Klute
2026-03-06 22:55:39 +01:00
committed by Thomas Petazzoni
parent 3872cf8a75
commit 10088b5381
2 changed files with 52 additions and 23 deletions

View File

@@ -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)

View File

@@ -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())