8000 gh-127718: Add colour to `test.regrtest` output (#127719) · python/cpython@212448b · GitHub
[go: up one dir, main page]

Skip to content

Commit 212448b

Browse files
authored
gh-127718: Add colour to test.regrtest output (#127719)
1 parent 2233c30 commit 212448b

File tree

7 files changed

+168
-53
lines changed

7 files changed

+168
-53
lines changed

Doc/library/test.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ top-level directory where Python was built. On Windows,
192192
executing :program:`rt.bat` from your :file:`PCbuild` directory will run all
193193
regression tests.
194194

195+
.. versionadded:: 3.14
196+
Output is colorized by default and can be
197+
:ref:`controlled using environment variables <using-on-controlling-color>`.
198+
195199

196200
:mod:`test.support` --- Utilities for the Python test suite
197201
===========================================================

Lib/test/libregrtest/main.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import sysconfig
77
import time
88
import trace
9+
from _colorize import get_colors # type: ignore[import-not-found]
910
from typing import NoReturn
1011

1112
from test.support import os_helper, MS_WINDOWS, flush_std_streams
@@ -270,6 +271,9 @@ def _rerun_failed_tests(self, runtests: RunTests) -> RunTests:
270271
return runtests
271272

272273
def rerun_failed_tests(self, runtests: RunTests) -> None:
274+
ansi = get_colors()
275+
red, reset = ansi.BOLD_RED, ansi.RESET
276+
273277
if self.python_cmd:
274278
# Temp patch for https://github.com/python/cpython/issues/94052
275279
self.log(
@@ -284,7 +288,10 @@ def rerun_failed_tests(self, runtests: RunTests) -> None:
284288
rerun_runtests = self._rerun_failed_tests(runtests)
285289

286290
if self.results.bad:
287-
print(count(len(self.results.bad), 'test'), "failed again:")
291+
print(
292+
f"{red}{count(len(self.results.bad), 'test')} "
293+
f"failed again:{reset}"
294+
)
288295
printlist(self.results.bad)
289296

290297
self.display_result(rerun_runtests)

Lib/test/libregrtest/result.py

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import dataclasses
22
import json
3+
from _colorize import get_colors # type: ignore[import-not-found]
34
from typing import Any
45

56
from .utils import (
@@ -105,54 +106,71 @@ def is_failed(self, fail_env_changed: bool) -> bool:
105106
return State.is_failed(self.state)
106107

107108
def _format_failed(self):
109+
ansi = get_colors()
110+
red, reset = ansi.BOLD_RED, ansi.RESET
108111
if self.errors and self.failures:
109112
le = len(self.errors)
110113
lf = len(self.failures)
111114
error_s = "error" + ("s" if le > 1 else "")
112115
failure_s = "failure" + ("s" if lf > 1 else "")
113-
return f"{self.test_name} failed ({le} {error_s}, {lf} {failure_s})"
116+
return (
117+
f"{red}{self.test_name} failed "
118+
f"({le} {error_s}, {lf} {failure_s}){reset}"
119+
)
114120

115121
if self.errors:
116122
le = len(self.errors)
117123
error_s = "error" + ("s" if le > 1 else "")
118-
return f"{self.test_name} failed ({le} {error_s})"
124+
return f"{red}{self.test_name} failed ({le} {error_s}){reset}"
119125

120126
if self.failures:
121127
lf = len(self.failures)
122128
failure_s = "failure" + ("s" if lf > 1 else "")
123-
return f"{self.test_name} failed ({lf} {failure_s})"
129+
return f"{red}{self.test_name} failed ({lf} {failure_s}){reset}"
124130

125-
return f"{self.test_name} failed"
131+
return f"{red}{self.test_name} failed{reset}"
126132

127133
def __str__(self) -> str:
134+
ansi = get_colors()
135+
green = ansi.GREEN
136+
red = ansi.BOLD_RED
137+
reset = ansi.RESET
138+
yellow = ansi.YELLOW
139+
128140
match self.state:
129141
case State.PASSED:
130-
return f"{self.test_name} passed"
142+
return f"{green}{self.test_name} passed{reset}"
131143
case State.FAILED:
132-
return self._format_failed()
144+
return f"{red}{self._format_failed()}{reset}"
133145
case State.SKIPPED:
134-
return f"{self.test_name} skipped"
146+
return f"{yellow}{self.test_name} skipped{reset}"
135147
case State.UNCAUGHT_EXC:
136-
return f"{self.test_name} failed (uncaught exception)"
148+
return (
149+
f"{red}{self.test_name} failed (uncaught exception){reset}"
150+
)
137151
case State.REFLEAK:
138-
return f"{self.test_name} failed (reference leak)"
152+
return f"{red}{self.test_name} failed (reference leak){reset}"
139153
case State.ENV_CHANGED:
140-
return f"{self.test_name} failed (env changed)"
154+
return f"{red}{self.test_name} failed (env changed){reset}"
141155
case State.RESOURCE_DENIED:
142-
return f"{self.test_name} skipped (resource denied)"
156+
return f"{yellow}{self.test_name} skipped (resource denied){reset}"
143157
case State.INTERRUPTED:
144-
return f"{self.test_name} interrupted"
158+
return f"{yellow}{self.test_name} interrupted{reset}"
145159
case State.WORKER_FAILED:
146-
return f"{self.test_name} worker non-zero exit code"
160+
return (
161+
f"{red}{self.test_name} worker non-zero exit code{reset}"
162+
)
147163
case State.WORKER_BUG:
148-
return f"{self.test_name} worker bug"
164+
return f"{red}{self.test_name} worker bug{reset}"
149165
case State.DID_NOT_RUN:
150-
return f"{self.test_name} ran no tests"
166+
return f"{yellow}{self.test_name} ran no tests{reset}"
151167
case State.TIMEOUT:
152168
assert self.duration is not None, "self.duration is None"
153169
return f"{self.test_name} timed out ({format_duration(self.duration)})"
154170
case _:
155-
raise ValueError("unknown result state: {state!r}")
171+
raise ValueError(
172+
f"{red}unknown result state: {{state!r}}{reset}"
173+
)
156174

157175
def has_meaningful_duration(self):
158176
return State.has_meaningful_duration(self.state)

Lib/test/libregrtest/results.py

Lines changed: 61 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import sys
22
import trace
3+
from _colorize import get_colors # type: ignore[import-not-found]
34
from typing import TYPE_CHECKING
45

56
from .runtests import RunTests
@@ -59,19 +60,24 @@ def no_tests_run(self) -> bool:
5960

6061
def get_state(self, fail_env_changed: bool) -> str:
6162
state = []
63+
ansi = get_colors()
64+
green = ansi.GREEN
65+
red = ansi.BOLD_RED
66+
reset = ansi.RESET
67+
yellow = ansi.YELLOW
6268
if self.bad:
63-
state.append("FAILURE")
69+
state.append(f"{red}FAILURE{reset}")
6470
elif fail_env_changed and self.env_changed:
65-
state.append("ENV CHANGED")
71+
state.append(f"{yellow}ENV CHANGED{reset}")
6672
elif self.no_tests_run():
67-
state.append("NO TESTS RAN")
73+
state.append(f"{yellow}NO TESTS RAN{reset}")
6874

6975
if self.interrupted:
70-
state.append("INTERRUPTED")
76+
state.append(f"{yellow}INTERRUPTED{reset}")
7177
if self.worker_bug:
72-
state.append("WORKER BUG")
78+
state.append(f"{red}WORKER BUG{reset}")
7379
if not state:
74-
state.append("SUCCESS")
80+
state.append(f"{green}SUCCESS{reset}")
7581

7682
return ', '.join(state)
7783

@@ -197,27 +203,51 @@ def write_junit(self, filename: StrPath) -> None:
197203
f.write(s)
198204

199205
def display_result(self, tests: TestTuple, quiet: bool, print_slowest: bool) -> None:
206+
ansi = get_colors()
207+
green = ansi.GREEN
208+
red = ansi.BOLD_RED
209+
reset = ansi.RESET
210+
yellow = ansi.YELLOW
211+
200212
if print_slowest:
201213
self.test_times.sort(reverse=True)
202214
print()
203-
print("10 slowest tests:")
215+
print(f"{yellow}10 slowest tests:{reset}")
204216
for test_time, test in self.test_times[:10]:
205-
print("- %s: %s" % (test, format_duration(test_time)))
217+
print(f"- {test}: {format_duration(test_time)}")
206218

207219
all_tests = []
208220
omitted = set(tests) - self.get_executed()
209221

210222
# less important
211-
all_tests.append((sorted(omitted), "test", "{} omitted:"))
223+
all_tests.append(
224+
(sorted(omitted), "test", f"{yellow}{{}} omitted:{reset}")
225+
)
212226
if not quiet:
213-
all_tests.append((self.skipped, "test", "{} skipped:"))
214-
all_tests.append((self.resource_denied, "test", "{} skipped (resource denied):"))
215-
all_tests.append((self.run_no_tests, "test", "{} run no tests:"))
227+
all_tests.append(
228+
(self.skipped, < 10000 span class=pl-s>"test", f"{yellow}{{}} skipped:{reset}")
229+
)
230+
all_tests.append(
231+
(
232+
self.resource_denied,
233+
"test",
234+
f"{yellow}{{}} skipped (resource denied):{reset}",
235+
)
236+
)
237+
all_tests.append(
238+
(self.run_no_tests, "test", f"{yellow}{{}} run no tests:{reset}")
239+
)
216240

217241
# more important
218-
all_tests.append((self.env_changed, "test", "{} altered the execution environment (env changed):"))
219-
all_tests.append((self.rerun, "re-run test", "{}:"))
220-
all_tests.append((self.bad, "test", "{} failed:"))
242+
all_tests.append(
243+
(
244+
self.env_changed,
245+
"test",
246+
f"{yellow}{{}} altered the execution environment (env changed):{reset}",
247+
)
248+
)
249+
all_tests.append((self.rerun, "re-run test", f"{yellow}{{}}:{reset}"))
250+
all_tests.append((self.bad, "test", f"{red}{{}} failed:{reset}"))
221251

222252
for tests_list, count_text, title_format in all_tests:
223253
if tests_list:
@@ -229,26 +259,29 @@ def display_result(self, tests: TestTuple, quiet: bool, print_slowest: bool) ->
229259
if self.good and not quiet:
230260
print()
231261
text = count(len(self.good), "test")
232-
text = f"{text} OK."
233-
if (self.is_all_good() and len(self.good) > 1):
262+
text = f"{green}{text} OK.{reset}"
263+
if self.is_all_good() and len(self.good) > 1:
234264
text = f"All {text}"
235265
print(text)
236266

237267
if self.interrupted:
238268
print()
239-
print("Test suite interrupted by signal SIGINT.")
269+
print(f"{yellow}Test suite interrupted by signal SIGINT.{reset}")
240270

241271
def display_summary(self, first_runtests: RunTests, filtered: bool) -> None:
242272
# Total tests
273+
ansi = get_colors()
274+
red, reset, yellow = ansi.RED, ansi.RESET, ansi.YELLOW
275+
243276
stats = self.stats
244277
text = f'run={stats.tests_run:,}'
245278
if filtered:
246279
text = f"{text} (filtered)"
247280
report = [text]
248281
if stats.failures:
249-
report.append(f'failures={stats.failures:,}')
282+
report.append(f'{red}failures={stats.failures:,}{reset}')
250283
if stats.skipped:
251-
report.append(f'skipped={stats.skipped:,}')
284+
report.append(f'{yellow}skipped={stats.skipped:,}{reset}')
252285
print(f"Total tests: {' '.join(report)}")
253286

254287
# Total test files
@@ -263,14 +296,14 @@ def display_summary(self, first_runtests: RunTests, filtered: bool) -> None:
263296
if filtered:
264297
text = f"{text} (filtered)"
265298
report = [text]
266-
for name, tests in (
267-
('failed', self.bad),
268-
('env_changed', self.env_changed),
269-
('skipped', self.skipped),
270-
('resource_denied', self.resource_denied),
271-
('rerun', self.rerun),
272-
('run_no_tests', self.run_no_tests),
299+
for name, tests, color in (
300+
('failed', self.bad, red),
301+
('env_changed', self.env_changed, yellow),
302+
('skipped', self.skipped, yellow),
303+
('resource_denied', self.resource_denied, yellow),
304+
('rerun', self.rerun, yellow),
305+
('run_no_tests', self.run_no_tests, yellow),
273306
):
274307
if tests:
275-
report.append(f'{name}={len(tests)}')
308+
report.append(f'{color}{name}={len(tests)}{reset}')
276309
print(f"Total test files: {' '.join(report)}")

Lib/test/libregrtest/single.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import traceback
88
import unittest
99

10+
from _colorize import get_colors # type: ignore[import-not-found]
1011
from test import support
1112
from test.support import threading_helper
1213

@@ -161,6 +162,8 @@ def test_func():
161162
def _runtest_env_changed_exc(result: TestResult, runtests: RunTests,
162163
display_failure: bool = True) -> None:
163164
# Handle exceptions, detect environment changes.
165+
ansi = get_colors()
166+
red, reset, yellow = ansi.RED, ansi.RESET, ansi.YELLOW
164167

165168
# Reset the environment_altered flag to detect if a test altered
166169
# the environment
@@ -181,28 +184,28 @@ def _runtest_env_changed_exc(result: TestResult, runtests: RunTests,
181184
_load_run_test(result, runtests)
182185
except support.ResourceDenied as exc:
183186
if not quiet and not pgo:
184-
print(f"{test_name} skipped -- {exc}", flush=True)
187+
print(f"{yellow}{test_name} skipped -- {exc}{reset}", flush=True)
185188
result.state = State.RESOURCE_DENIED
186189
return
187190
except unittest.SkipTest as exc:
188191
if not quiet and not pgo:
189-
print(f"{test_name} skipped -- {exc}", flush=True)
192+
print(f"{yellow}{test_name} skipped -- {exc}{reset}", flush=True)
190193
result.state = State.SKIPPED
191194
return
192195
except support.TestFailedWithDetails as exc:
193-
msg = f"test {test_name} failed"
196+
msg = f"{red}test {test_name} failed{reset}"
194197
if display_failure:
195-
msg = f"{msg} -- {exc}"
198+
msg = f"{red}{msg} -- {exc}{reset}"
196199
print(msg, file=sys.stderr, flush=True)
197200
result.state = State.FAILED
198201
result.errors = exc.errors
199202
result.failures = exc.failures
200203
result.stats = exc.stats
201204
return
202205
except support.TestFailed as exc:
203-
msg = f"test {test_name} failed"
206+
msg = f"{red}test {test_name} failed{reset}"
204207
if display_failure:
205-
msg = f"{msg} -- {exc}"
208+
msg = f"{red}{msg} -- {exc}{reset}"
206209
print(msg, file=sys.stderr, flush=True)
207210
result.state = State.FAILED
208211
result.stats = exc.stats
@@ -217,7 +220,7 @@ def _runtest_env_changed_exc(result: TestResult, runtests: RunTests,
217220
except:
218221
if not pgo:
219222
msg = traceback.format_exc()
220-
print(f"test {test_name} crashed -- {msg}",
223+
print(f"{red}test {test_name} crashed -- {msg}{reset}",
221224
file=sys.stderr, flush=True)
222225
result.state = State.UNCAUGHT_EXC
223226
return
@@ -300,6 +303,9 @@ def run_single_test(test_name: TestName, runtests: RunTests) -> TestResult:
300303
If runtests.use_junit, xml_data is a list containing each generated
301304
testsuite element.
302305
"""
306+
ansi = get_colors()
307+
red, reset, yellow = ansi.BOLD_RED, ansi.RESET, ansi.YELLOW
308+
303309
start_time = time.perf_counter()
304310
result = TestResult(test_name)
305311
pgo = runtests.pgo
@@ -308,7 +314,7 @@ def run_single_test(test_name: TestName, runtests: RunTests) -> TestResult:
308314
except:
309315
if not pgo:
310316
msg = traceback.format_exc()
311-
print(f"test {test_name} crashed -- {msg}",
317+
print(f"{red}test {test_name} crashed -- {msg}{reset}",
312318
file=sys.stderr, flush=True)
313319
result.state = State.UNCAUGHT_EXC
314320

0 commit comments

Comments
 (0)
0