diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 675a72824e..590a54a21d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,10 +30,10 @@ jobs: mkosi -f box -- true - name: Run unit tests - run: mkosi box -- python3 -m pytest -sv + run: mkosi box -- python3 -m barrage -v - name: Run installation tests - run: mkosi box -- python3 -m pytest -sv -m install + run: mkosi box -- python3 -m barrage -v --pattern 'install_*.py' - name: Test execution from current working directory (sudo call) run: sudo python3 -m mkosi -h @@ -148,14 +148,14 @@ jobs: - name: Run integration tests run: | # Without KVM the tests are way too slow and time out + # barrage currently has no dynamic runtime throttling to resources + # each test runs a ~ 1.5 GiB VM, and GitHub runners have 4 GiB if [[ -e /dev/kvm ]]; then sudo mkosi box -- \ timeout -k 30 1h \ - python3 -m pytest \ - --tb=no \ - --capture=no \ + python3 -m barrage \ --verbose \ - -m integration \ - --distribution ${{ matrix.distro }} \ + --max-concurrency=2 \ + --pattern 'integration_*.py' \ tests/ fi diff --git a/.gitignore b/.gitignore index adcd113f43..0b94187d87 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ .mypy_cache/ .project .pydevproject -.pytest_cache/ /.mkosi-* /SHA256SUMS /SHA256SUMS.gpg diff --git a/AGENTS.md b/AGENTS.md index 516b7c3595..1099b5a011 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,13 +18,12 @@ Always consult these files as needed: ## Build and Test Commands - Running tests: See the "Hacking on mkosi" section in `README.md` for complete instructions. -- `bin/mkosi box -- pytest` to run all unit tests including linters and type checkers -- append usual pytest options like `-k test_mypy` to run a specific check +- `bin/mkosi box -- python3 -m barrage` to run all unit tests including linters and type checkers +- select a subset by name, e.g. `bin/mkosi box -- python3 -m barrage test_mypy` to run a specific check - `bin/mkosi box -- ruff format mkosi tests kernel-install/*.install` to format code - `bin/mkosi box -- ruff check --fix mkosi tests kernel-install/*.install` to fix ruff issues -- `python3 -m pytest -m integration ...` to run integration tests. No need to run these by default. -- `bin/mkosi box -- pytest -m install` to run installation tests (venv/pip/zipapp). Skipped by default as they install from the network. No need to run these by default. - +- `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. +- `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. - Never invent your own build commands or try to optimize the build process. - Never use `head`, `tail`, or pipe (`|`) the output of build or test commands. Always let the full output display. This is critical for diagnosing build and test failures. diff --git a/README.md b/README.md index fa1f89d0a4..cfc2625b36 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ modifications. To hack on mkosi itself, you can run the full test suite locally, just like CI does. The tests include linting, type checking, and unit tests, -all runnable via [pytest](https://github.com/pytest-dev/pytest). +all runnable via [barrage](https://github.com/amutable-systems/barrage). All linters such as `ruff` or `mypy` are run inside `mkosi box` (i.e. from inside `mkosi.tools/`) for a consistent environment. Build that with: @@ -134,25 +134,26 @@ All linters such as `ruff` or `mypy` are run inside `mkosi box` bin/mkosi -f box -- true ``` -Then run the full test suite inside the tools tree: +Then run the full unit test suite inside the tools tree: ```bash -bin/mkosi box -- pytest +bin/mkosi box -- python3 -m barrage ``` -You can use [pytest options](https://docs.pytest.org/en/stable/how-to/usage.html) to only run a subset, for -example only run the linters: +You can select a subset, for example only run the linters or only a single test: ```sh -bin/mkosi box -- pytest -k test_linters +bin/mkosi box -- python3 -m barrage tests/test_linters.py + +bin/mkosi box -- python3 -m barrage test_conversion ``` -Installation tests (venv, editable and zipapp installs) are marked with the -`install` marker and are skipped by default, as they create virtual -environments and install packages from the network. Run them explicitly with: +Installation tests (venv, editable and zipapp installs) live in `install_*.py` +files and are skipped by default, as they create virtual environments and +install packages from the network. Run them explicitly with: ```sh -bin/mkosi box -- pytest -m install +bin/mkosi box -- python3 -m barrage --pattern 'install_*.py' ``` When a tool that mkosi runs inside its sandbox fails, see @@ -160,9 +161,9 @@ When a tool that mkosi runs inside its sandbox fails, see # Integration tests -Integration tests build and boot full images. They are marked with the -`integration` marker, and are skipped by default. They need a tools tree and an -image to be built first. `tools/integration-test-setup.sh` writes a local +Integration tests build and boot full images. They live in `integration_*.py` +files and are not run as part of the normal test suite. They need a tools tree +and an image to be built first; `tools/integration-test-setup.sh` writes a local configuration for the given image and tools tree distribution and builds both: ```sh @@ -175,13 +176,37 @@ single integration test: ```sh tools/integration-test-setup.sh arch fedora -bin/mkosi box -- pytest -m integration --distribution arch --capture=no --verbose \ - 'tests/test_boot.py::test_bootloader[systemd-boot]' +bin/mkosi box -- python3 -m barrage -v \ + 'tests/integration_boot.py::test_bootloader_systemd_boot' +``` + +To run all integration tests, select them by file name pattern: + +```sh +bin/mkosi box -- python3 -m barrage --pattern 'integration_*.py' tests/ ``` The integration tests require KVM and are skipped (or very slow) without `/dev/kvm`. +To debug a failing build, set `TEST_DEBUG_SHELL=1` to pass `--debug-shell` to +mkosi, which drops into an interactive shell when a build step fails. This only +makes sense when running a single test interactively (`barrage -i`): + +```sh +TEST_DEBUG_SHELL=1 bin/mkosi box -- python3 -m barrage -i \ + 'tests/integration_boot.py::test_bootloader_systemd_boot' +``` + +barrage runs tests concurrently with no limit by default. Pass +`--max-concurrency N` to restrict the parallelism. + +When running unprivileged (i.e. not as root), building several images in the +same session tends to exhaust systemd-nsresourced's pool of dynamic UID ranges, +which makes builds fail with `io.systemd.NamespaceResource.NoDynamicRange`. Run +the tests one at a time (or with `--max-concurrency 1`) to avoid this; it does +not happen when the tests run as root, as they do in CI. + # References * [Primary mkosi git repository on GitHub](https://github.com/systemd/mkosi/) diff --git a/REUSE.toml b/REUSE.toml index 10324ff052..a9a9c11add 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -39,6 +39,7 @@ path = [ "**.zsh", "mkosi.credentials/*", "mkosi/resources/pandoc/*.lua", + "mkosi.tools.conf/test-requirements.txt", ] precedence = "aggregate" SPDX-FileCopyrightText = "Mkosi Contributors" diff --git a/mkosi.tools.conf/mkosi.conf.d/arch.conf b/mkosi.tools.conf/mkosi.conf.d/arch.conf index 3a84ab16de..d5012bc0ba 100644 --- a/mkosi.tools.conf/mkosi.conf.d/arch.conf +++ b/mkosi.tools.conf/mkosi.conf.d/arch.conf @@ -10,7 +10,7 @@ Packages= mkinitcpio mypy pyright - python-pytest + python-pip reuse ruff sequoia-sop diff --git a/mkosi.tools.conf/mkosi.conf.d/azure-centos-fedora.conf b/mkosi.tools.conf/mkosi.conf.d/azure-centos-fedora.conf index d0adb35302..eaa4837fab 100644 --- a/mkosi.tools.conf/mkosi.conf.d/azure-centos-fedora.conf +++ b/mkosi.tools.conf/mkosi.conf.d/azure-centos-fedora.conf @@ -10,6 +10,6 @@ Packages= codespell cryptsetup python3-mypy + python3-pip npm - python3-pytest shellcheck diff --git a/mkosi.tools.conf/mkosi.conf.d/debian-kali-ubuntu.conf b/mkosi.tools.conf/mkosi.conf.d/debian-kali-ubuntu.conf index 96b9284e0b..bcee6d94f2 100644 --- a/mkosi.tools.conf/mkosi.conf.d/debian-kali-ubuntu.conf +++ b/mkosi.tools.conf/mkosi.conf.d/debian-kali-ubuntu.conf @@ -12,7 +12,7 @@ Packages= fdisk mypy npm - python3-pytest + python3-pip reuse sqop shellcheck diff --git a/mkosi.tools.conf/mkosi.conf.d/opensuse.conf b/mkosi.tools.conf/mkosi.conf.d/opensuse.conf index 0456df4ce8..e47d863f99 100644 --- a/mkosi.tools.conf/mkosi.conf.d/opensuse.conf +++ b/mkosi.tools.conf/mkosi.conf.d/opensuse.conf @@ -10,7 +10,7 @@ Packages= grub2 # TODO: Move to default tools tree when https://bugzilla.opensuse.org/show_bug.cgi?id=1227464 is resolved. mypy npm - python3-pytest + python3-pip reuse ruff ShellCheck diff --git a/mkosi.tools.conf/mkosi.conf.d/postmarketos.conf b/mkosi.tools.conf/mkosi.conf.d/postmarketos.conf index 9963e433c1..d4533a8c9f 100644 --- a/mkosi.tools.conf/mkosi.conf.d/postmarketos.conf +++ b/mkosi.tools.conf/mkosi.conf.d/postmarketos.conf @@ -6,5 +6,6 @@ Distribution=postmarketos [Content] Packages= py3-codespell + py3-pip py3-pytest ruff diff --git a/mkosi.tools.conf/mkosi.prepare.chroot b/mkosi.tools.conf/mkosi.prepare.chroot index 04606aff29..5ca0c004e4 100755 --- a/mkosi.tools.conf/mkosi.prepare.chroot +++ b/mkosi.tools.conf/mkosi.prepare.chroot @@ -8,3 +8,5 @@ if ! command -v pyright &>/dev/null; then fi done fi + +python3 -m pip install --break-system-packages -r "$SRCDIR/mkosi.tools.conf/test-requirements.txt" diff --git a/mkosi.tools.conf/test-requirements.txt b/mkosi.tools.conf/test-requirements.txt new file mode 100644 index 0000000000..3de848adfe --- /dev/null +++ b/mkosi.tools.conf/test-requirements.txt @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# Test runner installed into the tools tree. +# FIXME: move to version pin once published on pypi, to get dependabot coverage +barrage @ git+https://github.com/amutable-systems/barrage.git@8361c35b7ad7c00e6a446f16d5a269f07a35393b diff --git a/pyproject.toml b/pyproject.toml index 51c76db4e7..0c2b873ae9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,6 +84,18 @@ warn_unreachable = true strict_equality = true scripts_are_modules = true +# barrage (the async test runner used by tests/) requires Python 3.12+ syntax, which mypy cannot parse +# when checking under our supported floor (3.9/3.10). Skip following it and don't flag the test classes +# that subclass its (now Any-typed) AsyncTestCase. pyright still type-checks tests/ fully. +[[tool.mypy.overrides]] +module = "barrage.*" +ignore_missing_imports = true +follow_imports = "skip" + +[[tool.mypy.overrides]] +module = "tests.*" +disallow_subclassing_any = false + [tool.ruff] target-version = "py39" line-length = 109 @@ -98,11 +110,3 @@ lint.select = [ [tool.ruff.lint.flake8-bugbear] extend-immutable-calls = ["mkosi.run.nosandbox"] - -[tool.pytest.ini_options] -markers = [ - "integration: mark a test as an integration test.", - "install: mark a test as an installation test (venv/pip/zipapp).", -] -addopts = "-m \"not integration and not install\"" -testpaths = ["tests"] diff --git a/tests/__init__.py b/tests/__init__.py index 440d90a7c3..3028af4278 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,23 +1,25 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -import contextlib +import asyncio import dataclasses import os import subprocess import sys import uuid -from collections.abc import Iterator, Mapping, Sequence +from collections.abc import Mapping, Sequence from pathlib import Path from types import TracebackType -from typing import Any, Optional +from typing import Optional -import pytest +from barrage import Singleton +import mkosi.resources +from mkosi.config import parse_config from mkosi.distribution import Distribution from mkosi.run import CompletedProcess, run from mkosi.tree import rmtree from mkosi.user import INVOKING_USER -from mkosi.util import _FILE, PathString +from mkosi.util import _FILE, PathString, resource_path @dataclasses.dataclass(frozen=True) @@ -28,6 +30,36 @@ class ImageConfig: snapshot: Optional[str] = None +class ImageConfigManager(Singleton): + """Provide the integration test ImageConfig + + The distribution and release are read from mkosi.local.conf as written by + tools/integration-test-setup.sh. + """ + + config: ImageConfig + + async def __aenter__(self) -> "ImageConfigManager": + if not Path("mkosi.local.conf").exists(): + raise RuntimeError( + "mkosi.local.conf not found: run 'tools/integration-test-setup.sh " + " ' to configure and build the image before " + "running the integration tests." + ) + + with resource_path(mkosi.resources) as resources: + config = parse_config(resources=resources)[2][0] + self.config = ImageConfig( + distribution=config.distribution, + release=config.release, + debug_shell=bool(os.getenv("TEST_DEBUG_SHELL")), + # Pin the same snapshot as the main image so builds that don't read mkosi.local.conf still + # use it (e.g. the extension build, which passes --directory ''). + snapshot=config.snapshot, + ) + return self + + class Image: def __init__(self, config: ImageConfig) -> None: self.config = config @@ -53,7 +85,7 @@ def __exit__( ) -> None: rmtree(self.output_dir) - def mkosi( + async def mkosi( self, verb: str, options: Sequence[PathString] = (), @@ -62,7 +94,11 @@ def mkosi( check: bool = True, env: Mapping[str, str] = {}, ) -> CompletedProcess: - return run( + # mkosi.run.run() is synchronous, so run it in a worker thread. + # Safe because the test-level invocation uses no sandbox (and thus no preexec_fn) and installs no + # signal handlers; all sandboxing happens inside the spawned mkosi subprocess. + return await asyncio.to_thread( + run, [ "python3", "-m", "mkosi", "--debug", @@ -73,10 +109,10 @@ def mkosi( check=check, stdin=stdin, stdout=sys.stdout, - env=os.environ | env, + env={**os.environ, **env}, ) # fmt: skip - def build( + async def build( self, options: Sequence[PathString] = (), args: Sequence[str] = (), @@ -107,9 +143,9 @@ def build( *options, ] # fmt: skip - self.mkosi("summary", opt, env=env) + await self.mkosi("summary", opt, env=env) - return self.mkosi( + return await self.mkosi( "build", opt, args, @@ -117,8 +153,8 @@ def build( env=env, ) - def boot(self, options: Sequence[str] = (), args: Sequence[str] = ()) -> CompletedProcess: - result = self.mkosi( + async def boot(self, options: Sequence[str] = (), args: Sequence[str] = ()) -> CompletedProcess: + result = await self.mkosi( "boot", [ "--runtime-build-sources=no", @@ -139,7 +175,7 @@ def boot(self, options: Sequence[str] = (), args: Sequence[str] = ()) -> Complet return result - def vm( + async def vm( self, options: Sequence[str] = (), args: Sequence[str] = (), @@ -147,7 +183,7 @@ def vm( ) -> CompletedProcess: need_hyperv_workaround = os.uname().machine == "x86_64" - result = self.mkosi( + result = await self.mkosi( "vm", [ "--runtime-build-sources=no", @@ -171,35 +207,3 @@ def vm( raise subprocess.CalledProcessError(result.returncode, result.args, result.stdout, result.stderr) return result - - -@pytest.fixture(scope="session", autouse=True) -def suspend_capture_stdin(pytestconfig: Any) -> Iterator[None]: - """ - When --capture=no (or -s) is specified, pytest will still intercept - stdin. Let's explicitly make it not capture stdin when --capture=no is - specified so we can debug image boot failures by logging into the emergency - shell. - """ - - capmanager: Any = pytestconfig.pluginmanager.getplugin("capturemanager") - - if pytestconfig.getoption("capture") == "no": - capmanager.suspend_global_capture(in_=True) - - yield - - if pytestconfig.getoption("capture") == "no": - capmanager.resume_global_capture() - - -@contextlib.contextmanager -def ci_group(s: str) -> Iterator[None]: - github_actions = os.getenv("GITHUB_ACTIONS") - if github_actions: - print(f"\n::group::{s}", flush=True) - try: - yield - finally: - if github_actions: - print("\n::endgroup::", flush=True) diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 0e5e118f74..0000000000 --- a/tests/conftest.py +++ /dev/null @@ -1,79 +0,0 @@ -# SPDX-License-Identifier: LGPL-2.1-or-later - -from collections.abc import Iterator -from typing import Any, cast - -import pytest - -import mkosi.resources -from mkosi.config import parse_config -from mkosi.distribution import Distribution, detect_distribution -from mkosi.log import log_setup -from mkosi.util import resource_path - -from . import ImageConfig, ci_group - - -def pytest_addoption(parser: Any) -> None: - distribution = detect_distribution()[0] - parser.addoption( - "-D", - "--distribution", - metavar="DISTRIBUTION", - help="Run the integration tests for the given distribution.", - default=distribution if isinstance(distribution, Distribution) else None, - type=Distribution, - choices=[Distribution(d) for d in Distribution.values()], - ) - parser.addoption( - "-R", - "--release", - metavar="RELEASE", - help="Run the integration tests for the given release.", - ) - parser.addoption( - "--debug-shell", - help="Pass --debug-shell when running mkosi", - action="store_true", - ) - - -@pytest.fixture(scope="session") -def config(request: Any) -> ImageConfig: - distribution = cast(Distribution, request.config.getoption("--distribution")) - with resource_path(mkosi.resources) as resources: - # Reads the local configuration (mkosi.local.conf) written by integration-test-setup.sh. - parsed = parse_config(["-d", str(distribution)], resources=resources)[2][0] - release = cast(str, request.config.getoption("--release") or parsed.release) - return ImageConfig( - distribution=distribution, - release=release, - debug_shell=request.config.getoption("--debug-shell"), - # Pin the same snapshot as the main image so builds that don't read mkosi.local.conf still - # use it (e.g. the extension build, which passes --directory ''). - snapshot=parsed.snapshot, - ) - - -@pytest.fixture(autouse=True) -def ci_sections(request: Any) -> Iterator[None]: - with ci_group(request.node.name): - yield - - -@pytest.fixture(scope="session", autouse=True) -def logging() -> None: - log_setup() - - -@pytest.hookimpl(hookwrapper=True) -def pytest_runtest_makereport(item: Any, call: Any) -> Iterator[None]: - """Suppress tracebacks for test_linters.py tests. - - These are not helpful and just noise. - """ - outcome = yield - report = outcome.get_result() # type: ignore - - if item.parent.name == "test_linters.py" and report.failed and call.excinfo is not None: - report.longrepr = str(call.excinfo.value) diff --git a/tests/test_install.py b/tests/install_mkosi.py similarity index 89% rename from tests/test_install.py rename to tests/install_mkosi.py index d588b6542a..5b4dd79776 100644 --- a/tests/test_install.py +++ b/tests/install_mkosi.py @@ -4,21 +4,17 @@ import tempfile from pathlib import Path -import pytest - from mkosi.run import run REPO_ROOT = Path(__file__).parent.parent -pytestmark = pytest.mark.install - -def test_mkosi_help_direct() -> None: +async def test_mkosi_help_direct() -> None: """Test mkosi can be run from current directory.""" run(["python3", "-m", "mkosi", "-h"], cwd=REPO_ROOT) -def test_venv_installation() -> None: +async def test_venv_installation() -> None: """Test mkosi can be installed in a venv.""" with tempfile.TemporaryDirectory() as tmpdir: venv = Path(tmpdir) / "testvenv" @@ -40,7 +36,7 @@ def test_venv_installation() -> None: run([os.fspath(venv / "bin/mkosi"), "-h"], cwd=REPO_ROOT) -def test_editable_venv_installation() -> None: +async def test_editable_venv_installation() -> None: """Test mkosi can be installed in editable mode.""" with tempfile.TemporaryDirectory() as tmpdir: venv = Path(tmpdir) / "testvenv" @@ -61,7 +57,7 @@ def test_editable_venv_installation() -> None: run([os.fspath(venv / "bin/mkosi"), "-h"], cwd=REPO_ROOT) -def test_zipapp_creation() -> None: +async def test_zipapp_creation() -> None: """Test zipapp generation.""" run(["./tools/generate-zipapp.sh"], cwd=REPO_ROOT) diff --git a/tests/integration_boot.py b/tests/integration_boot.py new file mode 100644 index 0000000000..698ed472ef --- /dev/null +++ b/tests/integration_boot.py @@ -0,0 +1,129 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +import os +import subprocess + +import barrage.assertions as Assert + +from mkosi.config import Bootloader, Firmware, OutputFormat +from mkosi.distribution import Distribution +from mkosi.run import find_binary, run +from mkosi.versioncomp import GenericVersion + +from . import Image, ImageConfigManager + + +def have_vmspawn() -> bool: + return find_binary("systemd-vmspawn") is not None and ( + GenericVersion(run(["systemd-vmspawn", "--version"], stdout=subprocess.PIPE).stdout.strip()) >= 256 + ) + + +async def do_test_format(image_config: ImageConfigManager, format: OutputFormat) -> None: + with Image(image_config.config) as image: + if image.config.distribution == Distribution.rhel_ubi and format in ( + OutputFormat.esp, + OutputFormat.uki, + ): + Assert.skip("Cannot build RHEL-UBI images with format 'esp' or 'uki'") + + await image.build(options=["--format", str(format)]) + + # FIXME: Also boot directory images when the CI runs systemd v260 or newer. + if format == OutputFormat.directory: + return + + if format == OutputFormat.disk and os.getuid() == 0: + await image.boot() + + if format in (OutputFormat.cpio, OutputFormat.uki, OutputFormat.esp): + Assert.skip("Default image is too large to be able to boot in CPIO/UKI/ESP format") + + if image.config.distribution == Distribution.rhel_ubi: + return + + if format in (OutputFormat.tar, OutputFormat.oci, OutputFormat.none, OutputFormat.portable): + return + + await image.vm() + + if have_vmspawn() and format == OutputFormat.disk: + await image.vm(options=["--vmm=vmspawn"]) + + if format != OutputFormat.disk: + return + + +async def test_format_cpio(image_config: ImageConfigManager) -> None: + await do_test_format(image_config, OutputFormat.cpio) + + +async def test_format_directory(image_config: ImageConfigManager) -> None: + await do_test_format(image_config, OutputFormat.directory) + + +async def test_format_disk(image_config: ImageConfigManager) -> None: + await do_test_format(image_config, OutputFormat.disk) + + +async def test_format_esp(image_config: ImageConfigManager) -> None: + await do_test_format(image_config, OutputFormat.esp) + + +async def test_format_none(image_config: ImageConfigManager) -> None: + await do_test_format(image_config, OutputFormat.none) + + +async def test_format_portable(image_config: ImageConfigManager) -> None: + await do_test_format(image_config, OutputFormat.portable) + + +async def test_format_tar(image_config: ImageConfigManager) -> None: + await do_test_format(image_config, OutputFormat.tar) + + +async def test_format_uki(image_config: ImageConfigManager) -> None: + await do_test_format(image_config, OutputFormat.uki) + + +async def test_format_oci(image_config: ImageConfigManager) -> None: + await do_test_format(image_config, OutputFormat.oci) + + +async def do_test_bootloader(image_config: ImageConfigManager, bootloader: Bootloader) -> None: + if image_config.config.distribution == Distribution.rhel_ubi or bootloader.is_signed(): + return + + firmware = Firmware.linux if bootloader == Bootloader.none else Firmware.auto + + with Image(image_config.config) as image: + await image.build(["--format=disk", "--bootloader", str(bootloader)]) + await image.vm(["--firmware", str(firmware)]) + + +async def test_bootloader_none(image_config: ImageConfigManager) -> None: + await do_test_bootloader(image_config, Bootloader.none) + + +async def test_bootloader_uki(image_config: ImageConfigManager) -> None: + await do_test_bootloader(image_config, Bootloader.uki) + + +async def test_bootloader_systemd_boot(image_config: ImageConfigManager) -> None: + await do_test_bootloader(image_config, Bootloader.systemd_boot) + + +async def test_bootloader_grub(image_config: ImageConfigManager) -> None: + await do_test_bootloader(image_config, Bootloader.grub) + + +async def test_bootloader_uki_signed(image_config: ImageConfigManager) -> None: + await do_test_bootloader(image_config, Bootloader.uki_signed) + + +async def test_bootloader_systemd_boot_signed(image_config: ImageConfigManager) -> None: + await do_test_bootloader(image_config, Bootloader.systemd_boot_signed) + + +async def test_bootloader_grub_signed(image_config: ImageConfigManager) -> None: + await do_test_bootloader(image_config, Bootloader.grub_signed) diff --git a/tests/integration_extension.py b/tests/integration_extension.py new file mode 100644 index 0000000000..6b804bbdb9 --- /dev/null +++ b/tests/integration_extension.py @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +from pathlib import Path + +from mkosi.config import OutputFormat + +from . import Image, ImageConfigManager + + +async def do_test(image_config: ImageConfigManager, format: OutputFormat) -> None: + with Image(image_config.config) as image: + await image.build(["--clean-package-metadata=no", "--format=directory"]) + + with Image(image.config) as sysext: + await sysext.build( + [ + "--directory", + "", + "--incremental=no", + "--base-tree", Path(image.output_dir) / "image", + "--overlay=yes", + "--selinux-relabel=no", + "--package=lsof", + f"--format={format}", + ] + ) # fmt: skip + + +async def test_confext(image_config: ImageConfigManager) -> None: + await do_test(image_config, OutputFormat.confext) + + +async def test_sysext(image_config: ImageConfigManager) -> None: + await do_test(image_config, OutputFormat.sysext) + + +async def test_addon(image_config: ImageConfigManager) -> None: + await do_test(image_config, OutputFormat.addon) diff --git a/tests/test_initrd.py b/tests/integration_initrd.py similarity index 71% rename from tests/test_initrd.py rename to tests/integration_initrd.py index ef77c48065..105a9713f2 100644 --- a/tests/test_initrd.py +++ b/tests/integration_initrd.py @@ -8,16 +8,14 @@ from collections.abc import Iterator from pathlib import Path -import pytest +import barrage.assertions as Assert from mkosi.run import run from mkosi.sandbox import umask from mkosi.tree import copy_tree from mkosi.util import PathString -from . import Image, ImageConfig - -pytestmark = pytest.mark.integration +from . import Image, ImageConfigManager @contextlib.contextmanager @@ -35,10 +33,10 @@ def mount(what: PathString, where: PathString) -> Iterator[Path]: run(["umount", "--no-mtab", where]) -@pytest.fixture(scope="module") +@contextlib.contextmanager def passphrase() -> Iterator[Path]: - # We can't use tmp_path fixture because pytest creates it in a nested directory we can't access using our - # unprivileged user. + # We use a NamedTemporaryFile in /tmp rather than a per-test temporary directory because the latter is + # created in a nested directory we can't access using our unprivileged user. # TODO: Use delete_on_close=False and close() instead of flush() when we require Python 3.12 or newer. with tempfile.NamedTemporaryFile(prefix="mkosi.passphrase", mode="w") as passphrase: passphrase.write("mkosi") @@ -49,16 +47,18 @@ def passphrase() -> Iterator[Path]: yield Path(passphrase.name) -def test_initrd(config: ImageConfig) -> None: - with Image(config) as image: - image.build(options=["--format=disk"]) - image.vm() +async def test_initrd(image_config: ImageConfigManager) -> None: + with Image(image_config.config) as image: + await image.build(options=["--format=disk"]) + await image.vm() -@pytest.mark.skipif(os.getuid() != 0, reason="mkosi-initrd LVM test can only be executed as root") -def test_initrd_lvm(config: ImageConfig) -> None: - with Image(config) as image, contextlib.ExitStack() as stack: - image.build(["--format=disk"]) +async def test_initrd_lvm(image_config: ImageConfigManager) -> None: + if os.getuid() != 0: + Assert.skip("mkosi-initrd LVM test can only be executed as root") + + with Image(image_config.config) as image, contextlib.ExitStack() as stack: + await image.build(["--format=disk"]) lvm = Path(image.output_dir) / "lvm.raw" lvm.touch() @@ -68,7 +68,10 @@ def test_initrd_lvm(config: ImageConfig) -> None: ["losetup", "--show", "--find", "--partscan", lvm], stdout=subprocess.PIPE ).stdout.strip() stack.callback(run, ["losetup", "--detach", lodev]) - run(["sfdisk", "--label", "gpt", lodev], input="type=E6D6D379-F507-44C2-A23C-238F2A3DF928 bootable") + run( + ["sfdisk", "--label", "gpt", lodev], + input="type=E6D6D379-F507-44C2-A23C-238F2A3DF928 bootable", + ) run(["lvm", "pvcreate", "--devicesfile", "", f"{lodev}p1"]) run(["lvm", "pvs", "--devicesfile", ""]) run(["lvm", "vgcreate", "--devicesfile", "", "-An", "vg_mkosi", f"{lodev}p1"]) @@ -78,7 +81,14 @@ def test_initrd_lvm(config: ImageConfig) -> None: run(["lvm", "lvcreate", "--devicesfile", "", "-An", "-l", "100%FREE", "-n", "lv0", "vg_mkosi"]) run(["lvm", "lvs", "--devicesfile", ""]) run(["udevadm", "wait", "--timeout=30", "/dev/vg_mkosi/lv0"]) - run([f"mkfs.{image.config.distribution.installer.filesystem()}", "-L", "root", "/dev/vg_mkosi/lv0"]) + run( + [ + f"mkfs.{image.config.distribution.installer.filesystem()}", + "-L", + "root", + "/dev/vg_mkosi/lv0", + ] + ) src = Path(stack.enter_context(tempfile.TemporaryDirectory())) run(["systemd-dissect", "--mount", "--mkdir", Path(image.output_dir) / "image.raw", src]) @@ -93,7 +103,7 @@ def test_initrd_lvm(config: ImageConfig) -> None: lvm.rename(Path(image.output_dir) / "image.raw") - image.vm( + await image.vm( [ "--firmware=linux", # LVM confuses systemd-repart so we mask it for this test. @@ -103,8 +113,8 @@ def test_initrd_lvm(config: ImageConfig) -> None: ) -def test_initrd_luks(config: ImageConfig, passphrase: Path) -> None: - with tempfile.TemporaryDirectory() as repartd: +async def test_initrd_luks(image_config: ImageConfigManager) -> None: + with passphrase() as pp, tempfile.TemporaryDirectory() as repartd: st = Path.cwd().stat() os.chown(repartd, st.st_uid, st.st_gid) @@ -140,7 +150,7 @@ def test_initrd_luks(config: ImageConfig, passphrase: Path) -> None: f"""\ [Partition] Type=root - Format={config.distribution.installer.filesystem()} + Format={image_config.config.distribution.installer.filesystem()} Minimize=guess Encrypt=key-file CopyFiles=/ @@ -148,17 +158,19 @@ def test_initrd_luks(config: ImageConfig, passphrase: Path) -> None: ) ) - with Image(config) as image: - image.build(["--repart-directory", repartd, "--passphrase", passphrase, "--format=disk"]) + with Image(image_config.config) as image: + await image.build(["--repart-directory", repartd, "--passphrase", pp, "--format=disk"]) # repart's default LUKS2 KDF (Argon2id) is memory-hard and needs ~1 GiB of RAM just to derive # the key, so the default VM RAM isn't enough to unlock the root. - image.vm(["--credential=cryptsetup.passphrase=mkosi"], ram="2G") + await image.vm(["--credential=cryptsetup.passphrase=mkosi"], ram="2G") + +async def test_initrd_luks_lvm(image_config: ImageConfigManager) -> None: + if os.getuid() != 0: + Assert.skip("mkosi-initrd LUKS+LVM test can only be executed as root") -@pytest.mark.skipif(os.getuid() != 0, reason="mkosi-initrd LUKS+LVM test can only be executed as root") -def test_initrd_luks_lvm(config: ImageConfig, passphrase: Path) -> None: - with Image(config) as image, contextlib.ExitStack() as stack: - image.build(["--format=disk"]) + with passphrase() as pp, Image(image_config.config) as image, contextlib.ExitStack() as stack: + await image.build(["--format=disk"]) lvm = Path(image.output_dir) / "lvm.raw" lvm.touch() @@ -168,11 +180,14 @@ def test_initrd_luks_lvm(config: ImageConfig, passphrase: Path) -> None: ["losetup", "--show", "--find", "--partscan", lvm], stdout=subprocess.PIPE ).stdout.strip() stack.callback(run, ["losetup", "--detach", lodev]) - run(["sfdisk", "--label", "gpt", lodev], input="type=E6D6D379-F507-44C2-A23C-238F2A3DF928 bootable") + run( + ["sfdisk", "--label", "gpt", lodev], + input="type=E6D6D379-F507-44C2-A23C-238F2A3DF928 bootable", + ) run( [ "cryptsetup", - "--key-file", passphrase, + "--key-file", pp, "--use-random", "--pbkdf", "pbkdf2", "--pbkdf-force-iterations", "1000", @@ -180,7 +195,7 @@ def test_initrd_luks_lvm(config: ImageConfig, passphrase: Path) -> None: f"{lodev}p1", ] ) # fmt: skip - run(["cryptsetup", "--key-file", passphrase, "luksOpen", f"{lodev}p1", "lvm_root"]) + run(["cryptsetup", "--key-file", pp, "luksOpen", f"{lodev}p1", "lvm_root"]) stack.callback(run, ["cryptsetup", "close", "lvm_root"]) luks_uuid = run(["cryptsetup", "luksUUID", f"{lodev}p1"], stdout=subprocess.PIPE).stdout.strip() run(["lvm", "pvcreate", "--devicesfile", "", "/dev/mapper/lvm_root"]) @@ -192,7 +207,14 @@ def test_initrd_luks_lvm(config: ImageConfig, passphrase: Path) -> None: run(["lvm", "lvcreate", "--devicesfile", "", "-An", "-l", "100%FREE", "-n", "lv0", "vg_mkosi"]) run(["lvm", "lvs", "--devicesfile", ""]) run(["udevadm", "wait", "--timeout=30", "/dev/vg_mkosi/lv0"]) - run([f"mkfs.{image.config.distribution.installer.filesystem()}", "-L", "root", "/dev/vg_mkosi/lv0"]) + run( + [ + f"mkfs.{image.config.distribution.installer.filesystem()}", + "-L", + "root", + "/dev/vg_mkosi/lv0", + ] + ) src = Path(stack.enter_context(tempfile.TemporaryDirectory())) run(["systemd-dissect", "--mount", "--mkdir", Path(image.output_dir) / "image.raw", src]) @@ -207,7 +229,7 @@ def test_initrd_luks_lvm(config: ImageConfig, passphrase: Path) -> None: lvm.rename(Path(image.output_dir) / "image.raw") - image.vm( + await image.vm( [ "--format=disk", "--credential=cryptsetup.passphrase=mkosi", @@ -218,12 +240,12 @@ def test_initrd_luks_lvm(config: ImageConfig, passphrase: Path) -> None: ) -def test_initrd_size(config: ImageConfig) -> None: - with Image(config) as image: - image.build() +async def test_initrd_size(image_config: ImageConfigManager) -> None: + with Image(image_config.config) as image: + await image.build() # Set a reasonably high limit to avoid having to bump it every single time by # small amounts. 100M should do. maxsize = 1024**2 * 100 - assert (Path(image.output_dir) / "image.initrd").stat().st_size <= maxsize + Assert.le((Path(image.output_dir) / "image.initrd").stat().st_size, maxsize) diff --git a/tests/test_signing.py b/tests/integration_signing.py similarity index 75% rename from tests/test_signing.py rename to tests/integration_signing.py index 2ded84191a..ccabac7273 100644 --- a/tests/test_signing.py +++ b/tests/integration_signing.py @@ -4,20 +4,18 @@ import tempfile from pathlib import Path -import pytest +import barrage.assertions as Assert from mkosi.run import find_binary, run -from . import Image, ImageConfig +from . import Image, ImageConfigManager -pytestmark = pytest.mark.integration - -def test_signing_checksums_with_sop(config: ImageConfig) -> None: +async def test_signing_checksums_with_sop(image_config: ImageConfigManager) -> None: if find_binary("sqop") is None: - pytest.skip("Need 'sqop' binary to perform sop tests.") + Assert.skip("Need 'sqop' binary to perform sop tests.") - with tempfile.TemporaryDirectory() as path, Image(config) as image: + with tempfile.TemporaryDirectory() as path, Image(image_config.config) as image: tmp_path = Path(path) signing_key = tmp_path / "signing-key.pgp" @@ -31,7 +29,7 @@ def test_signing_checksums_with_sop(config: ImageConfig) -> None: with open(signing_key, "rb") as i, open(signing_cert, "wb") as o: run(cmdline=["sqop", "extract-cert"], stdin=i, stdout=o) - image.build( + await image.build( options=["--checksum=true", "--openpgp-tool=sqop", "--sign=true", f"--key={signing_key}"] ) @@ -42,8 +40,8 @@ def test_signing_checksums_with_sop(config: ImageConfig) -> None: run(cmdline=["sqop", "verify", signature, signing_cert], stdin=i) -def test_signing_checksums_with_gpg(config: ImageConfig) -> None: - with tempfile.TemporaryDirectory() as path, Image(config) as image: +async def test_signing_checksums_with_gpg(image_config: ImageConfigManager) -> None: + with tempfile.TemporaryDirectory() as path, Image(image_config.config) as image: tmp_path = Path(path) signing_key = "mkosi-test@example.org" @@ -66,7 +64,7 @@ def test_signing_checksums_with_gpg(config: ImageConfig) -> None: stdout=o, ) - image.build(options=["--checksum=true", "--sign=true", f"--key={signing_key}"], env=env) + await image.build(options=["--checksum=true", "--sign=true", f"--key={signing_key}"], env=env) signed_file = image.output_dir / "image.SHA256SUMS" signature = image.output_dir / "image.SHA256SUMS.gpg" diff --git a/tests/test_boot.py b/tests/test_boot.py deleted file mode 100644 index 2b5a331ef2..0000000000 --- a/tests/test_boot.py +++ /dev/null @@ -1,69 +0,0 @@ -# SPDX-License-Identifier: LGPL-2.1-or-later - -import os -import subprocess - -import pytest - -from mkosi.config import Bootloader, Firmware, OutputFormat -from mkosi.distribution import Distribution -from mkosi.run import find_binary, run -from mkosi.versioncomp import GenericVersion - -from . import Image, ImageConfig - -pytestmark = pytest.mark.integration - - -def have_vmspawn() -> bool: - return find_binary("systemd-vmspawn") is not None and ( - GenericVersion(run(["systemd-vmspawn", "--version"], stdout=subprocess.PIPE).stdout.strip()) >= 256 - ) - - -@pytest.mark.parametrize("format", [f for f in OutputFormat if not f.is_extension_image()]) -def test_format(config: ImageConfig, format: OutputFormat) -> None: - with Image(config) as image: - if image.config.distribution == Distribution.rhel_ubi and format in ( - OutputFormat.esp, - OutputFormat.uki, - ): - pytest.skip("Cannot build RHEL-UBI images with format 'esp' or 'uki'") - - image.build(options=["--format", str(format)]) - - # FIXME: Also boot directory images when the CI runs systemd v260 or newer. - if format == OutputFormat.directory: - return - - if format == OutputFormat.disk and os.getuid() == 0: - image.boot() - - if format in (OutputFormat.cpio, OutputFormat.uki, OutputFormat.esp): - pytest.skip("Default image is too large to be able to boot in CPIO/UKI/ESP format") - - if image.config.distribution == Distribution.rhel_ubi: - return - - if format in (OutputFormat.tar, OutputFormat.oci, OutputFormat.none, OutputFormat.portable): - return - - image.vm() - - if have_vmspawn() and format == OutputFormat.disk: - image.vm(options=["--vmm=vmspawn"]) - - if format != OutputFormat.disk: - return - - -@pytest.mark.parametrize("bootloader", Bootloader) -def test_bootloader(config: ImageConfig, bootloader: Bootloader) -> None: - if config.distribution == Distribution.rhel_ubi or bootloader.is_signed(): - return - - firmware = Firmware.linux if bootloader == Bootloader.none else Firmware.auto - - with Image(config) as image: - image.build(["--format=disk", "--bootloader", str(bootloader)]) - image.vm(["--firmware", str(firmware)]) diff --git a/tests/test_config.py b/tests/test_config.py index bdb5774332..0b00ae87fc 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,13 +1,16 @@ # SPDX-License-Identifier: LGPL-2.1-or-later import argparse +import contextlib import itertools import logging import operator import os +import tempfile from pathlib import Path +from typing import cast -import pytest +import barrage.assertions as Assert import mkosi.resources from mkosi import expand_kernel_specifiers @@ -29,39 +32,53 @@ from mkosi.util import chdir, resource_path -def test_compression_enum_creation() -> None: - assert Compression["none"] == Compression.none - assert Compression["zstd"] == Compression.zstd - assert Compression["zst"] == Compression.zstd - assert Compression["xz"] == Compression.xz - assert Compression["bz2"] == Compression.bz2 - assert Compression["gz"] == Compression.gz - assert Compression["lz4"] == Compression.lz4 - assert Compression["lzma"] == Compression.lzma +class _ListHandler(logging.Handler): + def __init__(self) -> None: + super().__init__() + self.records: list[logging.LogRecord] = [] + def emit(self, record: logging.LogRecord) -> None: + self.records.append(record) -def test_compression_enum_bool() -> None: - assert not bool(Compression.none) - assert bool(Compression.zstd) - assert bool(Compression.xz) - assert bool(Compression.bz2) - assert bool(Compression.gz) - assert bool(Compression.lz4) - assert bool(Compression.lzma) +def tmp_dir(stack: contextlib.AsyncExitStack) -> Path: + return Path(stack.enter_context(tempfile.TemporaryDirectory())) -def test_compression_enum_str() -> None: - assert str(Compression.none) == "none" - assert str(Compression.zstd) == "zstd" - assert str(Compression.zst) == "zstd" - assert str(Compression.xz) == "xz" - assert str(Compression.bz2) == "bz2" - assert str(Compression.gz) == "gz" - assert str(Compression.lz4) == "lz4" - assert str(Compression.lzma) == "lzma" +async def test_compression_enum_creation() -> None: + Assert.eq(Compression["none"], Compression.none) + Assert.eq(Compression["zstd"], Compression.zstd) + Assert.eq(Compression["zst"], Compression.zstd) + Assert.eq(Compression["xz"], Compression.xz) + Assert.eq(Compression["bz2"], Compression.bz2) + Assert.eq(Compression["gz"], Compression.gz) + Assert.eq(Compression["lz4"], Compression.lz4) + Assert.eq(Compression["lzma"], Compression.lzma) -def test_parse_ini(tmp_path: Path) -> None: + +async def test_compression_enum_bool() -> None: + Assert.false(bool(Compression.none)) + Assert.true(bool(Compression.zstd)) + Assert.true(bool(Compression.xz)) + Assert.true(bool(Compression.bz2)) + Assert.true(bool(Compression.gz)) + Assert.true(bool(Compression.lz4)) + Assert.true(bool(Compression.lzma)) + + +async def test_compression_enum_str() -> None: + Assert.eq(str(Compression.none), "none") + Assert.eq(str(Compression.zstd), "zstd") + Assert.eq(str(Compression.zst), "zstd") + Assert.eq(str(Compression.xz), "xz") + Assert.eq(str(Compression.bz2), "bz2") + Assert.eq(str(Compression.gz), "gz") + Assert.eq(str(Compression.lz4), "lz4") + Assert.eq(str(Compression.lzma), "lzma") + + +async def test_parse_ini(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) p = tmp_path / "ini" p.write_text( """\ @@ -83,16 +100,17 @@ def test_parse_ini(tmp_path: Path) -> None: g = parse_ini(p) - assert next(g) == ("MySection", "Value", "abc") - assert next(g) == ("MySection", "Other", "def") - assert next(g) == ("MySection", "ALLCAPS", "txt") - assert next(g) == ("MySection", "", "") - assert next(g) == ("EmptySection", "", "") - assert next(g) == ("AnotherSection", "EmptyValue", "") - assert next(g) == ("AnotherSection", "Multiline", "abc\ndef\nqed\nord") + Assert.eq(next(g), ("MySection", "Value", "abc")) + Assert.eq(next(g), ("MySection", "Other", "def")) + Assert.eq(next(g), ("MySection", "ALLCAPS", "txt")) + Assert.eq(next(g), ("MySection", "", "")) + Assert.eq(next(g), ("EmptySection", "", "")) + Assert.eq(next(g), ("AnotherSection", "EmptyValue", "")) + Assert.eq(next(g), ("AnotherSection", "Multiline", "abc\ndef\nqed\nord")) -def test_parse_config(tmp_path: Path) -> None: +async def test_parse_config(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path (d / "mkosi.conf").write_text( @@ -120,11 +138,11 @@ def test_parse_config(tmp_path: Path) -> None: with chdir(d): _, _, [config] = parse_config() - assert config.distribution == Distribution.ubuntu - assert config.architecture == Architecture.arm64 - assert config.profiles == ["abc"] - assert config.output_format == OutputFormat.cpio - assert config.image_id == "base" + Assert.eq(config.distribution, Distribution.ubuntu) + Assert.eq(config.architecture, Architecture.arm64) + Assert.eq(config.profiles, ["abc"]) + Assert.eq(config.output_format, OutputFormat.cpio) + Assert.eq(config.image_id, "base") with chdir(d): _, _, [config] = parse_config( @@ -137,10 +155,10 @@ def test_parse_config(tmp_path: Path) -> None: ) # fmt: skip # Values from the CLI should take priority. - assert config.distribution == Distribution.fedora - assert config.environment["MY_KEY"] == "CLI_VALUE" - assert any(c.name == "my.cred" and c.value == "cli.value" for c in config.credentials) - assert config.repositories == ["epel", "epel-next", "universe"] + Assert.eq(config.distribution, Distribution.fedora) + Assert.eq(config.environment["MY_KEY"], "CLI_VALUE") + Assert.true(any(c.name == "my.cred" and c.value == "cli.value" for c in config.credentials)) + Assert.eq(config.repositories, ["epel", "epel-next", "universe"]) with chdir(d): _, _, [config] = parse_config( @@ -152,11 +170,11 @@ def test_parse_config(tmp_path: Path) -> None: ] ) # fmt: skip - # Empty values on the CLIs resets non-collection based settings to their defaults and collection based - # settings to empty collections. - assert "MY_KEY" not in config.environment - assert not any(c.name == "my.cred" for c in config.credentials) - assert config.repositories == [] + # Empty values on the CLIs resets non-collection based settings to their defaults and collection + # based settings to empty collections. + Assert.not_in("MY_KEY", config.environment) + Assert.false(any(c.name == "my.cred" for c in config.credentials)) + Assert.eq(config.repositories, []) (d / "mkosi.conf.d").mkdir() (d / "mkosi.conf.d/d1.conf").write_text( @@ -179,15 +197,15 @@ def test_parse_config(tmp_path: Path) -> None: _, _, [config] = parse_config(["--profile", "last"]) # Setting a value explicitly in a dropin should override the default from mkosi.conf. - assert config.distribution == Distribution.debian + Assert.eq(config.distribution, Distribution.debian) # Lists should be merged by appending the new values to the existing values. Any values from the CLI # should be appended to the values from the configuration files. - assert config.profiles == ["abc", "qed", "def", "last"] - assert config.output_format == OutputFormat.cpio - assert config.image_id == "00-dropin" - assert config.image_version == "0" + Assert.eq(config.profiles, ["abc", "qed", "def", "last"]) + Assert.eq(config.output_format, OutputFormat.cpio) + Assert.eq(config.image_id, "00-dropin") + Assert.eq(config.image_version, "0") # '@' specifier should be automatically dropped. - assert config.output == "abc" + Assert.eq(config.output, "abc") (d / "mkosi.version").write_text("1.2.3") @@ -205,10 +223,10 @@ def test_parse_config(tmp_path: Path) -> None: _, _, [config] = parse_config() # Test that empty assignment resets settings. - assert config.packages == [] - assert config.image_id is None + Assert.eq(config.packages, []) + Assert.none(config.image_id) # mkosi.version should only be used if no version is set explicitly. - assert config.image_version == "0" + Assert.eq(config.image_version, "0") (d / "mkosi.conf.d/d1.conf").unlink() @@ -216,7 +234,7 @@ def test_parse_config(tmp_path: Path) -> None: _, _, [config] = parse_config() # ImageVersion= is not set explicitly anymore, so now the version from mkosi.version should be used. - assert config.image_version == "1.2.3" + Assert.eq(config.image_version, "1.2.3") (d / "abc").mkdir() (d / "abc/mkosi.conf").write_text( @@ -238,20 +256,20 @@ def test_parse_config(tmp_path: Path) -> None: with chdir(d): _, _, [config] = parse_config() - assert not config.cxl - assert config.split_artifacts == ArtifactOutput.compat_no() + Assert.false(config.cxl) + Assert.eq(config.split_artifacts, ArtifactOutput.compat_no()) # Passing the directory should include both the main config file and the dropin. _, _, [config] = parse_config(["--include", os.fspath(d / "abc")] * 2) - assert config.cxl - assert config.split_artifacts == ArtifactOutput.compat_yes() + Assert.true(config.cxl) + Assert.eq(config.split_artifacts, ArtifactOutput.compat_yes()) # The same extra config should not be parsed more than once. - assert config.build_packages == ["abc"] + Assert.eq(config.build_packages, ["abc"]) # Passing the main config file should not include the dropin. _, _, [config] = parse_config(["--include", os.fspath(d / "abc/mkosi.conf")]) - assert config.cxl - assert config.split_artifacts == ArtifactOutput.compat_no() + Assert.true(config.cxl) + Assert.eq(config.split_artifacts, ArtifactOutput.compat_no()) (d / "mkosi.images").mkdir() @@ -280,40 +298,41 @@ def test_parse_config(tmp_path: Path) -> None: ) # Universal settings should always come from the main image. - assert one.distribution == config.distribution - assert two.distribution == config.distribution - assert one.release == config.release - assert two.release == config.release + Assert.eq(one.distribution, config.distribution) + Assert.eq(two.distribution, config.distribution) + Assert.eq(one.release, config.release) + Assert.eq(two.release, config.release) # Non-universal settings should not be passed to the subimages. - assert one.packages == ["one"] - assert two.packages == ["two"] - assert one.build_packages == [] - assert two.build_packages == [] + Assert.eq(one.packages, ["one"]) + Assert.eq(two.packages, ["two"]) + Assert.eq(one.build_packages, []) + Assert.eq(two.build_packages, []) # But should apply to the main image of course. - assert config.packages == ["qed"] - assert config.build_packages == ["def"] + Assert.eq(config.packages, ["qed"]) + Assert.eq(config.build_packages, ["def"]) # Inherited settings should be passed down to subimages but overridable by subimages. - assert one.image_version == "1.2.3" - assert two.image_version == "4.5.6" + Assert.eq(one.image_version, "1.2.3") + Assert.eq(two.image_version, "4.5.6") # Default values from subimages for universal settings should not be picked up. - assert len(one.sandbox_trees) == 0 - assert len(two.sandbox_trees) == 0 + Assert.eq(len(one.sandbox_trees), 0) + Assert.eq(len(two.sandbox_trees), 0) with chdir(d): _, _, [one, two, config] = parse_config(["--image-version", "7.8.9"]) # Inherited settings specified on the CLI should not override subimages that configure the setting # explicitly. - assert config.image_version == "7.8.9" - assert one.image_version == "7.8.9" - assert two.image_version == "4.5.6" + Assert.eq(config.image_version, "7.8.9") + Assert.eq(one.image_version, "7.8.9") + Assert.eq(two.image_version, "4.5.6") -def test_parse_includes_once(tmp_path: Path) -> None: +async def test_parse_includes_once(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path (d / "mkosi.conf").write_text( @@ -331,7 +350,7 @@ def test_parse_includes_once(tmp_path: Path) -> None: with chdir(d): _, _, [config] = parse_config(["--include", "abc.conf", "--include", "abc.conf"]) - assert config.build_packages == ["abc", "def"] + Assert.eq(config.build_packages, ["abc", "def"]) (d / "mkosi.images").mkdir() @@ -345,11 +364,12 @@ def test_parse_includes_once(tmp_path: Path) -> None: with chdir(d): _, _, [one, two, config] = parse_config([]) - assert one.build_packages == ["def"] - assert two.build_packages == ["def"] + Assert.eq(one.build_packages, ["def"]) + Assert.eq(two.build_packages, ["def"]) -def test_profiles(tmp_path: Path) -> None: +async def test_profiles(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path (d / "mkosi.profiles").mkdir() @@ -381,20 +401,20 @@ def test_profiles(tmp_path: Path) -> None: with chdir(d): _, _, [config] = parse_config() - assert config.profiles == ["profile"] + Assert.eq(config.profiles, ["profile"]) # The profile should override mkosi.conf.d/ - assert config.distribution == Distribution.fedora - assert config.kvm == ConfigFeature.enabled + Assert.eq(config.distribution, Distribution.fedora) + Assert.eq(config.kvm, ConfigFeature.enabled) (d / "mkosi.conf").unlink() with chdir(d): _, _, [config] = parse_config(["--profile", "profile"]) - assert config.profiles == ["profile"] + Assert.eq(config.profiles, ["profile"]) # The profile should override mkosi.conf.d/ - assert config.distribution == Distribution.fedora - assert config.kvm == ConfigFeature.enabled + Assert.eq(config.distribution, Distribution.fedora) + Assert.eq(config.kvm, ConfigFeature.enabled) (d / "mkosi.conf").write_text( """\ @@ -416,8 +436,8 @@ def test_profiles(tmp_path: Path) -> None: with chdir(d): _, _, [config] = parse_config() - assert config.profiles == ["profile", "abc"] - assert config.distribution == Distribution.opensuse + Assert.eq(config.profiles, ["profile", "abc"]) + Assert.eq(config.distribution, Distribution.opensuse) # Check that mkosi.profiles/ is parsed in subimages as well. (d / "mkosi.images/subimage/mkosi.profiles").mkdir(parents=True) @@ -431,10 +451,11 @@ def test_profiles(tmp_path: Path) -> None: with chdir(d): _, _, [subimage, config] = parse_config() - assert subimage.environment["Image"] == "subimage" + Assert.eq(subimage.environment["Image"], "subimage") -def test_override_default(tmp_path: Path) -> None: +async def test_override_default(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path (d / "mkosi.conf").write_text( @@ -448,8 +469,8 @@ def test_override_default(tmp_path: Path) -> None: with chdir(d): _, _, [config] = parse_config(["--tools-tree", "", "--environment", ""]) - assert config.tools_tree is None - assert "MY_KEY" not in config.environment + Assert.none(config.tools_tree) + Assert.not_in("MY_KEY", config.environment) (d / "mkosi.tools.conf").touch() @@ -463,10 +484,11 @@ def test_override_default(tmp_path: Path) -> None: with chdir(d): _, _, [config] = parse_config([]) - assert config.tools_tree is None + Assert.none(config.tools_tree) -def test_local_config(tmp_path: Path) -> None: +async def test_local_config(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path (d / "mkosi.local.conf").write_text( @@ -484,7 +506,7 @@ def test_local_config(tmp_path: Path) -> None: with chdir(d): _, _, [config] = parse_config() - assert config.distribution == Distribution.debian + Assert.eq(config.distribution, Distribution.debian) (d / "mkosi.conf").write_text( """\ @@ -502,14 +524,14 @@ def test_local_config(tmp_path: Path) -> None: _, _, [config] = parse_config() # Local config should take precedence over non-local config. - assert config.distribution == Distribution.debian - assert config.with_tests + Assert.eq(config.distribution, Distribution.debian) + Assert.true(config.with_tests) with chdir(d): _, _, [config] = parse_config(["--distribution", "fedora", "-T"]) - assert config.distribution == Distribution.fedora - assert not config.with_tests + Assert.eq(config.distribution, Distribution.fedora) + Assert.false(config.with_tests) (d / "mkosi.local/mkosi.conf.d").mkdir(parents=True) (d / "mkosi.local/mkosi.conf.d/10-test.conf").write_text( @@ -523,45 +545,48 @@ def test_local_config(tmp_path: Path) -> None: with chdir(d): _, _, [config] = parse_config() - assert config.environment == {"FOO": "override", "BAR": "override", "BAZ": "override"} + Assert.eq(config.environment, {"FOO": "override", "BAR": "override", "BAZ": "override"}) -def test_parse_load_verb(tmp_path: Path) -> None: +async def test_parse_load_verb(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) with chdir(tmp_path): - assert parse_config(["build"])[0].verb == Verb.build - assert parse_config(["clean"])[0].verb == Verb.clean - assert parse_config(["genkey"])[0].verb == Verb.genkey - assert parse_config(["bump"])[0].verb == Verb.bump - assert parse_config(["serve"])[0].verb == Verb.serve - assert parse_config(["build"])[0].verb == Verb.build - assert parse_config(["shell"])[0].verb == Verb.shell - assert parse_config(["boot"])[0].verb == Verb.boot - assert parse_config(["qemu"])[0].verb == Verb.qemu - assert parse_config(["vm"])[0].verb == Verb.vm - assert parse_config(["journalctl"])[0].verb == Verb.journalctl - assert parse_config(["coredumpctl"])[0].verb == Verb.coredumpctl - with pytest.raises(SystemExit): + Assert.eq(parse_config(["build"])[0].verb, Verb.build) + Assert.eq(parse_config(["clean"])[0].verb, Verb.clean) + Assert.eq(parse_config(["genkey"])[0].verb, Verb.genkey) + Assert.eq(parse_config(["bump"])[0].verb, Verb.bump) + Assert.eq(parse_config(["serve"])[0].verb, Verb.serve) + Assert.eq(parse_config(["build"])[0].verb, Verb.build) + Assert.eq(parse_config(["shell"])[0].verb, Verb.shell) + Assert.eq(parse_config(["boot"])[0].verb, Verb.boot) + Assert.eq(parse_config(["qemu"])[0].verb, Verb.qemu) + Assert.eq(parse_config(["vm"])[0].verb, Verb.vm) + Assert.eq(parse_config(["journalctl"])[0].verb, Verb.journalctl) + Assert.eq(parse_config(["coredumpctl"])[0].verb, Verb.coredumpctl) + with Assert.raises(SystemExit): parse_config(["invalid"]) -def test_os_distribution(tmp_path: Path) -> None: +async def test_os_distribution(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) with chdir(tmp_path): for dist in Distribution: _, _, [config] = parse_config(["-d", dist.value]) - assert config.distribution == dist + Assert.eq(config.distribution, dist) - with pytest.raises(tuple((argparse.ArgumentError, SystemExit))): + with Assert.raises((argparse.ArgumentError, SystemExit)): # type: ignore[arg-type] parse_config(["-d", "invalidDistro"]) - with pytest.raises(tuple((argparse.ArgumentError, SystemExit))): + with Assert.raises((argparse.ArgumentError, SystemExit)): # type: ignore[arg-type] parse_config(["-d"]) for dist in Distribution: Path("mkosi.conf").write_text(f"[Distribution]\nDistribution={dist}") _, _, [config] = parse_config() - assert config.distribution == dist + Assert.eq(config.distribution, dist) -def test_parse_config_files_filter(tmp_path: Path) -> None: +async def test_parse_config_files_filter(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) with chdir(tmp_path): confd = Path("mkosi.conf.d") confd.mkdir() @@ -570,16 +595,18 @@ def test_parse_config_files_filter(tmp_path: Path) -> None: (confd / "20-file.noconf").write_text("[Content]\nPackages=nope") _, _, [config] = parse_config() - assert config.packages == ["yes"] + Assert.eq(config.packages, ["yes"]) -def test_compression(tmp_path: Path) -> None: +async def test_compression(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) with chdir(tmp_path): _, _, [config] = parse_config(["--format", "disk", "--compress-output", "False"]) - assert config.compress_output == Compression.none + Assert.eq(config.compress_output, Compression.none) -def test_match_only(tmp_path: Path) -> None: +async def test_match_only(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) with chdir(tmp_path): Path("mkosi.conf").write_text( """\ @@ -598,10 +625,11 @@ def test_match_only(tmp_path: Path) -> None: ) _, _, [config] = parse_config(["--format", "tar"]) - assert config.image_id != "abcde" + Assert.ne(config.image_id, "abcde") -def test_match_multiple(tmp_path: Path) -> None: +async def test_match_multiple(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) with chdir(tmp_path): Path("mkosi.conf").write_text( """\ @@ -620,15 +648,15 @@ def test_match_multiple(tmp_path: Path) -> None: # Both sections are not matched, so image ID should not be "abcde". _, _, [config] = parse_config(["--format", "tar", "--architecture", "s390x"]) - assert config.image_id != "abcde" + Assert.ne(config.image_id, "abcde") # Only a single section is matched, so image ID should not be "abcde". _, _, [config] = parse_config(["--format", "disk", "--architecture", "s390x"]) - assert config.image_id != "abcde" + Assert.ne(config.image_id, "abcde") # Both sections are matched, so image ID should be "abcde". _, _, [config] = parse_config(["--format", "disk", "--architecture", "x86-64"]) - assert config.image_id == "abcde" + Assert.eq(config.image_id, "abcde") Path("mkosi.conf").write_text( """\ @@ -647,19 +675,19 @@ def test_match_multiple(tmp_path: Path) -> None: # Both sections are not matched, so image ID should not be "abcde". _, _, [config] = parse_config(["--format", "tar", "--architecture", "s390x"]) - assert config.image_id != "abcde" + Assert.ne(config.image_id, "abcde") # The first section is matched, so image ID should be "abcde". _, _, [config] = parse_config(["--format", "disk", "--architecture", "x86-64"]) - assert config.image_id == "abcde" + Assert.eq(config.image_id, "abcde") # The second section is matched, so image ID should be "abcde". _, _, [config] = parse_config(["--format", "directory", "--architecture", "arm64"]) - assert config.image_id == "abcde" + Assert.eq(config.image_id, "abcde") # Parts of all section are matched, but none is matched fully, so image ID should not be "abcde". _, _, [config] = parse_config(["--format", "disk", "--architecture", "arm64"]) - assert config.image_id != "abcde" + Assert.ne(config.image_id, "abcde") Path("mkosi.conf").write_text( """\ @@ -678,7 +706,7 @@ def test_match_multiple(tmp_path: Path) -> None: # The first section is matched, so image ID should be "abcde". _, _, [config] = parse_config(["--format", "disk"]) - assert config.image_id == "abcde" + Assert.eq(config.image_id, "abcde") Path("mkosi.conf").write_text( """\ @@ -698,7 +726,7 @@ def test_match_multiple(tmp_path: Path) -> None: # No sections are matched, so image ID should be not "abcde". _, _, [config] = parse_config(["--format", "disk", "--architecture=arm64"]) - assert config.image_id != "abcde" + Assert.ne(config.image_id, "abcde") # Mixing both [Match] and [TriggerMatch] Path("mkosi.conf").write_text( @@ -719,18 +747,19 @@ def test_match_multiple(tmp_path: Path) -> None: # Match and first TriggerMatch sections match _, _, [config] = parse_config(["--format", "disk", "--architecture=arm64"]) - assert config.image_id == "abcde" + Assert.eq(config.image_id, "abcde") # Match section matches, but no TriggerMatch section matches _, _, [config] = parse_config(["--format", "disk", "--architecture=s390x"]) - assert config.image_id != "abcde" + Assert.ne(config.image_id, "abcde") # Second TriggerMatch section matches, but the Match section does not _, _, [config] = parse_config(["--format", "tar", "--architecture=x86-64"]) - assert config.image_id != "abcde" + Assert.ne(config.image_id, "abcde") -def test_match_empty(tmp_path: Path) -> None: +async def test_match_empty(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) with chdir(tmp_path): Path("mkosi.conf").write_text( """\ @@ -744,126 +773,132 @@ def test_match_empty(tmp_path: Path) -> None: _, _, [config] = parse_config([]) - assert config.environment.get("ABC") == "QED" + Assert.eq(config.environment.get("ABC"), "QED") _, _, [config] = parse_config(["--profile", "profile"]) - assert config.environment.get("ABC") is None + Assert.none(config.environment.get("ABC")) -@pytest.mark.parametrize( - "dist1,dist2", - itertools.combinations_with_replacement([Distribution.debian, Distribution.opensuse], 2), -) -def test_match_distribution(tmp_path: Path, dist1: Distribution, dist2: Distribution) -> None: - with chdir(tmp_path): - parent = Path("mkosi.conf") - parent.write_text( - f"""\ - [Distribution] - Distribution={dist1} - """ - ) - - Path("mkosi.conf.d").mkdir() - - child1 = Path("mkosi.conf.d/child1.conf") - child1.write_text( - f"""\ - [Match] - Distribution={dist1} +async def test_match_distribution(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) + for dist1, dist2 in itertools.combinations_with_replacement( + [Distribution.debian, Distribution.opensuse], 2 + ): + with chdir(tmp_path): + parent = Path("mkosi.conf") + parent.write_text( + f"""\ + [Distribution] + Distribution={dist1} + """ + ) - [Content] - Packages=testpkg1 - """ - ) - child2 = Path("mkosi.conf.d/child2.conf") - child2.write_text( - f"""\ - [Match] - Distribution={dist2} + confd = Path("mkosi.conf.d") + if not confd.exists(): + confd.mkdir() - [Content] - Packages=testpkg2 - """ - ) - child3 = Path("mkosi.conf.d/child3.conf") - child3.write_text( - f"""\ - [Match] - Distribution=|{dist1} - Distribution=|{dist2} + child1 = Path("mkosi.conf.d/child1.conf") + child1.write_text( + f"""\ + [Match] + Distribution={dist1} - [Content] - Packages=testpkg3 - """ - ) + [Content] + Packages=testpkg1 + """ + ) + child2 = Path("mkosi.conf.d/child2.conf") + child2.write_text( + f"""\ + [Match] + Distribution={dist2} - _, _, [conf] = parse_config() - assert "testpkg1" in conf.packages - if dist1 == dist2: - assert "testpkg2" in conf.packages - else: - assert "testpkg2" not in conf.packages - assert "testpkg3" in conf.packages + [Content] + Packages=testpkg2 + """ + ) + child3 = Path("mkosi.conf.d/child3.conf") + child3.write_text( + f"""\ + [Match] + Distribution=|{dist1} + Distribution=|{dist2} + [Content] + Packages=testpkg3 + """ + ) -@pytest.mark.parametrize("release1,release2", itertools.combinations_with_replacement([36, 37], 2)) -def test_match_release(tmp_path: Path, release1: int, release2: int) -> None: - with chdir(tmp_path): - parent = Path("mkosi.conf") - parent.write_text( - f"""\ - [Distribution] - Distribution=fedora - Release={release1} - """ - ) + _, _, [conf] = parse_config() + Assert.in_("testpkg1", conf.packages) + if dist1 == dist2: + Assert.in_("testpkg2", conf.packages) + else: + Assert.not_in("testpkg2", conf.packages) + Assert.in_("testpkg3", conf.packages) + + +async def test_match_release(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) + for release1, release2 in itertools.combinations_with_replacement([36, 37], 2): + with chdir(tmp_path): + parent = Path("mkosi.conf") + parent.write_text( + f"""\ + [Distribution] + Distribution=fedora + Release={release1} + """ + ) - Path("mkosi.conf.d").mkdir() + confd = Path("mkosi.conf.d") + if not confd.exists(): + confd.mkdir() - child1 = Path("mkosi.conf.d/child1.conf") - child1.write_text( - f"""\ - [Match] - Release={release1} + child1 = Path("mkosi.conf.d/child1.conf") + child1.write_text( + f"""\ + [Match] + Release={release1} - [Content] - Packages=testpkg1 - """ - ) - child2 = Path("mkosi.conf.d/child2.conf") - child2.write_text( - f"""\ - [Match] - Release={release2} + [Content] + Packages=testpkg1 + """ + ) + child2 = Path("mkosi.conf.d/child2.conf") + child2.write_text( + f"""\ + [Match] + Release={release2} - [Content] - Packages=testpkg2 - """ - ) - child3 = Path("mkosi.conf.d/child3.conf") - child3.write_text( - f"""\ - [Match] - Release=|{release1} - Release=|{release2} + [Content] + Packages=testpkg2 + """ + ) + child3 = Path("mkosi.conf.d/child3.conf") + child3.write_text( + f"""\ + [Match] + Release=|{release1} + Release=|{release2} - [Content] - Packages=testpkg3 - """ - ) + [Content] + Packages=testpkg3 + """ + ) - _, _, [conf] = parse_config() - assert "testpkg1" in conf.packages - if release1 == release2: - assert "testpkg2" in conf.packages - else: - assert "testpkg2" not in conf.packages - assert "testpkg3" in conf.packages + _, _, [conf] = parse_config() + Assert.in_("testpkg1", conf.packages) + if release1 == release2: + Assert.in_("testpkg2", conf.packages) + else: + Assert.not_in("testpkg2", conf.packages) + Assert.in_("testpkg3", conf.packages) -def test_match_build_sources(tmp_path: Path) -> None: +async def test_match_build_sources(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path (d / "mkosi.conf").write_text( @@ -880,10 +915,11 @@ def test_match_build_sources(tmp_path: Path) -> None: with chdir(d): _, _, [config] = parse_config(["--build-sources", ".:kernel"]) - assert config.output == "abc" + Assert.eq(config.output, "abc") -def test_match_repositories(tmp_path: Path) -> None: +async def test_match_repositories(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path (d / "mkosi.conf").write_text( @@ -899,10 +935,11 @@ def test_match_repositories(tmp_path: Path) -> None: with chdir(d): _, _, [config] = parse_config(["--repositories", "epel,epel-next"]) - assert config.output == "qed" + Assert.eq(config.output, "qed") -def test_match_architecture(tmp_path: Path) -> None: +async def test_match_architecture(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path (d / "mkosi.conf").write_text( @@ -918,145 +955,149 @@ def test_match_architecture(tmp_path: Path) -> None: with chdir(d): _, _, [config] = parse_config(["--architecture", "arm64"]) - assert config.output == "qed" + Assert.eq(config.output, "qed") -@pytest.mark.parametrize("image1,image2", itertools.combinations_with_replacement(["image_a", "image_b"], 2)) -def test_match_imageid(tmp_path: Path, image1: str, image2: str) -> None: - with chdir(tmp_path): - parent = Path("mkosi.conf") - parent.write_text( - f"""\ - [Distribution] - Distribution=fedora - - [Output] - ImageId={image1} - """ - ) +async def test_match_imageid(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) + for image1, image2 in itertools.combinations_with_replacement(["image_a", "image_b"], 2): + with chdir(tmp_path): + parent = Path("mkosi.conf") + parent.write_text( + f"""\ + [Distribution] + Distribution=fedora - Path("mkosi.conf.d").mkdir() + [Output] + ImageId={image1} + """ + ) - child1 = Path("mkosi.conf.d/child1.conf") - child1.write_text( - f"""\ - [Match] - ImageId={image1} + confd = Path("mkosi.conf.d") + if not confd.exists(): + confd.mkdir() - [Content] - Packages=testpkg1 - """ - ) - child2 = Path("mkosi.conf.d/child2.conf") - child2.write_text( - f"""\ - [Match] - ImageId={image2} + child1 = Path("mkosi.conf.d/child1.conf") + child1.write_text( + f"""\ + [Match] + ImageId={image1} - [Content] - Packages=testpkg2 - """ - ) - child3 = Path("mkosi.conf.d/child3.conf") - child3.write_text( - f"""\ - [Match] - ImageId=|{image1} - ImageId=|{image2} + [Content] + Packages=testpkg1 + """ + ) + child2 = Path("mkosi.conf.d/child2.conf") + child2.write_text( + f"""\ + [Match] + ImageId={image2} - [Content] - Packages=testpkg3 - """ - ) - child4 = Path("mkosi.conf.d/child4.conf") - child4.write_text( - """\ - [Match] - ImageId=image* + [Content] + Packages=testpkg2 + """ + ) + child3 = Path("mkosi.conf.d/child3.conf") + child3.write_text( + f"""\ + [Match] + ImageId=|{image1} + ImageId=|{image2} - [Content] - Packages=testpkg4 - """ - ) + [Content] + Packages=testpkg3 + """ + ) + child4 = Path("mkosi.conf.d/child4.conf") + child4.write_text( + """\ + [Match] + ImageId=image* + + [Content] + Packages=testpkg4 + """ + ) - _, _, [conf] = parse_config() - assert "testpkg1" in conf.packages - if image1 == image2: - assert "testpkg2" in conf.packages - else: - assert "testpkg2" not in conf.packages - assert "testpkg3" in conf.packages - assert "testpkg4" in conf.packages + _, _, [conf] = parse_config() + Assert.in_("testpkg1", conf.packages) + if image1 == image2: + Assert.in_("testpkg2", conf.packages) + else: + Assert.not_in("testpkg2", conf.packages) + Assert.in_("testpkg3", conf.packages) + Assert.in_("testpkg4", conf.packages) -@pytest.mark.parametrize( - "op,version", - itertools.product( +async def test_match_imageversion(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) + for op, version in itertools.product( ["", "==", "<", ">", "<=", ">="], [122, 123], - ), -) -def test_match_imageversion(tmp_path: Path, op: str, version: str) -> None: - opfunc = { - "==": operator.eq, - "!=": operator.ne, - "<": operator.lt, - "<=": operator.le, - ">": operator.gt, - ">=": operator.ge, - }.get(op, operator.eq) - - with chdir(tmp_path): - parent = Path("mkosi.conf") - parent.write_text( - """\ - [Output] - ImageId=testimage - ImageVersion=123 - """ - ) + ): + opfunc = { + "==": operator.eq, + "!=": operator.ne, + "<": operator.lt, + "<=": operator.le, + ">": operator.gt, + ">=": operator.ge, + }.get(op, operator.eq) + + with chdir(tmp_path): + parent = Path("mkosi.conf") + parent.write_text( + """\ + [Output] + ImageId=testimage + ImageVersion=123 + """ + ) - Path("mkosi.conf.d").mkdir() - child1 = Path("mkosi.conf.d/child1.conf") - child1.write_text( - f"""\ - [Match] - ImageVersion={op}{version} + confd = Path("mkosi.conf.d") + if not confd.exists(): + confd.mkdir() + child1 = Path("mkosi.conf.d/child1.conf") + child1.write_text( + f"""\ + [Match] + ImageVersion={op}{version} - [Content] - Packages=testpkg1 - """ - ) - child2 = Path("mkosi.conf.d/child2.conf") - child2.write_text( - f"""\ - [Match] - ImageVersion=<200 - ImageVersion={op}{version} + [Content] + Packages=testpkg1 + """ + ) + child2 = Path("mkosi.conf.d/child2.conf") + child2.write_text( + f"""\ + [Match] + ImageVersion=<200 + ImageVersion={op}{version} - [Content] - Packages=testpkg2 - """ - ) - child3 = Path("mkosi.conf.d/child3.conf") - child3.write_text( - f"""\ - [Match] - ImageVersion=>9000 - ImageVersion={op}{version} + [Content] + Packages=testpkg2 + """ + ) + child3 = Path("mkosi.conf.d/child3.conf") + child3.write_text( + f"""\ + [Match] + ImageVersion=>9000 + ImageVersion={op}{version} - [Content] - Packages=testpkg3 - """ - ) + [Content] + Packages=testpkg3 + """ + ) - _, _, [conf] = parse_config() - assert ("testpkg1" in conf.packages) == opfunc(123, version) - assert ("testpkg2" in conf.packages) == opfunc(123, version) - assert "testpkg3" not in conf.packages + _, _, [conf] = parse_config() + Assert.eq(("testpkg1" in conf.packages), opfunc(123, version)) + Assert.eq(("testpkg2" in conf.packages), opfunc(123, version)) + Assert.not_in("testpkg3", conf.packages) -def test_match_environment(tmp_path: Path) -> None: +async def test_match_environment(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path (d / "mkosi.conf").write_text( @@ -1071,13 +1112,13 @@ def test_match_environment(tmp_path: Path) -> None: with chdir(d): _, _, [conf] = parse_config(["--environment", "MYENV=abc"]) - assert conf.image_id == "matched" + Assert.eq(conf.image_id, "matched") _, _, [conf] = parse_config(["--environment", "MYENV=bad"]) - assert conf.image_id != "matched" + Assert.ne(conf.image_id, "matched") _, _, [conf] = parse_config(["--environment", "MYEN=abc"]) - assert conf.image_id != "matched" + Assert.ne(conf.image_id, "matched") _, _, [conf] = parse_config(["--environment", "MYEN=bad"]) - assert conf.image_id != "matched" + Assert.ne(conf.image_id, "matched") (d / "mkosi.conf").write_text( """\ @@ -1091,29 +1132,34 @@ def test_match_environment(tmp_path: Path) -> None: with chdir(d): _, _, [conf] = parse_config(["--environment", "MYENV=abc"]) - assert conf.image_id == "matched" + Assert.eq(conf.image_id, "matched") _, _, [conf] = parse_config(["--environment", "MYENV=bad"]) - assert conf.image_id == "matched" + Assert.eq(conf.image_id, "matched") _, _, [conf] = parse_config(["--environment", "MYEN=abc"]) - assert conf.image_id != "matched" + Assert.ne(conf.image_id, "matched") -def test_paths_with_default_factory(tmp_path: Path) -> None: +async def test_paths_with_default_factory(stack: contextlib.AsyncExitStack) -> None: """ If both paths= and default_factory= are defined, default_factory= should not be used when at least one of the files/directories from paths= has been found. """ + tmp_path = tmp_dir(stack) with chdir(tmp_path): Path("mkosi.sandbox.tar").touch() _, _, [config] = parse_config() - assert config.sandbox_trees == [ - ConfigTree(Path.cwd() / "mkosi.sandbox.tar", None), - ] + Assert.eq( + config.sandbox_trees, + [ + ConfigTree(Path.cwd() / "mkosi.sandbox.tar", None), + ], + ) -def test_glob_expansion(tmp_path: Path) -> None: +async def test_glob_expansion(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path (d / "script_a.sh").touch() @@ -1132,7 +1178,7 @@ def test_glob_expansion(tmp_path: Path) -> None: with chdir(d): _, _, [config] = parse_config() - assert config.prepare_scripts == [d / "script_a.sh", d / "script_b.sh", d / "script_c.sh"] + Assert.eq(config.prepare_scripts, [d / "script_a.sh", d / "script_b.sh", d / "script_c.sh"]) # Glob patterns that match nothing should result in an empty list. (d / "mkosi.conf").write_text( @@ -1145,7 +1191,7 @@ def test_glob_expansion(tmp_path: Path) -> None: with chdir(d): _, _, [config] = parse_config() - assert config.prepare_scripts == [] + Assert.eq(config.prepare_scripts, []) # Non-glob paths should be ordered before glob results when listed first. (d / "mkosi.conf").write_text( @@ -1158,12 +1204,15 @@ def test_glob_expansion(tmp_path: Path) -> None: with chdir(d): _, _, [config] = parse_config() - assert config.prepare_scripts == [ - d / "other.py", - d / "script_a.sh", - d / "script_b.sh", - d / "script_c.sh", - ] + Assert.eq( + config.prepare_scripts, + [ + d / "other.py", + d / "script_a.sh", + d / "script_b.sh", + d / "script_c.sh", + ], + ) # Glob expansion should work with other script options too. (d / "mkosi.conf").write_text( @@ -1176,69 +1225,70 @@ def test_glob_expansion(tmp_path: Path) -> None: with chdir(d): _, _, [config] = parse_config() - assert config.build_scripts == [d / "script_a.sh", d / "script_b.sh", d / "script_c.sh"] + Assert.eq(config.build_scripts, [d / "script_a.sh", d / "script_b.sh", d / "script_c.sh"]) -@pytest.mark.parametrize( - "sections,args,warning_count", - [ +async def test_wrong_section_warning(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) + for sections, args, warning_count in [ (["Output"], [], 0), (["Content"], [], 1), (["Content", "Output"], [], 1), (["Output", "Content"], [], 1), (["Output", "Content", "Distribution"], [], 2), (["Content"], ["--image-id=testimage"], 1), - ], -) -def test_wrong_section_warning( - tmp_path: Path, - caplog: pytest.LogCaptureFixture, - sections: list[str], - args: list[str], - warning_count: int, -) -> None: - with chdir(tmp_path): - # Create a config with ImageId in the wrong section, - # and sometimes in the correct section - Path("mkosi.conf").write_text( - "\n".join( - f"""\ - [{section}] - ImageId=testimage - """ - for section in sections + ]: + with chdir(tmp_path): + # Create a config with ImageId in the wrong section, + # and sometimes in the correct section + Path("mkosi.conf").write_text( + "\n".join( + f"""\ + [{section}] + ImageId=testimage + """ + for section in sections + ) ) - ) - - with caplog.at_level(logging.WARNING): - # Parse the config, with --image-id sometimes given on the command line - parse_config(args) - - assert len(caplog.records) == warning_count - -def test_config_parse_bytes() -> None: - assert config_parse_bytes(None) is None - assert config_parse_bytes("1") == 4096 - assert config_parse_bytes("8000") == 8192 - assert config_parse_bytes("8K") == 8192 - assert config_parse_bytes("4097") == 8192 - assert config_parse_bytes("1M") == 1024**2 - assert config_parse_bytes("1.9M") == 1994752 - assert config_parse_bytes("1G") == 1024**3 - assert config_parse_bytes("7.3G") == 7838318592 - - with pytest.raises(SystemExit): + handler = _ListHandler() + logger = logging.getLogger() + prev_level = logger.level + logger.addHandler(handler) + logger.setLevel(logging.WARNING) + try: + # Parse the config, with --image-id sometimes given on the command line + parse_config(args) + finally: + logger.removeHandler(handler) + logger.setLevel(prev_level) + + Assert.eq(len(handler.records), warning_count) + + +async def test_config_parse_bytes() -> None: + Assert.none(config_parse_bytes(None)) + Assert.eq(config_parse_bytes("1"), 4096) + Assert.eq(config_parse_bytes("8000"), 8192) + Assert.eq(config_parse_bytes("8K"), 8192) + Assert.eq(config_parse_bytes("4097"), 8192) + Assert.eq(config_parse_bytes("1M"), 1024**2) + Assert.eq(config_parse_bytes("1.9M"), 1994752) + Assert.eq(config_parse_bytes("1G"), 1024**3) + Assert.eq(config_parse_bytes("7.3G"), 7838318592) + + with Assert.raises(SystemExit): config_parse_bytes("-1") - with pytest.raises(SystemExit): + with Assert.raises(SystemExit): config_parse_bytes("-2K") - with pytest.raises(SystemExit): + with Assert.raises(SystemExit): config_parse_bytes("-3M") - with pytest.raises(SystemExit): + with Assert.raises(SystemExit): config_parse_bytes("-4G") -def test_specifiers(tmp_path: Path) -> None: +async def test_specifiers(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path (d / "mkosi.conf").write_text( @@ -1321,12 +1371,12 @@ def test_specifiers(tmp_path: Path) -> None: "Filesystem": "ext4", } - assert {k: v for k, v in config.environment.items() if k in expected} == expected + Assert.eq({k: v for k, v in config.environment.items() if k in expected}, expected) - assert subimage.environment["Image"] == "subimage" + Assert.eq(subimage.environment["Image"], "subimage") -def test_kernel_specifiers(tmp_path: Path) -> None: +async def test_kernel_specifiers() -> None: kver = "13.0.8-5.10.0-1057-oem" # taken from reporter of #1638 token = "MySystemImage" roothash = "67e893261799236dcf20529115ba9fae4fd7c2269e1e658d42269503e5760d38" @@ -1339,16 +1389,17 @@ def test_expand_kernel_specifiers(text: str) -> str: roothash=roothash, ) - assert test_expand_kernel_specifiers("&&") == "&" - assert test_expand_kernel_specifiers("&k") == kver - assert test_expand_kernel_specifiers("&e") == token - assert test_expand_kernel_specifiers("&h") == roothash + Assert.eq(test_expand_kernel_specifiers("&&"), "&") + Assert.eq(test_expand_kernel_specifiers("&k"), kver) + Assert.eq(test_expand_kernel_specifiers("&e"), token) + Assert.eq(test_expand_kernel_specifiers("&h"), roothash) - assert test_expand_kernel_specifiers("Image_1.0.3") == "Image_1.0.3" - assert test_expand_kernel_specifiers("Image+&h-&k-&e") == f"Image+{roothash}-{kver}-{token}" + Assert.eq(test_expand_kernel_specifiers("Image_1.0.3"), "Image_1.0.3") + Assert.eq(test_expand_kernel_specifiers("Image+&h-&k-&e"), f"Image+{roothash}-{kver}-{token}") -def test_output_id_version(tmp_path: Path) -> None: +async def test_output_id_version(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path (d / "mkosi.conf").write_text( @@ -1362,14 +1413,15 @@ def test_output_id_version(tmp_path: Path) -> None: with chdir(d): _, _, [config] = parse_config() - assert config.output == "output_1.2.3" + Assert.eq(config.output, "output_1.2.3") -def test_deterministic() -> None: - assert Config.default() == Config.default() +async def test_deterministic() -> None: + Assert.eq(Config.default(), Config.default()) -def test_environment(tmp_path: Path) -> None: +async def test_environment(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path (d / "mkosi.conf").write_text( @@ -1413,15 +1465,16 @@ def test_environment(tmp_path: Path) -> None: } # Only check values for keys from expected, as config.environment contains other items as well - assert {k: config.finalize_environment()[k] for k in expected.keys()} == expected + Assert.eq({k: config.finalize_environment()[k] for k in expected.keys()}, expected) - assert config.environment_files == [Path.cwd() / "mkosi.env", Path.cwd() / "other.env"] + Assert.eq(config.environment_files, [Path.cwd() / "mkosi.env", Path.cwd() / "other.env"]) - assert sub.environment["PassThisEnv"] == "abc" - assert "TestValue2" not in sub.environment + Assert.eq(sub.environment["PassThisEnv"], "abc") + Assert.not_in("TestValue2", sub.environment) -def test_proxy(tmp_path: Path) -> None: +async def test_proxy(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path # Verify environment variables are set correctly when GIT_CONFIG_COUNT is not set @@ -1444,7 +1497,7 @@ def test_proxy(tmp_path: Path) -> None: } # Only check values for keys from expected, as config.environment contains other items as well - assert {k: config.finalize_environment()[k] for k in expected.keys()} == expected + Assert.eq({k: config.finalize_environment()[k] for k in expected.keys()}, expected) (d / "mkosi.conf").write_text( """\ @@ -1470,30 +1523,32 @@ def test_proxy(tmp_path: Path) -> None: } # Only check values for keys from expected, as config.environment contains other items as well - assert {k: config.finalize_environment()[k] for k in expected.keys()} == expected + Assert.eq({k: config.finalize_environment()[k] for k in expected.keys()}, expected) -def test_mkosi_version_executable(tmp_path: Path) -> None: +async def test_mkosi_version_executable(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path version = d / "mkosi.version" version.write_text("#!/bin/sh\necho '1.2.3'\n") with chdir(d): - with pytest.raises(SystemExit) as error: + with Assert.raises(SystemExit) as error: _, _, [config] = parse_config() - assert error.type is SystemExit - assert error.value.code != 0 + Assert.is_(error.exception.__class__, SystemExit) + Assert.ne(error.exception.code, 0) version.chmod(0o755) with chdir(d): _, _, [config] = parse_config() - assert config.image_version == "1.2.3" + Assert.eq(config.image_version, "1.2.3") -def test_split_artifacts(tmp_path: Path) -> None: +async def test_split_artifacts(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path (d / "mkosi.conf").write_text( @@ -1505,7 +1560,7 @@ def test_split_artifacts(tmp_path: Path) -> None: with chdir(d): _, _, [config] = parse_config() - assert config.split_artifacts == [ArtifactOutput.uki] + Assert.eq(config.split_artifacts, [ArtifactOutput.uki]) (d / "mkosi.conf").write_text( """ @@ -1518,19 +1573,23 @@ def test_split_artifacts(tmp_path: Path) -> None: with chdir(d): _, _, [config] = parse_config() - assert config.split_artifacts == [ - ArtifactOutput.uki, - ArtifactOutput.kernel, - ArtifactOutput.initrd, - ] + Assert.eq( + config.split_artifacts, + [ + ArtifactOutput.uki, + ArtifactOutput.kernel, + ArtifactOutput.initrd, + ], + ) -def test_split_artifacts_compat(tmp_path: Path) -> None: +async def test_split_artifacts_compat(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path with chdir(d): _, _, [config] = parse_config() - assert config.split_artifacts == ArtifactOutput.compat_no() + Assert.eq(config.split_artifacts, ArtifactOutput.compat_no()) (d / "mkosi.conf").write_text( """ @@ -1541,10 +1600,11 @@ def test_split_artifacts_compat(tmp_path: Path) -> None: with chdir(d): _, _, [config] = parse_config() - assert config.split_artifacts == ArtifactOutput.compat_yes() + Assert.eq(config.split_artifacts, ArtifactOutput.compat_yes()) -def test_cli_collection_reset(tmp_path: Path) -> None: +async def test_cli_collection_reset(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path (d / "mkosi.conf").write_text( @@ -1556,32 +1616,35 @@ def test_cli_collection_reset(tmp_path: Path) -> None: with chdir(d): _, _, [config] = parse_config(["--package", ""]) - assert config.packages == [] + Assert.eq(config.packages, []) _, _, [config] = parse_config(["--package", "", "--package", "foo"]) - assert config.packages == ["foo"] + Assert.eq(config.packages, ["foo"]) _, _, [config] = parse_config(["--package", "foo", "--package", "", "--package", "bar"]) - assert config.packages == ["bar"] + Assert.eq(config.packages, ["bar"]) _, _, [config] = parse_config(["--package", "foo", "--package", ""]) - assert config.packages == [] + Assert.eq(config.packages, []) -def test_tools(tmp_path: Path) -> None: +async def test_tools(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path argv = ["--tools-tree=default"] if in_box(): - pytest.skip("Cannot run test_tools() test within mkosi box environment") + Assert.skip("Cannot run test_tools() test within mkosi box environment") with resource_path(mkosi.resources) as resources, chdir(d): _, tools, _ = parse_config(argv, resources=resources) - assert tools + Assert.not_none(tools) + tools = cast(Config, tools) host = detect_distribution()[0] if isinstance(host, Distribution): - assert tools.distribution == ( - host.installer.default_tools_tree_distribution() or tools.distribution + Assert.eq( + tools.distribution, + (host.installer.default_tools_tree_distribution() or tools.distribution), ) (d / "mkosi.tools.conf").write_text( @@ -1592,20 +1655,23 @@ def test_tools(tmp_path: Path) -> None: ) _, tools, _ = parse_config(argv, resources=resources) - assert tools - assert tools.package_directories == [Path(d)] + Assert.not_none(tools) + tools = cast(Config, tools) + Assert.eq(tools.package_directories, [Path(d)]) _, tools, _ = parse_config( argv + ["--tools-tree-distribution=arch", "--tools-tree-package-directory=/tmp"], resources=resources, ) - assert tools - assert tools.distribution == Distribution.arch - assert tools.package_directories == [Path(d), Path("/tmp")] + Assert.not_none(tools) + tools = cast(Config, tools) + Assert.eq(tools.distribution, Distribution.arch) + Assert.eq(tools.package_directories, [Path(d), Path("/tmp")]) _, tools, _ = parse_config(argv + ["--tools-tree-package-directory="], resources=resources) - assert tools - assert tools.package_directories == [] + Assert.not_none(tools) + tools = cast(Config, tools) + Assert.eq(tools.package_directories, []) (d / "mkosi.conf").write_text( """ @@ -1615,11 +1681,13 @@ def test_tools(tmp_path: Path) -> None: ) _, tools, _ = parse_config(argv, resources=resources) - assert tools - assert tools.distribution == Distribution.arch + Assert.not_none(tools) + tools = cast(Config, tools) + Assert.eq(tools.distribution, Distribution.arch) -def test_subdir(tmp_path: Path) -> None: +async def test_subdir(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path with chdir(d): @@ -1632,7 +1700,7 @@ def test_subdir(tmp_path: Path) -> None: ) _, _, [config] = parse_config() - assert config.output == "qed" + Assert.eq(config.output, "qed") os.chdir(d) @@ -1644,10 +1712,11 @@ def test_subdir(tmp_path: Path) -> None: ) _, _, [config] = parse_config() - assert config.output == "abc" + Assert.eq(config.output, "abc") -def test_assert(tmp_path: Path) -> None: +async def test_assert(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path with chdir(d): @@ -1658,7 +1727,7 @@ def test_assert(tmp_path: Path) -> None: """ ) - with pytest.raises(SystemExit): + with Assert.raises(SystemExit): parse_config() # Does not raise, i.e. parses successfully, but we don't care for the content. @@ -1674,11 +1743,11 @@ def test_assert(tmp_path: Path) -> None: """ ) - with pytest.raises(SystemExit): + with Assert.raises(SystemExit): parse_config([]) - with pytest.raises(SystemExit): + with Assert.raises(SystemExit): parse_config(["--image-id", "abcde"]) - with pytest.raises(SystemExit): + with Assert.raises(SystemExit): parse_config(["--environment", "ABC=QED"]) parse_config(["--image-id", "abcde", "--environment", "ABC=QED"]) @@ -1693,7 +1762,7 @@ def test_assert(tmp_path: Path) -> None: """ ) - with pytest.raises(SystemExit): + with Assert.raises(SystemExit): parse_config() parse_config(["--image-id", "abcde"]) @@ -1712,14 +1781,15 @@ def test_assert(tmp_path: Path) -> None: """ ) - with pytest.raises(SystemExit): + with Assert.raises(SystemExit): parse_config() parse_config(["--image-id", "abcde", "--environment", "ABC=QED"]) parse_config(["--image-id", "abcde", "--environment", "DEF=QEE"]) -def test_initrd_packages(tmp_path: Path) -> None: +async def test_initrd_packages(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path (d / "mkosi.conf").write_text( @@ -1736,8 +1806,8 @@ def test_initrd_packages(tmp_path: Path) -> None: with chdir(d), resource_path(mkosi.resources) as resources: _, _, [initrd, _] = parse_config(resources=resources) - assert "package1" in initrd.packages - assert "package2" in initrd.packages + Assert.in_("package1", initrd.packages) + Assert.in_("package2", initrd.packages) # Make sure the InitrdPackages= are also picked up when a subimage is defined. (d / "mkosi.images").mkdir() @@ -1746,11 +1816,12 @@ def test_initrd_packages(tmp_path: Path) -> None: with chdir(d), resource_path(mkosi.resources) as resources: _, _, [_, initrd, _] = parse_config(resources=resources) - assert "package1" in initrd.packages - assert "package2" in initrd.packages + Assert.in_("package1", initrd.packages) + Assert.in_("package2", initrd.packages) -def test_config_default_initrds(tmp_path: Path) -> None: +async def test_config_default_initrds(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path # Default initrd should be built when Bootable=yes and the image format supports it. @@ -1764,8 +1835,8 @@ def test_config_default_initrds(tmp_path: Path) -> None: with chdir(d), resource_path(mkosi.resources) as resources: _, _, [initrd, main] = parse_config(resources=resources) - assert len(main.initrds) == 1 - assert initrd.image == "default-initrd" + Assert.eq(len(main.initrds), 1) + Assert.eq(initrd.image, "default-initrd") # Default initrd should not be built when Bootable=disabled. (d / "mkosi.conf").write_text( @@ -1778,7 +1849,7 @@ def test_config_default_initrds(tmp_path: Path) -> None: with chdir(d), resource_path(mkosi.resources) as resources: _, _, [config] = parse_config(resources=resources) - assert not config.initrds + Assert.false(config.initrds) # Default initrd should not be built for UKI output format. (d / "mkosi.conf").write_text( @@ -1794,7 +1865,7 @@ def test_config_default_initrds(tmp_path: Path) -> None: with chdir(d), resource_path(mkosi.resources) as resources: _, _, [config] = parse_config(resources=resources) - assert not config.initrds + Assert.false(config.initrds) # Default initrd should not be built for ESP output format. (d / "mkosi.conf").write_text( @@ -1810,7 +1881,7 @@ def test_config_default_initrds(tmp_path: Path) -> None: with chdir(d), resource_path(mkosi.resources) as resources: _, _, [config] = parse_config(resources=resources) - assert not config.initrds + Assert.false(config.initrds) # Default initrd should not be built when Bootable=auto and output is cpio. (d / "mkosi.conf").write_text( @@ -1823,7 +1894,7 @@ def test_config_default_initrds(tmp_path: Path) -> None: with chdir(d), resource_path(mkosi.resources) as resources: _, _, [config] = parse_config(resources=resources) - assert not config.initrds + Assert.false(config.initrds) # Default initrd should not be built when Bootable=auto and output is a sysext image. (d / "mkosi.conf").write_text( @@ -1836,7 +1907,7 @@ def test_config_default_initrds(tmp_path: Path) -> None: with chdir(d), resource_path(mkosi.resources) as resources: _, _, [config] = parse_config(resources=resources) - assert not config.initrds + Assert.false(config.initrds) # Default initrd should not be built when Overlay=yes. (d / "mkosi.conf").write_text( @@ -1849,10 +1920,11 @@ def test_config_default_initrds(tmp_path: Path) -> None: with chdir(d), resource_path(mkosi.resources) as resources: _, _, [config] = parse_config(resources=resources) - assert not config.initrds + Assert.false(config.initrds) -def test_initrds_default_value(tmp_path: Path) -> None: +async def test_initrds_default_value(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path # The "default" special value should explicitly request the default initrd. @@ -1866,8 +1938,8 @@ def test_initrds_default_value(tmp_path: Path) -> None: with chdir(d), resource_path(mkosi.resources) as resources: _, _, [initrd, main] = parse_config(resources=resources) - assert len(main.initrds) == 1 - assert initrd.image == "default-initrd" + Assert.eq(len(main.initrds), 1) + Assert.eq(initrd.image, "default-initrd") (d / "myinitrd.cpio").touch() @@ -1885,10 +1957,10 @@ def test_initrds_default_value(tmp_path: Path) -> None: _, _, [initrd, main] = parse_config(resources=resources) # The main image should have two initrds: the custom one and the default one (resolved path). - assert len(main.initrds) == 2 - assert main.initrds[0] == d / "myinitrd.cpio" + Assert.eq(len(main.initrds), 2) + Assert.eq(main.initrds[0], d / "myinitrd.cpio") # Second initrd should be the resolved path from the default initrd image. - assert initrd.image == "default-initrd" + Assert.eq(initrd.image, "default-initrd") # When only "default" is specified, the default initrd should be built. (d / "mkosi.conf").write_text( @@ -1902,11 +1974,12 @@ def test_initrds_default_value(tmp_path: Path) -> None: with chdir(d), resource_path(mkosi.resources) as resources: _, _, [initrd, main] = parse_config(resources=resources) - assert len(main.initrds) == 1 - assert initrd.image == "default-initrd" + Assert.eq(len(main.initrds), 1) + Assert.eq(initrd.image, "default-initrd") -def test_initrds_empty_resets(tmp_path: Path) -> None: +async def test_initrds_empty_resets(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path # An empty value for Initrds= should disable the default initrd. @@ -1922,7 +1995,7 @@ def test_initrds_empty_resets(tmp_path: Path) -> None: _, _, [config] = parse_config(resources=resources) # Empty string should reset to empty, not to default. - assert not config.initrds + Assert.false(config.initrds) # Make sure dropin can override with empty to disable default initrd. (d / "mkosi.conf").write_text( @@ -1943,10 +2016,11 @@ def test_initrds_empty_resets(tmp_path: Path) -> None: with chdir(d), resource_path(mkosi.resources) as resources: _, _, [config] = parse_config(resources=resources) - assert not config.initrds + Assert.false(config.initrds) -def test_initrds_custom_only(tmp_path: Path) -> None: +async def test_initrds_custom_only(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path (d / "myinitrd.cpio").touch() @@ -1963,11 +2037,12 @@ def test_initrds_custom_only(tmp_path: Path) -> None: with chdir(d), resource_path(mkosi.resources) as resources: _, _, [config] = parse_config(resources=resources) - assert len(config.initrds) == 1 - assert config.initrds[0] == d / "myinitrd.cpio" + Assert.eq(len(config.initrds), 1) + Assert.eq(config.initrds[0], d / "myinitrd.cpio") -def test_history_empty_list(tmp_path: Path) -> None: +async def test_history_empty_list(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path (d / "packages").mkdir() @@ -1985,16 +2060,17 @@ def test_history_empty_list(tmp_path: Path) -> None: with chdir(d): _, _, [main] = parse_config(["--package-directory=", "build"]) - assert (d / ".mkosi-private/history/latest.json").exists() - assert main.package_directories == [] + Assert.true((d / ".mkosi-private/history/latest.json").exists()) + Assert.eq(main.package_directories, []) with chdir(d): _, _, [main] = parse_config(["summary"]) - assert main.package_directories == [] + Assert.eq(main.package_directories, []) -def test_history_found_via_configured_output_directory(tmp_path: Path) -> None: +async def test_history_found_via_configured_output_directory(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path out = d / "out" @@ -2023,10 +2099,11 @@ def test_history_found_via_configured_output_directory(tmp_path: Path) -> None: # the build's history. _, _, [config] = parse_config(["summary"]) - assert config.image_id == "img-x" + Assert.eq(config.image_id, "img-x") -def test_history_isolated_per_output_directory(tmp_path: Path) -> None: +async def test_history_isolated_per_output_directory(stack: contextlib.AsyncExitStack) -> None: + tmp_path = tmp_dir(stack) d = tmp_path (d / "mkosi.conf").write_text( @@ -2054,6 +2131,6 @@ def test_history_isolated_per_output_directory(tmp_path: Path) -> None: _, _, [config_a] = parse_config(["--output-directory", os.fspath(out_a), "summary"]) _, _, [config_b] = parse_config(["--output-directory", os.fspath(out_b), "summary"]) - assert config_last.image_id == "img-b" - assert config_a.image_id == "img-a" - assert config_b.image_id == "img-b" + Assert.eq(config_last.image_id, "img-b") + Assert.eq(config_a.image_id, "img-a") + Assert.eq(config_b.image_id, "img-b") diff --git a/tests/test_extension.py b/tests/test_extension.py deleted file mode 100644 index b205390ecc..0000000000 --- a/tests/test_extension.py +++ /dev/null @@ -1,31 +0,0 @@ -# SPDX-License-Identifier: LGPL-2.1-or-later - -from pathlib import Path - -import pytest - -from mkosi.config import OutputFormat - -from . import Image, ImageConfig - -pytestmark = pytest.mark.integration - - -@pytest.mark.parametrize("format", [f for f in OutputFormat if f.is_extension_image()]) -def test_extension(config: ImageConfig, format: OutputFormat) -> None: - with Image(config) as image: - image.build(["--clean-package-metadata=no", "--format=directory"]) - - with Image(image.config) as sysext: - sysext.build( - [ - "--directory", - "", - "--incremental=no", - "--base-tree", Path(image.output_dir) / "image", - "--overlay=yes", - "--selinux-relabel=no", - "--package=lsof", - f"--format={format}", - ] - ) # fmt: skip diff --git a/tests/test_json.py b/tests/test_json.py index 26f68b14a1..3ffdd8dc17 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import Optional -import pytest +import barrage.assertions as Assert from mkosi.config import ( Architecture, @@ -50,8 +50,7 @@ from mkosi.distribution import Distribution -@pytest.mark.parametrize("path", [None, "/baz/qux"]) -def test_args(path: Optional[Path]) -> None: +def check_args(path: Optional[Path]) -> None: dump = textwrap.dedent( f"""\ {{ @@ -99,11 +98,19 @@ def test_args(path: Optional[Path]) -> None: wipe_build_dir=True, ) - assert dump_json(args.to_dict()) == dump.rstrip() - assert Args.from_json(dump) == args + Assert.eq(dump_json(args.to_dict()), dump.rstrip()) + Assert.eq(Args.from_json(dump), args) -def test_config() -> None: +async def test_args_no_directory() -> None: + check_args(None) + + +async def test_args_directory() -> None: + check_args(Path("/baz/qux")) + + +async def test_config() -> None: dump = textwrap.dedent( """\ { @@ -654,5 +661,5 @@ def test_config() -> None: workspace_dir=Path("/cwd"), ) - assert dump_json(args.to_dict()) == dump.rstrip() - assert Config.from_json(dump) == args + Assert.eq(dump_json(args.to_dict()), dump.rstrip()) + Assert.eq(Config.from_json(dump), args) diff --git a/tests/test_kmod.py b/tests/test_kmod.py index e8f2a5e4b4..ee864ba70a 100644 --- a/tests/test_kmod.py +++ b/tests/test_kmod.py @@ -1,60 +1,62 @@ # SPDX-License-Identifier: LGPL-2.1-or-later +import barrage.assertions as Assert + from mkosi import kmod -def test_globs_match_module() -> None: - assert kmod.globs_match_module("drivers/ata/ahci.ko.xz", ["ahci"]) - assert not kmod.globs_match_module("drivers/ata/ahci.ko.xz.2", ["ahci"]) - assert not kmod.globs_match_module("drivers/ata/ahci.ko.xz", ["ata"]) - assert not kmod.globs_match_module("drivers/ata/ahci.ko.xz", ["drivers"]) - assert not kmod.globs_match_module("drivers/ata/ahci.ko.xz", ["/drivers"]) - assert not kmod.globs_match_module("drivers/ata/ahci.ko.xz", ["/drivers"]) - assert not kmod.globs_match_module("drivers/ata/ahci-2.ko.xz", ["ahci"]) - assert not kmod.globs_match_module("drivers/ata/ahci2.ko.zst", ["ahci"]) - assert kmod.globs_match_module("drivers/ata/ahci.ko", ["ata/*"]) - assert not kmod.globs_match_module("drivers/ata/ahci.ko.xz", ["/ata/*"]) - assert kmod.globs_match_module("drivers/ata/ahci.ko", ["drivers/*"]) - assert kmod.globs_match_module("drivers/ata/ahci.ko", ["/drivers/*"]) - assert not kmod.globs_match_module("drivers/ata/ahci.ko", ["ahci/*"]) - assert not kmod.globs_match_module("drivers/ata/ahci.ko", ["bahci*"]) - assert kmod.globs_match_module("drivers/ata/ahci.ko.zst", ["ahci*"]) - assert kmod.globs_match_module("drivers/ata/ahci.ko.xz", ["ahc*"]) - assert kmod.globs_match_module("drivers/ata/ahci.ko", ["ah*"]) - assert kmod.globs_match_module("drivers/ata/ahci.ko", ["*"]) - assert kmod.globs_match_module("drivers/ata/ahci.ko", ["ata/"]) - assert kmod.globs_match_module("drivers/ata/ahci.ko", ["drivers/"]) - assert kmod.globs_match_module("drivers/ata/ahci.ko", ["drivers/ata/"]) - - assert kmod.globs_match_module("drivers/ata/ahci.ko", ["-ahci", "*"]) - assert kmod.globs_match_module("drivers/ata/ahci.ko", ["-ahci", "*", "ahciahci"]) - assert kmod.globs_match_module("drivers/ata/ahci.ko.xz", ["-ahci", "*"]) - assert kmod.globs_match_module("drivers/ata/ahci.ko.zst", ["-ahci", "*"]) - assert kmod.globs_match_module("drivers/ata/ahci.ko.gz", ["-ahci", "*"]) - assert kmod.globs_match_module("drivers/ata/ahci.ko.gz", ["-ahci", "drivers/"]) - assert kmod.globs_match_module("drivers/ata/ahci.ko.gz", ["-ahci", "ata/"]) - assert not kmod.globs_match_module("drivers/ata/ahci.ko.gz", ["-ahci", "ata/ata/"]) - assert kmod.globs_match_module("drivers/ata/ahci.ko.gz", ["-ahci", "drivers/ata/"]) - assert not kmod.globs_match_module("drivers/ata/ahci.ko", ["*", "-ahci"]) - assert not kmod.globs_match_module("drivers/ata/ahci.ko", ["ahci", "-*"]) - assert not kmod.globs_match_module("drivers/ata/ahci.ko.zst", ["-*"]) - assert not kmod.globs_match_module("drivers/ata/ahci.ko.xz", ["-*"]) +async def test_globs_match_module() -> None: + Assert.true(kmod.globs_match_module("drivers/ata/ahci.ko.xz", ["ahci"])) + Assert.false(kmod.globs_match_module("drivers/ata/ahci.ko.xz.2", ["ahci"])) + Assert.false(kmod.globs_match_module("drivers/ata/ahci.ko.xz", ["ata"])) + Assert.false(kmod.globs_match_module("drivers/ata/ahci.ko.xz", ["drivers"])) + Assert.false(kmod.globs_match_module("drivers/ata/ahci.ko.xz", ["/drivers"])) + Assert.false(kmod.globs_match_module("drivers/ata/ahci.ko.xz", ["/drivers"])) + Assert.false(kmod.globs_match_module("drivers/ata/ahci-2.ko.xz", ["ahci"])) + Assert.false(kmod.globs_match_module("drivers/ata/ahci2.ko.zst", ["ahci"])) + Assert.true(kmod.globs_match_module("drivers/ata/ahci.ko", ["ata/*"])) + Assert.false(kmod.globs_match_module("drivers/ata/ahci.ko.xz", ["/ata/*"])) + Assert.true(kmod.globs_match_module("drivers/ata/ahci.ko", ["drivers/*"])) + Assert.true(kmod.globs_match_module("drivers/ata/ahci.ko", ["/drivers/*"])) + Assert.false(kmod.globs_match_module("drivers/ata/ahci.ko", ["ahci/*"])) + Assert.false(kmod.globs_match_module("drivers/ata/ahci.ko", ["bahci*"])) + Assert.true(kmod.globs_match_module("drivers/ata/ahci.ko.zst", ["ahci*"])) + Assert.true(kmod.globs_match_module("drivers/ata/ahci.ko.xz", ["ahc*"])) + Assert.true(kmod.globs_match_module("drivers/ata/ahci.ko", ["ah*"])) + Assert.true(kmod.globs_match_module("drivers/ata/ahci.ko", ["*"])) + Assert.true(kmod.globs_match_module("drivers/ata/ahci.ko", ["ata/"])) + Assert.true(kmod.globs_match_module("drivers/ata/ahci.ko", ["drivers/"])) + Assert.true(kmod.globs_match_module("drivers/ata/ahci.ko", ["drivers/ata/"])) + + Assert.true(kmod.globs_match_module("drivers/ata/ahci.ko", ["-ahci", "*"])) + Assert.true(kmod.globs_match_module("drivers/ata/ahci.ko", ["-ahci", "*", "ahciahci"])) + Assert.true(kmod.globs_match_module("drivers/ata/ahci.ko.xz", ["-ahci", "*"])) + Assert.true(kmod.globs_match_module("drivers/ata/ahci.ko.zst", ["-ahci", "*"])) + Assert.true(kmod.globs_match_module("drivers/ata/ahci.ko.gz", ["-ahci", "*"])) + Assert.true(kmod.globs_match_module("drivers/ata/ahci.ko.gz", ["-ahci", "drivers/"])) + Assert.true(kmod.globs_match_module("drivers/ata/ahci.ko.gz", ["-ahci", "ata/"])) + Assert.false(kmod.globs_match_module("drivers/ata/ahci.ko.gz", ["-ahci", "ata/ata/"])) + Assert.true(kmod.globs_match_module("drivers/ata/ahci.ko.gz", ["-ahci", "drivers/ata/"])) + Assert.false(kmod.globs_match_module("drivers/ata/ahci.ko", ["*", "-ahci"])) + Assert.false(kmod.globs_match_module("drivers/ata/ahci.ko", ["ahci", "-*"])) + Assert.false(kmod.globs_match_module("drivers/ata/ahci.ko.zst", ["-*"])) + Assert.false(kmod.globs_match_module("drivers/ata/ahci.ko.xz", ["-*"])) # absolute glob behavior unchanged when paths are relative to /lib/module/ - assert kmod.globs_match_module("kernel/drivers/ata/ahci.ko", ["drivers/*"]) - assert kmod.globs_match_module("kernel/drivers/ata/ahci.ko", ["/drivers/*"]) - assert not kmod.globs_match_module("kernel/drivers/ata/ahci.ko.xz", ["/ata/*"]) + Assert.true(kmod.globs_match_module("kernel/drivers/ata/ahci.ko", ["drivers/*"])) + Assert.true(kmod.globs_match_module("kernel/drivers/ata/ahci.ko", ["/drivers/*"])) + Assert.false(kmod.globs_match_module("kernel/drivers/ata/ahci.ko.xz", ["/ata/*"])) # absolute globs match both relative to kernel/ and module_dir root - assert kmod.globs_match_module("kernel/drivers/ata/ahci.ko.xz", ["/drivers/ata/ahci"]) - assert kmod.globs_match_module("kernel/drivers/ata/ahci.ko.xz", ["/kernel/drivers/ata/ahci"]) - - -def test_normalize_module_glob() -> None: - assert kmod.normalize_module_glob("raid[0-9]") == "raid[0-9]" - assert kmod.normalize_module_glob("raid[0_9]") == "raid[0_9]" - assert kmod.normalize_module_glob("raid[0_9]a_z") == "raid[0_9]a-z" - assert kmod.normalize_module_glob("0_9") == "0-9" - assert kmod.normalize_module_glob("[0_9") == "[0_9" - assert kmod.normalize_module_glob("0_9]") == "0-9]" - assert kmod.normalize_module_glob("raid[0_9]a_z[a_c]") == "raid[0_9]a-z[a_c]" + Assert.true(kmod.globs_match_module("kernel/drivers/ata/ahci.ko.xz", ["/drivers/ata/ahci"])) + Assert.true(kmod.globs_match_module("kernel/drivers/ata/ahci.ko.xz", ["/kernel/drivers/ata/ahci"])) + + +async def test_normalize_module_glob() -> None: + Assert.eq(kmod.normalize_module_glob("raid[0-9]"), "raid[0-9]") + Assert.eq(kmod.normalize_module_glob("raid[0_9]"), "raid[0_9]") + Assert.eq(kmod.normalize_module_glob("raid[0_9]a_z"), "raid[0_9]a-z") + Assert.eq(kmod.normalize_module_glob("0_9"), "0-9") + Assert.eq(kmod.normalize_module_glob("[0_9"), "[0_9") + Assert.eq(kmod.normalize_module_glob("0_9]"), "0-9]") + Assert.eq(kmod.normalize_module_glob("raid[0_9]a_z[a_c]"), "raid[0_9]a-z[a_c]") diff --git a/tests/test_linters.py b/tests/test_linters.py index f1d6464af6..d48da62a49 100644 --- a/tests/test_linters.py +++ b/tests/test_linters.py @@ -5,7 +5,7 @@ import subprocess from pathlib import Path -import pytest +import barrage.assertions as Assert from mkosi.run import run @@ -25,29 +25,36 @@ def skip_if_missing(tool: str) -> bool: return SKIP_MISSING_TOOLS and shutil.which(tool) is None -@pytest.mark.skipif(skip_if_missing("ruff"), reason="ruff not found") -def test_ruff_format_check() -> None: +async def test_ruff_format_check() -> None: """Check that code is formatted with ruff format.""" - run(["ruff", "format", "--check", "--diff", "mkosi/", "tests/", *kernel_install_files()], cwd=REPO_ROOT) + if skip_if_missing("ruff"): + Assert.skip("ruff not found") + run( + ["ruff", "format", "--check", "--diff", "mkosi/", "tests/", *kernel_install_files()], + cwd=REPO_ROOT, + ) -@pytest.mark.skipif(skip_if_missing("ruff"), reason="ruff not found") -def test_ruff_check() -> None: +async def test_ruff_check() -> None: """Check code quality with ruff.""" + if skip_if_missing("ruff"): + Assert.skip("ruff not found") run( ["ruff", "check", "--output-format=github", "mkosi/", "tests/", *kernel_install_files()], cwd=REPO_ROOT, ) -@pytest.mark.skipif(skip_if_missing("git"), reason="git not found") -def test_no_tabs_in_code() -> None: +async def test_no_tabs_in_code() -> None: + if skip_if_missing("git"): + Assert.skip("git not found") result = run(["git", "grep", "-P", r"\t", "*.py"], check=False, cwd=REPO_ROOT) - assert result.returncode != 0, "Found tabs in Python code" + Assert.ne(result.returncode, 0, "Found tabs in Python code") -@pytest.mark.skipif(skip_if_missing("codespell"), reason="codespell not found") -def test_codespell() -> None: +async def test_codespell() -> None: + if skip_if_missing("codespell"): + Assert.skip("codespell not found") run(["codespell", "--version"], cwd=REPO_ROOT) files = run(["git", "ls-files"], stdout=subprocess.PIPE, cwd=REPO_ROOT).stdout.strip().split("\n") # Filter out files we want to skip @@ -55,28 +62,33 @@ def test_codespell() -> None: run(["codespell", *files_to_check], cwd=REPO_ROOT) -@pytest.mark.skipif(skip_if_missing("reuse"), reason="reuse not found") -def test_reuse_lint() -> None: +async def test_reuse_lint() -> None: + if skip_if_missing("reuse"): + Assert.skip("reuse not found") run(["reuse", "lint"], cwd=REPO_ROOT) -@pytest.mark.skipif(skip_if_missing("mypy"), reason="mypy not found") -def test_mypy() -> None: +async def test_mypy() -> None: + if skip_if_missing("mypy"): + Assert.skip("mypy not found") run(["mypy", "mkosi/", *kernel_install_files()], cwd=REPO_ROOT) -@pytest.mark.skipif(skip_if_missing("mypy"), reason="mypy not found") -def test_mypy_python310() -> None: +async def test_mypy_python310() -> None: + if skip_if_missing("mypy"): + Assert.skip("mypy not found") run(["mypy", "--python-version", "3.10", "mkosi/", "tests/", *kernel_install_files()], cwd=REPO_ROOT) -@pytest.mark.skipif(skip_if_missing("pyright"), reason="pyright not found") -def test_pyright() -> None: +async def test_pyright() -> None: + if skip_if_missing("pyright"): + Assert.skip("pyright not found") run(["pyright", "mkosi/", "tests/", *kernel_install_files()], cwd=REPO_ROOT) -@pytest.mark.skipif(skip_if_missing("shellcheck"), reason="shellcheck not found") -def test_shellcheck() -> None: +async def test_shellcheck() -> None: + if skip_if_missing("shellcheck"): + Assert.skip("shellcheck not found") # Check bin/mkosi and tools/*.sh tools_scripts = [os.fspath(p) for p in (REPO_ROOT / "tools").glob("*.sh")] run(["shellcheck", "bin/mkosi", *tools_scripts], cwd=REPO_ROOT) @@ -86,10 +98,9 @@ def test_shellcheck() -> None: run(["shellcheck", "-"], input=completion, cwd=REPO_ROOT) -@pytest.mark.skipif(skip_if_missing("pandoc"), reason="pandoc not found") -@pytest.mark.skipif( - SKIP_MISSING_TOOLS and not (REPO_ROOT / "tools/make-man-page.sh").exists(), - reason="tools/make-man-page.sh not found", -) -def test_man_page_generation() -> None: +async def test_man_page_generation() -> None: + if skip_if_missing("pandoc"): + Assert.skip("pandoc not found") + if SKIP_MISSING_TOOLS and not (REPO_ROOT / "tools/make-man-page.sh").exists(): + Assert.skip("tools/make-man-page.sh not found") run(["tools/make-man-page.sh"], cwd=REPO_ROOT) diff --git a/tests/test_run.py b/tests/test_run.py index 52b4306756..fa7967df13 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -3,62 +3,65 @@ import contextlib import os import subprocess +import tempfile from pathlib import Path -import pytest +import barrage.assertions as Assert from mkosi.run import fork_and_wait -def test_fork_and_wait_returns_value() -> None: +async def test_fork_and_wait_returns_value() -> None: result = fork_and_wait(lambda: 42) - assert result == 42 + Assert.eq(result, 42) -def test_fork_and_wait_returns_none() -> None: +async def test_fork_and_wait_returns_none() -> None: result = fork_and_wait(lambda: None) - assert result is None + Assert.none(result) -def test_fork_and_wait_returns_string() -> None: +async def test_fork_and_wait_returns_string() -> None: result = fork_and_wait(lambda: "hello world") - assert result == "hello world" + Assert.eq(result, "hello world") -def test_fork_and_wait_returns_complex_type() -> None: +async def test_fork_and_wait_returns_complex_type() -> None: result = fork_and_wait(lambda: {"key": [1, 2, 3], "nested": {"a": True}}) - assert result == {"key": [1, 2, 3], "nested": {"a": True}} + Assert.eq(result, {"key": [1, 2, 3], "nested": {"a": True}}) -def test_fork_and_wait_passes_args() -> None: +async def test_fork_and_wait_passes_args() -> None: def add(a: int, b: int) -> int: return a + b result = fork_and_wait(add, 3, 4) - assert result == 7 + Assert.eq(result, 7) -def test_fork_and_wait_passes_kwargs() -> None: +async def test_fork_and_wait_passes_kwargs() -> None: def greet(name: str, greeting: str = "Hello") -> str: return f"{greeting}, {name}!" result = fork_and_wait(greet, "world", greeting="Hi") - assert result == "Hi, world!" + Assert.eq(result, "Hi, world!") -def test_fork_and_wait_child_failure() -> None: +async def test_fork_and_wait_child_failure() -> None: def fail() -> None: raise RuntimeError("boom") - with pytest.raises(subprocess.CalledProcessError): + with Assert.raises(subprocess.CalledProcessError): fork_and_wait(fail) -def test_fork_and_wait_sandbox(tmp_path: Path) -> None: - (tmp_path / "abc").mkdir() +async def test_fork_and_wait_sandbox() -> None: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + (tmp_path / "abc").mkdir() - def exists() -> bool: - return Path("/abc").exists() + def exists() -> bool: + return Path("/abc").exists() - result = fork_and_wait(exists, sandbox=contextlib.nullcontext(["--bind", os.fspath(tmp_path), "/"])) - assert result + result = fork_and_wait(exists, sandbox=contextlib.nullcontext(["--bind", os.fspath(tmp_path), "/"])) + Assert.true(result) diff --git a/tests/test_util.py b/tests/test_util.py index 8796b7826a..7c77b5340c 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -2,51 +2,51 @@ from pathlib import Path -import pytest +import barrage.assertions as Assert from mkosi.util import parents_below -def test_parents_below_basic() -> None: +async def test_parents_below_basic() -> None: path = Path("/a/b/c/d/e") below = Path("/a/b") - assert parents_below(path, below) == [Path("/a/b/c/d"), Path("/a/b/c")] + Assert.eq(parents_below(path, below), [Path("/a/b/c/d"), Path("/a/b/c")]) -def test_parents_below_root() -> None: +async def test_parents_below_root() -> None: path = Path("/a/b/c") below = Path("/") - assert parents_below(path, below) == [Path("/a/b"), Path("/a")] + Assert.eq(parents_below(path, below), [Path("/a/b"), Path("/a")]) -def test_parents_below_direct_child() -> None: +async def test_parents_below_direct_child() -> None: path = Path("/a/b/c") below = Path("/a/b") - assert parents_below(path, below) == [] + Assert.eq(parents_below(path, below), []) -def test_parents_below_relative_paths() -> None: +async def test_parents_below_relative_paths() -> None: path = Path("a/b/c/d") below = Path("a/b") - assert parents_below(path, below) == [Path("a/b/c")] + Assert.eq(parents_below(path, below), [Path("a/b/c")]) -def test_parents_below_same_path_raises() -> None: +async def test_parents_below_same_path_raises() -> None: path = Path("/a/b/c") below = Path("/a/b/c") - with pytest.raises(ValueError): + with Assert.raises(ValueError): parents_below(path, below) -def test_parents_below_not_parent_raises() -> None: +async def test_parents_below_not_parent_raises() -> None: path = Path("/a/b/c") below = Path("/x/y/z") - with pytest.raises(ValueError): + with Assert.raises(ValueError): parents_below(path, below) -def test_parents_below_below_is_child_raises() -> None: +async def test_parents_below_below_is_child_raises() -> None: path = Path("/a/b") below = Path("/a/b/c") - with pytest.raises(ValueError): + with Assert.raises(ValueError): parents_below(path, below) diff --git a/tests/test_versioncomp.py b/tests/test_versioncomp.py index e23f7d340b..c4c61a230e 100644 --- a/tests/test_versioncomp.py +++ b/tests/test_versioncomp.py @@ -2,103 +2,98 @@ import itertools -import pytest +import barrage.assertions as Assert from mkosi.versioncomp import GenericVersion -def test_conversion() -> None: - assert GenericVersion("1") < 2 - assert GenericVersion("1") < "2" - assert GenericVersion("2") > 1 - assert GenericVersion("2") > "1" - assert GenericVersion("1") == "1" +def RPMVERCMP(a: str, b: str, expected: int) -> None: + Assert.eq( + (GenericVersion(a) > GenericVersion(b)) - (GenericVersion(a) < GenericVersion(b)), + expected, + ) + +async def test_conversion() -> None: + Assert.lt(GenericVersion("1"), 2) + Assert.lt(GenericVersion("1"), "2") + Assert.gt(GenericVersion("2"), 1) + Assert.gt(GenericVersion("2"), "1") + Assert.eq(GenericVersion("1"), "1") -def test_generic_version_systemd() -> None: + +async def test_generic_version_systemd() -> None: """Same as the first block of systemd/test/test-compare-versions.sh""" - assert GenericVersion("1") < GenericVersion("2") - assert GenericVersion("1") <= GenericVersion("2") - assert GenericVersion("1") != GenericVersion("2") - assert not (GenericVersion("1") > GenericVersion("2")) - assert not (GenericVersion("1") == GenericVersion("2")) - assert not (GenericVersion("1") >= GenericVersion("2")) - assert GenericVersion.compare_versions("1", "2") == -1 - assert GenericVersion.compare_versions("2", "2") == 0 - assert GenericVersion.compare_versions("2", "1") == 1 - - -def test_generic_version_spec() -> None: + Assert.lt(GenericVersion("1"), GenericVersion("2")) + Assert.le(GenericVersion("1"), GenericVersion("2")) + Assert.ne(GenericVersion("1"), GenericVersion("2")) + Assert.false(GenericVersion("1") > GenericVersion("2")) + Assert.false(GenericVersion("1") == GenericVersion("2")) + Assert.false(GenericVersion("1") >= GenericVersion("2")) + Assert.eq(GenericVersion.compare_versions("1", "2"), -1) + Assert.eq(GenericVersion.compare_versions("2", "2"), 0) + Assert.eq(GenericVersion.compare_versions("2", "1"), 1) + + +async def test_generic_version_spec() -> None: """Examples from the uapi group version format spec""" - assert GenericVersion("11") == GenericVersion("11") - assert GenericVersion("systemd-123") == GenericVersion("systemd-123") - assert GenericVersion("bar-123") < GenericVersion("foo-123") - assert GenericVersion("123a") > GenericVersion("123") - assert GenericVersion("123.a") > GenericVersion("123") - assert GenericVersion("123.a") < GenericVersion("123.b") - assert GenericVersion("123a") > GenericVersion("123.a") - assert GenericVersion("11α") == GenericVersion("11β") - assert GenericVersion("A") < GenericVersion("a") - assert GenericVersion("") < GenericVersion("0") - assert GenericVersion("0.") > GenericVersion("0") - assert GenericVersion("0.0") > GenericVersion("0") - assert GenericVersion("0") > GenericVersion("~") - assert GenericVersion("") > GenericVersion("~") - assert GenericVersion("1_") == GenericVersion("1") - assert GenericVersion("_1") == GenericVersion("1") - assert GenericVersion("1_") < GenericVersion("1.2") - assert GenericVersion("1_2_3") > GenericVersion("1.3.3") - assert GenericVersion("1+") == GenericVersion("1") - assert GenericVersion("+1") == GenericVersion("1") - assert GenericVersion("1+") < GenericVersion("1.2") - assert GenericVersion("1+2+3") > GenericVersion("1.3.3") - - -@pytest.mark.parametrize( - "s1,s2", - itertools.combinations_with_replacement( - enumerate( - [ - GenericVersion("122.1"), - GenericVersion("123~rc1-1"), - GenericVersion("123"), - GenericVersion("123-a"), - GenericVersion("123-a.1"), - GenericVersion("123-1"), - GenericVersion("123-1.1"), - GenericVersion("123^post1"), - GenericVersion("123.a-1"), - GenericVersion("123.1-1"), - GenericVersion("123a-1"), - GenericVersion("124-1"), - ], - ), - 2, - ), -) -def test_generic_version_strverscmp_improved_doc( - s1: tuple[int, GenericVersion], - s2: tuple[int, GenericVersion], -) -> None: + Assert.eq(GenericVersion("11"), GenericVersion("11")) + Assert.eq(GenericVersion("systemd-123"), GenericVersion("systemd-123")) + Assert.lt(GenericVersion("bar-123"), GenericVersion("foo-123")) + Assert.gt(GenericVersion("123a"), GenericVersion("123")) + Assert.gt(GenericVersion("123.a"), GenericVersion("123")) + Assert.lt(GenericVersion("123.a"), GenericVersion("123.b")) + Assert.gt(GenericVersion("123a"), GenericVersion("123.a")) + Assert.eq(GenericVersion("11α"), GenericVersion("11β")) + Assert.lt(GenericVersion("A"), GenericVersion("a")) + Assert.lt(GenericVersion(""), GenericVersion("0")) + Assert.gt(GenericVersion("0."), GenericVersion("0")) + Assert.gt(GenericVersion("0.0"), GenericVersion("0")) + Assert.gt(GenericVersion("0"), GenericVersion("~")) + Assert.gt(GenericVersion(""), GenericVersion("~")) + Assert.eq(GenericVersion("1_"), GenericVersion("1")) + Assert.eq(GenericVersion("_1"), GenericVersion("1")) + Assert.lt(GenericVersion("1_"), GenericVersion("1.2")) + Assert.gt(GenericVersion("1_2_3"), GenericVersion("1.3.3")) + Assert.eq(GenericVersion("1+"), GenericVersion("1")) + Assert.eq(GenericVersion("+1"), GenericVersion("1")) + Assert.lt(GenericVersion("1+"), GenericVersion("1.2")) + Assert.gt(GenericVersion("1+2+3"), GenericVersion("1.3.3")) + + +async def test_generic_version_strverscmp_improved_doc() -> None: """Example from the doc string of strverscmp_improved. strverscmp_improved can be found in systemd/src/fundamental/string-util-fundamental.c """ - i1, v1 = s1 - i2, v2 = s2 - assert (v1 == v2) == (i1 == i2) - assert (v1 < v2) == (i1 < i2) - assert (v1 <= v2) == (i1 <= i2) - assert (v1 > v2) == (i1 > i2) - assert (v1 >= v2) == (i1 >= i2) - assert (v1 != v2) == (i1 != i2) - - -def RPMVERCMP(a: str, b: str, expected: int) -> None: - assert (GenericVersion(a) > GenericVersion(b)) - (GenericVersion(a) < GenericVersion(b)) == expected - - -def test_generic_version_rpmvercmp() -> None: + versions = enumerate( + [ + GenericVersion("122.1"), + GenericVersion("123~rc1-1"), + GenericVersion("123"), + GenericVersion("123-a"), + GenericVersion("123-a.1"), + GenericVersion("123-1"), + GenericVersion("123-1.1"), + GenericVersion("123^post1"), + GenericVersion("123.a-1"), + GenericVersion("123.1-1"), + GenericVersion("123a-1"), + GenericVersion("124-1"), + ], + ) + for s1, s2 in itertools.combinations_with_replacement(versions, 2): + i1, v1 = s1 + i2, v2 = s2 + Assert.eq(v1 == v2, i1 == i2) + Assert.eq(v1 < v2, i1 < i2) + Assert.eq(v1 <= v2, i1 <= i2) + Assert.eq(v1 > v2, i1 > i2) + Assert.eq(v1 >= v2, i1 >= i2) + Assert.eq(v1 != v2, i1 != i2) + + +async def test_generic_version_rpmvercmp() -> None: EQUAL = 0 RIGHT_SMALLER = 1 LEFT_SMALLER = -1