Skip to content

Commit 7b993c9

Browse files
committed
_get_main_color: no yellow ("boring") for non-last item
- refactor _get_main_color/build_summary_stats_line - factor out property _is_last_item; test_summary_stats: tr._is_last_item - _write_progress_information_filling_space: remove color arg - use setter for stats, handling main color - _get_main_color: skip cache for last item - Handle random order in test for py35.
1 parent 0d4f479 commit 7b993c9

File tree

3 files changed

+126
-72
lines changed

3 files changed

+126
-72
lines changed

changelog/6409.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fallback to green (instead of yellow) for non-last items without previous passes with colored terminal progress indicator.

src/_pytest/terminal.py

Lines changed: 74 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@ def __init__(self, config: Config, file=None) -> None:
247247
self._showfspath = None
248248

249249
self.stats = {} # type: Dict[str, List[Any]]
250+
self._main_color = None # type: Optional[str]
251+
self._known_types = None # type: Optional[List]
250252
self.startdir = config.invocation_dir
251253
if file is None:
252254
file = sys.stdout
@@ -365,6 +367,12 @@ def section(self, title, sep="=", **kw):
365367
def line(self, msg, **kw):
366368
self._tw.line(msg, **kw)
367369

370+
def _add_stats(self, category: str, items: List) -> None:
371+
set_main_color = category not in self.stats
372+
self.stats.setdefault(category, []).extend(items[:])
373+
if set_main_color:
374+
self._set_main_color()
375+
368376
def pytest_internalerror(self, excrepr):
369377
for line in str(excrepr).split("\n"):
370378
self.write_line("INTERNALERROR> " + line)
@@ -374,15 +382,14 @@ def pytest_warning_captured(self, warning_message, item):
374382
# from _pytest.nodes import get_fslocation_from_item
375383
from _pytest.warnings import warning_record_to_str
376384

377-
warnings = self.stats.setdefault("warnings", [])
378385
fslocation = warning_message.filename, warning_message.lineno
379386
message = warning_record_to_str(warning_message)
380387

381388
nodeid = item.nodeid if item is not None else ""
382389
warning_report = WarningReport(
383390
fslocation=fslocation, message=message, nodeid=nodeid
384391
)
385-
warnings.append(warning_report)
392+
self._add_stats("warnings", [warning_report])
386393

387394
def pytest_plugin_registered(self, plugin):
388395
if self.config.option.traceconfig:
@@ -393,7 +400,7 @@ def pytest_plugin_registered(self, plugin):
393400
self.write_line(msg)
394401

395402
def pytest_deselected(self, items):
396-
self.stats.setdefault("deselected", []).extend(items)
403+
self._add_stats("deselected", items)
397404

398405
def pytest_runtest_logstart(self, nodeid, location):
399406
# ensure that the path is printed before the
@@ -414,7 +421,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None:
414421
word, markup = word
415422
else:
416423
markup = None
417-
self.stats.setdefault(category, []).append(rep)
424+
self._add_stats(category, [rep])
418425
if not letter and not word:
419426
# probably passed setup/teardown
420427
return
@@ -456,6 +463,10 @@ def pytest_runtest_logreport(self, report: TestReport) -> None:
456463
self._tw.write(" " + line)
457464
self.currentfspath = -2
458465

466+
@property
467+
def _is_last_item(self):
468+
return len(self._progress_nodeids_reported) == self._session.testscollected
469+
459470
def pytest_runtest_logfinish(self, nodeid):
460471
assert self._session
461472
if self.verbosity <= 0 and self._show_progress_info:
@@ -465,15 +476,12 @@ def pytest_runtest_logfinish(self, nodeid):
465476
else:
466477
progress_length = len(" [100%]")
467478

468-
main_color, _ = _get_main_color(self.stats)
469-
470479
self._progress_nodeids_reported.add(nodeid)
471-
is_last_item = (
472-
len(self._progress_nodeids_reported) == self._session.testscollected
473-
)
474-
if is_last_item:
475-
self._write_progress_information_filling_space(color=main_color)
480+
481+
if self._is_last_item:
482+
self._write_progress_information_filling_space()
476483
else:
484+
main_color, _ = self._get_main_color()
477485
w = self._width_of_current_line
478486
past_edge = w + progress_length + 1 >= self._screen_width
479487
if past_edge:
@@ -497,9 +505,8 @@ def _get_progress_information_message(self) -> str:
497505
)
498506
return " [100%]"
499507

500-
def _write_progress_information_filling_space(self, color=None):
501-
if not color:
502-
color, _ = _get_main_color(self.stats)
508+
def _write_progress_information_filling_space(self):
509+
color, _ = self._get_main_color()
503510
msg = self._get_progress_information_message()
504511
w = self._width_of_current_line
505512
fill = self._tw.fullwidth - w - 1
@@ -524,9 +531,9 @@ def pytest_collection(self):
524531

525532
def pytest_collectreport(self, report: CollectReport) -> None:
526533
if report.failed:
527-
self.stats.setdefault("error", []).append(report)
534+
self._add_stats("error", [report])
528535
elif report.skipped:
529-
self.stats.setdefault("skipped", []).append(report)
536+
self._add_stats("skipped", [report])
530537
items = [x for x in report.result if isinstance(x, pytest.Item)]
531538
self._numcollected += len(items)
532539
if self.isatty:
@@ -909,7 +916,7 @@ def summary_stats(self):
909916
return
910917

911918
session_duration = time.time() - self._sessionstarttime
912-
(parts, main_color) = build_summary_stats_line(self.stats)
919+
(parts, main_color) = self.build_summary_stats_line()
913920
line_parts = []
914921

915922
display_sep = self.verbosity >= 0
@@ -1012,6 +1019,56 @@ def show_skipped(lines: List[str]) -> None:
10121019
for line in lines:
10131020
self.write_line(line)
10141021

1022+
def _get_main_color(self) -> Tuple[str, List[str]]:
1023+
if self._main_color is None or self._known_types is None or self._is_last_item:
1024+
self._set_main_color()
1025+
assert self._main_color
1026+
assert self._known_types
1027+
return self._main_color, self._known_types
1028+
1029+
def _set_main_color(self) -> Tuple[str, List[str]]:
1030+
stats = self.stats
1031+
known_types = (
1032+
"failed passed skipped deselected xfailed xpassed warnings error".split()
1033+
)
1034+
unknown_type_seen = False
1035+
for found_type in stats.keys():
1036+
if found_type not in known_types:
1037+
if found_type: # setup/teardown reports have an empty key, ignore them
1038+
known_types.append(found_type)
1039+
unknown_type_seen = True
1040+
1041+
# main color
1042+
if "failed" in stats or "error" in stats:
1043+
main_color = "red"
1044+
elif "warnings" in stats or "xpassed" in stats or unknown_type_seen:
1045+
main_color = "yellow"
1046+
elif "passed" in stats or not self._is_last_item:
1047+
main_color = "green"
1048+
else:
1049+
main_color = "yellow"
1050+
self._main_color, self._known_types = main_color, known_types
1051+
return main_color, known_types
1052+
1053+
def build_summary_stats_line(self) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]:
1054+
main_color, known_types = self._get_main_color()
1055+
1056+
parts = []
1057+
for key in known_types:
1058+
reports = self.stats.get(key, None)
1059+
if reports:
1060+
count = sum(
1061+
1 for rep in reports if getattr(rep, "count_towards_summary", True)
1062+
)
1063+
color = _color_for_type.get(key, _color_for_type_default)
1064+
markup = {color: True, "bold": color == main_color}
1065+
parts.append(("%d %s" % _make_plural(count, key), markup))
1066+
1067+
if not parts:
1068+
parts = [("no tests ran", {_color_for_type_default: True})]
1069+
1070+
return parts, main_color
1071+
10151072

10161073
def _get_pos(config, rep):
10171074
nodeid = config.cwd_relative_nodeid(rep.nodeid)
@@ -1100,50 +1157,6 @@ def _make_plural(count, noun):
11001157
return count, noun + "s" if count != 1 else noun
11011158

11021159

1103-
def _get_main_color(stats) -> Tuple[str, List[str]]:
1104-
known_types = (
1105-
"failed passed skipped deselected xfailed xpassed warnings error".split()
1106-
)
1107-
unknown_type_seen = False
1108-
for found_type in stats.keys():
1109-
if found_type not in known_types:
1110-
if found_type: # setup/teardown reports have an empty key, ignore them
1111-
known_types.append(found_type)
1112-
unknown_type_seen = True
1113-
1114-
# main color
1115-
if "failed" in stats or "error" in stats:
1116-
main_color = "red"
1117-
elif "warnings" in stats or unknown_type_seen:
1118-
main_color = "yellow"
1119-
elif "passed" in stats:
1120-
main_color = "green"
1121-
else:
1122-
main_color = "yellow"
1123-
1124-
return main_color, known_types
1125-
1126-
1127-
def build_summary_stats_line(stats):
1128-
main_color, known_types = _get_main_color(stats)
1129-
1130-
parts = []
1131-
for key in known_types:
1132-
reports = stats.get(key, None)
1133-
if reports:
1134-
count = sum(
1135-
1 for rep in reports if getattr(rep, "count_towards_summary", True)
1136-
)
1137-
color = _color_for_type.get(key, _color_for_type_default)
1138-
markup = {color: True, "bold": color == main_color}
1139-
parts.append(("%d %s" % _make_plural(count, key), markup))
1140-
1141-
if not parts:
1142-
parts = [("no tests ran", {_color_for_type_default: True})]
1143-
1144-
return parts, main_color
1145-
1146-
11471160
def _plugin_nameversions(plugininfo) -> List[str]:
11481161
values = [] # type: List[str]
11491162
for plugin, dist in plugininfo:

testing/test_terminal.py

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
import pluggy
1212
import py
1313

14+
import _pytest.config
1415
import pytest
1516
from _pytest.main import ExitCode
1617
from _pytest.reports import BaseReport
1718
from _pytest.terminal import _folded_skips
1819
from _pytest.terminal import _get_line_with_reprcrash_message
1920
from _pytest.terminal import _plugin_nameversions
20-
from _pytest.terminal import build_summary_stats_line
2121
from _pytest.terminal import getreportopt
2222
from _pytest.terminal import TerminalReporter
2323

@@ -1344,6 +1344,12 @@ def test_failure():
13441344
assert stdout.count("=== warnings summary ") == 1
13451345

13461346

1347+
@pytest.fixture(scope="session")
1348+
def tr():
1349+
config = _pytest.config._prepareconfig()
1350+
return TerminalReporter(config)
1351+
1352+
13471353
@pytest.mark.parametrize(
13481354
"exp_color, exp_line, stats_arg",
13491355
[
@@ -1431,10 +1437,10 @@ def test_failure():
14311437
),
14321438
("yellow", [("1 xpassed", {"bold": True, "yellow": True})], {"xpassed": (1,)}),
14331439
(
1434-
"green",
1440+
"yellow",
14351441
[
1436-
("1 passed", {"bold": True, "green": True}),
1437-
("1 xpassed", {"bold": False, "yellow": True}),
1442+
("1 passed", {"bold": False, "green": True}),
1443+
("1 xpassed", {"bold": True, "yellow": True}),
14381444
],
14391445
{"xpassed": (1,), "passed": (1,)},
14401446
),
@@ -1474,26 +1480,42 @@ def test_failure():
14741480
),
14751481
],
14761482
)
1477-
def test_summary_stats(exp_line, exp_color, stats_arg):
1483+
def test_summary_stats(tr, exp_line, exp_color, stats_arg):
1484+
tr.stats = stats_arg
1485+
1486+
# Fake "_is_last_item" to be True.
1487+
class fake_session:
1488+
testscollected = 0
1489+
1490+
tr._session = fake_session
1491+
assert tr._is_last_item
1492+
1493+
# Reset cache.
1494+
tr._main_color = None
1495+
14781496
print("Based on stats: %s" % stats_arg)
14791497
print('Expect summary: "{}"; with color "{}"'.format(exp_line, exp_color))
1480-
(line, color) = build_summary_stats_line(stats_arg)
1498+
(line, color) = tr.build_summary_stats_line()
14811499
print('Actually got: "{}"; with color "{}"'.format(line, color))
14821500
assert line == exp_line
14831501
assert color == exp_color
14841502

14851503

1486-
def test_skip_counting_towards_summary():
1504+
def test_skip_counting_towards_summary(tr):
14871505
class DummyReport(BaseReport):
14881506
count_towards_summary = True
14891507

14901508
r1 = DummyReport()
14911509
r2 = DummyReport()
1492-
res = build_summary_stats_line({"failed": (r1, r2)})
1510+
tr.stats = {"failed": (r1, r2)}
1511+
tr._main_color = None
1512+
res = tr.build_summary_stats_line()
14931513
assert res == ([("2 failed", {"bold": True, "red": True})], "red")
14941514

14951515
r1.count_towards_summary = False
1496-
res = build_summary_stats_line({"failed": (r1, r2)})
1516+
tr.stats = {"failed": (r1, r2)}
1517+
tr._main_color = None
1518+
res = tr.build_summary_stats_line()
14971519
assert res == ([("1 failed", {"bold": True, "red": True})], "red")
14981520

14991521

@@ -1595,6 +1617,11 @@ def test_normal(self, many_tests_files, testdir):
15951617
def test_colored_progress(self, testdir, monkeypatch):
15961618
monkeypatch.setenv("PY_COLORS", "1")
15971619
testdir.makepyfile(
1620+
test_axfail="""
1621+
import pytest
1622+
@pytest.mark.xfail
1623+
def test_axfail(): assert 0
1624+
""",
15981625
test_bar="""
15991626
import pytest
16001627
@pytest.mark.parametrize('i', range(10))
@@ -1619,13 +1646,26 @@ def test_foobar(i): raise ValueError()
16191646
[
16201647
line.format(**RE_COLORS)
16211648
for line in [
1622-
r"test_bar.py ({green}\.{reset}){{10}}{green} \s+ \[ 50%\]{reset}",
1623-
r"test_foo.py ({green}\.{reset}){{5}}{yellow} \s+ \[ 75%\]{reset}",
1649+
r"test_axfail.py {yellow}x{reset}{green} \s+ \[ 4%\]{reset}",
1650+
r"test_bar.py ({green}\.{reset}){{10}}{green} \s+ \[ 52%\]{reset}",
1651+
r"test_foo.py ({green}\.{reset}){{5}}{yellow} \s+ \[ 76%\]{reset}",
16241652
r"test_foobar.py ({red}F{reset}){{5}}{red} \s+ \[100%\]{reset}",
16251653
]
16261654
]
16271655
)
16281656

1657+
# Only xfail should have yellow progress indicator.
1658+
result = testdir.runpytest("test_axfail.py")
1659+
result.stdout.re_match_lines(
1660+
[
1661+
line.format(**RE_COLORS)
1662+
for line in [
1663+
r"test_axfail.py {yellow}x{reset}{yellow} \s+ \[100%\]{reset}",
1664+
r"^{yellow}=+ ({yellow}{bold}|{bold}{yellow})1 xfailed{reset}{yellow} in ",
1665+
]
1666+
]
1667+
)
1668+
16291669
def test_count(self, many_tests_files, testdir):
16301670
testdir.makeini(
16311671
"""

0 commit comments

Comments
 (0)