Merge pull request #5013 from blueyed/short-summary-message
Display message from reprcrash in short test summary
This commit is contained in:
commit
5eeb5ee960
|
@ -0,0 +1 @@
|
||||||
|
Messages from crash reports are displayed within test summaries now, truncated to the terminal width.
|
|
@ -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
setup.py
1
setup.py
|
@ -14,6 +14,7 @@ INSTALL_REQUIRES = [
|
||||||
'pathlib2>=2.2.0;python_version<"3.6"',
|
'pathlib2>=2.2.0;python_version<"3.6"',
|
||||||
'colorama;sys_platform=="win32"',
|
'colorama;sys_platform=="win32"',
|
||||||
"pluggy>=0.9",
|
"pluggy>=0.9",
|
||||||
|
"wcwidth",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# coding=utf8
|
||||||
""" support for skip/xfail functions and markers. """
|
""" support for skip/xfail functions and markers. """
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import division
|
from __future__ import division
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# encoding: utf-8
|
||||||
""" terminal reporting of the full testing process.
|
""" terminal reporting of the full testing process.
|
||||||
|
|
||||||
This is a good source for looking at the various reporting hooks.
|
This is a good source for looking at the various reporting hooks.
|
||||||
|
@ -887,10 +888,13 @@ class TerminalReporter(object):
|
||||||
|
|
||||||
def show_simple(stat, lines):
|
def show_simple(stat, lines):
|
||||||
failed = self.stats.get(stat, [])
|
failed = self.stats.get(stat, [])
|
||||||
|
if not failed:
|
||||||
|
return
|
||||||
|
termwidth = self.writer.fullwidth
|
||||||
|
config = self.config
|
||||||
for rep in failed:
|
for rep in failed:
|
||||||
verbose_word = rep._get_verbose_word(self.config)
|
line = _get_line_with_reprcrash_message(config, rep, termwidth)
|
||||||
pos = _get_pos(self.config, rep)
|
lines.append(line)
|
||||||
lines.append("%s %s" % (verbose_word, pos))
|
|
||||||
|
|
||||||
def show_xfailed(lines):
|
def show_xfailed(lines):
|
||||||
xfailed = self.stats.get("xfailed", [])
|
xfailed = self.stats.get("xfailed", [])
|
||||||
|
@ -927,10 +931,6 @@ class TerminalReporter(object):
|
||||||
else:
|
else:
|
||||||
lines.append("%s [%d] %s: %s" % (verbose_word, num, fspath, reason))
|
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 = {
|
REPORTCHAR_ACTIONS = {
|
||||||
"x": show_xfailed,
|
"x": show_xfailed,
|
||||||
"X": show_xpassed,
|
"X": show_xpassed,
|
||||||
|
@ -954,6 +954,56 @@ class TerminalReporter(object):
|
||||||
self.write_line(line)
|
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):
|
def _folded_skips(skipped):
|
||||||
d = {}
|
d = {}
|
||||||
for event in skipped:
|
for event in skipped:
|
||||||
|
|
|
@ -876,7 +876,9 @@ class TestInvocationVariants(object):
|
||||||
_fail, _sep, testid = line.partition(" ")
|
_fail, _sep, testid = line.partition(" ")
|
||||||
break
|
break
|
||||||
result = testdir.runpytest(testid, "-rf")
|
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):
|
def test_core_backward_compatibility(self):
|
||||||
"""Test backward compatibility for get_plugin_manager function. See #787."""
|
"""Test backward compatibility for get_plugin_manager function. See #787."""
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# coding=utf8
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import division
|
from __future__ import division
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
@ -1173,6 +1174,6 @@ def test_summary_list_after_errors(testdir):
|
||||||
[
|
[
|
||||||
"=* FAILURES *=",
|
"=* FAILURES *=",
|
||||||
"*= short test summary info =*",
|
"*= short test summary info =*",
|
||||||
"FAILED test_summary_list_after_errors.py::test_fail",
|
"FAILED test_summary_list_after_errors.py::test_fail - assert 0",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# encoding: utf-8
|
||||||
"""
|
"""
|
||||||
terminal reporting of the full testing process.
|
terminal reporting of the full testing process.
|
||||||
"""
|
"""
|
||||||
|
@ -17,6 +18,7 @@ import pytest
|
||||||
from _pytest.main import EXIT_NOTESTSCOLLECTED
|
from _pytest.main import EXIT_NOTESTSCOLLECTED
|
||||||
from _pytest.reports import BaseReport
|
from _pytest.reports import BaseReport
|
||||||
from _pytest.terminal import _folded_skips
|
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 _plugin_nameversions
|
||||||
from _pytest.terminal import build_summary_stats_line
|
from _pytest.terminal import build_summary_stats_line
|
||||||
from _pytest.terminal import getreportopt
|
from _pytest.terminal import getreportopt
|
||||||
|
@ -758,12 +760,18 @@ class TestTerminalFunctional(object):
|
||||||
result.stdout.fnmatch_lines(["collected 3 items", "hello from hook: 3 items"])
|
result.stdout.fnmatch_lines(["collected 3 items", "hello from hook: 3 items"])
|
||||||
|
|
||||||
|
|
||||||
def test_fail_extra_reporting(testdir):
|
def test_fail_extra_reporting(testdir, monkeypatch):
|
||||||
testdir.makepyfile("def test_this(): assert 0")
|
monkeypatch.setenv("COLUMNS", "80")
|
||||||
|
testdir.makepyfile("def test_this(): assert 0, 'this_failed' * 100")
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
assert "short test summary" not in result.stdout.str()
|
assert "short test summary" not in result.stdout.str()
|
||||||
result = testdir.runpytest("-rf")
|
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):
|
def test_fail_reporting_on_pass(testdir):
|
||||||
|
@ -1607,3 +1615,72 @@ def test_skip_reasons_folding():
|
||||||
assert fspath == path
|
assert fspath == path
|
||||||
assert lineno == lineno
|
assert lineno == lineno
|
||||||
assert reason == message
|
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 - 😄😄😄😄😄")
|
||||||
|
|
Loading…
Reference in New Issue