tests/test_state.py
back to source
import unittest
from numos.state import render, parse, verify, StateError
SAMPLE = {
"version": 1,
"arch_targets": [
{"canonical": "x86_64", "aliases": ["x86_64", "amd64"],
"zig_triple": "x86_64-linux-musl", "busybox_variant": "busybox-x86_64",
"artifact_sha256": "ab" * 32},
{"canonical": "riscv64", "aliases": ["riscv64"],
"zig_triple": None, "busybox_variant": "busybox-riscv64",
"artifact_sha256": None},
],
"boot_phases": [
{"ordinal": 20, "name": "net", "on_failure": "degrade", "required_units": ["ip-up"]},
{"ordinal": 10, "name": "mount", "on_failure": "halt", "required_units": ["mount-proc"]},
],
"units": [
{"name": "mount-proc", "kind": "oneshot", "restart": "never",
"backoff_ms": 0, "backoff_max_ms": 0, "arch_mask": [], "health_probe": None,
"requires": [], "after": [], "exec": "mount -t proc proc /proc"},
{"name": "ip-up", "kind": "oneshot", "restart": "on-failure",
"backoff_ms": 100, "backoff_max_ms": 800, "arch_mask": [], "health_probe": None,
"requires": ["mount-proc"], "after": [], "exec": "ip link set eth0 up"},
],
"health": [
{"name": "disk", "interval_s": 30, "threshold": 3, "local_action": "degrade-node",
"on_fire": None, "probe": "df -P / | awk 'NR==2 && $5+0 < 95'"},
],
}
def _sorted_state(state):
"""Return a copy of state with all lists sorted in render order."""
import copy
s = copy.deepcopy(state)
s["arch_targets"].sort(key=lambda a: "A %s" % a["canonical"])
s["boot_phases"].sort(key=lambda p: "P %d" % p["ordinal"])
s["units"].sort(key=lambda u: "U %s" % u["name"])
s["health"].sort(key=lambda h: "X %s" % h["name"])
return s
class TestRender(unittest.TestCase):
def test_blocks_appear_in_order(self):
tags = [ln[0] for ln in render(SAMPLE).strip().split("\n")]
self.assertEqual(tags, sorted(tags, key="VCAPUX".index))
def test_null_fields_render_as_dash(self):
line = [ln for ln in render(SAMPLE).split("\n") if ln.startswith("A riscv64")][0]
self.assertEqual(line, "A riscv64 riscv64 - busybox-riscv64 -")
def test_empty_lists_render_as_dash(self):
line = [ln for ln in render(SAMPLE).split("\n") if ln.startswith("U mount-proc")][0]
self.assertTrue(line.endswith("- - - - mount -t proc proc /proc"))
def test_records_are_sorted_within_block(self):
phases = [ln for ln in render(SAMPLE).split("\n") if ln.startswith("P ")]
self.assertEqual(phases[0], "P 10 mount halt mount-proc")
def test_render_is_deterministic(self):
self.assertEqual(render(SAMPLE), render(SAMPLE))
def test_c_line_is_stamped(self):
self.assertEqual(len([ln for ln in render(SAMPLE).split("\n") if ln.startswith("C ")]), 1)
class TestRoundTrip(unittest.TestCase):
def test_parse_inverts_render(self):
self.assertEqual(parse(render(SAMPLE)), _sorted_state(SAMPLE))
def test_exec_with_spaces_survives(self):
unit = [u for u in parse(render(SAMPLE))["units"] if u["name"] == "ip-up"][0]
self.assertEqual(unit["exec"], "ip link set eth0 up")
def test_probe_with_spaces_and_quotes_survives(self):
probe = parse(render(SAMPLE))["health"][0]["probe"]
self.assertEqual(probe, "df -P / | awk 'NR==2 && $5+0 < 95'")
class TestVerify(unittest.TestCase):
def test_good_state_verifies(self):
verify(render(SAMPLE))
def test_tampered_body_raises(self):
bad = render(SAMPLE).replace("busybox-riscv64", "busybox-EVIL")
with self.assertRaises(StateError):
verify(bad)
def test_missing_c_line_raises(self):
bad = "\n".join(ln for ln in render(SAMPLE).split("\n") if not ln.startswith("C "))
with self.assertRaises(StateError):
verify(bad)
if __name__ == "__main__":
unittest.main()