Skip to content

Commit b125f6f

Browse files
committed
Move to simpler regex traceback info extraction
1 parent c3cca61 commit b125f6f

2 files changed

Lines changed: 24 additions & 62 deletions

File tree

‎Lib/test/libregrtest/testresult.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,11 @@ def printErrorList(self, flavour, errors):
138138
for test, err in errors:
139139
# Precede each failure report with a GitHub Actions annotation,
140140
# so that the annotation links to the report in the job log
141-
self.stream.writeln(github_annotation(str(test), err))
141+
# Locate the annotation in the test file
142+
case = getattr(test, "test_case", test) # subTest()
143+
module = sys.modules.get(type(case).__module__)
144+
self.stream.writeln(github_annotation(
145+
str(test), err, getattr(module, "__file__", None)))
142146
super().printErrorList(flavour, [(test, err)])
143147

144148
def get_xml_element(self):

‎Lib/test/libregrtest/utils.py‎

Lines changed: 19 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import contextlib
22
import faulthandler
3-
import linecache
43
import locale
54
import math
65
import os.path
@@ -142,63 +141,13 @@ def print_warning(msg: str) -> None:
142141
orig_unraisablehook: Callable[..., None] | None = None
143142

144143

145-
def traceback_location(traceback: str) -> dict[str, str | int] | None:
146-
"""Location of the last frame of a traceback, relative to the repository.
147-
148-
Return a dict with file and line, plus col and endColumn when the
149-
traceback underlines the failing expression (~~~^^^).
150-
"""
151-
lines = traceback.splitlines()
152-
for index in reversed(range(len(lines))):
153-
if match := re.match(r' File "(.+)", line (\d+)', lines[index]):
154-
break
155-
else:
156-
return None
157-
filename, lineno = match.group(1), int(match.group(2))
158-
# Map the stdlib directory (Lib/ in a source checkout or an installed
159-
# lib/python3.X/) to Lib/ in the repository
160-
stdlib_dir = os.path.dirname(os.__file__) + os.sep
161-
if not filename.startswith(stdlib_dir):
162-
return None
163-
relpath = filename.removeprefix(stdlib_dir).replace(os.sep, "/")
164-
location: dict[str, str | int] = {"file": f"Lib/{relpath}", "line": lineno}
165-
166-
# The frame is followed by the dedented source line (" code") and,
167-
# for a single-line expression, by a line of ~ and ^ markers.
168-
markers = lines[index + 2] if index + 2 < len(lines) else ""
169-
source = linecache.getline(filename, lineno)
170-
if markers.strip() and not markers.strip(" ~^") and source:
171-
indent = len(source) - len(source.lstrip())
172-
start = len(markers) - len(markers.lstrip())
173-
location["col"] = indent + start - 4 + 1
174-
location["endColumn"] = indent + len(markers.rstrip()) - 4
175-
return location
176-
177-
178-
def traceback_exception(traceback: str) -> str:
179-
"""The exception which ends a traceback: type, message and notes.
180-
181-
Return traceback unchanged if it has no frame.
182-
"""
183-
lines = traceback.splitlines()
184-
for index in reversed(range(len(lines))):
185-
if lines[index].startswith(' File "'):
186-
break
187-
else:
188-
return traceback
189-
# Skip the frame's indented source line and ~^ markers
190-
index += 1
191-
while index < len(lines) and lines[index].startswith(" "):
192-
index += 1
193-
return "\n".join(lines[index:])
194-
195-
196-
def github_annotation(title: str, message: str) -> str:
144+
def github_annotation(title: str, message: str,
145+
filename: str | None = None) -> str:
197146
"""Format a GitHub Actions error annotation.
198147
199-
message is a traceback or a failure description. Locate the annotation at
200-
the last frame of the traceback, if any, and only keep the exception which
201-
ends the traceback: the job log has the full traceback.
148+
message is a traceback or a failure description. Only keep the exception
149+
which ends the traceback: the job log has the full traceback. Locate the
150+
annotation at the last frame of the traceback in filename, if any.
202151
"""
203152
def escape(text: str) -> str:
204153
return (text.replace("%", "%25").replace("\r", "%0D")
@@ -208,12 +157,21 @@ def escape_property(text: str) -> str:
208157
return escape(text).replace(":", "%3A").replace(",", "%2C")
209158

210159
message = decolor(message)
211-
props: dict[str, str | int] = {}
212-
if location := traceback_location(message):
213-
props |= location
160+
props: dict[str, str] = {}
161+
# Map the stdlib directory (Lib/ in a source checkout or an installed
162+
# lib/python3.X/) to Lib/ in the repository
163+
stdlib_dir = os.path.dirname(os.__file__) + os.sep
164+
if filename and filename.startswith(stdlib_dir):
165+
lines = re.findall(rf'^ File "{re.escape(filename)}", line (\d+)',
166+
message, re.MULTILINE)
167+
if lines:
168+
relpath = filename.removeprefix(stdlib_dir).replace(os.sep, "/")
169+
props |= {"file": f"Lib/{relpath}", "line": lines[-1]}
214170
props["title"] = title
215-
message = traceback_exception(message).strip()
216-
props_text = ",".join(f"{key}={escape_property(str(value))}"
171+
# Strip the frames: each " File" line and its indented source lines
172+
message = re.split(r'^ File .*\n(?: .*\n)*', message,
173+
flags=re.MULTILINE)[-1].strip()
174+
props_text = ",".join(f"{key}={escape_property(value)}"
217175
for key, value in props.items())
218176
return f"::error {props_text}::{escape(message)}"
219177

0 commit comments

Comments
 (0)