Skip to content

Commit 5b2e5e8

Browse files
Improve summary stats when using '--collect-only' (#7875)
Co-authored-by: Bruno Oliveira <[email protected]>
1 parent 29f2f4e commit 5b2e5e8

File tree

10 files changed

+128
-21
lines changed

10 files changed

+128
-21
lines changed

changelog/7701.improvement.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improved reporting when using ``--collected-only``. It will now show the number of collected tests in the summary stats.

doc/en/example/nonpython.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,4 @@ interesting to just look at the collection tree:
102102
<YamlItem hello>
103103
<YamlItem ok>
104104
105-
========================== no tests ran in 0.12s ===========================
105+
========================== 2 tests found in 0.12s ===========================

doc/en/example/parametrize.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ objects, they are still using the default pytest representation:
175175
<Function test_timedistance_v3[forward]>
176176
<Function test_timedistance_v3[backward]>
177177
178-
========================== no tests ran in 0.12s ===========================
178+
========================== 8 tests found in 0.12s ===========================
179179
180180
In ``test_timedistance_v3``, we used ``pytest.param`` to specify the test IDs
181181
together with the actual data, instead of listing them separately.
@@ -252,7 +252,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia
252252
<Function test_demo1[advanced]>
253253
<Function test_demo2[advanced]>
254254
255-
========================== no tests ran in 0.12s ===========================
255+
========================== 4 tests found in 0.12s ===========================
256256
257257
Note that we told ``metafunc.parametrize()`` that your scenario values
258258
should be considered class-scoped. With pytest-2.3 this leads to a
@@ -328,7 +328,7 @@ Let's first see how it looks like at collection time:
328328
<Function test_db_initialized[d1]>
329329
<Function test_db_initialized[d2]>
330330
331-
========================== no tests ran in 0.12s ===========================
331+
========================== 2/2 tests found in 0.12s ===========================
332332
333333
And then when we run the test:
334334

doc/en/example/pythoncollection.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ The test collection would look like this:
157157
<Function simple_check>
158158
<Function complex_check>
159159
160-
========================== no tests ran in 0.12s ===========================
160+
========================== 2 tests found in 0.12s ===========================
161161
162162
You can check for multiple glob patterns by adding a space between the patterns:
163163

@@ -220,7 +220,7 @@ You can always peek at the collection tree without running tests like this:
220220
<Function test_method>
221221
<Function test_anothermethod>
222222
223-
========================== no tests ran in 0.12s ===========================
223+
========================== 3 tests found in 0.12s ===========================
224224
225225
.. _customizing-test-collection:
226226

@@ -282,7 +282,7 @@ leave out the ``setup.py`` file:
282282
<Module 'pkg/module_py2.py'>
283283
<Function 'test_only_on_python2'>
284284
285-
====== no tests ran in 0.04 seconds ======
285+
====== 1 tests found in 0.04 seconds ======
286286
287287
If you run with a Python 3 interpreter both the one test and the ``setup.py``
288288
file will be left out:
@@ -296,7 +296,7 @@ file will be left out:
296296
rootdir: $REGENDOC_TMPDIR, configfile: pytest.ini
297297
collected 0 items
298298
299-
========================== no tests ran in 0.12s ===========================
299+
========================== no tests found in 0.12s ===========================
300300
301301
It's also possible to ignore files based on Unix shell-style wildcards by adding
302302
patterns to :globalvar:`collect_ignore_glob`.

doc/en/fixture.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -919,7 +919,7 @@ Running the above tests results in the following test IDs being used:
919919
<Function test_ehlo[mail.python.org]>
920920
<Function test_noop[mail.python.org]>
921921
922-
========================== no tests ran in 0.12s ===========================
922+
========================== 10 tests found in 0.12s ===========================
923923
924924
.. _`fixture-parametrize-marks`:
925925

src/_pytest/terminal.py

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,15 +1163,45 @@ def _set_main_color(self) -> None:
11631163
self._main_color = self._determine_main_color(bool(unknown_types))
11641164

11651165
def build_summary_stats_line(self) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]:
1166-
main_color, known_types = self._get_main_color()
1166+
"""
1167+
Build the parts used in the last summary stats line.
1168+
1169+
The summary stats line is the line shown at the end, "=== 12 passed, 2 errors in Xs===".
1170+
1171+
This function builds a list of the "parts" that make up for the text in that line, in
1172+
the example above it would be:
11671173
1174+
[
1175+
("12 passed", {"green": True}),
1176+
("2 errors", {"red": True}
1177+
]
1178+
1179+
That last dict for each line is a "markup dictionary", used by TerminalWriter to
1180+
color output.
1181+
1182+
The final color of the line is also determined by this function, and is the second
1183+
element of the returned tuple.
1184+
"""
1185+
if self.config.getoption("collectonly"):
1186+
return self._build_collect_only_summary_stats_line()
1187+
else:
1188+
return self._build_normal_summary_stats_line()
1189+
1190+
def _get_reports_to_display(self, key: str) -> List[Any]:
1191+
"""Get test/collection reports for the given status key, such as `passed` or `error`."""
1192+
reports = self.stats.get(key, [])
1193+
return [x for x in reports if getattr(x, "count_towards_summary", True)]
1194+
1195+
def _build_normal_summary_stats_line(
1196+
self,
1197+
) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]:
1198+
main_color, known_types = self._get_main_color()
11681199
parts = []
1200+
11691201
for key in known_types:
1170-
reports = self.stats.get(key, None)
1202+
reports = self._get_reports_to_display(key)
11711203
if reports:
1172-
count = sum(
1173-
1 for rep in reports if getattr(rep, "count_towards_summary", True)
1174-
)
1204+
count = len(reports)
11751205
color = _color_for_type.get(key, _color_for_type_default)
11761206
markup = {color: True, "bold": color == main_color}
11771207
parts.append(("%d %s" % _make_plural(count, key), markup))
@@ -1181,6 +1211,40 @@ def build_summary_stats_line(self) -> Tuple[List[Tuple[str, Dict[str, bool]]], s
11811211

11821212
return parts, main_color
11831213

1214+
def _build_collect_only_summary_stats_line(
1215+
self,
1216+
) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]:
1217+
deselected = len(self._get_reports_to_display("deselected"))
1218+
errors = len(self._get_reports_to_display("error"))
1219+
1220+
if self._numcollected == 0:
1221+
parts = [("no tests collected", {"yellow": True})]
1222+
main_color = "yellow"
1223+
1224+
elif deselected == 0:
1225+
main_color = "green"
1226+
collected_output = "%d %s collected" % _make_plural(
1227+
self._numcollected, "test"
1228+
)
1229+
parts = [(collected_output, {main_color: True})]
1230+
else:
1231+
all_tests_were_deselected = self._numcollected == deselected
1232+
if all_tests_were_deselected:
1233+
main_color = "yellow"
1234+
collected_output = f"no tests collected ({deselected} deselected)"
1235+
else:
1236+
main_color = "green"
1237+
selected = self._numcollected - deselected
1238+
collected_output = f"{selected}/{self._numcollected} tests collected ({deselected} deselected)"
1239+
1240+
parts = [(collected_output, {main_color: True})]
1241+
1242+
if errors:
1243+
main_color = _color_for_type["error"]
1244+
parts += [("%d %s" % _make_plural(errors, "error"), {main_color: True})]
1245+
1246+
return parts, main_color
1247+
11841248

11851249
def _get_pos(config: Config, rep: BaseReport):
11861250
nodeid = config.cwd_relative_nodeid(rep.nodeid)
@@ -1267,7 +1331,7 @@ def _folded_skips(
12671331

12681332
def _make_plural(count: int, noun: str) -> Tuple[int, str]:
12691333
# No need to pluralize words such as `failed` or `passed`.
1270-
if noun not in ["error", "warnings"]:
1334+
if noun not in ["error", "warnings", "test"]:
12711335
return count, noun
12721336

12731337
# The `warnings` key is plural. To avoid API breakage, we keep it that way but

testing/logging/test_reporting.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -889,15 +889,15 @@ def test_simple():
889889
[
890890
"*collected 1 item*",
891891
"*<Module test_collection_collect_only_live_logging.py>*",
892-
"*no tests ran*",
892+
"*1 test collected*",
893893
]
894894
)
895895
elif verbose == "-q":
896896
result.stdout.no_fnmatch_line("*collected 1 item**")
897897
expected_lines.extend(
898898
[
899899
"*test_collection_collect_only_live_logging.py::test_simple*",
900-
"no tests ran in [0-9].[0-9][0-9]s",
900+
"1 test collected in [0-9].[0-9][0-9]s",
901901
]
902902
)
903903
elif verbose == "-qq":

testing/python/metafunc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1417,7 +1417,7 @@ def test_foo(x):
14171417
' @pytest.mark.parametrise("x", range(2))',
14181418
"E Failed: Unknown 'parametrise' mark, did you mean 'parametrize'?",
14191419
"*! Interrupted: 1 error during collection !*",
1420-
"*= 1 error in *",
1420+
"*= no tests collected, 1 error in *",
14211421
]
14221422
)
14231423

testing/test_cacheprovider.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -909,7 +909,7 @@ def test_fail(): assert 0
909909
"",
910910
"<Module pkg1/test_1.py>",
911911
" <Function test_fail>",
912-
"*= 1 deselected in *",
912+
"*= 1/2 tests collected (1 deselected) in *",
913913
],
914914
)
915915

@@ -942,7 +942,7 @@ def test_other(): assert 0
942942
" <Function test_fail>",
943943
" <Function test_other>",
944944
"",
945-
"*= 1 deselected in *",
945+
"*= 2/3 tests collected (1 deselected) in *",
946946
],
947947
consecutive=True,
948948
)
@@ -977,7 +977,7 @@ def test_pass(): pass
977977
"<Module pkg1/test_1.py>",
978978
" <Function test_pass>",
979979
"",
980-
"*= no tests ran in*",
980+
"*= 1 test collected in*",
981981
],
982982
consecutive=True,
983983
)

testing/test_terminal.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,48 @@ def test_collectonly_more_quiet(self, testdir):
458458
result = testdir.runpytest("--collect-only", "-qq")
459459
result.stdout.fnmatch_lines(["*test_fun.py: 1*"])
460460

461+
def test_collect_only_summary_status(self, testdir):
462+
"""Custom status depending on test selection using -k or -m. #7701."""
463+
testdir.makepyfile(
464+
test_collect_foo="""
465+
def test_foo(): pass
466+
""",
467+
test_collect_bar="""
468+
def test_foobar(): pass
469+
def test_bar(): pass
470+
""",
471+
)
472+
result = testdir.runpytest("--collect-only")
473+
result.stdout.fnmatch_lines("*== 3 tests collected in * ==*")
474+
475+
result = testdir.runpytest("--collect-only", "test_collect_foo.py")
476+
result.stdout.fnmatch_lines("*== 1 test collected in * ==*")
477+
478+
result = testdir.runpytest("--collect-only", "-k", "foo")
479+
result.stdout.fnmatch_lines("*== 2/3 tests collected (1 deselected) in * ==*")
480+
481+
result = testdir.runpytest("--collect-only", "-k", "test_bar")
482+
result.stdout.fnmatch_lines("*== 1/3 tests collected (2 deselected) in * ==*")
483+
484+
result = testdir.runpytest("--collect-only", "-k", "invalid")
485+
result.stdout.fnmatch_lines("*== no tests collected (3 deselected) in * ==*")
486+
487+
testdir.mkdir("no_tests_here")
488+
result = testdir.runpytest("--collect-only", "no_tests_here")
489+
result.stdout.fnmatch_lines("*== no tests collected in * ==*")
490+
491+
testdir.makepyfile(
492+
test_contains_error="""
493+
raise RuntimeError
494+
""",
495+
)
496+
result = testdir.runpytest("--collect-only")
497+
result.stdout.fnmatch_lines("*== 3 tests collected, 1 error in * ==*")
498+
result = testdir.runpytest("--collect-only", "-k", "foo")
499+
result.stdout.fnmatch_lines(
500+
"*== 2/3 tests collected (1 deselected), 1 error in * ==*"
501+
)
502+
461503

462504
class TestFixtureReporting:
463505
def test_setup_fixture_error(self, testdir):

0 commit comments

Comments
 (0)