Skip to content

Commit d8f09d7

Browse files
committed
feat(pytest): parse output sections
Allows ultest to determine the suitable stack trace to use such as for hypothesis, the second trace should be used.
1 parent e5ba8a0 commit d8f09d7

File tree

4 files changed

+214
-84
lines changed

4 files changed

+214
-84
lines changed
Lines changed: 90 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
1+
from dataclasses import dataclass
2+
from typing import List, Optional
3+
14
from .. import parsec as p
25
from ..base import ParsedOutput, ParseResult
36
from ..parsec import generate
4-
from ..util import join_chars, until_eol
7+
from ..util import eol, join_chars, until_eol
8+
9+
10+
@dataclass
11+
class PytestCodeTrace:
12+
code: List[str]
13+
file: str
14+
line: int
15+
message: Optional[List[str]] = None
516

617

718
@generate
@@ -16,18 +27,19 @@ def pytest_output():
1627
def failed_test_section():
1728
namespaces, test_name = yield failed_test_section_title
1829
yield until_eol
19-
yield failed_test_section_code
20-
trace_origin = yield p.optional(until_eol >> failed_test_stacktrace)
21-
error_message = yield failed_test_section_error_message
22-
yield until_eol
23-
error_file, error_line_no = yield failed_test_error_location
24-
yield p.optional(failed_test_captured_stdout, [])
25-
if trace_origin:
26-
test_file = trace_origin[0]
27-
test_line_no = trace_origin[1]
28-
else:
29-
test_file = error_file
30-
test_line_no = error_line_no
30+
traces: List[PytestCodeTrace]
31+
traces = yield failed_test_code_sections
32+
sections = yield failed_test_captured_output_sections
33+
trace = traces[0]
34+
# Hypothesis traces provide the test definition as the first layer of trace
35+
if "Hypothesis" in sections and len(traces) > 1:
36+
trace = traces[1]
37+
test_file = trace.file
38+
test_line_no = trace.line
39+
error_message = None
40+
for trace in traces:
41+
if trace.message:
42+
error_message = trace.message
3143
return ParseResult(
3244
name=test_name,
3345
namespaces=namespaces,
@@ -37,14 +49,6 @@ def failed_test_section():
3749
)
3850

3951

40-
@generate
41-
def failed_test_stacktrace():
42-
file_name, line_no = yield failed_test_error_location
43-
yield p.string("_ _") >> until_eol
44-
yield p.many1(p.exclude(failed_test_code_line, failed_test_section_error_message))
45-
return file_name, line_no
46-
47-
4852
@generate
4953
def failed_test_section_title():
5054
yield p.many1(p.string("_")) >> p.space()
@@ -60,22 +64,45 @@ def failed_test_section_title():
6064

6165

6266
@generate
63-
def failed_test_error_message_line():
64-
yield p.string("E")
65-
yield p.many(p.string(" "))
66-
error_text = yield until_eol
67-
return error_text
67+
def failed_test_captured_output_sections():
68+
sections = yield p.many(failed_test_captured_output)
69+
return dict(sections)
70+
71+
72+
failed_test_section_sep = p.many1(p.one_of("_ ")) >> eol
6873

6974

7075
@generate
71-
def failed_test_section_code():
72-
code = yield p.many1(
76+
def failed_test_code_sections():
77+
78+
sections = yield p.sepBy(failed_test_code_section, failed_test_section_sep)
79+
return sections
80+
81+
82+
@generate
83+
def failed_test_code_section():
84+
code = yield p.many(
7385
p.exclude(
7486
failed_test_code_line,
75-
failed_test_section_error_message ^ failed_test_stacktrace,
87+
failed_test_section_error_message ^ failed_test_error_location,
88+
)
89+
)
90+
message = yield p.optional(failed_test_section_error_message)
91+
if code or message:
92+
yield until_eol
93+
trace = yield failed_test_error_location
94+
yield p.many(
95+
p.exclude(
96+
until_eol,
97+
failed_test_section_sep
98+
^ failed_test_captured_output_title
99+
^ failed_test_section_title
100+
^ pytest_summary_info_title,
76101
)
77102
)
78-
return code
103+
return PytestCodeTrace(
104+
code=code, file=trace and trace[0], line=trace and trace[1], message=message
105+
)
79106

80107

81108
@generate
@@ -90,19 +117,33 @@ def failed_test_section_error_message():
90117
return lines
91118

92119

120+
@generate
121+
def failed_test_error_message_line():
122+
yield p.string("E")
123+
yield p.many(p.string(" "))
124+
error_text = yield until_eol
125+
return error_text
126+
127+
93128
@generate
94129
def pytest_summary_info():
130+
yield pytest_summary_info_title
131+
summary = yield p.many(until_eol)
132+
return summary
133+
134+
135+
@generate
136+
def pytest_summary_info_title():
95137
yield p.many1(p.string("="))
96138
yield p.string(" short test summary info ")
97139
yield until_eol
98-
summary = yield p.many(until_eol)
99-
return summary
100140

101141

102142
@generate
103143
def pytest_test_results_summary():
104-
summary = yield p.many(p.exclude(until_eol, pytest_failed_tests_title))
105-
yield pytest_failed_tests_title
144+
failures_title = p.many1(p.string("=")) >> p.string(" FAILURES ") >> until_eol
145+
summary = yield p.many(p.exclude(until_eol, failures_title))
146+
yield failures_title
106147
return summary
107148

108149

@@ -117,18 +158,24 @@ def failed_test_error_location():
117158

118159

119160
@generate
120-
def failed_test_captured_stdout():
121-
yield p.many1(p.string("-"))
122-
yield p.string(" Captured stdout call ")
123-
yield until_eol
161+
def failed_test_captured_output():
162+
title = yield failed_test_captured_output_title.parsecmap(join_chars)
124163
stdout = yield p.many(
125-
p.exclude(until_eol, failed_test_section_title ^ pytest_summary_info)
164+
p.exclude(
165+
until_eol,
166+
failed_test_section_title
167+
^ pytest_summary_info_title
168+
^ failed_test_captured_output_title,
169+
)
126170
)
127-
return stdout
171+
return title, stdout
128172

129173

130174
@generate
131-
def pytest_failed_tests_title():
132-
yield p.many1(p.string("="))
133-
yield p.string(" FAILURES ")
175+
def failed_test_captured_output_title():
176+
yield p.many1(p.string("-"))
177+
yield p.string(" ")
178+
title = yield p.many1(p.exclude(p.any(), p.string(" ---")))
179+
yield p.string(" ")
134180
yield until_eol
181+
return title

tests/mocks/test_outputs/pytest_hypothesis

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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/vim-ultest, configfile: pyproject.toml
4+
plugins: asyncio-0.16.0, hypothesis-6.23.3, cov-3.0.0
5+
collecting ... collected 4 items 
6+
7+
tests/unit/models/test_tree.py F... [100%]
8+
9+
======================================================= FAILURES =======================================================
10+
__________________________________________ test_get_nearest_from_strict_match __________________________________________
11+
12+
@given(sorted_tests())
13+
> def test_get_nearest_from_strict_match(tests: List[Union[Test, Namespace]]):
14+
15+
tests/unit/models/test_tree.py:29:
16+
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
17+
18+
tests = [Test(id='', name='', file='', line=2, col=0, running=0, namespaces=[], type='test'), Test(id='', name='', file='', li...namespaces=[], type='test'), Test(id='', name='', file='', line=12, col=0, running=0, namespaces=[], type='test'), ...]
19+
20+
@given(sorted_tests())
21+
def test_get_nearest_from_strict_match(tests: List[Union[Test, Namespace]]):
22+
test_i = int(random.random() * len(tests))
23+
expected = tests[test_i]
24+
tree = Tree[Position].from_list([File(file="", name="", id=""), *tests])
25+
result = tree.sorted_search(expected.line, lambda test: test.line, strict=True)
26+
> assert expected != result.data
27+
E AssertionError: assert Test(id='', name='', file='', line=18, col=0, running=0, namespaces=[], type='test') != Test(id='', name='', file='', line=18, col=0, running=0, namespaces=[], type='test')
28+
E + where Test(id='', name='', file='', line=18, col=0, running=0, namespaces=[], type='test') = Tree(data=Test(id='', name='', file='', line=18, col=0, running=0, namespaces=[], type='test'), children=[]).data
29+
30+
tests/unit/models/test_tree.py:34: AssertionError
31+
------------------------------------------------------ Hypothesis ------------------------------------------------------
32+
Falsifying example: test_get_nearest_from_strict_match(
33+
tests=[Test(id='', name='', file='', line=2, col=0, running=0, namespaces=[], type='test'),
34+
Test(id='', name='', file='', line=4, col=0, running=0, namespaces=[], type='test'),
35+
Test(id='', name='', file='', line=6, col=0, running=0, namespaces=[], type='test'),
36+
Test(id='', name='', file='', line=8, col=0, running=0, namespaces=[], type='test'),
37+
Test(id='', name='', file='', line=10, col=0, running=0, namespaces=[], type='test'),
38+
Test(id='', name='', file='', line=12, col=0, running=0, namespaces=[], type='test'),
39+
Test(id='', name='', file='', line=14, col=0, running=0, namespaces=[], type='test'),
40+
Test(id='', name='', file='', line=16, col=0, running=0, namespaces=[], type='test'),
41+
Test(id='', name='', file='', line=18, col=0, running=0, namespaces=[], type='test'),
42+
Test(id='', name='', file='', line=20, col=0, running=0, namespaces=[], type='test')],
43+
)
44+
=============================================== short test summary info ================================================
45+
FAILED tests/unit/models/test_tree.py::test_get_nearest_from_strict_match - AssertionError: assert Test(id='', name='...
46+
============================================= 1 failed, 3 passed in 20.07s =============================================

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

Lines changed: 77 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
from dataclasses import asdict
21
from unittest import TestCase
32

43
from rplugin.python3.ultest.handler.parsers.output import OutputParser
54
from rplugin.python3.ultest.handler.parsers.output.python.pytest import (
65
ParseResult,
76
failed_test_section,
8-
failed_test_section_code,
97
failed_test_section_error_message,
108
failed_test_section_title,
119
)
@@ -86,6 +84,27 @@ def test_parse_file(self):
8684
],
8785
)
8886

87+
def test_parse_hypothesis_file(self):
88+
output = get_output("pytest_hypothesis")
89+
parser = OutputParser([])
90+
result = parser.parse_failed("python#pytest", output)
91+
self.assertEqual(
92+
result,
93+
[
94+
ParseResult(
95+
name="test_get_nearest_from_strict_match",
96+
namespaces=[],
97+
file="tests/unit/models/test_tree.py",
98+
message=[
99+
"AssertionError: assert Test(id='', name='', file='', line=18, col=0, running=0, namespaces=[], type='test') != Test(id='', name='', file='', line=18, col=0, running=0, namespaces=[], type='test')",
100+
"+ where Test(id='', name='', file='', line=18, col=0, running=0, namespaces=[], type='test') = Tree(data=Test(id='', name='', file='', line=18, col=0, running=0, namespaces=[], type='test'), children=[]).data",
101+
],
102+
output=None,
103+
line=34,
104+
)
105+
],
106+
)
107+
89108
def test_parse_failed_test_section_title(self):
90109
raw = "_____ MyClass.test_a ______"
91110
result = failed_test_section_title.parse(raw)
@@ -111,36 +130,6 @@ def test_parse_failed_test_section_error(self):
111130
]
112131
self.assertEqual(expected, result)
113132

114-
def test_parse_failed_test_section_code(self):
115-
self.maxDiff = None
116-
raw = """self = <test_a.TestClass testMethod=test_b>
117-
118-
def test_b(self):
119-
> self.assertEqual({
120-
"a": 1,
121-
"b": 2,
122-
"c": 3},
123-
{"a": 1,
124-
"b": 5,
125-
"c": 3,
126-
"d": 4})
127-
E This should not be parsed"""
128-
result, _ = failed_test_section_code.parse_partial(raw)
129-
expected = [
130-
"self = <test_a.TestClass testMethod=test_b>",
131-
"",
132-
" def test_b(self):",
133-
"> self.assertEqual({",
134-
' "a": 1,',
135-
' "b": 2,',
136-
' "c": 3},',
137-
' {"a": 1,',
138-
' "b": 5,',
139-
' "c": 3,',
140-
' "d": 4})',
141-
]
142-
self.assertEqual(expected, result)
143-
144133
def test_parse_failed_test_section(self):
145134
raw = """_____________________________________________________________________ MyClass.test_b _____________________________________________________________________
146135
@@ -197,14 +186,61 @@ def a_function():
197186
"""
198187
result = failed_test_section.parse(raw)
199188
self.assertEqual(
200-
asdict(result),
201-
asdict(
202-
ParseResult(
203-
file="test_a.py",
204-
name="test_c",
205-
namespaces=["TestClass"],
206-
message=["Exception: OH NO"],
207-
line=39,
208-
)
189+
result,
190+
ParseResult(
191+
file="test_a.py",
192+
name="test_c",
193+
namespaces=["TestClass"],
194+
message=["Exception: OH NO"],
195+
line=39,
196+
),
197+
)
198+
199+
def test_parse_failed_test_with_code_below_trace_location(self):
200+
raw = """__________________________________________ test_get_nearest_from_strict_match __________________________________________
201+
202+
@given(sorted_tests())
203+
> def test_get_nearest_from_strict_match(tests: List[Union[Test, Namespace]]):
204+
205+
tests/unit/models/test_tree.py:30:
206+
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
207+
tests/unit/models/test_tree.py:35: in test_get_nearest_from_strict_match
208+
logging.warn("AAAAAAH")
209+
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
210+
211+
msg = 'AAAAAAH', args = (), kwargs = {}
212+
213+
def warn(msg, *args, **kwargs):
214+
> warnings.warn("The 'warn' function is deprecated, "
215+
"use 'warning' instead", DeprecationWarning, 2)
216+
E DeprecationWarning: The 'warn' function is deprecated, use 'warning' instead
217+
218+
../../../.pyenv/versions/3.8.6/lib/python3.8/logging/__init__.py:2058: DeprecationWarning
219+
------------------------------------------------------ Hypothesis ------------------------------------------------------
220+
Falsifying example: test_get_nearest_from_strict_match(
221+
tests=[Test(id='', name='', file='', line=2, col=0, running=0, namespaces=[], type='test'),
222+
Test(id='', name='0', file='', line=4, col=0, running=0, namespaces=[], type='test'),
223+
Test(id='', name='', file='', line=6, col=0, running=0, namespaces=[], type='test'),
224+
Test(id='', name='', file='', line=8, col=0, running=0, namespaces=[], type='test'),
225+
Test(id='', name='', file='', line=10, col=0, running=0, namespaces=[], type='test'),
226+
Test(id='', name='', file='', line=12, col=0, running=0, namespaces=[], type='test'),
227+
Test(id='', name='', file='', line=14, col=0, running=0, namespaces=[], type='test'),
228+
Test(id='', name='', file='', line=16, col=0, running=0, namespaces=[], type='test'),
229+
Test(id='', name='', file='', line=18, col=0, running=0, namespaces=[], type='test'),
230+
Test(id='', name='', file='', line=514, col=0, running=0, namespaces=[], type='test')],
231+
)"""
232+
233+
result = failed_test_section.parse(raw)
234+
self.assertEqual(
235+
result,
236+
ParseResult(
237+
name="test_get_nearest_from_strict_match",
238+
namespaces=[],
239+
file="tests/unit/models/test_tree.py",
240+
message=[
241+
"DeprecationWarning: The 'warn' function is deprecated, use 'warning' instead"
242+
],
243+
output=None,
244+
line=35,
209245
),
210246
)

tests/unit/models/test_tree.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
import random
23
from typing import List, Union
34

0 commit comments

Comments
 (0)