crackerjack 0.31.10__py3-none-any.whl โ 0.31.13__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.
Potentially problematic release.
This version of crackerjack might be problematic. Click here for more details.
- crackerjack/CLAUDE.md +288 -705
- crackerjack/__main__.py +22 -8
- crackerjack/agents/__init__.py +0 -3
- crackerjack/agents/architect_agent.py +0 -43
- crackerjack/agents/base.py +1 -9
- crackerjack/agents/coordinator.py +2 -148
- crackerjack/agents/documentation_agent.py +109 -81
- crackerjack/agents/dry_agent.py +122 -97
- crackerjack/agents/formatting_agent.py +3 -16
- crackerjack/agents/import_optimization_agent.py +1174 -130
- crackerjack/agents/performance_agent.py +956 -188
- crackerjack/agents/performance_helpers.py +229 -0
- crackerjack/agents/proactive_agent.py +1 -48
- crackerjack/agents/refactoring_agent.py +516 -246
- crackerjack/agents/refactoring_helpers.py +282 -0
- crackerjack/agents/security_agent.py +393 -90
- crackerjack/agents/test_creation_agent.py +1776 -120
- crackerjack/agents/test_specialist_agent.py +59 -15
- crackerjack/agents/tracker.py +0 -102
- crackerjack/api.py +145 -37
- crackerjack/cli/handlers.py +48 -30
- crackerjack/cli/interactive.py +11 -11
- crackerjack/cli/options.py +66 -4
- crackerjack/code_cleaner.py +808 -148
- crackerjack/config/global_lock_config.py +110 -0
- crackerjack/config/hooks.py +43 -64
- crackerjack/core/async_workflow_orchestrator.py +247 -97
- crackerjack/core/autofix_coordinator.py +192 -109
- crackerjack/core/enhanced_container.py +46 -63
- crackerjack/core/file_lifecycle.py +549 -0
- crackerjack/core/performance.py +9 -8
- crackerjack/core/performance_monitor.py +395 -0
- crackerjack/core/phase_coordinator.py +281 -94
- crackerjack/core/proactive_workflow.py +9 -58
- crackerjack/core/resource_manager.py +501 -0
- crackerjack/core/service_watchdog.py +490 -0
- crackerjack/core/session_coordinator.py +4 -8
- crackerjack/core/timeout_manager.py +504 -0
- crackerjack/core/websocket_lifecycle.py +475 -0
- crackerjack/core/workflow_orchestrator.py +343 -209
- crackerjack/dynamic_config.py +50 -9
- crackerjack/errors.py +3 -4
- crackerjack/executors/async_hook_executor.py +63 -13
- crackerjack/executors/cached_hook_executor.py +14 -14
- crackerjack/executors/hook_executor.py +100 -37
- crackerjack/executors/hook_lock_manager.py +856 -0
- crackerjack/executors/individual_hook_executor.py +120 -86
- crackerjack/intelligence/__init__.py +0 -7
- crackerjack/intelligence/adaptive_learning.py +13 -86
- crackerjack/intelligence/agent_orchestrator.py +15 -78
- crackerjack/intelligence/agent_registry.py +12 -59
- crackerjack/intelligence/agent_selector.py +31 -92
- crackerjack/intelligence/integration.py +1 -41
- crackerjack/interactive.py +9 -9
- crackerjack/managers/async_hook_manager.py +25 -8
- crackerjack/managers/hook_manager.py +9 -9
- crackerjack/managers/publish_manager.py +57 -59
- crackerjack/managers/test_command_builder.py +6 -36
- crackerjack/managers/test_executor.py +9 -61
- crackerjack/managers/test_manager.py +17 -63
- crackerjack/managers/test_manager_backup.py +77 -127
- crackerjack/managers/test_progress.py +4 -23
- crackerjack/mcp/cache.py +5 -12
- crackerjack/mcp/client_runner.py +10 -10
- crackerjack/mcp/context.py +64 -6
- crackerjack/mcp/dashboard.py +14 -11
- crackerjack/mcp/enhanced_progress_monitor.py +55 -55
- crackerjack/mcp/file_monitor.py +72 -42
- crackerjack/mcp/progress_components.py +103 -84
- crackerjack/mcp/progress_monitor.py +122 -49
- crackerjack/mcp/rate_limiter.py +12 -12
- crackerjack/mcp/server_core.py +16 -22
- crackerjack/mcp/service_watchdog.py +26 -26
- crackerjack/mcp/state.py +15 -0
- crackerjack/mcp/tools/core_tools.py +95 -39
- crackerjack/mcp/tools/error_analyzer.py +6 -32
- crackerjack/mcp/tools/execution_tools.py +1 -56
- crackerjack/mcp/tools/execution_tools_backup.py +35 -131
- crackerjack/mcp/tools/intelligence_tool_registry.py +0 -36
- crackerjack/mcp/tools/intelligence_tools.py +2 -55
- crackerjack/mcp/tools/monitoring_tools.py +308 -145
- crackerjack/mcp/tools/proactive_tools.py +12 -42
- crackerjack/mcp/tools/progress_tools.py +23 -15
- crackerjack/mcp/tools/utility_tools.py +3 -40
- crackerjack/mcp/tools/workflow_executor.py +40 -60
- crackerjack/mcp/websocket/app.py +0 -3
- crackerjack/mcp/websocket/endpoints.py +206 -268
- crackerjack/mcp/websocket/jobs.py +213 -66
- crackerjack/mcp/websocket/server.py +84 -6
- crackerjack/mcp/websocket/websocket_handler.py +137 -29
- crackerjack/models/config_adapter.py +3 -16
- crackerjack/models/protocols.py +162 -3
- crackerjack/models/resource_protocols.py +454 -0
- crackerjack/models/task.py +3 -3
- crackerjack/monitoring/__init__.py +0 -0
- crackerjack/monitoring/ai_agent_watchdog.py +25 -71
- crackerjack/monitoring/regression_prevention.py +28 -87
- crackerjack/orchestration/advanced_orchestrator.py +44 -78
- crackerjack/orchestration/coverage_improvement.py +10 -60
- crackerjack/orchestration/execution_strategies.py +16 -16
- crackerjack/orchestration/test_progress_streamer.py +61 -53
- crackerjack/plugins/base.py +1 -1
- crackerjack/plugins/managers.py +22 -20
- crackerjack/py313.py +65 -21
- crackerjack/services/backup_service.py +467 -0
- crackerjack/services/bounded_status_operations.py +627 -0
- crackerjack/services/cache.py +7 -9
- crackerjack/services/config.py +35 -52
- crackerjack/services/config_integrity.py +5 -16
- crackerjack/services/config_merge.py +542 -0
- crackerjack/services/contextual_ai_assistant.py +17 -19
- crackerjack/services/coverage_ratchet.py +44 -73
- crackerjack/services/debug.py +25 -39
- crackerjack/services/dependency_monitor.py +52 -50
- crackerjack/services/enhanced_filesystem.py +14 -11
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/filesystem.py +1 -12
- crackerjack/services/git.py +71 -47
- crackerjack/services/health_metrics.py +31 -27
- crackerjack/services/initialization.py +276 -428
- crackerjack/services/input_validator.py +760 -0
- crackerjack/services/log_manager.py +16 -16
- crackerjack/services/logging.py +7 -6
- crackerjack/services/metrics.py +43 -43
- crackerjack/services/pattern_cache.py +2 -31
- crackerjack/services/pattern_detector.py +26 -63
- crackerjack/services/performance_benchmarks.py +20 -45
- crackerjack/services/regex_patterns.py +2887 -0
- crackerjack/services/regex_utils.py +537 -0
- crackerjack/services/secure_path_utils.py +683 -0
- crackerjack/services/secure_status_formatter.py +534 -0
- crackerjack/services/secure_subprocess.py +605 -0
- crackerjack/services/security.py +47 -10
- crackerjack/services/security_logger.py +492 -0
- crackerjack/services/server_manager.py +109 -50
- crackerjack/services/smart_scheduling.py +8 -25
- crackerjack/services/status_authentication.py +603 -0
- crackerjack/services/status_security_manager.py +442 -0
- crackerjack/services/thread_safe_status_collector.py +546 -0
- crackerjack/services/tool_version_service.py +1 -23
- crackerjack/services/unified_config.py +36 -58
- crackerjack/services/validation_rate_limiter.py +269 -0
- crackerjack/services/version_checker.py +9 -40
- crackerjack/services/websocket_resource_limiter.py +572 -0
- crackerjack/slash_commands/__init__.py +52 -2
- crackerjack/tools/__init__.py +0 -0
- crackerjack/tools/validate_input_validator_patterns.py +262 -0
- crackerjack/tools/validate_regex_patterns.py +198 -0
- {crackerjack-0.31.10.dist-info โ crackerjack-0.31.13.dist-info}/METADATA +197 -12
- crackerjack-0.31.13.dist-info/RECORD +178 -0
- crackerjack/cli/facade.py +0 -104
- crackerjack-0.31.10.dist-info/RECORD +0 -149
- {crackerjack-0.31.10.dist-info โ crackerjack-0.31.13.dist-info}/WHEEL +0 -0
- {crackerjack-0.31.10.dist-info โ crackerjack-0.31.13.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.31.10.dist-info โ crackerjack-0.31.13.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
"""Test execution engine with progress tracking and output parsing.
|
|
2
|
-
|
|
3
|
-
This module handles the actual test execution, subprocess management, and real-time
|
|
4
|
-
output parsing. Split from test_manager.py for better separation of concerns.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
1
|
import re
|
|
8
2
|
import subprocess
|
|
9
3
|
import threading
|
|
@@ -18,8 +12,6 @@ from .test_progress import TestProgress
|
|
|
18
12
|
|
|
19
13
|
|
|
20
14
|
class TestExecutor:
|
|
21
|
-
"""Handles test execution with real-time progress tracking."""
|
|
22
|
-
|
|
23
15
|
def __init__(self, console: Console, pkg_path: Path) -> None:
|
|
24
16
|
self.console = console
|
|
25
17
|
self.pkg_path = pkg_path
|
|
@@ -29,7 +21,6 @@ class TestExecutor:
|
|
|
29
21
|
cmd: list[str],
|
|
30
22
|
timeout: int = 600,
|
|
31
23
|
) -> subprocess.CompletedProcess[str]:
|
|
32
|
-
"""Execute test command with live progress display."""
|
|
33
24
|
return self._execute_with_live_progress(cmd, timeout)
|
|
34
25
|
|
|
35
26
|
def execute_with_ai_progress(
|
|
@@ -38,13 +29,11 @@ class TestExecutor:
|
|
|
38
29
|
progress_callback: t.Callable[[dict[str, t.Any]], None],
|
|
39
30
|
timeout: int = 600,
|
|
40
31
|
) -> subprocess.CompletedProcess[str]:
|
|
41
|
-
"""Execute test command with AI-compatible progress callbacks."""
|
|
42
32
|
return self._run_test_command_with_ai_progress(cmd, progress_callback, timeout)
|
|
43
33
|
|
|
44
34
|
def _execute_with_live_progress(
|
|
45
35
|
self, cmd: list[str], timeout: int
|
|
46
36
|
) -> subprocess.CompletedProcess[str]:
|
|
47
|
-
"""Execute tests with Rich live progress display."""
|
|
48
37
|
progress = self._initialize_progress()
|
|
49
38
|
|
|
50
39
|
with Live(progress.format_progress(), console=self.console) as live:
|
|
@@ -61,12 +50,10 @@ class TestExecutor:
|
|
|
61
50
|
env=env,
|
|
62
51
|
)
|
|
63
52
|
|
|
64
|
-
# Start reader threads
|
|
65
53
|
stdout_thread, stderr_thread, monitor_thread = self._start_reader_threads(
|
|
66
54
|
process, progress, live
|
|
67
55
|
)
|
|
68
56
|
|
|
69
|
-
# Wait for completion
|
|
70
57
|
try:
|
|
71
58
|
process.wait(timeout=timeout)
|
|
72
59
|
except subprocess.TimeoutExpired:
|
|
@@ -74,10 +61,8 @@ class TestExecutor:
|
|
|
74
61
|
process, progress, "Test execution timed out"
|
|
75
62
|
)
|
|
76
63
|
|
|
77
|
-
# Cleanup
|
|
78
64
|
self._cleanup_threads([stdout_thread, stderr_thread, monitor_thread])
|
|
79
65
|
|
|
80
|
-
# Return completed process with string output
|
|
81
66
|
stdout_str = process.stdout.read() if process.stdout else ""
|
|
82
67
|
stderr_str = process.stderr.read() if process.stderr else ""
|
|
83
68
|
return subprocess.CompletedProcess(
|
|
@@ -90,7 +75,6 @@ class TestExecutor:
|
|
|
90
75
|
progress_callback: t.Callable[[dict[str, t.Any]], None],
|
|
91
76
|
timeout: int = 600,
|
|
92
77
|
) -> subprocess.CompletedProcess[str]:
|
|
93
|
-
"""Execute test command with AI progress callbacks."""
|
|
94
78
|
progress = self._initialize_progress()
|
|
95
79
|
env = self._setup_coverage_env()
|
|
96
80
|
|
|
@@ -101,13 +85,11 @@ class TestExecutor:
|
|
|
101
85
|
return result
|
|
102
86
|
|
|
103
87
|
def _initialize_progress(self) -> TestProgress:
|
|
104
|
-
"""Initialize progress tracker."""
|
|
105
88
|
progress = TestProgress()
|
|
106
89
|
progress.start_time = time.time()
|
|
107
90
|
return progress
|
|
108
91
|
|
|
109
92
|
def _setup_test_environment(self) -> dict[str, str]:
|
|
110
|
-
"""Set up environment variables for test execution."""
|
|
111
93
|
import os
|
|
112
94
|
|
|
113
95
|
cache_dir = Path.home() / ".cache" / "crackerjack" / "coverage"
|
|
@@ -119,7 +101,6 @@ class TestExecutor:
|
|
|
119
101
|
return env
|
|
120
102
|
|
|
121
103
|
def _setup_coverage_env(self) -> dict[str, str]:
|
|
122
|
-
"""Set up coverage environment for AI mode."""
|
|
123
104
|
import os
|
|
124
105
|
from pathlib import Path
|
|
125
106
|
|
|
@@ -133,7 +114,6 @@ class TestExecutor:
|
|
|
133
114
|
def _start_reader_threads(
|
|
134
115
|
self, process: subprocess.Popen[str], progress: TestProgress, live: Live
|
|
135
116
|
) -> tuple[threading.Thread, threading.Thread, threading.Thread]:
|
|
136
|
-
"""Start threads for reading stdout, stderr, and monitoring."""
|
|
137
117
|
stdout_thread = self._create_stdout_reader(process, progress, live)
|
|
138
118
|
stderr_thread = self._create_stderr_reader(process, progress, live)
|
|
139
119
|
monitor_thread = self._create_monitor_thread(progress)
|
|
@@ -147,8 +127,6 @@ class TestExecutor:
|
|
|
147
127
|
def _create_stdout_reader(
|
|
148
128
|
self, process: subprocess.Popen[str], progress: TestProgress, live: Live
|
|
149
129
|
) -> threading.Thread:
|
|
150
|
-
"""Create thread for reading stdout."""
|
|
151
|
-
|
|
152
130
|
def read_output() -> None:
|
|
153
131
|
if process.stdout:
|
|
154
132
|
for line in iter(process.stdout.readline, ""):
|
|
@@ -163,8 +141,6 @@ class TestExecutor:
|
|
|
163
141
|
def _create_stderr_reader(
|
|
164
142
|
self, process: subprocess.Popen[str], progress: TestProgress, live: Live
|
|
165
143
|
) -> threading.Thread:
|
|
166
|
-
"""Create thread for reading stderr."""
|
|
167
|
-
|
|
168
144
|
def read_stderr() -> None:
|
|
169
145
|
if process.stderr:
|
|
170
146
|
for line in iter(process.stderr.readline, ""):
|
|
@@ -175,8 +151,6 @@ class TestExecutor:
|
|
|
175
151
|
return threading.Thread(target=read_stderr, daemon=True)
|
|
176
152
|
|
|
177
153
|
def _create_monitor_thread(self, progress: TestProgress) -> threading.Thread:
|
|
178
|
-
"""Create thread for monitoring stuck tests."""
|
|
179
|
-
|
|
180
154
|
def monitor_stuck_tests() -> None:
|
|
181
155
|
last_update = time.time()
|
|
182
156
|
last_test = ""
|
|
@@ -198,31 +172,26 @@ class TestExecutor:
|
|
|
198
172
|
return threading.Thread(target=monitor_stuck_tests, daemon=True)
|
|
199
173
|
|
|
200
174
|
def _process_test_output_line(self, line: str, progress: TestProgress) -> None:
|
|
201
|
-
"""Process a single line of test output."""
|
|
202
175
|
self._parse_test_line(line, progress)
|
|
203
176
|
|
|
204
177
|
def _parse_test_line(self, line: str, progress: TestProgress) -> None:
|
|
205
|
-
"""Parse test output line and update progress."""
|
|
206
|
-
# Handle collection completion
|
|
207
178
|
if self._handle_collection_completion(line, progress):
|
|
208
179
|
return
|
|
209
180
|
|
|
210
|
-
# Handle session events
|
|
211
181
|
if self._handle_session_events(line, progress):
|
|
212
182
|
return
|
|
213
183
|
|
|
214
|
-
# Handle collection progress
|
|
215
184
|
if self._handle_collection_progress(line, progress):
|
|
216
185
|
return
|
|
217
186
|
|
|
218
|
-
# Handle test execution
|
|
219
187
|
if self._handle_test_execution(line, progress):
|
|
220
188
|
return
|
|
221
189
|
|
|
222
190
|
def _handle_collection_completion(self, line: str, progress: TestProgress) -> bool:
|
|
223
|
-
"""Handle test collection completion."""
|
|
224
191
|
if "collected" in line and ("item" in line or "test" in line):
|
|
225
|
-
match = re.search(
|
|
192
|
+
match = re.search(
|
|
193
|
+
r"(\d +) (?: item | test)", line
|
|
194
|
+
) # REGEX OK: parsing pytest output format
|
|
226
195
|
if match:
|
|
227
196
|
progress.update(
|
|
228
197
|
total_tests=int(match.group(1)),
|
|
@@ -233,7 +202,6 @@ class TestExecutor:
|
|
|
233
202
|
return False
|
|
234
203
|
|
|
235
204
|
def _handle_session_events(self, line: str, progress: TestProgress) -> bool:
|
|
236
|
-
"""Handle pytest session events."""
|
|
237
205
|
if "session starts" in line and progress.collection_status != "Session started":
|
|
238
206
|
progress.update(collection_status="Session started")
|
|
239
207
|
return True
|
|
@@ -246,9 +214,7 @@ class TestExecutor:
|
|
|
246
214
|
return False
|
|
247
215
|
|
|
248
216
|
def _handle_collection_progress(self, line: str, progress: TestProgress) -> bool:
|
|
249
|
-
"""Handle collection progress updates."""
|
|
250
217
|
if progress.is_collecting:
|
|
251
|
-
# Look for file discovery patterns
|
|
252
218
|
if line.endswith(".py") and ("test_" in line or "_test.py" in line):
|
|
253
219
|
with progress._lock:
|
|
254
220
|
if line not in progress._seen_files:
|
|
@@ -261,8 +227,6 @@ class TestExecutor:
|
|
|
261
227
|
return False
|
|
262
228
|
|
|
263
229
|
def _handle_test_execution(self, line: str, progress: TestProgress) -> bool:
|
|
264
|
-
"""Handle test execution progress."""
|
|
265
|
-
# Test result patterns
|
|
266
230
|
if " PASSED " in line:
|
|
267
231
|
progress.update(passed=progress.passed + 1)
|
|
268
232
|
self._extract_current_test(line, progress)
|
|
@@ -279,39 +243,32 @@ class TestExecutor:
|
|
|
279
243
|
progress.update(errors=progress.errors + 1)
|
|
280
244
|
self._extract_current_test(line, progress)
|
|
281
245
|
return True
|
|
282
|
-
elif "::" in line and any(x in line for x in ("RUNNING", "test_")):
|
|
246
|
+
elif ":: " in line and any(x in line for x in ("RUNNING", "test_")):
|
|
283
247
|
self._handle_running_test(line, progress)
|
|
284
248
|
return True
|
|
285
249
|
|
|
286
250
|
return False
|
|
287
251
|
|
|
288
252
|
def _handle_running_test(self, line: str, progress: TestProgress) -> None:
|
|
289
|
-
""
|
|
290
|
-
|
|
291
|
-
# Extract test name from line
|
|
292
|
-
test_parts = line.split("::")
|
|
253
|
+
if ":: " in line:
|
|
254
|
+
test_parts = line.split(":: ")
|
|
293
255
|
if len(test_parts) >= 2:
|
|
294
|
-
test_name = "::".join(test_parts[-2:])
|
|
256
|
+
test_name = ":: ".join(test_parts[-2:])
|
|
295
257
|
progress.update(current_test=test_name)
|
|
296
258
|
|
|
297
259
|
def _extract_current_test(self, line: str, progress: TestProgress) -> None:
|
|
298
|
-
""
|
|
299
|
-
if "::" in line:
|
|
300
|
-
# Extract test identifier
|
|
260
|
+
if ":: " in line:
|
|
301
261
|
parts = line.split(" ")
|
|
302
262
|
for part in parts:
|
|
303
|
-
if "::" in part:
|
|
263
|
+
if ":: " in part:
|
|
304
264
|
progress.update(current_test=part)
|
|
305
265
|
break
|
|
306
266
|
|
|
307
267
|
def _update_display_if_needed(self, progress: TestProgress, live: Live) -> None:
|
|
308
|
-
"""Update display if enough time has passed or significant change occurred."""
|
|
309
268
|
if self._should_refresh_display(progress):
|
|
310
269
|
live.update(progress.format_progress())
|
|
311
270
|
|
|
312
271
|
def _should_refresh_display(self, progress: TestProgress) -> bool:
|
|
313
|
-
"""Determine if display should be refreshed."""
|
|
314
|
-
# Only refresh on significant changes to reduce spam
|
|
315
272
|
return (
|
|
316
273
|
progress.is_complete
|
|
317
274
|
or progress.total_tests > 0
|
|
@@ -319,12 +276,10 @@ class TestExecutor:
|
|
|
319
276
|
)
|
|
320
277
|
|
|
321
278
|
def _mark_test_as_stuck(self, progress: TestProgress, test_name: str) -> None:
|
|
322
|
-
"""Mark a test as potentially stuck."""
|
|
323
279
|
if test_name:
|
|
324
280
|
progress.update(current_test=f"๐ {test_name} (slow)")
|
|
325
281
|
|
|
326
282
|
def _cleanup_threads(self, threads: list[threading.Thread]) -> None:
|
|
327
|
-
"""Clean up reader threads."""
|
|
328
283
|
for thread in threads:
|
|
329
284
|
if thread.is_alive():
|
|
330
285
|
thread.join(timeout=1.0)
|
|
@@ -332,7 +287,6 @@ class TestExecutor:
|
|
|
332
287
|
def _handle_progress_error(
|
|
333
288
|
self, process: subprocess.Popen[str], progress: TestProgress, error_msg: str
|
|
334
289
|
) -> None:
|
|
335
|
-
"""Handle progress tracking errors."""
|
|
336
290
|
process.terminate()
|
|
337
291
|
progress.update(is_complete=True, current_test=f"โ {error_msg}")
|
|
338
292
|
|
|
@@ -344,7 +298,6 @@ class TestExecutor:
|
|
|
344
298
|
progress_callback: t.Callable[[dict[str, t.Any]], None],
|
|
345
299
|
timeout: int,
|
|
346
300
|
) -> subprocess.CompletedProcess[str]:
|
|
347
|
-
"""Execute test process with AI progress tracking."""
|
|
348
301
|
process = subprocess.Popen(
|
|
349
302
|
cmd,
|
|
350
303
|
cwd=self.pkg_path,
|
|
@@ -371,7 +324,6 @@ class TestExecutor:
|
|
|
371
324
|
progress: TestProgress,
|
|
372
325
|
progress_callback: t.Callable[[dict[str, t.Any]], None],
|
|
373
326
|
) -> list[str]:
|
|
374
|
-
"""Read stdout with progress updates."""
|
|
375
327
|
stdout_lines = []
|
|
376
328
|
|
|
377
329
|
if process.stdout:
|
|
@@ -388,7 +340,6 @@ class TestExecutor:
|
|
|
388
340
|
return stdout_lines
|
|
389
341
|
|
|
390
342
|
def _read_stderr_lines(self, process: subprocess.Popen[str]) -> list[str]:
|
|
391
|
-
"""Read stderr lines."""
|
|
392
343
|
stderr_lines = []
|
|
393
344
|
|
|
394
345
|
if process.stderr:
|
|
@@ -404,7 +355,6 @@ class TestExecutor:
|
|
|
404
355
|
def _wait_for_process_completion(
|
|
405
356
|
self, process: subprocess.Popen[str], timeout: int
|
|
406
357
|
) -> int:
|
|
407
|
-
"""Wait for process completion with timeout."""
|
|
408
358
|
try:
|
|
409
359
|
process.wait(timeout=timeout)
|
|
410
360
|
return process.returncode
|
|
@@ -417,7 +367,6 @@ class TestExecutor:
|
|
|
417
367
|
progress: TestProgress,
|
|
418
368
|
progress_callback: t.Callable[[dict[str, t.Any]], None],
|
|
419
369
|
) -> None:
|
|
420
|
-
"""Emit progress update for AI consumption."""
|
|
421
370
|
progress_data = {
|
|
422
371
|
"type": "test_progress",
|
|
423
372
|
"total_tests": progress.total_tests,
|
|
@@ -439,5 +388,4 @@ class TestExecutor:
|
|
|
439
388
|
from contextlib import suppress
|
|
440
389
|
|
|
441
390
|
with suppress(Exception):
|
|
442
|
-
# Don't let progress callback errors affect test execution
|
|
443
391
|
progress_callback(progress_data)
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
"""Refactored test manager with focused responsibilities.
|
|
2
|
-
|
|
3
|
-
This is the new modular test manager that delegates to specialized components:
|
|
4
|
-
- TestExecutor: Handles test execution and progress tracking
|
|
5
|
-
- TestCommandBuilder: Builds pytest commands with appropriate options
|
|
6
|
-
- TestProgress: Tracks and displays test progress
|
|
7
|
-
- CoverageRatchetService: Manages coverage requirements
|
|
8
|
-
|
|
9
|
-
REFACTORING NOTE: Original test_manager.py was 1133 lines with 60+ methods.
|
|
10
|
-
This refactored version is ~200 lines and delegates to focused modules.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
1
|
import subprocess
|
|
14
2
|
import time
|
|
15
3
|
import typing as t
|
|
@@ -25,18 +13,14 @@ from .test_executor import TestExecutor
|
|
|
25
13
|
|
|
26
14
|
|
|
27
15
|
class TestManager:
|
|
28
|
-
"""Refactored test manager with modular architecture."""
|
|
29
|
-
|
|
30
16
|
def __init__(self, console: Console, pkg_path: Path) -> None:
|
|
31
17
|
self.console = console
|
|
32
18
|
self.pkg_path = pkg_path
|
|
33
19
|
|
|
34
|
-
# Initialize specialized components
|
|
35
20
|
self.executor = TestExecutor(console, pkg_path)
|
|
36
21
|
self.command_builder = TestCommandBuilder(pkg_path)
|
|
37
22
|
self.coverage_ratchet = CoverageRatchetService(pkg_path, console)
|
|
38
23
|
|
|
39
|
-
# State
|
|
40
24
|
self._last_test_failures: list[str] = []
|
|
41
25
|
self._progress_callback: t.Callable[[dict[str, t.Any]], None] | None = None
|
|
42
26
|
self.coverage_ratchet_enabled = True
|
|
@@ -45,21 +29,18 @@ class TestManager:
|
|
|
45
29
|
self,
|
|
46
30
|
callback: t.Callable[[dict[str, t.Any]], None] | None,
|
|
47
31
|
) -> None:
|
|
48
|
-
"""Set callback for AI mode structured progress updates."""
|
|
49
32
|
self._progress_callback = callback
|
|
50
33
|
|
|
51
34
|
def set_coverage_ratchet_enabled(self, enabled: bool) -> None:
|
|
52
|
-
"""Enable or disable the coverage ratchet system."""
|
|
53
35
|
self.coverage_ratchet_enabled = enabled
|
|
54
36
|
if enabled:
|
|
55
37
|
self.console.print(
|
|
56
|
-
"[cyan]๐[/cyan] Coverage ratchet enabled
|
|
38
|
+
"[cyan]๐[/ cyan] Coverage ratchet enabled-targeting 100 % coverage"
|
|
57
39
|
)
|
|
58
40
|
else:
|
|
59
|
-
self.console.print("[yellow]โ ๏ธ[/yellow] Coverage ratchet disabled")
|
|
41
|
+
self.console.print("[yellow]โ ๏ธ[/ yellow] Coverage ratchet disabled")
|
|
60
42
|
|
|
61
43
|
def run_tests(self, options: OptionsProtocol) -> bool:
|
|
62
|
-
"""Run tests with comprehensive progress tracking and coverage analysis."""
|
|
63
44
|
start_time = time.time()
|
|
64
45
|
|
|
65
46
|
try:
|
|
@@ -77,44 +58,39 @@ class TestManager:
|
|
|
77
58
|
return self._handle_test_error(start_time, e)
|
|
78
59
|
|
|
79
60
|
def run_specific_tests(self, test_pattern: str) -> bool:
|
|
80
|
-
"
|
|
81
|
-
self.console.print(f"[cyan]๐งช[/cyan] Running tests matching: {test_pattern}")
|
|
61
|
+
self.console.print(f"[cyan]๐งช[/ cyan] Running tests matching: {test_pattern}")
|
|
82
62
|
|
|
83
63
|
cmd = self.command_builder.build_specific_test_command(test_pattern)
|
|
84
64
|
result = self.executor.execute_with_progress(cmd)
|
|
85
65
|
|
|
86
66
|
success = result.returncode == 0
|
|
87
67
|
if success:
|
|
88
|
-
self.console.print("[green]โ
[/green] Specific tests passed")
|
|
68
|
+
self.console.print("[green]โ
[/ green] Specific tests passed")
|
|
89
69
|
else:
|
|
90
|
-
self.console.print("[red]โ[/red] Some specific tests failed")
|
|
70
|
+
self.console.print("[red]โ[/ red] Some specific tests failed")
|
|
91
71
|
|
|
92
72
|
return success
|
|
93
73
|
|
|
94
74
|
def validate_test_environment(self) -> bool:
|
|
95
|
-
"""Validate test environment and configuration."""
|
|
96
75
|
if not self.has_tests():
|
|
97
|
-
self.console.print("[yellow]โ ๏ธ[/yellow] No tests found")
|
|
76
|
+
self.console.print("[yellow]โ ๏ธ[/ yellow] No tests found")
|
|
98
77
|
return False
|
|
99
78
|
|
|
100
|
-
# Test basic pytest collection
|
|
101
79
|
cmd = self.command_builder.build_validation_command()
|
|
102
80
|
result = subprocess.run(cmd, cwd=self.pkg_path, capture_output=True, text=True)
|
|
103
81
|
|
|
104
82
|
if result.returncode != 0:
|
|
105
|
-
self.console.print("[red]โ[/red] Test environment validation failed")
|
|
83
|
+
self.console.print("[red]โ[/ red] Test environment validation failed")
|
|
106
84
|
self.console.print(result.stderr)
|
|
107
85
|
return False
|
|
108
86
|
|
|
109
|
-
self.console.print("[green]โ
[/green] Test environment validated")
|
|
87
|
+
self.console.print("[green]โ
[/ green] Test environment validated")
|
|
110
88
|
return True
|
|
111
89
|
|
|
112
90
|
def get_coverage_ratchet_status(self) -> dict[str, t.Any]:
|
|
113
|
-
"""Get comprehensive coverage ratchet status."""
|
|
114
91
|
return self.coverage_ratchet.get_status_report()
|
|
115
92
|
|
|
116
93
|
def get_test_stats(self) -> dict[str, t.Any]:
|
|
117
|
-
"""Get comprehensive test execution statistics."""
|
|
118
94
|
return {
|
|
119
95
|
"has_tests": self.has_tests(),
|
|
120
96
|
"coverage_ratchet_enabled": self.coverage_ratchet_enabled,
|
|
@@ -122,24 +98,19 @@ class TestManager:
|
|
|
122
98
|
}
|
|
123
99
|
|
|
124
100
|
def get_test_failures(self) -> list[str]:
|
|
125
|
-
"""Get list of recent test failures."""
|
|
126
101
|
return self._last_test_failures.copy()
|
|
127
102
|
|
|
128
103
|
def get_test_command(self, options: OptionsProtocol) -> list[str]:
|
|
129
|
-
"""Get the test command that would be executed."""
|
|
130
104
|
return self.command_builder.build_command(options)
|
|
131
105
|
|
|
132
106
|
def get_coverage_report(self) -> str | None:
|
|
133
|
-
"""Get coverage report if available."""
|
|
134
107
|
try:
|
|
135
108
|
return self.coverage_ratchet.get_coverage_report()
|
|
136
109
|
except Exception:
|
|
137
110
|
return None
|
|
138
111
|
|
|
139
112
|
def get_coverage(self) -> dict[str, t.Any]:
|
|
140
|
-
"""Get coverage information as required by TestManagerProtocol."""
|
|
141
113
|
try:
|
|
142
|
-
# Get the ratchet status which includes coverage information
|
|
143
114
|
status = self.coverage_ratchet.get_status_report()
|
|
144
115
|
|
|
145
116
|
if status.get("status") == "not_initialized":
|
|
@@ -167,7 +138,6 @@ class TestManager:
|
|
|
167
138
|
}
|
|
168
139
|
|
|
169
140
|
def has_tests(self) -> bool:
|
|
170
|
-
"""Check if project has tests."""
|
|
171
141
|
test_directories = ["tests", "test"]
|
|
172
142
|
test_files = ["test_*.py", "*_test.py"]
|
|
173
143
|
|
|
@@ -178,19 +148,15 @@ class TestManager:
|
|
|
178
148
|
if list(test_path.glob(f"**/{test_file_pattern}")):
|
|
179
149
|
return True
|
|
180
150
|
|
|
181
|
-
# Check for test files in root directory
|
|
182
151
|
for test_file_pattern in test_files:
|
|
183
152
|
if list(self.pkg_path.glob(test_file_pattern)):
|
|
184
153
|
return True
|
|
185
154
|
|
|
186
155
|
return False
|
|
187
156
|
|
|
188
|
-
# Private implementation methods
|
|
189
|
-
|
|
190
157
|
def _execute_test_workflow(
|
|
191
158
|
self, options: OptionsProtocol
|
|
192
159
|
) -> subprocess.CompletedProcess[str]:
|
|
193
|
-
"""Execute the complete test workflow."""
|
|
194
160
|
self._print_test_start_message(options)
|
|
195
161
|
|
|
196
162
|
cmd = self.command_builder.build_command(options)
|
|
@@ -202,17 +168,15 @@ class TestManager:
|
|
|
202
168
|
return self.executor.execute_with_progress(cmd, self._get_timeout(options))
|
|
203
169
|
|
|
204
170
|
def _print_test_start_message(self, options: OptionsProtocol) -> None:
|
|
205
|
-
"""Print test execution start message."""
|
|
206
171
|
workers = self.command_builder.get_optimal_workers(options)
|
|
207
172
|
timeout = self.command_builder.get_test_timeout(options)
|
|
208
173
|
|
|
209
174
|
self.console.print(
|
|
210
|
-
f"[cyan]๐งช[/cyan] Running tests (workers: {workers}, timeout: {timeout}s)"
|
|
175
|
+
f"[cyan]๐งช[/ cyan] Running tests (workers: {workers}, timeout: {timeout}s)"
|
|
211
176
|
)
|
|
212
177
|
|
|
213
178
|
def _handle_test_success(self, output: str, duration: float) -> bool:
|
|
214
|
-
"
|
|
215
|
-
self.console.print(f"[green]โ
[/green] Tests passed in {duration:.1f}s")
|
|
179
|
+
self.console.print(f"[green]โ
[/ green] Tests passed in {duration: .1f}s")
|
|
216
180
|
|
|
217
181
|
if self.coverage_ratchet_enabled:
|
|
218
182
|
return self._process_coverage_ratchet()
|
|
@@ -220,22 +184,19 @@ class TestManager:
|
|
|
220
184
|
return True
|
|
221
185
|
|
|
222
186
|
def _handle_test_failure(self, output: str, duration: float) -> bool:
|
|
223
|
-
"
|
|
224
|
-
self.console.print(f"[red]โ[/red] Tests failed in {duration:.1f}s")
|
|
187
|
+
self.console.print(f"[red]โ[/ red] Tests failed in {duration: .1f}s")
|
|
225
188
|
|
|
226
189
|
self._last_test_failures = self._extract_failure_lines(output)
|
|
227
190
|
return False
|
|
228
191
|
|
|
229
192
|
def _handle_test_error(self, start_time: float, error: Exception) -> bool:
|
|
230
|
-
"""Handle test execution errors."""
|
|
231
193
|
duration = time.time() - start_time
|
|
232
194
|
self.console.print(
|
|
233
|
-
f"[red]๐ฅ[/red] Test execution error after {duration
|
|
195
|
+
f"[red]๐ฅ[/ red] Test execution error after {duration: .1f}s: {error}"
|
|
234
196
|
)
|
|
235
197
|
return False
|
|
236
198
|
|
|
237
199
|
def _process_coverage_ratchet(self) -> bool:
|
|
238
|
-
"""Process coverage ratchet checks."""
|
|
239
200
|
if not self.coverage_ratchet_enabled:
|
|
240
201
|
return True
|
|
241
202
|
|
|
@@ -243,37 +204,32 @@ class TestManager:
|
|
|
243
204
|
return self._handle_ratchet_result(ratchet_result)
|
|
244
205
|
|
|
245
206
|
def _handle_ratchet_result(self, ratchet_result: dict[str, t.Any]) -> bool:
|
|
246
|
-
"""Handle coverage ratchet results."""
|
|
247
207
|
if ratchet_result.get("success", False):
|
|
248
208
|
if ratchet_result.get("improved", False):
|
|
249
209
|
self._handle_coverage_improvement(ratchet_result)
|
|
250
210
|
return True
|
|
251
211
|
else:
|
|
252
|
-
# Use the message from the ratchet result if available, or construct from available data
|
|
253
212
|
if "message" in ratchet_result:
|
|
254
|
-
self.console.print(f"[red]๐[/red] {ratchet_result['message']}")
|
|
213
|
+
self.console.print(f"[red]๐[/ red] {ratchet_result['message']}")
|
|
255
214
|
else:
|
|
256
|
-
# Fallback to constructing message from available keys
|
|
257
215
|
current = ratchet_result.get("current_coverage", 0)
|
|
258
216
|
previous = ratchet_result.get("previous_coverage", 0)
|
|
259
217
|
self.console.print(
|
|
260
|
-
f"[red]๐[/red] Coverage regression: "
|
|
218
|
+
f"[red]๐[/ red] Coverage regression: "
|
|
261
219
|
f"{current:.2f}% < {previous:.2f}%"
|
|
262
220
|
)
|
|
263
221
|
return False
|
|
264
222
|
|
|
265
223
|
def _handle_coverage_improvement(self, ratchet_result: dict[str, t.Any]) -> None:
|
|
266
|
-
"""Handle coverage improvements."""
|
|
267
224
|
improvement = ratchet_result.get("improvement", 0)
|
|
268
225
|
current = ratchet_result.get("current_coverage", 0)
|
|
269
226
|
|
|
270
227
|
self.console.print(
|
|
271
|
-
f"[green]๐[/green] Coverage improved by {improvement
|
|
272
|
-
f"to {current
|
|
228
|
+
f"[green]๐[/ green] Coverage improved by {improvement: .2f}% "
|
|
229
|
+
f"to {current: .2f}%"
|
|
273
230
|
)
|
|
274
231
|
|
|
275
232
|
def _extract_failure_lines(self, output: str) -> list[str]:
|
|
276
|
-
"""Extract failure information from test output."""
|
|
277
233
|
failures = []
|
|
278
234
|
lines = output.split("\n")
|
|
279
235
|
|
|
@@ -283,12 +239,10 @@ class TestManager:
|
|
|
283
239
|
):
|
|
284
240
|
failures.append(line.strip())
|
|
285
241
|
|
|
286
|
-
return failures[:10]
|
|
242
|
+
return failures[:10]
|
|
287
243
|
|
|
288
244
|
def _get_timeout(self, options: OptionsProtocol) -> int:
|
|
289
|
-
"""Get timeout for test execution."""
|
|
290
245
|
return self.command_builder.get_test_timeout(options)
|
|
291
246
|
|
|
292
247
|
|
|
293
|
-
# For backward compatibility, also export the old class name
|
|
294
248
|
TestManagementImpl = TestManager
|