Skip to content

Commit 9a94bf1

Browse files
cmaloneyclaude
andcommitted
Simplify GitHub Actions annotations
Locate annotations at the last traceback frame in a test file instead of mapping each test to its file. Pass annotations to workers in RunTests, like use_junit, so tests which run regrtest don't inherit GITHUB_STEP_SUMMARY. Shrink the tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent b5247ed commit 9a94bf1

9 files changed

Lines changed: 74 additions & 169 deletions

File tree

‎Lib/test/libregrtest/main.py‎

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,8 @@ def _rerun_failed_tests(self, runtests: RunTests) -> RunTests:
285285
forever=False,
286286
fail_fast=False,
287287
match_tests_dict=match_tests_dict,
288-
output_on_failure=False)
288+
output_on_failure=False,
289+
github_annotations=bool(self.github_summary))
289290
self.logger.set_tests(runtests)
290291

291292
msg = f"Re-running {len(tests)} failed tests in verbose mode"
@@ -485,18 +486,17 @@ def finalize_tests(self, coverage: trace.CoverageResults | None) -> None:
485486
self.write_github_summary()
486487

487488
def write_github_summary(self) -> None:
488-
filename = os.environ.get("GITHUB_STEP_SUMMARY")
489489
# Tests which failed in the last run (the re-run, if any)
490490
failed = self.results.rerun_results
491491
# Only write a summary on failure: a CI run has many test jobs
492492
exitcode = self.results.get_exitcode(self.fail_env_changed,
493493
self.fail_rerun)
494-
if not filename or not failed or not exitcode:
494+
if not self.github_summary or not failed or not exitcode:
495495
return
496496
cases = [(result.errors or []) + (result.failures or [])
497497
for result in failed]
498498
ncases = sum(map(len, cases))
499-
with open(filename, "a", encoding="utf-8") as fp:
499+
with open(self.github_summary, "a", encoding="utf-8") as fp:
500500
def write(text: str) -> None:
501501
# Separate Markdown blocks with an empty line
502502
print(text, end="\n\n", file=fp)
@@ -555,6 +555,10 @@ def create_run_tests(self, tests: TestTuple) -> RunTests:
555555
hunt_refleak=self.hunt_refleak,
556556
test_dir=self.test_dir,
557557
use_junit=(self.junit_filename is not None),
558+
# Only annotate failures of the last run: not the first run if
559+
# failed tests will be re-run (with --python, they are not)
560+
github_annotations=(bool(self.github_summary) and
561+
not (self.want_rerun and not self.python_cmd)),
558562
coverage=self.coverage,
559563
memory_limit=self.memory_limit,
560564
gc_threshold=self.gc_threshold,
@@ -617,16 +621,10 @@ def _run_tests(self, selected: TestTuple, tests: TestList | None) -> int:
617621
if use_load_tracker:
618622
self.logger.start_load_tracker()
619623
try:
620-
with os_helper.EnvironmentVarGuard() as env:
621-
# In GitHub Actions, only annotate failures of the last run:
622-
# not the first run if failed tests will be re-run (with
623-
# --python, they are not: see rerun_failed_tests())
624-
if self.want_rerun and not self.python_cmd:
625-
env.unset("GITHUB_STEP_SUMMARY")
626-
if self.num_workers:
627-
self._run_tests_mp(runtests, self.num_workers)
628-
else:
629-
self.run_tests_sequentially(runtests)
624+
if self.num_workers:
625+
self._run_tests_mp(runtests, self.num_workers)
626+
else:
627+
self.run_tests_sequentially(runtests)
630628

631629
coverage = self.results.get_coverage_results()
632630
self.display_result(runtests)
@@ -808,6 +806,10 @@ def _init(self):
808806

809807
strip_py_suffix(self.cmdline_args)
810808

809+
# Pass GitHub Actions annotations to workers in RunTests, not to
810+
# regrtest run by tests
811+
self.github_summary = os.environ.pop("GITHUB_STEP_SUMMARY", None)
812+
811813
self._tmp_dir = get_temp_dir(self._tmp_dir)
812814

813815
@property

‎Lib/test/libregrtest/result.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ def print_github_annotation(self, runtests: RunTests) -> None:
189189
test file. Test case failures are annotated by the test runner, where
190190
they are reported (see RegressionTestResult.printErrorList()).
191191
"""
192-
if (self.is_failed(runtests.fail_env_changed)
192+
if (runtests.github_annotations
193+
and self.is_failed(runtests.fail_env_changed)
193194
and not self.errors and not self.failures):
194195
message = "\n".join([str(self), *(self.env_changed_reasons or ())])
195196
# Test file: "test_x" is test_x.py or test_x/__init__.py,

‎Lib/test/libregrtest/runtests.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ class RunTests:
9393
hunt_refleak: HuntRefleak | None
9494
test_dir: StrPath | None
9595
use_junit: bool
96+
github_annotations: bool
9697
coverage: bool
9798
memory_limit: str | None
9899
gc_threshold: int | None

‎Lib/test/libregrtest/setup.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ def setup_tests(runtests: RunTests) -> None:
113113
set_match_tests(runtests.match_tests)
114114

115115
if runtests.use_junit:
116-
from .testresult import RegressionTestResult
117116
support.junit_xml_list = []
117+
from .testresult import RegressionTestResult
118118
RegressionTestResult.USE_XML = True
119119
else:
120120
support.junit_xml_list = None

‎Lib/test/libregrtest/single.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from .runtests import RunTests
1717
from .save_env import saved_test_environment
1818
from .setup import setup_tests
19-
from .testresult import get_test_runner
19+
from .testresult import RegressionTestResult, get_test_runner
2020
from .parallel_case import ParallelTestCase
2121
from .utils import (
2222
TestName,
@@ -37,6 +37,8 @@ def run_unittest(test_mod, runtests: RunTests):
3737
if loader.errors:
3838
raise Exception("errors while loading tests")
3939
_filter_suite(tests, match_test)
40+
RegressionTestResult.GITHUB_TEST_FILE = (
41+
test_mod.__file__ if runtests.github_annotations else None)
4042
if runtests.parallel_threads:
4143
_parallelize_tests(tests, runtests.parallel_threads)
4244
return _run_suite(tests)

‎Lib/test/libregrtest/testresult.py‎

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,10 @@
1111
from test import support
1212
from test.libregrtest.utils import print_github_annotation, sanitize_xml
1313

14-
def test_to_filename(test):
15-
test = getattr(test, 'test_case', test) # subTest()
16-
if dt_test := getattr(test, '_dt_test', None): # doctest.DocTestCase
17-
return dt_test.filename
18-
module = type(test).__module__
19-
# Fixture errors (setUpClass, setUpModule) are not supported
20-
if module.startswith('unittest.'):
21-
return None
22-
return getattr(sys.modules.get(module), '__file__', None)
23-
2414
class RegressionTestResult(unittest.TextTestResult):
2515
USE_XML = False
16+
# Test file of the GitHub Actions annotations: don't annotate if None
17+
GITHUB_TEST_FILE: str | None = None
2618

2719
def __init__(self, stream, descriptions, verbosity):
2820
super().__init__(stream=stream, descriptions=descriptions,
@@ -145,8 +137,8 @@ def printErrorList(self, flavour, errors):
145137
super().printErrorList(flavour, [(test, err)])
146138
# Annotate just after the failure report, so that the annotation
147139
# links to the report in the job log
148-
if filename := test_to_filename(test):
149-
print_github_annotation(str(test), err, filename,
140+
if self.GITHUB_TEST_FILE:
141+
print_github_annotation(str(test), err, self.GITHUB_TEST_FILE,
150142
file=self.stream)
151143

152144
def get_xml_element(self):

‎Lib/test/libregrtest/utils.py‎

Lines changed: 17 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -142,56 +142,40 @@ def print_warning(msg: str) -> None:
142142
orig_unraisablehook: Callable[..., None] | None = None
143143

144144

145-
# Stdlib directory: Lib/ in a source checkout or an installed lib/python3.X/
146-
STDLIB_DIR = pathlib.Path(os.__file__).parent
147-
# Stdlib directory relative to the repository
148-
REPOSITORY_LIB_DIR = pathlib.Path("Lib")
149-
150-
151-
def repository_path(filename: str) -> str:
152-
"""Path of a stdlib file relative to the repository.
153-
154-
Map STDLIB_DIR to Lib/. Return other paths unchanged.
155-
"""
156-
path = pathlib.Path(filename)
157-
if not path.is_relative_to(STDLIB_DIR):
158-
return filename
159-
return (REPOSITORY_LIB_DIR / path.relative_to(STDLIB_DIR)).as_posix()
160-
161-
162145
# Escape the data and the properties of GitHub Actions workflow commands
163146
GITHUB_ESCAPE_DATA = str.maketrans({"%": "%25", "\r": "%0D", "\n": "%0A"})
164147
GITHUB_ESCAPE_PROPERTY = GITHUB_ESCAPE_DATA | str.maketrans({":": "%3A",
165148
",": "%2C"})
166149

167150

168-
def print_github_annotation(title: str, message: str, filename: str | None,
151+
def print_github_annotation(title: str, message: str, filename: str,
169152
file=None) -> None:
170-
"""Print a GitHub Actions error annotation, if run in GitHub Actions.
153+
"""Print a GitHub Actions error annotation.
171154
172155
message is a traceback or a failure description. Only keep the exception
173156
which ends the traceback: the job log has the full traceback. Locate the
174-
annotation in filename, the test file, at the last frame of the traceback
175-
in this file, if any.
157+
annotation at the last traceback frame in a test file, if any, or else
158+
in filename, the test file.
176159
"""
177-
if not os.environ.get("GITHUB_STEP_SUMMARY"):
178-
return
179-
if not filename:
180-
raise ValueError(f"missing test file of annotation {title!r}")
181160
message = decolor(message)
182-
props = {"file": repository_path(filename)}
183-
# Line of the last traceback frame (' File "filename", line 123') or
184-
# failed doctest example (same, but not indented) in filename
185-
if lines := re.findall(rf'^ *File "{re.escape(filename)}", line (\d+)',
186-
message, re.MULTILINE):
187-
props["line"] = lines[-1]
188-
props["title"] = title
161+
line = None
162+
# Last traceback frame (' File "filename", line 123') or failed doctest
163+
# example (same, but not indented) in a test file
164+
if frames := re.findall(r'^ *File "(.*[/\\]test_[^/\\]*\.py)", line (\d+)',
165+
message, re.MULTILINE):
166+
filename, line = frames[-1]
167+
# In the job, the stdlib can be outside of the repository checkout
168+
path = pathlib.Path(filename)
169+
stdlib_dir = pathlib.Path(os.__file__).parent
170+
if path.is_relative_to(stdlib_dir):
171+
filename = ("Lib" / path.relative_to(stdlib_dir)).as_posix()
172+
props = {"file": filename, "line": line, "title": title}
189173
# Only keep the exception: strip the traceback frames, each one a
190174
# ' File' line followed by its indented source lines
191175
message = re.split(r'^ File .*\n(?: .*\n)*', message,
192176
flags=re.MULTILINE)[-1].strip()
193177
props_text = ",".join(f"{key}={value.translate(GITHUB_ESCAPE_PROPERTY)}"
194-
for key, value in props.items())
178+
for key, value in props.items() if value)
195179
print(f"::error {props_text}::{message.translate(GITHUB_ESCAPE_DATA)}",
196180
file=file, flush=True)
197181

‎Lib/test/test_profiling/test_sampling_profiler/test_live_collector_ui.py‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111
import time
1212
import unittest
1313
from unittest import mock
14-
from test.support import (
15-
os_helper, requires, requires_remote_subprocess_debugging)
14+
from test.support import requires, requires_remote_subprocess_debugging
1615
from test.support.import_helper import import_module
1716

1817
# Only run these tests if curses is available
@@ -857,9 +856,6 @@ def mock_init_curses_side_effect(self, n_times, mock_self, stdscr):
857856

858857
def test_run_failed_module_live(self):
859858
"""Test that running a existing module that fails exits with clean error."""
860-
# Don't write to the GitHub Actions job summary of the test suite
861-
env = self.enterContext(os_helper.EnvironmentVarGuard())
862-
env.unset('GITHUB_STEP_SUMMARY')
863859

864860
args = [
865861
"profiling.sampling.cli", "run", "--live", "-m", "test",

0 commit comments

Comments
 (0)