"""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