Skip to content

Commit ff7b75a

Browse files
committed
Add groups more of the time
1 parent 3468742 commit ff7b75a

4 files changed

Lines changed: 67 additions & 15 deletions

File tree

‎Lib/test/libregrtest/main.py‎

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,6 @@
3030
get_process_memory_usage, EXIT_TIMEOUT)
3131

3232

33-
def github_error(title: str, message: str) -> None:
34-
# Emit a GitHub Actions error annotation (workflow command)
35-
def escape(text: str) -> str:
36-
return (text.replace("%", "%25").replace("\r", "%0D")
37-
.replace("\n", "%0A"))
38-
title = escape(title).replace(":", "%3A").replace(",", "%2C")
39-
print(f"::error title={title}::{escape(message)}", flush=True)
40-
41-
4233
class Regrtest:
4334
"""Execute a test suite.
4435
@@ -513,14 +504,10 @@ def write(text: str) -> None:
513504
f"{count(ncase, 'test case')} failed")
514505
for result, result_cases in zip(failed, cases):
515506
write(f"### {decolor(str(result))}")
516-
if not result_cases:
517-
github_error(result.test_name, decolor(str(result)))
518507
if result.env_changed_reasons:
519508
write("\n".join(f"- {reason}"
520509
for reason in result.env_changed_reasons))
521510
for name, traceback in result_cases:
522-
github_error(f"{result.test_name}: {name}",
523-
decolor(traceback))
524511
# Expand short tracebacks when there are only a few
525512
is_open = ncase <= 5 and traceback.count("\n") < 30
526513
write(f"<details{' open' if is_open else ''}>"

‎Lib/test/libregrtest/results.py‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1+
import os
12
import sys
23
import trace
3-
from _colorize import get_colors # type: ignore[import-not-found]
4+
from _colorize import decolor, get_colors # type: ignore[import-not-found]
45
lazy from xml.etree.ElementTree import Element
56

67
from .runtests import RunTests
78
from .result import State, TestResult, TestStats, Location
89
from .utils import (
910
StrPath, TestName, TestTuple, TestList, FilterDict,
10-
printlist, count, format_duration)
11+
printlist, count, format_duration, github_annotation)
1112

1213

1314
# Python uses exit code 1 when an exception is not caught
@@ -19,6 +20,16 @@
1920
EXITCODE_INTERRUPTED = 130 # 128 + signal.SIGINT=2
2021

2122

23+
def annotate_github(result: TestResult, level: str) -> None:
24+
cases = (result.errors or []) + (result.failures or [])
25+
for name, traceback in cases:
26+
github_annotation(level, f"{result.test_name}: {name}",
27+
decolor(traceback))
28+
if not cases:
29+
message = "\n".join([str(result), *(result.env_changed_reasons or ())])
30+
github_annotation(level, result.test_name, decolor(message))
31+
32+
2233
class TestResults:
2334
def __init__(self) -> None:
2435
self.bad: TestList = []
@@ -128,6 +139,11 @@ def accumulate_result(self, result: TestResult, runtests: RunTests) -> None:
128139
if result.state == State.WORKER_BUG:
129140
self.worker_bug = True
130141

142+
if (os.environ.get("GITHUB_STEP_SUMMARY")
143+
and result.is_failed(fail_env_changed)):
144+
# A failure is only an error if it fails again when re-run
145+
annotate_github(result, "error" if rerun else "warning")
146+
131147
if result.has_meaningful_duration() and not rerun:
132148
if result.duration is None:
133149
raise ValueError("result.duration is None")

‎Lib/test/libregrtest/run_workers.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import threading
1111
import time
1212
import traceback
13+
from _colorize import decolor # type: ignore[import-not-found]
1314
from typing import Any, Literal, TextIO
1415

1516
from test import support
@@ -519,6 +520,11 @@ def __init__(self, num_workers: int, runtests: RunTests,
519520
else:
520521
self.worker_timeout = None
521522
self.workers: list[WorkerThread] = []
523+
# On GitHub Actions, fold the first run's log into groups: ranges of
524+
# passing tests, and one group per failed test. Re-runs aren't folded.
525+
self.github_groups = (not runtests.rerun
526+
and bool(os.environ.get("GITHUB_STEP_SUMMARY")))
527+
self.in_github_group = False
522528

523529
jobs = self.runtests.get_jobs()
524530
if jobs is not None:
@@ -611,7 +617,16 @@ def _process_result(self, item: QueueOutput) -> TestResult:
611617
self.test_index += 1
612618
mp_result = item[1]
613619
result = mp_result.result
620+
failed = result.is_failed(self.runtests.fail_env_changed)
621+
if self.github_groups and failed:
622+
# Keep the failure annotation out of collapsed groups
623+
self.github_group(None)
614624
self.results.accumulate_result(result, self.runtests)
625+
if self.github_groups:
626+
if failed:
627+
self.github_group(decolor(str(result)))
628+
elif not self.in_github_group:
629+
self.github_group(f"Tests from #{self.test_index}")
615630
self.display_result(mp_result)
616631

617632
# Display worker stdout
@@ -624,9 +639,19 @@ def _process_result(self, item: QueueOutput) -> TestResult:
624639
stdout = mp_result.worker_stdout
625640
if stdout:
626641
print(stdout, flush=True)
642+
if self.github_groups and failed:
643+
self.github_group(None)
627644

628645
return result
629646

647+
def github_group(self, title: str | None) -> None:
648+
"""End the current GitHub Actions log group, start a new one if title."""
649+
if self.in_github_group:
650+
print("::endgroup::", flush=True)
651+
self.in_github_group = title is not None
652+
if title is not None:
653+
print(f"::group::{title}", flush=True)
654+
630655
def get_mem_usage(self):
631656
usage = 0
632657
main_mem = get_process_memory_usage(os.getpid())
@@ -659,6 +684,7 @@ def run(self) -> None:
659684
print()
660685
self.results.interrupted = True
661686
finally:
687+
self.github_group(None)
662688
if self.timeout is not None:
663689
faulthandler.cancel_dump_traceback_later()
664690

‎Lib/test/libregrtest/utils.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,29 @@ def print_warning(msg: str) -> None:
140140
orig_unraisablehook: Callable[..., None] | None = None
141141

142142

143+
def github_annotation(level: str, title: str, message: str) -> None:
144+
"""Emit a GitHub Actions annotation (workflow command).
145+
146+
Point it at the last frame of the traceback in message, if any.
147+
"""
148+
def escape(text: str) -> str:
149+
return (text.replace("%", "%25").replace("\r", "%0D")
150+
.replace("\n", "%0A"))
151+
152+
def escape_property(text: str) -> str:
153+
return escape(text).replace(":", "%3A").replace(",", "%2C")
154+
155+
props = f"title={escape_property(title)}"
156+
frames = re.findall(r'^ File "(.+)", line (\d+)', message, re.MULTILINE)
157+
# Source checkout which contains Lib/
158+
srcdir = os.path.dirname(os.path.dirname(os.__file__)) + os.sep
159+
if frames and frames[-1][0].startswith(srcdir):
160+
filename, line = frames[-1]
161+
filename = filename.removeprefix(srcdir).replace(os.sep, "/")
162+
props = f"file={escape_property(filename)},line={line},{props}"
163+
print(f"::{level} {props}::{escape(message)}", flush=True)
164+
165+
143166
def regrtest_unraisable_hook(unraisable) -> None:
144167
global orig_unraisablehook
145168
support.set_environment_altered(

0 commit comments

Comments
 (0)