Skip to content

Commit c33eba6

Browse files
committed
tests: Port integration tests to barrage
barrage has no equivalent to pytest's `parametrize`. Open-code that with `do_test_*(params...)` helpers and individual tests calling that. Stop passing the explicit `--distribution` to the test. The new `ImageConfigManager` singleton (which replaces the old `config` pytest fixture) reads the distribution from `mkosi.local.conf`. This avoids having to specify it twice, and leaves the configuration to `integration-test-setup.sh`. Drop the `log_setup()` call. The integration tests themselves don't use `logging`, this mostly just affects the called `mkosi` subprocesses which already do it. This is very prone to interfere with barrage's own stderr capturing. Replace the `--debug-shell` pytest option with a `TEST_DEBUG_SHELL` env variable, as barrage doesn't have test-defined CLI options. Make the mkosi invocations in tests/__init__.py async, so that they can actually run in parallel. Remove installation and remaining traces of pytest. Co-developed-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c3e9ce5 commit c33eba6

15 files changed

Lines changed: 250 additions & 230 deletions

.github/workflows/ci.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,14 +144,14 @@ jobs:
144144
- name: Run integration tests
145145
run: |
146146
# Without KVM the tests are way too slow and time out
147+
# barrage currently has no dynamic runtime throttling to resources
148+
# each test runs a ~ 1.5 GiB VM, and GitHub runners have 4 GiB
147149
if [[ -e /dev/kvm ]]; then
148150
sudo mkosi box -- \
149151
timeout -k 30 1h \
150-
python3 -m pytest \
151-
--tb=no \
152-
--capture=no \
152+
python3 -m barrage \
153153
--verbose \
154-
-m integration \
155-
--distribution ${{ matrix.distro }} \
154+
--max-concurrency=2 \
155+
--pattern 'integration_*.py' \
156156
tests/
157157
fi

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
.mypy_cache/
66
.project
77
.pydevproject
8-
.pytest_cache/
98
/.mkosi-*
109
/SHA256SUMS
1110
/SHA256SUMS.gpg

AGENTS.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@ Always consult these files as needed:
1717
- select a subset by name, e.g. `bin/mkosi box -- python3 -m barrage test_mypy` to run a specific check
1818
- `bin/mkosi box -- ruff format mkosi tests kernel-install/*.install` to format code
1919
- `bin/mkosi box -- ruff check --fix mkosi tests kernel-install/*.install` to fix ruff issues
20-
- `python3 -m pytest -m integration ...` to run integration tests. No need to run these by default.
20+
- `bin/mkosi box -- python3 -m barrage --pattern 'integration_*.py' tests/` to run integration tests (after `tools/integration-test-setup.sh`). No need to run these by default.
2121
- `bin/mkosi box -- python3 -m barrage --pattern 'install_*.py'` to run installation tests (venv/pip/zipapp). Skipped by default as they install from the network. No need to run these by default.
22-
2322
- Never invent your own build commands or try to optimize the build process.
2423
- Never use `head`, `tail`, or pipe (`|`) the output of build or test commands. Always let the full output
2524
display. This is critical for diagnosing build and test failures.

README.md

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,9 @@ When a tool that mkosi runs inside its sandbox fails, see
161161

162162
# Integration tests
163163

164-
Integration tests build and boot full images. They are marked with the
165-
`integration` marker, and are skipped by default. They need a tools tree and an
166-
image to be built first. `tools/integration-test-setup.sh` writes a local
164+
Integration tests build and boot full images. They live in `integration_*.py`
165+
files and are not run as part of the normal test suite. They need a tools tree
166+
and an image to be built first; `tools/integration-test-setup.sh` writes a local
167167
configuration for the given image and tools tree distribution and builds both:
168168

169169
```sh
@@ -176,13 +176,37 @@ single integration test:
176176
```sh
177177
tools/integration-test-setup.sh arch fedora
178178

179-
bin/mkosi box -- pytest -m integration --distribution arch --capture=no --verbose \
180-
'tests/test_boot.py::test_bootloader[systemd-boot]'
179+
bin/mkosi box -- python3 -m barrage -v \
180+
'tests/integration_boot.py::test_bootloader_systemd_boot'
181+
```
182+
183+
To run all integration tests, select them by file name pattern:
184+
185+
```sh
186+
bin/mkosi box -- python3 -m barrage --pattern 'integration_*.py' tests/
181187
```
182188

183189
The integration tests require KVM and are skipped (or very slow) without
184190
`/dev/kvm`.
185191

192+
To debug a failing build, set `TEST_DEBUG_SHELL=1` to pass `--debug-shell` to
193+
mkosi, which drops into an interactive shell when a build step fails. This only
194+
makes sense when running a single test interactively (`barrage -i`):
195+
196+
```sh
197+
TEST_DEBUG_SHELL=1 bin/mkosi box -- python3 -m barrage -i \
198+
'tests/integration_boot.py::test_bootloader_systemd_boot'
199+
```
200+
201+
barrage runs tests concurrently with no limit by default. Pass
202+
`--max-concurrency N` to restrict the parallelism.
203+
204+
When running unprivileged (i.e. not as root), building several images in the
205+
same session tends to exhaust systemd-nsresourced's pool of dynamic UID ranges,
206+
which makes builds fail with `io.systemd.NamespaceResource.NoDynamicRange`. Run
207+
the tests one at a time (or with `--max-concurrency 1`) to avoid this; it does
208+
not happen when the tests run as root, as they do in CI.
209+
186210
# References
187211

188212
* [Primary mkosi git repository on GitHub](https://github.com/systemd/mkosi/)

mkosi.tools.conf/mkosi.conf.d/arch.conf

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ Packages=
1111
mypy
1212
pyright
1313
python-pip
14-
python-pytest
1514
reuse
1615
ruff
1716
sequoia-sop

mkosi.tools.conf/mkosi.conf.d/azure-centos-fedora.conf

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,4 @@ Packages=
1212
python3-mypy
1313
python3-pip
1414
npm
15-
python3-pytest
1615
shellcheck

mkosi.tools.conf/mkosi.conf.d/debian-kali-ubuntu.conf

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ Packages=
1313
mypy
1414
npm
1515
python3-pip
16-
python3-pytest
1716
reuse
1817
sqop
1918
shellcheck

mkosi.tools.conf/mkosi.conf.d/opensuse.conf

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ Packages=
1111
mypy
1212
npm
1313
python3-pip
14-
python3-pytest
1514
reuse
1615
ruff
1716
ShellCheck

pyproject.toml

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,3 @@ lint.select = [
110110

111111
[tool.ruff.lint.flake8-bugbear]
112112
extend-immutable-calls = ["mkosi.run.nosandbox"]
113-
114-
[tool.pytest.ini_options]
115-
markers = [
116-
"integration: mark a test as an integration test.",
117-
"install: mark a test as an installation test (venv/pip/zipapp).",
118-
]
119-
testpaths = ["tests"]
120-
# The unit and install tests run under barrage now; pytest only runs the integration tests, which live
121-
# in integration_*.py files (not pytest's default test_*.py pattern).
122-
python_files = ["integration_*.py"]

tests/__init__.py

Lines changed: 51 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
# SPDX-License-Identifier: LGPL-2.1-or-later
22

3-
import contextlib
3+
import asyncio
44
import dataclasses
55
import os
66
import subprocess
77
import sys
88
import uuid
9-
from collections.abc import Iterator, Mapping, Sequence
9+
from collections.abc import Mapping, Sequence
1010
from pathlib import Path
1111
from types import TracebackType
12-
from typing import Any, Optional
12+
from typing import Optional
1313

14-
import pytest
14+
from barrage import Singleton
1515

16+
import mkosi.resources
17+
from mkosi.config import parse_config
1618
from mkosi.distribution import Distribution
1719
from mkosi.run import CompletedProcess, run
1820
from mkosi.tree import rmtree
1921
from mkosi.user import INVOKING_USER
20-
from mkosi.util import _FILE, PathString
22+
from mkosi.util import _FILE, PathString, resource_path
2123

2224

2325
@dataclasses.dataclass(frozen=True)
@@ -28,6 +30,36 @@ class ImageConfig:
2830
snapshot: Optional[str] = None
2931

3032

33+
class ImageConfigManager(Singleton):
34+
"""Provide the integration test ImageConfig
35+
36+
The distribution and release are read from mkosi.local.conf as written by
37+
tools/integration-test-setup.sh.
38+
"""
39+
40+
config: ImageConfig
41+
42+
async def __aenter__(self) -> "ImageConfigManager":
43+
if not Path("mkosi.local.conf").exists():
44+
raise RuntimeError(
45+
"mkosi.local.conf not found: run 'tools/integration-test-setup.sh "
46+
"<distribution> <tools-tree-distribution>' to configure and build the image before "
47+
"running the integration tests."
48+
)
49+
50+
with resource_path(mkosi.resources) as resources:
51+
config = parse_config(resources=resources)[2][0]
52+
self.config = ImageConfig(
53+
distribution=config.distribution,
54+
release=config.release,
55+
debug_shell=bool(os.getenv("TEST_DEBUG_SHELL")),
56+
# Pin the same snapshot as the main image so builds that don't read mkosi.local.conf still
57+
# use it (e.g. the extension build, which passes --directory '').
58+
snapshot=config.snapshot,
59+
)
60+
return self
61+
62+
3163
class Image:
3264
def __init__(self, config: ImageConfig) -> None:
3365
self.config = config
@@ -53,7 +85,7 @@ def __exit__(
5385
) -> None:
5486
rmtree(self.output_dir)
5587

56-
def mkosi(
88+
async def mkosi(
5789
self,
5890
verb: str,
5991
options: Sequence[PathString] = (),
@@ -62,7 +94,11 @@ def mkosi(
6294
check: bool = True,
6395
env: Mapping[str, str] = {},
6496
) -> CompletedProcess:
65-
return run(
97+
# mkosi.run.run() is synchronous, so run it in a worker thread.
98+
# Safe because the test-level invocation uses no sandbox (and thus no preexec_fn) and installs no
99+
# signal handlers; all sandboxing happens inside the spawned mkosi subprocess.
100+
return await asyncio.to_thread(
101+
run,
66102
[
67103
"python3", "-m", "mkosi",
68104
"--debug",
@@ -73,10 +109,10 @@ def mkosi(
73109
check=check,
74110
stdin=stdin,
75111
stdout=sys.stdout,
76-
env=os.environ | env,
112+
env={**os.environ, **env},
77113
) # fmt: skip
78114

79-
def build(
115+
async def build(
80116
self,
81117
options: Sequence[PathString] = (),
82118
args: Sequence[str] = (),
@@ -107,18 +143,18 @@ def build(
107143
*options,
108144
] # fmt: skip
109145

110-
self.mkosi("summary", opt, env=env)
146+
await self.mkosi("summary", opt, env=env)
111147

112-
return self.mkosi(
148+
return await self.mkosi(
113149
"build",
114150
opt,
115151
args,
116152
stdin=sys.stdin if sys.stdin.isatty() else None,
117153
env=env,
118154
)
119155

120-
def boot(self, options: Sequence[str] = (), args: Sequence[str] = ()) -> CompletedProcess:
121-
result = self.mkosi(
156+
async def boot(self, options: Sequence[str] = (), args: Sequence[str] = ()) -> CompletedProcess:
157+
result = await self.mkosi(
122158
"boot",
123159
[
124160
"--runtime-build-sources=no",
@@ -139,15 +175,15 @@ def boot(self, options: Sequence[str] = (), args: Sequence[str] = ()) -> Complet
139175

140176
return result
141177

142-
def vm(
178+
async def vm(
143179
self,
144180
options: Sequence[str] = (),
145181
args: Sequence[str] = (),
146182
ram: str = "1536M",
147183
) -> CompletedProcess:
148184
need_hyperv_workaround = os.uname().machine == "x86_64"
149185

150-
result = self.mkosi(
186+
result = await self.mkosi(
151187
"vm",
152188
[
153189
"--runtime-build-sources=no",
@@ -171,35 +207,3 @@ def vm(
171207
raise subprocess.CalledProcessError(result.returncode, result.args, result.stdout, result.stderr)
172208

173209
return result
174-
175-
176-
@pytest.fixture(scope="session", autouse=True)
177-
def suspend_capture_stdin(pytestconfig: Any) -> Iterator[None]:
178-
"""
179-
When --capture=no (or -s) is specified, pytest will still intercept
180-
stdin. Let's explicitly make it not capture stdin when --capture=no is
181-
specified so we can debug image boot failures by logging into the emergency
182-
shell.
183-
"""
184-
185-
capmanager: Any = pytestconfig.pluginmanager.getplugin("capturemanager")
186-
187-
if pytestconfig.getoption("capture") == "no":
188-
capmanager.suspend_global_capture(in_=True)
189-
190-
yield
191-
192-
if pytestconfig.getoption("capture") == "no":
193-
capmanager.resume_global_capture()
194-
195-
196-
@contextlib.contextmanager
197-
def ci_group(s: str) -> Iterator[None]:
198-
github_actions = os.getenv("GITHUB_ACTIONS")
199-
if github_actions:
200-
print(f"\n::group::{s}", flush=True)
201-
try:
202-
yield
203-
finally:
204-
if github_actions:
205-
print("\n::endgroup::", flush=True)

0 commit comments

Comments
 (0)