toolplane-python-client 0.1.0__py3-none-any.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.
- toolplane/__init__.py +106 -0
- toolplane/common/__init__.py +93 -0
- toolplane/common/base_config.py +129 -0
- toolplane/common/base_connection_manager.py +171 -0
- toolplane/common/base_session_manager.py +321 -0
- toolplane/common/base_tool_manager.py +347 -0
- toolplane/common/constants.py +47 -0
- toolplane/common/utils.py +310 -0
- toolplane/core/__init__.py +67 -0
- toolplane/core/config.py +107 -0
- toolplane/core/connection.py +285 -0
- toolplane/core/errors.py +298 -0
- toolplane/core/machine.py +480 -0
- toolplane/core/request.py +775 -0
- toolplane/core/session.py +332 -0
- toolplane/core/session_context.py +514 -0
- toolplane/core/task.py +130 -0
- toolplane/core/tool.py +329 -0
- toolplane/http_core/__init__.py +37 -0
- toolplane/http_core/http_config.py +97 -0
- toolplane/http_core/http_connection.py +409 -0
- toolplane/http_core/http_machine.py +298 -0
- toolplane/http_core/http_request.py +748 -0
- toolplane/http_core/http_session.py +348 -0
- toolplane/http_core/http_session_context.py +491 -0
- toolplane/http_core/http_task.py +101 -0
- toolplane/http_core/http_tool.py +400 -0
- toolplane/interfaces/__init__.py +27 -0
- toolplane/interfaces/client_interface.py +122 -0
- toolplane/interfaces/connection_interface.py +193 -0
- toolplane/interfaces/event_interface.py +290 -0
- toolplane/interfaces/request_interface.py +439 -0
- toolplane/interfaces/session_interface.py +288 -0
- toolplane/interfaces/tool_interface.py +441 -0
- toolplane/proto/__init__.py +0 -0
- toolplane/proto/service_pb2.py +315 -0
- toolplane/proto/service_pb2_grpc.py +2240 -0
- toolplane/provider_cli.py +268 -0
- toolplane/provider_registry.py +77 -0
- toolplane/provider_runtime.py +302 -0
- toolplane/toolkits/__init__.py +0 -0
- toolplane/toolkits/standalone_tools/__init__.py +0 -0
- toolplane/toolkits/standalone_tools/create_directory.py +94 -0
- toolplane/toolkits/standalone_tools/create_file.py +124 -0
- toolplane/toolkits/standalone_tools/file_search.py +229 -0
- toolplane/toolkits/standalone_tools/grep_search.py +372 -0
- toolplane/toolkits/standalone_tools/launcher.py +146 -0
- toolplane/toolkits/standalone_tools/list_dir.py +395 -0
- toolplane/toolkits/standalone_tools/read_file.py +346 -0
- toolplane/toolkits/standalone_tools/replace_string_in_file.py +407 -0
- toolplane/toolkits/standalone_tools/run_tests.py +66 -0
- toolplane/toolkits/standalone_tools/semantic_search.py +485 -0
- toolplane/toolkits/standalone_tools/standalone_toolkit.py +979 -0
- toolplane/toolkits/standalone_tools/test_failure_analysis.py +618 -0
- toolplane/toolkits/standalone_tools/test_standalone_toolkit.py +517 -0
- toolplane/toolkits/swe/__init__.py +35 -0
- toolplane/toolkits/swe/create_directory.py +15 -0
- toolplane/toolkits/swe/create_file.py +15 -0
- toolplane/toolkits/swe/descriptions.py +273 -0
- toolplane/toolkits/swe/execute_bash.py +93 -0
- toolplane/toolkits/swe/file_editor.py +775 -0
- toolplane/toolkits/swe/file_search.py +16 -0
- toolplane/toolkits/swe/finish.py +50 -0
- toolplane/toolkits/swe/grep_search.py +19 -0
- toolplane/toolkits/swe/list_dir.py +407 -0
- toolplane/toolkits/swe/read_file.py +18 -0
- toolplane/toolkits/swe/replace_string_in_file.py +17 -0
- toolplane/toolkits/swe/search.py +260 -0
- toolplane/toolkits/swe/semantic_search.py +20 -0
- toolplane/toolkits/swe/str_replace_editor.py +647 -0
- toolplane/toolkits/swe/submit.py +29 -0
- toolplane/toolkits/swe/swe_toolkit.py +1296 -0
- toolplane/toolplane_client.py +686 -0
- toolplane/toolplane_http_client.py +681 -0
- toolplane/utils/__init__.py +3 -0
- toolplane/utils/schema.py +146 -0
- toolplane_python_client-0.1.0.dist-info/METADATA +543 -0
- toolplane_python_client-0.1.0.dist-info/RECORD +81 -0
- toolplane_python_client-0.1.0.dist-info/WHEEL +5 -0
- toolplane_python_client-0.1.0.dist-info/entry_points.txt +2 -0
- toolplane_python_client-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Description: Test failure analysis tool for examining and debugging test failures.
|
|
4
|
+
|
|
5
|
+
This tool analyzes test failures, provides detailed error analysis, and suggests
|
|
6
|
+
potential fixes based on common failure patterns.
|
|
7
|
+
|
|
8
|
+
Parameters:
|
|
9
|
+
test_output (string, optional): Path to test output file or direct test output.
|
|
10
|
+
test_framework (string, optional): Test framework used (pytest, unittest, jest, etc.).
|
|
11
|
+
verbose (boolean, optional): Show detailed analysis (default: False).
|
|
12
|
+
suggest_fixes (boolean, optional): Suggest potential fixes (default: True).
|
|
13
|
+
group_by_type (boolean, optional): Group failures by error type (default: True).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
from collections import defaultdict
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def analyze_test_failures(
|
|
27
|
+
test_output: str = None,
|
|
28
|
+
test_framework: str = None,
|
|
29
|
+
verbose: bool = False,
|
|
30
|
+
suggest_fixes: bool = True,
|
|
31
|
+
group_by_type: bool = True,
|
|
32
|
+
) -> Dict[str, Any]:
|
|
33
|
+
"""
|
|
34
|
+
Analyze test failures and provide detailed analysis.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
test_output: Path to test output file or direct output
|
|
38
|
+
test_framework: Test framework used
|
|
39
|
+
verbose: Show detailed analysis
|
|
40
|
+
suggest_fixes: Suggest potential fixes
|
|
41
|
+
group_by_type: Group failures by error type
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Dictionary containing analysis results
|
|
45
|
+
"""
|
|
46
|
+
# Get test output content
|
|
47
|
+
if test_output and os.path.exists(test_output):
|
|
48
|
+
with open(test_output, "r", encoding="utf-8", errors="ignore") as f:
|
|
49
|
+
output_content = f.read()
|
|
50
|
+
elif test_output:
|
|
51
|
+
output_content = test_output
|
|
52
|
+
else:
|
|
53
|
+
# Read from stdin
|
|
54
|
+
output_content = sys.stdin.read()
|
|
55
|
+
|
|
56
|
+
# Auto-detect test framework if not specified
|
|
57
|
+
if not test_framework:
|
|
58
|
+
test_framework = detect_test_framework(output_content)
|
|
59
|
+
|
|
60
|
+
# Parse failures based on framework
|
|
61
|
+
failures = parse_failures(output_content, test_framework)
|
|
62
|
+
|
|
63
|
+
# Analyze failures
|
|
64
|
+
analysis = analyze_failures(failures, suggest_fixes, group_by_type)
|
|
65
|
+
|
|
66
|
+
# Add metadata
|
|
67
|
+
analysis["metadata"] = {
|
|
68
|
+
"test_framework": test_framework,
|
|
69
|
+
"total_failures": len(failures),
|
|
70
|
+
"unique_error_types": len(set(f["error_type"] for f in failures)),
|
|
71
|
+
"verbose": verbose,
|
|
72
|
+
"suggest_fixes": suggest_fixes,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return analysis
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def detect_test_framework(output: str) -> str:
|
|
79
|
+
"""Auto-detect the test framework from output."""
|
|
80
|
+
output_lower = output.lower()
|
|
81
|
+
|
|
82
|
+
if "pytest" in output_lower or "::" in output:
|
|
83
|
+
return "pytest"
|
|
84
|
+
elif "unittest" in output_lower or "test_" in output:
|
|
85
|
+
return "unittest"
|
|
86
|
+
elif "jest" in output_lower or "describe(" in output:
|
|
87
|
+
return "jest"
|
|
88
|
+
elif "rspec" in output_lower or "describe " in output:
|
|
89
|
+
return "rspec"
|
|
90
|
+
elif "mocha" in output_lower:
|
|
91
|
+
return "mocha"
|
|
92
|
+
elif "phpunit" in output_lower:
|
|
93
|
+
return "phpunit"
|
|
94
|
+
elif "cargo test" in output_lower:
|
|
95
|
+
return "cargo"
|
|
96
|
+
elif "go test" in output_lower:
|
|
97
|
+
return "go"
|
|
98
|
+
else:
|
|
99
|
+
return "unknown"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def parse_failures(output: str, framework: str) -> List[Dict[str, Any]]:
|
|
103
|
+
"""Parse test failures based on the framework."""
|
|
104
|
+
if framework == "pytest":
|
|
105
|
+
return parse_pytest_failures(output)
|
|
106
|
+
elif framework == "unittest":
|
|
107
|
+
return parse_unittest_failures(output)
|
|
108
|
+
elif framework == "jest":
|
|
109
|
+
return parse_jest_failures(output)
|
|
110
|
+
else:
|
|
111
|
+
return parse_generic_failures(output)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def parse_pytest_failures(output: str) -> List[Dict[str, Any]]:
|
|
115
|
+
"""Parse pytest failure output."""
|
|
116
|
+
failures = []
|
|
117
|
+
|
|
118
|
+
# First, try to find formal FAILURES section
|
|
119
|
+
failure_section_match = re.search(
|
|
120
|
+
r"=+ FAILURES =+.*?(?=^=+|\Z)", output, re.MULTILINE | re.DOTALL
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if failure_section_match:
|
|
124
|
+
failure_section = failure_section_match.group(0)
|
|
125
|
+
|
|
126
|
+
# Split individual failures
|
|
127
|
+
individual_failures = re.split(
|
|
128
|
+
r"^_+ (.+?) _+$", failure_section, flags=re.MULTILINE
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
for i in range(1, len(individual_failures), 2):
|
|
132
|
+
if i + 1 >= len(individual_failures):
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
test_name = individual_failures[i].strip()
|
|
136
|
+
failure_content = individual_failures[i + 1].strip()
|
|
137
|
+
|
|
138
|
+
# Extract file and line info
|
|
139
|
+
file_match = re.search(r"(\S+\.py):(\d+):", failure_content)
|
|
140
|
+
file_path = file_match.group(1) if file_match else None
|
|
141
|
+
line_number = int(file_match.group(2)) if file_match else None
|
|
142
|
+
|
|
143
|
+
# Extract error type and message
|
|
144
|
+
error_match = re.search(
|
|
145
|
+
r"(AssertionError|TypeError|ValueError|AttributeError|KeyError|IndexError|ImportError|NameError|RuntimeError|Exception)[:>]?\s*(.*)",
|
|
146
|
+
failure_content,
|
|
147
|
+
re.DOTALL,
|
|
148
|
+
)
|
|
149
|
+
error_type = error_match.group(1) if error_match else "Unknown"
|
|
150
|
+
error_message = error_match.group(2).strip() if error_match else ""
|
|
151
|
+
|
|
152
|
+
# Extract assertion details
|
|
153
|
+
assertion_match = re.search(r"assert (.+)", failure_content)
|
|
154
|
+
assertion = assertion_match.group(1) if assertion_match else None
|
|
155
|
+
|
|
156
|
+
# Extract traceback
|
|
157
|
+
traceback_lines = []
|
|
158
|
+
for line in failure_content.split("\n"):
|
|
159
|
+
if re.match(r'^\s+File "', line) or re.match(r"^\s+", line):
|
|
160
|
+
traceback_lines.append(line.strip())
|
|
161
|
+
|
|
162
|
+
failure = {
|
|
163
|
+
"test_name": test_name,
|
|
164
|
+
"file_path": file_path,
|
|
165
|
+
"line_number": line_number,
|
|
166
|
+
"error_type": error_type,
|
|
167
|
+
"error_message": error_message,
|
|
168
|
+
"assertion": assertion,
|
|
169
|
+
"traceback": traceback_lines,
|
|
170
|
+
"full_output": failure_content,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
failures.append(failure)
|
|
174
|
+
|
|
175
|
+
# Also look for simple FAILED lines (common in pytest short output)
|
|
176
|
+
failed_lines = re.findall(r"FAILED (.+?) - (.+)", output)
|
|
177
|
+
for test_name, error_info in failed_lines:
|
|
178
|
+
# Extract error type from error_info
|
|
179
|
+
error_match = re.search(
|
|
180
|
+
r"(AssertionError|TypeError|ValueError|AttributeError|KeyError|IndexError|ImportError|NameError|RuntimeError|Exception)[:>]?\s*(.*)",
|
|
181
|
+
error_info,
|
|
182
|
+
)
|
|
183
|
+
error_type = error_match.group(1) if error_match else "Unknown"
|
|
184
|
+
error_message = error_match.group(2).strip() if error_match else error_info
|
|
185
|
+
|
|
186
|
+
failure = {
|
|
187
|
+
"test_name": test_name,
|
|
188
|
+
"file_path": None,
|
|
189
|
+
"line_number": None,
|
|
190
|
+
"error_type": error_type,
|
|
191
|
+
"error_message": error_message,
|
|
192
|
+
"assertion": None,
|
|
193
|
+
"traceback": [],
|
|
194
|
+
"full_output": f"FAILED {test_name} - {error_info}",
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
failures.append(failure)
|
|
198
|
+
|
|
199
|
+
return failures
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def parse_unittest_failures(output: str) -> List[Dict[str, Any]]:
|
|
203
|
+
"""Parse unittest failure output."""
|
|
204
|
+
failures = []
|
|
205
|
+
|
|
206
|
+
# Find FAIL or ERROR sections
|
|
207
|
+
fail_pattern = r"(FAIL|ERROR): (\S+) \((\S+)\)\n-+\n(.*?)(?=\n\n|\Z)"
|
|
208
|
+
matches = re.findall(fail_pattern, output, re.DOTALL)
|
|
209
|
+
|
|
210
|
+
for match in matches:
|
|
211
|
+
failure_type, method_name, class_name, content = match
|
|
212
|
+
|
|
213
|
+
# Extract file and line info
|
|
214
|
+
file_match = re.search(r'File "([^"]+)", line (\d+)', content)
|
|
215
|
+
file_path = file_match.group(1) if file_match else None
|
|
216
|
+
line_number = int(file_match.group(2)) if file_match else None
|
|
217
|
+
|
|
218
|
+
# Extract error type and message
|
|
219
|
+
error_match = re.search(
|
|
220
|
+
r"(AssertionError|TypeError|ValueError|AttributeError|KeyError|IndexError|ImportError|NameError|RuntimeError|Exception)[:>]?\s*(.*)",
|
|
221
|
+
content,
|
|
222
|
+
re.DOTALL,
|
|
223
|
+
)
|
|
224
|
+
error_type = error_match.group(1) if error_match else failure_type
|
|
225
|
+
error_message = error_match.group(2).strip() if error_match else ""
|
|
226
|
+
|
|
227
|
+
failure = {
|
|
228
|
+
"test_name": f"{class_name}.{method_name}",
|
|
229
|
+
"file_path": file_path,
|
|
230
|
+
"line_number": line_number,
|
|
231
|
+
"error_type": error_type,
|
|
232
|
+
"error_message": error_message,
|
|
233
|
+
"full_output": content,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
failures.append(failure)
|
|
237
|
+
|
|
238
|
+
return failures
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def parse_jest_failures(output: str) -> List[Dict[str, Any]]:
|
|
242
|
+
"""Parse Jest failure output."""
|
|
243
|
+
failures = []
|
|
244
|
+
|
|
245
|
+
# Find test failures
|
|
246
|
+
fail_pattern = r"● (.+?)\n\n(.*?)(?=\n ●|\n\nTest Suites|\Z)"
|
|
247
|
+
matches = re.findall(fail_pattern, output, re.DOTALL)
|
|
248
|
+
|
|
249
|
+
for match in matches:
|
|
250
|
+
test_name, content = match
|
|
251
|
+
|
|
252
|
+
# Extract file and line info
|
|
253
|
+
file_match = re.search(r"at (.+?):(\d+):(\d+)", content)
|
|
254
|
+
file_path = file_match.group(1) if file_match else None
|
|
255
|
+
line_number = int(file_match.group(2)) if file_match else None
|
|
256
|
+
|
|
257
|
+
# Extract error type and message
|
|
258
|
+
error_match = re.search(
|
|
259
|
+
r"(Error|TypeError|ReferenceError|SyntaxError)[:>]?\s*(.*)",
|
|
260
|
+
content,
|
|
261
|
+
re.DOTALL,
|
|
262
|
+
)
|
|
263
|
+
error_type = error_match.group(1) if error_match else "Unknown"
|
|
264
|
+
error_message = error_match.group(2).strip() if error_match else ""
|
|
265
|
+
|
|
266
|
+
failure = {
|
|
267
|
+
"test_name": test_name,
|
|
268
|
+
"file_path": file_path,
|
|
269
|
+
"line_number": line_number,
|
|
270
|
+
"error_type": error_type,
|
|
271
|
+
"error_message": error_message,
|
|
272
|
+
"full_output": content,
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
failures.append(failure)
|
|
276
|
+
|
|
277
|
+
return failures
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def parse_generic_failures(output: str) -> List[Dict[str, Any]]:
|
|
281
|
+
"""Parse generic test failure output."""
|
|
282
|
+
failures = []
|
|
283
|
+
|
|
284
|
+
# Look for common error patterns
|
|
285
|
+
error_patterns = [
|
|
286
|
+
r"(AssertionError|TypeError|ValueError|AttributeError|KeyError|IndexError|ImportError|NameError|RuntimeError|Exception)[:>]?\s*(.*)",
|
|
287
|
+
r"FAILED (.+?) - (.+)",
|
|
288
|
+
r"ERROR (.+?) - (.+)",
|
|
289
|
+
r"✕ (.+)",
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
for pattern in error_patterns:
|
|
293
|
+
matches = re.findall(pattern, output, re.MULTILINE)
|
|
294
|
+
for match in matches:
|
|
295
|
+
if len(match) >= 2:
|
|
296
|
+
error_type = match[0]
|
|
297
|
+
error_message = match[1]
|
|
298
|
+
|
|
299
|
+
failure = {
|
|
300
|
+
"test_name": "Unknown",
|
|
301
|
+
"file_path": None,
|
|
302
|
+
"line_number": None,
|
|
303
|
+
"error_type": error_type,
|
|
304
|
+
"error_message": error_message,
|
|
305
|
+
"full_output": str(match),
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
failures.append(failure)
|
|
309
|
+
|
|
310
|
+
return failures
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def analyze_failures(
|
|
314
|
+
failures: List[Dict[str, Any]], suggest_fixes: bool, group_by_type: bool
|
|
315
|
+
) -> Dict[str, Any]:
|
|
316
|
+
"""Analyze parsed failures and provide insights."""
|
|
317
|
+
analysis = {"failures": failures, "summary": {}, "patterns": {}, "suggestions": []}
|
|
318
|
+
|
|
319
|
+
if not failures:
|
|
320
|
+
return analysis
|
|
321
|
+
|
|
322
|
+
# Group failures by error type
|
|
323
|
+
if group_by_type:
|
|
324
|
+
error_groups = defaultdict(list)
|
|
325
|
+
for failure in failures:
|
|
326
|
+
error_groups[failure["error_type"]].append(failure)
|
|
327
|
+
|
|
328
|
+
analysis["error_groups"] = dict(error_groups)
|
|
329
|
+
|
|
330
|
+
# Generate summary statistics
|
|
331
|
+
analysis["summary"] = {
|
|
332
|
+
"total_failures": len(failures),
|
|
333
|
+
"unique_tests": len(set(f["test_name"] for f in failures)),
|
|
334
|
+
"unique_files": len(set(f["file_path"] for f in failures if f["file_path"])),
|
|
335
|
+
"most_common_error": (
|
|
336
|
+
max(
|
|
337
|
+
set(f["error_type"] for f in failures),
|
|
338
|
+
key=lambda x: sum(1 for f in failures if f["error_type"] == x),
|
|
339
|
+
)
|
|
340
|
+
if failures
|
|
341
|
+
else "None"
|
|
342
|
+
),
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
# Identify patterns
|
|
346
|
+
analysis["patterns"] = identify_patterns(failures)
|
|
347
|
+
|
|
348
|
+
# Generate suggestions
|
|
349
|
+
if suggest_fixes:
|
|
350
|
+
analysis["suggestions"] = generate_suggestions(failures, analysis["patterns"])
|
|
351
|
+
|
|
352
|
+
return analysis
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def identify_patterns(failures: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
356
|
+
"""Identify common patterns in test failures."""
|
|
357
|
+
patterns = {
|
|
358
|
+
"common_errors": {},
|
|
359
|
+
"file_hotspots": {},
|
|
360
|
+
"assertion_patterns": [],
|
|
361
|
+
"import_errors": [],
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
# Count error types
|
|
365
|
+
error_counts = defaultdict(int)
|
|
366
|
+
for failure in failures:
|
|
367
|
+
error_counts[failure["error_type"]] += 1
|
|
368
|
+
|
|
369
|
+
patterns["common_errors"] = dict(error_counts)
|
|
370
|
+
|
|
371
|
+
# Find file hotspots
|
|
372
|
+
file_counts = defaultdict(int)
|
|
373
|
+
for failure in failures:
|
|
374
|
+
if failure["file_path"]:
|
|
375
|
+
file_counts[failure["file_path"]] += 1
|
|
376
|
+
|
|
377
|
+
patterns["file_hotspots"] = dict(file_counts)
|
|
378
|
+
|
|
379
|
+
# Find assertion patterns
|
|
380
|
+
assertion_patterns = []
|
|
381
|
+
for failure in failures:
|
|
382
|
+
if failure.get("assertion"):
|
|
383
|
+
assertion_patterns.append(failure["assertion"])
|
|
384
|
+
|
|
385
|
+
patterns["assertion_patterns"] = assertion_patterns
|
|
386
|
+
|
|
387
|
+
# Find import errors
|
|
388
|
+
import_errors = []
|
|
389
|
+
for failure in failures:
|
|
390
|
+
if failure["error_type"] in ["ImportError", "ModuleNotFoundError"]:
|
|
391
|
+
import_errors.append(failure["error_message"])
|
|
392
|
+
|
|
393
|
+
patterns["import_errors"] = import_errors
|
|
394
|
+
|
|
395
|
+
return patterns
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def generate_suggestions(
|
|
399
|
+
failures: List[Dict[str, Any]], patterns: Dict[str, Any]
|
|
400
|
+
) -> List[Dict[str, Any]]:
|
|
401
|
+
"""Generate suggestions for fixing test failures."""
|
|
402
|
+
suggestions = []
|
|
403
|
+
|
|
404
|
+
# Suggestions based on common error types
|
|
405
|
+
for error_type, count in patterns["common_errors"].items():
|
|
406
|
+
if error_type == "AssertionError":
|
|
407
|
+
suggestions.append(
|
|
408
|
+
{
|
|
409
|
+
"type": "fix_suggestion",
|
|
410
|
+
"priority": "high",
|
|
411
|
+
"message": f"You have {count} assertion failures. Review test expectations and actual values.",
|
|
412
|
+
"actions": [
|
|
413
|
+
"Check if test data has changed",
|
|
414
|
+
"Verify expected vs actual values",
|
|
415
|
+
"Update assertions if requirements changed",
|
|
416
|
+
],
|
|
417
|
+
}
|
|
418
|
+
)
|
|
419
|
+
elif error_type == "ImportError":
|
|
420
|
+
suggestions.append(
|
|
421
|
+
{
|
|
422
|
+
"type": "fix_suggestion",
|
|
423
|
+
"priority": "high",
|
|
424
|
+
"message": f"You have {count} import errors. Check dependencies and module paths.",
|
|
425
|
+
"actions": [
|
|
426
|
+
"Install missing dependencies",
|
|
427
|
+
"Check PYTHONPATH or module paths",
|
|
428
|
+
"Verify module names and locations",
|
|
429
|
+
],
|
|
430
|
+
}
|
|
431
|
+
)
|
|
432
|
+
elif error_type == "AttributeError":
|
|
433
|
+
suggestions.append(
|
|
434
|
+
{
|
|
435
|
+
"type": "fix_suggestion",
|
|
436
|
+
"priority": "medium",
|
|
437
|
+
"message": f"You have {count} attribute errors. Check object interfaces and method names.",
|
|
438
|
+
"actions": [
|
|
439
|
+
"Verify object has expected attributes/methods",
|
|
440
|
+
"Check for typos in attribute names",
|
|
441
|
+
"Ensure objects are properly initialized",
|
|
442
|
+
],
|
|
443
|
+
}
|
|
444
|
+
)
|
|
445
|
+
elif error_type == "TypeError":
|
|
446
|
+
suggestions.append(
|
|
447
|
+
{
|
|
448
|
+
"type": "fix_suggestion",
|
|
449
|
+
"priority": "medium",
|
|
450
|
+
"message": f"You have {count} type errors. Check function arguments and return types.",
|
|
451
|
+
"actions": [
|
|
452
|
+
"Verify function signatures",
|
|
453
|
+
"Check argument types being passed",
|
|
454
|
+
"Ensure proper type conversions",
|
|
455
|
+
],
|
|
456
|
+
}
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
# Suggestions based on file hotspots
|
|
460
|
+
for file_path, count in patterns["file_hotspots"].items():
|
|
461
|
+
if count > 1:
|
|
462
|
+
suggestions.append(
|
|
463
|
+
{
|
|
464
|
+
"type": "code_review",
|
|
465
|
+
"priority": "medium",
|
|
466
|
+
"message": f"File {file_path} has {count} failing tests. Consider reviewing this file.",
|
|
467
|
+
"actions": [
|
|
468
|
+
"Review recent changes to this file",
|
|
469
|
+
"Check for systematic issues",
|
|
470
|
+
"Consider refactoring if needed",
|
|
471
|
+
],
|
|
472
|
+
}
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# Suggestions based on import errors
|
|
476
|
+
if patterns["import_errors"]:
|
|
477
|
+
unique_imports = set(patterns["import_errors"])
|
|
478
|
+
suggestions.append(
|
|
479
|
+
{
|
|
480
|
+
"type": "environment",
|
|
481
|
+
"priority": "high",
|
|
482
|
+
"message": f'Missing imports detected: {", ".join(list(unique_imports)[:3])}...',
|
|
483
|
+
"actions": [
|
|
484
|
+
"Check requirements.txt or package.json",
|
|
485
|
+
"Run pip install or npm install",
|
|
486
|
+
"Verify virtual environment activation",
|
|
487
|
+
],
|
|
488
|
+
}
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
return suggestions
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def format_analysis(analysis: Dict[str, Any], output_format: str) -> str:
|
|
495
|
+
"""Format analysis results for display."""
|
|
496
|
+
if output_format == "json":
|
|
497
|
+
return json.dumps(analysis, indent=2)
|
|
498
|
+
|
|
499
|
+
lines = []
|
|
500
|
+
|
|
501
|
+
# Header
|
|
502
|
+
lines.append("TEST FAILURE ANALYSIS")
|
|
503
|
+
lines.append("=" * 50)
|
|
504
|
+
|
|
505
|
+
# Summary
|
|
506
|
+
summary = analysis["summary"]
|
|
507
|
+
lines.append(f"Total failures: {summary['total_failures']}")
|
|
508
|
+
lines.append(f"Unique tests: {summary['unique_tests']}")
|
|
509
|
+
lines.append(f"Unique files: {summary['unique_files']}")
|
|
510
|
+
lines.append(f"Most common error: {summary['most_common_error']}")
|
|
511
|
+
lines.append("")
|
|
512
|
+
|
|
513
|
+
# Error groups
|
|
514
|
+
if "error_groups" in analysis:
|
|
515
|
+
lines.append("ERROR GROUPS:")
|
|
516
|
+
lines.append("-" * 30)
|
|
517
|
+
for error_type, group_failures in analysis["error_groups"].items():
|
|
518
|
+
lines.append(f"{error_type}: {len(group_failures)} failures")
|
|
519
|
+
for failure in group_failures[:3]: # Show first 3
|
|
520
|
+
lines.append(f" - {failure['test_name']}")
|
|
521
|
+
if len(group_failures) > 3:
|
|
522
|
+
lines.append(f" ... and {len(group_failures) - 3} more")
|
|
523
|
+
lines.append("")
|
|
524
|
+
|
|
525
|
+
# Suggestions
|
|
526
|
+
if analysis["suggestions"]:
|
|
527
|
+
lines.append("SUGGESTIONS:")
|
|
528
|
+
lines.append("-" * 30)
|
|
529
|
+
for suggestion in analysis["suggestions"]:
|
|
530
|
+
lines.append(f"[{suggestion['priority'].upper()}] {suggestion['message']}")
|
|
531
|
+
for action in suggestion["actions"]:
|
|
532
|
+
lines.append(f" • {action}")
|
|
533
|
+
lines.append("")
|
|
534
|
+
|
|
535
|
+
# Detailed failures
|
|
536
|
+
lines.append("DETAILED FAILURES:")
|
|
537
|
+
lines.append("-" * 30)
|
|
538
|
+
for i, failure in enumerate(analysis["failures"][:5], 1): # Show first 5
|
|
539
|
+
lines.append(f"{i}. {failure['test_name']}")
|
|
540
|
+
if failure["file_path"]:
|
|
541
|
+
lines.append(f" File: {failure['file_path']}:{failure['line_number']}")
|
|
542
|
+
lines.append(f" Error: {failure['error_type']}")
|
|
543
|
+
lines.append(f" Message: {failure['error_message'][:100]}...")
|
|
544
|
+
lines.append("")
|
|
545
|
+
|
|
546
|
+
if len(analysis["failures"]) > 5:
|
|
547
|
+
lines.append(f"... and {len(analysis['failures']) - 5} more failures")
|
|
548
|
+
|
|
549
|
+
return "\n".join(lines)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def main():
|
|
553
|
+
parser = argparse.ArgumentParser(
|
|
554
|
+
description="Test failure analysis tool for examining and debugging test failures."
|
|
555
|
+
)
|
|
556
|
+
parser.add_argument(
|
|
557
|
+
"--test_output", type=str, help="Path to test output file or direct test output"
|
|
558
|
+
)
|
|
559
|
+
parser.add_argument(
|
|
560
|
+
"--test_framework",
|
|
561
|
+
choices=[
|
|
562
|
+
"pytest",
|
|
563
|
+
"unittest",
|
|
564
|
+
"jest",
|
|
565
|
+
"rspec",
|
|
566
|
+
"mocha",
|
|
567
|
+
"phpunit",
|
|
568
|
+
"cargo",
|
|
569
|
+
"go",
|
|
570
|
+
],
|
|
571
|
+
help="Test framework used",
|
|
572
|
+
)
|
|
573
|
+
parser.add_argument(
|
|
574
|
+
"--verbose",
|
|
575
|
+
action="store_true",
|
|
576
|
+
default=False,
|
|
577
|
+
help="Show detailed analysis (default: False)",
|
|
578
|
+
)
|
|
579
|
+
parser.add_argument(
|
|
580
|
+
"--suggest_fixes",
|
|
581
|
+
action="store_true",
|
|
582
|
+
default=True,
|
|
583
|
+
help="Suggest potential fixes (default: True)",
|
|
584
|
+
)
|
|
585
|
+
parser.add_argument(
|
|
586
|
+
"--group_by_type",
|
|
587
|
+
action="store_true",
|
|
588
|
+
default=True,
|
|
589
|
+
help="Group failures by error type (default: True)",
|
|
590
|
+
)
|
|
591
|
+
parser.add_argument(
|
|
592
|
+
"--output_format",
|
|
593
|
+
choices=["default", "json"],
|
|
594
|
+
default="default",
|
|
595
|
+
help="Output format (default: default)",
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
args = parser.parse_args()
|
|
599
|
+
|
|
600
|
+
try:
|
|
601
|
+
analysis = analyze_test_failures(
|
|
602
|
+
test_output=args.test_output,
|
|
603
|
+
test_framework=args.test_framework,
|
|
604
|
+
verbose=args.verbose,
|
|
605
|
+
suggest_fixes=args.suggest_fixes,
|
|
606
|
+
group_by_type=args.group_by_type,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
output = format_analysis(analysis, args.output_format)
|
|
610
|
+
print(output)
|
|
611
|
+
|
|
612
|
+
except Exception as e:
|
|
613
|
+
print(f"Error analyzing test failures: {e}", file=sys.stderr)
|
|
614
|
+
sys.exit(1)
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
if __name__ == "__main__":
|
|
618
|
+
main()
|