Skip to content

Commit 42d7918

Browse files
committed
Personal review adjustments, move more into TestRestul
1 parent f2ba9db commit 42d7918

5 files changed

Lines changed: 56 additions & 59 deletions

File tree

‎Lib/test/libregrtest/main.py‎

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import html
21
import os
32
import random
43
import re
@@ -493,30 +492,14 @@ def write_github_summary(self) -> None:
493492
self.fail_rerun)
494493
if not self.github_summary or not failed or not exitcode:
495494
return
496-
cases = [(result.errors or []) + (result.failures or [])
497-
for result in failed]
498-
ncases = sum(map(len, cases))
495+
ncases = sum(len(result.test_cases) for result in failed)
499496
with open(self.github_summary, "a", encoding="utf-8") as fp:
500-
def write(text: str) -> None:
501-
# Separate Markdown blocks with an empty line
502-
print(text, end="\n\n", file=fp)
503-
504-
write(f"## {decolor(self.get_state())}: "
505-
f"{count(len(failed), 'test file')} and "
506-
f"{count(ncases, 'test case')} failed")
507-
for result, result_cases in zip(failed, cases):
508-
write(f"### {decolor(str(result))}")
509-
if result.env_changed_reasons:
510-
write("\n".join(f"- {reason}"
511-
for reason in result.env_changed_reasons))
512-
for name, traceback in result_cases:
513-
# Expand the tracebacks if they fit on a screen: up to 5
514-
# test cases with a traceback of less than 30 lines
515-
is_open = ncases <= 5 and traceback.count("\n") < 30
516-
write(f"<details{' open' if is_open else ''}>"
517-
f"<summary>{html.escape(name)}</summary>")
518-
write(f"```pytb\n{decolor(traceback).rstrip()}\n```")
519-
write("</details>")
497+
fp.write(f"## {decolor(self.get_state())}: "
498+
f"{count(len(failed), 'test file')} and "
499+
f"{count(ncases, 'test case')} failed\n\n")
500+
for result in failed:
501+
# Expand the tracebacks of up to 5 test cases
502+
fp.write(result.format_github_summary(expand=ncases <= 5))
520503

521504
def display_summary(self) -> None:
522505
if self.first_runtests is None:

‎Lib/test/libregrtest/result.py‎

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import dataclasses
2+
import html
23
import json
3-
import os
4-
from _colorize import get_colors # type: ignore[import-not-found]
4+
import pathlib
5+
from _colorize import decolor, get_colors # type: ignore[import-not-found]
56
from typing import Any
67

78
from .findtests import findtestdir
@@ -182,25 +183,49 @@ def __str__(self) -> str:
182183
def has_meaningful_duration(self):
183184
return State.has_meaningful_duration(self.state)
184185

186+
@property
187+
def test_cases(self) -> list[tuple[str, str]]:
188+
"""Failed test cases: list of (name, traceback)."""
189+
return (self.errors or []) + (self.failures or [])
190+
191+
def format_github_summary(self, expand: bool) -> str:
192+
"""Format the Markdown section of the GitHub Actions job summary."""
193+
blocks = [f"### {decolor(str(self))}"]
194+
if self.env_changed_reasons:
195+
blocks.append("\n".join(f"- {reason}"
196+
for reason in self.env_changed_reasons))
197+
for name, traceback in self.test_cases:
198+
# Expand the tracebacks if they fit on a screen: a traceback of
199+
# less than 30 lines
200+
is_open = expand and traceback.count("\n") < 30
201+
blocks += [f"<details{' open' if is_open else ''}>"
202+
f"<summary>{html.escape(name)}</summary>",
203+
f"```pytb\n{decolor(traceback).rstrip()}\n```",
204+
"</details>"]
205+
# Separate Markdown blocks with an empty line
206+
return "".join(f"{block}\n\n" for block in blocks)
207+
185208
def print_github_annotation(self, runtests: RunTests) -> None:
186-
"""Annotate a failed test without test case failures.
209+
"""Annotate each failed test case, or else the failed test file.
187210
188-
For example: crash, timeout, env changed. Locate the annotation in the
189-
test file. Test case failures are annotated by the test runner, where
190-
they are reported (see RegressionTestResult.printErrorList()).
211+
For example, the test file for a crash, timeout or env changed.
191212
"""
192-
if (runtests.github_annotations
193-
and self.is_failed(runtests.fail_env_changed)
194-
and not self.errors and not self.failures):
213+
if not (runtests.github_annotations
214+
and self.is_failed(runtests.fail_env_changed)):
215+
return
216+
# Test file: "test_x" is test_x.py or test_x/__init__.py,
217+
# "test.test_x.test_y" is test_x/test_y.py
218+
name = self.test_name.removeprefix("test.")
219+
path = pathlib.Path(findtestdir(runtests.test_dir), *name.split("."))
220+
filename = path.with_suffix(".py")
221+
if not filename.exists():
222+
filename = path / "__init__.py"
223+
test_cases = self.test_cases
224+
if not test_cases:
195225
message = "\n".join([str(self), *(self.env_changed_reasons or ())])
196-
# Test file: "test_x" is test_x.py or test_x/__init__.py,
197-
# "test.test_x.test_y" is test_x/test_y.py
198-
name = self.test_name.removeprefix("test.")
199-
path = os.path.join(findtestdir(runtests.test_dir), *name.split("."))
200-
filename = path + ".py"
201-
if not os.path.exists(filename):
202-
filename = os.path.join(path, "__init__.py")
203-
print_github_annotation(self.test_name, message, filename)
226+
test_cases = [(self.test_name, message)]
227+
for title, message in test_cases:
228+
print_github_annotation(title, message, str(filename))
204229

205230
def set_env_changed(self, *reasons):
206231
if self.state is None or self.state == State.PASSED:

‎Lib/test/libregrtest/setup.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
from .filter import set_match_tests
1414
from .runtests import RunTests
15-
lazy from .testresult import RegressionTestResult
1615
from .utils import (
1716
setup_unraisable_hook, setup_threading_excepthook,
1817
adjust_rlimit_nofile)
@@ -113,9 +112,9 @@ def setup_tests(runtests: RunTests) -> None:
113112

114113
set_match_tests(runtests.match_tests)
115114

116-
RegressionTestResult.GITHUB_ANNOTATIONS = runtests.github_annotations
117115
if runtests.use_junit:
118116
support.junit_xml_list = []
117+
from .testresult import RegressionTestResult
119118
RegressionTestResult.USE_XML = True
120119
else:
121120
support.junit_xml_list = None

‎Lib/test/libregrtest/testresult.py‎

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@
99
import traceback
1010
import unittest
1111
from test import support
12-
from test.libregrtest.utils import print_github_annotation, sanitize_xml
12+
from test.libregrtest.utils import sanitize_xml
1313

1414
class RegressionTestResult(unittest.TextTestResult):
1515
USE_XML = False
16-
GITHUB_ANNOTATIONS = False
1716

1817
def __init__(self, stream, descriptions, verbosity):
1918
super().__init__(stream=stream, descriptions=descriptions,
@@ -131,14 +130,6 @@ def addUnexpectedSuccess(self, test):
131130
self._add_result(test, outcome='UNEXPECTED_SUCCESS')
132131
super().addUnexpectedSuccess(test)
133132

134-
def printErrorList(self, flavour, errors):
135-
for test, err in errors:
136-
super().printErrorList(flavour, [(test, err)])
137-
# Annotate just after the failure report, so that the annotation
138-
# links to the report in the job log
139-
if self.GITHUB_ANNOTATIONS:
140-
print_github_annotation(str(test), err, file=self.stream)
141-
142133
def get_xml_element(self):
143134
if not self.USE_XML:
144135
raise ValueError("USE_XML is false")

‎Lib/test/libregrtest/utils.py‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,26 +148,25 @@ def print_warning(msg: str) -> None:
148148
",": "%2C"})
149149

150150

151-
def print_github_annotation(title: str, message: str,
152-
filename: str | None = None, file=None) -> None:
151+
def print_github_annotation(title: str, message: str, filename: str) -> None:
153152
"""Print a GitHub Actions error annotation.
154153
155154
message is a traceback or a failure description. Only keep the exception
156155
which ends the traceback: the job log has the full traceback. Locate the
157156
annotation at the last traceback frame in a test file, or else at the
158157
first frame (the test), or else in filename (the test file).
159158
"""
159+
if not filename:
160+
raise ValueError(f"missing test file of annotation {title!r}")
160161
message = decolor(message)
161162
line = None
162163
# Traceback frames (' File "filename", line 123') and failed doctest
163164
# examples (same, but not indented)
164165
if frames := re.findall(r'^ *File "(.*)", line (\d+)', message,
165166
re.MULTILINE):
166167
test_frames = [frame for frame in frames
167-
if os.path.basename(frame[0]).startswith('test_')]
168+
if pathlib.Path(frame[0]).name.startswith('test_')]
168169
filename, line = test_frames[-1] if test_frames else frames[0]
169-
if not filename:
170-
raise ValueError(f"missing test file of annotation {title!r}")
171170
# In the job, the stdlib can be outside of the repository checkout
172171
path = pathlib.Path(filename)
173172
stdlib_dir = pathlib.Path(os.__file__).parent
@@ -181,7 +180,7 @@ def print_github_annotation(title: str, message: str,
181180
props_text = ",".join(f"{key}={value.translate(GITHUB_ESCAPE_PROPERTY)}"
182181
for key, value in props.items() if value)
183182
print(f"::error {props_text}::{message.translate(GITHUB_ESCAPE_DATA)}",
184-
file=file, flush=True)
183+
flush=True)
185184

186185

187186
def regrtest_unraisable_hook(unraisable) -> None:

0 commit comments

Comments
 (0)