nix-builder-autoscaler/agent/nix_builder_autoscaler/tests/test_runtime_fake.py

117 lines
3.8 KiB
Python
Raw Normal View History

2026-02-27 11:59:16 +01:00
"""Unit tests for the FakeRuntime adapter."""
import contextlib
from nix_builder_autoscaler.runtime.base import RuntimeError as RuntimeAdapterError
from nix_builder_autoscaler.runtime.fake import FakeRuntime
class TestLaunchSpot:
def test_returns_synthetic_instance_id(self):
rt = FakeRuntime()
iid = rt.launch_spot("slot001", "#!/bin/bash\necho hello")
assert iid.startswith("i-fake-")
assert len(iid) > 10
def test_instance_starts_pending(self):
rt = FakeRuntime()
iid = rt.launch_spot("slot001", "")
info = rt.describe_instance(iid)
assert info["state"] == "pending"
assert info["tailscale_ip"] is None
class TestTickProgression:
def test_transitions_to_running_after_configured_ticks(self):
rt = FakeRuntime(launch_latency_ticks=3, ip_delay_ticks=1)
iid = rt.launch_spot("slot001", "")
for _ in range(2):
rt.tick()
assert rt.describe_instance(iid)["state"] == "pending"
rt.tick() # tick 3
assert rt.describe_instance(iid)["state"] == "running"
def test_tailscale_ip_appears_after_configured_delay(self):
rt = FakeRuntime(launch_latency_ticks=2, ip_delay_ticks=2)
iid = rt.launch_spot("slot001", "")
for _ in range(2):
rt.tick()
assert rt.describe_instance(iid)["state"] == "running"
assert rt.describe_instance(iid)["tailscale_ip"] is None
rt.tick() # tick 3 — still no IP (need tick 4)
assert rt.describe_instance(iid)["tailscale_ip"] is None
rt.tick() # tick 4
info = rt.describe_instance(iid)
assert info["tailscale_ip"] is not None
assert info["tailscale_ip"].startswith("100.64.0.")
class TestInjectedFailure:
def test_launch_failure_raises(self):
rt = FakeRuntime()
rt.inject_launch_failure("slot001")
try:
rt.launch_spot("slot001", "")
raise AssertionError("Should have raised")
except RuntimeAdapterError as e:
assert e.category == "capacity_unavailable"
def test_failure_is_one_shot(self):
rt = FakeRuntime()
rt.inject_launch_failure("slot001")
with contextlib.suppress(RuntimeAdapterError):
rt.launch_spot("slot001", "")
# Second call should succeed
iid = rt.launch_spot("slot001", "")
assert iid.startswith("i-fake-")
class TestInjectedInterruption:
def test_interruption_returns_terminated(self):
rt = FakeRuntime(launch_latency_ticks=1)
iid = rt.launch_spot("slot001", "")
rt.tick()
assert rt.describe_instance(iid)["state"] == "running"
rt.inject_interruption(iid)
info = rt.describe_instance(iid)
assert info["state"] == "terminated"
def test_interruption_is_one_shot(self):
"""After the interruption fires, subsequent describes stay terminated."""
rt = FakeRuntime(launch_latency_ticks=1)
iid = rt.launch_spot("slot001", "")
rt.tick()
rt.inject_interruption(iid)
rt.describe_instance(iid) # consumes the injection
info = rt.describe_instance(iid)
assert info["state"] == "terminated"
class TestTerminate:
def test_terminate_marks_instance(self):
rt = FakeRuntime(launch_latency_ticks=1)
iid = rt.launch_spot("slot001", "")
rt.tick()
rt.terminate_instance(iid)
assert rt.describe_instance(iid)["state"] == "terminated"
class TestListManaged:
def test_lists_non_terminated(self):
rt = FakeRuntime(launch_latency_ticks=1)
iid1 = rt.launch_spot("slot001", "")
iid2 = rt.launch_spot("slot002", "")
rt.tick()
rt.terminate_instance(iid1)
managed = rt.list_managed_instances()
ids = [m["instance_id"] for m in managed]
assert iid2 in ids
assert iid1 not in ids