diff --git a/src/pytest_reportlog/plugin.py b/src/pytest_reportlog/plugin.py index d44d56f..7f8d872 100644 --- a/src/pytest_reportlog/plugin.py +++ b/src/pytest_reportlog/plugin.py @@ -1,10 +1,13 @@ import json +import re from typing import Dict, Any, TextIO from _pytest.pathlib import Path import pytest +_ANSI_ESCAPE_SEQUENCE_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]") + def pytest_addoption(parser): group = parser.getgroup("terminal reporting", "report-log plugin options") @@ -69,10 +72,12 @@ def close(self): self._file = None def _write_json_data(self, data): + data = _strip_ansi_escape_sequences(data) try: json_data = json.dumps(data) except TypeError: data = cleanup_unserializable(data) + data = _strip_ansi_escape_sequences(data) json_data = json.dumps(data) self._file.write(json_data + "\n") self._file.flush() @@ -148,3 +153,21 @@ def cleanup_unserializable(d: Dict[str, Any]) -> Dict[str, Any]: v = str(v) result[k] = v return result + + +def _strip_ansi_escape_sequences(value: Any) -> Any: + """ + Return a new value with ANSI escape sequences removed from any strings. + + Report log output is intended to be machine-readable, and ANSI codes can + appear in pytest's formatted output (e.g. colored diffs). + """ + if isinstance(value, str): + return _ANSI_ESCAPE_SEQUENCE_RE.sub("", value) + if isinstance(value, dict): + return {k: _strip_ansi_escape_sequences(v) for k, v in value.items()} + if isinstance(value, list): + return [_strip_ansi_escape_sequences(v) for v in value] + if isinstance(value, tuple): + return tuple(_strip_ansi_escape_sequences(v) for v in value) + return value diff --git a/tests/test_reportlog.py b/tests/test_reportlog.py index 032df8b..0c3704c 100644 --- a/tests/test_reportlog.py +++ b/tests/test_reportlog.py @@ -184,6 +184,7 @@ def __str__(self): def test_subtest(pytester, tmp_path): """Regression test for #90.""" + pytest.importorskip("pytest_subtests") pytester.makepyfile(""" def test_foo(subtests): with subtests.test(): @@ -196,3 +197,36 @@ def test_foo(subtests): for line in lines: data = json.loads(line) assert "$report_type" in data + + +def test_strips_ansi_escape_sequences(testdir, tmp_path): + testdir.makeconftest(r""" +def pytest_report_to_serializable(config, report): + data = { + "$report_type": "TestReport", + "nodeid": report.nodeid, + "outcome": report.outcome, + } + + if getattr(report, "when", None) == "call": + data["longreprtext"] = "\x1b[31mRED\x1b[0m" + data["sections"] = [("Captured stdout call", "\x1b[32mGREEN\x1b[0m")] + + return data +""") + testdir.makepyfile(""" + def test_fail(): + assert 0 + """) + + log_file = tmp_path / "log.json" + result = testdir.runpytest("--report-log", str(log_file)) + assert result.ret == pytest.ExitCode.TESTS_FAILED + + json_objs = [json.loads(x) for x in log_file.read_text().splitlines()] + report = next(obj for obj in json_objs if obj.get("longreprtext")) + + assert report["longreprtext"] == "RED" + assert "\x1b" not in report["longreprtext"] + assert report["sections"][0][1] == "GREEN" + assert "\x1b" not in report["sections"][0][1]