codetool-shell 0.1.1__py3-none-win_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codetool_shell/__init__.py +11 -0
- codetool_shell/api.py +59 -0
- codetool_shell/bin/windows-arm64/codetool-shell-rust.exe +0 -0
- codetool_shell/filters/__init__.py +14 -0
- codetool_shell/filters/build_compiler/__init__.py +7 -0
- codetool_shell/filters/build_compiler/detector.py +412 -0
- codetool_shell/filters/build_compiler/reducer.py +166 -0
- codetool_shell/filters/build_compiler/summary.py +617 -0
- codetool_shell/filters/ci_job_log/__init__.py +7 -0
- codetool_shell/filters/ci_job_log/detector.py +64 -0
- codetool_shell/filters/ci_job_log/reducer.py +99 -0
- codetool_shell/filters/ci_job_log/summary.py +243 -0
- codetool_shell/filters/diff/__init__.py +7 -0
- codetool_shell/filters/diff/detector.py +136 -0
- codetool_shell/filters/diff/reducer.py +308 -0
- codetool_shell/filters/generic_log/__init__.py +7 -0
- codetool_shell/filters/generic_log/detector.py +175 -0
- codetool_shell/filters/generic_log/reducer.py +99 -0
- codetool_shell/filters/generic_log/summary.py +161 -0
- codetool_shell/filters/git.py +514 -0
- codetool_shell/filters/html_cleanup/__init__.py +7 -0
- codetool_shell/filters/html_cleanup/detector.py +136 -0
- codetool_shell/filters/html_cleanup/reducer.py +27 -0
- codetool_shell/filters/html_cleanup/summary.py +422 -0
- codetool_shell/filters/json_payload/__init__.py +7 -0
- codetool_shell/filters/json_payload/detector.py +62 -0
- codetool_shell/filters/json_payload/reducer.py +81 -0
- codetool_shell/filters/json_payload/summary.py +233 -0
- codetool_shell/filters/listing/__init__.py +7 -0
- codetool_shell/filters/listing/detector.py +294 -0
- codetool_shell/filters/listing/reducer.py +30 -0
- codetool_shell/filters/log_template/__init__.py +7 -0
- codetool_shell/filters/log_template/constants.py +76 -0
- codetool_shell/filters/log_template/detector.py +331 -0
- codetool_shell/filters/log_template/reducer.py +78 -0
- codetool_shell/filters/log_template/template.py +280 -0
- codetool_shell/filters/log_template/types.py +21 -0
- codetool_shell/filters/opaque_payload/__init__.py +7 -0
- codetool_shell/filters/opaque_payload/detector.py +563 -0
- codetool_shell/filters/opaque_payload/reducer.py +142 -0
- codetool_shell/filters/opaque_payload/summary.py +61 -0
- codetool_shell/filters/package_manager/__init__.py +7 -0
- codetool_shell/filters/package_manager/detector.py +220 -0
- codetool_shell/filters/package_manager/reducer.py +110 -0
- codetool_shell/filters/package_manager/summary.py +172 -0
- codetool_shell/filters/pipeline.py +65 -0
- codetool_shell/filters/rg.py +250 -0
- codetool_shell/filters/system_output/__init__.py +7 -0
- codetool_shell/filters/system_output/detector.py +600 -0
- codetool_shell/filters/system_output/reducer.py +331 -0
- codetool_shell/filters/system_output/summary.py +164 -0
- codetool_shell/filters/table/__init__.py +7 -0
- codetool_shell/filters/table/detector.py +244 -0
- codetool_shell/filters/table/reducer.py +57 -0
- codetool_shell/filters/table/summary.py +37 -0
- codetool_shell/filters/test_runner/__init__.py +7 -0
- codetool_shell/filters/test_runner/ansi.py +80 -0
- codetool_shell/filters/test_runner/detector.py +409 -0
- codetool_shell/filters/test_runner/reducer.py +288 -0
- codetool_shell/filters/test_runner/summary.py +449 -0
- codetool_shell/filters/text.py +38 -0
- codetool_shell/filters/traceback/__init__.py +7 -0
- codetool_shell/filters/traceback/detector.py +209 -0
- codetool_shell/filters/traceback/reducer.py +141 -0
- codetool_shell/filters/traceback/summary.py +122 -0
- codetool_shell/filters/tree.py +59 -0
- codetool_shell/py.typed +0 -0
- codetool_shell/python_backend.py +38 -0
- codetool_shell/rust_backend.py +254 -0
- codetool_shell-0.1.1.dist-info/METADATA +152 -0
- codetool_shell-0.1.1.dist-info/RECORD +72 -0
- codetool_shell-0.1.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
"""Line classification for test-runner summaries and diagnostics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def is_summary_line(line: str) -> bool:
|
|
9
|
+
"""Return true for compact test-runner summary/timing lines."""
|
|
10
|
+
|
|
11
|
+
stripped = line.strip()
|
|
12
|
+
return (
|
|
13
|
+
_is_pytest_terminal_summary(stripped)
|
|
14
|
+
or _is_pytest_quiet_summary(stripped)
|
|
15
|
+
or stripped.startswith("collected ")
|
|
16
|
+
or _is_cargo_running_line(stripped)
|
|
17
|
+
or _is_go_summary_line(stripped)
|
|
18
|
+
or _is_unittest_ran_line(stripped)
|
|
19
|
+
or _is_unittest_terminal_outcome_line(stripped)
|
|
20
|
+
or is_playwright_run_line(stripped)
|
|
21
|
+
or is_playwright_summary_line(stripped)
|
|
22
|
+
or _is_jest_summary_line(stripped)
|
|
23
|
+
or _is_maven_surefire_summary_line(stripped)
|
|
24
|
+
or _is_gradle_test_summary_line(stripped)
|
|
25
|
+
or _is_dotnet_test_summary_line(stripped)
|
|
26
|
+
or _is_rspec_summary_line(stripped)
|
|
27
|
+
or _is_minitest_summary_line(stripped)
|
|
28
|
+
or stripped.startswith(("Test Files", "Tests", "Duration", "Start at"))
|
|
29
|
+
or stripped.startswith("test result:")
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_run_marker_line(line: str) -> bool:
|
|
34
|
+
"""Return true for a concise framework run marker."""
|
|
35
|
+
|
|
36
|
+
stripped = line.lstrip()
|
|
37
|
+
return stripped.startswith("RUN ") or stripped.startswith("=== RUN ")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def is_failure_detail_line(line: str) -> bool:
|
|
41
|
+
"""Return true for failure identifiers, assertions, and source locations."""
|
|
42
|
+
|
|
43
|
+
stripped = line.strip()
|
|
44
|
+
pytest_heading = stripped.strip("_").strip()
|
|
45
|
+
location = stripped.removeprefix("❯").strip()
|
|
46
|
+
if _is_playwright_progress_line(stripped):
|
|
47
|
+
return False
|
|
48
|
+
return (
|
|
49
|
+
stripped.startswith(
|
|
50
|
+
(
|
|
51
|
+
"FAILED ",
|
|
52
|
+
"FAIL ",
|
|
53
|
+
"FAIL:",
|
|
54
|
+
"FAIL",
|
|
55
|
+
"ERROR ",
|
|
56
|
+
"ERROR:",
|
|
57
|
+
"AssertionError",
|
|
58
|
+
"Error:",
|
|
59
|
+
"ImportError",
|
|
60
|
+
"Traceback",
|
|
61
|
+
"thread '",
|
|
62
|
+
"failures:",
|
|
63
|
+
"--- FAIL:",
|
|
64
|
+
"--- SKIP:",
|
|
65
|
+
"Failed!",
|
|
66
|
+
"Failed tests:",
|
|
67
|
+
"Failures:",
|
|
68
|
+
"Pending:",
|
|
69
|
+
"Test Failed.",
|
|
70
|
+
"Expected:",
|
|
71
|
+
"Received:",
|
|
72
|
+
"Actual:",
|
|
73
|
+
"Failure/Error:",
|
|
74
|
+
"Message:",
|
|
75
|
+
"Messages:",
|
|
76
|
+
"Stacktrace:",
|
|
77
|
+
"[ERROR]",
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
or " FAILED" in stripped
|
|
81
|
+
or pytest_heading.startswith("ERROR ")
|
|
82
|
+
or _is_unittest_verbose_failure_line(stripped)
|
|
83
|
+
or _is_python_frame_line(stripped)
|
|
84
|
+
or _is_python_exception_line(stripped)
|
|
85
|
+
or _is_java_exception_line(stripped)
|
|
86
|
+
or _is_dotnet_failure_line(stripped)
|
|
87
|
+
or _is_rspec_failure_line(stripped)
|
|
88
|
+
or _is_minitest_failure_line(stripped)
|
|
89
|
+
or "test report" in stripped.lower()
|
|
90
|
+
or "report at:" in stripped.lower()
|
|
91
|
+
or _is_assert_or_raise_source_line(stripped)
|
|
92
|
+
or _is_playwright_failure_detail_line(stripped)
|
|
93
|
+
or _looks_like_path_location(location)
|
|
94
|
+
or line.startswith(("E ", "E ", "> ", " left:", " right:"))
|
|
95
|
+
or stripped.startswith(("left:", "right:"))
|
|
96
|
+
or (stripped.startswith("test ") and " ... FAILED" in stripped)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def normalize_selected_line(line: str) -> str:
|
|
101
|
+
"""Normalize selected lines for compact output."""
|
|
102
|
+
|
|
103
|
+
stripped = line.strip()
|
|
104
|
+
if _is_playwright_decorated_line(stripped):
|
|
105
|
+
return stripped.rstrip("─").rstrip()
|
|
106
|
+
return stripped
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def is_unittest_progress_line(line: str) -> bool:
|
|
110
|
+
"""Return true for compact unittest progress-marker lines."""
|
|
111
|
+
|
|
112
|
+
stripped = line.strip()
|
|
113
|
+
return len(stripped) >= 20 and all(
|
|
114
|
+
char in _UNITTEST_PROGRESS_CHARS for char in stripped
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def collapse_unittest_progress_line(line: str) -> str:
|
|
119
|
+
"""Summarize a long unittest progress-marker line."""
|
|
120
|
+
|
|
121
|
+
stripped = line.strip()
|
|
122
|
+
counts = [
|
|
123
|
+
_count_part(stripped.count("."), "passed"),
|
|
124
|
+
_count_part(stripped.count("s") + stripped.count("S"), "skipped"),
|
|
125
|
+
_count_part(
|
|
126
|
+
stripped.count("x") + stripped.count("X"),
|
|
127
|
+
"expected failures",
|
|
128
|
+
),
|
|
129
|
+
_count_part(stripped.count("u") + stripped.count("U"), "unexpected successes"),
|
|
130
|
+
_count_part(stripped.count("F"), "failed"),
|
|
131
|
+
_count_part(stripped.count("E"), "errors"),
|
|
132
|
+
]
|
|
133
|
+
details = ", ".join(part for part in counts if part)
|
|
134
|
+
if not details:
|
|
135
|
+
details = "markers only"
|
|
136
|
+
return f"progress: {len(stripped)} unittest markers ({details})"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def is_playwright_run_line(line: str) -> bool:
|
|
140
|
+
"""Return true for the Playwright run-count marker."""
|
|
141
|
+
|
|
142
|
+
return _PLAYWRIGHT_RUN_RE.match(line.strip()) is not None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def is_playwright_summary_line(line: str) -> bool:
|
|
146
|
+
"""Return true for strict Playwright final total lines."""
|
|
147
|
+
|
|
148
|
+
return _PLAYWRIGHT_SUMMARY_RE.match(line.strip()) is not None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def extract_playwright_project(line: str) -> str | None:
|
|
152
|
+
"""Return a visible Playwright project name from a spec line."""
|
|
153
|
+
|
|
154
|
+
match = _PLAYWRIGHT_PROJECT_RE.search(line)
|
|
155
|
+
if match is None:
|
|
156
|
+
return None
|
|
157
|
+
project = match.group(1).strip()
|
|
158
|
+
return project or None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def is_playwright_attention_line(line: str) -> bool:
|
|
162
|
+
"""Return true for non-failing Playwright retry/flaky identifiers."""
|
|
163
|
+
|
|
164
|
+
stripped = line.strip()
|
|
165
|
+
return "retry #" in stripped.lower() or _is_playwright_final_spec_line(stripped)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _is_pytest_terminal_summary(line: str) -> bool:
|
|
169
|
+
return bool(
|
|
170
|
+
line.startswith("=")
|
|
171
|
+
and " in " in line
|
|
172
|
+
and any(
|
|
173
|
+
token in line
|
|
174
|
+
for token in (" passed", " failed", " error", " skipped", " xfailed", " xpassed")
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _is_pytest_quiet_summary(line: str) -> bool:
|
|
180
|
+
return _PYTEST_QUIET_SUMMARY_RE.match(line) is not None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _is_cargo_running_line(line: str) -> bool:
|
|
184
|
+
return re.match(r"^running \d+ tests?$", line) is not None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _is_unittest_ran_line(line: str) -> bool:
|
|
188
|
+
return _UNITTEST_RAN_RE.match(line) is not None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _is_unittest_terminal_outcome_line(line: str) -> bool:
|
|
192
|
+
return line == "OK" or line.startswith(("OK (", "FAILED ("))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _is_unittest_verbose_failure_line(line: str) -> bool:
|
|
196
|
+
return bool(
|
|
197
|
+
_UNITTEST_VERBOSE_RESULT_RE.match(line)
|
|
198
|
+
and any(line.endswith(f" ... {result}") for result in ("FAIL", "ERROR"))
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _is_python_frame_line(line: str) -> bool:
|
|
203
|
+
return line.startswith('File "') and '", line ' in line
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _is_python_exception_line(line: str) -> bool:
|
|
207
|
+
match = _PYTHON_EXCEPTION_RE.match(line)
|
|
208
|
+
if match is None:
|
|
209
|
+
return False
|
|
210
|
+
exception_name = match.group(1)
|
|
211
|
+
return exception_name.endswith(("Error", "Exception", "Warning"))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _is_java_exception_line(line: str) -> bool:
|
|
215
|
+
return bool(_JAVA_EXCEPTION_RE.match(line) or line.startswith("Caused by: "))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _is_dotnet_failure_line(line: str) -> bool:
|
|
219
|
+
return (
|
|
220
|
+
line.startswith(("Failed ", "Error Message:", "Stack Trace:", "at "))
|
|
221
|
+
or _DOTNET_FAILED_TEST_RE.match(line) is not None
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _is_rspec_failure_line(line: str) -> bool:
|
|
226
|
+
return (
|
|
227
|
+
_RSPEC_FAILURE_NUMBER_RE.match(line) is not None
|
|
228
|
+
or line.startswith(("# ./", "# /", "rspec "))
|
|
229
|
+
or line.startswith(("Failure/Error:", "expected", "got:"))
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def is_jest_source_context_line(line: str) -> bool:
|
|
234
|
+
return _JEST_SOURCE_CONTEXT_RE.match(line.strip()) is not None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _is_minitest_failure_line(line: str) -> bool:
|
|
238
|
+
return (
|
|
239
|
+
_MINITEST_FAILURE_HEADER_RE.match(line) is not None
|
|
240
|
+
or ("[" in line and ".rb:" in line and line.endswith("]:"))
|
|
241
|
+
or line.startswith(("Expected", "Actual", "Failure:", "Error:"))
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _is_assert_or_raise_source_line(line: str) -> bool:
|
|
246
|
+
return line.startswith(("assert ", "self.assert", "raise "))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _is_playwright_failure_detail_line(line: str) -> bool:
|
|
250
|
+
return (
|
|
251
|
+
_PLAYWRIGHT_NUMBERED_FAILURE_RE.match(line) is not None
|
|
252
|
+
or _is_playwright_final_spec_line(line)
|
|
253
|
+
or line.startswith(
|
|
254
|
+
(
|
|
255
|
+
"Test timeout",
|
|
256
|
+
"TimeoutError:",
|
|
257
|
+
"Locator:",
|
|
258
|
+
"Expected:",
|
|
259
|
+
"Received:",
|
|
260
|
+
"Timeout:",
|
|
261
|
+
"Call log:",
|
|
262
|
+
"attachment #",
|
|
263
|
+
"Error Context:",
|
|
264
|
+
"Usage:",
|
|
265
|
+
"npx playwright show-trace",
|
|
266
|
+
"locator.",
|
|
267
|
+
"page.",
|
|
268
|
+
"expect.",
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
or _is_playwright_action_line(line)
|
|
272
|
+
or _is_playwright_source_focus_line(line)
|
|
273
|
+
or _looks_like_artifact_path(line)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _is_playwright_decorated_line(line: str) -> bool:
|
|
278
|
+
return "─" in line and (
|
|
279
|
+
_PLAYWRIGHT_NUMBERED_FAILURE_RE.match(line) is not None
|
|
280
|
+
or line.startswith("attachment #")
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _is_playwright_final_spec_line(line: str) -> bool:
|
|
285
|
+
return line.startswith("[") and "] ›" in line
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _is_playwright_progress_line(line: str) -> bool:
|
|
289
|
+
return line.startswith(("✓", "✘", "×", "-")) and "] ›" in line
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _is_playwright_action_line(line: str) -> bool:
|
|
293
|
+
return line.startswith("- ") and any(
|
|
294
|
+
token in line
|
|
295
|
+
for token in (
|
|
296
|
+
"expect",
|
|
297
|
+
"waiting for",
|
|
298
|
+
"locator",
|
|
299
|
+
"page.",
|
|
300
|
+
"click",
|
|
301
|
+
"fill",
|
|
302
|
+
"press",
|
|
303
|
+
"goto",
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _is_playwright_source_focus_line(line: str) -> bool:
|
|
309
|
+
return line.startswith(">") and "|" in line
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _looks_like_artifact_path(line: str) -> bool:
|
|
313
|
+
return (
|
|
314
|
+
"/" in line
|
|
315
|
+
and not line.startswith("- ")
|
|
316
|
+
and not line.startswith(("http://", "https://"))
|
|
317
|
+
and line.endswith((".png", ".jpg", ".jpeg", ".webm", ".zip", ".html"))
|
|
318
|
+
and (
|
|
319
|
+
"test-results/" in line
|
|
320
|
+
or "playwright-report/" in line
|
|
321
|
+
or "blob-report/" in line
|
|
322
|
+
or "trace" in line
|
|
323
|
+
or "screenshot" in line
|
|
324
|
+
or "video" in line
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _count_part(count: int, label: str) -> str:
|
|
330
|
+
if count == 0:
|
|
331
|
+
return ""
|
|
332
|
+
return f"{count} {label}"
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _looks_like_path_location(line: str) -> bool:
|
|
336
|
+
parts = line.split(":")
|
|
337
|
+
if len(parts) < 2:
|
|
338
|
+
return False
|
|
339
|
+
for index in range(1, len(parts)):
|
|
340
|
+
if parts[index].isdigit():
|
|
341
|
+
path = ":".join(parts[:index])
|
|
342
|
+
return bool(path) and _looks_like_path(path)
|
|
343
|
+
return False
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _looks_like_path(path: str) -> bool:
|
|
347
|
+
if not path or path[0].isspace() or "://" in path:
|
|
348
|
+
return False
|
|
349
|
+
return "/" in path or "\\" in path or "." in path
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _is_go_summary_line(line: str) -> bool:
|
|
353
|
+
return (
|
|
354
|
+
_GO_TEST_RESULT_RE.match(line) is not None
|
|
355
|
+
or _GO_PACKAGE_RESULT_RE.match(line) is not None
|
|
356
|
+
or line in {"PASS", "FAIL"}
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _is_jest_summary_line(line: str) -> bool:
|
|
361
|
+
return (
|
|
362
|
+
line.startswith(("PASS ", "FAIL ", "Test Suites:", "Tests:", "Snapshots:", "Time:"))
|
|
363
|
+
or _JEST_FAILED_TEST_RE.match(line) is not None
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _is_maven_surefire_summary_line(line: str) -> bool:
|
|
368
|
+
return (
|
|
369
|
+
_MAVEN_SUREFIRE_SUMMARY_RE.match(line) is not None
|
|
370
|
+
or line.startswith("Results:")
|
|
371
|
+
or "surefire-reports" in line
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _is_gradle_test_summary_line(line: str) -> bool:
|
|
376
|
+
return (
|
|
377
|
+
_GRADLE_TEST_SUMMARY_RE.match(line) is not None
|
|
378
|
+
or (line.startswith("> Task ") and "FAILED" in line)
|
|
379
|
+
or "test report" in line.lower()
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _is_dotnet_test_summary_line(line: str) -> bool:
|
|
384
|
+
return (
|
|
385
|
+
line.startswith(("Passed!", "Failed!", "Total tests:", "Test Run "))
|
|
386
|
+
or _DOTNET_TEST_COUNT_RE.match(line) is not None
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _is_rspec_summary_line(line: str) -> bool:
|
|
391
|
+
return (
|
|
392
|
+
_RSPEC_SUMMARY_RE.match(line) is not None
|
|
393
|
+
or line.startswith(("Finished in ", "Failures:", "Pending:"))
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _is_minitest_summary_line(line: str) -> bool:
|
|
398
|
+
return _MINITEST_SUMMARY_RE.match(line) is not None
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
_UNITTEST_RAN_RE = re.compile(r"^Ran \d+ tests? in [0-9.]+s$")
|
|
402
|
+
_PYTEST_QUIET_SUMMARY_RE = re.compile(
|
|
403
|
+
r"^(?:\d+ (?:passed|failed|errors?|skipped|xfailed|xpassed|warnings?)"
|
|
404
|
+
r"(?:, \d+ (?:passed|failed|errors?|skipped|xfailed|xpassed|warnings?))*"
|
|
405
|
+
r"|no tests ran) in [0-9.]+s$"
|
|
406
|
+
)
|
|
407
|
+
_UNITTEST_VERBOSE_RESULT_RE = re.compile(
|
|
408
|
+
r"^[A-Za-z_][\w.<>-]*\s+\(.+\)\s+\.\.\.\s+"
|
|
409
|
+
r"(?:ok|FAIL|ERROR|skipped|expected failure|unexpected success)(?:\s+.*)?$"
|
|
410
|
+
)
|
|
411
|
+
_UNITTEST_PROGRESS_CHARS = frozenset(".FEsxXuUS")
|
|
412
|
+
_PYTHON_EXCEPTION_RE = re.compile(r"^((?:[A-Za-z_]\w*\.)*[A-Za-z_]\w*):")
|
|
413
|
+
_JAVA_EXCEPTION_RE = re.compile(r"^(?:[A-Za-z_$][\w$]*\.)+[A-Za-z_$][\w$]*(?:Exception|Error):")
|
|
414
|
+
_PLAYWRIGHT_RUN_RE = re.compile(r"^Running \d+ tests? using \d+ workers?$")
|
|
415
|
+
_PLAYWRIGHT_SUMMARY_RE = re.compile(
|
|
416
|
+
r"^\d+ (?:passed|failed|flaky|skipped|timed out|did not run|interrupted)"
|
|
417
|
+
r"(?: \([^)]*\))?$"
|
|
418
|
+
)
|
|
419
|
+
_PLAYWRIGHT_PROJECT_RE = re.compile(r"\[([^\]]+)\]\s+›")
|
|
420
|
+
_PLAYWRIGHT_NUMBERED_FAILURE_RE = re.compile(r"^\d+\) .+›")
|
|
421
|
+
_GO_TEST_RESULT_RE = re.compile(r"^--- (?:PASS|FAIL|SKIP):\s+\S+\s+\([0-9.]+s\)$")
|
|
422
|
+
_GO_PACKAGE_RESULT_RE = re.compile(
|
|
423
|
+
r"^(?:ok|FAIL|\?)\s+\S+(?:\s+[0-9.]+s|\s+\[no test files\])$"
|
|
424
|
+
)
|
|
425
|
+
_JEST_FAILED_TEST_RE = re.compile(r"^[✕●]\s+")
|
|
426
|
+
_JEST_SOURCE_CONTEXT_RE = re.compile(r"^(?:>?\s*)?\d+\s+\|.*|^\|\s*\^+")
|
|
427
|
+
_MAVEN_SUREFIRE_SUMMARY_RE = re.compile(
|
|
428
|
+
r"^(?:\[ERROR\]\s+)?Tests run:\s+\d+,\s+Failures:\s+\d+,\s+Errors:\s+\d+,\s+Skipped:\s+\d+",
|
|
429
|
+
re.IGNORECASE,
|
|
430
|
+
)
|
|
431
|
+
_GRADLE_TEST_SUMMARY_RE = re.compile(
|
|
432
|
+
r"^\d+\s+tests? completed(?:,\s+\d+\s+failed)?(?:,\s+\d+\s+skipped)?$",
|
|
433
|
+
re.IGNORECASE,
|
|
434
|
+
)
|
|
435
|
+
_DOTNET_TEST_COUNT_RE = re.compile(
|
|
436
|
+
r"^(?:Passed|Failed|Skipped):\s+\d+",
|
|
437
|
+
re.IGNORECASE,
|
|
438
|
+
)
|
|
439
|
+
_DOTNET_FAILED_TEST_RE = re.compile(r"^Failed\s+\S")
|
|
440
|
+
_RSPEC_SUMMARY_RE = re.compile(
|
|
441
|
+
r"^\d+\s+examples?,\s+\d+\s+failures?(?:,\s+\d+\s+pending)?$",
|
|
442
|
+
re.IGNORECASE,
|
|
443
|
+
)
|
|
444
|
+
_RSPEC_FAILURE_NUMBER_RE = re.compile(r"^\d+\)\s+")
|
|
445
|
+
_MINITEST_SUMMARY_RE = re.compile(
|
|
446
|
+
r"^\d+\s+runs?,\s+\d+\s+assertions?,\s+\d+\s+failures?,\s+\d+\s+errors?",
|
|
447
|
+
re.IGNORECASE,
|
|
448
|
+
)
|
|
449
|
+
_MINITEST_FAILURE_HEADER_RE = re.compile(r"^\d+\)\s+(?:Failure|Error):")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Small text helpers shared by output filters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def score(text: str) -> tuple[int, int]:
|
|
7
|
+
"""Return a cheap size proxy used to keep only smaller filter output."""
|
|
8
|
+
|
|
9
|
+
return (len(text.encode("utf-8")), len(text))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def split_preserving_final_newline(text: str) -> tuple[list[str], bool]:
|
|
13
|
+
final_newline = text.endswith("\n")
|
|
14
|
+
body = text[:-1] if final_newline else text
|
|
15
|
+
if body == "":
|
|
16
|
+
return [], final_newline
|
|
17
|
+
return body.split("\n"), final_newline
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def join_preserving_final_newline(lines: list[str], final_newline: bool) -> str:
|
|
21
|
+
output = "\n".join(lines)
|
|
22
|
+
if final_newline:
|
|
23
|
+
output += "\n"
|
|
24
|
+
return output
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def choose_smaller_lines(original: list[str], candidate: list[str]) -> list[str]:
|
|
28
|
+
original_text = "\n".join(original)
|
|
29
|
+
candidate_text = "\n".join(candidate)
|
|
30
|
+
if score(candidate_text) < score(original_text):
|
|
31
|
+
return candidate
|
|
32
|
+
return original
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def indent_lines(text: str) -> list[str]:
|
|
36
|
+
if not text:
|
|
37
|
+
return []
|
|
38
|
+
return [f" {line}" if line else "" for line in text.splitlines()]
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Conservative parser for standalone Python tracebacks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from .summary import (
|
|
9
|
+
is_chain_separator,
|
|
10
|
+
is_context_line,
|
|
11
|
+
is_traceback_header,
|
|
12
|
+
is_unindented_exception_summary,
|
|
13
|
+
normalize_context_line,
|
|
14
|
+
normalize_selected_line,
|
|
15
|
+
parse_frame_line,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class TracebackFrame:
|
|
21
|
+
"""A parsed traceback frame with optional source/caret context."""
|
|
22
|
+
|
|
23
|
+
line: str
|
|
24
|
+
path: str
|
|
25
|
+
context: tuple[str, ...] = ()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class TracebackBlock:
|
|
30
|
+
"""One Python traceback block ending in an exception summary."""
|
|
31
|
+
|
|
32
|
+
start: int
|
|
33
|
+
end: int
|
|
34
|
+
frames: tuple[TracebackFrame, ...]
|
|
35
|
+
exception: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class TracebackSeparator:
|
|
40
|
+
"""A chained-exception separator line."""
|
|
41
|
+
|
|
42
|
+
start: int
|
|
43
|
+
end: int
|
|
44
|
+
line: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
TracebackEvent = TracebackBlock | TracebackSeparator
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class TracebackParse:
|
|
52
|
+
"""Parsed traceback events in source order."""
|
|
53
|
+
|
|
54
|
+
events: tuple[TracebackEvent, ...]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def parse_python_traceback(lines: list[str]) -> TracebackParse | None:
|
|
58
|
+
"""Return parsed standalone traceback events, or ``None`` if uncertain."""
|
|
59
|
+
|
|
60
|
+
if _has_other_filter_ownership(lines):
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
events: list[TracebackEvent] = []
|
|
64
|
+
invalid_header = False
|
|
65
|
+
index = 0
|
|
66
|
+
while index < len(lines):
|
|
67
|
+
stripped = lines[index].strip()
|
|
68
|
+
if is_traceback_header(stripped):
|
|
69
|
+
parsed = _parse_block(lines, index)
|
|
70
|
+
if parsed is None:
|
|
71
|
+
invalid_header = True
|
|
72
|
+
break
|
|
73
|
+
events.append(parsed)
|
|
74
|
+
index = parsed.end
|
|
75
|
+
continue
|
|
76
|
+
if is_chain_separator(stripped):
|
|
77
|
+
events.append(
|
|
78
|
+
TracebackSeparator(
|
|
79
|
+
start=index,
|
|
80
|
+
end=index + 1,
|
|
81
|
+
line=normalize_selected_line(lines[index]),
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
index += 1
|
|
85
|
+
|
|
86
|
+
blocks = [event for event in events if isinstance(event, TracebackBlock)]
|
|
87
|
+
if invalid_header or not blocks:
|
|
88
|
+
return None
|
|
89
|
+
return TracebackParse(tuple(_drop_unanchored_separators(events)))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _parse_block(lines: list[str], start: int) -> TracebackBlock | None:
|
|
93
|
+
frames: list[TracebackFrame] = []
|
|
94
|
+
index = start + 1
|
|
95
|
+
|
|
96
|
+
while index < len(lines):
|
|
97
|
+
stripped = lines[index].strip()
|
|
98
|
+
if not stripped:
|
|
99
|
+
index += 1
|
|
100
|
+
continue
|
|
101
|
+
if is_traceback_header(stripped) or is_chain_separator(stripped):
|
|
102
|
+
break
|
|
103
|
+
|
|
104
|
+
parsed_frame = parse_frame_line(lines[index])
|
|
105
|
+
if parsed_frame is None:
|
|
106
|
+
if frames and is_unindented_exception_summary(lines[index]):
|
|
107
|
+
return TracebackBlock(
|
|
108
|
+
start=start,
|
|
109
|
+
end=index + 1,
|
|
110
|
+
frames=tuple(frames),
|
|
111
|
+
exception=normalize_selected_line(lines[index]),
|
|
112
|
+
)
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
index += 1
|
|
116
|
+
context: list[str] = []
|
|
117
|
+
while index < len(lines):
|
|
118
|
+
inner = lines[index].strip()
|
|
119
|
+
if not inner:
|
|
120
|
+
index += 1
|
|
121
|
+
continue
|
|
122
|
+
if (
|
|
123
|
+
is_traceback_header(inner)
|
|
124
|
+
or is_chain_separator(inner)
|
|
125
|
+
or parse_frame_line(lines[index]) is not None
|
|
126
|
+
):
|
|
127
|
+
break
|
|
128
|
+
if is_unindented_exception_summary(lines[index]):
|
|
129
|
+
frames.append(
|
|
130
|
+
TracebackFrame(
|
|
131
|
+
line=parsed_frame.line,
|
|
132
|
+
path=parsed_frame.path,
|
|
133
|
+
context=tuple(context),
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
return TracebackBlock(
|
|
137
|
+
start=start,
|
|
138
|
+
end=index + 1,
|
|
139
|
+
frames=tuple(frames),
|
|
140
|
+
exception=normalize_selected_line(lines[index]),
|
|
141
|
+
)
|
|
142
|
+
if is_context_line(lines[index]):
|
|
143
|
+
context.append(normalize_context_line(lines[index]))
|
|
144
|
+
index += 1
|
|
145
|
+
continue
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
frames.append(
|
|
149
|
+
TracebackFrame(
|
|
150
|
+
line=parsed_frame.line,
|
|
151
|
+
path=parsed_frame.path,
|
|
152
|
+
context=tuple(context),
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _drop_unanchored_separators(events: list[TracebackEvent]) -> list[TracebackEvent]:
|
|
160
|
+
output: list[TracebackEvent] = []
|
|
161
|
+
for position, event in enumerate(events):
|
|
162
|
+
if isinstance(event, TracebackBlock):
|
|
163
|
+
output.append(event)
|
|
164
|
+
continue
|
|
165
|
+
has_block_before = any(isinstance(item, TracebackBlock) for item in events[:position])
|
|
166
|
+
has_block_after = any(isinstance(item, TracebackBlock) for item in events[position + 1 :])
|
|
167
|
+
if has_block_before and has_block_after:
|
|
168
|
+
output.append(event)
|
|
169
|
+
return output
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _has_other_filter_ownership(lines: list[str]) -> bool:
|
|
173
|
+
stripped = [line.strip() for line in lines]
|
|
174
|
+
lowered_text = "\n".join(stripped).lower()
|
|
175
|
+
|
|
176
|
+
if any(line.startswith(("test output:", "ci log:")) for line in stripped):
|
|
177
|
+
return True
|
|
178
|
+
if "test session starts" in lowered_text or "short test summary info" in lowered_text:
|
|
179
|
+
return True
|
|
180
|
+
if _has_unittest_shape(stripped):
|
|
181
|
+
return True
|
|
182
|
+
if any(_has_ci_marker(line) for line in lines):
|
|
183
|
+
return True
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _has_unittest_shape(lines: list[str]) -> bool:
|
|
188
|
+
return (
|
|
189
|
+
any(_UNITTEST_RAN_RE.match(line) is not None for line in lines)
|
|
190
|
+
and any(line == "OK" or line.startswith(("OK (", "FAILED (")) for line in lines)
|
|
191
|
+
and any(
|
|
192
|
+
line.startswith(("FAIL:", "ERROR:"))
|
|
193
|
+
or (len(line) >= 5 and set(line) == {"-"})
|
|
194
|
+
for line in lines
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _has_ci_marker(line: str) -> bool:
|
|
200
|
+
stripped = line.strip()
|
|
201
|
+
return (
|
|
202
|
+
"##[" in stripped
|
|
203
|
+
or stripped.startswith(("::error", "::warning", "::group::", "::endgroup::"))
|
|
204
|
+
or bool(_GHA_TIMESTAMP_PREFIX_RE.match(line))
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
_UNITTEST_RAN_RE = re.compile(r"^Ran \d+ tests? in [0-9.]+s$")
|
|
209
|
+
_GHA_TIMESTAMP_PREFIX_RE = re.compile(r"^(?:[^\t]+\t[^\t]+\t)?\d{4}-\d{2}-\d{2}T.*Z ")
|