Skip to content

Commit

Permalink
Merge pull request #5013 from blueyed/short-summary-message
Browse files Browse the repository at this point in the history
Display message from reprcrash in short test summary
  • Loading branch information
blueyed authored May 8, 2019
2 parents ed2b715 + f339147 commit 5eeb5ee
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 12 deletions.
1 change: 1 addition & 0 deletions changelog/5013.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Messages from crash reports are displayed within test summaries now, truncated to the terminal width.
1 change: 1 addition & 0 deletions changelog/5013.trivial.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pytest now depends on `wcwidth <https://pypi.org/project/wcwidth>`__ to properly track unicode character sizes for more precise terminal output.
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
'pathlib2>=2.2.0;python_version<"3.6"',
'colorama;sys_platform=="win32"',
"pluggy>=0.9",
"wcwidth",
]


Expand Down
1 change: 1 addition & 0 deletions src/_pytest/skipping.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# coding=utf8
""" support for skip/xfail functions and markers. """
from __future__ import absolute_import
from __future__ import division
Expand Down
64 changes: 57 additions & 7 deletions src/_pytest/terminal.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# encoding: utf-8
""" terminal reporting of the full testing process.
This is a good source for looking at the various reporting hooks.
Expand Down Expand Up @@ -887,10 +888,13 @@ def short_test_summary(self):

def show_simple(stat, lines):
failed = self.stats.get(stat, [])
if not failed:
return
termwidth = self.writer.fullwidth
config = self.config
for rep in failed:
verbose_word = rep._get_verbose_word(self.config)
pos = _get_pos(self.config, rep)
lines.append("%s %s" % (verbose_word, pos))
line = _get_line_with_reprcrash_message(config, rep, termwidth)
lines.append(line)

def show_xfailed(lines):
xfailed = self.stats.get("xfailed", [])
Expand Down Expand Up @@ -927,10 +931,6 @@ def show_skipped(lines):
else:
lines.append("%s [%d] %s: %s" % (verbose_word, num, fspath, reason))

def _get_pos(config, rep):
nodeid = config.cwd_relative_nodeid(rep.nodeid)
return nodeid

REPORTCHAR_ACTIONS = {
"x": show_xfailed,
"X": show_xpassed,
Expand All @@ -954,6 +954,56 @@ def _get_pos(config, rep):
self.write_line(line)


def _get_pos(config, rep):
nodeid = config.cwd_relative_nodeid(rep.nodeid)
return nodeid


def _get_line_with_reprcrash_message(config, rep, termwidth):
"""Get summary line for a report, trying to add reprcrash message."""
from wcwidth import wcswidth

verbose_word = rep._get_verbose_word(config)
pos = _get_pos(config, rep)

line = "%s %s" % (verbose_word, pos)
len_line = wcswidth(line)
ellipsis, len_ellipsis = "...", 3
if len_line > termwidth - len_ellipsis:
# No space for an additional message.
return line

try:
msg = rep.longrepr.reprcrash.message
except AttributeError:
pass
else:
# Only use the first line.
i = msg.find("\n")
if i != -1:
msg = msg[:i]
len_msg = wcswidth(msg)

sep, len_sep = " - ", 3
max_len_msg = termwidth - len_line - len_sep
if max_len_msg >= len_ellipsis:
if len_msg > max_len_msg:
max_len_msg -= len_ellipsis
msg = msg[:max_len_msg]
while wcswidth(msg) > max_len_msg:
msg = msg[:-1]
if six.PY2:
# on python 2 systems with narrow unicode compilation, trying to
# get a single character out of a multi-byte unicode character such as
# u'😄' will result in a High Surrogate (U+D83D) character, which is
# rendered as u'�'; in this case we just strip that character out as it
# serves no purpose being rendered
msg = msg.rstrip(u"\uD83D")
msg += ellipsis
line += sep + msg
return line


def _folded_skips(skipped):
d = {}
for event in skipped:
Expand Down
4 changes: 3 additions & 1 deletion testing/acceptance_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -876,7 +876,9 @@ def test_doctest_id(self, testdir):
_fail, _sep, testid = line.partition(" ")
break
result = testdir.runpytest(testid, "-rf")
result.stdout.fnmatch_lines([line, "*1 failed*"])
result.stdout.fnmatch_lines(
["FAILED test_doctest_id.txt::test_doctest_id.txt", "*1 failed*"]
)

def test_core_backward_compatibility(self):
"""Test backward compatibility for get_plugin_manager function. See #787."""
Expand Down
3 changes: 2 additions & 1 deletion testing/test_skipping.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# coding=utf8
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
Expand Down Expand Up @@ -1173,6 +1174,6 @@ def test_fail():
[
"=* FAILURES *=",
"*= short test summary info =*",
"FAILED test_summary_list_after_errors.py::test_fail",
"FAILED test_summary_list_after_errors.py::test_fail - assert 0",
]
)
83 changes: 80 additions & 3 deletions testing/test_terminal.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# encoding: utf-8
"""
terminal reporting of the full testing process.
"""
Expand All @@ -17,6 +18,7 @@
from _pytest.main import EXIT_NOTESTSCOLLECTED
from _pytest.reports import BaseReport
from _pytest.terminal import _folded_skips
from _pytest.terminal import _get_line_with_reprcrash_message
from _pytest.terminal import _plugin_nameversions
from _pytest.terminal import build_summary_stats_line
from _pytest.terminal import getreportopt
Expand Down Expand Up @@ -758,12 +760,18 @@ def test(i):
result.stdout.fnmatch_lines(["collected 3 items", "hello from hook: 3 items"])


def test_fail_extra_reporting(testdir):
testdir.makepyfile("def test_this(): assert 0")
def test_fail_extra_reporting(testdir, monkeypatch):
monkeypatch.setenv("COLUMNS", "80")
testdir.makepyfile("def test_this(): assert 0, 'this_failed' * 100")
result = testdir.runpytest()
assert "short test summary" not in result.stdout.str()
result = testdir.runpytest("-rf")
result.stdout.fnmatch_lines(["*test summary*", "FAIL*test_fail_extra_reporting*"])
result.stdout.fnmatch_lines(
[
"*test summary*",
"FAILED test_fail_extra_reporting.py::test_this - AssertionError: this_failedt...",
]
)


def test_fail_reporting_on_pass(testdir):
Expand Down Expand Up @@ -1607,3 +1615,72 @@ class X(object):
assert fspath == path
assert lineno == lineno
assert reason == message


def test_line_with_reprcrash(monkeypatch):
import _pytest.terminal
from wcwidth import wcswidth

mocked_verbose_word = "FAILED"

mocked_pos = "some::nodeid"

def mock_get_pos(*args):
return mocked_pos

monkeypatch.setattr(_pytest.terminal, "_get_pos", mock_get_pos)

class config(object):
pass

class rep(object):
def _get_verbose_word(self, *args):
return mocked_verbose_word

class longrepr:
class reprcrash:
pass

def check(msg, width, expected):
__tracebackhide__ = True
if msg:
rep.longrepr.reprcrash.message = msg
actual = _get_line_with_reprcrash_message(config, rep(), width)

assert actual == expected
if actual != "%s %s" % (mocked_verbose_word, mocked_pos):
assert len(actual) <= width
assert wcswidth(actual) <= width

# AttributeError with message
check(None, 80, "FAILED some::nodeid")

check("msg", 80, "FAILED some::nodeid - msg")
check("msg", 3, "FAILED some::nodeid")

check("msg", 24, "FAILED some::nodeid")
check("msg", 25, "FAILED some::nodeid - msg")

check("some longer msg", 24, "FAILED some::nodeid")
check("some longer msg", 25, "FAILED some::nodeid - ...")
check("some longer msg", 26, "FAILED some::nodeid - s...")

check("some\nmessage", 25, "FAILED some::nodeid - ...")
check("some\nmessage", 26, "FAILED some::nodeid - some")
check("some\nmessage", 80, "FAILED some::nodeid - some")

# Test unicode safety.
check(u"😄😄😄😄😄\n2nd line", 25, u"FAILED some::nodeid - ...")
check(u"😄😄😄😄😄\n2nd line", 26, u"FAILED some::nodeid - ...")
check(u"😄😄😄😄😄\n2nd line", 27, u"FAILED some::nodeid - 😄...")
check(u"😄😄😄😄😄\n2nd line", 28, u"FAILED some::nodeid - 😄...")
check(u"😄😄😄😄😄\n2nd line", 29, u"FAILED some::nodeid - 😄😄...")

# NOTE: constructed, not sure if this is supported.
# It would fail if not using u"" in Python 2 for mocked_pos.
mocked_pos = u"nodeid::😄::withunicode"
check(u"😄😄😄😄😄\n2nd line", 29, u"FAILED nodeid::😄::withunicode")
check(u"😄😄😄😄😄\n2nd line", 40, u"FAILED nodeid::😄::withunicode - 😄😄...")
check(u"😄😄😄😄😄\n2nd line", 41, u"FAILED nodeid::😄::withunicode - 😄😄...")
check(u"😄😄😄😄😄\n2nd line", 42, u"FAILED nodeid::😄::withunicode - 😄😄😄...")
check(u"😄😄😄😄😄\n2nd line", 80, u"FAILED nodeid::😄::withunicode - 😄😄😄😄😄")

0 comments on commit 5eeb5ee

Please sign in to comment.