|
1 | 1 | import dataclasses |
2 | 2 | import json |
3 | 3 | import os |
4 | | -from _colorize import decolor, get_colors # type: ignore[import-not-found] |
| 4 | +from _colorize import get_colors # type: ignore[import-not-found] |
5 | 5 | from typing import Any |
6 | 6 |
|
7 | 7 | from .utils import ( |
8 | 8 | StrJSON, TestName, FilterTuple, |
9 | | - format_duration, normalize_test_name, print_warning, traceback_location) |
| 9 | + format_duration, normalize_test_name, print_warning, github_annotation) |
10 | 10 |
|
11 | 11 |
|
12 | 12 | @dataclasses.dataclass(slots=True) |
@@ -179,37 +179,34 @@ def __str__(self) -> str: |
179 | 179 | def has_meaningful_duration(self): |
180 | 180 | return State.has_meaningful_duration(self.state) |
181 | 181 |
|
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") |
| 182 | + def get_github_annotations(self) -> list[tuple[str, str]]: |
| 183 | + """(test case name, message) of each failed test case. |
193 | 184 |
|
| 185 | + Without test case details (crash, timeout, env changed, etc.), return |
| 186 | + the test name and the test result. |
| 187 | + """ |
| 188 | + if not os.environ.get("GITHUB_STEP_SUMMARY"): |
| 189 | + return [] |
194 | 190 | annotations = (self.errors or []) + (self.failures or []) |
195 | 191 | if not annotations: |
196 | | - # No test case details: crash, timeout, env changed, etc. |
197 | 192 | message = "\n".join([str(self), *(self.env_changed_reasons or ())]) |
198 | 193 | annotations = [(self.test_name, message)] |
199 | | - for title, message in annotations: |
200 | | - message = decolor(message) |
201 | | - props: dict[str, str | int] = {} |
202 | | - if location := traceback_location(message): |
203 | | - props |= location |
204 | | - # The job log only shows the message: start it with the location |
205 | | - position = ":".join(str(location[key]) |
206 | | - for key in ("file", "line", "col") |
207 | | - if key in location) |
208 | | - message = f"{position}\n{message}" |
209 | | - props["title"] = title |
210 | | - props_text = ",".join(f"{key}={escape_property(str(value))}" |
211 | | - for key, value in props.items()) |
212 | | - print(f"::error {props_text}::{escape(message)}", flush=True) |
| 194 | + return annotations |
| 195 | + |
| 196 | + def print_github_annotations(self) -> None: |
| 197 | + """Print a GitHub Actions error annotation per failed test case.""" |
| 198 | + for title, message in self.get_github_annotations(): |
| 199 | + print(github_annotation(title, message), flush=True) |
| 200 | + |
| 201 | + def print_github_crash_annotation(self, fail_env_changed: bool) -> None: |
| 202 | + """Annotate a failed test without test case failures. |
| 203 | +
|
| 204 | + Test case failures are annotated by the test runner, where they are |
| 205 | + reported (see RegressionTestResult.printErrorList()). |
| 206 | + """ |
| 207 | + if (self.is_failed(fail_env_changed) |
| 208 | + and not self.errors and not self.failures): |
| 209 | + self.print_github_annotations() |
213 | 210 |
|
214 | 211 | def set_env_changed(self, *reasons): |
215 | 212 | if self.state is None or self.state == State.PASSED: |
|
0 commit comments