Skip to content

Commit 15cc828

Browse files
[test] Cover the dispatcher budget wiring and plaintext fallback
The callequal/callop helpers run pytest_assertrepr_compare with truncation disabled, so the branch that derives a TruncationBudget from the truncation limits and feeds it to the comparison helpers went untested. Extend mock_config with truncation limits and a has_terminalreporter flag, then add TestAssertReprCompareDispatcher covering: both limits (capped formatting work), each partial-budget arm (only lines / only chars), and the no-terminalreporter plaintext fallback (#14377), which now asserts the terminal writer is never reached.
1 parent e8deced commit 15cc828

1 file changed

Lines changed: 108 additions & 6 deletions

File tree

testing/test_assertion.py

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,28 @@ def mock_config(
3030
verbose: int = 0,
3131
assertion_override: int | None = None,
3232
assertion_text_diff_style: str = util.ASSERTION_TEXT_DIFF_STYLE_NDIFF,
33+
truncation_limit_lines: str = "0",
34+
truncation_limit_chars: str = "0",
35+
has_terminalreporter: bool = True,
3336
):
3437
class TerminalWriter:
3538
def _highlight(self, source, lexer="python"):
3639
return source
3740

3841
class PluginManager:
3942
def has_plugin(self, name: str) -> bool:
40-
return True
43+
return has_terminalreporter
4144

4245
class Config:
4346
pluginmanager = PluginManager()
4447

4548
def get_terminal_writer(self):
49+
# When the terminalreporter plugin is absent the dispatcher must
50+
# fall back to the plaintext highlighter without reaching for a
51+
# terminal writer (#14377); make that misuse fail loudly.
52+
assert has_terminalreporter, (
53+
"get_terminal_writer() must not be called without terminalreporter"
54+
)
4655
return TerminalWriter()
4756

4857
def get_verbosity(self, verbosity_type: str | None = None) -> int:
@@ -58,11 +67,13 @@ def get_verbosity(self, verbosity_type: str | None = None) -> int:
5867
def getini(self, name: str) -> str:
5968
if name == util.ASSERTION_TEXT_DIFF_STYLE_INI:
6069
return assertion_text_diff_style
61-
# Disable truncation so ``callop``-style tests can compare
62-
# against the full explanation. Dedicated truncation tests
63-
# use their own config in :class:`TestTruncateMaterialize`.
64-
if name in ("truncation_limit_lines", "truncation_limit_chars"):
65-
return "0"
70+
# Truncation defaults to disabled (``"0"``) so ``callop``-style
71+
# tests can compare against the full explanation; the dispatcher
72+
# tests pass explicit limits to exercise the budget wiring.
73+
if name == "truncation_limit_lines":
74+
return truncation_limit_lines
75+
if name == "truncation_limit_chars":
76+
return truncation_limit_chars
6677
raise KeyError(f"Not mocked out: {name}")
6778

6879
return Config()
@@ -2022,6 +2033,97 @@ def work(n: int) -> tuple[int, int]:
20222033
assert reprs_big < 200 # nowhere near the 100_000-element input
20232034

20242035

2036+
class TestAssertReprCompareDispatcher:
2037+
"""Tests for the budget wiring in ``plugin.pytest_assertrepr_compare``.
2038+
2039+
The ``callequal``/``callop`` helpers run the dispatcher with truncation
2040+
*disabled* (limits ``"0"`` ⇒ ``NO_TRUNCATION_BUDGET``), so they never
2041+
exercise the branch that derives a :class:`TruncationBudget` from the
2042+
truncation limits and feeds it to the comparison helpers. These tests
2043+
pass explicit limits to cover that path — including the partial-budget
2044+
arms where exactly one dimension is disabled. Behaviour only: the
2045+
footer wording is never asserted.
2046+
"""
2047+
2048+
def test_both_limits_truncate_without_formatting_everything(self) -> None:
2049+
# Both limits set ⇒ budget is ``(lines+slack, chars+slack)``; a huge
2050+
# list comparison is truncated to a few lines and, crucially, the
2051+
# per-side pformat is capped so the element ``repr`` work stays
2052+
# bounded rather than scaling with the 50k-element input.
2053+
class Tracked:
2054+
reprs = 0
2055+
2056+
def __init__(self, v: int) -> None:
2057+
self.v = v
2058+
2059+
def __repr__(self) -> str:
2060+
Tracked.reprs += 1
2061+
return f"T({self.v})"
2062+
2063+
def __eq__(self, o: object) -> bool:
2064+
return isinstance(o, Tracked) and self.v == o.v
2065+
2066+
config = mock_config(
2067+
verbose=1, # -v: emit the full diff that truncation then clips
2068+
truncation_limit_lines="4",
2069+
truncation_limit_chars="160",
2070+
)
2071+
left = [Tracked(i) for i in range(50_000)]
2072+
right = [Tracked(i) for i in range(50_000)]
2073+
right[0] = Tracked(-1)
2074+
Tracked.reprs = 0 # count only the formatting, not the construction
2075+
result = plugin.pytest_assertrepr_compare(config, "==", left, right)
2076+
assert result is not None
2077+
assert len(result) < 20
2078+
assert any("truncated" in line for line in result)
2079+
assert Tracked.reprs < 200 # nowhere near the 50k-element input
2080+
2081+
def test_chars_only_budget_still_truncates(self) -> None:
2082+
# ``truncation_limit_lines == 0`` ⇒ the ``max_lines`` budget arm is
2083+
# ``None`` (line cap disabled), but the char cap alone must still
2084+
# bound the explanation of a large multi-line diff.
2085+
config = mock_config(
2086+
verbose=1, # -v: emit the full diff that truncation then clips
2087+
truncation_limit_lines="0",
2088+
truncation_limit_chars="160",
2089+
)
2090+
left = list(range(1000))
2091+
right = list(range(1, 1001))
2092+
result = plugin.pytest_assertrepr_compare(config, "==", left, right)
2093+
assert result is not None
2094+
assert any("truncated" in line for line in result)
2095+
assert sum(len(line) for line in result) < 1000
2096+
2097+
def test_lines_only_budget_still_truncates(self) -> None:
2098+
# ``truncation_limit_chars == 0`` ⇒ the ``max_chars`` budget arm is
2099+
# ``None`` (char cap disabled), but the line cap alone must still
2100+
# bound the explanation.
2101+
config = mock_config(
2102+
verbose=1, # -v: emit the full diff that truncation then clips
2103+
truncation_limit_lines="4",
2104+
truncation_limit_chars="0",
2105+
)
2106+
left = list(range(1000))
2107+
right = list(range(1, 1001))
2108+
result = plugin.pytest_assertrepr_compare(config, "==", left, right)
2109+
assert result is not None
2110+
assert len(result) < 20
2111+
assert any("truncated" in line for line in result)
2112+
2113+
def test_no_terminalreporter_uses_plaintext_highlighter(self) -> None:
2114+
# Without the terminalreporter plugin the dispatcher must use the
2115+
# plaintext ``dummy_highlighter`` and never touch a terminal writer
2116+
# (#14377). ``mock_config.get_terminal_writer`` asserts it isn't
2117+
# reached, so taking the wrong branch fails the test.
2118+
config = mock_config(has_terminalreporter=False)
2119+
result = plugin.pytest_assertrepr_compare(config, "==", {1, 2}, {1, 3})
2120+
assert result is not None
2121+
# The set-comparison explanation is still produced via the plaintext
2122+
# path; no ANSI escape sequences leak in.
2123+
assert any("Extra items" in line for line in result)
2124+
assert not any("\x1b[" in line for line in result)
2125+
2126+
20252127
def test_python25_compile_issue257(pytester: Pytester) -> None:
20262128
pytester.makepyfile(
20272129
"""

0 commit comments

Comments
 (0)