Service page: - The header carries the verdict, mode, stack, image and when it was reported. 'In trouble since' shows only while the service is in trouble; once recovered, 'Last failed X ago · its logs'. - The failure card shows only while the service is down, degraded or crash-looping. - The tile strip and the kv card are gone. - One Tasks table: running tasks first with CPU, memory and since when, then Swarm's record. The node is named as its Steward host throughout, and 'N failed in 24h' sits in the caption. Container page: - Same shape: header verdict, a 'Last exit' card only while it is stopped after a failing exit, set-up and I/O in one kv card, and lifecycle as a table. - A swarm task's breadcrumb goes through its service. Both pages: - Resource history is the one fragment (_usage_history.html): now and peak in cores and bytes, drawn with a floor of 1 core / 1 GiB (sparkline_svg floor=), so idle noise draws flat. - The container page's old % sparklines, its private _sparkline and the unused bucket()/container_history() helpers are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
93 lines
3.1 KiB
Python
93 lines
3.1 KiB
Python
"""Unit tests for the status-source registry + sparkline (no DB, no Quart).
|
|
|
|
The DB-backed sources (ping/dns/http) are exercised in the integration lane;
|
|
here we cover the registry contract, ordering, failure isolation, and the
|
|
pure sparkline helper.
|
|
"""
|
|
import asyncio
|
|
|
|
from steward.core import status as status_mod
|
|
from steward.core.status import (
|
|
StatusEntry,
|
|
clear_status_sources,
|
|
collect_status,
|
|
register_status_source,
|
|
sparkline_svg,
|
|
)
|
|
|
|
|
|
def _entry(name, st, kind="ping"):
|
|
return StatusEntry(kind=kind, key=name, name=name, status=st)
|
|
|
|
|
|
def test_collect_orders_down_then_pending_then_up_alphabetical():
|
|
clear_status_sources()
|
|
|
|
async def src(db):
|
|
return [_entry("zeta", "up"), _entry("alpha", "down"),
|
|
_entry("beta", "pending"), _entry("gamma", "up")]
|
|
|
|
register_status_source(src)
|
|
entries = asyncio.run(collect_status(None))
|
|
assert [e.status for e in entries] == ["down", "pending", "up", "up"]
|
|
# within the trailing "up" group, alphabetical by name
|
|
assert entries[2].name == "gamma"
|
|
assert entries[3].name == "zeta"
|
|
clear_status_sources()
|
|
|
|
|
|
def test_failing_source_is_skipped_not_fatal():
|
|
clear_status_sources()
|
|
|
|
async def good(db):
|
|
return [_entry("a", "up")]
|
|
|
|
async def bad(db):
|
|
raise RuntimeError("boom")
|
|
|
|
register_status_source(good)
|
|
register_status_source(bad)
|
|
entries = asyncio.run(collect_status(None))
|
|
assert [e.name for e in entries] == ["a"]
|
|
clear_status_sources()
|
|
|
|
|
|
def test_register_is_idempotent_on_same_callable():
|
|
clear_status_sources()
|
|
|
|
async def src(db):
|
|
return []
|
|
|
|
register_status_source(src)
|
|
register_status_source(src)
|
|
assert status_mod._SOURCES.count(src) == 1
|
|
clear_status_sources()
|
|
|
|
|
|
def test_sparkline_svg_handles_edge_cases():
|
|
assert "polyline" not in sparkline_svg([5]) # <2 points → empty svg
|
|
assert "polyline" in sparkline_svg([1, 2, 3])
|
|
assert "polyline" in sparkline_svg([4, 4, 4]) # flat series, no div-by-zero
|
|
assert "polyline" in sparkline_svg([1, None, 3]) # None values filtered
|
|
|
|
|
|
def test_a_sparkline_can_stretch_across_its_tile():
|
|
"""#5290: the host page's tiles are wide; the line fills them, stroke kept."""
|
|
wide = sparkline_svg([1, 2, 3], height=24, fill_width=True)
|
|
assert 'width="100%"' in wide and 'preserveAspectRatio="none"' in wide
|
|
assert 'vector-effect="non-scaling-stroke"' in wide
|
|
fixed = sparkline_svg([1, 2, 3])
|
|
assert 'width="80"' in fixed and "preserveAspectRatio" not in fixed
|
|
|
|
|
|
def test_a_sparkline_with_a_floor_draws_idle_noise_flat():
|
|
"""#5310: 0.01 → 0.09 cores against a 1-core floor stays near the bottom;
|
|
without it the same wobble fills the height. A peak above the floor is
|
|
the top, and the line starts from zero."""
|
|
def ys(svg):
|
|
pts = svg.split('points="')[1].split('"')[0].split()
|
|
return [float(p.split(",")[1]) for p in pts]
|
|
assert ys(sparkline_svg([0.01, 0.09])) == [19.0, 1.0]
|
|
assert ys(sparkline_svg([0.01, 0.09], floor=1.0)) == [18.8, 17.4]
|
|
assert ys(sparkline_svg([1.0, 2.0], floor=1.0)) == [10.0, 1.0]
|