diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/installer-steps/test_clone_user_repos.py | 155 | ||||
| -rw-r--r-- | tests/post-rebuild-check/test_post_rebuild_check.py | 317 |
2 files changed, 461 insertions, 11 deletions
diff --git a/tests/installer-steps/test_clone_user_repos.py b/tests/installer-steps/test_clone_user_repos.py new file mode 100644 index 0000000..51d8434 --- /dev/null +++ b/tests/installer-steps/test_clone_user_repos.py @@ -0,0 +1,155 @@ +"""Test clone_user_repos: the two user repos are cloned with full history. + +archsetup and dotfiles are not build directories. They are the two repos I +actively develop in on every machine this installer builds, so a shallow clone +is wrong for both. Velox came back from its 2026-08-13 rebuild with 7 commits +of history in each instead of 851, and nothing about the tree said so. + +The quiet failure is what makes this worth a test rather than a one-line fix. +`git log -- <path>` against a shallow clone does not error; it answers "no +commits". So a credential-history check run on that machine reported five +sensitive files absent from history and exited clean, when the real answer was +that the clone could not see the history they live in. A security question came +back falsely reassuring. Everything else it breaks — blame, bisect, any +archaeology past the graft point — is merely annoying by comparison. + +The AUR build clones are a different case and stay shallow: they are throwaway +build trees, cloned to run `make install` and then discarded, where history has +no value and the download cost is real. So this suite asserts both halves — +full history for the two user repos, and depth still pinned on the AUR path — +because a fix applied with too broad a brush would regress the build clones +without failing any test that only looked at the user repos. + +Method: sed-extract clone_user_repos from the real `archsetup`, fake git / +mkdir / chown / display / error_warn / error_fatal, and read back the git +command lines the function issued. + +Run from repo root: + python3 -m unittest tests.installer-steps.test_clone_user_repos +""" + +import os +import re +import subprocess +import tempfile +import textwrap +import unittest + +REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +ARCHSETUP = os.path.join(REPO_ROOT, "archsetup") + + +def run(clone_fails=False, make_git_dir=True): + """Drive clone_user_repos with every side effect faked. + + dotfiles_dir is pre-created with a .git so the function's "is this a real + checkout?" guard passes on the happy path; make_git_dir=False exercises the + guard itself. + """ + with tempfile.TemporaryDirectory() as d: + dotfiles_dir = os.path.join(d, "dotfiles") + os.makedirs(dotfiles_dir) + if make_git_dir: + os.makedirs(os.path.join(dotfiles_dir, ".git")) + clone_rc = 1 if clone_fails else 0 + script = textwrap.dedent(f"""\ + logfile=/dev/null + action="" + username=testuser + archsetup_repo="https://example.invalid/archsetup.git" + dotfiles_repo="https://example.invalid/dotfiles.git" + dotfiles_branch=main + dotfiles_dir="{dotfiles_dir}" + display() {{ :; }} + mkdir() {{ echo "MKDIR: $*" >> "{d}/calls.log"; return 0; }} + chown() {{ echo "CHOWN: $*" >> "{d}/calls.log"; return 0; }} + git() {{ + echo "GIT: $*" >> "{d}/calls.log" + case "$1" in + clone) return {clone_rc} ;; + *) return 0 ;; + esac + }} + error_warn() {{ echo "WARN: $1" >> "{d}/calls.log"; return 1; }} + error_fatal() {{ echo "FATAL: $1" >> "{d}/calls.log"; exit 1; }} + source <(sed -n '/^clone_user_repos() {{/,/^}}/p' "{ARCHSETUP}") + clone_user_repos + echo "RC=$?" >> "{d}/calls.log" + exit 0 + """) + subprocess.run( + ["bash", "-c", script], capture_output=True, text=True, timeout=10, + ) + with open(os.path.join(d, "calls.log")) as fh: + return fh.read() + + +def clone_lines(log): + return [ln for ln in log.splitlines() if ln.startswith("GIT: clone")] + + +class CloneUserRepos(unittest.TestCase): + # ------------------------------------------------------------ normal ---- + def test_both_user_repos_are_cloned(self): + lines = clone_lines(run()) + self.assertEqual(len(lines), 2, + f"expected an archsetup clone and a dotfiles clone, got: {lines}") + self.assertTrue(any("archsetup.git" in ln for ln in lines)) + self.assertTrue(any("dotfiles.git" in ln for ln in lines)) + + def test_archsetup_clone_carries_full_history(self): + """A shallow archsetup clone answers history questions wrongly.""" + line = next(ln for ln in clone_lines(run()) if "archsetup.git" in ln) + self.assertNotIn("--depth", line, + "archsetup is a working repo, not a build tree — a shallow " + "clone makes `git log -- <path>` answer 'no commits' instead " + "of failing, which is how a credential-history check came " + "back falsely clean on velox") + + def test_dotfiles_clone_carries_full_history(self): + line = next(ln for ln in clone_lines(run()) if "dotfiles.git" in ln) + self.assertNotIn("--depth", line, + "dotfiles is a working repo, not a build tree") + + def test_dotfiles_clone_still_pins_the_branch(self): + """Dropping --depth must not disturb the --branch argument beside it.""" + line = next(ln for ln in clone_lines(run()) if "dotfiles.git" in ln) + self.assertIn("--branch main", line) + + # ---------------------------------------------------------- boundary ---- + def test_no_user_repo_clone_is_shallow_by_any_spelling(self): + """--depth, --depth=N and -depth are all shallow; catch the lot.""" + for line in clone_lines(run()): + self.assertNotRegex(line, r"(^|\s)-{1,2}depth(\s|=)", + f"user-repo clone must be full: {line}") + + def test_aur_build_clones_stay_shallow(self): + """The fix must not over-apply — build trees are throwaway. + + Read against the real file rather than the extracted function, because + these clones live in a different function entirely and the risk being + guarded is a careless repo-wide sed. + """ + with open(ARCHSETUP) as fh: + source = fh.read() + build_clones = re.findall(r"^.*git clone.*build_dir.*$", source, re.M) + self.assertTrue(build_clones, "expected AUR build clones to exist") + for line in build_clones: + self.assertIn("--depth 1", line, + f"AUR build clone should stay shallow: {line.strip()}") + + # ------------------------------------------------------------- error ---- + def test_clone_failure_is_reported_not_swallowed(self): + log = run(clone_fails=True) + self.assertIn("WARN:", log, + "a failed clone must surface through error_warn") + + def test_dotfiles_clone_producing_no_checkout_is_fatal(self): + """The stow/restore steps downstream need a real checkout.""" + log = run(make_git_dir=False) + self.assertIn("FATAL:", log) + self.assertNotIn("RC=", log, "error_fatal must halt, not fall through") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/post-rebuild-check/test_post_rebuild_check.py b/tests/post-rebuild-check/test_post_rebuild_check.py index bad337e..a034f87 100644 --- a/tests/post-rebuild-check/test_post_rebuild_check.py +++ b/tests/post-rebuild-check/test_post_rebuild_check.py @@ -2,7 +2,7 @@ A rebuilt machine looks finished and isn't: on velox 2026-08-13 five gaps surfaced within two days, three of which LOOKED fine (a stowed unit file, an -enabled timer, a present git clone). The script runs the five checks from the +enabled timer, a present git clone). The script runs the checks from the post-rebuild task and turns each silent no-op into a visible line: 1. failed systemd units (user and system scope) @@ -10,6 +10,9 @@ post-rebuild task and turns each silent no-op into a visible line: 3. tracked *.example files whose real sibling is missing 4. gitignore-mode projects missing tooling paths their own .gitignore names 5. signal-cli holds no registered account + 6. every NTP source named by hostname (a wrong clock takes DNS with it) + 7. hypridle installed but not running (nothing triggers idle suspend) + 8. a working repo cloned read-only (push returns 403) Exit 0 with every check clean, 1 when any check found something. @@ -24,6 +27,13 @@ probe"): PRC_NTP_SOURCES newline list of configured NTP server addresses ("MISSING" = no NTP daemon active) the special value MISSING means the binary is absent + PRC_IDLE_DAEMON pgrep output for hypridle ("" = installed but not + running; "MISSING" = not installed on this machine) + PRC_REPO_REMOTES newline list of "path<space>origin-url" for the + push-capability check ("" = no repos to check) + PRC_UNITS_EXPECTED_DISABLED + newline list of units whose not-enabled state is + deliberate on this machine ("" = no exemptions) Run from repo root: python3 -m unittest tests.post-rebuild-check.test_post_rebuild_check @@ -43,7 +53,9 @@ CHECK = os.path.join(REPO_ROOT, "scripts", "post-rebuild-check") def run_check(failed_units="", unit_states="", local_roots="", project_roots="", signal_accounts="+15045551234", - ntp_sources="162.159.200.1\npool.ntp.org"): + ntp_sources="162.159.200.1\npool.ntp.org", + idle_daemon="4242", repo_remotes="", + units_expected_disabled=""): """Run the script with every probe stubbed; defaults are all-clean. Roots are newline-separated. Empty means "the seam is set and names no @@ -57,6 +69,9 @@ def run_check(failed_units="", unit_states="", local_roots="", env["PRC_PROJECT_ROOTS"] = project_roots env["PRC_SIGNAL_ACCOUNTS"] = signal_accounts env["PRC_NTP_SOURCES"] = ntp_sources + env["PRC_IDLE_DAEMON"] = idle_daemon + env["PRC_REPO_REMOTES"] = repo_remotes + env["PRC_UNITS_EXPECTED_DISABLED"] = units_expected_disabled return subprocess.run( ["sh", CHECK], capture_output=True, text=True, timeout=30, env=env, ) @@ -76,7 +91,7 @@ class NtpBootstrap(unittest.TestCase): def test_an_ip_addressed_source_is_clean(self): r = run_check(ntp_sources="162.159.200.1\npool.ntp.org") - self.assertIn("check 6/6: NTP bootstrap — ok", r.stdout) + self.assertIn("check 6/8: NTP bootstrap — ok", r.stdout) self.assertEqual(r.returncode, 0, r.stdout) def test_all_hostname_sources_is_a_finding(self): @@ -88,7 +103,7 @@ class NtpBootstrap(unittest.TestCase): def test_an_ipv6_addressed_source_counts(self): r = run_check(ntp_sources="2606:4700:f1::1") - self.assertIn("check 6/6: NTP bootstrap — ok", r.stdout) + self.assertIn("check 6/8: NTP bootstrap — ok", r.stdout) # --- Boundary cases ------------------------------------------------- @@ -96,11 +111,11 @@ class NtpBootstrap(unittest.TestCase): # Order must not matter; the property is "at least one", and the # drop-in that carries it is read after the main config. r = run_check(ntp_sources="a.pool.ntp.org\nb.pool.ntp.org\n162.159.200.1") - self.assertIn("check 6/6: NTP bootstrap — ok", r.stdout) + self.assertIn("check 6/8: NTP bootstrap — ok", r.stdout) def test_blank_lines_between_sources_are_ignored(self): r = run_check(ntp_sources="\n\n162.159.200.1\n\n") - self.assertIn("check 6/6: NTP bootstrap — ok", r.stdout) + self.assertIn("check 6/8: NTP bootstrap — ok", r.stdout) def test_a_hostname_containing_digits_and_dots_is_not_an_address(self): # The trap in any naive "looks like an IP" test: these resolve through @@ -153,6 +168,9 @@ class NtpBootstrap(unittest.TestCase): env.update({"PRC_FAILED_UNITS": "", "PRC_UNIT_STATES": "", "PRC_LOCAL_SCAN_ROOTS": "", "PRC_PROJECT_ROOTS": "", "PRC_SIGNAL_ACCOUNTS": "+15045551234", + "PRC_IDLE_DAEMON": "4242", + "PRC_REPO_REMOTES": "", + "PRC_UNITS_EXPECTED_DISABLED": "", "PRC_CHRONY_CONF": chrony_conf}) env.pop("PRC_NTP_SOURCES", None) return subprocess.run(["sh", CHECK], capture_output=True, text=True, @@ -177,7 +195,7 @@ class NtpBootstrap(unittest.TestCase): r = self._run_real_probe(conf) if "no NTP implementation is active" in r.stdout: self.skipTest("no chronyd on this host — the reader branch can't run") - self.assertIn("check 6/6: NTP bootstrap — ok", r.stdout) + self.assertIn("check 6/8: NTP bootstrap — ok", r.stdout) def test_confdir_naming_an_empty_directory_is_not_a_pass(self): # confdir present, nothing behind it: the sources are the hostname-only @@ -199,7 +217,255 @@ class NtpBootstrap(unittest.TestCase): env.pop("PRC_NTP_SOURCES", None) r = subprocess.run(["sh", CHECK], capture_output=True, text=True, timeout=30, env=env) - self.assertIn("check 6/6: NTP bootstrap", r.stdout) + self.assertIn("check 6/8: NTP bootstrap", r.stdout) + + +class IdleDaemon(unittest.TestCase): + """Check 7 — whether anything still triggers idle lock and suspend. + + A laptop that never sleeps has no symptom until the battery is gone, which + is why this needs a check rather than trusting the desktop to look right. + On velox 2026-08-19 hypridle started cleanly at 15:29:48 and `settings + restore` killed it six seconds later, replaying a caffeine stored in an + earlier boot. The machine then ran 11h40m fully awake on battery, died when + it flattened, and reset its RTC — which took DNS down with it, the very + deadlock check 6 exists for. Nothing looked wrong at any point. + + The check is behavioural: it asks whether the daemon is alive, not why it + might not be, so a stale caffeine, a crash and a bad config all surface the + same way. + """ + + # --- Normal cases --------------------------------------------------- + + def test_running_daemon_is_clean(self): + r = run_check(idle_daemon="4242") + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + self.assertNotIn("DEVIATION", r.stdout) + + def test_installed_but_not_running_flags(self): + r = run_check(idle_daemon="") + self.assertEqual(r.returncode, 1) + self.assertIn("hypridle", r.stdout) + self.assertIn("DEVIATION", r.stdout) + + def test_the_finding_names_the_consequence_not_just_the_process(self): + # "hypridle is not running" reads as a detail. The reason it matters is + # that the machine stays awake until the battery is gone, and that is + # what has to be in the line someone skims at 1am. + r = run_check(idle_daemon="") + self.assertIn("awake", r.stdout.lower()) + + def test_the_finding_names_the_known_cause(self): + # Behavioural checks are cheap to write and expensive to act on. Naming + # the one cause already seen saves the reader the investigation this + # session had to do from scratch. + r = run_check(idle_daemon="") + self.assertIn("caffeine", r.stdout.lower()) + + # --- Boundary cases ------------------------------------------------- + + def test_not_installed_is_not_a_finding(self): + # A headless or dwm machine never installs hypridle — archsetup pulls + # it in only for Hyprland. Flagging its absence there would be noise on + # every run, and noise is how a real finding gets skimmed past. + r = run_check(idle_daemon="MISSING") + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + self.assertNotIn("DEVIATION", r.stdout) + + def test_several_pids_still_read_as_running(self): + # pgrep prints one pid per line. More than one is its own problem (five + # concurrent daemons wedged a velox session on 2026-07-22) but it is + # not *this* check's, and it must not read as "not running". + r = run_check(idle_daemon="4242\n4243") + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + + def test_the_check_always_prints_its_line(self): + for pids in ("4242", "", "MISSING"): + with self.subTest(pids=pids): + self.assertIn("idle daemon", + run_check(idle_daemon=pids).stdout.lower()) + + +class UnitsExpectedDisabled(unittest.TestCase): + """Check 2 — units nothing intends to enable on this machine. + + "Enabled" is the check's proxy for "will actually run", and the proxy is + wrong for a unit nobody means to enable here. velox carries four such + units, for four different reasons: geoclue-agent is redundant because + hyprland's exec-once starts the binary directly, emacs is started on demand + by emacsclient, obs-record-watchdog only matters while recording, and + obsbot-wb-guard needs an OBSBOT the machine doesn't have. + + Left unexempted they report at every run, which is the standing-findings + problem check 4's own comment already argues against: four permanent lines + in front of every real one teach you to skim the output. + + The exemption is machine-local rather than a marker in the shared unit + file, because obsbot-wb-guard is correctly ENABLED on ratio. Same unit, + different right answer per machine. + """ + + # --- Normal cases --------------------------------------------------- + + def test_an_exempt_unit_is_not_flagged(self): + r = run_check(unit_states="emacs.service linked", + units_expected_disabled="emacs.service") + self.assertEqual(r.returncode, 0, r.stdout) + self.assertNotIn("emacs.service", r.stdout) + + def test_a_non_exempt_unit_still_flags(self): + r = run_check(unit_states="roam-sync.timer linked", + units_expected_disabled="emacs.service") + self.assertEqual(r.returncode, 1) + self.assertIn("roam-sync.timer", r.stdout) + + def test_several_exemptions_all_apply(self): + r = run_check( + unit_states=("emacs.service linked\n" + "geoclue-agent.service linked\n" + "obsbot-wb-guard.service linked"), + units_expected_disabled=("emacs.service\n" + "geoclue-agent.service\n" + "obsbot-wb-guard.service")) + self.assertEqual(r.returncode, 0, r.stdout) + + # --- Boundary cases ------------------------------------------------- + + def test_an_exemption_that_is_actually_enabled_is_a_finding(self): + # A stale exemption must surface rather than sit there suppressing + # nothing. Otherwise the list rots into a place real findings go to + # die, which is worse than the noise it was added to remove. + r = run_check(unit_states="obsbot-wb-guard.service enabled", + units_expected_disabled="obsbot-wb-guard.service") + self.assertEqual(r.returncode, 1) + self.assertIn("obsbot-wb-guard.service", r.stdout) + + def test_comments_and_blank_lines_are_ignored(self): + # The reason a unit is exempt is the most useful thing about the + # entry, so the format has to hold a comment next to it. + r = run_check(unit_states="emacs.service linked", + units_expected_disabled=("# started on demand\n" + "\n" + "emacs.service # not by systemd\n")) + self.assertEqual(r.returncode, 0, r.stdout) + + def test_no_exemptions_flags_everything_as_before(self): + r = run_check(unit_states="emacs.service linked", + units_expected_disabled="") + self.assertEqual(r.returncode, 1) + self.assertIn("emacs.service", r.stdout) + + def test_an_exemption_does_not_suppress_a_dangling_link(self): + # A stowed unit pointing at a missing target is a different finding, + # decided on the filesystem. Exempting the name must not hide that. + d = tempfile.mkdtemp(prefix="prc-units-") + self.addCleanup(shutil.rmtree, d, True) + unit_dir = os.path.join(d, "systemd", "user") + os.makedirs(unit_dir) + link = os.path.join(unit_dir, "emacs.service") + os.symlink(os.path.join(d, "gone.service"), link) + env = dict(os.environ) + env.update({"PRC_FAILED_UNITS": "", "PRC_LOCAL_SCAN_ROOTS": "", + "PRC_PROJECT_ROOTS": "", + "PRC_SIGNAL_ACCOUNTS": "+15045551234", + "PRC_NTP_SOURCES": "162.159.200.1", + "PRC_IDLE_DAEMON": "4242", + "PRC_REPO_REMOTES": "", + "PRC_UNITS_EXPECTED_DISABLED": "emacs.service", + "XDG_CONFIG_HOME": d}) + env.pop("PRC_UNIT_STATES", None) + r = subprocess.run(["sh", CHECK], capture_output=True, text=True, + timeout=30, env=env) + self.assertIn("points at a missing target", r.stdout) + self.assertEqual(r.returncode, 1) + + +class RepoPushCapability(unittest.TestCase): + """Check 8 — a working repo cloned from the read-only endpoint. + + archsetup clones the user's own archsetup and dotfiles from + https://git.cjennings.net/..., the anonymous read-only endpoint. That is + the right default for a stranger installing archsetup, who has no key on + the server, and the wrong one for this machine, which has to push. The + override exists (ARCHSETUP_REPO / DOTFILES_REPO) but only applies where it + is configured — a curl|bash install, or a rebuild from a stock ISO, picks + the default straight back up. + + Nothing about the tree shows it. The clone is complete and ordinary, and + the machine finds out at the first push, with a 403. That is how velox's + dotfiles remote was found on 2026-08-17, four days after its rebuild. + + Only the read-only endpoint is flagged. An https remote to some other host + may well be pushable with a credential helper, and guessing about hosts + this machine does not own would put standing noise in front of the real + findings. + """ + + RO = "https://git.cjennings.net/dotfiles.git" + RW = "git@cjennings.net:dotfiles.git" + + # --- Normal cases --------------------------------------------------- + + def test_an_ssh_remote_is_clean(self): + r = run_check(repo_remotes=f"/home/x/.dotfiles {self.RW}") + self.assertEqual(r.returncode, 0, r.stdout) + self.assertNotIn("DEVIATION", r.stdout) + + def test_the_read_only_endpoint_flags(self): + r = run_check(repo_remotes=f"/home/x/.dotfiles {self.RO}") + self.assertEqual(r.returncode, 1) + self.assertIn("/home/x/.dotfiles", r.stdout) + + def test_the_finding_names_the_consequence(self): + # "the remote is https" is a detail. That pushing fails is the point. + r = run_check(repo_remotes=f"/home/x/.dotfiles {self.RO}") + self.assertIn("push", r.stdout.lower()) + + def test_every_offending_repo_is_named(self): + r = run_check(repo_remotes=(f"/home/x/.dotfiles {self.RO}\n" + f"/home/x/code/archsetup {self.RO}")) + self.assertIn("/home/x/.dotfiles", r.stdout) + self.assertIn("/home/x/code/archsetup", r.stdout) + + # --- Boundary cases ------------------------------------------------- + + def test_a_mixed_set_flags_only_the_read_only_one(self): + r = run_check(repo_remotes=(f"/home/x/.dotfiles {self.RW}\n" + f"/home/x/code/archsetup {self.RO}")) + self.assertEqual(r.returncode, 1) + self.assertIn("/home/x/code/archsetup", r.stdout) + self.assertNotIn("/home/x/.dotfiles", r.stdout) + + def test_an_https_remote_to_another_host_is_not_flagged(self): + # GitHub over https is pushable with a credential helper. Flagging it + # would be a guess about a host this machine does not own. + r = run_check(repo_remotes="/home/x/code/thing https://github.com/a/b.git") + self.assertEqual(r.returncode, 0, r.stdout) + + def test_no_repos_is_not_a_finding(self): + r = run_check(repo_remotes="") + self.assertEqual(r.returncode, 0, r.stdout) + + def test_blank_lines_are_ignored(self): + r = run_check(repo_remotes=f"\n\n/home/x/.dotfiles {self.RW}\n\n") + self.assertEqual(r.returncode, 0, r.stdout) + + def test_the_check_always_prints_its_line(self): + for remotes in ("", f"/home/x/.dotfiles {self.RW}", + f"/home/x/.dotfiles {self.RO}"): + with self.subTest(remotes=remotes): + self.assertIn("repo remotes", + run_check(repo_remotes=remotes).stdout.lower()) + + # --- Error cases ---------------------------------------------------- + + def test_a_repo_with_no_origin_is_a_finding(self): + # Fails closed. A repo whose origin could not be read was not checked, + # and reporting it clean is the false pass this script exists to avoid. + r = run_check(repo_remotes="/home/x/.dotfiles") + self.assertEqual(r.returncode, 1) + self.assertIn("/home/x/.dotfiles", r.stdout) class AllClean(unittest.TestCase): @@ -629,11 +895,31 @@ class SignalAccount(unittest.TestCase): "PRC_LOCAL_SCAN_ROOTS": "", "PRC_PROJECT_ROOTS": "", "PRC_NTP_SOURCES": "162.159.200.1", "PRC_SIGNAL_ACCOUNTS": "+15045551234", + "PRC_IDLE_DAEMON": "4242", + "PRC_REPO_REMOTES": "", + "PRC_UNITS_EXPECTED_DISABLED": "", "signal_missing": "1"}) r = subprocess.run(["sh", CHECK], capture_output=True, text=True, timeout=30, env=env) self.assertEqual(r.returncode, 0, r.stdout) + def test_a_stray_idle_absent_in_the_environment_is_ignored(self): + # Same class as the flag above, and the dangerous direction: an + # inherited idle_absent=1 would make check 7 skip silently and report + # ok on a machine that cannot sleep, which is the exact false pass the + # check exists to prevent. + env = dict(os.environ) + env.update({"PRC_FAILED_UNITS": "", "PRC_UNIT_STATES": "", + "PRC_LOCAL_SCAN_ROOTS": "", "PRC_PROJECT_ROOTS": "", + "PRC_NTP_SOURCES": "162.159.200.1", + "PRC_SIGNAL_ACCOUNTS": "+15045551234", + "PRC_IDLE_DAEMON": "", + "idle_absent": "1"}) + r = subprocess.run(["sh", CHECK], capture_output=True, text=True, + timeout=30, env=env) + self.assertEqual(r.returncode, 1, r.stdout) + self.assertIn("hypridle is installed but not running", r.stdout) + class ProbeFailure(unittest.TestCase): """A probe that could not run must never read as a clean check. @@ -680,6 +966,9 @@ class ProbeFailure(unittest.TestCase): "PRC_UNIT_STATES": "", "PRC_LOCAL_SCAN_ROOTS": "", "PRC_PROJECT_ROOTS": "", "PRC_SIGNAL_ACCOUNTS": "+15045551234", + "PRC_IDLE_DAEMON": "4242", + "PRC_REPO_REMOTES": "", + "PRC_UNITS_EXPECTED_DISABLED": "", "TMPDIR": "/nonexistent-tmp-dir"}) r = subprocess.run(["sh", CHECK], capture_output=True, text=True, timeout=30, env=env) @@ -701,6 +990,9 @@ class RealUnitDirEnumeration(unittest.TestCase): env.update({"PRC_FAILED_UNITS": "", "PRC_LOCAL_SCAN_ROOTS": "", "PRC_PROJECT_ROOTS": "", "PRC_SIGNAL_ACCOUNTS": "+15045551234", + "PRC_IDLE_DAEMON": "4242", + "PRC_REPO_REMOTES": "", + "PRC_UNITS_EXPECTED_DISABLED": "", "XDG_CONFIG_HOME": config_home}) env.pop("PRC_UNIT_STATES", None) return subprocess.run(["sh", CHECK], capture_output=True, text=True, @@ -754,6 +1046,9 @@ class WedgedSystemctl(unittest.TestCase): env = dict(os.environ) env.update({"PRC_LOCAL_SCAN_ROOTS": "", "PRC_PROJECT_ROOTS": "", "PRC_SIGNAL_ACCOUNTS": "+15045551234", + "PRC_IDLE_DAEMON": "4242", + "PRC_REPO_REMOTES": "", + "PRC_UNITS_EXPECTED_DISABLED": "", "PRC_SYSTEMCTL": fake, "PRC_SYSTEMCTL_TIMEOUT": timeout_s, "XDG_CONFIG_HOME": d}) @@ -771,7 +1066,7 @@ class WedgedSystemctl(unittest.TestCase): self.assertEqual(r.returncode, 1) self.assertIn("could not query user units", r.stdout) # The run must reach the end rather than stopping at the first call. - self.assertIn("check 6/6", r.stdout) + self.assertIn("check 8/8", r.stdout) def test_a_hanging_systemctl_does_not_stall_the_whole_run(self): # The fake sleeps 8s against a 1s bound, so a bounded run lands near @@ -795,7 +1090,7 @@ class Reporting(unittest.TestCase): unit_states="c.timer disabled") self.assertEqual(r.returncode, 1) summary = r.stdout.strip().splitlines()[-1] - self.assertEqual(summary, "3 finding(s) across 6 checks") + self.assertEqual(summary, "3 finding(s) across 8 checks") def test_the_summary_count_tracks_every_check(self): # One finding from each of the five, so a counter that drops or @@ -811,7 +1106,7 @@ class Reporting(unittest.TestCase): local_roots=scan, project_roots=proj, signal_accounts="") summary = r.stdout.strip().splitlines()[-1] - self.assertEqual(summary, "5 finding(s) across 6 checks") + self.assertEqual(summary, "5 finding(s) across 8 checks") def test_help_exits_zero(self): r = subprocess.run(["sh", CHECK, "--help"], |
