Skip to content

Commit

Permalink
Increase truncation threshold with -vvv, disable with -vvvv
Browse files Browse the repository at this point in the history
  • Loading branch information
nicoddemus committed Mar 3, 2021
1 parent cd783eb commit d81aeb5
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 12 deletions.
5 changes: 5 additions & 0 deletions changelog/6682.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
By default, pytest will truncate long strings in assert errors so they don't clutter the output too much,
currently at ``240`` characters by default.

However, in some cases the longer output helps, or even is crucial, to diagnose the problem. Using ``-vvv`` will
increase the truncation threshold to ``2400`` characters, and ``-vvvv`` or higher will disable truncation completely.
25 changes: 18 additions & 7 deletions src/_pytest/_io/saferepr.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,19 @@ def _ellipsize(s: str, maxsize: int) -> str:


class SafeRepr(reprlib.Repr):
"""repr.Repr that limits the resulting size of repr() and includes
information on exceptions raised during the call."""
"""
repr.Repr that limits the resulting size of repr() and includes
information on exceptions raised during the call.
"""

def __init__(self, maxsize: int) -> None:
def __init__(self, maxsize: Optional[int]) -> None:
"""
:param maxsize:
If not None, will limit the overall repr result to that specific size.
If None, will not impose any size limits on the returning repr.
"""
super().__init__()
self.maxstring = maxsize
self.maxstring = maxsize if maxsize is not None else 1000000000
self.maxsize = maxsize

def repr(self, x: object) -> str:
Expand All @@ -51,7 +58,9 @@ def repr(self, x: object) -> str:
raise
except BaseException as exc:
s = _format_repr_exception(exc, x)
return _ellipsize(s, self.maxsize)
if self.maxsize is not None:
s = _ellipsize(s, self.maxsize)
return s

def repr_instance(self, x: object, level: int) -> str:
try:
Expand All @@ -60,7 +69,9 @@ def repr_instance(self, x: object, level: int) -> str:
raise
except BaseException as exc:
s = _format_repr_exception(exc, x)
return _ellipsize(s, self.maxsize)
if self.maxsize is not None:
s = _ellipsize(s, self.maxsize)
return s


def safeformat(obj: object) -> str:
Expand All @@ -75,7 +86,7 @@ def safeformat(obj: object) -> str:
return _format_repr_exception(exc, obj)


def saferepr(obj: object, maxsize: int = 240) -> str:
def saferepr(obj: object, maxsize: Optional[int] = 240) -> str:
"""Return a size-limited safe repr-string for the given object.
Failing __repr__ functions of user instances will be represented
Expand Down
5 changes: 4 additions & 1 deletion src/_pytest/assertion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ def callbinrepr(op, left: object, right: object) -> Optional[str]:

saved_assert_hooks = util._reprcompare, util._assertion_pass
util._reprcompare = callbinrepr
util._config = item.config

if ihook.pytest_assertion_pass.get_hookimpls():

Expand All @@ -164,6 +165,7 @@ def call_assertion_pass_hook(lineno: int, orig: str, expl: str) -> None:
yield

util._reprcompare, util._assertion_pass = saved_assert_hooks
util._config = None


def pytest_sessionfinish(session: "Session") -> None:
Expand All @@ -176,4 +178,5 @@ def pytest_sessionfinish(session: "Session") -> None:
def pytest_assertrepr_compare(
config: Config, op: str, left: Any, right: Any
) -> Optional[List[str]]:
return util.assertrepr_compare(config=config, op=op, left=left, right=right)
x = util.assertrepr_compare(config=config, op=op, left=left, right=right)
return x
12 changes: 11 additions & 1 deletion src/_pytest/assertion/rewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,8 +427,18 @@ def _saferepr(obj: object) -> str:
sequences, especially '\n{' and '\n}' are likely to be present in
JSON reprs.
"""
return saferepr(obj).replace("\n", "\\n")
maxsize = _get_maxsize_for_saferepr(util._config)
return saferepr(obj, maxsize=maxsize).replace("\n", "\\n")

def _get_maxsize_for_saferepr(config: Optional[Config]) -> Optional[int]:
verbosity = config.getoption("verbose") if config is not None else 0
if verbosity >= 4:
return None
if verbosity >= 3:
return 2400
return 240

_DEFAULT_REPR_MAX_SIZE = 240

def _format_assertmsg(obj: object) -> str:
r"""Format the custom assertion message given.
Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/assertion/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from _pytest._io.saferepr import _pformat_dispatch
from _pytest._io.saferepr import safeformat
from _pytest._io.saferepr import saferepr
from _pytest.config import Config


# The _reprcompare attribute on the util module is used by the new assertion
# interpretation code and assertion rewriter to detect this plugin was
Expand All @@ -26,6 +28,9 @@
# when pytest_runtest_setup is called.
_assertion_pass: Optional[Callable[[int, str, str], None]] = None

# Config object which is assigned
_config: Optional[Config] = None


def format_explanation(explanation: str) -> str:
r"""Format an explanation.
Expand Down
6 changes: 6 additions & 0 deletions testing/io/test_saferepr.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ def test_maxsize():
expected = repr("x" * 10 + "..." + "x" * 10)
assert s == expected

def test_no_maxsize():
text = "x" * 1000
s = saferepr(text, maxsize=None)
expected = repr(text)
assert s == expected


def test_maxsize_error_on_instance():
class A:
Expand Down
47 changes: 44 additions & 3 deletions testing/test_assertrewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import zipfile
from functools import partial
from pathlib import Path
from typing import Dict
from typing import Dict, Any, cast
from typing import List
from typing import Mapping
from typing import Optional
Expand All @@ -20,13 +20,13 @@
import _pytest._code
import pytest
from _pytest.assertion import util
from _pytest.assertion.rewrite import _get_assertion_exprs
from _pytest.assertion.rewrite import _get_assertion_exprs, _get_maxsize_for_saferepr, _DEFAULT_REPR_MAX_SIZE
from _pytest.assertion.rewrite import AssertionRewritingHook
from _pytest.assertion.rewrite import get_cache_dir
from _pytest.assertion.rewrite import PYC_TAIL
from _pytest.assertion.rewrite import PYTEST_TAG
from _pytest.assertion.rewrite import rewrite_asserts
from _pytest.config import ExitCode
from _pytest.config import ExitCode, Config
from _pytest.pathlib import make_numbered_dir
from _pytest.pytester import Pytester

Expand Down Expand Up @@ -1708,3 +1708,44 @@ def test_foo():
cache_tag=sys.implementation.cache_tag
)
assert bar_init_pyc.is_file()

class TestReprSizeVerbosity:
"""
Check that verbosity also controls the string length threshold to shorten it using
ellipsis.
"""

@pytest.mark.parametrize("verbose, expected_size", [(0, 240), (1, 240), (2, 240), (3, 2400), (4, None)])
def test_get_maxsize_for_saferepr(self, verbose, expected_size) -> None:
class FakeConfig:

def getoption(self, name: str) -> int:
assert name == "verbose"
return verbose

config = FakeConfig()
assert _get_maxsize_for_saferepr(cast(Config, config)) == expected_size

def create_test_file(self, pytester: Pytester, size: int) -> None:
pytester.makepyfile(
f"""
def test_very_long_string():
text = "x" * {size}
assert "hello world" in text
"""
)

def test_default_verbosity(self, pytester: Pytester) -> None:
self.create_test_file(pytester, _DEFAULT_REPR_MAX_SIZE)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*xxx...xxx*"])

def test_increased_verbosity(self, pytester: Pytester) -> None:
self.create_test_file(pytester, _DEFAULT_REPR_MAX_SIZE)
result = pytester.runpytest("-vvv")
result.stdout.no_fnmatch_line("*xxx...xxx*")

def test_max_increased_verbosity(self, pytester: Pytester) -> None:
self.create_test_file(pytester, _DEFAULT_REPR_MAX_SIZE * 10)
result = pytester.runpytest("-vvvv")
result.stdout.no_fnmatch_line("*xxx...xxx*")

0 comments on commit d81aeb5

Please sign in to comment.