Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/6003.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Improve short excinfo with LineMatcher failures in short test summaries, via new ``OutcomeException.short_msg``.
8 changes: 8 additions & 0 deletions src/_pytest/_code/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import _pytest
from _pytest._io.saferepr import safeformat
from _pytest._io.saferepr import saferepr
from _pytest.outcomes import OutcomeException

if False: # TYPE_CHECKING
from typing import Type
Expand Down Expand Up @@ -520,6 +521,13 @@ def exconly(self, tryshort: bool = False) -> str:
the exception representation is returned (so 'AssertionError: ' is
removed from the beginning)
"""
if (
tryshort
and isinstance(self.value, OutcomeException)
and self.value.short_msg
):
return self.value.short_msg

lines = format_exception_only(self.type, self.value)
text = "".join(lines)
text = text.rstrip()
Expand Down
31 changes: 25 additions & 6 deletions src/_pytest/outcomes.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ class OutcomeException(BaseException):
contain info about test and collection outcomes.
"""

def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None:
def __init__(
self,
msg: Optional[str] = None,
pytrace: bool = True,
*,
short_msg: Optional[str] = None
) -> None:
if msg is not None and not isinstance(msg, str):
error_msg = (
"{} expected string as 'msg' parameter, got '{}' instead.\n"
Expand All @@ -27,13 +33,24 @@ def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None:
BaseException.__init__(self, msg)
self.msg = msg
self.pytrace = pytrace
self.short_msg = short_msg

def __repr__(self) -> str:
if self.short_msg:
return "<{} short_msg={!r}>".format(self.__class__.__name__, self.short_msg)
msg = self.msg
if msg:
lines = msg.split("\n", maxsplit=1)
if len(lines) > 1:
msg = lines[0] + "..."
else:
msg = lines[0]
return "<{} msg={!r}>".format(self.__class__.__name__, msg)

def __str__(self) -> str:
if self.msg:
return self.msg
return "<{} instance>".format(self.__class__.__name__)

__str__ = __repr__
return repr(self)


TEST_OUTCOME = (OutcomeException, Exception)
Expand Down Expand Up @@ -116,7 +133,9 @@ def skip(msg: str = "", *, allow_module_level: bool = False) -> "NoReturn":
skip.Exception = Skipped # type: ignore


def fail(msg: str = "", pytrace: bool = True) -> "NoReturn":
def fail(
msg: str = "", pytrace: bool = True, *, short_msg: Optional[str] = None
) -> "NoReturn":
"""
Explicitly fail an executing test with the given message.

Expand All @@ -125,7 +144,7 @@ def fail(msg: str = "", pytrace: bool = True) -> "NoReturn":
python traceback will be reported.
"""
__tracebackhide__ = True
raise Failed(msg=msg, pytrace=pytrace)
raise Failed(msg=msg, pytrace=pytrace, short_msg=short_msg)


# Ignore type because of https://github.com/python/mypy/issues/2087.
Expand Down
4 changes: 3 additions & 1 deletion src/_pytest/pytester.py
Original file line number Diff line number Diff line change
Expand Up @@ -1387,7 +1387,9 @@ def _match_lines(self, lines2, match_func, match_nickname):
extralines.append(nextline)
else:
self._log("remains unmatched: {!r}".format(line))
pytest.fail(self._log_text.lstrip())
pytest.fail(
self._log_text, short_msg="remains unmatched: {!r}".format(line)
)

def no_fnmatch_line(self, pat):
"""Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``.
Expand Down
11 changes: 11 additions & 0 deletions testing/test_outcomes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from _pytest.outcomes import OutcomeException


def test_OutcomeException():
assert repr(OutcomeException()) == "<OutcomeException msg=None>"
assert repr(OutcomeException(msg="msg")) == "<OutcomeException msg='msg'>"
assert repr(OutcomeException(msg="msg\nline2")) == "<OutcomeException msg='msg...'>"
assert (
repr(OutcomeException(short_msg="short"))
== "<OutcomeException short_msg='short'>"
)
15 changes: 15 additions & 0 deletions testing/test_pytester.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,21 @@ def test_linematcher_match_failure():
]


def test_linematcher_fnmatch_lines():
lm = LineMatcher(["1", "2", "3"])
with pytest.raises(pytest.fail.Exception) as excinfo:
lm.fnmatch_lines(["2", "last_unmatched"])
assert excinfo.value.short_msg == "remains unmatched: 'last_unmatched'"
assert str(excinfo.value).splitlines() == [
"nomatch: '2'",
" and: '1'",
"exact match: '2'",
"nomatch: 'last_unmatched'",
" and: '3'",
"remains unmatched: 'last_unmatched'",
]


@pytest.mark.parametrize("function", ["no_fnmatch_line", "no_re_match_line"])
def test_no_matching(function):
if function == "no_fnmatch_line":
Expand Down
9 changes: 7 additions & 2 deletions testing/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,8 +559,13 @@ def test_pytest_exit():
def test_pytest_fail():
with pytest.raises(pytest.fail.Exception) as excinfo:
pytest.fail("hello")
s = excinfo.exconly(tryshort=True)
assert s.startswith("Failed")
assert excinfo.exconly(tryshort=True) == "Failed: hello"
assert excinfo.exconly(tryshort=False) == "Failed: hello"

with pytest.raises(pytest.fail.Exception) as excinfo:
pytest.fail("hello", short_msg="short message")
assert excinfo.exconly(tryshort=True) == "short message"
assert excinfo.exconly(tryshort=False) == "Failed: hello"


def test_pytest_exit_msg(testdir):
Expand Down
14 changes: 13 additions & 1 deletion testing/test_terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,14 +758,26 @@ def test(i):

def test_fail_extra_reporting(testdir, monkeypatch):
monkeypatch.setenv("COLUMNS", "80")
testdir.makepyfile("def test_this(): assert 0, 'this_failed' * 100")
testdir.makepyfile(
"""
def test_this():
assert 0, 'this_failed' * 100

def test_linematcher():
from _pytest.pytester import LineMatcher

LineMatcher(["1", "2", "3"]).fnmatch_lines(["2", "last_unmatched"])
"""
)
result = testdir.runpytest()
result.stdout.no_fnmatch_line("*short test summary*")
result = testdir.runpytest("-rf")
result.stdout.fnmatch_lines(
[
"*test summary*",
"FAILED test_fail_extra_reporting.py::test_this - AssertionError: this_failedt...",
"FAILED test_fail_extra_reporting.py::test_linematcher - remains unmatched: 'l...",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

"*= 2 failed in *",
]
)

Expand Down