tests/test_numinit_failure.py
back to source
import os
import subprocess
import tempfile
import unittest
from numos.state import render
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
NUMINIT = os.path.join(ROOT, "boot", "numinit.sh").replace("\\", "/")
# A later phase's unit prints this. Nothing else in numinit's output
# contains it, so its presence or absence is the only direct evidence of
# whether the later phase actually ran. Asserting on the unit's *name*
# proves nothing: numos_run_unit prints nothing outside dry run, so the
# name can never appear on stdout whether the phase ran or not.
LATER_MARKER = "NUMOS_LATER_PHASE_RAN"
def unit(name, exec_="true"):
return {"name": name, "kind": "oneshot", "restart": "never",
"backoff_ms": 0, "backoff_max_ms": 0, "arch_mask": [],
"health_probe": None, "requires": [], "after": [], "exec": exec_}
def marker_unit(name="later"):
return unit(name, exec_="echo %s" % LATER_MARKER)
def state_file(units, phases):
text = render({"version": 1, "arch_targets": [], "units": units,
"boot_phases": phases, "health": []})
handle = tempfile.NamedTemporaryFile("w", suffix=".numos", delete=False, newline="\n")
handle.write(text)
handle.close()
return handle.name.replace("\\", "/")
def run_main(state_path):
"""Run numinit end to end for real (no dry run) so failing units fail."""
env = dict(os.environ)
env["NUMOS_STATE"] = state_path
env.pop("NUMOS_DRY_RUN", None)
proc = subprocess.run(["bash", NUMINIT], capture_output=True, text=True, env=env)
return proc.returncode, proc.stdout.strip(), proc.stderr.strip()
class TestHaltMode(unittest.TestCase):
def test_halt_phase_failure_stops_boot_with_named_reason(self):
path = state_file([unit("boom", exec_="false"), marker_unit()],
[{"ordinal": 10, "name": "critical", "on_failure": "halt",
"required_units": ["boom"]},
{"ordinal": 20, "name": "after", "on_failure": "continue",
"required_units": ["later"]}])
try:
code, out, err = run_main(path)
self.assertNotEqual(code, 0)
self.assertIn("numos: HALT:", err)
self.assertIn("critical", err)
# The halt has to actually stop the walk, not just print.
self.assertNotIn(LATER_MARKER, out)
self.assertNotIn("boot complete", out)
finally:
os.unlink(path)
def test_the_marker_appears_when_the_later_phase_does_run(self):
"""Positive control for the assertion above.
Without this, assertNotIn(LATER_MARKER) would be indistinguishable
from asserting on a string the program can never emit -- which is
exactly the defect this test replaced. Same state, same later phase,
only the first phase's outcome differs.
"""
path = state_file([unit("fine", exec_="true"), marker_unit()],
[{"ordinal": 10, "name": "critical", "on_failure": "halt",
"required_units": ["fine"]},
{"ordinal": 20, "name": "after", "on_failure": "continue",
"required_units": ["later"]}])
try:
code, out, err = run_main(path)
self.assertEqual(code, 0, err)
self.assertIn(LATER_MARKER, out)
self.assertIn("boot complete", out)
finally:
os.unlink(path)
class TestDegradeMode(unittest.TestCase):
def test_degrade_continues_boot_and_marks_node(self):
path = state_file([unit("boom", exec_="false"), unit("later")],
[{"ordinal": 10, "name": "net", "on_failure": "degrade",
"required_units": ["boom"]},
{"ordinal": 20, "name": "after", "on_failure": "continue",
"required_units": ["later"]}])
try:
code, out, _ = run_main(path)
self.assertEqual(code, 0)
self.assertIn("numos: DEGRADED: net", out)
self.assertIn("numos: boot complete degraded=1", out)
finally:
os.unlink(path)
class TestContinueMode(unittest.TestCase):
def test_continue_logs_but_does_not_degrade(self):
path = state_file([unit("boom", exec_="false")],
[{"ordinal": 10, "name": "clock", "on_failure": "continue",
"required_units": ["boom"]}])
try:
code, out, _ = run_main(path)
self.assertEqual(code, 0)
self.assertIn("numos: WARN: phase clock failed", out)
self.assertIn("degraded=0", out)
finally:
os.unlink(path)
class TestHappyPath(unittest.TestCase):
def test_all_phases_succeed_reports_not_degraded(self):
path = state_file([unit("a"), unit("b")],
[{"ordinal": 10, "name": "one", "on_failure": "halt",
"required_units": ["a"]},
{"ordinal": 20, "name": "two", "on_failure": "halt",
"required_units": ["b"]}])
try:
code, out, _ = run_main(path)
self.assertEqual(code, 0)
self.assertIn("numos: boot complete degraded=0", out)
finally:
os.unlink(path)
class TestUnknownPolicyHalts(unittest.TestCase):
def test_unknown_on_failure_value_halts_with_named_reason(self):
# This state file bypasses numos/validate.py's ON_FAILURE enum check
# (render() does not validate) to simulate numinit being handed a
# state file that never passed through the exporter's validation --
# numinit is PID 1 and must fail closed on its own, not rely on an
# upstream guarantee. The required unit fails so numos_run_phase
# returns nonzero and numos_handle_phase_failure's `*)` arm is
# actually reached.
path = state_file([unit("boom", exec_="false")],
[{"ordinal": 10, "name": "weird", "on_failure": "explode",
"required_units": ["boom"]}])
try:
code, out, err = run_main(path)
self.assertNotEqual(code, 0)
self.assertIn("numos: HALT:", err)
self.assertIn("weird", err)
finally:
os.unlink(path)
if __name__ == "__main__":
unittest.main()