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
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>
529 lines
23 KiB
Python
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()
|