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 20, 2021
1 parent 35df3e6 commit baf9703
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 10 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.
31 changes: 23 additions & 8 deletions src/_pytest/_io/saferepr.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,23 @@ 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 truncate the resulting repr to that specific size, using ellipsis
somewhere in the middle to hide the extra text.
If None, will not impose any size limits on the returning repr.
"""
super().__init__()
self.maxstring = maxsize
# ``maxsize`` is used by the superclass, and needs to be an int; using a
# very large number in case maxsize is None, meaning we want to disable
# truncation.
self.maxstring = maxsize if maxsize is not None else 1_000_000_000
self.maxsize = maxsize

def repr(self, x: object) -> str:
Expand All @@ -51,7 +62,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 +73,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,15 +90,15 @@ 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
with a short exception info and 'saferepr' generally takes
care to never raise exceptions itself.
This function is a wrapper around the Repr/reprlib functionality of the
standard 2.6 lib.
stdlib.
"""
return SafeRepr(maxsize).repr(obj)

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
17 changes: 16 additions & 1 deletion src/_pytest/assertion/rewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,22 @@ 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]:
"""Get `maxsize` configuration for saferepr based on the given config object."""
verbosity = config.getoption("verbose") if config is not None else 0
if verbosity >= 4:
return None
if verbosity >= 3:
return _DEFAULT_REPR_MAX_SIZE * 10
return _DEFAULT_REPR_MAX_SIZE


# Maximum size of overall repr of objects to display during assertion errors.
_DEFAULT_REPR_MAX_SIZE = 240


def _format_assertmsg(obj: object) -> str:
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
7 changes: 7 additions & 0 deletions testing/io/test_saferepr.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ def test_maxsize():
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:
def __repr__(self):
Expand Down
48 changes: 48 additions & 0 deletions testing/test_assertrewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import zipfile
from functools import partial
from pathlib import Path
from typing import cast
from typing import Dict
from typing import List
from typing import Mapping
Expand All @@ -20,12 +21,15 @@
import _pytest._code
import pytest
from _pytest.assertion import util
from _pytest.assertion.rewrite import _DEFAULT_REPR_MAX_SIZE
from _pytest.assertion.rewrite import _get_assertion_exprs
from _pytest.assertion.rewrite import _get_maxsize_for_saferepr
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 Config
from _pytest.config import ExitCode
from _pytest.pathlib import make_numbered_dir
from _pytest.pytester import Pytester
Expand Down Expand Up @@ -1706,3 +1710,47 @@ 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: int, 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
"""
)

@pytest.mark.parametrize("verbose_arg", ["", "-v", "-vv"])
def test_default_verbosity(self, pytester: Pytester, verbose_arg: str) -> None:
self.create_test_file(pytester, _DEFAULT_REPR_MAX_SIZE)
result = pytester.runpytest(verbose_arg)
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 baf9703

Please sign in to comment.