# SPDX-License-Identifier: GPL-3.0-or-later """Pytest + Testinfra config for archsetup post-install validation. These tests run on the *host* and connect to the freshly-installed VM over SSH (Testinfra provides the `host` fixture, parametrized from --hosts). This file adds two things the bespoke shell harness had that Testinfra does not: - Failure attribution. Each check is marked with the layer that owns a failure (archsetup | base_install | unknown), mirroring validation.sh's attribute_issue. Failures are bucketed and written to --attribution-file so run-test.sh can route base-install issues to the archzfs inbox as before. - Tiering markers (smoke | integration) so `pytest -m smoke` is a fast gate. The `target_user` fixture supplies the account archsetup created; it reads ARCHSETUP_TEST_USER (set by run-test.sh from the VM conf) and defaults to the historical "cjennings". """ import os import pytest _ATTRIBUTION_BUCKETS = ("archsetup", "base_install", "unknown") _failures = {bucket: [] for bucket in _ATTRIBUTION_BUCKETS} def pytest_addoption(parser): parser.addoption( "--attribution-file", action="store", default=None, help="write the failure attribution report (archsetup/base_install/unknown) here", ) def pytest_configure(config): config.addinivalue_line( "markers", "attribution(bucket): layer that owns a failure — archsetup, base_install, or unknown", ) config.addinivalue_line("markers", "smoke: fast subset (user, key packages, dotfiles present)") config.addinivalue_line("markers", "integration: full post-install checks") @pytest.hookimpl(wrapper=True) def pytest_runtest_makereport(item, call): report = yield if report.when == "call" and report.failed: marker = item.get_closest_marker("attribution") bucket = marker.args[0] if (marker and marker.args) else "archsetup" if bucket not in _failures: bucket = "unknown" _failures[bucket].append(item.nodeid) return report def pytest_sessionfinish(session, exitstatus): path = session.config.getoption("--attribution-file") if not path: return with open(path, "w") as fh: for bucket in _ATTRIBUTION_BUCKETS: fh.write("[%s]\n" % bucket) for nodeid in _failures[bucket]: fh.write(" %s\n" % nodeid) @pytest.fixture(scope="session") def target_user(): """The account archsetup created in the VM under test.""" return os.environ.get("ARCHSETUP_TEST_USER", "cjennings") @pytest.fixture(scope="session") def home(target_user): return "/home/%s" % target_user @pytest.fixture(scope="session") def zfs_root(host): """True when the VM's root filesystem is ZFS (gates ZFS-specific checks).""" return host.run("findmnt -n -o FSTYPE /").stdout.strip() == "zfs" @pytest.fixture(scope="session") def has_nvme(host): """True when the VM exposes an NVMe device.""" return host.run("ls /dev/nvme0n1 2>/dev/null").rc == 0 @pytest.fixture(scope="session") def hyprland_installed(host): return host.package("hyprland").is_installed @pytest.fixture(scope="session") def dwm_installed(host): return host.file("/usr/local/bin/dwm").exists @pytest.fixture(scope="session") def compositor_running(host): """A graphical session is live (gates socket/portal checks that need one).""" return host.run("pgrep -x Hyprland").rc == 0 @pytest.fixture(scope="session") def on_slirp(host): """QEMU user-mode networking (10.0.2.x) — no multicast, so mDNS can't work.""" return "10.0.2." in host.run("ip -4 addr show").stdout