From 013018d846dad538823060dfdb6e13ed9088b68f Mon Sep 17 00:00:00 2001 From: Naoto Ono Date: Fri, 11 Jul 2025 11:39:25 +0900 Subject: [PATCH 1/2] Enhance longrepr handling in PytestJSONReportParser and add unit tests --- launchable/test_runners/pytest.py | 56 +++++++++++------- tests/test_runners/test_pytest_longrepr.py | 66 ++++++++++++++++++++++ 2 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 tests/test_runners/test_pytest_longrepr.py diff --git a/launchable/test_runners/pytest.py b/launchable/test_runners/pytest.py index 2919767f0..3ecbbd94d 100644 --- a/launchable/test_runners/pytest.py +++ b/launchable/test_runners/pytest.py @@ -264,27 +264,41 @@ def parse_func( stderr = "" longrepr = data.get("longrepr", None) if longrepr: - message = None - reprcrash = longrepr.get("reprcrash", None) - if reprcrash: - message = reprcrash.get("message", None) - - text = None - reprtraceback = longrepr.get("reprtraceback", None) - if reprtraceback: - reprentries = reprtraceback.get("reprentries", None) - if reprentries: - for r in reprentries: - d = r.get("data", None) - if d: - text = "\n".join(d.get("lines", [])) - - if message and text: - stderr = message + "\n" + text - elif message: - stderr = stderr + message - elif text: - stderr = stderr + text + # https://github.com/pytest-dev/pytest/blob/1d7d63555e431d4562bcacbdc97038b0613d20ba/src/_pytest/reports.py#L60 + if isinstance(longrepr, dict): + # https://github.com/pytest-dev/pytest/blob/1d7d63555e431d4562bcacbdc97038b0613d20ba/src/_pytest/reports.py#L361 + message = None + reprcrash = longrepr.get("reprcrash", None) + if reprcrash: + message = reprcrash.get("message", None) + + text = None + reprtraceback = longrepr.get("reprtraceback", None) + if reprtraceback: + reprentries = reprtraceback.get("reprentries", None) + if reprentries: + for r in reprentries: + d = r.get("data", None) + if d: + text = "\n".join(d.get("lines", [])) + + if message and text: + stderr = message + "\n" + text + elif message: + stderr = stderr + message + elif text: + stderr = stderr + text + elif isinstance(longrepr, list): + # [path, lineno, messge] + # https://github.com/pytest-dev/pytest/blob/1d7d63555e431d4562bcacbdc97038b0613d20ba/src/_pytest/reports.py#L371 + if len(longrepr) == 3: + stderr = longrepr[2] + + elif isinstance(longrepr, str): + # When longrepr is a string, it is the same as the stderr. + # https://github.com/pytest-dev/pytest/blob/1d7d63555e431d4562bcacbdc97038b0613d20ba/src/_pytest/reports.py#L377 + # https://github.com/pytest-dev/pytest/blob/1d7d63555e431d4562bcacbdc97038b0613d20ba/src/_pytest/nodes.py#L470 + stderr = longrepr test_path = _parse_pytest_nodeid(nodeid) for path in test_path: diff --git a/tests/test_runners/test_pytest_longrepr.py b/tests/test_runners/test_pytest_longrepr.py new file mode 100644 index 000000000..236a4c594 --- /dev/null +++ b/tests/test_runners/test_pytest_longrepr.py @@ -0,0 +1,66 @@ +import json +import tempfile +import unittest + +from launchable.test_runners.pytest import PytestJSONReportParser + + +class DummyClient: + pass + + +class TestPytestJSONReportParserLongrepr(unittest.TestCase): + def setUp(self): + self.parser = PytestJSONReportParser(DummyClient()) + + def parse_line(self, data): + with tempfile.NamedTemporaryFile(mode="w+", delete=False) as f: + f.write(json.dumps(data) + "\n") + f.flush() + results = list(self.parser.parse_func(f.name)) + return results + + def test_longrepr_dict_message_and_text(self): + data = self.make_event_data( + { + "reprcrash": {"message": "AssertionError: fail"}, + "reprtraceback": { + "reprentries": [{"data": {"lines": ["line1", "line2"]}}] + }, + } + ) + events = self.parse_line(data) + self.assert_stderr(events, "AssertionError: fail\nline1\nline2") + + def test_longrepr_dict_only_message(self): + data = self.make_event_data({"reprcrash": {"message": "Only message"}}) + events = self.parse_line(data) + self.assert_stderr(events, "Only message") + + def test_longrepr_dict_only_text(self): + data = self.make_event_data( + {"reprtraceback": {"reprentries": [{"data": {"lines": ["text only"]}}]}} + ) + events = self.parse_line(data) + self.assert_stderr(events, "text only") + + def test_longrepr_list(self): + data = self.make_event_data(["file.py", 10, "list message"]) + events = self.parse_line(data) + self.assert_stderr(events, "list message") + + def test_longrepr_str(self): + data = self.make_event_data("string message") + events = self.parse_line(data) + self.assert_stderr(events, "string message") + + def assert_stderr(self, events, expected_stderr): + self.assertEqual(events[0]["stderr"], expected_stderr) + + def make_event_data(self, longrepr): + return { + "nodeid": "tests/test_sample.py::test_fail", + "when": "call", + "outcome": "failed", + "longrepr": longrepr, + } From 18bb647603c14122df2f9cdf3d06916b8aec71a9 Mon Sep 17 00:00:00 2001 From: Konboi Date: Fri, 11 Jul 2025 12:03:19 +0900 Subject: [PATCH 2/2] merge and refactor codes --- tests/test_runners/test_pytest.py | 65 ++++++++++++++++++++- tests/test_runners/test_pytest_longrepr.py | 66 ---------------------- 2 files changed, 63 insertions(+), 68 deletions(-) delete mode 100644 tests/test_runners/test_pytest_longrepr.py diff --git a/tests/test_runners/test_pytest.py b/tests/test_runners/test_pytest.py index 092fd3f31..5cfe90790 100644 --- a/tests/test_runners/test_pytest.py +++ b/tests/test_runners/test_pytest.py @@ -1,11 +1,12 @@ import gzip import json import os -from unittest import mock +import tempfile +from unittest import TestCase, mock import responses # type: ignore -from launchable.test_runners.pytest import _parse_pytest_nodeid +from launchable.test_runners.pytest import PytestJSONReportParser, _parse_pytest_nodeid from tests.cli_test_case import CliTestCase @@ -77,3 +78,63 @@ def test_parse_pytest_nodeid(self): {"type": "class", "name": "tests.fooo.func4_test"}, {"type": "testcase", "name": "test_func6"}, ]) + + +class PytestJSONReportParserLongreprTest(TestCase): + class DummyClient: + pass + + def setUp(self): + self.parser = PytestJSONReportParser(self.DummyClient()) + + def _parse_line(self, data): + with tempfile.NamedTemporaryFile(mode="w+", delete=False) as f: + f.write(json.dumps(data) + "\n") + f.flush() + results = list(self.parser.parse_func(f.name)) + return results + + def _make_event_data(self, longrepr): + return { + "nodeid": "tests/test_sample.py::test_fail", + "when": "call", + "outcome": "failed", + "longrepr": longrepr, + } + + def _assert_stderr(self, events, expected_stderr): + self.assertEqual(events[0]["stderr"], expected_stderr) + + def test_longrepr_dict_message_and_text(self): + data = self._make_event_data( + { + "reprcrash": {"message": "AssertionError: fail"}, + "reprtraceback": { + "reprentries": [{"data": {"lines": ["line1", "line2"]}}] + }, + } + ) + events = self._parse_line(data) + self._assert_stderr(events, "AssertionError: fail\nline1\nline2") + + def test_longrepr_dict_only_message(self): + data = self._make_event_data({"reprcrash": {"message": "Only message"}}) + events = self._parse_line(data) + self._assert_stderr(events, "Only message") + + def test_longrepr_dict_only_text(self): + data = self._make_event_data( + {"reprtraceback": {"reprentries": [{"data": {"lines": ["text only"]}}]}} + ) + events = self._parse_line(data) + self._assert_stderr(events, "text only") + + def test_longrepr_list(self): + data = self._make_event_data(["file.py", 10, "list message"]) + events = self._parse_line(data) + self._assert_stderr(events, "list message") + + def test_longrepr_str(self): + data = self._make_event_data("string message") + events = self._parse_line(data) + self._assert_stderr(events, "string message") diff --git a/tests/test_runners/test_pytest_longrepr.py b/tests/test_runners/test_pytest_longrepr.py deleted file mode 100644 index 236a4c594..000000000 --- a/tests/test_runners/test_pytest_longrepr.py +++ /dev/null @@ -1,66 +0,0 @@ -import json -import tempfile -import unittest - -from launchable.test_runners.pytest import PytestJSONReportParser - - -class DummyClient: - pass - - -class TestPytestJSONReportParserLongrepr(unittest.TestCase): - def setUp(self): - self.parser = PytestJSONReportParser(DummyClient()) - - def parse_line(self, data): - with tempfile.NamedTemporaryFile(mode="w+", delete=False) as f: - f.write(json.dumps(data) + "\n") - f.flush() - results = list(self.parser.parse_func(f.name)) - return results - - def test_longrepr_dict_message_and_text(self): - data = self.make_event_data( - { - "reprcrash": {"message": "AssertionError: fail"}, - "reprtraceback": { - "reprentries": [{"data": {"lines": ["line1", "line2"]}}] - }, - } - ) - events = self.parse_line(data) - self.assert_stderr(events, "AssertionError: fail\nline1\nline2") - - def test_longrepr_dict_only_message(self): - data = self.make_event_data({"reprcrash": {"message": "Only message"}}) - events = self.parse_line(data) - self.assert_stderr(events, "Only message") - - def test_longrepr_dict_only_text(self): - data = self.make_event_data( - {"reprtraceback": {"reprentries": [{"data": {"lines": ["text only"]}}]}} - ) - events = self.parse_line(data) - self.assert_stderr(events, "text only") - - def test_longrepr_list(self): - data = self.make_event_data(["file.py", 10, "list message"]) - events = self.parse_line(data) - self.assert_stderr(events, "list message") - - def test_longrepr_str(self): - data = self.make_event_data("string message") - events = self.parse_line(data) - self.assert_stderr(events, "string message") - - def assert_stderr(self, events, expected_stderr): - self.assertEqual(events[0]["stderr"], expected_stderr) - - def make_event_data(self, longrepr): - return { - "nodeid": "tests/test_sample.py::test_fail", - "when": "call", - "outcome": "failed", - "longrepr": longrepr, - }