WIP autoscaler agent
This commit is contained in:
parent
c610a3e284
commit
28059dcedf
34 changed files with 2409 additions and 35 deletions
0
agent/nix_builder_autoscaler/tests/__init__.py
Normal file
0
agent/nix_builder_autoscaler/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""End-to-end integration tests with FakeRuntime — Plan 05."""
|
||||
|
|
@ -0,0 +1 @@
|
|||
"""HAProxy provider unit tests — Plan 02."""
|
||||
|
|
@ -0,0 +1 @@
|
|||
"""Reservations API unit tests — Plan 04."""
|
||||
1
agent/nix_builder_autoscaler/tests/test_runtime_ec2.py
Normal file
1
agent/nix_builder_autoscaler/tests/test_runtime_ec2.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""EC2 runtime unit tests — Plan 02."""
|
||||
116
agent/nix_builder_autoscaler/tests/test_runtime_fake.py
Normal file
116
agent/nix_builder_autoscaler/tests/test_runtime_fake.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
"""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
|
||||
1
agent/nix_builder_autoscaler/tests/test_scheduler.py
Normal file
1
agent/nix_builder_autoscaler/tests/test_scheduler.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Scheduler unit tests — Plan 03."""
|
||||
Loading…
Add table
Add a link
Reference in a new issue