Skip to content

Commit 91c6347

Browse files
committed
Internal refactorings in order to support the new pytest-subtests plugin
Related to #1367
1 parent 951e07d commit 91c6347

File tree

5 files changed

+127
-54
lines changed

5 files changed

+127
-54
lines changed

changelog/4920.feature.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Internal refactorings have been made in order to make the implementation of the
2+
`pytest-subtests <https://github.com/pytest-dev/pytest-subtests>`__ plugin
3+
possible, which adds unittest sub-test support and a new ``subtests`` fixture as discussed in
4+
`#1367 <https://github.com/pytest-dev/pytest/issues/1367>`__.
5+
6+
For details on the internal refactorings, please see the details on the related PR.

src/_pytest/reports.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import py
22

3+
from _pytest._code.code import ExceptionInfo
34
from _pytest._code.code import TerminalRepr
5+
from _pytest.outcomes import skip
46

57

68
def getslaveinfoline(node):
@@ -20,6 +22,7 @@ def getslaveinfoline(node):
2022

2123
class BaseReport(object):
2224
when = None
25+
location = None
2326

2427
def __init__(self, **kw):
2528
self.__dict__.update(kw)
@@ -97,6 +100,43 @@ def capstderr(self):
97100
def fspath(self):
98101
return self.nodeid.split("::")[0]
99102

103+
@property
104+
def count_towards_summary(self):
105+
"""
106+
**Experimental**
107+
108+
Returns True if this report should be counted towards the totals shown at the end of the
109+
test session: "1 passed, 1 failure, etc".
110+
111+
.. note::
112+
113+
This function is considered **experimental**, so beware that it is subject to changes
114+
even in patch releases.
115+
"""
116+
return True
117+
118+
@property
119+
def head_line(self):
120+
"""
121+
**Experimental**
122+
123+
Returns the head line shown when longrepr output for this report, more commonly during
124+
traceback representation during failures::
125+
126+
________ Test.foo ________
127+
128+
129+
In the example above, the head_line is "Test.foo".
130+
131+
.. note::
132+
133+
This function is considered **experimental**, so beware that it is subject to changes
134+
even in patch releases.
135+
"""
136+
if self.location is not None:
137+
fspath, lineno, domain = self.location
138+
return domain
139+
100140

101141
class TestReport(BaseReport):
102142
""" Basic test report object (also used for setup and teardown calls if
@@ -159,6 +199,49 @@ def __repr__(self):
159199
self.outcome,
160200
)
161201

202+
@classmethod
203+
def from_item_and_call(cls, item, call):
204+
"""
205+
Factory method to create and fill a TestReport with standard item and call info.
206+
"""
207+
when = call.when
208+
duration = call.stop - call.start
209+
keywords = {x: 1 for x in item.keywords}
210+
excinfo = call.excinfo
211+
sections = []
212+
if not call.excinfo:
213+
outcome = "passed"
214+
longrepr = None
215+
else:
216+
if not isinstance(excinfo, ExceptionInfo):
217+
outcome = "failed"
218+
longrepr = excinfo
219+
elif excinfo.errisinstance(skip.Exception):
220+
outcome = "skipped"
221+
r = excinfo._getreprcrash()
222+
longrepr = (str(r.path), r.lineno, r.message)
223+
else:
224+
outcome = "failed"
225+
if call.when == "call":
226+
longrepr = item.repr_failure(excinfo)
227+
else: # exception in setup or teardown
228+
longrepr = item._repr_failure_py(
229+
excinfo, style=item.config.option.tbstyle
230+
)
231+
for rwhen, key, content in item._report_sections:
232+
sections.append(("Captured %s %s" % (key, rwhen), content))
233+
return cls(
234+
item.nodeid,
235+
item.location,
236+
keywords,
237+
outcome,
238+
longrepr,
239+
when,
240+
sections,
241+
duration,
242+
user_properties=item.user_properties,
243+
)
244+
162245

163246
class CollectReport(BaseReport):
164247
when = "collect"

src/_pytest/runner.py

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -246,43 +246,7 @@ def __repr__(self):
246246

247247

248248
def pytest_runtest_makereport(item, call):
249-
when = call.when
250-
duration = call.stop - call.start
251-
keywords = {x: 1 for x in item.keywords}
252-
excinfo = call.excinfo
253-
sections = []
254-
if not call.excinfo:
255-
outcome = "passed"
256-
longrepr = None
257-
else:
258-
if not isinstance(excinfo, ExceptionInfo):
259-
outcome = "failed"
260-
longrepr = excinfo
261-
elif excinfo.errisinstance(skip.Exception):
262-
outcome = "skipped"
263-
r = excinfo._getreprcrash()
264-
longrepr = (str(r.path), r.lineno, r.message)
265-
else:
266-
outcome = "failed"
267-
if call.when == "call":
268-
longrepr = item.repr_failure(excinfo)
269-
else: # exception in setup or teardown
270-
longrepr = item._repr_failure_py(
271-
excinfo, style=item.config.option.tbstyle
272-
)
273-
for rwhen, key, content in item._report_sections:
274-
sections.append(("Captured %s %s" % (key, rwhen), content))
275-
return TestReport(
276-
item.nodeid,
277-
item.location,
278-
keywords,
279-
outcome,
280-
longrepr,
281-
when,
282-
sections,
283-
duration,
284-
user_properties=item.user_properties,
285-
)
249+
return TestReport.from_item_and_call(item, call)
286250

287251

288252
def pytest_make_collect_report(collector):

src/_pytest/terminal.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ class WarningReport(object):
197197
message = attr.ib()
198198
nodeid = attr.ib(default=None)
199199
fslocation = attr.ib(default=None)
200+
count_towards_summary = True
200201

201202
def get_location(self, config):
202203
"""
@@ -383,6 +384,7 @@ def pytest_runtest_logstart(self, nodeid, location):
383384
self.write_fspath_result(fsid, "")
384385

385386
def pytest_runtest_logreport(self, report):
387+
self._tests_ran = True
386388
rep = report
387389
res = self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
388390
category, letter, word = res
@@ -391,7 +393,6 @@ def pytest_runtest_logreport(self, report):
391393
else:
392394
markup = None
393395
self.stats.setdefault(category, []).append(rep)
394-
self._tests_ran = True
395396
if not letter and not word:
396397
# probably passed setup/teardown
397398
return
@@ -724,9 +725,8 @@ def mkrel(nodeid):
724725
return res + " "
725726

726727
def _getfailureheadline(self, rep):
727-
if hasattr(rep, "location"):
728-
fspath, lineno, domain = rep.location
729-
return domain
728+
if rep.head_line:
729+
return rep.head_line
730730
else:
731731
return "test session" # XXX?
732732

@@ -874,18 +874,23 @@ def summary_stats(self):
874874

875875

876876
def build_summary_stats_line(stats):
877-
keys = ("failed passed skipped deselected xfailed xpassed warnings error").split()
878-
unknown_key_seen = False
879-
for key in stats.keys():
880-
if key not in keys:
881-
if key: # setup/teardown reports have an empty key, ignore them
882-
keys.append(key)
883-
unknown_key_seen = True
877+
known_types = (
878+
"failed passed skipped deselected xfailed xpassed warnings error".split()
879+
)
880+
unknown_type_seen = False
881+
for found_type in stats:
882+
if found_type not in known_types:
883+
if found_type: # setup/teardown reports have an empty key, ignore them
884+
known_types.append(found_type)
885+
unknown_type_seen = True
884886
parts = []
885-
for key in keys:
886-
val = stats.get(key, None)
887-
if val:
888-
parts.append("%d %s" % (len(val), key))
887+
for key in known_types:
888+
reports = stats.get(key, None)
889+
if reports:
890+
count = sum(
891+
1 for rep in reports if getattr(rep, "count_towards_summary", True)
892+
)
893+
parts.append("%d %s" % (count, key))
889894

890895
if parts:
891896
line = ", ".join(parts)
@@ -894,14 +899,14 @@ def build_summary_stats_line(stats):
894899

895900
if "failed" in stats or "error" in stats:
896901
color = "red"
897-
elif "warnings" in stats or unknown_key_seen:
902+
elif "warnings" in stats or unknown_type_seen:
898903
color = "yellow"
899904
elif "passed" in stats:
900905
color = "green"
901906
else:
902907
color = "yellow"
903908

904-
return (line, color)
909+
return line, color
905910

906911

907912
def _plugin_nameversions(plugininfo):

testing/test_terminal.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import pytest
1717
from _pytest.main import EXIT_NOTESTSCOLLECTED
18+
from _pytest.reports import BaseReport
1819
from _pytest.terminal import _plugin_nameversions
1920
from _pytest.terminal import build_summary_stats_line
2021
from _pytest.terminal import getreportopt
@@ -1228,6 +1229,20 @@ def test_summary_stats(exp_line, exp_color, stats_arg):
12281229
assert color == exp_color
12291230

12301231

1232+
def test_skip_counting_towards_summary():
1233+
class DummyReport(BaseReport):
1234+
count_towards_summary = True
1235+
1236+
r1 = DummyReport()
1237+
r2 = DummyReport()
1238+
res = build_summary_stats_line({"failed": (r1, r2)})
1239+
assert res == ("2 failed", "red")
1240+
1241+
r1.count_towards_summary = False
1242+
res = build_summary_stats_line({"failed": (r1, r2)})
1243+
assert res == ("1 failed", "red")
1244+
1245+
12311246
class TestClassicOutputStyle(object):
12321247
"""Ensure classic output style works as expected (#3883)"""
12331248

0 commit comments

Comments
 (0)