From eef9b484f72bbafd8001a34018964a183fcdd9d0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 2 Feb 2020 10:29:11 -0300 Subject: [PATCH 1/5] Use code highlighting if pygments is installed --- changelog/6658.improvement.rst | 4 ++ src/_pytest/_code/code.py | 49 ++++++++++++++++++++--- src/_pytest/_io/__init__.py | 42 +++++++++++++++++-- testing/code/test_terminal_writer.py | 45 +++++++++++++++++++++ testing/conftest.py | 6 +++ testing/test_terminal.py | 60 +++++++++++++++++++++++++--- tox.ini | 1 + 7 files changed, 192 insertions(+), 15 deletions(-) create mode 100644 changelog/6658.improvement.rst create mode 100644 testing/code/test_terminal_writer.py diff --git a/changelog/6658.improvement.rst b/changelog/6658.improvement.rst new file mode 100644 index 00000000000..56b21f7e208 --- /dev/null +++ b/changelog/6658.improvement.rst @@ -0,0 +1,4 @@ +Code is now highlighted in tracebacks when ``pygments`` is installed. + +Users are encouraged to install ``pygments`` into their environment and provide feedback, because +the plan is to make ``pygments`` a regular dependency in the future. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index fba52926ee6..babac34f6c8 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -1039,21 +1039,58 @@ def __init__( self.reprfileloc = filelocrepr self.style = style + def _write_entry_lines(self, tw: TerminalWriter) -> None: + """Writes the source code portions of a list of traceback entries with syntax highlighting. + + Usually entries are lines like these: + + " x = 1" + "> assert x == 2" + "E assert 1 == 2" + + This function takes care of rendering the "source" portions of it (the lines without + the "E" prefix) using syntax highlighting, taking care to not highlighting the ">" + character, as doing so might break line continuations. + """ + + indent_size = 4 + + def is_fail(line): + return line.startswith("{} ".format(FormattedExcinfo.fail_marker)) + + if not self.lines: + return + + # separate indents and source lines that are not failures: we want to + # highlight the code but not the indentation, which may contain markers + # such as "> assert 0" + indents = [] + source_lines = [] + for line in self.lines: + if not is_fail(line): + indents.append(line[:indent_size]) + source_lines.append(line[indent_size:]) + + tw._write_source(source_lines, indents) + + # failure lines are always completely red and bold + for line in (x for x in self.lines if is_fail(x)): + tw.line(line, bold=True, red=True) + def toterminal(self, tw: TerminalWriter) -> None: if self.style == "short": assert self.reprfileloc is not None self.reprfileloc.toterminal(tw) - for line in self.lines: - red = line.startswith("E ") - tw.line(line, bold=True, red=red) + self._write_entry_lines(tw) if self.reprlocals: self.reprlocals.toterminal(tw, indent=" " * 8) return + if self.reprfuncargs: self.reprfuncargs.toterminal(tw) - for line in self.lines: - red = line.startswith("E ") - tw.line(line, bold=True, red=red) + + self._write_entry_lines(tw) + if self.reprlocals: tw.line("") self.reprlocals.toterminal(tw) diff --git a/src/_pytest/_io/__init__.py b/src/_pytest/_io/__init__.py index 047bb179a89..f56579806cc 100644 --- a/src/_pytest/_io/__init__.py +++ b/src/_pytest/_io/__init__.py @@ -1,3 +1,39 @@ -# Reexport TerminalWriter from here instead of py, to make it easier to -# extend or swap our own implementation in the future. -from py.io import TerminalWriter as TerminalWriter # noqa: F401 +from typing import List +from typing import Sequence + +from py.io import TerminalWriter as BaseTerminalWriter # noqa: F401 + + +class TerminalWriter(BaseTerminalWriter): + def _write_source(self, lines: List[str], indents: Sequence[str] = ()) -> None: + """Write lines of source code possibly highlighted. + + Keeping this private for now because the API is clunky. We should discuss how + to evolve the terminal writer so we can have more precise color support, for example + being able to write part of a line in one color and the rest in another, and so on. + """ + if indents and len(indents) != len(lines): + raise ValueError( + "indents size ({}) should have same size as lines ({})".format( + len(indents), len(lines) + ) + ) + if not indents: + indents = [""] * len(lines) + source = "\n".join(lines) + new_lines = self._highlight(source).splitlines() + for indent, new_line in zip(indents, new_lines): + self.line(indent + new_line) + + def _highlight(self, source): + """Highlight the given source code according to the "code_highlight" option""" + if not self.hasmarkup: + return source + try: + from pygments.formatters.terminal import TerminalFormatter + from pygments.lexers.python import PythonLexer + from pygments import highlight + except ImportError: + return source + else: + return highlight(source, PythonLexer(), TerminalFormatter(bg="dark")) diff --git a/testing/code/test_terminal_writer.py b/testing/code/test_terminal_writer.py new file mode 100644 index 00000000000..e6b0f3223ec --- /dev/null +++ b/testing/code/test_terminal_writer.py @@ -0,0 +1,45 @@ +import re +from io import StringIO + +import pytest +from _pytest._io import TerminalWriter + + +# TODO: move this and the other two related attributes from test_terminal.py into conftest as a +# fixture +COLORS = { + "red": "\x1b[31m", + "green": "\x1b[32m", + "yellow": "\x1b[33m", + "bold": "\x1b[1m", + "reset": "\x1b[0m", + "kw": "\x1b[94m", + "hl-reset": "\x1b[39;49;00m", + "function": "\x1b[92m", + "number": "\x1b[94m", + "str": "\x1b[33m", + "print": "\x1b[96m", +} + + +@pytest.mark.parametrize( + "has_markup, expected", + [ + pytest.param( + True, "{kw}assert{hl-reset} {number}0{hl-reset}\n", id="with markup" + ), + pytest.param(False, "assert 0\n", id="no markup"), + ], +) +def test_code_highlight(has_markup, expected): + f = StringIO() + tw = TerminalWriter(f) + tw.hasmarkup = has_markup + tw._write_source(["assert 0"]) + assert f.getvalue() == expected.format(**COLORS) + + with pytest.raises( + ValueError, + match=re.escape("indents size (2) should have same size as lines (1)"), + ): + tw._write_source(["assert 0"], [" ", " "]) diff --git a/testing/conftest.py b/testing/conftest.py index 3127fda6a83..dab82842ef3 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -78,6 +78,12 @@ def sep(self, sep, line=None): def write(self, msg, **kw): self.lines.append((TWMock.WRITE, msg)) + def _write_source(self, lines, indents=()): + if not indents: + indents = [""] * len(lines) + for indent, line in zip(indents, lines): + self.line(indent + line) + def line(self, line, **kw): self.lines.append(line) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index d1ebd25a15f..4d5c2fc8baf 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -30,6 +30,12 @@ "yellow": "\x1b[33m", "bold": "\x1b[1m", "reset": "\x1b[0m", + "kw": "\x1b[94m", + "hl-reset": "\x1b[39;49;00m", + "function": "\x1b[92m", + "number": "\x1b[94m", + "str": "\x1b[33m", + "print": "\x1b[96m", } RE_COLORS = {k: re.escape(v) for k, v in COLORS.items()} @@ -918,14 +924,14 @@ def test_this(): "=*= FAILURES =*=", "{red}{bold}_*_ test_this _*_{reset}", "", - "{bold} def test_this():{reset}", - "{bold}> fail(){reset}", + " {kw}def{hl-reset} {function}test_this{hl-reset}():", + "> fail()", "", "{bold}{red}test_color_yes.py{reset}:5: ", "_ _ * _ _*", "", - "{bold} def fail():{reset}", - "{bold}> assert 0{reset}", + " {kw}def{hl-reset} {function}fail{hl-reset}():", + "> {kw}assert{hl-reset} {number}0{hl-reset}", "{bold}{red}E assert 0{reset}", "", "{bold}{red}test_color_yes.py{reset}:2: AssertionError", @@ -946,9 +952,9 @@ def test_this(): "=*= FAILURES =*=", "{red}{bold}_*_ test_this _*_{reset}", "{bold}{red}test_color_yes.py{reset}:5: in test_this", - "{bold} fail(){reset}", + " fail()", "{bold}{red}test_color_yes.py{reset}:2: in fail", - "{bold} assert 0{reset}", + " {kw}assert{hl-reset} {number}0{hl-reset}", "{bold}{red}E assert 0{reset}", "{red}=*= {red}{bold}1 failed{reset}{red} in *s{reset}{red} =*={reset}", ] @@ -2021,3 +2027,45 @@ def test_via_exec(testdir: Testdir) -> None: result.stdout.fnmatch_lines( ["test_via_exec.py::test_via_exec <- PASSED*", "*= 1 passed in *"] ) + + +class TestCodeHighlight: + def test_code_highlight_simple(self, testdir: Testdir) -> None: + testdir.makepyfile( + """ + def test_foo(): + assert 1 == 10 + """ + ) + result = testdir.runpytest("--color=yes") + result.stdout.fnmatch_lines( + [ + line.format(**COLORS).replace("[", "[[]") + for line in [ + " {kw}def{hl-reset} {function}test_foo{hl-reset}():", + "> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}", + "{bold}{red}E assert 1 == 10{reset}", + ] + ] + ) + + def test_code_highlight_continuation(self, testdir: Testdir) -> None: + testdir.makepyfile( + """ + def test_foo(): + print(''' + '''); assert 0 + """ + ) + result = testdir.runpytest("--color=yes") + result.stdout.fnmatch_lines( + [ + line.format(**COLORS).replace("[", "[[]") + for line in [ + " {kw}def{hl-reset} {function}test_foo{hl-reset}():", + " {print}print{hl-reset}({str}'''{hl-reset}{str}{hl-reset}", + "> {str} {hl-reset}{str}'''{hl-reset}); {kw}assert{hl-reset} {number}0{hl-reset}", + "{bold}{red}E assert 0{reset}", + ] + ] + ) diff --git a/tox.ini b/tox.ini index 65af0a4a86c..2cdc7ad56c1 100644 --- a/tox.ini +++ b/tox.ini @@ -49,6 +49,7 @@ deps = numpy: numpy pexpect: pexpect pluggymaster: git+https://github.com/pytest-dev/pluggy.git@master + pygments twisted: twisted xdist: pytest-xdist>=1.13 {env:_PYTEST_TOX_EXTRA_DEP:} From d218218e3090bc18fe3d728ccc1840ac1600226a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 11 Feb 2020 20:00:32 -0300 Subject: [PATCH 2/5] Use colorama constants instead of bare ascii codes Could not find the exact equivalent of 'hl-reset' code using colorama constants though. --- testing/test_terminal.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 4d5c2fc8baf..438fa82087a 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -10,6 +10,8 @@ import pluggy import py +from colorama import Fore +from colorama import Style import pytest from _pytest.config import ExitCode @@ -25,17 +27,17 @@ DistInfo = collections.namedtuple("DistInfo", ["project_name", "version"]) COLORS = { - "red": "\x1b[31m", - "green": "\x1b[32m", - "yellow": "\x1b[33m", - "bold": "\x1b[1m", - "reset": "\x1b[0m", - "kw": "\x1b[94m", + "red": Fore.RED, + "green": Fore.GREEN, + "yellow": Fore.YELLOW, + "bold": Style.BRIGHT, + "reset": Style.RESET_ALL, + "kw": Fore.LIGHTBLUE_EX, "hl-reset": "\x1b[39;49;00m", - "function": "\x1b[92m", - "number": "\x1b[94m", - "str": "\x1b[33m", - "print": "\x1b[96m", + "function": Fore.LIGHTGREEN_EX, + "number": Fore.LIGHTBLUE_EX, + "str": Fore.YELLOW, + "print": Fore.LIGHTCYAN_EX, } RE_COLORS = {k: re.escape(v) for k, v in COLORS.items()} From 84406db423c4ee775da7864b8b5bcd13ce8e5823 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 11 Feb 2020 20:21:21 -0300 Subject: [PATCH 3/5] Refactor ASCII color handling into a fixture --- testing/code/test_terminal_writer.py | 21 +------- testing/conftest.py | 46 ++++++++++++++++++ testing/test_terminal.py | 71 ++++++++++------------------ 3 files changed, 73 insertions(+), 65 deletions(-) diff --git a/testing/code/test_terminal_writer.py b/testing/code/test_terminal_writer.py index e6b0f3223ec..01da3c23500 100644 --- a/testing/code/test_terminal_writer.py +++ b/testing/code/test_terminal_writer.py @@ -5,23 +5,6 @@ from _pytest._io import TerminalWriter -# TODO: move this and the other two related attributes from test_terminal.py into conftest as a -# fixture -COLORS = { - "red": "\x1b[31m", - "green": "\x1b[32m", - "yellow": "\x1b[33m", - "bold": "\x1b[1m", - "reset": "\x1b[0m", - "kw": "\x1b[94m", - "hl-reset": "\x1b[39;49;00m", - "function": "\x1b[92m", - "number": "\x1b[94m", - "str": "\x1b[33m", - "print": "\x1b[96m", -} - - @pytest.mark.parametrize( "has_markup, expected", [ @@ -31,12 +14,12 @@ pytest.param(False, "assert 0\n", id="no markup"), ], ) -def test_code_highlight(has_markup, expected): +def test_code_highlight(has_markup, expected, color_mapping): f = StringIO() tw = TerminalWriter(f) tw.hasmarkup = has_markup tw._write_source(["assert 0"]) - assert f.getvalue() == expected.format(**COLORS) + assert f.getvalue().splitlines(keepends=True) == color_mapping.format([expected]) with pytest.raises( ValueError, diff --git a/testing/conftest.py b/testing/conftest.py index dab82842ef3..53d8dff8ce8 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -1,4 +1,6 @@ +import re import sys +from typing import List import pytest from _pytest.pytester import Testdir @@ -127,7 +129,51 @@ def runtest(self): testdir.makefile(".yaml", test1="") + @pytest.fixture def testdir(testdir: Testdir) -> Testdir: testdir.monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") return testdir + +@pytest.fixture(scope="session") +def color_mapping(): + """Returns a utility class which can replace keys in strings in the form "{NAME}" + by their equivalent ASCII codes in the terminal. + + Used by tests which check the actual colors output by pytest. + """ + from colorama import Fore, Style + + class ColorMapping: + COLORS = { + "red": Fore.RED, + "green": Fore.GREEN, + "yellow": Fore.YELLOW, + "bold": Style.BRIGHT, + "reset": Style.RESET_ALL, + "kw": Fore.LIGHTBLUE_EX, + "hl-reset": "\x1b[39;49;00m", + "function": Fore.LIGHTGREEN_EX, + "number": Fore.LIGHTBLUE_EX, + "str": Fore.YELLOW, + "print": Fore.LIGHTCYAN_EX, + } + RE_COLORS = {k: re.escape(v) for k, v in COLORS.items()} + + @classmethod + def format(cls, lines: List[str]) -> List[str]: + """Straightforward replacement of color names to their ASCII codes.""" + return [line.format(**cls.COLORS) for line in lines] + + @classmethod + def format_for_fnmatch(cls, lines: List[str]) -> List[str]: + """Replace color names for use with LineMatcher.fnmatch_lines""" + return [line.format(**cls.COLORS).replace("[", "[[]") for line in lines] + + @classmethod + def format_for_rematch(cls, lines: List[str]) -> List[str]: + """Replace color names for use with LineMatcher.re_match_lines""" + return [line.format(**cls.RE_COLORS) for line in lines] + + return ColorMapping + diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 438fa82087a..bef3ead78a6 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -3,15 +3,12 @@ """ import collections import os -import re import sys import textwrap from io import StringIO import pluggy import py -from colorama import Fore -from colorama import Style import pytest from _pytest.config import ExitCode @@ -26,20 +23,6 @@ DistInfo = collections.namedtuple("DistInfo", ["project_name", "version"]) -COLORS = { - "red": Fore.RED, - "green": Fore.GREEN, - "yellow": Fore.YELLOW, - "bold": Style.BRIGHT, - "reset": Style.RESET_ALL, - "kw": Fore.LIGHTBLUE_EX, - "hl-reset": "\x1b[39;49;00m", - "function": Fore.LIGHTGREEN_EX, - "number": Fore.LIGHTBLUE_EX, - "str": Fore.YELLOW, - "print": Fore.LIGHTCYAN_EX, -} -RE_COLORS = {k: re.escape(v) for k, v in COLORS.items()} TRANS_FNMATCH = str.maketrans({"[": "[[]", "]": "[]]"}) @@ -897,7 +880,7 @@ def test_pass_no_output(): ) -def test_color_yes(testdir): +def test_color_yes(testdir, color_mapping): p1 = testdir.makepyfile( """ def fail(): @@ -915,9 +898,8 @@ def test_this(): assert "\x1b[1m" in output return result.stdout.fnmatch_lines( - [ - line.format(**COLORS).replace("[", "[[]") - for line in [ + color_mapping.format_for_fnmatch( + [ "{bold}=*= test session starts =*={reset}", "collected 1 item", "", @@ -939,13 +921,12 @@ def test_this(): "{bold}{red}test_color_yes.py{reset}:2: AssertionError", "{red}=*= {red}{bold}1 failed{reset}{red} in *s{reset}{red} =*={reset}", ] - ] + ) ) result = testdir.runpytest("--color=yes", "--tb=short", str(p1)) result.stdout.fnmatch_lines( - [ - line.format(**COLORS).replace("[", "[[]") - for line in [ + color_mapping.format_for_fnmatch( + [ "{bold}=*= test session starts =*={reset}", "collected 1 item", "", @@ -960,7 +941,7 @@ def test_this(): "{bold}{red}E assert 0{reset}", "{red}=*= {red}{bold}1 failed{reset}{red} in *s{reset}{red} =*={reset}", ] - ] + ) ) @@ -1681,7 +1662,7 @@ def test_normal(self, many_tests_files, testdir): ] ) - def test_colored_progress(self, testdir, monkeypatch): + def test_colored_progress(self, testdir, monkeypatch, color_mapping): monkeypatch.setenv("PY_COLORS", "1") testdir.makepyfile( test_bar=""" @@ -1705,14 +1686,13 @@ def test_foobar(i): raise ValueError() ) result = testdir.runpytest() result.stdout.re_match_lines( - [ - line.format(**RE_COLORS) - for line in [ + color_mapping.format_for_rematch( + [ r"test_bar.py ({green}\.{reset}){{10}}{green} \s+ \[ 50%\]{reset}", r"test_foo.py ({green}\.{reset}){{5}}{yellow} \s+ \[ 75%\]{reset}", r"test_foobar.py ({red}F{reset}){{5}}{red} \s+ \[100%\]{reset}", ] - ] + ) ) def test_count(self, many_tests_files, testdir): @@ -1864,12 +1844,13 @@ def test_teardown_many(self, testdir, many_files): [r"test_bar.py (\.E){5}\s+\[ 25%\]", r"test_foo.py (\.E){15}\s+\[100%\]"] ) - def test_teardown_many_verbose(self, testdir: Testdir, many_files) -> None: + def test_teardown_many_verbose( + self, testdir: Testdir, many_files, color_mapping + ) -> None: result = testdir.runpytest("-v") result.stdout.fnmatch_lines( - [ - line.translate(TRANS_FNMATCH) - for line in [ + color_mapping.format_for_fnmatch( + [ "test_bar.py::test_bar[0] PASSED * [ 5%]", "test_bar.py::test_bar[0] ERROR * [ 5%]", "test_bar.py::test_bar[4] PASSED * [ 25%]", @@ -1877,7 +1858,7 @@ def test_teardown_many_verbose(self, testdir: Testdir, many_files) -> None: "test_foo.py::test_foo[14] ERROR * [100%]", "=* 20 passed, 20 errors in *", ] - ] + ) ) def test_xdist_normal(self, many_files, testdir, monkeypatch): @@ -2032,7 +2013,7 @@ def test_via_exec(testdir: Testdir) -> None: class TestCodeHighlight: - def test_code_highlight_simple(self, testdir: Testdir) -> None: + def test_code_highlight_simple(self, testdir: Testdir, color_mapping) -> None: testdir.makepyfile( """ def test_foo(): @@ -2041,17 +2022,16 @@ def test_foo(): ) result = testdir.runpytest("--color=yes") result.stdout.fnmatch_lines( - [ - line.format(**COLORS).replace("[", "[[]") - for line in [ + color_mapping.format_for_fnmatch( + [ " {kw}def{hl-reset} {function}test_foo{hl-reset}():", "> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}", "{bold}{red}E assert 1 == 10{reset}", ] - ] + ) ) - def test_code_highlight_continuation(self, testdir: Testdir) -> None: + def test_code_highlight_continuation(self, testdir: Testdir, color_mapping) -> None: testdir.makepyfile( """ def test_foo(): @@ -2061,13 +2041,12 @@ def test_foo(): ) result = testdir.runpytest("--color=yes") result.stdout.fnmatch_lines( - [ - line.format(**COLORS).replace("[", "[[]") - for line in [ + color_mapping.format_for_fnmatch( + [ " {kw}def{hl-reset} {function}test_foo{hl-reset}():", " {print}print{hl-reset}({str}'''{hl-reset}{str}{hl-reset}", "> {str} {hl-reset}{str}'''{hl-reset}); {kw}assert{hl-reset} {number}0{hl-reset}", "{bold}{red}E assert 0{reset}", ] - ] + ) ) From 74663a13c8f3a1e46151755e2016d36e2f584eb8 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 11 Feb 2020 20:37:28 -0300 Subject: [PATCH 4/5] Revert back to using explicit color codes --- testing/conftest.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/testing/conftest.py b/testing/conftest.py index 53d8dff8ce8..db8fb1bf84a 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -129,12 +129,12 @@ def runtest(self): testdir.makefile(".yaml", test1="") - @pytest.fixture def testdir(testdir: Testdir) -> Testdir: testdir.monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") return testdir + @pytest.fixture(scope="session") def color_mapping(): """Returns a utility class which can replace keys in strings in the form "{NAME}" @@ -142,21 +142,20 @@ def color_mapping(): Used by tests which check the actual colors output by pytest. """ - from colorama import Fore, Style class ColorMapping: COLORS = { - "red": Fore.RED, - "green": Fore.GREEN, - "yellow": Fore.YELLOW, - "bold": Style.BRIGHT, - "reset": Style.RESET_ALL, - "kw": Fore.LIGHTBLUE_EX, + "red": "\x1b[31m", + "green": "\x1b[32m", + "yellow": "\x1b[33m", + "bold": "\x1b[1m", + "reset": "\x1b[0m", + "kw": "\x1b[94m", "hl-reset": "\x1b[39;49;00m", - "function": Fore.LIGHTGREEN_EX, - "number": Fore.LIGHTBLUE_EX, - "str": Fore.YELLOW, - "print": Fore.LIGHTCYAN_EX, + "function": "\x1b[92m", + "number": "\x1b[94m", + "str": "\x1b[33m", + "print": "\x1b[96m", } RE_COLORS = {k: re.escape(v) for k, v in COLORS.items()} @@ -176,4 +175,3 @@ def format_for_rematch(cls, lines: List[str]) -> List[str]: return [line.format(**cls.RE_COLORS) for line in lines] return ColorMapping - From ea74b35f24c63f0bcc3546cd3b344515c29381ec Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 11 Feb 2020 20:50:22 -0300 Subject: [PATCH 5/5] In Python 3.5 skip rest of tests that require ordered markup in colored output --- testing/conftest.py | 20 ++++++++++++++++++++ testing/test_terminal.py | 10 ++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/testing/conftest.py b/testing/conftest.py index db8fb1bf84a..90cdcb869fd 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -3,6 +3,7 @@ from typing import List import pytest +from _pytest.pytester import RunResult from _pytest.pytester import Testdir if sys.gettrace(): @@ -174,4 +175,23 @@ def format_for_rematch(cls, lines: List[str]) -> List[str]: """Replace color names for use with LineMatcher.re_match_lines""" return [line.format(**cls.RE_COLORS) for line in lines] + @classmethod + def requires_ordered_markup(cls, result: RunResult): + """Should be called if a test expects markup to appear in the output + in the order they were passed, for example: + + tw.write(line, bold=True, red=True) + + In Python 3.5 there's no guarantee that the generated markup will appear + in the order called, so we do some limited color testing and skip the rest of + the test. + """ + if sys.version_info < (3, 6): + # terminal writer.write accepts keyword arguments, so + # py36+ is required so the markup appears in the expected order + output = result.stdout.str() + assert "test session starts" in output + assert "\x1b[1m" in output + pytest.skip("doing limited testing because lacking ordered markup") + return ColorMapping diff --git a/testing/test_terminal.py b/testing/test_terminal.py index bef3ead78a6..a0b1b41be6d 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -891,12 +891,7 @@ def test_this(): """ ) result = testdir.runpytest("--color=yes", str(p1)) - if sys.version_info < (3, 6): - # py36 required for ordered markup - output = result.stdout.str() - assert "test session starts" in output - assert "\x1b[1m" in output - return + color_mapping.requires_ordered_markup(result) result.stdout.fnmatch_lines( color_mapping.format_for_fnmatch( [ @@ -2021,6 +2016,7 @@ def test_foo(): """ ) result = testdir.runpytest("--color=yes") + color_mapping.requires_ordered_markup(result) result.stdout.fnmatch_lines( color_mapping.format_for_fnmatch( [ @@ -2040,6 +2036,8 @@ def test_foo(): """ ) result = testdir.runpytest("--color=yes") + color_mapping.requires_ordered_markup(result) + result.stdout.fnmatch_lines( color_mapping.format_for_fnmatch( [