mirror of
https://github.com/basnijholt/compose-farm.git
synced 2026-02-03 14:13:26 +00:00
* fix: Ignore _version.py in type checkers The _version.py file is generated at build time by hatchling, so mypy and ty can't resolve it during development. * Update README.md * cli: Respect --host flag in stats summary and add tests - Fix --host filter to work in non-containers mode (was ignored) - Filter hosts table, pending migrations, and --live queries by host - Add tests for stats --containers functionality * refactor: Remove redundant _format_bytes wrappers Use format_bytes directly from glances module instead of wrapper functions that add no value. * Fix stats --host filtering * refactor: Move validate_hosts to top-level imports
169 lines
5.9 KiB
Python
169 lines
5.9 KiB
Python
"""Tests for CLI monitoring commands (stats)."""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
import typer
|
|
|
|
from compose_farm.cli.monitoring import _build_summary_table, stats
|
|
from compose_farm.config import Config, Host
|
|
from compose_farm.glances import ContainerStats
|
|
|
|
|
|
def _make_config(tmp_path: Path, glances_stack: str | None = None) -> Config:
|
|
"""Create a minimal config for testing."""
|
|
config_path = tmp_path / "compose-farm.yaml"
|
|
config_path.write_text("")
|
|
|
|
return Config(
|
|
compose_dir=tmp_path / "compose",
|
|
hosts={"host1": Host(address="localhost")},
|
|
stacks={"svc1": "host1"},
|
|
config_path=config_path,
|
|
glances_stack=glances_stack,
|
|
)
|
|
|
|
|
|
class TestStatsCommand:
|
|
"""Tests for the stats command."""
|
|
|
|
def test_stats_containers_requires_glances_config(
|
|
self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
"""--containers fails if glances_stack is not configured."""
|
|
cfg = _make_config(tmp_path, glances_stack=None)
|
|
|
|
with (
|
|
patch("compose_farm.cli.monitoring.load_config_or_exit", return_value=cfg),
|
|
pytest.raises(typer.Exit) as exc_info,
|
|
):
|
|
stats(live=False, containers=True, host=None, config=None)
|
|
|
|
assert exc_info.value.exit_code == 1
|
|
captured = capsys.readouterr()
|
|
assert "Glances not configured" in captured.err
|
|
|
|
def test_stats_containers_success(
|
|
self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
"""--containers fetches and displays container stats."""
|
|
cfg = _make_config(tmp_path, glances_stack="glances")
|
|
|
|
mock_containers = [
|
|
ContainerStats(
|
|
name="nginx",
|
|
host="host1",
|
|
status="running",
|
|
image="nginx:latest",
|
|
cpu_percent=10.5,
|
|
memory_usage=100 * 1024 * 1024,
|
|
memory_limit=1024 * 1024 * 1024,
|
|
memory_percent=10.0,
|
|
network_rx=1000,
|
|
network_tx=2000,
|
|
uptime="1h",
|
|
ports="80->80",
|
|
engine="docker",
|
|
stack="web",
|
|
service="nginx",
|
|
)
|
|
]
|
|
|
|
async def mock_fetch_async(
|
|
cfg: Config, hosts: list[str] | None = None
|
|
) -> list[ContainerStats]:
|
|
return mock_containers
|
|
|
|
with (
|
|
patch("compose_farm.cli.monitoring.load_config_or_exit", return_value=cfg),
|
|
patch(
|
|
"compose_farm.glances.fetch_all_container_stats", side_effect=mock_fetch_async
|
|
) as mock_fetch,
|
|
):
|
|
stats(live=False, containers=True, host=None, config=None)
|
|
|
|
mock_fetch.assert_called_once_with(cfg, hosts=None)
|
|
|
|
captured = capsys.readouterr()
|
|
# Verify table output
|
|
assert "nginx" in captured.out
|
|
assert "host1" in captured.out
|
|
assert "runni" in captured.out
|
|
assert "10.5%" in captured.out
|
|
|
|
def test_stats_containers_empty(
|
|
self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
"""--containers handles empty result gracefully."""
|
|
cfg = _make_config(tmp_path, glances_stack="glances")
|
|
|
|
async def mock_fetch_empty(
|
|
cfg: Config, hosts: list[str] | None = None
|
|
) -> list[ContainerStats]:
|
|
return []
|
|
|
|
with (
|
|
patch("compose_farm.cli.monitoring.load_config_or_exit", return_value=cfg),
|
|
patch("compose_farm.glances.fetch_all_container_stats", side_effect=mock_fetch_empty),
|
|
):
|
|
with pytest.raises(typer.Exit) as exc_info:
|
|
stats(live=False, containers=True, host=None, config=None)
|
|
|
|
assert exc_info.value.exit_code == 0
|
|
|
|
captured = capsys.readouterr()
|
|
assert "No containers found" in captured.err
|
|
|
|
def test_stats_containers_host_filter(self, tmp_path: Path) -> None:
|
|
"""--host limits container queries in --containers mode."""
|
|
cfg = _make_config(tmp_path, glances_stack="glances")
|
|
|
|
async def mock_fetch_async(
|
|
cfg: Config, hosts: list[str] | None = None
|
|
) -> list[ContainerStats]:
|
|
return []
|
|
|
|
with (
|
|
patch("compose_farm.cli.monitoring.load_config_or_exit", return_value=cfg),
|
|
patch(
|
|
"compose_farm.glances.fetch_all_container_stats", side_effect=mock_fetch_async
|
|
) as mock_fetch,
|
|
pytest.raises(typer.Exit),
|
|
):
|
|
stats(live=False, containers=True, host="host1", config=None)
|
|
|
|
mock_fetch.assert_called_once_with(cfg, hosts=["host1"])
|
|
|
|
def test_stats_summary_respects_host_filter(self, tmp_path: Path) -> None:
|
|
"""--host filters summary counts to the selected host."""
|
|
compose_dir = tmp_path / "compose"
|
|
for name in ("svc1", "svc2", "svc3"):
|
|
stack_dir = compose_dir / name
|
|
stack_dir.mkdir(parents=True)
|
|
(stack_dir / "compose.yaml").write_text("services: {}\n")
|
|
|
|
config_path = tmp_path / "compose-farm.yaml"
|
|
config_path.write_text("")
|
|
|
|
cfg = Config(
|
|
compose_dir=compose_dir,
|
|
hosts={
|
|
"host1": Host(address="localhost"),
|
|
"host2": Host(address="127.0.0.2"),
|
|
},
|
|
stacks={"svc1": "host1", "svc2": "host2", "svc3": "host1"},
|
|
config_path=config_path,
|
|
)
|
|
|
|
state: dict[str, str | list[str]] = {"svc1": "host1", "svc2": "host2"}
|
|
table = _build_summary_table(cfg, state, pending=[], host_filter="host1")
|
|
labels = table.columns[0]._cells
|
|
values = table.columns[1]._cells
|
|
summary = dict(zip(labels, values, strict=True))
|
|
|
|
assert summary["Total hosts"] == "1"
|
|
assert summary["Stacks (configured)"] == "2"
|
|
assert summary["Stacks (tracked)"] == "1"
|
|
assert summary["Compose files on disk"] == "2"
|