Skip to content

Commit ba449b4

Browse files
committed
Annotate inline with test output
1 parent 041e577 commit ba449b4

4 files changed

Lines changed: 47 additions & 35 deletions

File tree

‎Lib/test/libregrtest/main.py‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
printlist, get_temp_dir, get_work_dir, exit_timeout,
2828
display_header, cleanup_temp_dir, print_warning,
2929
is_cross_compiled, get_host_runner, display_title,
30-
get_process_memory_usage, github_annotation, EXIT_TIMEOUT)
30+
get_process_memory_usage, EXIT_TIMEOUT)
3131

3232

3333
class Regrtest:
@@ -395,6 +395,8 @@ def run_test(
395395
result = run_single_test(test_name, runtests)
396396

397397
self.results.accumulate_result(result, runtests)
398+
if runtests.rerun and result.is_failed(runtests.fail_env_changed):
399+
result.print_github_annotations()
398400

399401
return result
400402

@@ -491,6 +493,11 @@ def write_github_summary(self) -> None:
491493
return
492494
# Tests which failed in the last run (the re-run, if any)
493495
failed = self.results.rerun_results
496+
if not self.results.rerun:
497+
# Failed tests were not re-run (ex: --python): annotate them now.
498+
# Otherwise, they were annotated when they failed again.
499+
for result in failed:
500+
result.print_github_annotations()
494501
cases = [(result.errors or []) + (result.failures or [])
495502
for result in failed]
496503
ncase = sum(map(len, cases))
@@ -507,14 +514,7 @@ def write(text: str) -> None:
507514
if result.env_changed_reasons:
508515
write("\n".join(f"- {reason}"
509516
for reason in result.env_changed_reasons))
510-
if not result_cases:
511-
message = "\n".join([str(result),
512-
*(result.env_changed_reasons or ())])
513-
github_annotation("error", result.test_name,
514-
decolor(message))
515517
for name, traceback in result_cases:
516-
github_annotation("error", f"{result.test_name}: {name}",
517-
decolor(traceback))
518518
# Expand short tracebacks when there are only a few
519519
is_open = ncase <= 5 and traceback.count("\n") < 30
520520
write(f"<details{' open' if is_open else ''}>"

‎Lib/test/libregrtest/result.py‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import dataclasses
22
import json
3-
from _colorize import get_colors # type: ignore[import-not-found]
3+
import os
4+
from _colorize import decolor, get_colors # type: ignore[import-not-found]
45
from typing import Any
56

67
from .utils import (
78
StrJSON, TestName, FilterTuple,
8-
format_duration, normalize_test_name, print_warning)
9+
format_duration, normalize_test_name, print_warning, traceback_location)
910

1011

1112
@dataclasses.dataclass(slots=True)
@@ -178,6 +179,40 @@ def __str__(self) -> str:
178179
def has_meaningful_duration(self):
179180
return State.has_meaningful_duration(self.state)
180181

182+
def print_github_annotations(self) -> None:
183+
"""Print a GitHub Actions error annotation per failed test case."""
184+
if not os.environ.get("GITHUB_STEP_SUMMARY"):
185+
return
186+
187+
def escape(text: str) -> str:
188+
return (text.replace("%", "%25").replace("\r", "%0D")
189+
.replace("\n", "%0A"))
190+
191+
def escape_property(text: str) -> str:
192+
return escape(text).replace(":", "%3A").replace(",", "%2C")
193+
194+
annotations = [(f"{self.test_name}: {name}", traceback)
195+
for name, traceback in (self.errors or [])
196+
+ (self.failures or [])]
197+
if not annotations:
198+
# No test case details: crash, timeout, env changed, etc.
199+
message = "\n".join([str(self), *(self.env_changed_reasons or ())])
200+
annotations = [(self.test_name, message)]
201+
for title, message in annotations:
202+
message = decolor(message)
203+
props: dict[str, str | int] = {}
204+
if location := traceback_location(message):
205+
props |= location
206+
# The job log only shows the message: start it with the location
207+
position = ":".join(str(location[key])
208+
for key in ("file", "line", "col")
209+
if key in location)
210+
message = f"{position}\n{message}"
211+
props["title"] = title
212+
props_text = ",".join(f"{key}={escape_property(str(value))}"
213+
for key, value in props.items())
214+
print(f"::error {props_text}::{escape(message)}", flush=True)
215+
181216
def set_env_changed(self, *reasons):
182217
if self.state is None or self.state == State.PASSED:
183218
self.state = State.ENV_CHANGED

‎Lib/test/libregrtest/run_workers.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,8 @@ def _process_result(self, item: QueueOutput) -> TestResult:
624624
stdout = mp_result.worker_stdout
625625
if stdout:
626626
print(stdout, flush=True)
627+
if self.runtests.rerun and result.is_failed(self.runtests.fail_env_changed):
628+
result.print_github_annotations()
627629

628630
return result
629631

‎Lib/test/libregrtest/utils.py‎

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -174,31 +174,6 @@ def traceback_location(traceback: str) -> dict[str, str | int] | None:
174174
return location
175175

176176

177-
def github_annotation(level: str, title: str, message: str) -> None:
178-
"""Emit a GitHub Actions annotation (workflow command).
179-
180-
Point it at the last frame of the traceback in message, if any.
181-
"""
182-
def escape(text: str) -> str:
183-
return (text.replace("%", "%25").replace("\r", "%0D")
184-
.replace("\n", "%0A"))
185-
186-
def escape_property(text: str) -> str:
187-
return escape(text).replace(":", "%3A").replace(",", "%2C")
188-
189-
props: dict[str, str | int] = {}
190-
if location := traceback_location(message):
191-
props |= location
192-
# The job log only shows the message: start it with the location
193-
position = ":".join(str(location[key])
194-
for key in ("file", "line", "col") if key in location)
195-
message = f"{position}\n{message}"
196-
props["title"] = title
197-
props_text = ",".join(f"{key}={escape_property(str(value))}"
198-
for key, value in props.items())
199-
print(f"::{level} {props_text}::{escape(message)}", flush=True)
200-
201-
202177
def regrtest_unraisable_hook(unraisable) -> None:
203178
global orig_unraisablehook
204179
support.set_environment_altered(

0 commit comments

Comments
 (0)