8f1c8c5cf7
The user-facing half (rule 27). Per-container log viewer + admin controls, so container logs are something the operator can actually touch. Viewer (plugins/docker): - /container/<host>/<name>/logs full page + /logs/lines HTMX fragment polled every 5s (near-live follow); stream filter (all/stdout/stderr) + case-insensitive text search; newest-first so the latest lines survive the poll's scroll reset; empty/loading states; a "View logs" link from the container detail page. Jinja auto-escapes log content (no markup injection). Settings (Thresholds & Retention tab): - global on/off toggle, comma-separated exclude list, retention days + max MB per container — DB-backed, no restart. The toggle + exclude are enforced authoritatively at ingest (_persist_logs drops disabled/excluded lines), since the push model has no channel to tell an agent to stop. Tests: route-defined smoke; template-parse covers the new templates; integration test for the ingest-time toggle + exclude enforcement. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CAGR73DUowdVFVvYzLXC5C
73 lines
2.7 KiB
Python
73 lines
2.7 KiB
Python
"""Unit tests for the docker plugin's presentation layer (no DB).
|
|
|
|
CI never renders these templates through the running app (the unit-lane app is
|
|
created with testing=True, which skips plugin loading), so a Jinja syntax error
|
|
would otherwise ship green. These tests parse every docker template and smoke
|
|
the routes module so a broken tag or import is caught in the unit lane.
|
|
"""
|
|
import pathlib
|
|
|
|
import jinja2
|
|
|
|
from plugins.docker import routes as r
|
|
|
|
_TEMPLATES = pathlib.Path(r.__file__).parent / "templates" / "docker"
|
|
|
|
|
|
def test_all_docker_templates_parse():
|
|
env = jinja2.Environment()
|
|
files = sorted(_TEMPLATES.glob("*.html"))
|
|
assert files, "no docker templates found"
|
|
for f in files:
|
|
# Raises TemplateSyntaxError on an unbalanced/typo'd tag.
|
|
env.parse(f.read_text())
|
|
|
|
|
|
def test_routes_module_exposes_new_views():
|
|
# Import-smoke + confirms the #942 view functions are defined.
|
|
for name in ("container_detail", "container_history", "swarm", "disk", "index", "rows"):
|
|
assert callable(getattr(r, name)), name
|
|
assert r.docker_bp.name == "docker"
|
|
|
|
|
|
def test_disk_prune_view_defined():
|
|
# M78 admin-gated prune action.
|
|
assert callable(r.disk_prune)
|
|
|
|
|
|
def test_container_log_views_defined():
|
|
# M79 per-container log viewer + its HTMX-polled line fragment.
|
|
assert callable(r.container_logs)
|
|
assert callable(r.container_logs_lines)
|
|
|
|
|
|
def test_prune_extra_vars_mapping():
|
|
"""The prune buttons drive one playbook via prune_target; only 'images'
|
|
widens to ALL unused images (docker image prune -a), system stays -f."""
|
|
assert r._prune_extra_vars("containers") == {"prune_target": "containers"}
|
|
assert r._prune_extra_vars("images") == {
|
|
"prune_target": "images", "prune_all_images": True,
|
|
}
|
|
# System prune stays conservative — no prune_all_images key.
|
|
assert r._prune_extra_vars("system") == {"prune_target": "system"}
|
|
|
|
|
|
def test_human_bytes_formats_binary_units():
|
|
assert r._human_bytes(None) == "—"
|
|
assert r._human_bytes(0) == "0 B"
|
|
assert r._human_bytes(512) == "512 B"
|
|
assert r._human_bytes(1024) == "1.0 KiB"
|
|
assert r._human_bytes(1536) == "1.5 KiB"
|
|
assert r._human_bytes(1024 ** 2) == "1.0 MiB"
|
|
assert r._human_bytes(int(1.5 * 1024 ** 3)) == "1.5 GiB"
|
|
assert r._human_bytes(1024 ** 4) == "1.0 TiB"
|
|
|
|
|
|
def test_human_uptime_compact():
|
|
from datetime import datetime, timedelta, timezone
|
|
now = datetime.now(timezone.utc)
|
|
assert r._human_uptime(None) is None
|
|
assert r._human_uptime(now - timedelta(minutes=8)).endswith("m")
|
|
assert "h" in r._human_uptime(now - timedelta(hours=5, minutes=12))
|
|
assert "d" in r._human_uptime(now - timedelta(days=3, hours=4))
|