#!/usr/bin/python3 """Tests for felhom-crash-guard (`11` §5.9). Temp dirs only; nothing real is touched. Red-proof seam: CRASHGUARD_UNDER_TEST.""" import importlib.machinery import importlib.util import json import os import pathlib import tempfile import unittest HERE = pathlib.Path(__file__).resolve().parent _loader = importlib.machinery.SourceFileLoader("crashguard", os.environ.get("CRASHGUARD_UNDER_TEST", str(HERE / "felhom-crash-guard"))) _spec = importlib.util.spec_from_loader("crashguard", _loader) cg = importlib.util.module_from_spec(_spec) _loader.exec_module(cg) T0 = 1791115200.0 # 2026-10-04T12:00:00Z class FakeEnv(cg.Env): def __init__(self, d): super().__init__(conf=os.path.join(d, "conf"), state_dir=os.path.join(d, "state"), panic_path=os.path.join(d, "panic"), uptime_path=os.path.join(d, "uptime"), boot_id_path=os.path.join(d, "bootid")) self.t = T0 self.logs = [] open(self.panic_path, "w").write("0\n") open(self.uptime_path, "w").write("20.00 10.00\n") def now(self): return self.t def log(self, line): self.logs.append(line) def panic(self): return int(open(self.panic_path).read()) def state(self): return json.load(open(os.path.join(self.state_dir, "state.json"))) class Guard(unittest.TestCase): def setUp(self): self.d = tempfile.TemporaryDirectory() self.e = FakeEnv(self.d.name) def tearDown(self): self.d.cleanup() def crash_boot(self, minutes_later): self.e.t += minutes_later * 60 cg.main(["x", "boot"], self.e) # no clean-stop before it: an unclean stop def clean_reboot(self, minutes_later): cg.main(["x", "clean-stop"], self.e) self.e.t += minutes_later * 60 cg.main(["x", "boot"], self.e) def test_first_boot_is_not_a_crash_and_arms(self): cg.main(["x", "boot"], self.e) s = self.e.state() self.assertFalse(s["last_boot_unclean"]) self.assertEqual(self.e.panic(), 10) self.assertTrue(s["armed"]) def test_clean_reboots_never_count(self): cg.main(["x", "boot"], self.e) for _ in range(5): self.clean_reboot(1) s = self.e.state() self.assertEqual(s["unclean_boots_in_window"], 0) self.assertEqual(self.e.panic(), 10) def test_third_crash_in_an_hour_leaves_the_box_off(self): # operator's words: "if it crashes 3 times within one hour, it stays off" — after crash 2 the guard trips, # so crash 3 (kernel.panic = 0) does not restart the box. cg.main(["x", "boot"], self.e) self.crash_boot(5) self.assertEqual(self.e.panic(), 10, "one crash: still restarts") self.crash_boot(5) s = self.e.state() self.assertTrue(s["tripped"], s) self.assertEqual(self.e.panic(), 0, "after the 2nd crash boot the 3rd crash must leave the box off") self.assertIn("2 unclean boots within 60 minutes", s["tripped_reason"]) def test_crashes_spread_over_more_than_the_window_do_not_trip(self): cg.main(["x", "boot"], self.e) self.crash_boot(5) self.crash_boot(61) self.assertFalse(self.e.state()["tripped"]) self.assertEqual(self.e.panic(), 10) def test_tripped_stays_tripped_across_boots(self): cg.main(["x", "boot"], self.e) self.crash_boot(5) self.crash_boot(5) self.clean_reboot(30) # the operator switched it on; even a clean boot keeps the trip self.assertTrue(self.e.state()["tripped"]) self.assertEqual(self.e.panic(), 0) def test_rearms_after_24h_of_normal_running(self): cg.main(["x", "boot"], self.e) self.crash_boot(5) self.crash_boot(5) self.e.t += 23 * 3600 cg.main(["x", "check"], self.e) self.assertTrue(self.e.state()["tripped"], "not before 24 h") self.e.t += 3600 cg.main(["x", "check"], self.e) s = self.e.state() self.assertFalse(s["tripped"]) self.assertEqual(self.e.panic(), 10) self.assertIn("timer", s["rearmed_by"]) def test_operator_rearm_starts_a_fresh_window(self): cg.main(["x", "boot"], self.e) self.crash_boot(5) self.crash_boot(5) self.e.t += 60 cg.main(["x", "rearm"], self.e) s = self.e.state() self.assertFalse(s["tripped"]) self.assertEqual(s["rearmed_by"], "operator") self.assertEqual(s["unclean_boots_24h"], 2, "the history stays") self.crash_boot(5) self.assertFalse(self.e.state()["tripped"], "one crash after a re-arm must not trip at once") def test_config_numbers_are_read(self): open(self.e.conf, "w").write("LIMIT=2\nPANIC_SECONDS=30\n") cg.main(["x", "boot"], self.e) self.assertEqual(self.e.panic(), 30) self.crash_boot(1) self.assertTrue(self.e.state()["tripped"], "LIMIT=2: the first crash boot trips") def test_state_is_world_readable_for_the_agent(self): cg.main(["x", "boot"], self.e) mode = os.stat(os.path.join(self.e.state_dir, "state.json")).st_mode & 0o777 self.assertEqual(mode, 0o644) if __name__ == "__main__": unittest.main()