mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-07 03:15:56 +00:00
`roles/<role>/files/<lang>/<rel>` is tested by
`tests/unit/<lang>/roles/<role>/files/<rel>`, renamed to that language's
convention. The language appears once, in `tests/unit/`, because the suite
already says what the file is. Without a fixed mapping a shipped script is
tested wherever its author happened to look, and nobody can tell an untested
file from one whose test lives somewhere unexpected.
Most of what a role ships is a script against a live console -- it boots a
Laravel kernel, imports frappe, or writes through an ActiveRecord model -- so
the escape is a `nocheck: mirrored-unit-test` comment in the file, with a
rationale of its own. A file carrying both a test and an exemption fails: the
exemption outlived its reason and reads as "untested" to everyone after you.
Five scripts turned out to have real units behind the console wrapper, so they
get tests rather than markers (62 cases):
* svc-net-tor/onion_self_reach.py -- pins that an HTTP error status, a closed
connection and a refused connection all count as reached, because each
proves the loopback fast path resolved; only a timeout or an unresolved
name is a failure.
* sys-lock/sys-lock.py -- `parse_time_to_seconds` unit precedence, and that a
service still activating holds the lock (otherwise the gate races it).
* svc-opt-keyboard-color/script.py -- colour interpolation and the
zero-length transition window that would otherwise divide by zero.
* sys-svc-compose/swarm/secret_rotate.py -- including the binary fallback a
TLS key needs, and that a missing source leaves the name alone rather than
pinning the hash of a file that is not there.
* sys-front-inj-logout/logout.js -- pins that `href="/logout"` alone does not
match and a non-interactive tag is never patched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
120 lines
4.1 KiB
Python
Executable File
120 lines
4.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import importlib.util
|
|
from unittest import TestCase, main, mock
|
|
|
|
from . import PROJECT_ROOT
|
|
|
|
|
|
def load_target_module():
|
|
script_path = (
|
|
PROJECT_ROOT / "roles" / "sys-lock" / "files" / "python" / "sys-lock.py"
|
|
)
|
|
|
|
if not script_path.is_file():
|
|
raise FileNotFoundError(f"Target script not found at: {script_path}")
|
|
|
|
spec = importlib.util.spec_from_file_location("sys_lock_script", script_path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
assert spec and spec.loader
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
SCRIPT = load_target_module()
|
|
|
|
|
|
class ParseTimeToSecondsTests(TestCase):
|
|
def test_bare_seconds(self):
|
|
self.assertEqual(SCRIPT.parse_time_to_seconds("45s"), 45)
|
|
|
|
def test_minutes_use_the_three_letter_unit(self):
|
|
self.assertEqual(SCRIPT.parse_time_to_seconds("30min"), 1800)
|
|
|
|
def test_hours(self):
|
|
self.assertEqual(SCRIPT.parse_time_to_seconds("1h"), 3600)
|
|
|
|
def test_the_longest_matching_unit_wins(self):
|
|
"""``min`` must not be read as the ``n``-less ``s`` or as bare digits."""
|
|
self.assertEqual(SCRIPT.parse_time_to_seconds("2min"), 120)
|
|
|
|
def test_an_unknown_unit_raises_rather_than_defaulting(self):
|
|
with self.assertRaises(ValueError):
|
|
SCRIPT.parse_time_to_seconds("10d")
|
|
|
|
def test_a_bare_number_raises_rather_than_assuming_seconds(self):
|
|
with self.assertRaises(ValueError):
|
|
SCRIPT.parse_time_to_seconds("60")
|
|
|
|
|
|
class FilterServicesTests(TestCase):
|
|
def test_it_drops_every_ignored_service(self):
|
|
self.assertEqual(
|
|
SCRIPT.filter_services(
|
|
["a.service", "b.service", "c.service"], ["b.service"]
|
|
),
|
|
["a.service", "c.service"],
|
|
)
|
|
|
|
def test_it_preserves_the_original_order(self):
|
|
self.assertEqual(
|
|
SCRIPT.filter_services(["c", "a", "b"], []),
|
|
["c", "a", "b"],
|
|
)
|
|
|
|
def test_ignoring_everything_leaves_nothing(self):
|
|
self.assertEqual(SCRIPT.filter_services(["a", "b"], ["a", "b"]), [])
|
|
|
|
|
|
class GetMaxAttemptsTests(TestCase):
|
|
def test_the_budget_is_the_timeout_divided_by_the_break(self):
|
|
self.assertEqual(SCRIPT.get_max_attempts(60), 60 // SCRIPT.BREAK_TIME_SECONDS)
|
|
|
|
def test_a_timeout_shorter_than_one_break_yields_no_attempts(self):
|
|
self.assertEqual(SCRIPT.get_max_attempts(SCRIPT.BREAK_TIME_SECONDS - 1), 0)
|
|
|
|
def test_the_result_is_a_whole_number_of_attempts(self):
|
|
self.assertIsInstance(SCRIPT.get_max_attempts(61), int)
|
|
|
|
|
|
class CheckServiceActiveTests(TestCase):
|
|
def _with_status(self, status):
|
|
completed = mock.Mock()
|
|
completed.stdout = status.encode("utf-8")
|
|
return mock.patch.object(SCRIPT.subprocess, "run", return_value=completed)
|
|
|
|
def test_active_counts_as_active(self):
|
|
with self._with_status("active"):
|
|
self.assertTrue(SCRIPT.check_service_active("x.service"))
|
|
|
|
def test_activating_also_counts_as_active(self):
|
|
"""A service still starting must hold the lock, or the gate races it."""
|
|
with self._with_status("activating"):
|
|
self.assertTrue(SCRIPT.check_service_active("x.service"))
|
|
|
|
def test_inactive_does_not(self):
|
|
with self._with_status("inactive"):
|
|
self.assertFalse(SCRIPT.check_service_active("x.service"))
|
|
|
|
def test_failed_does_not(self):
|
|
with self._with_status("failed"):
|
|
self.assertFalse(SCRIPT.check_service_active("x.service"))
|
|
|
|
|
|
class CheckAnyServiceActiveTests(TestCase):
|
|
def test_one_active_service_is_enough(self):
|
|
with mock.patch.object(
|
|
SCRIPT, "check_service_active", side_effect=[False, True]
|
|
):
|
|
self.assertTrue(SCRIPT.check_any_service_active(["a", "b"]))
|
|
|
|
def test_all_inactive_is_false(self):
|
|
with mock.patch.object(SCRIPT, "check_service_active", return_value=False):
|
|
self.assertFalse(SCRIPT.check_any_service_active(["a", "b"]))
|
|
|
|
def test_an_empty_list_is_false(self):
|
|
self.assertFalse(SCRIPT.check_any_service_active([]))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|