c08b09dc9f
Assisted-by: Codex
149 lines
5.4 KiB
Python
149 lines
5.4 KiB
Python
"""Unit: smolmachines backend cleanup (`cleanup.py` +
|
|
`bottle_cleanup_plan.py`).
|
|
|
|
Tests mock `subprocess.run` + `has_backend` so they execute
|
|
without docker / smolvm on PATH. Each cleanup step verifies argv
|
|
shape; teardown verifies order (machines → bundles → networks)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from bot_bottle import backend as backend_mod
|
|
from bot_bottle.backend.smolmachines import cleanup
|
|
from bot_bottle.backend.smolmachines.bottle_cleanup_plan import (
|
|
SmolmachinesBottleCleanupPlan,
|
|
)
|
|
|
|
|
|
def _ok(stdout: str = "", stderr: str = "") -> subprocess.CompletedProcess:
|
|
return subprocess.CompletedProcess(
|
|
args=[], returncode=0, stdout=stdout, stderr=stderr,
|
|
)
|
|
|
|
|
|
class TestPrepareCleanup(unittest.TestCase):
|
|
def test_empty_when_nothing_running(self):
|
|
with patch.object(cleanup, "_smolvm") as smolvm, \
|
|
patch.object(cleanup.subprocess, "run") as run, \
|
|
patch.object(backend_mod, "has_backend", return_value=True):
|
|
smolvm.is_available.return_value = True
|
|
run.return_value = _ok(stdout="[]")
|
|
plan = cleanup.prepare_cleanup()
|
|
self.assertTrue(plan.empty)
|
|
|
|
def test_lists_machines_bundles_networks(self):
|
|
def fake_run(argv, *a, **kw):
|
|
if argv[:3] == ["smolvm", "machine", "ls"]:
|
|
return _ok(stdout=(
|
|
'[{"name":"bot-bottle-a-1","state":"running"},'
|
|
' {"name":"bot-bottle-b-2","state":"created"},'
|
|
' {"name":"unrelated","state":"running"}]'
|
|
))
|
|
if argv[:2] == ["docker", "ps"]:
|
|
return _ok(stdout=(
|
|
"bot-bottle-sidecars-a-1\n"
|
|
"bot-bottle-sidecars-b-2\n"
|
|
))
|
|
if argv[:3] == ["docker", "network", "ls"]:
|
|
return _ok(stdout=(
|
|
"bot-bottle-bundle-a-1\n"
|
|
"bot-bottle-bundle-b-2\n"
|
|
))
|
|
return _ok()
|
|
|
|
with patch.object(cleanup, "_smolvm") as smolvm, \
|
|
patch.object(cleanup.subprocess, "run", side_effect=fake_run), \
|
|
patch.object(backend_mod, "has_backend", return_value=True):
|
|
smolvm.is_available.return_value = True
|
|
plan = cleanup.prepare_cleanup()
|
|
|
|
# `unrelated` filtered out (no bot-bottle- prefix).
|
|
self.assertEqual(
|
|
("bot-bottle-a-1", "bot-bottle-b-2"),
|
|
plan.machines,
|
|
)
|
|
self.assertEqual(
|
|
("bot-bottle-sidecars-a-1", "bot-bottle-sidecars-b-2"),
|
|
plan.bundles,
|
|
)
|
|
self.assertEqual(
|
|
("bot-bottle-bundle-a-1", "bot-bottle-bundle-b-2"),
|
|
plan.networks,
|
|
)
|
|
|
|
def test_no_smolvm_means_no_machines(self):
|
|
with patch.object(cleanup, "_smolvm") as smolvm, \
|
|
patch.object(cleanup.subprocess, "run", return_value=_ok()), \
|
|
patch.object(backend_mod, "has_backend", return_value=True):
|
|
smolvm.is_available.return_value = False
|
|
plan = cleanup.prepare_cleanup()
|
|
self.assertEqual((), plan.machines)
|
|
|
|
|
|
class TestCleanup(unittest.TestCase):
|
|
def test_machines_stopped_then_deleted_then_bundles_then_networks(self):
|
|
plan = SmolmachinesBottleCleanupPlan(
|
|
machines=("bot-bottle-a-1",),
|
|
bundles=("bot-bottle-sidecars-a-1",),
|
|
networks=("bot-bottle-bundle-a-1",),
|
|
)
|
|
calls: list[list[str]] = []
|
|
|
|
def fake_run(argv, *a, **kw):
|
|
calls.append(list(argv[:4]))
|
|
return _ok()
|
|
|
|
with patch.object(cleanup.subprocess, "run", side_effect=fake_run):
|
|
cleanup.cleanup(plan)
|
|
|
|
# Stop precedes delete precedes bundle rm precedes network rm.
|
|
self.assertEqual(
|
|
["smolvm", "machine", "stop", "--name"], calls[0],
|
|
)
|
|
self.assertEqual(
|
|
["smolvm", "machine", "delete", "-f"], calls[1],
|
|
)
|
|
self.assertEqual(
|
|
["docker", "rm", "-f", "bot-bottle-sidecars-a-1"], calls[2],
|
|
)
|
|
self.assertEqual(
|
|
["docker", "network", "rm", "bot-bottle-bundle-a-1"], calls[3],
|
|
)
|
|
|
|
def test_failures_are_warnings_not_fatal(self):
|
|
# smolvm machine delete -f returning non-zero should warn
|
|
# but continue with bundles + networks. The cleanup is
|
|
# idempotent on success and tries to remove every resource.
|
|
plan = SmolmachinesBottleCleanupPlan(
|
|
machines=("bot-bottle-a-1",),
|
|
bundles=("bot-bottle-sidecars-a-1",),
|
|
networks=(),
|
|
)
|
|
results = iter([
|
|
_ok(), # stop succeeds
|
|
subprocess.CompletedProcess(args=[], returncode=1, stdout="", stderr="boom"), # delete fails
|
|
_ok(), # bundle rm succeeds
|
|
])
|
|
|
|
def fake_run(argv, *a, **kw):
|
|
return next(results)
|
|
|
|
with patch.object(cleanup.subprocess, "run", side_effect=fake_run), \
|
|
patch.object(cleanup, "warn") as warn:
|
|
cleanup.cleanup(plan)
|
|
# warn called once for the delete failure.
|
|
warn.assert_called_once()
|
|
|
|
def test_empty_plan_is_noop(self):
|
|
plan = SmolmachinesBottleCleanupPlan()
|
|
with patch.object(cleanup.subprocess, "run") as run:
|
|
cleanup.cleanup(plan)
|
|
run.assert_not_called()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|