@@ -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+
20252127def test_python25_compile_issue257 (pytester : Pytester ) -> None :
20262128 pytester .makepyfile (
20272129 """
0 commit comments