Merge pull request #5013 from blueyed/short-summary-message

Display message from reprcrash in short test summary
This commit is contained in:
Daniel Hahler 2019-05-08 22:01:04 +02:00 committed by GitHub
commit 5eeb5ee960
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 146 additions and 12 deletions

View File

@ -0,0 +1 @@
Messages from crash reports are displayed within test summaries now, truncated to the terminal width.

View File

@ -0,0 +1 @@
pytest now depends on `wcwidth <https://pypi.org/project/wcwidth>`__ to properly track unicode character sizes for more precise terminal output.

View File

@ -14,6 +14,7 @@ INSTALL_REQUIRES = [
'pathlib2>=2.2.0;python_version<"3.6"',
'colorama;sys_platform=="win32"',
"pluggy>=0.9",
"wcwidth",
]

View File

@ -1,3 +1,4 @@
# coding=utf8
""" support for skip/xfail functions and markers. """
from __future__ import absolute_import
from __future__ import division

View File

@ -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.
@ -887,10 +888,13 @@ class TerminalReporter(object):
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", [])
@ -927,10 +931,6 @@ class TerminalReporter(object):
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,
@ -954,6 +954,56 @@ class TerminalReporter(object):
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'<27>'; 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:

View File

@ -876,7 +876,9 @@ class TestInvocationVariants(object):
_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."""

View File

@ -1,3 +1,4 @@
# coding=utf8
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
@ -1173,6 +1174,6 @@ def test_summary_list_after_errors(testdir):
[
"=* 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",
]
)

View File

@ -1,3 +1,4 @@
# encoding: utf-8
"""
terminal reporting of the full testing process.
"""
@ -17,6 +18,7 @@ import pytest
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
@ -758,12 +760,18 @@ class TestTerminalFunctional(object):
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):
@ -1607,3 +1615,72 @@ def test_skip_reasons_folding():
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 - 😄😄😄😄😄")