https://github.com/python/cpython/commit/350fc64fbd0c8ebb6452041b9d09511e7971f524
commit: 350fc64fbd0c8ebb6452041b9d09511e7971f524
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-18T10:07:47+03:00
summary:
gh-132581: Report why the execution environment was altered (GH-155294)
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".
files:
A Misc/NEWS.d/next/Tests/2026-08-06-18-20-00.gh-issue-132581.envchg.rst
M Lib/test/libregrtest/main.py
M Lib/test/libregrtest/result.py
M Lib/test/libregrtest/results.py
M Lib/test/libregrtest/run_workers.py
M Lib/test/libregrtest/save_env.py
M Lib/test/libregrtest/single.py
M Lib/test/libregrtest/utils.py
M Lib/test/support/__init__.py
M Lib/test/test_regrtest.py
diff --git a/Lib/test/libregrtest/main.py b/Lib/test/libregrtest/main.py
index 7391056627cd068..db2e9acb850f107 100644
--- a/Lib/test/libregrtest/main.py
+++ b/Lib/test/libregrtest/main.py
@@ -453,7 +453,7 @@ def run_tests_sequentially(self, runtests: RunTests) ->
None:
def get_state(self) -> str:
state = self.results.get_state(self.fail_env_changed)
- if self.first_state:
+ if self.first_state and self.first_state != state:
state = f'{self.first_state} then {state}'
return state
diff --git a/Lib/test/libregrtest/result.py b/Lib/test/libregrtest/result.py
index daf7624366ee207..5324a46bb8cb1c5 100644
--- a/Lib/test/libregrtest/result.py
+++ b/Lib/test/libregrtest/result.py
@@ -100,6 +100,9 @@ class TestResult:
# partial coverage in a worker run; not used by sequential in-process runs
covered_lines: list[Location] | None = None
+ # short descriptions of how the test altered the execution environment
+ env_changed_reasons: list[str] | None = None
+
def is_failed(self, fail_env_changed: bool) -> bool:
if self.state == State.ENV_CHANGED:
return fail_env_changed
@@ -175,9 +178,15 @@ def __str__(self) -> str:
def has_meaningful_duration(self):
return State.has_meaningful_duration(self.state)
- def set_env_changed(self):
+ def set_env_changed(self, *reasons):
if self.state is None or self.state == State.PASSED:
self.state = State.ENV_CHANGED
+ if reasons:
+ if self.env_changed_reasons is None:
+ self.env_changed_reasons = []
+ for reason in reasons:
+ if reason not in self.env_changed_reasons:
+ self.env_changed_reasons.append(reason)
def must_stop(self, fail_fast: bool, fail_env_changed: bool) -> bool:
if State.must_stop(self.state):
diff --git a/Lib/test/libregrtest/results.py b/Lib/test/libregrtest/results.py
index a35934fc2c9ca82..ea5fee334215417 100644
--- a/Lib/test/libregrtest/results.py
+++ b/Lib/test/libregrtest/results.py
@@ -30,6 +30,8 @@ def __init__(self) -> None:
self.skipped: TestList = []
self.resource_denied: TestList = []
self.env_changed: TestList = []
+ # test name => how the test altered the execution environment
+ self.env_changed_reasons: dict[TestName, list[str]] = {}
self.run_no_tests: TestList = []
self.rerun: TestList = []
self.rerun_results: list[TestResult] = []
@@ -107,6 +109,9 @@ def accumulate_result(self, result: TestResult, runtests:
RunTests) -> None:
self.good.append(test_name)
case State.ENV_CHANGED:
self.env_changed.append(test_name)
+ if result.env_changed_reasons:
+ self.env_changed_reasons[test_name] = \
+ result.env_changed_reasons
self.rerun_results.append(result)
case State.SKIPPED:
self.skipped.append(test_name)
@@ -254,7 +259,18 @@ def display_result(self, tests: TestTuple, quiet: bool,
print_slowest: bool) ->
print()
count_text = count(len(tests_list), count_text)
print(title_format.format(count_text))
- printlist(tests_list)
+ if tests_list is self.env_changed:
+ # List every test and every reason on a separate line.
+ for test_name in sorted(tests_list):
+ reasons = self.env_changed_reasons.get(test_name)
+ if reasons:
+ print(f" {test_name}:")
+ for reason in reasons:
+ print(f" {reason}")
+ else:
+ print(f" {test_name}")
+ else:
+ printlist(tests_list)
if self.good and not quiet:
print()
diff --git a/Lib/test/libregrtest/run_workers.py
b/Lib/test/libregrtest/run_workers.py
index a146a439d9d3eca..c6db78e0882ba31 100644
--- a/Lib/test/libregrtest/run_workers.py
+++ b/Lib/test/libregrtest/run_workers.py
@@ -393,7 +393,8 @@ def _runtest(self, test_name: TestName,
f'Warning -- {test_name} leaked temporary files '
f'({len(tmp_files)}): {", ".join(sorted(tmp_files))}')
stdout += msg
- result.set_env_changed()
+ result.set_env_changed(
+ f"leaked temporary files: {', '.join(sorted(tmp_files))}")
return MultiprocessResult(result, stdout)
diff --git a/Lib/test/libregrtest/save_env.py b/Lib/test/libregrtest/save_env.py
index 6393036118010e6..4149210fa173109 100644
--- a/Lib/test/libregrtest/save_env.py
+++ b/Lib/test/libregrtest/save_env.py
@@ -347,7 +347,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
current = get()
# Check for changes to the resource's value
if current != original:
- support.environment_altered = True
+ support.set_environment_altered(f"{name} was modified")
restore(original)
if not self.quiet and not self.pgo:
print_warning(
diff --git a/Lib/test/libregrtest/single.py b/Lib/test/libregrtest/single.py
index d0759d2626989d6..d7a1c6bea202a65 100644
--- a/Lib/test/libregrtest/single.py
+++ b/Lib/test/libregrtest/single.py
@@ -173,7 +173,8 @@ def test_func():
remove_testfn(test_name, runtests.verbose)
if gc.garbage:
- support.environment_altered = True
+ support.set_environment_altered(
+ f"{len(gc.garbage)} uncollectable object(s)")
print_warning(f"{test_name} created {len(gc.garbage)} "
f"uncollectable object(s)")
@@ -194,6 +195,7 @@ def _runtest_env_changed_exc(result: TestResult, runtests:
RunTests,
# Reset the environment_altered flag to detect if a test altered
# the environment
support.environment_altered = False
+ support.environment_altered_reasons.clear()
pgo = runtests.pgo
if pgo:
@@ -261,7 +263,7 @@ def _runtest_env_changed_exc(result: TestResult, runtests:
RunTests,
return
if support.environment_altered:
- result.set_env_changed()
+ result.set_env_changed(*support.environment_altered_reasons)
# Don't override the state if it was already set (REFLEAK or ENV_CHANGED)
if result.state is None:
result.state = State.PASSED
diff --git a/Lib/test/libregrtest/utils.py b/Lib/test/libregrtest/utils.py
index f6c13227dbbcdf7..dfec7a59b02583b 100644
--- a/Lib/test/libregrtest/utils.py
+++ b/Lib/test/libregrtest/utils.py
@@ -142,7 +142,8 @@ def print_warning(msg: str) -> None:
def regrtest_unraisable_hook(unraisable) -> None:
global orig_unraisablehook
- support.environment_altered = True
+ support.set_environment_altered(
+ f"unraisable exception ({unraisable.exc_type.__name__})")
support.print_warning("Unraisable exception")
old_stderr = sys.stderr
try:
@@ -166,7 +167,8 @@ def setup_unraisable_hook() -> None:
def regrtest_threading_excepthook(args) -> None:
global orig_threading_excepthook
- support.environment_altered = True
+ support.set_environment_altered(
+ f"uncaught thread exception ({args.exc_type.__name__})")
support.print_warning(f"Uncaught thread exception:
{args.exc_type.__name__}")
old_stderr = sys.stderr
try:
@@ -525,7 +527,7 @@ def remove_testfn(test_name: TestName, verbose: int) ->
None:
if verbose:
print_warning(f"{test_name} left behind {kind} {name!r}")
- support.environment_altered = True
+ support.set_environment_altered(f"left behind {kind} {name!r}")
try:
import stat
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
index 3f2caebd21e336c..da116355eeea154 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -1563,14 +1563,25 @@ def print_warning(msg):
# to cleanup threads.
environment_altered = False
+# Short descriptions of what was altered, e.g. "unraisable exception".
+# They are reported by regrtest together with the name of the test.
+environment_altered_reasons = []
+
+
+def set_environment_altered(reason):
+ """Set the environment_altered flag and record why it was set."""
+ global environment_altered
+ environment_altered = True
+ if reason not in environment_altered_reasons:
+ environment_altered_reasons.append(reason)
+
+
def reap_children():
"""Use this function at the end of test_main() whenever sub-processes
are started. This will help ensure that no extra children (zombies)
stick around to hog resources and create problems when looking
for refleaks.
"""
- global environment_altered
-
# Need os.waitpid(-1, os.WNOHANG): Windows is not supported
if not (hasattr(os, 'waitpid') and hasattr(os, 'WNOHANG')):
return
@@ -1590,7 +1601,7 @@ def reap_children():
break
print_warning(f"reap_children() reaped child process {pid}")
- environment_altered = True
+ set_environment_altered("reaped child process")
@contextlib.contextmanager
diff --git a/Lib/test/test_regrtest.py b/Lib/test/test_regrtest.py
index f05008bf8bc2c01..f4baa965b3f632b 100644
--- a/Lib/test/test_regrtest.py
+++ b/Lib/test/test_regrtest.py
@@ -743,9 +743,13 @@ def list_regex(line_format, tests):
self.check_line(output, regex)
if env_changed:
- regex = list_regex(r'%s test%s altered the execution environment '
- r'\(env changed\)',
- env_changed)
+ # Every test is listed on a separate line, followed by the
+ # reasons why the environment was altered, one per line.
+ count = len(env_changed)
+ regex = (r'%s test%s altered the execution environment '
+ r'\(env changed\):\n' % (count, plural(count)))
+ regex += ''.join(r' %s:?\n(?: .*\n)*' % re.escape(name)
+ for name in sorted(env_changed))
self.check_line(output, regex)
if omitted:
@@ -834,7 +838,8 @@ def list_regex(line_format, tests):
state = ', '.join(state)
if rerun is not None:
new_state = 'SUCCESS' if rerun.success else 'FAILURE'
- state = f'{state} then {new_state}'
+ if new_state != state:
+ state = f'{state} then {new_state}'
self.check_line(output, f'Result: {state}', full=True)
def parse_random_seed(self, output: str) -> str:
diff --git
a/Misc/NEWS.d/next/Tests/2026-08-06-18-20-00.gh-issue-132581.envchg.rst
b/Misc/NEWS.d/next/Tests/2026-08-06-18-20-00.gh-issue-132581.envchg.rst
new file mode 100644
index 000000000000000..ff71f7a90a021b6
--- /dev/null
+++ b/Misc/NEWS.d/next/Tests/2026-08-06-18-20-00.gh-issue-132581.envchg.rst
@@ -0,0 +1,4 @@
+The list of tests which altered the execution environment now includes the
+reasons why the environment was considered altered, for example an unraisable
+exception or a modified :data:`sys.path`. The final result no longer repeats
+the same state twice (like ``ENV CHANGED then ENV CHANGED``).
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]