Skip to content

Commit 7a2d6e0

Browse files
[3.13] gh-132581: Report why the execution environment was altered (GH-155294) (GH-155988)
The list of tests which altered the execution environment now includes the reasons -- an unraisable exception, a modified sys.path, leaked temporary files, etc -- one per line. The final result no longer repeats the same state twice, like "ENV CHANGED then ENV CHANGED". (cherry picked from commit 350fc64) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent 01189bd commit 7a2d6e0

10 files changed

Lines changed: 67 additions & 17 deletions

File tree

Lib/test/libregrtest/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ def run_tests_sequentially(self, runtests: RunTests) -> None:
424424

425425
def get_state(self) -> str:
426426
state = self.results.get_state(self.fail_env_changed)
427-
if self.first_state:
427+
if self.first_state and self.first_state != state:
428428
state = f'{self.first_state} then {state}'
429429
return state
430430

Lib/test/libregrtest/result.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ class TestResult:
9999
# partial coverage in a worker run; not used by sequential in-process runs
100100
covered_lines: list[Location] | None = None
101101

102+
# short descriptions of how the test altered the execution environment
103+
env_changed_reasons: list[str] | None = None
104+
102105
def is_failed(self, fail_env_changed: bool) -> bool:
103106
if self.state == State.ENV_CHANGED:
104107
return fail_env_changed
@@ -157,9 +160,15 @@ def __str__(self) -> str:
157160
def has_meaningful_duration(self):
158161
return State.has_meaningful_duration(self.state)
159162

160-
def set_env_changed(self):
163+
def set_env_changed(self, *reasons):
161164
if self.state is None or self.state == State.PASSED:
162165
self.state = State.ENV_CHANGED
166+
if reasons:
167+
if self.env_changed_reasons is None:
168+
self.env_changed_reasons = []
169+
for reason in reasons:
170+
if reason not in self.env_changed_reasons:
171+
self.env_changed_reasons.append(reason)
163172

164173
def must_stop(self, fail_fast: bool, fail_env_changed: bool) -> bool:
165174
if State.must_stop(self.state):

Lib/test/libregrtest/results.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ def __init__(self) -> None:
2929
self.skipped: TestList = []
3030
self.resource_denied: TestList = []
3131
self.env_changed: TestList = []
32+
# test name => how the test altered the execution environment
33+
self.env_changed_reasons: dict[TestName, list[str]] = {}
3234
self.run_no_tests: TestList = []
3335
self.rerun: TestList = []
3436
self.rerun_results: list[TestResult] = []
@@ -101,6 +103,9 @@ def accumulate_result(self, result: TestResult, runtests: RunTests) -> None:
101103
self.good.append(test_name)
102104
case State.ENV_CHANGED:
103105
self.env_changed.append(test_name)
106+
if result.env_changed_reasons:
107+
self.env_changed_reasons[test_name] = \
108+
result.env_changed_reasons
104109
self.rerun_results.append(result)
105110
case State.SKIPPED:
106111
self.skipped.append(test_name)
@@ -224,7 +229,18 @@ def display_result(self, tests: TestTuple, quiet: bool, print_slowest: bool) ->
224229
print()
225230
count_text = count(len(tests_list), count_text)
226231
print(title_format.format(count_text))
227-
printlist(tests_list)
232+
if tests_list is self.env_changed:
233+
# List every test and every reason on a separate line.
234+
for test_name in sorted(tests_list):
235+
reasons = self.env_changed_reasons.get(test_name)
236+
if reasons:
237+
print(f" {test_name}:")
238+
for reason in reasons:
239+
print(f" {reason}")
240+
else:
241+
print(f" {test_name}")
242+
else:
243+
printlist(tests_list)
228244

229245
if self.good and not quiet:
230246
print()

Lib/test/libregrtest/run_workers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,8 @@ def _runtest(self, test_name: TestName) -> MultiprocessResult:
377377
f'Warning -- {test_name} leaked temporary files '
378378
f'({len(tmp_files)}): {", ".join(sorted(tmp_files))}')
379379
stdout += msg
380-
result.set_env_changed()
380+
result.set_env_changed(
381+
f"leaked temporary files: {', '.join(sorted(tmp_files))}")
381382

382383
return MultiprocessResult(result, stdout)
383384

Lib/test/libregrtest/save_env.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
347347
current = get()
348348
# Check for changes to the resource's value
349349
if current != original:
350-
support.environment_altered = True
350+
support.set_environment_altered(f"{name} was modified")
351351
restore(original)
352352
if not self.quiet and not self.pgo:
353353
print_warning(

Lib/test/libregrtest/single.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ def test_func():
146146
remove_testfn(test_name, runtests.verbose)
147147

148148
if gc.garbage:
149-
support.environment_altered = True
149+
support.set_environment_altered(
150+
f"{len(gc.garbage)} uncollectable object(s)")
150151
print_warning(f"{test_name} created {len(gc.garbage)} "
151152
f"uncollectable object(s)")
152153

@@ -165,6 +166,7 @@ def _runtest_env_changed_exc(result: TestResult, runtests: RunTests,
165166
# Reset the environment_altered flag to detect if a test altered
166167
# the environment
167168
support.environment_altered = False
169+
support.environment_altered_reasons.clear()
168170

169171
pgo = runtests.pgo
170172
if pgo:
@@ -223,7 +225,7 @@ def _runtest_env_changed_exc(result: TestResult, runtests: RunTests,
223225
return
224226

225227
if support.environment_altered:
226-
result.set_env_changed()
228+
result.set_env_changed(*support.environment_altered_reasons)
227229
# Don't override the state if it was already set (REFLEAK or ENV_CHANGED)
228230
if result.state is None:
229231
result.state = State.PASSED

Lib/test/libregrtest/utils.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ def print_warning(msg: str) -> None:
133133

134134
def regrtest_unraisable_hook(unraisable) -> None:
135135
global orig_unraisablehook
136-
support.environment_altered = True
136+
support.set_environment_altered(
137+
f"unraisable exception ({unraisable.exc_type.__name__})")
137138
support.print_warning("Unraisable exception")
138139
old_stderr = sys.stderr
139140
try:
@@ -157,7 +158,8 @@ def setup_unraisable_hook() -> None:
157158

158159
def regrtest_threading_excepthook(args) -> None:
159160
global orig_threading_excepthook
160-
support.environment_altered = True
161+
support.set_environment_altered(
162+
f"uncaught thread exception ({args.exc_type.__name__})")
161163
support.print_warning(f"Uncaught thread exception: {args.exc_type.__name__}")
162164
old_stderr = sys.stderr
163165
try:
@@ -541,7 +543,7 @@ def remove_testfn(test_name: TestName, verbose: int) -> None:
541543

542544
if verbose:
543545
print_warning(f"{test_name} left behind {kind} {name!r}")
544-
support.environment_altered = True
546+
support.set_environment_altered(f"left behind {kind} {name!r}")
545547

546548
try:
547549
import stat

Lib/test/support/__init__.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1424,14 +1424,25 @@ def print_warning(msg):
14241424
# to cleanup threads.
14251425
environment_altered = False
14261426

1427+
# Short descriptions of what was altered, e.g. "unraisable exception".
1428+
# They are reported by regrtest together with the name of the test.
1429+
environment_altered_reasons = []
1430+
1431+
1432+
def set_environment_altered(reason):
1433+
"""Set the environment_altered flag and record why it was set."""
1434+
global environment_altered
1435+
environment_altered = True
1436+
if reason not in environment_altered_reasons:
1437+
environment_altered_reasons.append(reason)
1438+
1439+
14271440
def reap_children():
14281441
"""Use this function at the end of test_main() whenever sub-processes
14291442
are started. This will help ensure that no extra children (zombies)
14301443
stick around to hog resources and create problems when looking
14311444
for refleaks.
14321445
"""
1433-
global environment_altered
1434-
14351446
# Need os.waitpid(-1, os.WNOHANG): Windows is not supported
14361447
if not (hasattr(os, 'waitpid') and hasattr(os, 'WNOHANG')):
14371448
return
@@ -1451,7 +1462,7 @@ def reap_children():
14511462
break
14521463

14531464
print_warning(f"reap_children() reaped child process {pid}")
1454-
environment_altered = True
1465+
set_environment_altered("reaped child process")
14551466

14561467

14571468
@contextlib.contextmanager

Lib/test/test_regrtest.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -702,9 +702,13 @@ def list_regex(line_format, tests):
702702
self.check_line(output, regex)
703703

704704
if env_changed:
705-
regex = list_regex(r'%s test%s altered the execution environment '
706-
r'\(env changed\)',
707-
env_changed)
705+
# Every test is listed on a separate line, followed by the
706+
# reasons why the environment was altered, one per line.
707+
count = len(env_changed)
708+
regex = (r'%s test%s altered the execution environment '
709+
r'\(env changed\):\n' % (count, plural(count)))
710+
regex += ''.join(r' %s:?\n(?: .*\n)*' % re.escape(name)
711+
for name in sorted(env_changed))
708712
self.check_line(output, regex)
709713

710714
if omitted:
@@ -793,7 +797,8 @@ def list_regex(line_format, tests):
793797
state = ', '.join(state)
794798
if rerun is not None:
795799
new_state = 'SUCCESS' if rerun.success else 'FAILURE'
796-
state = f'{state} then {new_state}'
800+
if new_state != state:
801+
state = f'{state} then {new_state}'
797802
self.check_line(output, f'Result: {state}', full=True)
798803

799804
def parse_random_seed(self, output: str) -> str:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
The list of tests which altered the execution environment now includes the
2+
reasons why the environment was considered altered, for example an unraisable
3+
exception or a modified :data:`sys.path`. The final result no longer repeats
4+
the same state twice (like ``ENV CHANGED then ENV CHANGED``).

0 commit comments

Comments
 (0)