ci: backend-agnostic integration guards + per-backend preflight
tracker-policy-pr / check-pr (pull_request) Successful in 15s
test / integration-docker (pull_request) Successful in 20s
lint / lint (push) Successful in 1m5s
test / unit (pull_request) Successful in 1m49s
test / integration-firecracker (pull_request) Successful in 2m0s
test / coverage (pull_request) Successful in 16s
test / publish-infra (pull_request) Has been skipped

Integration tests now select their backend from BOT_BOTTLE_BACKEND and
skip on the capability that backend actually needs, instead of gating
every backend on unrelated Docker availability.

Task 1 — backend-agnostic guards (tests/_backend.py):
- Capability probes: docker_capability() (reachable daemon) and
  firecracker_capability() (accessible /dev/kvm + firecracker on PATH,
  Docker-independent). backend_capability()/selected_backend() resolve
  the target from BOT_BOTTLE_BACKEND (default docker).
- skip_unless_selected_backend_available() for backend-agnostic tests
  (test_sandbox_escape) — runs through whichever backend is selected and
  checks that backend's real capability.
- skip_unless_backend("docker") for Docker-implementation tests
  (DockerBroker, DockerGateway, backend.docker.*) — they no-op under a
  non-Docker run rather than testing internals that run doesn't target.
- Retires tests/_docker.py; the KVM job no longer needs SKIP_DOCKER_TESTS
  to steer Docker-only classes.

Task 2 — explicit per-backend skip visibility:
- tests/backend_preflight.py prints a clear PASS/FAIL capability line and
  exits non-zero when the selected backend is missing.
- Both integration jobs run it as a preflight, so absent infrastructure
  is surfaced at the job level instead of hidden among unittest.skip
  lines. The docker job replaces its soft "Show environment" step; the
  firecracker job keeps its richer backend-status check.

Docs (tests/README.md, docs/ci.md) updated; unit coverage for the probes,
guards, and preflight in test_backend_skip_guards.py.

Closes #414

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 01:15:45 +00:00
committed by didericis
parent f24ae45d13
commit 9fdaba4bd4
16 changed files with 428 additions and 113 deletions
+13 -9
View File
@@ -103,14 +103,15 @@ jobs:
- name: Install coverage
run: python3 -m pip install --break-system-packages coverage
- name: Show environment
# Fail loudly if the backend this job promises isn't actually usable,
# rather than letting every test silently `unittest.skip` and the job
# go green on zero coverage. Same capability probe the skip guards use.
- name: Preflight — Docker backend is ready
env:
BOT_BOTTLE_BACKEND: docker
run: |
python3 --version
if command -v docker >/dev/null 2>&1; then
docker version || true
else
echo "docker not on PATH — integration tests will skip"
fi
python3 -m tests.backend_preflight docker
- name: Run integration tests (docker) with coverage
env:
@@ -157,10 +158,13 @@ jobs:
uses: actions/checkout@v4
- name: Preflight — Firecracker host is ready
env:
BOT_BOTTLE_BACKEND: firecracker
run: |
command -v firecracker >/dev/null || {
echo "firecracker not on PATH — provision the runner (README: Firecracker on Linux)"; exit 1; }
test -e /dev/kvm || { echo "/dev/kvm missing — KVM not available on this runner"; exit 1; }
# Standardized capability line — same probe (and PASS/FAIL wording)
# the integration skip guards use, so a missing backend is visible
# at the job level rather than hidden among `unittest.skip` lines.
python3 -m tests.backend_preflight firecracker
# `backend status` exits non-zero unless the TAP pool is up + no
# range overlap; it prints the exact `backend setup` fix.
python3 cli.py backend status --backend=firecracker
+13 -3
View File
@@ -1,13 +1,23 @@
# CI
The test workflow lives at [`.gitea/workflows/test.yml`](../.gitea/workflows/test.yml).
It runs `tests/run_tests.py` (full suite — unit + integration) on:
It runs the unit suite plus one integration job per backend
(`integration-docker`, `integration-firecracker`) on:
- every push to a branch with an open pull request, and
- every push to `main`.
Integration tests need Docker on the runner; they skip cleanly via
`tests/_docker.skip_unless_docker` when no daemon is reachable.
Each integration job selects its backend via `BOT_BOTTLE_BACKEND` and
runs a **preflight** (`python3 -m tests.backend_preflight <backend>`)
that prints a clear PASS/FAIL capability line and fails the job when the
backend is missing — so absent infrastructure is visible at the job level
rather than hidden among per-test `unittest.skip` lines. The same
capability probes back the skip guards in
[`tests/_backend.py`](../tests/_backend.py): backend-agnostic tests use
`skip_unless_selected_backend_available()` and run through whichever
backend is selected (checking, e.g., `/dev/kvm` for Firecracker rather
than unrelated Docker availability); Docker-implementation tests use
`skip_unless_backend("docker")` and no-op under a non-Docker run.
A small subset of integration tests skip when running specifically
under Gitea Actions (`GITEA_ACTIONS=true`), because `act_runner` runs
+18 -5
View File
@@ -2,14 +2,16 @@
Plain-Python test suite using stdlib `unittest`. No external
dependencies. Unit tests run anywhere Python 3 is present; integration
tests need Docker and skip cleanly otherwise.
tests run through the backend named by `BOT_BOTTLE_BACKEND` (default
`docker`) and skip cleanly when that backend isn't available on the host.
## Layout
```
tests/
fixtures.py # JSON manifest builders (shared)
_docker.py # docker-availability skip helper (shared)
_backend.py # backend capability probes + skip guards
backend_preflight.py # `python -m tests.backend_preflight` (CI)
unit/
test_egress.py
test_egress_addon_core.py
@@ -73,7 +75,7 @@ BOT_BOTTLE_RUN_CANARIES=1 python -m unittest discover -t . -s tests/canaries -v
## Adding a test
1. Pick the directory: `tests/unit/` for a pure unit test,
`tests/integration/` for one that needs Docker.
`tests/integration/` for one that needs a backend.
2. Filename: `test_<topic>.py`.
3. Boilerplate:
```python
@@ -88,5 +90,16 @@ BOT_BOTTLE_RUN_CANARIES=1 python -m unittest discover -t . -s tests/canaries -v
if __name__ == "__main__":
unittest.main()
```
4. For Docker-dependent tests, decorate the class with
`@skip_unless_docker()` from `tests._docker`.
4. Skip guards live in `tests._backend`:
- Backend-agnostic tests (go through `get_bottle_backend()`) decorate
the class with `@skip_unless_selected_backend_available()` — the test
runs against whichever backend `BOT_BOTTLE_BACKEND` selects and skips
unless that backend's capability is present (a reachable Docker daemon,
or an accessible `/dev/kvm` + `firecracker` for Firecracker).
- Backend-specific tests (exercise `DockerBroker`, `DockerGateway`,
`backend.docker.*`, …) decorate with `@skip_unless_backend("docker")`
so they no-op under a run targeting a different backend.
The same capability probes back the CI preflight,
`python -m tests.backend_preflight [<backend>]`, which prints a clear
PASS/FAIL line and exits non-zero when the selected backend is missing.
+153
View File
@@ -0,0 +1,153 @@
"""Backend-aware capability probes and skip guards for the integration suite.
Every integration test selects its backend from ``BOT_BOTTLE_BACKEND``
(default ``docker``) and exercises the full test through that backend. Skip
guards check the *capability* the selected backend actually needs — a
reachable Docker daemon for ``docker``, an accessible ``/dev/kvm`` plus a
``firecracker`` binary for ``firecracker`` — rather than gating every backend
on unrelated Docker availability.
The same probes back the CI preflight (``python3 -m tests.backend_preflight``)
so a job surfaces missing infrastructure at the job level with a clear
PASS/FAIL line instead of turning every test into a silent ``unittest.skip``.
"""
from __future__ import annotations
import os
import shutil
import subprocess
import unittest
from dataclasses import dataclass
# Default when ``BOT_BOTTLE_BACKEND`` is unset. Docker preserves the historical
# Docker-backed CI path (and mirrors the pin in ``test_sandbox_escape``).
DEFAULT_BACKEND = "docker"
# `/dev/kvm` must exist and be openable by the invoking user for the
# Firecracker backend to boot a guest (mirrors
# ``bot_bottle.backend.firecracker.util``).
_KVM_DEVICE = "/dev/kvm"
def selected_backend() -> str:
"""The backend this test run targets, from ``BOT_BOTTLE_BACKEND``.
Mirrors the CLI's env selector; unset means ``docker`` so an
unconfigured run behaves exactly as the suite did before backends were
pluggable.
"""
return os.environ.get("BOT_BOTTLE_BACKEND") or DEFAULT_BACKEND
@dataclass(frozen=True)
class Capability:
"""Outcome of a backend capability probe.
``ok`` gates the tests; ``detail`` is a one-line human string reused for
both preflight output and ``unittest.skip`` reasons so the same wording
shows up at the job level and next to a skipped test.
"""
backend: str
ok: bool
detail: str
def docker_capability() -> Capability:
"""Whether a Docker daemon is reachable (and not opted out)."""
name = "docker"
if os.environ.get("SKIP_DOCKER_TESTS"):
return Capability(name, False, "SKIP_DOCKER_TESTS is set")
if shutil.which("docker") is None:
return Capability(name, False, "docker not on PATH")
try:
returncode = subprocess.run(
["docker", "info"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
timeout=5,
).returncode
except subprocess.TimeoutExpired:
return Capability(name, False, "docker info timed out")
if returncode != 0:
return Capability(name, False, "docker daemon unreachable")
return Capability(name, True, "docker daemon reachable")
def firecracker_capability() -> Capability:
"""Whether this host can boot a Firecracker guest: an accessible
``/dev/kvm`` and a ``firecracker`` binary on ``PATH``.
Deliberately does not probe unrelated Docker availability — the
Firecracker backend is Docker-independent at runtime.
"""
name = "firecracker"
if not os.path.exists(_KVM_DEVICE):
return Capability(name, False, f"{_KVM_DEVICE} missing — KVM unavailable")
if not os.access(_KVM_DEVICE, os.R_OK | os.W_OK):
return Capability(
name, False, f"{_KVM_DEVICE} not accessible — add your user to the kvm group"
)
if shutil.which("firecracker") is None:
return Capability(name, False, "firecracker not on PATH")
return Capability(name, True, f"{_KVM_DEVICE} accessible; firecracker on PATH")
_PROBES = {
"docker": docker_capability,
"firecracker": firecracker_capability,
}
def backend_capability(backend: str | None = None) -> Capability:
"""Probe ``backend`` (default: the selected backend) for readiness."""
backend = backend or selected_backend()
probe = _PROBES.get(backend)
if probe is None:
return Capability(backend, False, f"no capability probe for backend {backend!r}")
return probe()
# ---- back-compat boolean probe -------------------------------------------
def docker_available() -> bool:
"""Boolean Docker probe (kept for callers that only need the flag)."""
return docker_capability().ok
# ---- skip guards ---------------------------------------------------------
def skip_unless_backend(backend: str):
"""Skip a backend-specific test unless the selected backend matches AND
that backend's capability is present.
Docker-implementation tests (``DockerBroker``, ``DockerGateway``,
``backend.docker.*``) use ``skip_unless_backend("docker")`` so they no-op
under a Firecracker run instead of testing Docker internals that run
doesn't target — the guard reads ``BOT_BOTTLE_BACKEND`` rather than
"is Docker installed".
"""
sel = selected_backend()
if sel != backend:
return unittest.skip(
f"backend {backend!r} not selected (BOT_BOTTLE_BACKEND={sel})"
)
cap = backend_capability(backend)
return unittest.skipUnless(cap.ok, f"{backend} backend unavailable: {cap.detail}")
def skip_unless_selected_backend_available():
"""Skip a backend-agnostic test unless the *selected* backend can run it.
The test then exercises whichever backend ``BOT_BOTTLE_BACKEND`` names,
checking that backend's real capability (e.g. ``/dev/kvm`` for
Firecracker) rather than unrelated Docker availability.
"""
cap = backend_capability()
return unittest.skipUnless(
cap.ok, f"selected backend {cap.backend!r} unavailable: {cap.detail}"
)
-45
View File
@@ -1,45 +0,0 @@
"""Docker availability check used by integration tests."""
from __future__ import annotations
import os
import shutil
import subprocess
import unittest
def docker_available() -> bool:
if os.environ.get("SKIP_DOCKER_TESTS"):
return False
if shutil.which("docker") is None:
return False
try:
return (
subprocess.run(
["docker", "info"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
timeout=5,
).returncode
== 0
)
except subprocess.TimeoutExpired:
return False
def skip_unless_docker(reason: str = "docker unreachable"):
return unittest.skipUnless(docker_available(), reason)
def skip_unless_docker_or_firecracker(
reason: str = "neither Docker nor Firecracker selected",
):
"""Skip a backend-agnostic test unless one supported backend can run.
Firecracker does not require the host Docker daemon. The KVM coverage job
deliberately sets ``SKIP_DOCKER_TESTS`` to exclude Docker-only integration
classes while still exercising this path.
"""
firecracker_selected = os.environ.get("BOT_BOTTLE_BACKEND") == "firecracker"
return unittest.skipUnless(firecracker_selected or docker_available(), reason)
+27
View File
@@ -0,0 +1,27 @@
"""Backend capability preflight for the integration CI jobs.
Run as ``python3 -m tests.backend_preflight`` (optionally with a backend name
argument; default: ``BOT_BOTTLE_BACKEND`` or ``docker``). Prints a single
clear PASS/FAIL line for the selected backend's capability and exits non-zero
on failure, so a job surfaces missing infrastructure at the job level instead
of hiding it among ``unittest.skip`` lines further down the output.
"""
from __future__ import annotations
import sys
from tests._backend import backend_capability, selected_backend
def main(argv: list[str] | None = None) -> int:
argv = sys.argv[1:] if argv is None else argv
backend = argv[0] if argv else selected_backend()
cap = backend_capability(backend)
status = "PASS" if cap.ok else "FAIL"
print(f"[preflight] backend={backend} {status}: {cap.detail}")
return 0 if cap.ok else 1
if __name__ == "__main__":
raise SystemExit(main())
+2 -2
View File
@@ -25,14 +25,14 @@ import os
import subprocess
import unittest
from tests._docker import skip_unless_docker
from tests._backend import skip_unless_backend
_IMAGE = "bot-bottle-gateway-test:chunk1"
_DOCKERFILE = "Dockerfile.gateway"
@skip_unless_docker()
@skip_unless_backend("docker")
@unittest.skipIf(
os.environ.get("GITEA_ACTIONS") == "true",
"skipped under act_runner: multi-stage build pulls a 200+MB "
@@ -31,7 +31,7 @@ from bot_bottle.backend.docker.gateway_net import next_free_ip
from bot_bottle.orchestrator.client import OrchestratorClient
from bot_bottle.orchestrator.gateway import GATEWAY_IMAGE, GATEWAY_NAME, GATEWAY_NETWORK
from bot_bottle.orchestrator.lifecycle import OrchestratorService
from tests._docker import skip_unless_docker
from tests._backend import skip_unless_backend
# One upstream reachable under two names; reflects the Authorization header so
# the probe can see exactly what the gateway injected (or didn't).
@@ -72,7 +72,7 @@ _PROBE_SRC = (
)
@skip_unless_docker()
@skip_unless_backend("docker")
@unittest.skipIf(
os.environ.get("GITEA_ACTIONS") == "true",
"skipped under act_runner: the orchestrator container bind-mounts the repo "
@@ -13,12 +13,12 @@ import unittest
from bot_bottle.orchestrator.broker import LaunchRequest, sign_request
from bot_bottle.orchestrator.docker_broker import DockerBroker, container_name
from tests._docker import skip_unless_docker
from tests._backend import skip_unless_backend
IMAGE = "busybox"
@skip_unless_docker()
@skip_unless_backend("docker")
class TestDockerBrokerIntegration(unittest.TestCase):
def setUp(self) -> None:
self.secret = secrets.token_bytes(16)
@@ -28,7 +28,7 @@ from pathlib import Path
from bot_bottle.orchestrator.client import OrchestratorClient
from bot_bottle.orchestrator.lifecycle import OrchestratorService
from bot_bottle.paths import host_control_plane_token
from tests._docker import skip_unless_docker
from tests._backend import skip_unless_backend
# Fixed (not per-run-suffixed) so repeated runs reuse the same layer-cached
# image instead of leaking a new dangling tag on every invocation.
@@ -37,7 +37,7 @@ _TEST_GATEWAY_IMAGE = "bot-bottle-gateway:itest"
_TEST_INFRA_IMAGE = "bot-bottle-infra:itest"
@skip_unless_docker()
@skip_unless_backend("docker")
@unittest.skipIf(
os.environ.get("GITEA_ACTIONS") == "true",
"skipped under act_runner: the orchestrator container bind-mounts the repo "
@@ -11,12 +11,12 @@ import subprocess
import unittest
from bot_bottle.orchestrator.gateway import DockerGateway
from tests._docker import skip_unless_docker
from tests._backend import skip_unless_backend
IMAGE = "busybox"
@skip_unless_docker()
@skip_unless_backend("docker")
class TestDockerGatewayIntegration(unittest.TestCase):
def setUp(self) -> None:
self.name = "bot-bottle-orch-gateway-itest-" + secrets.token_hex(4)
@@ -12,12 +12,12 @@ import subprocess
import unittest
from bot_bottle.orchestrator.gateway import DockerGateway
from tests._docker import skip_unless_docker
from tests._backend import skip_unless_backend
IMAGE = "busybox"
@skip_unless_docker()
@skip_unless_backend("docker")
class TestDockerGatewayImageExists(unittest.TestCase):
def test_image_exists_true_for_present_false_for_absent(self) -> None:
# Ensure the tiny image is present (build_if_missing is disabled here
+2 -2
View File
@@ -18,10 +18,10 @@ from bot_bottle.backend.docker.network import (
network_create_internal,
network_remove,
)
from tests._docker import skip_unless_docker
from tests._backend import skip_unless_backend
@skip_unless_docker()
@skip_unless_backend("docker")
class TestOrphanCleanup(unittest.TestCase):
def setUp(self):
self.slug = f"cb-test-orphan-{os.getpid()}"
+2 -2
View File
@@ -31,7 +31,7 @@ from pathlib import Path
from bot_bottle.backend import BottleSpec, get_bottle_backend
from bot_bottle.bottle_state import cleanup_state
from bot_bottle.manifest import ManifestIndex
from tests._docker import skip_unless_docker_or_firecracker
from tests._backend import skip_unless_selected_backend_available
# Secrets planted in the bottle env as literals (agents substitute via
@@ -67,7 +67,7 @@ _DUMMY_HOST_KEY = (
)
@skip_unless_docker_or_firecracker()
@skip_unless_selected_backend_available()
@unittest.skipIf(
os.environ.get("GITEA_ACTIONS") == "true"
and os.environ.get("BOT_BOTTLE_BACKEND") != "firecracker",
+188
View File
@@ -0,0 +1,188 @@
"""Tests for the backend-aware capability probes and skip guards
(``tests/_backend.py``) and the CI preflight (``tests/backend_preflight.py``).
The probes shell out to Docker / inspect ``/dev/kvm``; here they are driven
entirely through mocks so the unit job asserts the decision logic without
needing either backend present on the runner.
"""
from __future__ import annotations
import os
import unittest
from unittest.mock import patch
from tests import _backend, backend_preflight
from tests._backend import (
Capability,
backend_capability,
docker_capability,
firecracker_capability,
selected_backend,
skip_unless_backend,
skip_unless_selected_backend_available,
)
def _skipped(decorated: type) -> bool:
return getattr(decorated, "__unittest_skip__", False)
def _new_case() -> type:
return type("Case", (unittest.TestCase,), {})
class TestSelectedBackend(unittest.TestCase):
def test_defaults_to_docker_when_unset(self):
with patch.dict(os.environ, {}, clear=True):
self.assertEqual("docker", selected_backend())
def test_reads_env(self):
with patch.dict(os.environ, {"BOT_BOTTLE_BACKEND": "firecracker"}, clear=True):
self.assertEqual("firecracker", selected_backend())
class TestDockerCapability(unittest.TestCase):
def test_opt_out_via_skip_docker_tests(self):
with patch.dict(os.environ, {"SKIP_DOCKER_TESTS": "1"}, clear=True):
cap = docker_capability()
self.assertFalse(cap.ok)
self.assertIn("SKIP_DOCKER_TESTS", cap.detail)
def test_not_on_path(self):
with patch.dict(os.environ, {}, clear=True), \
patch("tests._backend.shutil.which", return_value=None):
cap = docker_capability()
self.assertFalse(cap.ok)
self.assertIn("PATH", cap.detail)
def test_daemon_reachable(self):
with patch.dict(os.environ, {}, clear=True), \
patch("tests._backend.shutil.which", return_value="/usr/bin/docker"), \
patch("tests._backend.subprocess.run") as run:
run.return_value.returncode = 0
cap = docker_capability()
self.assertTrue(cap.ok)
def test_daemon_unreachable(self):
with patch.dict(os.environ, {}, clear=True), \
patch("tests._backend.shutil.which", return_value="/usr/bin/docker"), \
patch("tests._backend.subprocess.run") as run:
run.return_value.returncode = 1
cap = docker_capability()
self.assertFalse(cap.ok)
self.assertIn("unreachable", cap.detail)
class TestFirecrackerCapability(unittest.TestCase):
def test_kvm_missing(self):
with patch("tests._backend.os.path.exists", return_value=False):
cap = firecracker_capability()
self.assertFalse(cap.ok)
self.assertIn("KVM", cap.detail)
def test_kvm_inaccessible(self):
with patch("tests._backend.os.path.exists", return_value=True), \
patch("tests._backend.os.access", return_value=False):
cap = firecracker_capability()
self.assertFalse(cap.ok)
self.assertIn("kvm group", cap.detail)
def test_binary_missing(self):
with patch("tests._backend.os.path.exists", return_value=True), \
patch("tests._backend.os.access", return_value=True), \
patch("tests._backend.shutil.which", return_value=None):
cap = firecracker_capability()
self.assertFalse(cap.ok)
self.assertIn("firecracker not on PATH", cap.detail)
def test_ready(self):
with patch("tests._backend.os.path.exists", return_value=True), \
patch("tests._backend.os.access", return_value=True), \
patch("tests._backend.shutil.which", return_value="/usr/bin/firecracker"):
cap = firecracker_capability()
self.assertTrue(cap.ok)
class TestBackendCapability(unittest.TestCase):
def test_unknown_backend(self):
cap = backend_capability("qemu")
self.assertFalse(cap.ok)
self.assertIn("no capability probe", cap.detail)
def test_defaults_to_selected(self):
def probe() -> Capability:
return Capability("docker", True, "ok")
with patch.dict(os.environ, {"BOT_BOTTLE_BACKEND": "docker"}, clear=True), \
patch.dict("tests._backend._PROBES", {"docker": probe}):
cap = backend_capability()
self.assertEqual("docker", cap.backend)
self.assertTrue(cap.ok)
class TestSkipUnlessBackend(unittest.TestCase):
def test_skips_when_other_backend_selected(self):
with patch.dict(os.environ, {"BOT_BOTTLE_BACKEND": "firecracker"}, clear=True):
decorated = skip_unless_backend("docker")(_new_case())
self.assertTrue(_skipped(decorated))
def test_runs_when_selected_and_capable(self):
with patch.dict(os.environ, {"BOT_BOTTLE_BACKEND": "docker"}, clear=True), \
patch("tests._backend.backend_capability",
return_value=Capability("docker", True, "ok")):
decorated = skip_unless_backend("docker")(_new_case())
self.assertFalse(_skipped(decorated))
def test_skips_when_selected_but_incapable(self):
with patch.dict(os.environ, {"BOT_BOTTLE_BACKEND": "docker"}, clear=True), \
patch("tests._backend.backend_capability",
return_value=Capability("docker", False, "docker daemon unreachable")):
decorated = skip_unless_backend("docker")(_new_case())
self.assertTrue(_skipped(decorated))
class TestSkipUnlessSelectedBackendAvailable(unittest.TestCase):
def test_runs_when_selected_backend_capable(self):
with patch.dict(os.environ, {"BOT_BOTTLE_BACKEND": "firecracker"}, clear=True), \
patch("tests._backend.backend_capability",
return_value=Capability("firecracker", True, "ok")):
decorated = skip_unless_selected_backend_available()(_new_case())
self.assertFalse(_skipped(decorated))
def test_skips_when_selected_backend_incapable(self):
with patch.dict(os.environ, {"BOT_BOTTLE_BACKEND": "firecracker"}, clear=True), \
patch("tests._backend.backend_capability",
return_value=Capability("firecracker", False, "/dev/kvm missing")):
decorated = skip_unless_selected_backend_available()(_new_case())
self.assertTrue(_skipped(decorated))
class TestPreflightMain(unittest.TestCase):
def test_pass_exits_zero(self):
with patch.object(backend_preflight, "backend_capability",
return_value=Capability("docker", True, "docker daemon reachable")):
rc = backend_preflight.main(["docker"])
self.assertEqual(0, rc)
def test_fail_exits_nonzero(self):
with patch.object(backend_preflight, "backend_capability",
return_value=Capability("firecracker", False, "/dev/kvm missing")):
rc = backend_preflight.main(["firecracker"])
self.assertEqual(1, rc)
def test_defaults_to_selected_backend(self):
with patch.dict(os.environ, {"BOT_BOTTLE_BACKEND": "docker"}, clear=True), \
patch.object(backend_preflight, "backend_capability",
return_value=Capability("docker", True, "ok")) as cap:
rc = backend_preflight.main([])
self.assertEqual(0, rc)
cap.assert_called_once_with("docker")
def test_module_exposes_selected_backend(self):
# Import-surface guard: the preflight re-exports the selector it uses.
self.assertIs(backend_preflight.selected_backend, _backend.selected_backend)
if __name__ == "__main__":
unittest.main()
-35
View File
@@ -1,35 +0,0 @@
"""Tests for integration-test backend selection helpers."""
from __future__ import annotations
import os
import unittest
from unittest.mock import patch
from tests._docker import skip_unless_docker_or_firecracker
class TestSkipUnlessDockerOrFirecracker(unittest.TestCase):
def test_firecracker_runs_when_docker_tests_are_disabled(self):
with patch.dict(
os.environ,
{"BOT_BOTTLE_BACKEND": "firecracker", "SKIP_DOCKER_TESTS": "1"},
clear=True,
):
decorated = skip_unless_docker_or_firecracker()(type("Case", (), {}))
self.assertFalse(getattr(decorated, "__unittest_skip__", False))
def test_non_firecracker_still_skips_when_docker_tests_are_disabled(self):
with patch.dict(
os.environ,
{"BOT_BOTTLE_BACKEND": "docker", "SKIP_DOCKER_TESTS": "1"},
clear=True,
):
decorated = skip_unless_docker_or_firecracker()(type("Case", (), {}))
self.assertTrue(getattr(decorated, "__unittest_skip__", False))
if __name__ == "__main__":
unittest.main()