fix(cleanup): use authoritative resource identities
test / integration-docker (pull_request) Has been cancelled
test / image-input-builds (pull_request) Successful in 53s
test / unit (pull_request) Successful in 59s
test / coverage (pull_request) Has been skipped
tracker-policy-pr / check-pr (pull_request) Failing after 12m23s
test / integration-docker (pull_request) Has been cancelled
test / image-input-builds (pull_request) Successful in 53s
test / unit (pull_request) Successful in 59s
test / coverage (pull_request) Has been skipped
tracker-policy-pr / check-pr (pull_request) Failing after 12m23s
This commit is contained in:
@@ -14,9 +14,12 @@ from __future__ import annotations
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from tests.unit import use_bottle_root
|
||||
from bot_bottle import bottle_state
|
||||
from bot_bottle.backend import EnumerationError
|
||||
from bot_bottle.backend.docker import cleanup
|
||||
from bot_bottle.backend.docker.cleanup import _list_orphan_state_dirs
|
||||
|
||||
|
||||
@@ -116,5 +119,37 @@ class TestOrphanStateDirs(_FakeHomeMixin, unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class TestAuthoritativeDiscovery(unittest.TestCase):
|
||||
def test_prepare_requires_authoritative_compose_projects(self):
|
||||
with patch.object(cleanup.docker_mod, "require_docker"), \
|
||||
patch.object(
|
||||
cleanup, "list_compose_projects",
|
||||
side_effect=EnumerationError("compose unavailable"),
|
||||
) as projects, self.assertRaisesRegex(
|
||||
EnumerationError, "compose unavailable",
|
||||
):
|
||||
cleanup.prepare_cleanup()
|
||||
projects.assert_called_once_with(
|
||||
warn_on_error=False,
|
||||
raise_on_error=True,
|
||||
)
|
||||
|
||||
def test_container_query_failure_raises(self):
|
||||
failed = cleanup.subprocess.CompletedProcess(
|
||||
[], 1, stdout="", stderr="daemon unavailable",
|
||||
)
|
||||
with patch.object(cleanup.subprocess, "run", return_value=failed), \
|
||||
self.assertRaisesRegex(EnumerationError, "daemon unavailable"):
|
||||
cleanup._list_prefixed_containers()
|
||||
|
||||
def test_network_query_failure_raises(self):
|
||||
failed = cleanup.subprocess.CompletedProcess(
|
||||
[], 1, stdout="", stderr="daemon unavailable",
|
||||
)
|
||||
with patch.object(cleanup.subprocess, "run", return_value=failed), \
|
||||
self.assertRaisesRegex(EnumerationError, "daemon unavailable"):
|
||||
cleanup._list_prefixed_networks()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -32,15 +32,19 @@ class TestProcessScan(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
Path("/cache/run/dev-a"),
|
||||
fc_cleanup._run_dir_of(
|
||||
f"firecracker --config-file {run_root}/dev-a/config.json", run_root
|
||||
("firecracker", "--config-file",
|
||||
f"{run_root}/dev-a/config.json"), run_root
|
||||
),
|
||||
)
|
||||
# infra/builder VMs elsewhere, or nested paths, are not ours.
|
||||
self.assertIsNone(
|
||||
fc_cleanup._run_dir_of("firecracker --config-file /elsewhere/config.json", run_root)
|
||||
fc_cleanup._run_dir_of(
|
||||
("firecracker", "--config-file", "/elsewhere/config.json"),
|
||||
run_root,
|
||||
)
|
||||
)
|
||||
self.assertIsNone(
|
||||
fc_cleanup._run_dir_of("firecracker --no-config", run_root)
|
||||
fc_cleanup._run_dir_of(("firecracker", "--no-config"), run_root)
|
||||
)
|
||||
|
||||
def test_scan_splits_live_dirs_from_orphan_pids(self):
|
||||
@@ -48,17 +52,40 @@ class TestProcessScan(unittest.TestCase):
|
||||
run_root = Path(tmp)
|
||||
(run_root / "live-a").mkdir() # dir present -> live VM, protected
|
||||
# "gone-b" dir intentionally absent -> lingering VMM, orphan pid
|
||||
out = (
|
||||
f"111 firecracker --config-file {run_root}/live-a/config.json\n"
|
||||
f"222 firecracker --config-file {run_root}/gone-b/config.json\n"
|
||||
"333 firecracker --config-file /elsewhere/config.json\n"
|
||||
"notanint firecracker --config-file x\n"
|
||||
)
|
||||
with patch.object(fc_cleanup.subprocess, "run", return_value=_proc(out)):
|
||||
args = {
|
||||
111: ("firecracker", "--config-file",
|
||||
f"{run_root}/live-a/config.json"),
|
||||
222: ("firecracker", "--config-file",
|
||||
f"{run_root}/gone-b/config.json"),
|
||||
333: ("firecracker", "--config-file", "/elsewhere/config.json"),
|
||||
}
|
||||
with patch.object(
|
||||
fc_cleanup.subprocess, "run",
|
||||
return_value=_proc("111\n222\n333\nnotanint\n"),
|
||||
), patch.object(
|
||||
fc_cleanup, "_process_args", side_effect=args.get,
|
||||
):
|
||||
live, orphan_pids = fc_cleanup._scan_processes(run_root)
|
||||
self.assertEqual({str(run_root / "live-a")}, live)
|
||||
self.assertEqual([222], orphan_pids)
|
||||
|
||||
def test_scan_preserves_spaces_in_config_path(self):
|
||||
with tempfile.TemporaryDirectory(prefix="fc cache ") as tmp:
|
||||
run_root = Path(tmp)
|
||||
live = run_root / "live bottle"
|
||||
live.mkdir()
|
||||
with patch.object(
|
||||
fc_cleanup.subprocess, "run", return_value=_proc("111\n"),
|
||||
), patch.object(
|
||||
fc_cleanup, "_process_args",
|
||||
return_value=("firecracker", "--config-file",
|
||||
str(live / "config.json")),
|
||||
):
|
||||
self.assertEqual(
|
||||
({str(live)}, []),
|
||||
fc_cleanup._scan_processes(run_root),
|
||||
)
|
||||
|
||||
def test_scan_empty_when_pgrep_finds_no_processes(self):
|
||||
with patch.object(fc_cleanup.subprocess, "run", return_value=_proc(returncode=1)):
|
||||
self.assertEqual((set(), []), fc_cleanup._scan_processes(Path("/x")))
|
||||
@@ -117,9 +144,14 @@ class TestProcessScan(unittest.TestCase):
|
||||
run_root = Path(tmp)
|
||||
(run_root / "live-a").mkdir()
|
||||
(run_root / "dead-b").mkdir()
|
||||
out = f"111 firecracker --config-file {run_root}/live-a/config.json\n"
|
||||
with patch.object(fc_cleanup, "_run_root", return_value=run_root), \
|
||||
patch.object(fc_cleanup.subprocess, "run", return_value=_proc(out)):
|
||||
patch.object(fc_cleanup.subprocess, "run",
|
||||
return_value=_proc("111\n")), \
|
||||
patch.object(
|
||||
fc_cleanup, "_process_args",
|
||||
return_value=("firecracker", "--config-file",
|
||||
str(run_root / "live-a/config.json")),
|
||||
):
|
||||
plan = fc_cleanup.prepare_cleanup()
|
||||
self.assertEqual((), plan.vm_pids)
|
||||
self.assertEqual((str(run_root / "dead-b"),), plan.run_dirs)
|
||||
|
||||
@@ -284,6 +284,18 @@ class TestEnsureArtifact(_CacheMixin):
|
||||
ia.ensure_artifact_gz(version, role=_ROLE)
|
||||
self.assertEqual(first_calls, len(net.calls))
|
||||
|
||||
def test_download_uses_network_deadline(self) -> None:
|
||||
response = mock.MagicMock()
|
||||
response.__enter__.return_value = io.BytesIO(b"payload")
|
||||
with tempfile.TemporaryDirectory() as d, mock.patch.object(
|
||||
ia.urllib.request, "urlopen", return_value=response,
|
||||
) as urlopen:
|
||||
ia._download("https://registry/artifact", Path(d) / "artifact")
|
||||
self.assertEqual(
|
||||
ia.ARTIFACT_HTTP_TIMEOUT_SECONDS,
|
||||
urlopen.call_args.kwargs["timeout"],
|
||||
)
|
||||
|
||||
def test_checksum_mismatch_fails_closed(self) -> None:
|
||||
version = "beefbeefbeefbeef"
|
||||
gz = _gz(b"payload")
|
||||
|
||||
@@ -49,6 +49,16 @@ class TestPut(unittest.TestCase):
|
||||
self.assertNotIsInstance(req.data, (bytes, bytearray))
|
||||
self.assertEqual(str(len(payload)), req.get_header("Content-length"))
|
||||
|
||||
def test_put_uses_network_deadline(self) -> None:
|
||||
with mock.patch.object(
|
||||
pub.urllib.request, "urlopen", return_value=_Resp(),
|
||||
) as urlopen:
|
||||
pub._put("https://reg/pkg", b"payload", token="")
|
||||
self.assertEqual(
|
||||
pub._REGISTRY_HTTP_TIMEOUT_SECONDS,
|
||||
urlopen.call_args.kwargs["timeout"],
|
||||
)
|
||||
|
||||
def test_small_bytes_body_still_works(self) -> None:
|
||||
captured: list[urllib.request.Request] = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user