codetool-shell 0.1.1__py3-none-win_amd64.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-x86_64/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,409 @@
|
|
|
1
|
+
"""Detect supported test-runner output shapes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestFramework(str, Enum):
|
|
11
|
+
"""Supported test-runner families."""
|
|
12
|
+
|
|
13
|
+
PYTEST = "pytest"
|
|
14
|
+
UNITTEST = "unittest"
|
|
15
|
+
VITEST = "vitest"
|
|
16
|
+
PLAYWRIGHT = "playwright"
|
|
17
|
+
CARGO = "cargo"
|
|
18
|
+
GO = "go"
|
|
19
|
+
JEST = "jest"
|
|
20
|
+
MAVEN_SUREFIRE = "maven-surefire"
|
|
21
|
+
GRADLE_TEST = "gradle-test"
|
|
22
|
+
DOTNET_TEST = "dotnet-test"
|
|
23
|
+
RSPEC = "rspec"
|
|
24
|
+
MINITEST = "minitest"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class TestRunSignal:
|
|
29
|
+
"""Detected test-runner family and outcome."""
|
|
30
|
+
|
|
31
|
+
framework: TestFramework
|
|
32
|
+
failed: bool
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def detect_test_run(lines: list[str]) -> TestRunSignal | None:
|
|
36
|
+
"""Return a strong test-runner signal, or ``None`` for uncertain text."""
|
|
37
|
+
|
|
38
|
+
framework = _detect_framework(lines)
|
|
39
|
+
if framework is None:
|
|
40
|
+
return None
|
|
41
|
+
return TestRunSignal(framework=framework, failed=_has_failure(lines, framework))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _detect_framework(lines: list[str]) -> TestFramework | None:
|
|
45
|
+
text = "\n".join(lines)
|
|
46
|
+
stripped = [line.strip() for line in lines]
|
|
47
|
+
|
|
48
|
+
if _has_pytest_shape(text, stripped):
|
|
49
|
+
return TestFramework.PYTEST
|
|
50
|
+
|
|
51
|
+
if _has_unittest_shape(stripped):
|
|
52
|
+
return TestFramework.UNITTEST
|
|
53
|
+
|
|
54
|
+
if any(line.lstrip().startswith("RUN ") for line in lines) and any(
|
|
55
|
+
line.startswith(("Test Files", "Tests", "FAIL ")) for line in stripped
|
|
56
|
+
):
|
|
57
|
+
return TestFramework.VITEST
|
|
58
|
+
|
|
59
|
+
if _has_playwright_shape(stripped):
|
|
60
|
+
return TestFramework.PLAYWRIGHT
|
|
61
|
+
|
|
62
|
+
if any(_is_cargo_running_line(line) for line in stripped) and any(
|
|
63
|
+
line.startswith("test result:") for line in stripped
|
|
64
|
+
):
|
|
65
|
+
return TestFramework.CARGO
|
|
66
|
+
|
|
67
|
+
if _has_go_test_shape(stripped):
|
|
68
|
+
return TestFramework.GO
|
|
69
|
+
|
|
70
|
+
if _has_jest_shape(stripped):
|
|
71
|
+
return TestFramework.JEST
|
|
72
|
+
|
|
73
|
+
if _has_maven_surefire_shape(stripped):
|
|
74
|
+
return TestFramework.MAVEN_SUREFIRE
|
|
75
|
+
|
|
76
|
+
if _has_gradle_test_shape(stripped):
|
|
77
|
+
return TestFramework.GRADLE_TEST
|
|
78
|
+
|
|
79
|
+
if _has_dotnet_test_shape(stripped):
|
|
80
|
+
return TestFramework.DOTNET_TEST
|
|
81
|
+
|
|
82
|
+
if _has_rspec_shape(stripped):
|
|
83
|
+
return TestFramework.RSPEC
|
|
84
|
+
|
|
85
|
+
if _has_minitest_shape(stripped):
|
|
86
|
+
return TestFramework.MINITEST
|
|
87
|
+
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _has_failure(lines: list[str], framework: TestFramework) -> bool:
|
|
92
|
+
if any(_line_has_failure(line) for line in lines):
|
|
93
|
+
return True
|
|
94
|
+
if framework is TestFramework.PYTEST:
|
|
95
|
+
return _has_pytest_error(lines)
|
|
96
|
+
if framework is TestFramework.PLAYWRIGHT:
|
|
97
|
+
return _has_playwright_failure(lines)
|
|
98
|
+
if framework in {
|
|
99
|
+
TestFramework.GO,
|
|
100
|
+
TestFramework.JEST,
|
|
101
|
+
TestFramework.MAVEN_SUREFIRE,
|
|
102
|
+
TestFramework.GRADLE_TEST,
|
|
103
|
+
TestFramework.DOTNET_TEST,
|
|
104
|
+
TestFramework.RSPEC,
|
|
105
|
+
TestFramework.MINITEST,
|
|
106
|
+
}:
|
|
107
|
+
return _has_extended_failure(lines)
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _has_pytest_error(lines: list[str]) -> bool:
|
|
112
|
+
return any(_NONZERO_PYTEST_ERROR_RE.search(line.strip()) is not None for line in lines)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _has_pytest_shape(text: str, lines: list[str]) -> bool:
|
|
116
|
+
if "test session starts" in text and any(
|
|
117
|
+
line.startswith("collected ") or _is_pytest_terminal_summary(line)
|
|
118
|
+
for line in lines
|
|
119
|
+
):
|
|
120
|
+
return True
|
|
121
|
+
return _has_pytest_quiet_shape(lines)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _has_pytest_quiet_shape(lines: list[str]) -> bool:
|
|
125
|
+
return any(_is_pytest_progress_line(line) for line in lines) and any(
|
|
126
|
+
_is_pytest_quiet_summary(line)
|
|
127
|
+
or line.startswith(("FAILED ", "ERROR "))
|
|
128
|
+
or "short test summary info" in line.lower()
|
|
129
|
+
for line in lines
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _line_has_failure(line: str) -> bool:
|
|
134
|
+
stripped = line.strip()
|
|
135
|
+
return (
|
|
136
|
+
stripped.startswith(("FAILED ", "FAILED (", "FAIL ", "FAIL:", "ERROR:", "failures:"))
|
|
137
|
+
or stripped.startswith("test result: FAILED")
|
|
138
|
+
or " FAILURES " in stripped
|
|
139
|
+
or _is_unittest_verbose_failure_line(stripped)
|
|
140
|
+
or _NONZERO_FAILED_RE.search(stripped) is not None
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _is_pytest_terminal_summary(line: str) -> bool:
|
|
145
|
+
return bool(
|
|
146
|
+
line.startswith("=")
|
|
147
|
+
and " in " in line
|
|
148
|
+
and any(
|
|
149
|
+
token in line
|
|
150
|
+
for token in (" passed", " failed", " error", " skipped", " xfailed", " xpassed")
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _is_pytest_quiet_summary(line: str) -> bool:
|
|
156
|
+
return _PYTEST_QUIET_SUMMARY_RE.match(line) is not None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _is_pytest_progress_line(line: str) -> bool:
|
|
160
|
+
return _PYTEST_PROGRESS_RE.match(line) is not None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _is_cargo_running_line(line: str) -> bool:
|
|
164
|
+
match = re.match(r"^running (\d+) tests?$", line)
|
|
165
|
+
return match is not None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _has_unittest_shape(lines: list[str]) -> bool:
|
|
169
|
+
return (
|
|
170
|
+
any(_is_unittest_ran_line(line) for line in lines)
|
|
171
|
+
and any(_is_unittest_terminal_outcome_line(line) for line in lines)
|
|
172
|
+
and any(_is_unittest_shape_marker(line) for line in lines)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _has_playwright_shape(lines: list[str]) -> bool:
|
|
177
|
+
return any(_is_playwright_run_line(line) for line in lines) and any(
|
|
178
|
+
_is_playwright_summary_line(line) for line in lines
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _has_playwright_failure(lines: list[str]) -> bool:
|
|
183
|
+
return any(_is_playwright_failure_line(line.strip()) for line in lines)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _has_extended_failure(lines: list[str]) -> bool:
|
|
187
|
+
return any(_is_extended_failure_line(line.strip()) for line in lines)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _is_playwright_run_line(line: str) -> bool:
|
|
191
|
+
return _PLAYWRIGHT_RUN_RE.match(line) is not None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _is_playwright_summary_line(line: str) -> bool:
|
|
195
|
+
return _PLAYWRIGHT_SUMMARY_RE.match(line) is not None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _is_playwright_failure_line(line: str) -> bool:
|
|
199
|
+
return bool(
|
|
200
|
+
_PLAYWRIGHT_NONZERO_FAILURE_RE.match(line)
|
|
201
|
+
or _PLAYWRIGHT_NUMBERED_FAILURE_RE.match(line)
|
|
202
|
+
or ((line.startswith("✘") or line.startswith("×")) and "›" in line)
|
|
203
|
+
or line.startswith(("Test timeout", "TimeoutError:"))
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _has_go_test_shape(lines: list[str]) -> bool:
|
|
208
|
+
has_run = any(_GO_RUN_RE.match(line) is not None for line in lines)
|
|
209
|
+
has_test_result = any(_GO_TEST_RESULT_RE.match(line) is not None for line in lines)
|
|
210
|
+
has_package_result = any(_looks_like_go_package_result(line) for line in lines)
|
|
211
|
+
return (has_run and has_test_result) or has_package_result
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _looks_like_go_package_result(line: str) -> bool:
|
|
215
|
+
match = _GO_PACKAGE_RESULT_RE.match(line)
|
|
216
|
+
if match is None:
|
|
217
|
+
return False
|
|
218
|
+
parts = line.split()
|
|
219
|
+
if len(parts) < 2:
|
|
220
|
+
return False
|
|
221
|
+
package = parts[1]
|
|
222
|
+
return "/" in package or "." in package or package.startswith(("./", "../"))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _has_jest_shape(lines: list[str]) -> bool:
|
|
226
|
+
has_suite = any(line.startswith(("PASS ", "FAIL ")) for line in lines)
|
|
227
|
+
has_summary = any(
|
|
228
|
+
line.startswith(("Test Suites:", "Tests:", "Snapshots:", "Time:"))
|
|
229
|
+
for line in lines
|
|
230
|
+
)
|
|
231
|
+
return has_suite and has_summary
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _has_maven_surefire_shape(lines: list[str]) -> bool:
|
|
235
|
+
has_surefire = any(
|
|
236
|
+
"surefire" in line.lower()
|
|
237
|
+
or "maven-surefire-plugin" in line.lower()
|
|
238
|
+
or line.startswith("Tests run:")
|
|
239
|
+
or _MAVEN_SUREFIRE_SUMMARY_RE.match(line) is not None
|
|
240
|
+
for line in lines
|
|
241
|
+
)
|
|
242
|
+
has_summary = any(_MAVEN_SUREFIRE_SUMMARY_RE.match(line) is not None for line in lines)
|
|
243
|
+
return has_surefire and has_summary
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _has_gradle_test_shape(lines: list[str]) -> bool:
|
|
247
|
+
has_gradle = any(
|
|
248
|
+
line.startswith(("> Task ", "Gradle Test Executor", "Test worker"))
|
|
249
|
+
or "test report" in line.lower()
|
|
250
|
+
for line in lines
|
|
251
|
+
)
|
|
252
|
+
has_summary = any(_GRADLE_TEST_SUMMARY_RE.match(line) is not None for line in lines)
|
|
253
|
+
return has_gradle and has_summary
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _has_dotnet_test_shape(lines: list[str]) -> bool:
|
|
257
|
+
has_vstest = any(
|
|
258
|
+
line.startswith(("Test run for ", "Starting test execution"))
|
|
259
|
+
or "vstest" in line.lower()
|
|
260
|
+
for line in lines
|
|
261
|
+
)
|
|
262
|
+
has_summary = any(
|
|
263
|
+
line.startswith(("Passed!", "Failed!", "Total tests:"))
|
|
264
|
+
or _DOTNET_TEST_SUMMARY_RE.match(line) is not None
|
|
265
|
+
for line in lines
|
|
266
|
+
)
|
|
267
|
+
return has_vstest and has_summary
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _has_rspec_shape(lines: list[str]) -> bool:
|
|
271
|
+
has_summary = any(_RSPEC_SUMMARY_RE.match(line) is not None for line in lines)
|
|
272
|
+
has_marker = any(
|
|
273
|
+
line.startswith(("Failures:", "Pending:", "Finished in "))
|
|
274
|
+
or _RSPEC_FAILURE_NUMBER_RE.match(line) is not None
|
|
275
|
+
for line in lines
|
|
276
|
+
)
|
|
277
|
+
return has_summary and has_marker
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _has_minitest_shape(lines: list[str]) -> bool:
|
|
281
|
+
has_run_marker = any(line.startswith("Run options:") for line in lines)
|
|
282
|
+
has_summary = any(_MINITEST_SUMMARY_RE.match(line) is not None for line in lines)
|
|
283
|
+
has_progress = any(_MINITEST_PROGRESS_RE.match(line) is not None for line in lines)
|
|
284
|
+
return has_summary and (has_run_marker or has_progress)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _is_extended_failure_line(line: str) -> bool:
|
|
288
|
+
return (
|
|
289
|
+
line.startswith(
|
|
290
|
+
(
|
|
291
|
+
"--- FAIL:",
|
|
292
|
+
"FAIL",
|
|
293
|
+
"FAIL ",
|
|
294
|
+
"Failed!",
|
|
295
|
+
"Failed tests:",
|
|
296
|
+
"Failures:",
|
|
297
|
+
"Test Failed.",
|
|
298
|
+
"[ERROR]",
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
or " FAILED" in line
|
|
302
|
+
or " failed" in line
|
|
303
|
+
or "Failures: " in line
|
|
304
|
+
or "Errors: " in line
|
|
305
|
+
or _GO_PACKAGE_FAIL_RE.match(line) is not None
|
|
306
|
+
or _MAVEN_SUREFIRE_NONZERO_RE.search(line) is not None
|
|
307
|
+
or _DOTNET_FAILED_SUMMARY_RE.search(line) is not None
|
|
308
|
+
or _RSPEC_NONZERO_FAILURE_RE.match(line) is not None
|
|
309
|
+
or _MINITEST_NONZERO_FAILURE_RE.search(line) is not None
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _is_unittest_shape_marker(line: str) -> bool:
|
|
314
|
+
return (
|
|
315
|
+
_is_unittest_separator(line)
|
|
316
|
+
or _is_unittest_progress_line(line)
|
|
317
|
+
or _is_unittest_verbose_result_line(line)
|
|
318
|
+
or line.startswith(("FAIL:", "ERROR:"))
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _is_unittest_ran_line(line: str) -> bool:
|
|
323
|
+
return _UNITTEST_RAN_RE.match(line) is not None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _is_unittest_terminal_outcome_line(line: str) -> bool:
|
|
327
|
+
return line == "OK" or line.startswith(("OK (", "FAILED ("))
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _is_unittest_separator(line: str) -> bool:
|
|
331
|
+
return len(line) >= 5 and set(line) == {"-"}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _is_unittest_progress_line(line: str) -> bool:
|
|
335
|
+
return len(line) >= 3 and all(char in _UNITTEST_PROGRESS_CHARS for char in line)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _is_unittest_verbose_result_line(line: str) -> bool:
|
|
339
|
+
return _UNITTEST_VERBOSE_RESULT_RE.match(line) is not None
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _is_unittest_verbose_failure_line(line: str) -> bool:
|
|
343
|
+
return bool(
|
|
344
|
+
_UNITTEST_VERBOSE_RESULT_RE.match(line)
|
|
345
|
+
and any(line.endswith(f" ... {result}") for result in ("FAIL", "ERROR"))
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
_NONZERO_FAILED_RE = re.compile(r"\b[1-9]\d* failed\b")
|
|
350
|
+
_NONZERO_PYTEST_ERROR_RE = re.compile(r"\b[1-9]\d* errors?\b")
|
|
351
|
+
_PYTEST_PROGRESS_RE = re.compile(r"^[.FEfsxX]+(?:\s+\[\s*\d+%\])?$")
|
|
352
|
+
_PYTEST_QUIET_SUMMARY_RE = re.compile(
|
|
353
|
+
r"^(?:\d+ (?:passed|failed|errors?|skipped|xfailed|xpassed|warnings?)"
|
|
354
|
+
r"(?:, \d+ (?:passed|failed|errors?|skipped|xfailed|xpassed|warnings?))*"
|
|
355
|
+
r"|no tests ran) in [0-9.]+s$"
|
|
356
|
+
)
|
|
357
|
+
_UNITTEST_RAN_RE = re.compile(r"^Ran \d+ tests? in [0-9.]+s$")
|
|
358
|
+
_UNITTEST_VERBOSE_RESULT_RE = re.compile(
|
|
359
|
+
r"^[A-Za-z_][\w.<>-]*\s+\(.+\)\s+\.\.\.\s+"
|
|
360
|
+
r"(?:ok|FAIL|ERROR|skipped|expected failure|unexpected success)(?:\s+.*)?$"
|
|
361
|
+
)
|
|
362
|
+
_UNITTEST_PROGRESS_CHARS = frozenset(".FEsxXuUS")
|
|
363
|
+
_PLAYWRIGHT_RUN_RE = re.compile(r"^Running \d+ tests? using \d+ workers?$")
|
|
364
|
+
_PLAYWRIGHT_SUMMARY_RE = re.compile(
|
|
365
|
+
r"^\d+ (?:passed|failed|flaky|skipped|timed out|did not run|interrupted)"
|
|
366
|
+
r"(?: \([^)]*\))?$"
|
|
367
|
+
)
|
|
368
|
+
_PLAYWRIGHT_NONZERO_FAILURE_RE = re.compile(
|
|
369
|
+
r"^[1-9]\d* (?:failed|timed out|interrupted)(?: \([^)]*\))?$"
|
|
370
|
+
)
|
|
371
|
+
_PLAYWRIGHT_NUMBERED_FAILURE_RE = re.compile(r"^\d+\) .+›")
|
|
372
|
+
_GO_RUN_RE = re.compile(r"^=== RUN\s+\S+")
|
|
373
|
+
_GO_TEST_RESULT_RE = re.compile(r"^--- (?:PASS|FAIL|SKIP):\s+\S+\s+\([0-9.]+s\)$")
|
|
374
|
+
_GO_PACKAGE_RESULT_RE = re.compile(
|
|
375
|
+
r"^(?:ok|FAIL|\?)\s+\S+(?:\s+[0-9.]+s|\s+\[no test files\])$"
|
|
376
|
+
)
|
|
377
|
+
_GO_PACKAGE_FAIL_RE = re.compile(r"^FAIL\s+\S+(?:\s+[0-9.]+s)?$")
|
|
378
|
+
_MAVEN_SUREFIRE_SUMMARY_RE = re.compile(
|
|
379
|
+
r"^(?:\[ERROR\]\s+)?Tests run:\s+\d+,\s+Failures:\s+\d+,\s+Errors:\s+\d+,\s+Skipped:\s+\d+",
|
|
380
|
+
re.IGNORECASE,
|
|
381
|
+
)
|
|
382
|
+
_MAVEN_SUREFIRE_NONZERO_RE = re.compile(
|
|
383
|
+
r"(?:Failures|Errors):\s*[1-9]\d*",
|
|
384
|
+
re.IGNORECASE,
|
|
385
|
+
)
|
|
386
|
+
_GRADLE_TEST_SUMMARY_RE = re.compile(
|
|
387
|
+
r"^\d+\s+tests? completed(?:,\s+\d+\s+failed)?(?:,\s+\d+\s+skipped)?$",
|
|
388
|
+
re.IGNORECASE,
|
|
389
|
+
)
|
|
390
|
+
_DOTNET_TEST_SUMMARY_RE = re.compile(
|
|
391
|
+
r"^(?:Passed|Failed|Skipped):\s+\d+",
|
|
392
|
+
re.IGNORECASE,
|
|
393
|
+
)
|
|
394
|
+
_DOTNET_FAILED_SUMMARY_RE = re.compile(r"Failed:\s*[1-9]\d*", re.IGNORECASE)
|
|
395
|
+
_RSPEC_SUMMARY_RE = re.compile(
|
|
396
|
+
r"^\d+\s+examples?,\s+\d+\s+failures?(?:,\s+\d+\s+pending)?$",
|
|
397
|
+
re.IGNORECASE,
|
|
398
|
+
)
|
|
399
|
+
_RSPEC_FAILURE_NUMBER_RE = re.compile(r"^\d+\)\s+")
|
|
400
|
+
_RSPEC_NONZERO_FAILURE_RE = re.compile(r"^\d+\s+examples?,\s+[1-9]\d*\s+failures?", re.I)
|
|
401
|
+
_MINITEST_SUMMARY_RE = re.compile(
|
|
402
|
+
r"^\d+\s+runs?,\s+\d+\s+assertions?,\s+\d+\s+failures?,\s+\d+\s+errors?",
|
|
403
|
+
re.IGNORECASE,
|
|
404
|
+
)
|
|
405
|
+
_MINITEST_PROGRESS_RE = re.compile(r"^[.FESE]+$")
|
|
406
|
+
_MINITEST_NONZERO_FAILURE_RE = re.compile(
|
|
407
|
+
r"[1-9]\d*\s+(?:failures?|errors?)",
|
|
408
|
+
re.IGNORECASE,
|
|
409
|
+
)
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Conservative reducers for supported test-runner output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..text import join_preserving_final_newline, score, split_preserving_final_newline
|
|
9
|
+
from .ansi import strip_ansi_control_sequences
|
|
10
|
+
from .detector import TestFramework, TestRunSignal, detect_test_run
|
|
11
|
+
from .summary import (
|
|
12
|
+
collapse_unittest_progress_line,
|
|
13
|
+
extract_playwright_project,
|
|
14
|
+
is_failure_detail_line,
|
|
15
|
+
is_jest_source_context_line,
|
|
16
|
+
is_playwright_attention_line,
|
|
17
|
+
is_playwright_run_line,
|
|
18
|
+
is_run_marker_line,
|
|
19
|
+
is_summary_line,
|
|
20
|
+
is_unittest_progress_line,
|
|
21
|
+
normalize_selected_line,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def compress_test_runner_output(text: str) -> str:
|
|
26
|
+
"""Compress clear test-runner output while preserving diagnostics."""
|
|
27
|
+
|
|
28
|
+
sanitized = strip_ansi_control_sequences(text)
|
|
29
|
+
lines, final_newline = split_preserving_final_newline(sanitized)
|
|
30
|
+
go_jsonl_candidate = _reduce_go_jsonl_lines(lines)
|
|
31
|
+
if go_jsonl_candidate is not None:
|
|
32
|
+
candidate = join_preserving_final_newline(go_jsonl_candidate, final_newline)
|
|
33
|
+
if score(candidate) < score(text):
|
|
34
|
+
return candidate
|
|
35
|
+
|
|
36
|
+
signal = detect_test_run(lines)
|
|
37
|
+
if signal is None:
|
|
38
|
+
return text
|
|
39
|
+
|
|
40
|
+
candidate_lines = _reduce_lines(lines, signal)
|
|
41
|
+
if len(candidate_lines) < 2:
|
|
42
|
+
return text
|
|
43
|
+
|
|
44
|
+
candidate = join_preserving_final_newline(candidate_lines, final_newline)
|
|
45
|
+
if score(candidate) < score(text):
|
|
46
|
+
return candidate
|
|
47
|
+
return text
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _reduce_lines(lines: list[str], signal: TestRunSignal) -> list[str]:
|
|
51
|
+
if signal.framework is TestFramework.PLAYWRIGHT:
|
|
52
|
+
return _reduce_playwright_lines(lines, signal)
|
|
53
|
+
|
|
54
|
+
outcome = "failure" if signal.failed else "success"
|
|
55
|
+
selected = [f"test output: {signal.framework.value} {outcome}"]
|
|
56
|
+
|
|
57
|
+
for line in lines:
|
|
58
|
+
if (
|
|
59
|
+
signal.framework is TestFramework.UNITTEST
|
|
60
|
+
and not signal.failed
|
|
61
|
+
and is_unittest_progress_line(line)
|
|
62
|
+
):
|
|
63
|
+
selected.append(collapse_unittest_progress_line(line))
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
if signal.failed:
|
|
67
|
+
keep = is_summary_line(line) or is_failure_detail_line(line)
|
|
68
|
+
if signal.framework is TestFramework.JEST and is_jest_source_context_line(line):
|
|
69
|
+
keep = True
|
|
70
|
+
else:
|
|
71
|
+
keep = is_summary_line(line) or is_run_marker_line(line)
|
|
72
|
+
if keep:
|
|
73
|
+
selected.append(normalize_selected_line(line))
|
|
74
|
+
|
|
75
|
+
return _drop_blank_and_adjacent_duplicates(selected)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _reduce_playwright_lines(lines: list[str], signal: TestRunSignal) -> list[str]:
|
|
79
|
+
outcome = "failure" if signal.failed else "success"
|
|
80
|
+
selected = [f"test output: {signal.framework.value} {outcome}"]
|
|
81
|
+
projects = _playwright_projects(lines)
|
|
82
|
+
projects_added = False
|
|
83
|
+
|
|
84
|
+
for line in lines:
|
|
85
|
+
keep = False
|
|
86
|
+
if is_summary_line(line):
|
|
87
|
+
keep = True
|
|
88
|
+
elif signal.failed:
|
|
89
|
+
keep = is_failure_detail_line(line)
|
|
90
|
+
else:
|
|
91
|
+
keep = is_playwright_attention_line(line)
|
|
92
|
+
|
|
93
|
+
if not keep:
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
selected.append(normalize_selected_line(line))
|
|
97
|
+
if (
|
|
98
|
+
not projects_added
|
|
99
|
+
and projects
|
|
100
|
+
and is_playwright_run_line(line)
|
|
101
|
+
):
|
|
102
|
+
selected.append(f"projects: {', '.join(projects)}")
|
|
103
|
+
projects_added = True
|
|
104
|
+
|
|
105
|
+
return _drop_blank_and_adjacent_duplicates(selected)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _reduce_go_jsonl_lines(lines: list[str]) -> list[str] | None:
|
|
109
|
+
records = _parse_go_jsonl_records(lines)
|
|
110
|
+
if records is None:
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
failed = any(record.get("Action") == "fail" for record in records)
|
|
114
|
+
failed_tests = _go_jsonl_tests_by_action(records, "fail")
|
|
115
|
+
skipped_tests = _go_jsonl_tests_by_action(records, "skip")
|
|
116
|
+
selected = [f"test output: go {'failure' if failed else 'success'}"]
|
|
117
|
+
|
|
118
|
+
for record in records:
|
|
119
|
+
action = record.get("Action")
|
|
120
|
+
package = _string_field(record, "Package")
|
|
121
|
+
test = _string_field(record, "Test")
|
|
122
|
+
|
|
123
|
+
if action == "output":
|
|
124
|
+
output = _string_field(record, "Output")
|
|
125
|
+
if output is None:
|
|
126
|
+
continue
|
|
127
|
+
detail = output.rstrip("\n").strip()
|
|
128
|
+
if not detail:
|
|
129
|
+
continue
|
|
130
|
+
key = (package or "", test or "")
|
|
131
|
+
if (
|
|
132
|
+
key in failed_tests
|
|
133
|
+
and _is_go_jsonl_relevant_output(detail, failed=True)
|
|
134
|
+
) or (
|
|
135
|
+
key in skipped_tests
|
|
136
|
+
and _is_go_jsonl_relevant_output(detail, failed=False)
|
|
137
|
+
) or _is_go_jsonl_global_output(detail):
|
|
138
|
+
selected.append(detail)
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
if action in {"fail", "skip"} and test is not None:
|
|
142
|
+
selected.append(_format_go_jsonl_test_event(action, package, test, record))
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
if action in {"pass", "fail", "skip"} and package is not None and test is None:
|
|
146
|
+
selected.append(_format_go_jsonl_package_event(action, package, record))
|
|
147
|
+
|
|
148
|
+
return _drop_blank_and_adjacent_duplicates(selected)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _parse_go_jsonl_records(lines: list[str]) -> list[dict[str, Any]] | None:
|
|
152
|
+
records: list[dict[str, Any]] = []
|
|
153
|
+
for line in lines:
|
|
154
|
+
stripped = line.strip()
|
|
155
|
+
if not stripped:
|
|
156
|
+
continue
|
|
157
|
+
try:
|
|
158
|
+
parsed = json.loads(stripped)
|
|
159
|
+
except json.JSONDecodeError:
|
|
160
|
+
return None
|
|
161
|
+
if not isinstance(parsed, dict):
|
|
162
|
+
return None
|
|
163
|
+
action = parsed.get("Action")
|
|
164
|
+
if action not in _GO_JSONL_ACTIONS:
|
|
165
|
+
return None
|
|
166
|
+
if not isinstance(parsed.get("Package"), str) and not isinstance(
|
|
167
|
+
parsed.get("Test"), str
|
|
168
|
+
):
|
|
169
|
+
return None
|
|
170
|
+
records.append(parsed)
|
|
171
|
+
|
|
172
|
+
if len(records) < 3 or not _has_go_jsonl_test_signal(records):
|
|
173
|
+
return None
|
|
174
|
+
return records
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _has_go_jsonl_test_signal(records: list[dict[str, Any]]) -> bool:
|
|
178
|
+
has_terminal = any(record.get("Action") in {"pass", "fail", "skip"} for record in records)
|
|
179
|
+
has_output_marker = any(
|
|
180
|
+
record.get("Action") == "output"
|
|
181
|
+
and isinstance(record.get("Output"), str)
|
|
182
|
+
and record["Output"].strip().startswith(("=== RUN", "--- PASS:", "--- FAIL:", "--- SKIP:"))
|
|
183
|
+
for record in records
|
|
184
|
+
)
|
|
185
|
+
has_go_test_name = any(
|
|
186
|
+
isinstance(record.get("Test"), str)
|
|
187
|
+
and record["Test"].startswith(("Test", "Benchmark", "Example"))
|
|
188
|
+
for record in records
|
|
189
|
+
)
|
|
190
|
+
has_go_package = any(
|
|
191
|
+
isinstance(record.get("Package"), str)
|
|
192
|
+
and ("/" in record["Package"] or "." in record["Package"])
|
|
193
|
+
for record in records
|
|
194
|
+
)
|
|
195
|
+
return has_terminal and has_output_marker and (has_go_test_name or has_go_package)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _go_jsonl_tests_by_action(
|
|
199
|
+
records: list[dict[str, Any]], action: str
|
|
200
|
+
) -> set[tuple[str, str]]:
|
|
201
|
+
tests: set[tuple[str, str]] = set()
|
|
202
|
+
for record in records:
|
|
203
|
+
package = _string_field(record, "Package")
|
|
204
|
+
test = _string_field(record, "Test")
|
|
205
|
+
if record.get("Action") == action and test is not None:
|
|
206
|
+
tests.add((package or "", test))
|
|
207
|
+
return tests
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _format_go_jsonl_test_event(
|
|
211
|
+
action: Any, package: str | None, test: str, record: dict[str, Any]
|
|
212
|
+
) -> str:
|
|
213
|
+
label = "FAIL" if action == "fail" else "SKIP"
|
|
214
|
+
package_prefix = f"{package} " if package else ""
|
|
215
|
+
elapsed = _format_go_jsonl_elapsed(record)
|
|
216
|
+
return f"{label} {package_prefix}{test}{elapsed}"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _format_go_jsonl_package_event(
|
|
220
|
+
action: Any, package: str, record: dict[str, Any]
|
|
221
|
+
) -> str:
|
|
222
|
+
label = {"pass": "ok", "fail": "FAIL", "skip": "SKIP"}[action]
|
|
223
|
+
return f"{label} {package}{_format_go_jsonl_elapsed(record)}"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _format_go_jsonl_elapsed(record: dict[str, Any]) -> str:
|
|
227
|
+
elapsed = record.get("Elapsed")
|
|
228
|
+
if isinstance(elapsed, (int, float)):
|
|
229
|
+
return f" ({elapsed:g}s)"
|
|
230
|
+
return ""
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _is_go_jsonl_relevant_output(detail: str, *, failed: bool) -> bool:
|
|
234
|
+
if detail.startswith(("=== RUN", "=== PAUSE", "=== CONT", "--- PASS:")):
|
|
235
|
+
return False
|
|
236
|
+
if detail.startswith(("--- FAIL:", "--- SKIP:", "FAIL", "panic:", "fatal error:")):
|
|
237
|
+
return True
|
|
238
|
+
if detail.startswith(
|
|
239
|
+
(
|
|
240
|
+
"Error Trace:",
|
|
241
|
+
"Error:",
|
|
242
|
+
"Messages:",
|
|
243
|
+
"Expected:",
|
|
244
|
+
"Actual:",
|
|
245
|
+
"Received:",
|
|
246
|
+
"Not equal:",
|
|
247
|
+
)
|
|
248
|
+
):
|
|
249
|
+
return True
|
|
250
|
+
if ".go:" in detail:
|
|
251
|
+
return True
|
|
252
|
+
if not failed:
|
|
253
|
+
return "skip" in detail.lower()
|
|
254
|
+
return len(detail) <= 240
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _is_go_jsonl_global_output(detail: str) -> bool:
|
|
258
|
+
return detail == "FAIL" or detail.startswith(("FAIL\t", "ok \t", "? \t"))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _string_field(record: dict[str, Any], field: str) -> str | None:
|
|
262
|
+
value = record.get(field)
|
|
263
|
+
return value if isinstance(value, str) else None
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _playwright_projects(lines: list[str]) -> list[str]:
|
|
267
|
+
projects: list[str] = []
|
|
268
|
+
for line in lines:
|
|
269
|
+
project = extract_playwright_project(line)
|
|
270
|
+
if project is not None and project not in projects:
|
|
271
|
+
projects.append(project)
|
|
272
|
+
return projects
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _drop_blank_and_adjacent_duplicates(lines: list[str]) -> list[str]:
|
|
276
|
+
output: list[str] = []
|
|
277
|
+
for line in lines:
|
|
278
|
+
if not line:
|
|
279
|
+
continue
|
|
280
|
+
if output and output[-1] == line:
|
|
281
|
+
continue
|
|
282
|
+
output.append(line)
|
|
283
|
+
return output
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
_GO_JSONL_ACTIONS = frozenset(
|
|
287
|
+
{"start", "run", "pause", "cont", "pass", "bench", "fail", "output", "skip"}
|
|
288
|
+
)
|