tests/test_numinit_order.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("\\", "/")
def unit(name, requires=None, after=None, exec_="true"):
return {"name": name, "kind": "oneshot", "restart": "never",
"backoff_ms": 0, "backoff_max_ms": 0, "arch_mask": [],
"health_probe": None, "requires": requires or [],
"after": after or [], "exec": exec_}
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 sh(snippet, state_path):
full = 'NUMOS_SOURCE_ONLY=1 . "%s"; numos_load_state "%s"; %s' % (
NUMINIT, state_path, snippet)
env = dict(os.environ)
env["NUMOS_SOURCE_ONLY"] = "1"
env["NUMOS_DRY_RUN"] = "1"
proc = subprocess.run(["bash", "-c", full], capture_output=True, text=True, env=env)
return proc.returncode, proc.stdout.strip(), proc.stderr.strip()
class TestPhaseOrdering(unittest.TestCase):
def test_phases_run_in_ordinal_order_not_file_order(self):
path = state_file(
[unit("a"), unit("b")],
[{"ordinal": 30, "name": "late", "on_failure": "halt", "required_units": ["b"]},
{"ordinal": 10, "name": "early", "on_failure": "halt", "required_units": ["a"]}])
try:
_, out, _ = sh("numos_phase_ordinals", path)
self.assertEqual(out.split(), ["10", "30"])
finally:
os.unlink(path)
class TestDagResolution(unittest.TestCase):
def test_requires_edge_orders_dependency_first(self):
path = state_file([unit("a"), unit("b", requires=["a"])],
[{"ordinal": 10, "name": "p", "on_failure": "halt",
"required_units": ["b"]}])
try:
_, out, _ = sh("numos_resolve_order b", path)
self.assertEqual(out.split(), ["a", "b"])
finally:
os.unlink(path)
def test_after_edge_orders_without_pulling_in_failure(self):
path = state_file([unit("a"), unit("b", after=["a"])],
[{"ordinal": 10, "name": "p", "on_failure": "halt",
"required_units": ["b"]}])
try:
_, out, _ = sh("numos_resolve_order b", path)
self.assertEqual(out.split(), ["a", "b"])
finally:
os.unlink(path)
def test_transitive_chain_fully_ordered(self):
path = state_file(
[unit("a"), unit("b", requires=["a"]), unit("c", requires=["b"])],
[{"ordinal": 10, "name": "p", "on_failure": "halt", "required_units": ["c"]}])
try:
_, out, _ = sh("numos_resolve_order c", path)
self.assertEqual(out.split(), ["a", "b", "c"])
finally:
os.unlink(path)
def test_diamond_emits_each_unit_once(self):
path = state_file(
[unit("a"), unit("b", requires=["a"]), unit("c", requires=["a"]),
unit("d", requires=["b", "c"])],
[{"ordinal": 10, "name": "p", "on_failure": "halt", "required_units": ["d"]}])
try:
_, out, _ = sh("numos_resolve_order d", path)
names = out.split()
self.assertEqual(len(names), 4)
self.assertEqual(names[0], "a")
self.assertEqual(names[-1], "d")
finally:
os.unlink(path)
class TestUnitFieldExtraction(unittest.TestCase):
def test_exec_with_spaces_extracted_whole(self):
path = state_file([unit("a", exec_="mount -t proc proc /proc")],
[{"ordinal": 10, "name": "p", "on_failure": "halt",
"required_units": ["a"]}])
try:
_, out, _ = sh("numos_unit_field a exec", path)
self.assertEqual(out, "mount -t proc proc /proc")
finally:
os.unlink(path)
class TestDryRun(unittest.TestCase):
def test_phase_reports_each_unit_in_order(self):
path = state_file([unit("a"), unit("b", requires=["a"])],
[{"ordinal": 10, "name": "p", "on_failure": "halt",
"required_units": ["b"]}])
try:
_, out, _ = sh("numos_run_phase 10", path)
self.assertEqual([l for l in out.split("\n") if l.startswith("RUN ")],
["RUN a", "RUN b"])
finally:
os.unlink(path)
class TestFailClosedHalting(unittest.TestCase):
# These halts are all reached through a command substitution
# ($(numos_resolve_order ...)) inside numos_run_phase. `exit` inside a
# substitution only kills the subshell, so a caller that never checks the
# substitution's status would carry on and return 0 -- a silent no-op
# instead of a halt. Assert on exit status, not just stderr content: a
# broken (status-blind) implementation prints the same HALT text.
def test_dependency_cycle_halts_nonzero_through_run_phase(self):
path = state_file(
[unit("a", requires=["b"]), unit("b", requires=["a"])],
[{"ordinal": 10, "name": "p", "on_failure": "halt", "required_units": ["a"]}])
try:
rc, _, err = sh("numos_run_phase 10", path)
self.assertNotEqual(rc, 0)
self.assertIn("numos: HALT:", err)
finally:
os.unlink(path)
def test_dangling_requires_halts_nonzero_through_run_phase(self):
path = state_file(
[unit("a", requires=["ghost"])],
[{"ordinal": 10, "name": "p", "on_failure": "halt", "required_units": ["a"]}])
try:
rc, _, err = sh("numos_run_phase 10", path)
self.assertNotEqual(rc, 0)
self.assertIn("numos: HALT:", err)
finally:
os.unlink(path)
if __name__ == "__main__":
unittest.main()