From 3d0ecd03ed554442b43b1eb4c04d8bab2a99f4f4 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 29 Mar 2019 17:59:02 +0100 Subject: [PATCH 01/13] Display message from reprcrash in short test summary This is useful to see common patterns easily, but also for single failures already. --- src/_pytest/skipping.py | 20 +++++++++++++++++++- testing/acceptance_test.py | 4 +++- testing/test_skipping.py | 2 +- testing/test_terminal.py | 12 +++++++++--- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 22acafbddfb..9abed5b1f6f 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -211,7 +211,25 @@ def show_simple(terminalreporter, lines, stat): for rep in failed: verbose_word = _get_report_str(config, rep) pos = _get_pos(config, rep) - lines.append("%s %s" % (verbose_word, pos)) + + line = "%s %s" % (verbose_word, pos) + try: + msg = rep.longrepr.reprcrash.message + except AttributeError: + pass + else: + # Only use the first line. + # Might be worth having a short_message property, which + # could default to this behavior. + i = msg.find("\n") + if i != -1: + msg = msg[:i] + max_len = terminalreporter.writer.fullwidth - len(line) - 2 + if len(msg) > max_len: + msg = msg[: (max_len - 1)] + "…" + line += ": %s" % msg + + lines.append(line) def show_xfailed(terminalreporter, lines): diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 408fa076e1f..295dd832c2b 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -865,7 +865,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.""" diff --git a/testing/test_skipping.py b/testing/test_skipping.py index e5206a44ebf..fb0cf60e058 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1208,6 +1208,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", ] ) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index d0fdce23eb6..9b221366fb7 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -726,12 +726,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_failedthis…", + ] + ) def test_fail_reporting_on_pass(testdir): From 37ecca3ba9b1715735ec745b975f75327c3261b7 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 4 Apr 2019 22:13:28 +0200 Subject: [PATCH 02/13] factor out _get_line_with_reprcrash_message --- changelog/5013.feature.rst | 1 + src/_pytest/skipping.py | 58 +++++++++++++++++++++++++------------- testing/test_skipping.py | 44 +++++++++++++++++++++++++++++ testing/test_terminal.py | 2 +- 4 files changed, 84 insertions(+), 21 deletions(-) create mode 100644 changelog/5013.feature.rst diff --git a/changelog/5013.feature.rst b/changelog/5013.feature.rst new file mode 100644 index 00000000000..08f82efeb5e --- /dev/null +++ b/changelog/5013.feature.rst @@ -0,0 +1 @@ +Messages from crash reports are displayed within test summaries now, truncated to the terminal width. diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 9abed5b1f6f..e72eecccf3f 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -204,31 +204,49 @@ def pytest_terminal_summary(terminalreporter): tr._tw.line(line) +def _get_line_with_reprcrash_message(config, rep, termwidth): + """Get summary line for a report, trying to add reprcrash message.""" + verbose_word = _get_report_str(config, rep) + pos = _get_pos(config, rep) + + line = "%s %s" % (verbose_word, pos) + + len_line = len(line) + ellipsis = "..." + len_ellipsis = len(ellipsis) + + 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 = len(msg) + + sep = ": " + len_sep = len(sep) + max_len = termwidth - len_line - len_sep + if max_len >= len_ellipsis: + if len_msg > max_len: + msg = msg[: (max_len - len_ellipsis)] + ellipsis + line += sep + msg + return line + + def show_simple(terminalreporter, lines, stat): failed = terminalreporter.stats.get(stat) if failed: config = terminalreporter.config + termwidth = terminalreporter.writer.fullwidth for rep in failed: - verbose_word = _get_report_str(config, rep) - pos = _get_pos(config, rep) - - line = "%s %s" % (verbose_word, pos) - try: - msg = rep.longrepr.reprcrash.message - except AttributeError: - pass - else: - # Only use the first line. - # Might be worth having a short_message property, which - # could default to this behavior. - i = msg.find("\n") - if i != -1: - msg = msg[:i] - max_len = terminalreporter.writer.fullwidth - len(line) - 2 - if len(msg) > max_len: - msg = msg[: (max_len - 1)] + "…" - line += ": %s" % msg - + line = _get_line_with_reprcrash_message(config, rep, termwidth) lines.append(line) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index fb0cf60e058..93dd2a97d04 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1211,3 +1211,47 @@ def test_fail(): "FAILED test_summary_list_after_errors.py::test_fail: assert 0", ] ) + + +def test_line_with_reprcrash(monkeypatch): + import _pytest.skipping + from _pytest.skipping import _get_line_with_reprcrash_message + + def mock_get_report_str(*args): + return "FAILED" + + def mock_get_pos(*args): + return "some::nodeid" + + monkeypatch.setattr(_pytest.skipping, "_get_report_str", mock_get_report_str) + monkeypatch.setattr(_pytest.skipping, "_get_pos", mock_get_pos) + + class config: + pass + + class rep: + pass + + f = _get_line_with_reprcrash_message + assert f(config, rep, 80) == "FAILED some::nodeid" + + class rep: + class longrepr: + class reprcrash: + message = "msg" + + assert f(config, rep, 80) == "FAILED some::nodeid: msg" + assert f(config, rep, 3) == "FAILED some::nodeid" + + assert f(config, rep, 23) == "FAILED some::nodeid" + assert f(config, rep, 24) == "FAILED some::nodeid: msg" + + rep.longrepr.reprcrash.message = "some longer message" + assert f(config, rep, 23) == "FAILED some::nodeid" + assert f(config, rep, 24) == "FAILED some::nodeid: ..." + assert f(config, rep, 25) == "FAILED some::nodeid: s..." + + rep.longrepr.reprcrash.message = "some\nmessage" + assert f(config, rep, 24) == "FAILED some::nodeid: ..." + assert f(config, rep, 25) == "FAILED some::nodeid: some" + assert f(config, rep, 80) == "FAILED some::nodeid: some" diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 9b221366fb7..c465fc903c2 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -735,7 +735,7 @@ def test_fail_extra_reporting(testdir, monkeypatch): result.stdout.fnmatch_lines( [ "*test summary*", - "FAILED test_fail_extra_reporting.py::test_this: AssertionError: this_failedthis…", + "FAILED test_fail_extra_reporting.py::test_this: AssertionError: this_failedth...", ] ) From 159704421ebef2b2b0e40d1a8074b09f2d0f8a0f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 5 Apr 2019 12:17:08 +0200 Subject: [PATCH 03/13] change separator to hyphen --- src/_pytest/skipping.py | 16 ++++++---------- testing/test_skipping.py | 20 ++++++++++---------- testing/test_terminal.py | 2 +- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index e72eecccf3f..f7f085303b0 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -210,11 +210,8 @@ def _get_line_with_reprcrash_message(config, rep, termwidth): pos = _get_pos(config, rep) line = "%s %s" % (verbose_word, pos) - len_line = len(line) - ellipsis = "..." - len_ellipsis = len(ellipsis) - + ellipsis, len_ellipsis = "...", 3 if len_line > termwidth - len_ellipsis: # No space for an additional message. return line @@ -230,12 +227,11 @@ def _get_line_with_reprcrash_message(config, rep, termwidth): msg = msg[:i] len_msg = len(msg) - sep = ": " - len_sep = len(sep) - max_len = termwidth - len_line - len_sep - if max_len >= len_ellipsis: - if len_msg > max_len: - msg = msg[: (max_len - len_ellipsis)] + ellipsis + sep, len_sep = " - ", 3 + max_len_msg = termwidth - len_line - len_sep + if max_len_msg >= len_ellipsis: + if len_msg > max_len_msg: + msg = msg[: (max_len_msg - len_ellipsis)] + ellipsis line += sep + msg return line diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 93dd2a97d04..4121f0c28ac 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1208,7 +1208,7 @@ def test_fail(): [ "=* FAILURES *=", "*= short test summary info =*", - "FAILED test_summary_list_after_errors.py::test_fail: assert 0", + "FAILED test_summary_list_after_errors.py::test_fail - assert 0", ] ) @@ -1240,18 +1240,18 @@ class longrepr: class reprcrash: message = "msg" - assert f(config, rep, 80) == "FAILED some::nodeid: msg" + assert f(config, rep, 80) == "FAILED some::nodeid - msg" assert f(config, rep, 3) == "FAILED some::nodeid" - assert f(config, rep, 23) == "FAILED some::nodeid" - assert f(config, rep, 24) == "FAILED some::nodeid: msg" + assert f(config, rep, 24) == "FAILED some::nodeid" + assert f(config, rep, 25) == "FAILED some::nodeid - msg" rep.longrepr.reprcrash.message = "some longer message" - assert f(config, rep, 23) == "FAILED some::nodeid" - assert f(config, rep, 24) == "FAILED some::nodeid: ..." - assert f(config, rep, 25) == "FAILED some::nodeid: s..." + assert f(config, rep, 24) == "FAILED some::nodeid" + assert f(config, rep, 25) == "FAILED some::nodeid - ..." + assert f(config, rep, 26) == "FAILED some::nodeid - s..." rep.longrepr.reprcrash.message = "some\nmessage" - assert f(config, rep, 24) == "FAILED some::nodeid: ..." - assert f(config, rep, 25) == "FAILED some::nodeid: some" - assert f(config, rep, 80) == "FAILED some::nodeid: some" + assert f(config, rep, 25) == "FAILED some::nodeid - ..." + assert f(config, rep, 26) == "FAILED some::nodeid - some" + assert f(config, rep, 80) == "FAILED some::nodeid - some" diff --git a/testing/test_terminal.py b/testing/test_terminal.py index c465fc903c2..9664d9c7f4e 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -735,7 +735,7 @@ def test_fail_extra_reporting(testdir, monkeypatch): result.stdout.fnmatch_lines( [ "*test summary*", - "FAILED test_fail_extra_reporting.py::test_this: AssertionError: this_failedth...", + "FAILED test_fail_extra_reporting.py::test_this - AssertionError: this_failedt...", ] ) From f599172addeba19d992086187c309df416887f00 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 5 Apr 2019 16:08:11 +0200 Subject: [PATCH 04/13] =?UTF-8?q?test=20with=20=F0=9F=98=84=20in=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- testing/test_skipping.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 4121f0c28ac..e167d44770e 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1255,3 +1255,11 @@ class reprcrash: assert f(config, rep, 25) == "FAILED some::nodeid - ..." assert f(config, rep, 26) == "FAILED some::nodeid - some" assert f(config, rep, 80) == "FAILED some::nodeid - some" + + # Test unicode safety. + rep.longrepr.reprcrash.message = "πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line" + assert f(config, rep, 26) == "FAILED some::nodeid - πŸ˜„..." + # XXX: this is actually wrong - since the character uses two terminal + # cells. + rep.longrepr.reprcrash.message = "πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line" + assert f(config, rep, 26) == "FAILED some::nodeid - πŸ˜„πŸ˜„πŸ˜„πŸ˜„" From df377b589f5345b6cc5774f803a656fd0bdc9bf8 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 5 Apr 2019 17:43:11 +0200 Subject: [PATCH 05/13] use wcwidth --- setup.py | 1 + src/_pytest/skipping.py | 12 +++++-- testing/test_skipping.py | 73 +++++++++++++++++++++++++--------------- 3 files changed, 55 insertions(+), 31 deletions(-) diff --git a/setup.py b/setup.py index a924d4aba49..795a8c75fb0 100644 --- a/setup.py +++ b/setup.py @@ -14,6 +14,7 @@ 'pathlib2>=2.2.0;python_version<"3.6"', 'colorama;sys_platform=="win32"', "pluggy>=0.9", + "wcwidth", ] diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index f7f085303b0..1013f4889c5 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -206,11 +206,13 @@ def pytest_terminal_summary(terminalreporter): 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 = _get_report_str(config, rep) pos = _get_pos(config, rep) line = "%s %s" % (verbose_word, pos) - len_line = len(line) + len_line = wcswidth(line) ellipsis, len_ellipsis = "...", 3 if len_line > termwidth - len_ellipsis: # No space for an additional message. @@ -225,13 +227,17 @@ def _get_line_with_reprcrash_message(config, rep, termwidth): i = msg.find("\n") if i != -1: msg = msg[:i] - len_msg = len(msg) + 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: - msg = msg[: (max_len_msg - len_ellipsis)] + ellipsis + max_len_msg -= len_ellipsis + msg = msg[:max_len_msg] + while wcswidth(msg) > max_len_msg: + msg = msg[:-1] + msg += ellipsis line += sep + msg return line diff --git a/testing/test_skipping.py b/testing/test_skipping.py index e167d44770e..952877d8e6e 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1216,50 +1216,67 @@ def test_fail(): def test_line_with_reprcrash(monkeypatch): import _pytest.skipping from _pytest.skipping import _get_line_with_reprcrash_message + from wcwidth import wcswidth + + mocked_verbose_word = "FAILED" def mock_get_report_str(*args): - return "FAILED" + return mocked_verbose_word + + monkeypatch.setattr(_pytest.skipping, "_get_report_str", mock_get_report_str) + + mocked_pos = "some::nodeid" def mock_get_pos(*args): - return "some::nodeid" + return mocked_pos - monkeypatch.setattr(_pytest.skipping, "_get_report_str", mock_get_report_str) monkeypatch.setattr(_pytest.skipping, "_get_pos", mock_get_pos) class config: pass - class rep: - pass - - f = _get_line_with_reprcrash_message - assert f(config, rep, 80) == "FAILED some::nodeid" - class rep: class longrepr: class reprcrash: - message = "msg" + pass + + def check(msg, width, expected): + 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") - assert f(config, rep, 80) == "FAILED some::nodeid - msg" - assert f(config, rep, 3) == "FAILED some::nodeid" + check("msg", 80, "FAILED some::nodeid - msg") + check("msg", 3, "FAILED some::nodeid") - assert f(config, rep, 24) == "FAILED some::nodeid" - assert f(config, rep, 25) == "FAILED some::nodeid - msg" + check("msg", 24, "FAILED some::nodeid") + check("msg", 25, "FAILED some::nodeid - msg") - rep.longrepr.reprcrash.message = "some longer message" - assert f(config, rep, 24) == "FAILED some::nodeid" - assert f(config, rep, 25) == "FAILED some::nodeid - ..." - assert f(config, rep, 26) == "FAILED some::nodeid - s..." + 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...") - rep.longrepr.reprcrash.message = "some\nmessage" - assert f(config, rep, 25) == "FAILED some::nodeid - ..." - assert f(config, rep, 26) == "FAILED some::nodeid - some" - assert f(config, rep, 80) == "FAILED some::nodeid - some" + 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. - rep.longrepr.reprcrash.message = "πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line" - assert f(config, rep, 26) == "FAILED some::nodeid - πŸ˜„..." - # XXX: this is actually wrong - since the character uses two terminal - # cells. - rep.longrepr.reprcrash.message = "πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line" - assert f(config, rep, 26) == "FAILED some::nodeid - πŸ˜„πŸ˜„πŸ˜„πŸ˜„" + check("πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line", 25, "FAILED some::nodeid - ...") + check("πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line", 26, "FAILED some::nodeid - ...") + check("πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line", 27, "FAILED some::nodeid - πŸ˜„...") + check("πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line", 28, "FAILED some::nodeid - πŸ˜„...") + check("πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line", 29, "FAILED some::nodeid - πŸ˜„πŸ˜„...") + + mocked_pos = "nodeid::πŸ˜„::withunicode" + check("πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line", 29, "FAILED nodeid::πŸ˜„::withunicode") + check("πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line", 40, "FAILED nodeid::πŸ˜„::withunicode - πŸ˜„πŸ˜„...") + check("πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line", 41, "FAILED nodeid::πŸ˜„::withunicode - πŸ˜„πŸ˜„...") + check("πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line", 42, "FAILED nodeid::πŸ˜„::withunicode - πŸ˜„πŸ˜„πŸ˜„...") + check("πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line", 80, "FAILED nodeid::πŸ˜„::withunicode - πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„") From 2ebb69b50a761a041a8d9b96207c0e74c49db798 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 6 Apr 2019 15:00:12 +0200 Subject: [PATCH 06/13] py2 fixes --- testing/test_skipping.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 952877d8e6e..421531cc432 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1,3 +1,4 @@ +# coding=utf8 from __future__ import absolute_import from __future__ import division from __future__ import print_function @@ -1268,15 +1269,17 @@ def check(msg, width, expected): check("some\nmessage", 80, "FAILED some::nodeid - some") # Test unicode safety. - check("πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line", 25, "FAILED some::nodeid - ...") - check("πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line", 26, "FAILED some::nodeid - ...") - check("πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line", 27, "FAILED some::nodeid - πŸ˜„...") - check("πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line", 28, "FAILED some::nodeid - πŸ˜„...") - check("πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line", 29, "FAILED some::nodeid - πŸ˜„πŸ˜„...") - - mocked_pos = "nodeid::πŸ˜„::withunicode" - check("πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line", 29, "FAILED nodeid::πŸ˜„::withunicode") - check("πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line", 40, "FAILED nodeid::πŸ˜„::withunicode - πŸ˜„πŸ˜„...") - check("πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line", 41, "FAILED nodeid::πŸ˜„::withunicode - πŸ˜„πŸ˜„...") - check("πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line", 42, "FAILED nodeid::πŸ˜„::withunicode - πŸ˜„πŸ˜„πŸ˜„...") - check("πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„\n2nd line", 80, "FAILED nodeid::πŸ˜„::withunicode - πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„") + 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 - πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„") From 2b1ae8a66d07c2a0dcedf79125fe4dadf2df7abc Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 6 Apr 2019 15:00:23 +0200 Subject: [PATCH 07/13] __tracebackhide__ for check --- testing/test_skipping.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 421531cc432..f0da0488c78 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1242,6 +1242,7 @@ 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) From 14d3d9187fde788b715fa358a9d4519a9a4edfbf Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 11 Apr 2019 19:01:21 -0300 Subject: [PATCH 08/13] Remove partial unicode characters from summary messages in Python 2 --- src/_pytest/skipping.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 1013f4889c5..28a553e1831 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -1,8 +1,11 @@ +# coding=utf8 """ support for skip/xfail functions and markers. """ from __future__ import absolute_import from __future__ import division from __future__ import print_function +import six + from _pytest.config import hookimpl from _pytest.mark.evaluate import MarkEvaluator from _pytest.outcomes import fail @@ -237,6 +240,14 @@ def _get_line_with_reprcrash_message(config, rep, termwidth): 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 + while msg.endswith(u"\uD83D"): + msg = msg[:-1] msg += ellipsis line += sep + msg return line From c3178a176dda482f912da4d5c99c9396e7a8e9db Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 17 Apr 2019 15:30:34 +0200 Subject: [PATCH 09/13] move test --- testing/test_skipping.py | 72 ---------------------------------------- testing/test_terminal.py | 70 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 72 deletions(-) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 4782e7065f2..fb0822f8fd1 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1177,75 +1177,3 @@ def test_fail(): "FAILED test_summary_list_after_errors.py::test_fail - assert 0", ] ) - - -def test_line_with_reprcrash(monkeypatch): - import _pytest.skipping - from _pytest.skipping import _get_line_with_reprcrash_message - from wcwidth import wcswidth - - mocked_verbose_word = "FAILED" - - def mock_get_report_str(*args): - return mocked_verbose_word - - monkeypatch.setattr(_pytest.skipping, "_get_report_str", mock_get_report_str) - - mocked_pos = "some::nodeid" - - def mock_get_pos(*args): - return mocked_pos - - monkeypatch.setattr(_pytest.skipping, "_get_pos", mock_get_pos) - - class config: - pass - - class rep: - 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 - πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„") diff --git a/testing/test_terminal.py b/testing/test_terminal.py index ee546a4a114..cf0faf5c3e4 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -17,6 +17,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 @@ -1582,3 +1583,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: + pass + + class rep: + 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 - πŸ˜„πŸ˜„πŸ˜„πŸ˜„πŸ˜„") From 0e8a8f94f6967dc549dd3def4b64bd07106e4547 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 5 May 2019 09:14:07 -0300 Subject: [PATCH 10/13] Add encoding header to test_terminal.py --- testing/test_terminal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index cf0faf5c3e4..35981b56871 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1,3 +1,4 @@ +# encoding: utf-8 """ terminal reporting of the full testing process. """ From 32a5e80a6d805e3cefb67b201f2e960b5cdd82f9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 5 May 2019 09:33:37 -0300 Subject: [PATCH 11/13] Add encoding: header and fix rep mock in test_line_with_reprcrash on py27 --- src/_pytest/terminal.py | 1 + testing/test_terminal.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index af836658b0c..5330d81cb84 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -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. diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 35981b56871..da5d9ca44be 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1599,10 +1599,10 @@ def mock_get_pos(*args): monkeypatch.setattr(_pytest.terminal, "_get_pos", mock_get_pos) - class config: + class config(object): pass - class rep: + class rep(object): def _get_verbose_word(self, *args): return mocked_verbose_word @@ -1614,7 +1614,7 @@ def check(msg, width, expected): __tracebackhide__ = True if msg: rep.longrepr.reprcrash.message = msg - actual = _get_line_with_reprcrash_message(config, rep, width) + actual = _get_line_with_reprcrash_message(config, rep(), width) assert actual == expected if actual != "%s %s" % (mocked_verbose_word, mocked_pos): From c04767f9469c58984bcfd004c1e2995bc36bbc0d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 7 May 2019 15:20:00 -0300 Subject: [PATCH 12/13] Use msg.rstrip() as suggested in review --- src/_pytest/terminal.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 5330d81cb84..91526d00f76 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -1000,8 +1000,7 @@ def _get_line_with_reprcrash_message(config, rep, termwidth): # 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 - while msg.endswith(u"\uD83D"): - msg = msg[:-1] + msg = msg.rstrip(u"\uD83D") msg += ellipsis line += sep + msg return line From f339147d120452f0dcc7a524822bc6ad53696142 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 7 May 2019 19:34:57 -0300 Subject: [PATCH 13/13] Add CHANGELOG entry about depending on wcwidth --- changelog/5013.trivial.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/5013.trivial.rst diff --git a/changelog/5013.trivial.rst b/changelog/5013.trivial.rst new file mode 100644 index 00000000000..fff4eaf3fcb --- /dev/null +++ b/changelog/5013.trivial.rst @@ -0,0 +1 @@ +pytest now depends on `wcwidth `__ to properly track unicode character sizes for more precise terminal output.