Push builds have been RED on the lint step since ~build 209 from accumulated formatting drift. This is the mechanical cleanup: ruff format + ruff --fix (UP038 isinstance unions, SIM105 contextlib.suppress, UP031 f-strings, SIM115 tempfile context manager), shfmt -i 2 -ci, nixpkgs-fmt/statix/deadnix (merged attrsets, dropped unused lib args), yamllint, and shell quoting fixes in tests/lasuite-docs/setup_custom_tests.sh. No behaviour changes intended; lint: PASS, unit tests: 138 passed.
173 lines
6.1 KiB
Python
173 lines
6.1 KiB
Python
"""Phase 3 U4 — dashboard YunoHost-style grid + per-recipe history (pure-render + helpers).
|
|
|
|
The dashboard reads a Drone admin token at import; point DRONE_TOKEN_FILE at a temp file so the
|
|
module imports without the real secret. All tests here are pure (no network): they exercise the
|
|
rendering + results.json projection, asserting the grid/history mirror the artifact and never present
|
|
a run greener than its data (R5 / cardinal guardrail)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
|
|
with tempfile.NamedTemporaryFile("w", delete=False, suffix=".tok") as _tok:
|
|
_tok.write("test-token")
|
|
os.environ["DRONE_TOKEN_FILE"] = _tok.name
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "dashboard"))
|
|
|
|
import dashboard # noqa: E402
|
|
|
|
|
|
def _row(**kw):
|
|
base = {
|
|
"recipe": "custom-html",
|
|
"status": "success",
|
|
"number": 4,
|
|
"ref": "db9a9502",
|
|
"version": "db9a95024e9d",
|
|
"level": 4,
|
|
"level_cap_reason": "",
|
|
"has_screenshot": True,
|
|
"flags": {"clean_teardown": True, "no_secret_leak": True},
|
|
"finished": 0,
|
|
"url": "https://drone.x/cc-ci/4",
|
|
}
|
|
base.update(kw)
|
|
return base
|
|
|
|
|
|
def test_level_color_ramp_and_fallback():
|
|
assert dashboard.level_color(0) == "#e5534b"
|
|
assert dashboard.level_color(6) == "#3fb950"
|
|
assert dashboard.level_color(4) == "#a0b93f"
|
|
assert dashboard.level_color(99) == "#8b949e"
|
|
assert dashboard.level_color(None) == "#8b949e"
|
|
|
|
|
|
def test_overview_grid_mirrors_results():
|
|
out = dashboard.render_overview([_row()])
|
|
assert "custom-html" in out
|
|
assert "level 4" in out # the corner level pill
|
|
assert dashboard.level_color(4) in out # coloured by level
|
|
assert "db9a95024e9d" in out # version from results.json
|
|
assert "/runs/4/screenshot.png" in out # thumbnail
|
|
assert "/runs/4/summary.png" in out # links to full card
|
|
assert "/recipe/custom-html" in out # history link
|
|
assert "✔ teardown" in out and "✔ no-leak" in out
|
|
|
|
|
|
def test_overview_never_greener_than_data():
|
|
# A failed run at level 0 must show level 0 + the failure pill — never a green/high level.
|
|
out = dashboard.render_overview(
|
|
[
|
|
_row(
|
|
status="failure",
|
|
level=0,
|
|
has_screenshot=False,
|
|
flags={},
|
|
level_cap_reason="L1 install FAILED",
|
|
)
|
|
]
|
|
)
|
|
assert "level 0" in out
|
|
assert dashboard.level_color(0) in out # red
|
|
assert dashboard._COLORS["failure"] in out
|
|
assert "level 4" not in out and "level 5" not in out and "level 6" not in out
|
|
assert "no screenshot" in out # placeholder, no broken image
|
|
|
|
|
|
def test_level_pill_unknown_when_no_results():
|
|
assert "level —" in dashboard._level_pill(None)
|
|
assert "#8b949e" in dashboard._level_pill(None)
|
|
|
|
|
|
def test_history_table_lists_runs():
|
|
out = dashboard.render_history("custom-html", [_row(number=4), _row(number=3, level=2)])
|
|
assert "custom-html — run history" in out
|
|
assert "#4" in out and "#3" in out
|
|
assert "L4" in out and "L2" in out
|
|
assert "← all recipes" in out
|
|
assert "/runs/4/summary.png" in out # per-run card link
|
|
|
|
|
|
def test_history_empty():
|
|
out = dashboard.render_history("hedgedoc", [])
|
|
assert "no runs for this recipe yet" in out
|
|
|
|
|
|
def test_build_row_projects_results(monkeypatch):
|
|
monkeypatch.setattr(
|
|
dashboard,
|
|
"_results_for",
|
|
lambda n: {
|
|
"version": "1.2.3",
|
|
"level": 2,
|
|
"level_cap_reason": "cap",
|
|
"screenshot": "screenshot.png",
|
|
"flags": {"clean_teardown": True},
|
|
},
|
|
)
|
|
b = {
|
|
"number": 7,
|
|
"status": "success",
|
|
"event": "custom",
|
|
"params": {"RECIPE": "n8n", "REF": "abcdef1234567890"},
|
|
"finished": 10,
|
|
}
|
|
r = dashboard._build_row(b)
|
|
assert r["recipe"] == "n8n" and r["number"] == 7
|
|
assert r["level"] == 2 and r["version"] == "1.2.3"
|
|
assert r["has_screenshot"] is True
|
|
assert r["url"].endswith("/cc-ci/7")
|
|
|
|
|
|
def test_build_row_degrades_without_results(monkeypatch):
|
|
# No results.json (e.g. an old run): grid still renders from Drone fields, level absent.
|
|
monkeypatch.setattr(dashboard, "_results_for", lambda n: {})
|
|
b = {
|
|
"number": 9,
|
|
"status": "running",
|
|
"event": "custom",
|
|
"params": {"RECIPE": "ghost", "REF": "deadbeefcafe1234567890"},
|
|
"finished": 0,
|
|
}
|
|
r = dashboard._build_row(b)
|
|
assert r["level"] is None and r["has_screenshot"] is False
|
|
assert r["version"] == "deadbeefcafe" # ref[:12] fallback
|
|
# render must not crash or claim a level
|
|
assert "level —" in dashboard.render_overview([r])
|
|
|
|
|
|
def test_level_badge_shows_level_coloured(monkeypatch):
|
|
svg = dashboard.render_level_badge("custom-html", 4)
|
|
assert "custom-html" in svg and "level 4" in svg
|
|
assert dashboard.level_color(4) in svg # coloured by level
|
|
assert svg.startswith("<svg") and "image" not in svg # plain SVG
|
|
# A higher displayed level than earned would be inflation — badge shows exactly the given level.
|
|
assert "level 5" not in svg and "level 6" not in svg
|
|
|
|
|
|
def test_status_badge_fallback_when_no_level():
|
|
# Recipe with no results.json level → status badge, not a fabricated level.
|
|
svg = dashboard.render_badge("ghost", "failure")
|
|
assert "failure" in svg and "level" not in svg
|
|
assert dashboard._COLORS["failure"] in svg
|
|
|
|
|
|
def test_results_for_traversal_guarded():
|
|
with tempfile.TemporaryDirectory() as d:
|
|
os.makedirs(os.path.join(d, "5"))
|
|
with open(os.path.join(d, "5", "results.json"), "w") as f:
|
|
json.dump({"level": 3}, f)
|
|
orig = dashboard.CCCI_RUNS_DIR
|
|
dashboard.CCCI_RUNS_DIR = d
|
|
try:
|
|
assert dashboard._results_for("5") == {"level": 3}
|
|
assert dashboard._results_for("../../etc") == {} # traversal rejected
|
|
assert dashboard._results_for("nonexist") == {} # missing → {}
|
|
assert dashboard._results_for("") == {}
|
|
finally:
|
|
dashboard.CCCI_RUNS_DIR = orig
|