NumericalOS

tests/test_export.py

back to source

import json
import os
import shutil
import tempfile
import unittest

from numos.export_state import build_state, export
from numos.state import parse, verify
from numos.validate import validate, ValidationError

OPS = ["SwarmExecutor_v1", "ObserverChain_v1", "MetaGraphSync_v1"]


class TestBuildState(unittest.TestCase):
    def test_every_op_becomes_a_unit(self):
        names = [u["name"] for u in build_state(OPS)["units"]]
        for op in OPS:
            self.assertIn("op-" + op.lower().replace("_", "-"), names)

    def test_infra_units_are_present_alongside_ops(self):
        names = [u["name"] for u in build_state(OPS)["units"]]
        self.assertIn("mount-proc", names)

    def test_op_units_are_longrun_and_restart_on_failure(self):
        unit = [u for u in build_state(OPS)["units"]
                if u["name"] == "op-swarmexecutor-v1"][0]
        self.assertEqual(unit["kind"], "longrun")
        self.assertEqual(unit["restart"], "on-failure")

    def test_op_units_depend_on_join(self):
        unit = [u for u in build_state(OPS)["units"]
                if u["name"] == "op-swarmexecutor-v1"][0]
        self.assertIn("join", unit["requires"])

    def test_built_state_passes_validation(self):
        validate(build_state(OPS))

    def test_all_nine_arch_targets_included(self):
        self.assertEqual(len(build_state(OPS)["arch_targets"]), 9)

    def test_op_name_with_a_space_is_refused_at_export(self):
        # The op name comes over HTTP from the documented --ops-url CLI, and
        # unit_name_for_op passes the space straight through into the unit
        # name, shifting every later field of the U record.
        with self.assertRaises(ValidationError) as ctx:
            build_state(["Bad Op_v1"])
        self.assertIn("op-bad op-v1", str(ctx.exception))


class TestExport(unittest.TestCase):
    def setUp(self):
        self.out = tempfile.mkdtemp()

    def tearDown(self):
        shutil.rmtree(self.out, ignore_errors=True)

    def test_writes_both_artifacts(self):
        export(OPS, self.out)
        self.assertTrue(os.path.exists(os.path.join(self.out, "numos.state")))
        self.assertTrue(os.path.exists(os.path.join(self.out, "numos.state.json")))

    def test_written_state_self_verifies(self):
        export(OPS, self.out)
        with open(os.path.join(self.out, "numos.state")) as handle:
            verify(handle.read())

    def test_export_is_byte_identical_across_runs(self):
        export(OPS, self.out)
        with open(os.path.join(self.out, "numos.state"), "rb") as handle:
            first = handle.read()
        export(OPS, self.out)
        with open(os.path.join(self.out, "numos.state"), "rb") as handle:
            second = handle.read()
        self.assertEqual(first, second)

    def test_op_order_does_not_change_output(self):
        export(OPS, self.out)
        with open(os.path.join(self.out, "numos.state"), "rb") as handle:
            first = handle.read()
        export(list(reversed(OPS)), self.out)
        with open(os.path.join(self.out, "numos.state"), "rb") as handle:
            second = handle.read()
        self.assertEqual(first, second)

    def test_json_view_round_trips_to_the_same_state(self):
        export(OPS, self.out)
        with open(os.path.join(self.out, "numos.state")) as handle:
            from_lines = parse(handle.read())
        with open(os.path.join(self.out, "numos.state.json")) as handle:
            from_json = json.load(handle)
        self.assertEqual(from_lines["units"], from_json["units"])

    def test_state_uses_lf_endings_only(self):
        export(OPS, self.out)
        with open(os.path.join(self.out, "numos.state"), "rb") as handle:
            self.assertNotIn(b"\r", handle.read())


if __name__ == "__main__":
    unittest.main()