"""Renderer tests.
The load-bearing property is that no screen ever emits a line wider than the
terminal. When that fails the terminal wraps the line, every column below it
shifts, and the display becomes unreadable -- so it is checked at every width,
with real data and with degenerate data.
"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from rich.cells import cell_len # noqa: E402
from updater import icons # noqa: E402
from updater.audit import Change, Severity, audit # noqa: E402
from updater.aur import AurPackage # noqa: E402
from updater.buildgraph import build_graph # noqa: E402
from updater.procwatch import BuildState # noqa: E402
from updater.render import ( # noqa: E402
MIN_WIDTH,
AurRow,
CheckRow,
Columns,
FeedEntry,
RepoRow,
Review,
Screen,
Step,
Totals,
VcsRow,
audit_screen,
build_screen,
review_screen,
)
from updater.render.audit import diff_lines, summary_parts # noqa: E402
from updater.srcinfo import SrcInfo, parse_pkgbuild, parse_srcinfo # noqa: E402
from updater.theme import Theme # noqa: E402
THEME = Theme.default()
GLYPHS = icons.glyphs("unicode")
WIDTHS = (MIN_WIDTH, 72, 88, 100, 120, 160)
def assert_fits(lines, width: int) -> None:
for i, line in enumerate(lines):
got = cell_len(line.plain)
assert got <= width, f"line {i} is {got} cells at width {width}: {line.plain!r}"
# --------------------------------------------------------------------------
# fixtures
# --------------------------------------------------------------------------
def sample_review(*, rows: int = 4, blocked: bool = False) -> Review:
return Review(
meta="2026-09-28 16:52 · arch linux · rolling",
repo=[
RepoRow("U", "linux", "6.16.7.arch1-1", "6.16.8.arch1-1", "1.4 MiB"),
RepoRow("U", "mesa", "1:25.2.2-1", "1:25.2.3-1", "142.2 MiB"),
RepoRow("N", "rebuild-detector", "-", "5-1", "12 KiB"),
RepoRow("R", "linux-kepler", "6.15.9-1", "-", "-310 MiB"),
][:rows],
repo_total=184,
repo_download=1258291200,
repo_delta=503316480,
aur=[
AurRow("U", "helium-browser-bin", "0.9.2-1", "0.9.3-1", "PKGBUILD +3 -1"),
AurRow(
"!!" if blocked else "!",
"enca-bin",
"1.19-4",
"1.19-5",
"adopted by a new maintainer",
loud=True,
details=[
"maintainer changed (Felp98 -> mkolb)",
".install adds: systemctl enable --now encad",
],
),
AurRow("U", "termux-language-server", "0.1.12-1", "0.1.13-1"),
],
aur_total=23,
aur_build=21,
vcs=[
VcsRow("helix-git", "r9021 → r9044"),
VcsRow("mangowm-git", "a1b2c → e9f01"),
VcsRow("noctalia-git", "r1204 → r1288"),
],
checks=[
CheckRow(True, "arch-audit", "no CVEs among pending upgrades"),
CheckRow(True, "news", "no unread items affecting this update"),
CheckRow(False, "pacnew", "2 unresolved config files"),
],
steps=[
Step("1", "pacman -Su", "184 packages · 1.2 GiB download"),
Step("2", "paru -Sua", "21 packages built from source"),
Step("3", "restart", "12 services affected"),
],
)
def sample_graph():
infos = {
"top": SrcInfo(pkgbase="top", pkgver="1", pkgrel="1", depends=["mid"]),
"mid": SrcInfo(pkgbase="mid", pkgver="1", pkgrel="1", depends=["base"]),
"base": SrcInfo(pkgbase="base", pkgver="1", pkgrel="1"),
"free": SrcInfo(pkgbase="free", pkgver="2", pkgrel="1"),
}
return build_graph(infos, set(infos))
def sample_report(blocked: bool = True):
body = "sudo make install" if blocked else "make"
change = Change(
package="demo",
old_srcinfo=parse_srcinfo(
"pkgbase = demo\n\tpkgver = 1.0\n\tpkgrel = 1\n"
"\tsource = https://x.invalid/v1.tar.gz\n"
"\tsha256sums = aaaa\n\npkgname = demo\n"
),
new_srcinfo=parse_srcinfo(
"pkgbase = demo\n\tpkgver = 1.1\n\tpkgrel = 1\n"
"\tsource = https://y.invalid/v2.tar.gz\n"
"\tsha256sums = SKIP\n\npkgname = demo\n"
),
old_pkgbuild=parse_pkgbuild("pkgver=1.0\nbuild() {\n make\n}\n"),
new_pkgbuild=parse_pkgbuild(f"pkgver=1.1\nbuild() {{\n {body}\n}}\n"),
)
return audit(change)
# --------------------------------------------------------------------------
# the width invariant, everywhere
# --------------------------------------------------------------------------
def test_review_fits_at_every_width():
for width in WIDTHS:
lines = review_screen(sample_review(), theme=THEME, glyphs=GLYPHS, width=width)
assert_fits(lines, width)
assert lines
def test_build_dashboard_fits_at_every_width():
for width in WIDTHS:
lines = build_screen(
graph=sample_graph(),
theme=THEME,
glyphs=GLYPHS,
width=width,
feed=[FeedEntry("base", "1-1", 41.0)],
states=[BuildState("mid", "cargo", 12.0)],
completed={"base"},
running={"mid"},
failed=set(),
totals=Totals(
total=4,
elapsed=252,
download_bytes=12_400_000,
download_rate=3_100_000,
download_remaining=8_200_000,
download_eta=3,
),
parallelism="16 cores · 8 workers",
)
assert_fits(lines, width)
def test_audit_screen_fits_at_every_width():
for width in WIDTHS:
lines = audit_screen(
sample_report(),
theme=THEME,
glyphs=GLYPHS,
width=width,
old_text="pkgver=1.0\nbuild() {\n make\n}\n",
new_text="pkgver=1.1\nbuild() {\n sudo make install\n}\n",
)
assert_fits(lines, width)
def test_the_actual_terminal_width_never_overflows():
"""Whatever the tool is run in, the lines it emits fit."""
import shutil
width = shutil.get_terminal_size((100, 40)).columns
for lines in (
review_screen(sample_review(), theme=THEME, glyphs=GLYPHS, width=width),
build_screen(graph=sample_graph(), theme=THEME, glyphs=GLYPHS, width=width),
audit_screen(sample_report(), theme=THEME, glyphs=GLYPHS, width=width),
):
assert_fits(lines, width)
# --------------------------------------------------------------------------
# degenerate input must not crash
# --------------------------------------------------------------------------
def test_every_screen_handles_completely_empty_data():
empty_review = Review()
lines = review_screen(empty_review, theme=THEME, glyphs=GLYPHS, width=100)
assert lines
lines = build_screen(
graph=build_graph({}, set()), theme=THEME, glyphs=GLYPHS, width=100, totals=Totals()
)
assert any("nothing finished" in line.plain for line in lines)
lines = audit_screen(audit(Change(package="x")), theme=THEME, glyphs=GLYPHS, width=100)
assert lines
def test_absurdly_long_names_and_versions_are_truncated_not_wrapped():
review = Review(repo=[RepoRow("U", "a" * 300, "1" * 120, "2" * 120, "1.0 MiB")], repo_total=1)
for width in WIDTHS:
assert_fits(review_screen(review, theme=THEME, glyphs=GLYPHS, width=width), width)
def test_below_minimum_width_columns_are_clamped_not_corrupted():
lines = review_screen(sample_review(), theme=THEME, glyphs=GLYPHS, width=10)
assert_fits(lines, MIN_WIDTH)
def test_every_icon_tier_renders_within_the_grid():
"""Nerd Font glyphs must not be wider than their geometric equivalents."""
for tier in ("nerd", "unicode", "ascii"):
glyphs = icons.glyphs(tier)
lines = build_screen(
graph=sample_graph(),
theme=THEME,
glyphs=glyphs,
width=100,
feed=[FeedEntry("base", "1-1", 5.0)],
completed={"base"},
states=[BuildState("mid", "cargo", 1.0)],
)
assert_fits(lines, 100)
# --------------------------------------------------------------------------
# semantics the screen is supposed to convey
# --------------------------------------------------------------------------
def test_dashboard_toggle_changes_the_output():
kwargs = {
"graph": sample_graph(),
"theme": THEME,
"glyphs": GLYPHS,
"width": 100,
"feed": [FeedEntry("base", "1-1", 5.0)],
"completed": {"base"},
}
full = build_screen(dashboard=True, **kwargs)
ledger = build_screen(dashboard=False, **kwargs)
assert len(full) > len(ledger)
assert any("BUILDING" in l.plain.upper() for l in full)
assert not any("BUILDING" in l.plain.upper() for l in ledger)
# the ledger still shows what finished: it is a feed, not a downgrade
assert any("base" in l.plain for l in ledger)
def test_feed_preserves_order_so_the_newest_lands_at_the_bottom():
feed = [
FeedEntry("first", "1", 1.0),
FeedEntry("second", "1", 2.0),
FeedEntry("third", "1", 3.0),
]
lines = build_screen(graph=sample_graph(), theme=THEME, glyphs=GLYPHS, width=100, feed=feed)
positions = [
next(i for i, l in enumerate(lines) if name in l.plain)
for name in ("first", "second", "third")
]
assert positions == sorted(positions), "feed order was not preserved"
def test_shared_dependency_is_marked_rather_than_duplicated():
infos = {
"top": SrcInfo(pkgbase="top", pkgver="1", pkgrel="1", depends=["left", "right"]),
"left": SrcInfo(pkgbase="left", pkgver="1", pkgrel="1", depends=["shared"]),
"right": SrcInfo(pkgbase="right", pkgver="1", pkgrel="1", depends=["shared"]),
"shared": SrcInfo(pkgbase="shared", pkgver="1", pkgrel="1"),
}
graph = build_graph(infos, set(infos))
lines = build_screen(graph=graph, theme=THEME, glyphs=GLYPHS, width=100)
marked = [l.plain for l in lines if "shared" in l.plain]
assert any("↻" in l for l in marked), f"no shared marker found in {marked}"
def test_a_node_without_metadata_never_claims_to_be_ready():
"""Its deps are unknown, not satisfied, so "ready" would be a lie."""
graph = build_graph(
{"app": SrcInfo(pkgbase="app", pkgver="1", pkgrel="1", depends=["helper"])},
{"app", "helper"},
)
lines = build_screen(graph=graph, theme=THEME, glyphs=GLYPHS, width=100)
text = "\n".join(l.plain for l in lines)
assert "helper" in text
assert "metadata missing" in text
helper_line = next(l.plain for l in lines if "helper" in l.plain)
assert "ready" not in helper_line, helper_line
def test_an_unorderable_graph_is_reported_not_silently_omitted():
graph = build_graph(
{"app": SrcInfo(pkgbase="app", pkgver="1", pkgrel="1", depends=["helper"])},
{"app", "helper"},
)
lines = build_screen(graph=graph, theme=THEME, glyphs=GLYPHS, width=100)
text = "\n".join(l.plain for l in lines)
assert "CANNOT BE ORDERED" in text.upper()
assert "no metadata" in text
def test_settled_packages_are_not_listed_as_unorderable():
"""A package cannot be both in flight and waiting on missing metadata."""
graph = build_graph(
{"app": SrcInfo(pkgbase="app", pkgver="1", pkgrel="1", depends=["helper"])},
{"app", "helper"},
)
lines = build_screen(
graph=graph,
theme=THEME,
glyphs=GLYPHS,
width=100,
running={"app"},
states=[BuildState("app", "cargo", 3.0)],
)
text = "\n".join(l.plain for l in lines)
upper = text.upper()
assert "CANNOT BE ORDERED" in upper, "the unorderable section was not rendered"
# slice the original text, not the uppercased copy, or the name checks below
# would pass vacuously
section = text[upper.index("CANNOT BE ORDERED") :]
assert "app" not in section, "a running package was listed as unorderable"
assert "helper" in section, "a genuinely unknown package was dropped"
def test_failed_node_reports_what_it_blocks():
graph = sample_graph()
lines = build_screen(graph=graph, theme=THEME, glyphs=GLYPHS, width=100, failed={"base"})
assert any("blocked by" in l.plain for l in lines)
def test_a_dry_run_never_renders_a_prompt_it_will_not_wait_for():
"""Showing "Apply this plan?" and then exiting reads as a crash."""
lines = review_screen(sample_review(), theme=THEME, glyphs=GLYPHS, width=100, dry_run=True)
text = "\n".join(l.plain for l in lines)
assert "Apply this plan?" not in text
assert "dry run" in text.lower()
assert "nothing has been applied" in text
# and it must say how to actually apply it
assert "without --dry-run" in text
def test_an_interactive_review_is_silent_about_dry_runs():
lines = review_screen(sample_review(), theme=THEME, glyphs=GLYPHS, width=100, dry_run=False)
text = "\n".join(l.plain for l in lines)
assert "dry run" not in text.lower()
def test_the_plan_screen_never_renders_a_prompt():
"""A prompt in the screen and a prompt from confirm() gave two lines."""
for review in (sample_review(), sample_review(blocked=True)):
lines = review_screen(review, theme=THEME, glyphs=GLYPHS, width=100)
text = "\n".join(l.plain for l in lines)
assert "Apply this plan?" not in text
assert "Apply the rest" not in text
assert "[y/N" not in text
def test_a_blocked_plan_still_warns_that_packages_need_attention():
risky = review_screen(sample_review(blocked=True), theme=THEME, glyphs=GLYPHS, width=100)
assert any("needs attention" in l.plain for l in risky), [
l.plain for l in risky if "attention" in l.plain.lower()
]
def test_the_confirmation_prompt_is_owned_by_the_confirming_step():
from updater.cli import Options, Run, confirm_prompt
calm = Run(options=Options())
assert confirm_prompt(calm) == "Apply this plan?"
from updater.audit import Finding, Report
risky = Run(options=Options())
risky.audits = {
"a": Report(
package="a",
diff=None,
findings=[Finding("pipe-to-shell", Severity.BLOCK, "pipes a download into a shell")],
)
}
assert "skipping 1 flagged package" in confirm_prompt(risky)
def test_a_loud_row_says_its_consequence_and_carries_evidence():
from updater.render.review import aur_row_from
blocked = aur_row_from("demo", "1", "2", sample_report(blocked=True))
assert blocked.tag == "!!"
assert blocked.note == "will be skipped"
assert blocked.details, "a loud row must carry its evidence"
look = aur_row_from("demo", "1", "2", sample_report(blocked=False))
assert look.tag in ("!", "!!")
def test_single_blocked_package_is_not_referred_to_in_the_plural():
risky = sample_review(blocked=True)
risky.aur = [r for r in risky.aur if r.tag == "!!"]
lines = review_screen(risky, theme=THEME, glyphs=GLYPHS, width=100)
assert any("1 package needs attention" in l.plain for l in lines)
assert not any("package(s)" in l.plain for l in lines)
def test_audit_verdict_drives_the_row_tag():
from updater.render.review import aur_row_from
clean = aur_row_from("demo", "1", "2", None)
assert clean.tag == "U" and not clean.loud
report = sample_report(blocked=True)
flagged = aur_row_from("demo", "1.0", "1.1", report)
assert flagged.tag == "!!"
assert flagged.details, "a loud row must carry its evidence"
def test_summary_parts_are_populated_for_a_real_change():
parts = summary_parts(sample_report())
text = " ".join(p[0] for p in parts)
assert "pkgver" in text and "sums" in text and "source" in text
def test_no_baseline_shows_a_reference_listing_not_a_fake_diff():
"""An all-additions diff would look like a total rewrite that never happened."""
lines = audit_screen(
sample_report(),
theme=THEME,
glyphs=GLYPHS,
width=100,
old_text="",
new_text="pkgver=1.0\nbuild() {\n make\n}\n",
baseline=False,
)
text = "\n".join(l.plain for l in lines)
assert "no baseline recorded" in text
# check the section heading, not the substring: "different host" contains DIFF
assert not any(l.plain.strip().upper().startswith("DIFF") for l in lines), (
"a diff section was rendered without a baseline"
)
assert "shown as reference" in text
# and no line may claim to be an addition
assert not any(l.plain.startswith("+ ") or l.plain.startswith("+ ") for l in lines)
def test_with_a_baseline_the_diff_is_shown():
lines = audit_screen(
sample_report(),
theme=THEME,
glyphs=GLYPHS,
width=100,
old_text="pkgver=1.0\n",
new_text="pkgver=1.1\n",
baseline=True,
)
assert any(l.plain.strip().upper().startswith("DIFF") for l in lines)
def test_diff_lines_marks_additions_and_removals():
rows = diff_lines("a\nb\nc\n", "a\nB\nc\n")
markers = {m for m, _n, _t in rows}
assert "-" in markers and "+" in markers
added = [t for m, _n, t in rows if m == "+"]
assert added == ["B"]
def test_aur_package_metadata_renders_without_a_network():
"""The client's shape is what the screen consumes; keep them compatible."""
pkg = AurPackage(name="demo", version="1.0-1", maintainer="alice", votes=3)
assert not pkg.is_orphaned and not pkg.is_flagged
def main() -> int:
tests = [(n, f) for n, f in sorted(globals().items()) if n.startswith("test_") and callable(f)]
failed = 0
for name, fn in tests:
try:
fn()
except Exception as exc: # noqa: BLE001
failed += 1
print(f"FAIL {name}\n {type(exc).__name__}: {exc}")
else:
print(f"ok {name}")
print(f"\n{len(tests) - failed}/{len(tests)} passed")
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())
# --------------------------------------------------------------------------
# clipping: "fits" versus "fits by luck"
# --------------------------------------------------------------------------
def test_realistic_screens_need_no_clipping_at_comfortable_widths():
"""Wide enough that nothing should have to be thrown away."""
for width in (88, 100, 120, 160):
for kind in ("review", "build", "audit"):
screen = Screen(theme=THEME, glyphs=GLYPHS, columns=Columns.for_width(width))
if kind == "review":
review_screen(sample_review(), theme=THEME, glyphs=GLYPHS, width=width, into=screen)
elif kind == "build":
build_screen(
graph=sample_graph(),
theme=THEME,
glyphs=GLYPHS,
width=width,
into=screen,
feed=[FeedEntry("base", "1-1", 41.0)],
states=[BuildState("mid", "cargo", 12.0)],
completed={"base"},
running={"mid"},
totals=Totals(total=4, elapsed=252, download_bytes=12_400_000),
)
else:
audit_screen(sample_report(), theme=THEME, glyphs=GLYPHS, width=width, into=screen)
assert screen.clipped == 0, f"{kind} clipped {screen.clipped} cell(s) at width {width}"
def test_narrow_terminals_clip_instead_of_corrupting():
"""At the minimum width clipping is expected; overflow never is."""
for width in (MIN_WIDTH,):
screen = Screen(theme=THEME, glyphs=GLYPHS, columns=Columns.for_width(width))
lines = review_screen(sample_review(), theme=THEME, glyphs=GLYPHS, width=width, into=screen)
assert_fits(lines, width)
def test_advisory_severity_decides_the_colour():
"""Fixability has its own channel; the colour is for severity.
Using the colour for fixability made Critical and Low the same shade, which
is the one distinction a security list exists to make.
"""
from updater.render.review import advisory_role
assert advisory_role("Critical") == "alert"
assert advisory_role("High") == "alert"
assert advisory_role("Medium") == "warn"
assert advisory_role("Low") == "accent"
assert advisory_role("Unknown") == "muted"
assert advisory_role("") == "muted"
roles = {advisory_role(s) for s in ("Critical", "Medium", "Low", "Unknown")}
assert len(roles) == 4, roles