Files
bot-bottle/tests/unit/test_backend_setup.py
T
didericis-claude 65a49a239e
test / integration-docker (pull_request) Successful in 19s
tracker-policy-pr / check-pr (pull_request) Successful in 19s
test / unit (pull_request) Successful in 40s
lint / lint (push) Successful in 58s
test / integration-firecracker (pull_request) Successful in 3m20s
test / coverage (pull_request) Successful in 22s
test / publish-infra (pull_request) Has been skipped
firecracker status(): open /dev/kvm O_RDWR; check kernel, dropbear, mke2fs
Two reviewer findings addressed:

1. _kvm_accessible() now opens /dev/kvm with O_RDWR|O_CLOEXEC instead
   of read-only. VM creation requires write access; a read-only
   descriptor can satisfy KVM_GET_API_VERSION but fails at boot time.

2. status() now checks every hard prerequisite that require_firecracker()
   checks at launch: guest kernel image, static dropbear binary, and
   mke2fs. Previously a host with a configured TAP pool but missing
   artifacts could pass status() and then fail during launch.

Regression coverage: TestFirecrackerKvmCheck gains test_kvm_open_rdwr_fails;
TestFirecrackerArtifactCheck covers each missing artifact; existing
TestFirecrackerStatus fixtures updated to stub the new checks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 16:03:51 +00:00

529 lines
23 KiB
Python

"""Unit: `backend {setup,status,teardown}` across the three backends.
Exercises the branch matrix — NixOS vs systemd vs neither, root vs
non-root, missing binary/daemon/service — so the generic host-setup
paths are covered without a real host. All output is captured; the point
is the control flow + return codes, not the prose.
"""
from __future__ import annotations
import contextlib
import io
import subprocess
import unittest
from typing import Callable
from unittest.mock import MagicMock, patch
from bot_bottle.backend.docker import setup as dk
from bot_bottle.backend.firecracker import netpool
from bot_bottle.backend.firecracker import setup as fc
from bot_bottle.backend.macos_container import setup as mc
def _cap(fn: Callable[[], int | None]) -> tuple[int | None, str]:
buf = io.StringIO()
with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf):
rc = fn()
return rc, buf.getvalue()
def _ok(stdout: str = "") -> "subprocess.CompletedProcess[str]":
return subprocess.CompletedProcess([], 0, stdout=stdout, stderr="")
# --- firecracker ----------------------------------------------------
class TestFirecrackerPrereqs(unittest.TestCase):
def test_binary_found_and_kvm(self):
with patch.object(fc.shutil, "which", return_value="/usr/bin/firecracker"), \
patch.object(fc.util, "is_host_capable", return_value=True):
_, out = _cap(fc._print_prereqs)
self.assertIn("firecracker binary: found", out)
self.assertIn("KVM: /dev/kvm present", out)
def test_binary_missing_and_no_kvm(self):
with patch.object(fc.shutil, "which", return_value=None), \
patch.object(fc.util, "is_host_capable", return_value=False):
_, out = _cap(fc._print_prereqs)
self.assertIn("NOT found on PATH", out)
self.assertIn("KVM: /dev/kvm missing", out)
class TestFirecrackerSetup(unittest.TestCase):
def setUp(self):
self._p = [
patch.object(fc, "_print_prereqs", lambda: None),
patch.object(fc, "_warn_overlaps", lambda: None),
]
for p in self._p:
p.start()
self.addCleanup(lambda: [p.stop() for p in self._p])
def test_nixos_prints_module_import(self):
with patch.object(fc, "_is_nixos", return_value=True):
rc, out = _cap(fc.setup)
self.assertEqual(0, rc)
self.assertIn("nixosModules.firecracker-netpool", out)
self.assertIn("NON-INVASIVE", out)
def test_systemd_root_installs_unit(self):
unit = MagicMock()
with patch.object(fc, "_is_nixos", return_value=False), \
patch.object(fc, "_has_systemd", return_value=True), \
patch.object(fc.os, "geteuid", return_value=0), \
patch.object(fc, "_UNIT_PATH", unit), \
patch.object(fc.subprocess, "run", return_value=_ok()):
rc, out = _cap(fc.setup)
self.assertEqual(0, rc)
unit.write_text.assert_called_once()
self.assertIn("Installed and started", out)
def test_systemd_root_enable_failure(self):
unit = MagicMock()
with patch.object(fc, "_is_nixos", return_value=False), \
patch.object(fc, "_has_systemd", return_value=True), \
patch.object(fc.os, "geteuid", return_value=0), \
patch.object(fc, "_UNIT_PATH", unit), \
patch.object(fc.subprocess, "run",
side_effect=[_ok(), subprocess.CompletedProcess([], 1)]):
rc, out = _cap(fc.setup)
self.assertEqual(0, rc)
self.assertIn("enable --now` failed", out)
def test_systemd_nonroot_prints_block(self):
with patch.object(fc, "_is_nixos", return_value=False), \
patch.object(fc, "_has_systemd", return_value=True), \
patch.object(fc.os, "geteuid", return_value=1000):
rc, out = _cap(fc.setup)
self.assertEqual(0, rc)
self.assertIn("sudo tee", out)
self.assertIn("systemctl enable --now", out)
def test_no_systemd_prints_shell(self):
with patch.object(fc, "_is_nixos", return_value=False), \
patch.object(fc, "_has_systemd", return_value=False):
rc, out = _cap(fc.setup)
self.assertEqual(0, rc)
self.assertIn("firecracker-netpool.sh up", out)
class TestFirecrackerWarnOverlaps(unittest.TestCase):
def test_emits_on_conflict(self):
conflict = netpool.RouteConflict(dst="10.243.0.0/24", dev="eth0")
with patch.object(fc.netpool, "overlapping_routes", return_value=[conflict]):
_, out = _cap(fc._warn_overlaps)
self.assertIn("overlaps existing routes", out)
def test_silent_when_clear(self):
with patch.object(fc.netpool, "overlapping_routes", return_value=[]):
_, out = _cap(fc._warn_overlaps)
self.assertEqual("", out)
class TestFirecrackerTeardown(unittest.TestCase):
def test_nixos(self):
with patch.object(fc, "_is_nixos", return_value=True):
rc, out = _cap(fc.teardown)
self.assertEqual(0, rc)
self.assertIn("enable = false", out)
def test_systemd_root_removes_unit(self):
unit = MagicMock()
with patch.object(fc, "_is_nixos", return_value=False), \
patch.object(fc, "_has_systemd", return_value=True), \
patch.object(fc.os, "geteuid", return_value=0), \
patch.object(fc, "_UNIT_PATH", unit), \
patch.object(fc.subprocess, "run", return_value=_ok()):
rc, out = _cap(fc.teardown)
self.assertEqual(0, rc)
unit.unlink.assert_called_once()
self.assertIn("removed", out)
def test_systemd_nonroot_prints(self):
with patch.object(fc, "_is_nixos", return_value=False), \
patch.object(fc, "_has_systemd", return_value=True), \
patch.object(fc.os, "geteuid", return_value=1000):
rc, out = _cap(fc.teardown)
self.assertEqual(0, rc)
self.assertIn("systemctl disable", out)
def test_no_systemd(self):
with patch.object(fc, "_is_nixos", return_value=False), \
patch.object(fc, "_has_systemd", return_value=False):
rc, out = _cap(fc.teardown)
self.assertEqual(0, rc)
self.assertIn("firecracker-netpool.sh down", out)
class TestFirecrackerPersistence(unittest.TestCase):
def test_active(self):
with patch.object(fc, "_has_systemd", return_value=True), \
patch.object(fc.subprocess, "run", return_value=_ok("active\n")):
_, out = _cap(fc._report_persistence)
self.assertIn("active (survives reboot)", out)
def test_inactive(self):
with patch.object(fc, "_has_systemd", return_value=True), \
patch.object(fc.subprocess, "run", return_value=_ok("inactive\n")):
_, out = _cap(fc._report_persistence)
self.assertIn("not installed as a persistent unit", out)
def test_no_systemd_is_noop(self):
with patch.object(fc, "_has_systemd", return_value=False):
_, out = _cap(fc._report_persistence)
self.assertEqual("", out)
class TestFirecrackerHostDetect(unittest.TestCase):
def test_is_nixos_via_etc_nixos(self):
with patch.object(fc.Path, "exists", return_value=True):
self.assertTrue(fc._is_nixos())
def test_has_systemd(self):
with patch.object(fc.Path, "is_dir", return_value=True):
self.assertTrue(fc._has_systemd())
# --- docker ---------------------------------------------------------
class TestDockerSetupStatus(unittest.TestCase):
def test_setup_daemon_unreachable(self):
with patch.object(dk.shutil, "which", return_value="/usr/bin/docker"), \
patch.object(dk, "_daemon_reachable", return_value=False):
rc, out = _cap(dk.setup)
self.assertEqual(1, rc)
self.assertIn("daemon isn't reachable", out)
def test_setup_ok_with_runsc_note(self):
with patch.object(dk.shutil, "which", return_value="/usr/bin/docker"), \
patch.object(dk, "_daemon_reachable", return_value=True), \
patch.object(dk._util, "runsc_available", return_value=False):
rc, out = _cap(dk.setup)
self.assertEqual(0, rc)
self.assertIn("gVisor", out)
def test_status_all_ok(self):
with patch.object(dk.shutil, "which", return_value="/usr/bin/docker"), \
patch.object(dk, "_daemon_reachable", return_value=True), \
patch.object(dk._util, "runsc_available", return_value=True):
rc, out = _cap(dk.status)
self.assertEqual(0, rc)
self.assertIn("registered", out)
def test_daemon_reachable_false_without_docker(self):
with patch.object(dk.shutil, "which", return_value=None):
self.assertFalse(dk._daemon_reachable())
def test_daemon_reachable_true_when_daemon_responds(self):
with patch.object(dk.shutil, "which", return_value="/usr/bin/docker"), \
patch.object(dk.subprocess, "run",
return_value=subprocess.CompletedProcess([], 0)):
self.assertTrue(dk._daemon_reachable())
def test_daemon_reachable_false_on_timeout(self):
with patch.object(dk.shutil, "which", return_value="/usr/bin/docker"), \
patch.object(dk.subprocess, "run",
side_effect=subprocess.TimeoutExpired(["docker", "info"], 5)):
self.assertFalse(dk._daemon_reachable())
def test_status_reports_missing_docker(self):
with patch.object(dk.shutil, "which", return_value=None):
rc, out = _cap(dk.status)
self.assertEqual(1, rc)
self.assertIn("docker on PATH: NO", out)
def test_setup_missing_docker_prints_install(self):
with patch.object(dk.shutil, "which", return_value=None):
rc, out = _cap(dk.setup)
self.assertEqual(1, rc)
self.assertIn("docker.com", out)
class TestNetpoolSpanConflict(unittest.TestCase):
def test_pool_span_bounds(self):
with patch.dict("os.environ", {"BOT_BOTTLE_FC_IP_BASE": "10.243.0.0",
"BOT_BOTTLE_FC_POOL_SIZE": "8"}):
lo, hi = netpool._pool_span()
# base .. base + 2*8 - 1 (16 addresses)
self.assertEqual(15, hi - lo)
def test_route_conflict_fields(self):
c = netpool.RouteConflict(dst="10.243.0.0/24", dev="eth0")
self.assertEqual("10.243.0.0/24", c.dst)
self.assertEqual("eth0", c.dev)
# --- macos-container ------------------------------------------------
class TestMacosSetupStatus(unittest.TestCase):
def test_setup_not_macos(self):
with patch.object(mc._container, "is_macos", return_value=False):
rc, out = _cap(mc.setup)
self.assertEqual(1, rc)
self.assertIn("requires macOS", out)
def test_setup_no_cli(self):
with patch.object(mc._container, "is_macos", return_value=True), \
patch.object(mc.shutil, "which", return_value=None):
rc, out = _cap(mc.setup)
self.assertEqual(1, rc)
self.assertIn("not found on PATH", out)
def test_setup_service_not_running(self):
with patch.object(mc._container, "is_macos", return_value=True), \
patch.object(mc.shutil, "which", return_value="/usr/bin/container"), \
patch.object(mc, "_service_running", return_value=False):
rc, out = _cap(mc.setup)
self.assertEqual(1, rc)
self.assertIn("system start", out)
def test_setup_ready(self):
with patch.object(mc._container, "is_macos", return_value=True), \
patch.object(mc.shutil, "which", return_value="/usr/bin/container"), \
patch.object(mc, "_service_running", return_value=True):
rc, out = _cap(mc.setup)
self.assertEqual(0, rc)
self.assertIn("ready", out)
def test_status_all_ok(self):
with patch.object(mc._container, "is_macos", return_value=True), \
patch.object(mc.shutil, "which", return_value="/usr/bin/container"), \
patch.object(mc, "_service_running", return_value=True):
rc, _ = _cap(mc.status)
self.assertEqual(0, rc)
def test_status_not_macos_no_cli(self):
with patch.object(mc._container, "is_macos", return_value=False), \
patch.object(mc.shutil, "which", return_value=None):
rc, _ = _cap(mc.status)
self.assertEqual(1, rc)
def test_teardown_noop(self):
rc, out = _cap(mc.teardown)
self.assertEqual(0, rc)
self.assertIn("nothing to undo", out)
def test_service_running_false_without_cli(self):
with patch.object(mc.shutil, "which", return_value=None):
self.assertFalse(mc._service_running())
# --- netpool renderers (imperative + env overrides) -----------------
class TestNetpoolShellRenderers(unittest.TestCase):
def test_shell_setup_default(self):
with patch.dict("os.environ", {}, clear=True):
out = netpool.render_shell_setup()
self.assertIn("firecracker-netpool.sh up", out)
self.assertNotIn("BOT_BOTTLE_FC_", out) # no env prefix when all default
def test_shell_setup_with_overrides(self):
# pool_size()/ip_base() re-read the env; IFACE_PREFIX is a
# module constant, so exercise the function-backed knobs.
with patch.dict("os.environ", {"BOT_BOTTLE_FC_IP_BASE": "10.9.0.0",
"BOT_BOTTLE_FC_POOL_SIZE": "4"}):
out = netpool.render_shell_setup()
self.assertIn("BOT_BOTTLE_FC_IP_BASE=10.9.0.0", out)
self.assertIn("BOT_BOTTLE_FC_POOL_SIZE=4", out)
class TestFirecrackerBinaryCheck(unittest.TestCase):
def test_binary_missing_returns_false(self):
with patch.object(fc.shutil, "which", return_value=None):
self.assertFalse(fc._firecracker_binary_ok())
def test_binary_present_and_runs_ok(self):
with patch.object(fc.shutil, "which", return_value="/usr/bin/firecracker"), \
patch.object(fc.subprocess, "run",
return_value=subprocess.CompletedProcess([], 0)):
self.assertTrue(fc._firecracker_binary_ok())
def test_binary_found_but_exits_nonzero(self):
with patch.object(fc.shutil, "which", return_value="/usr/bin/firecracker"), \
patch.object(fc.subprocess, "run",
return_value=subprocess.CompletedProcess([], 1)):
self.assertFalse(fc._firecracker_binary_ok())
def test_binary_found_but_oserror(self):
with patch.object(fc.shutil, "which", return_value="/usr/bin/firecracker"), \
patch.object(fc.subprocess, "run", side_effect=OSError("exec failed")):
self.assertFalse(fc._firecracker_binary_ok())
class TestFirecrackerKvmCheck(unittest.TestCase):
def test_kvm_device_absent_returns_false(self):
with patch.object(fc.os.path, "exists", return_value=False):
self.assertFalse(fc._kvm_accessible())
def test_kvm_accessible_when_ioctl_succeeds(self):
with patch.object(fc.os.path, "exists", return_value=True), \
patch.object(fc.os, "open", return_value=42), \
patch.object(fc.os, "close"), \
patch.object(fc.fcntl, "ioctl", return_value=12):
self.assertTrue(fc._kvm_accessible())
def test_kvm_open_rdwr_fails(self):
with patch.object(fc.os.path, "exists", return_value=True), \
patch.object(fc.os, "open", side_effect=OSError("Permission denied")):
self.assertFalse(fc._kvm_accessible())
def test_kvm_present_but_ioctl_fails(self):
with patch.object(fc.os.path, "exists", return_value=True), \
patch.object(fc.os, "open", return_value=42), \
patch.object(fc.os, "close"), \
patch.object(fc.fcntl, "ioctl", side_effect=OSError("permission denied")):
self.assertFalse(fc._kvm_accessible())
class TestFirecrackerArtifactCheck(unittest.TestCase):
"""status() reports missing guest kernel, dropbear binary, and mke2fs."""
def _apply_all_ok(self, stack: contextlib.ExitStack) -> None:
"""Stub every status() check to pass except what the test overrides."""
stack.enter_context(patch.object(fc, "_firecracker_binary_ok", return_value=True))
stack.enter_context(patch.object(fc, "_kvm_accessible", return_value=True))
k: MagicMock = MagicMock()
k.is_file.return_value = True
stack.enter_context(patch.object(fc.util, "kernel_path", return_value=k))
d: MagicMock = MagicMock()
d.is_file.return_value = True
stack.enter_context(patch.object(fc.util, "dropbear_path", return_value=d))
stack.enter_context(patch.object(fc.shutil, "which", return_value="/usr/bin/x"))
stack.enter_context(patch.object(netpool, "missing_taps", return_value=[]))
stack.enter_context(patch.object(netpool, "pool_size", return_value=8))
stack.enter_context(patch.object(netpool, "overlapping_routes", return_value=[]))
stack.enter_context(patch.object(fc, "_report_persistence", lambda: None))
def test_status_fails_when_kernel_missing(self):
with contextlib.ExitStack() as stack:
self._apply_all_ok(stack)
k: MagicMock = MagicMock()
k.is_file.return_value = False
stack.enter_context(patch.object(fc.util, "kernel_path", return_value=k))
rc, out = _cap(fc.status)
self.assertEqual(1, rc)
self.assertIn("guest kernel: NOT found", out)
def test_status_fails_when_dropbear_missing(self):
with contextlib.ExitStack() as stack:
self._apply_all_ok(stack)
d: MagicMock = MagicMock()
d.is_file.return_value = False
stack.enter_context(patch.object(fc.util, "dropbear_path", return_value=d))
rc, out = _cap(fc.status)
self.assertEqual(1, rc)
self.assertIn("dropbear: NOT found", out)
def test_status_fails_when_mke2fs_missing(self):
with contextlib.ExitStack() as stack:
self._apply_all_ok(stack)
stack.enter_context(patch.object(
fc.shutil, "which",
side_effect=lambda cmd: (None if cmd == "mke2fs" else "/usr/bin/x"), # type: ignore[misc]
))
rc, out = _cap(fc.status)
self.assertEqual(1, rc)
self.assertIn("mke2fs: NOT found", out)
class TestFirecrackerStatusRuntime(unittest.TestCase):
"""status() reports binary and KVM problems and returns non-zero."""
def _apply_pool_ok(self, stack: contextlib.ExitStack) -> None:
kernel_mock = MagicMock()
kernel_mock.is_file.return_value = True
dropbear_mock = MagicMock()
dropbear_mock.is_file.return_value = True
stack.enter_context(patch.object(fc.util, "kernel_path", return_value=kernel_mock))
stack.enter_context(patch.object(fc.util, "dropbear_path", return_value=dropbear_mock))
stack.enter_context(patch.object(netpool, "missing_taps", return_value=[]))
stack.enter_context(patch.object(netpool, "pool_size", return_value=8))
stack.enter_context(patch.object(netpool, "overlapping_routes", return_value=[]))
stack.enter_context(patch.object(fc, "_report_persistence", lambda: None))
def test_status_fails_when_binary_missing(self):
with contextlib.ExitStack() as stack:
stack.enter_context(
patch.object(fc, "_firecracker_binary_ok", return_value=False))
stack.enter_context(
patch.object(fc, "_kvm_accessible", return_value=True))
stack.enter_context(
patch.object(fc.shutil, "which", return_value=None))
self._apply_pool_ok(stack)
rc, out = _cap(fc.status)
self.assertEqual(1, rc)
self.assertIn("NOT found on PATH", out)
def test_status_fails_when_kvm_not_accessible(self):
with contextlib.ExitStack() as stack:
stack.enter_context(
patch.object(fc, "_firecracker_binary_ok", return_value=True))
stack.enter_context(
patch.object(fc.shutil, "which", return_value="/usr/bin/firecracker"))
stack.enter_context(
patch.object(fc, "_kvm_accessible", return_value=False))
stack.enter_context(
patch.object(fc.os.path, "exists", return_value=True))
self._apply_pool_ok(stack)
rc, out = _cap(fc.status)
self.assertEqual(1, rc)
self.assertIn("not accessible", out)
def test_status_ok_when_binary_and_kvm_ready(self):
with contextlib.ExitStack() as stack:
stack.enter_context(
patch.object(fc, "_firecracker_binary_ok", return_value=True))
stack.enter_context(
patch.object(fc.shutil, "which", return_value="/usr/bin/firecracker"))
stack.enter_context(
patch.object(fc, "_kvm_accessible", return_value=True))
stack.enter_context(
patch.object(netpool, "nft_table_present", return_value=True))
self._apply_pool_ok(stack)
rc, out = _cap(fc.status)
self.assertEqual(0, rc)
self.assertIn("firecracker binary: ok", out)
self.assertIn("KVM:", out)
class TestBackendStatusQuiet(unittest.TestCase):
"""status(quiet=True) routes the underlying status() call through
redirect_stderr so diagnostic output is suppressed. Verify the return
code is propagated and that calling with quiet=False leaves the non-quiet
path active (covered by the other TestDockerSetupStatus tests)."""
def test_docker_quiet_true_propagates_return_code(self):
from bot_bottle.backend.docker.backend import DockerBottleBackend
with patch.object(dk, "status", return_value=0):
self.assertEqual(0, DockerBottleBackend.status(quiet=True))
def test_docker_quiet_true_suppresses_stderr(self):
import io as _io
from bot_bottle.backend.docker.backend import DockerBottleBackend
def _loud_status():
import sys
print("should be suppressed", file=sys.stderr)
return 0
with patch.object(dk, "status", side_effect=_loud_status):
buf = _io.StringIO()
with contextlib.redirect_stderr(buf):
DockerBottleBackend.status(quiet=True)
self.assertEqual("", buf.getvalue())
def test_firecracker_quiet_true_propagates_return_code(self):
from bot_bottle.backend.firecracker.backend import FirecrackerBottleBackend
with patch.object(fc, "status", return_value=1):
self.assertEqual(1, FirecrackerBottleBackend.status(quiet=True))
def test_macos_quiet_true_propagates_return_code(self):
from bot_bottle.backend.macos_container.backend import MacosContainerBottleBackend
with patch.object(mc, "status", return_value=0):
self.assertEqual(0, MacosContainerBottleBackend.status(quiet=True))
if __name__ == "__main__":
unittest.main()