refactor(backend): type enumeration failures
This commit is contained in:
@@ -30,6 +30,7 @@ if TYPE_CHECKING:
|
|||||||
BottleImages,
|
BottleImages,
|
||||||
BottlePlan,
|
BottlePlan,
|
||||||
BottleSpec,
|
BottleSpec,
|
||||||
|
EnumerationError,
|
||||||
ExecResult,
|
ExecResult,
|
||||||
)
|
)
|
||||||
from .selection import (
|
from .selection import (
|
||||||
@@ -59,6 +60,7 @@ _LAZY_MODULES: dict[str, str] = {
|
|||||||
"BottleImages": "base",
|
"BottleImages": "base",
|
||||||
"BottleBackend": "base",
|
"BottleBackend": "base",
|
||||||
"BackendStatus": "base",
|
"BackendStatus": "base",
|
||||||
|
"EnumerationError": "base",
|
||||||
"get_bottle_backend": "selection",
|
"get_bottle_backend": "selection",
|
||||||
"known_backend_names": "selection",
|
"known_backend_names": "selection",
|
||||||
"has_backend": "selection",
|
"has_backend": "selection",
|
||||||
@@ -100,6 +102,7 @@ __all__ = [
|
|||||||
"BottlePlan",
|
"BottlePlan",
|
||||||
"BottleSpec",
|
"BottleSpec",
|
||||||
"ExecResult",
|
"ExecResult",
|
||||||
|
"EnumerationError",
|
||||||
"CommitCancelled",
|
"CommitCancelled",
|
||||||
"Freezer",
|
"Freezer",
|
||||||
"get_freezer",
|
"get_freezer",
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ class BackendStatus(enum.IntEnum):
|
|||||||
READY = 0
|
READY = 0
|
||||||
|
|
||||||
|
|
||||||
|
class EnumerationError(RuntimeError):
|
||||||
|
"""A backend could not produce an authoritative live-resource snapshot."""
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class BottleSpec:
|
class BottleSpec:
|
||||||
"""CLI-supplied intent. Backend-agnostic — each backend's prepare
|
"""CLI-supplied intent. Backend-agnostic — each backend's prepare
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from pathlib import Path
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from ...log import die, warn
|
from ...log import die, warn
|
||||||
|
from ..base import EnumerationError
|
||||||
|
|
||||||
|
|
||||||
# --- Lifecycle helpers (PRD 0018 chunk 3) ----------------------------------
|
# --- Lifecycle helpers (PRD 0018 chunk 3) ----------------------------------
|
||||||
@@ -75,12 +76,14 @@ def list_compose_projects(
|
|||||||
)
|
)
|
||||||
except FileNotFoundError as exc:
|
except FileNotFoundError as exc:
|
||||||
if raise_on_error:
|
if raise_on_error:
|
||||||
raise RuntimeError("docker compose ls failed: docker not found") from exc
|
raise EnumerationError(
|
||||||
|
"docker compose ls failed: docker not found"
|
||||||
|
) from exc
|
||||||
return []
|
return []
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
message = f"docker compose ls failed: {result.stderr.strip()}"
|
message = f"docker compose ls failed: {result.stderr.strip()}"
|
||||||
if raise_on_error:
|
if raise_on_error:
|
||||||
raise RuntimeError(message)
|
raise EnumerationError(message)
|
||||||
if warn_on_error:
|
if warn_on_error:
|
||||||
warn(message)
|
warn(message)
|
||||||
return []
|
return []
|
||||||
@@ -89,7 +92,7 @@ def list_compose_projects(
|
|||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
message = f"docker compose ls returned malformed JSON: {e}"
|
message = f"docker compose ls returned malformed JSON: {e}"
|
||||||
if raise_on_error:
|
if raise_on_error:
|
||||||
raise RuntimeError(message) from e
|
raise EnumerationError(message) from e
|
||||||
if warn_on_error:
|
if warn_on_error:
|
||||||
warn(message)
|
warn(message)
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from .. import ActiveAgent
|
from .. import ActiveAgent, EnumerationError
|
||||||
from ...bottle_state import read_metadata
|
from ...bottle_state import read_metadata
|
||||||
from .compose import compose_project_name, list_active_slugs
|
from .compose import compose_project_name, list_active_slugs
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ def _query_services_by_project() -> dict[str, set[str]]:
|
|||||||
capture_output=True, text=True, check=False,
|
capture_output=True, text=True, check=False,
|
||||||
)
|
)
|
||||||
except FileNotFoundError as exc:
|
except FileNotFoundError as exc:
|
||||||
raise RuntimeError("docker ps failed: docker not found") from exc
|
raise EnumerationError("docker ps failed: docker not found") from exc
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
raise RuntimeError(f"docker ps failed: {r.stderr.strip()}")
|
raise EnumerationError(f"docker ps failed: {r.stderr.strip()}")
|
||||||
return _parse_services_by_project(r.stdout or "")
|
return _parse_services_by_project(r.stdout or "")
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from ...bottle_state import read_metadata
|
from ...bottle_state import read_metadata
|
||||||
from .. import ActiveAgent
|
from .. import ActiveAgent, EnumerationError
|
||||||
from .infra import INFRA_NAME, ORCHESTRATOR_NAME
|
from .infra import INFRA_NAME, ORCHESTRATOR_NAME
|
||||||
|
|
||||||
# The name every agent container carries: `bot-bottle-<slug>`. Exported
|
# The name every agent container carries: `bot-bottle-<slug>`. Exported
|
||||||
@@ -20,17 +20,18 @@ CONTAINER_NAME_PREFIX = "bot-bottle-"
|
|||||||
_INFRA_NAMES = frozenset({INFRA_NAME, ORCHESTRATOR_NAME})
|
_INFRA_NAMES = frozenset({INFRA_NAME, ORCHESTRATOR_NAME})
|
||||||
|
|
||||||
|
|
||||||
class EnumerationError(RuntimeError):
|
|
||||||
"""container list failed; the resulting live set is not authoritative."""
|
|
||||||
|
|
||||||
|
|
||||||
def enumerate_active() -> list[ActiveAgent]:
|
def enumerate_active() -> list[ActiveAgent]:
|
||||||
result = subprocess.run(
|
try:
|
||||||
["container", "list", "--quiet"],
|
result = subprocess.run(
|
||||||
capture_output=True,
|
["container", "list", "--quiet"],
|
||||||
text=True,
|
capture_output=True,
|
||||||
check=False,
|
text=True,
|
||||||
)
|
check=False,
|
||||||
|
)
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise EnumerationError(
|
||||||
|
"container list failed: container CLI not found"
|
||||||
|
) from exc
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise EnumerationError(
|
raise EnumerationError(
|
||||||
f"container list failed: "
|
f"container list failed: "
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from bot_bottle.backend.docker.compose import (
|
|||||||
list_compose_projects,
|
list_compose_projects,
|
||||||
slug_from_compose_project,
|
slug_from_compose_project,
|
||||||
)
|
)
|
||||||
|
from bot_bottle.backend import EnumerationError
|
||||||
|
|
||||||
|
|
||||||
class TestProjectNaming(unittest.TestCase):
|
class TestProjectNaming(unittest.TestCase):
|
||||||
@@ -76,7 +77,7 @@ class TestComposeProjectListing(unittest.TestCase):
|
|||||||
args=["docker"], returncode=1, stdout="", stderr="no daemon",
|
args=["docker"], returncode=1, stdout="", stderr="no daemon",
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
with self.assertRaisesRegex(RuntimeError, "no daemon"):
|
with self.assertRaisesRegex(EnumerationError, "no daemon"):
|
||||||
list_active_slugs(
|
list_active_slugs(
|
||||||
warn_on_error=False,
|
warn_on_error=False,
|
||||||
raise_on_error=True,
|
raise_on_error=True,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from unittest.mock import patch
|
|||||||
from tests.unit import use_bottle_root
|
from tests.unit import use_bottle_root
|
||||||
from bot_bottle import bottle_state
|
from bot_bottle import bottle_state
|
||||||
from bot_bottle.backend.docker import enumerate as _enumerate
|
from bot_bottle.backend.docker import enumerate as _enumerate
|
||||||
|
from bot_bottle.backend import EnumerationError
|
||||||
|
|
||||||
|
|
||||||
class TestParseServicesByProject(unittest.TestCase):
|
class TestParseServicesByProject(unittest.TestCase):
|
||||||
@@ -82,7 +83,7 @@ class TestQueryServicesByProject(unittest.TestCase):
|
|||||||
args=["docker"], returncode=1, stdout="", stderr="daemon down",
|
args=["docker"], returncode=1, stdout="", stderr="daemon down",
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
with self.assertRaisesRegex(RuntimeError, "daemon down"):
|
with self.assertRaisesRegex(EnumerationError, "daemon down"):
|
||||||
_enumerate._query_services_by_project()
|
_enumerate._query_services_by_project()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from bot_bottle.backend import EnumerationError
|
||||||
from bot_bottle.backend.macos_container import cleanup, enumerate as enum_mod
|
from bot_bottle.backend.macos_container import cleanup, enumerate as enum_mod
|
||||||
from bot_bottle.backend.macos_container.bottle_cleanup_plan import (
|
from bot_bottle.backend.macos_container.bottle_cleanup_plan import (
|
||||||
MacosContainerBottleCleanupPlan,
|
MacosContainerBottleCleanupPlan,
|
||||||
@@ -69,10 +70,18 @@ class TestMacosContainerEnumerate(unittest.TestCase):
|
|||||||
self.assertEqual(["dev-abc"], [a.slug for a in agents])
|
self.assertEqual(["dev-abc"], [a.slug for a in agents])
|
||||||
|
|
||||||
def test_raises_when_the_cli_fails(self):
|
def test_raises_when_the_cli_fails(self):
|
||||||
from bot_bottle.backend.macos_container.enumerate import EnumerationError
|
|
||||||
with self.assertRaises(EnumerationError):
|
with self.assertRaises(EnumerationError):
|
||||||
self._enumerate("", returncode=1)
|
self._enumerate("", returncode=1)
|
||||||
|
|
||||||
|
def test_raises_typed_error_when_cli_is_missing(self):
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
enum_mod.subprocess, "run", side_effect=FileNotFoundError,
|
||||||
|
),
|
||||||
|
self.assertRaisesRegex(EnumerationError, "CLI not found"),
|
||||||
|
):
|
||||||
|
enum_mod.enumerate_active()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user