1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
|
# 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
|