Skip to content

Commit efd96a0

Browse files
committed
feat(pytest): fallback to summary info
1 parent d8f09d7 commit efd96a0

File tree

5 files changed

+216
-19
lines changed

5 files changed

+216
-19
lines changed

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
mypy
21
pytest
32
pytest-cov
43
pytest-asyncio

rplugin/python3/ultest/handler/parsers/output/python/pytest.py

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
from dataclasses import dataclass
2+
from logging import getLogger
23
from typing import List, Optional
34

45
from .. import parsec as p
56
from ..base import ParsedOutput, ParseResult
67
from ..parsec import generate
78
from ..util import eol, join_chars, until_eol
89

10+
logger = getLogger(__name__)
11+
912

1013
@dataclass
1114
class PytestCodeTrace:
@@ -18,15 +21,41 @@ class PytestCodeTrace:
1821
@generate
1922
def pytest_output():
2023
yield pytest_test_results_summary
21-
failed = yield p.many(failed_test_section)
22-
yield pytest_summary_info
23-
return ParsedOutput(results=failed)
24+
parsed_outputs = yield p.many1(failed_test_section)
25+
parsed_summary = yield pytest_summary_info
26+
parsed_results = {
27+
(r.file, r.name, *r.namespaces): r
28+
for r in [*parsed_summary, *parsed_outputs]
29+
if r
30+
}
31+
return ParsedOutput(results=list(parsed_results.values()))
2432

2533

2634
@generate
2735
def failed_test_section():
2836
namespaces, test_name = yield failed_test_section_title
2937
yield until_eol
38+
raw_output_lines = yield p.many1(
39+
p.exclude(until_eol, failed_test_section_title ^ pytest_summary_info_title)
40+
)
41+
output_text = "\n".join(raw_output_lines) + "\n"
42+
try:
43+
44+
file, err_msg, err_line = failed_test_section_output.parse(output_text)
45+
return ParseResult(
46+
name=test_name,
47+
namespaces=namespaces,
48+
file=file,
49+
message=err_msg,
50+
line=err_line,
51+
)
52+
except Exception as e:
53+
logger.debug(f"Failed to parse output: {e}\n----\n{output_text}\n----")
54+
return None
55+
56+
57+
@generate
58+
def failed_test_section_output():
3059
traces: List[PytestCodeTrace]
3160
traces = yield failed_test_code_sections
3261
sections = yield failed_test_captured_output_sections
@@ -40,26 +69,24 @@ def failed_test_section():
4069
for trace in traces:
4170
if trace.message:
4271
error_message = trace.message
43-
return ParseResult(
44-
name=test_name,
45-
namespaces=namespaces,
46-
file=test_file,
47-
message=error_message,
48-
line=test_line_no,
72+
return (
73+
test_file,
74+
error_message,
75+
test_line_no,
4976
)
5077

5178

5279
@generate
5380
def failed_test_section_title():
54-
yield p.many1(p.string("_")) >> p.space()
81+
yield p.string("_") >> p.many1(p.string("_")) >> p.space()
5582
name_elements = (
5683
yield p.many1(p.none_of(" "))
5784
.parsecmap(join_chars)
5885
.parsecmap(lambda elems: elems.split("."))
5986
)
6087
namespaces = name_elements[:-1]
6188
test_name = name_elements[-1]
62-
yield p.space() >> p.many1(p.string("_"))
89+
yield until_eol
6390
return (namespaces, test_name)
6491

6592

@@ -74,8 +101,7 @@ def failed_test_captured_output_sections():
74101

75102
@generate
76103
def failed_test_code_sections():
77-
78-
sections = yield p.sepBy(failed_test_code_section, failed_test_section_sep)
104+
sections = yield p.sepBy1(failed_test_code_section, failed_test_section_sep)
79105
return sections
80106

81107

@@ -125,11 +151,27 @@ def failed_test_error_message_line():
125151
return error_text
126152

127153

154+
@generate
155+
def pytest_summary_failed_test():
156+
yield p.string("FAILED ")
157+
names = yield p.sepBy1(
158+
p.many1(p.none_of(": ")).parsecmap(join_chars), p.string("::")
159+
)
160+
file, *namespaces = names
161+
yield until_eol
162+
return ParseResult(
163+
name=namespaces[-1],
164+
namespaces=namespaces[:-1],
165+
file=file,
166+
)
167+
168+
128169
@generate
129170
def pytest_summary_info():
130171
yield pytest_summary_info_title
131-
summary = yield p.many(until_eol)
132-
return summary
172+
parsed_summary = yield p.many1(pytest_summary_failed_test)
173+
yield p.many(until_eol)
174+
return parsed_summary
133175

134176

135177
@generate

tests/mocks/test_outputs/pytest_hypothesis_2

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
================================================= test session starts ==================================================
2+
platform linux -- Python 3.8.6, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
3+
rootdir: /home/ronan/Dev/repos/hypothesis, configfile: pytest.ini
4+
plugins: xdist-2.4.0, forked-1.3.0, hypothesis-6.24.0
5+
collecting ... collected 1 item 
6+
7+
tests/numpy/test_from_dtype.py F [100%]
8+
9+
======================================================= FAILURES =======================================================
10+
______________________________________________ test_can_cast_for_scalars _______________________________________________
11+
Traceback (most recent call last):
12+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/tests/numpy/test_from_dtype.py", line 80, in test_can_cast_for_scalars
13+
def test_can_cast_for_scalars(data):
14+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/src/hypothesis/core.py", line 1199, in wrapped_test
15+
raise the_error_hypothesis_found
16+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/src/hypothesis/core.py", line 1168, in wrapped_test
17+
state.run_engine()
18+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/src/hypothesis/core.py", line 780, in run_engine
19+
runner.run()
20+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/src/hypothesis/internal/conjecture/engine.py", line 475, in run
21+
self._run()
22+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/src/hypothesis/internal/conjecture/engine.py", line 877, in _run
23+
self.generate_new_examples()
24+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/src/hypothesis/internal/conjecture/engine.py", line 609, in generate_new_examples
25+
zero_data = self.cached_test_function(bytes(BUFFER_SIZE))
26+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/src/hypothesis/internal/conjecture/engine.py", line 1056, in cached_test_function
27+
self.test_function(data)
28+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/src/hypothesis/internal/conjecture/engine.py", line 213, in test_function
29+
self.__stoppable_test_function(data)
30+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/src/hypothesis/internal/conjecture/engine.py", line 189, in __stoppable_test_function
31+
self._test_function(data)
32+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/src/hypothesis/core.py", line 727, in _execute_once_for_engine
33+
escalate_hypothesis_internal_error()
34+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/src/hypothesis/core.py", line 701, in _execute_once_for_engine
35+
result = self.execute_once(data)
36+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/src/hypothesis/core.py", line 639, in execute_once
37+
result = self.test_runner(data, run)
38+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/src/hypothesis/executors.py", line 52, in default_new_style_executor
39+
return function(data)
40+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/src/hypothesis/core.py", line 635, in run
41+
return test(*args, **kwargs)
42+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/tests/numpy/test_from_dtype.py", line 80, in test_can_cast_for_scalars
43+
def test_can_cast_for_scalars(data):
44+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/src/hypothesis/core.py", line 577, in test
45+
result = self.test(*args, **kwargs)
46+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/tests/numpy/test_from_dtype.py", line 87, in test_can_cast_for_scalars
47+
result = data.draw(
48+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/src/hypothesis/strategies/_internal/core.py", line 1692, in draw
49+
result = self.conjecture_data.draw(strategy)
50+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/src/hypothesis/internal/conjecture/data.py", line 866, in draw
51+
strategy.validate()
52+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py", line 401, in validate
53+
self.do_validate()
54+
File "/home/ronan/Dev/repos/hypothesis/hypothesis-python/src/hypothesis/strategies/_internal/lazy.py", line 134, in do_validate
55+
assert isinstance(w, SearchStrategy), f"{self!r} returned non-strategy {w!r}"
56+
AssertionError: arrays(dtype=dtype('int16'), shape=(), elements=from_dtype(dtype('bool'))) returned non-strategy []
57+
------------------------------------------------------ Hypothesis ------------------------------------------------------
58+
You can add @seed(11024453522097809882419571698055639517) to this test or run pytest with --hypothesis-seed=11024453522097809882419571698055639517 to reproduce this failure.
59+
================================================= slowest 20 durations =================================================
60+
0.01s setup hypothesis-python/tests/numpy/test_from_dtype.py::test_can_cast_for_scalars
61+
62+
(2 durations < 0.005s hidden. Use -vv to show these durations.)
63+
=============================================== short test summary info ================================================
64+
FAILED tests/numpy/test_from_dtype.py::test_can_cast_for_scalars - AssertionError: arrays(dtype=dtype('int16'), shape...
65+
================================================== 1 failed in 0.06s ===================================================

tests/unit/handler/parsers/output/python/test_pytest.py

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
failed_test_section,
77
failed_test_section_error_message,
88
failed_test_section_title,
9+
pytest_summary_info,
910
)
1011
from tests.mocks import get_output
1112

@@ -105,13 +106,41 @@ def test_parse_hypothesis_file(self):
105106
],
106107
)
107108

109+
def test_parse_summary_if_output_not_parsable(self):
110+
raw = """
111+
================================================= test session starts ==================================================
112+
...
113+
114+
======================================================= FAILURES =======================================================
115+
______________________________________________ test_can_cast_for_scalars _______________________________________________
116+
This is not parsable
117+
------------------------------------------------------ Hypothesis ------------------------------------------------------
118+
You can add @seed(11024453522097809882419571698055639517) to this test or run pytest with --hypothesis-seed=11024453522097809882419571698055639517 to reproduce this failure.
119+
================================================= slowest 20 durations =================================================
120+
0.01s setup hypothesis-python/tests/numpy/test_from_dtype.py::test_can_cast_for_scalars
121+
122+
(2 durations < 0.005s hidden. Use -vv to show these durations.)
123+
=============================================== short test summary info ================================================
124+
FAILED tests/numpy/test_from_dtype.py::test_can_cast_for_scalars - AssertionError: arrays(dtype=dtype('int16'), shape...
125+
================================================== 1 failed in 0.06s ===================================================
126+
"""
127+
expected = [
128+
ParseResult(
129+
name="test_can_cast_for_scalars",
130+
namespaces=[],
131+
file="tests/numpy/test_from_dtype.py",
132+
)
133+
]
134+
parser = OutputParser([])
135+
result = parser.parse_failed("python#pytest", raw)
136+
self.assertEqual(result, expected)
137+
108138
def test_parse_failed_test_section_title(self):
109-
raw = "_____ MyClass.test_a ______"
139+
raw = "_____ MyClass.test_a ______\n"
110140
result = failed_test_section_title.parse(raw)
111141
self.assertEqual(result, (["MyClass"], "test_a"))
112142

113143
def test_parse_failed_test_section_error(self):
114-
self.maxDiff = None
115144
raw = """E AssertionError: {'a': 1, 'b': 2, 'c': 3} != {'a': 1, 'b': 5, 'c': 3, 'd': 4}
116145
E - {'a': 1, 'b': 2, 'c': 3}
117146
E ? ^
@@ -130,6 +159,69 @@ def test_parse_failed_test_section_error(self):
130159
]
131160
self.assertEqual(expected, result)
132161

162+
def test_parse_failed_test_short_summary(self):
163+
raw = """======================================================================================================================== short test summary info =========================================================================================================================
164+
FAILED test_a.py::test_b - Exception: OH NO
165+
FAILED test_a.py::TestClass::test_b - AssertionError: {'a': 1, 'b': 2, 'c': 3} != {'a': 1, 'b': 5, 'c': 3, 'd': 4}
166+
FAILED test_b.py::AnotherClass::test_a - AssertionError: assert 2 == 3
167+
FAILED test_b.py::test_d - assert 2 == 3
168+
FAILED subtests/test_c.py::TestStuff::test_thing_2 - AssertionError: assert False
169+
FAILED subtests/test_c.py::test_a - assert False
170+
====================================================================================================================== 6 failed, 5 passed in 0.39s =======================================================================================================================
171+
"""
172+
result = pytest_summary_info.parse(raw)
173+
expected = [
174+
ParseResult(
175+
name="test_b",
176+
namespaces=[],
177+
file="test_a.py",
178+
message=None,
179+
output=None,
180+
line=None,
181+
),
182+
ParseResult(
183+
name="test_b",
184+
namespaces=["TestClass"],
185+
file="test_a.py",
186+
message=None,
187+
output=None,
188+
line=None,
189+
),
190+
ParseResult(
191+
name="test_a",
192+
namespaces=["AnotherClass"],
193+
file="test_b.py",
194+
message=None,
195+
output=None,
196+
line=None,
197+
),
198+
ParseResult(
199+
name="test_d",
200+
namespaces=[],
201+
file="test_b.py",
202+
message=None,
203+
output=None,
204+
line=None,
205+
),
206+
ParseResult(
207+
name="test_thing_2",
208+
namespaces=["TestStuff"],
209+
file="subtests/test_c.py",
210+
message=None,
211+
output=None,
212+
line=None,
213+
),
214+
ParseResult(
215+
name="test_a",
216+
namespaces=[],
217+
file="subtests/test_c.py",
218+
message=None,
219+
output=None,
220+
line=None,
221+
),
222+
]
223+
self.assertEqual(expected, result)
224+
133225
def test_parse_failed_test_section(self):
134226
raw = """_____________________________________________________________________ MyClass.test_b _____________________________________________________________________
135227

tests/unit/models/test_tree.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import logging
21
import random
32
from typing import List, Union
43

0 commit comments

Comments
 (0)