crackerjack 0.31.9__py3-none-any.whl โ 0.31.12__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 +282 -95
- 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 +355 -204
- crackerjack/dynamic_config.py +47 -6
- 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 +52 -62
- 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 +51 -76
- 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 +78 -44
- crackerjack/services/health_metrics.py +31 -27
- crackerjack/services/initialization.py +281 -433
- 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.9.dist-info โ crackerjack-0.31.12.dist-info}/METADATA +197 -12
- crackerjack-0.31.12.dist-info/RECORD +178 -0
- crackerjack/cli/facade.py +0 -104
- crackerjack-0.31.9.dist-info/RECORD +0 -149
- {crackerjack-0.31.9.dist-info โ crackerjack-0.31.12.dist-info}/WHEEL +0 -0
- {crackerjack-0.31.9.dist-info โ crackerjack-0.31.12.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.31.9.dist-info โ crackerjack-0.31.12.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,22 +98,46 @@ 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
|
|
|
112
|
+
def get_coverage(self) -> dict[str, t.Any]:
|
|
113
|
+
try:
|
|
114
|
+
status = self.coverage_ratchet.get_status_report()
|
|
115
|
+
|
|
116
|
+
if status.get("status") == "not_initialized":
|
|
117
|
+
return {
|
|
118
|
+
"status": "not_initialized",
|
|
119
|
+
"coverage_percent": 0.0,
|
|
120
|
+
"message": "Coverage ratchet not initialized",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
"status": "active",
|
|
125
|
+
"coverage_percent": status.get("current_coverage", 0.0),
|
|
126
|
+
"target_coverage": status.get("target_coverage", 100.0),
|
|
127
|
+
"next_milestone": status.get("next_milestone"),
|
|
128
|
+
"progress_percent": status.get("progress_percent", 0.0),
|
|
129
|
+
"last_updated": status.get("last_updated"),
|
|
130
|
+
"milestones_achieved": status.get("milestones_achieved", []),
|
|
131
|
+
}
|
|
132
|
+
except Exception as e:
|
|
133
|
+
return {
|
|
134
|
+
"status": "error",
|
|
135
|
+
"coverage_percent": 0.0,
|
|
136
|
+
"error": str(e),
|
|
137
|
+
"message": "Failed to get coverage information",
|
|
138
|
+
}
|
|
139
|
+
|
|
139
140
|
def has_tests(self) -> bool:
|
|
140
|
-
"""Check if project has tests."""
|
|
141
141
|
test_directories = ["tests", "test"]
|
|
142
142
|
test_files = ["test_*.py", "*_test.py"]
|
|
143
143
|
|
|
@@ -148,19 +148,15 @@ class TestManager:
|
|
|
148
148
|
if list(test_path.glob(f"**/{test_file_pattern}")):
|
|
149
149
|
return True
|
|
150
150
|
|
|
151
|
-
# Check for test files in root directory
|
|
152
151
|
for test_file_pattern in test_files:
|
|
153
152
|
if list(self.pkg_path.glob(test_file_pattern)):
|
|
154
153
|
return True
|
|
155
154
|
|
|
156
155
|
return False
|
|
157
156
|
|
|
158
|
-
# Private implementation methods
|
|
159
|
-
|
|
160
157
|
def _execute_test_workflow(
|
|
161
158
|
self, options: OptionsProtocol
|
|
162
159
|
) -> subprocess.CompletedProcess[str]:
|
|
163
|
-
"""Execute the complete test workflow."""
|
|
164
160
|
self._print_test_start_message(options)
|
|
165
161
|
|
|
166
162
|
cmd = self.command_builder.build_command(options)
|
|
@@ -172,17 +168,15 @@ class TestManager:
|
|
|
172
168
|
return self.executor.execute_with_progress(cmd, self._get_timeout(options))
|
|
173
169
|
|
|
174
170
|
def _print_test_start_message(self, options: OptionsProtocol) -> None:
|
|
175
|
-
"""Print test execution start message."""
|
|
176
171
|
workers = self.command_builder.get_optimal_workers(options)
|
|
177
172
|
timeout = self.command_builder.get_test_timeout(options)
|
|
178
173
|
|
|
179
174
|
self.console.print(
|
|
180
|
-
f"[cyan]๐งช[/cyan] Running tests (workers: {workers}, timeout: {timeout}s)"
|
|
175
|
+
f"[cyan]๐งช[/ cyan] Running tests (workers: {workers}, timeout: {timeout}s)"
|
|
181
176
|
)
|
|
182
177
|
|
|
183
178
|
def _handle_test_success(self, output: str, duration: float) -> bool:
|
|
184
|
-
"
|
|
185
|
-
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")
|
|
186
180
|
|
|
187
181
|
if self.coverage_ratchet_enabled:
|
|
188
182
|
return self._process_coverage_ratchet()
|
|
@@ -190,22 +184,19 @@ class TestManager:
|
|
|
190
184
|
return True
|
|
191
185
|
|
|
192
186
|
def _handle_test_failure(self, output: str, duration: float) -> bool:
|
|
193
|
-
"
|
|
194
|
-
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")
|
|
195
188
|
|
|
196
189
|
self._last_test_failures = self._extract_failure_lines(output)
|
|
197
190
|
return False
|
|
198
191
|
|
|
199
192
|
def _handle_test_error(self, start_time: float, error: Exception) -> bool:
|
|
200
|
-
"""Handle test execution errors."""
|
|
201
193
|
duration = time.time() - start_time
|
|
202
194
|
self.console.print(
|
|
203
|
-
f"[red]๐ฅ[/red] Test execution error after {duration
|
|
195
|
+
f"[red]๐ฅ[/ red] Test execution error after {duration: .1f}s: {error}"
|
|
204
196
|
)
|
|
205
197
|
return False
|
|
206
198
|
|
|
207
199
|
def _process_coverage_ratchet(self) -> bool:
|
|
208
|
-
"""Process coverage ratchet checks."""
|
|
209
200
|
if not self.coverage_ratchet_enabled:
|
|
210
201
|
return True
|
|
211
202
|
|
|
@@ -213,31 +204,32 @@ class TestManager:
|
|
|
213
204
|
return self._handle_ratchet_result(ratchet_result)
|
|
214
205
|
|
|
215
206
|
def _handle_ratchet_result(self, ratchet_result: dict[str, t.Any]) -> bool:
|
|
216
|
-
"""Handle coverage ratchet results."""
|
|
217
207
|
if ratchet_result.get("success", False):
|
|
218
208
|
if ratchet_result.get("improved", False):
|
|
219
209
|
self._handle_coverage_improvement(ratchet_result)
|
|
220
210
|
return True
|
|
221
211
|
else:
|
|
222
|
-
|
|
223
|
-
f"[red]๐[/red]
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
212
|
+
if "message" in ratchet_result:
|
|
213
|
+
self.console.print(f"[red]๐[/ red] {ratchet_result['message']}")
|
|
214
|
+
else:
|
|
215
|
+
current = ratchet_result.get("current_coverage", 0)
|
|
216
|
+
previous = ratchet_result.get("previous_coverage", 0)
|
|
217
|
+
self.console.print(
|
|
218
|
+
f"[red]๐[/ red] Coverage regression: "
|
|
219
|
+
f"{current:.2f}% < {previous:.2f}%"
|
|
220
|
+
)
|
|
227
221
|
return False
|
|
228
222
|
|
|
229
223
|
def _handle_coverage_improvement(self, ratchet_result: dict[str, t.Any]) -> None:
|
|
230
|
-
"""Handle coverage improvements."""
|
|
231
224
|
improvement = ratchet_result.get("improvement", 0)
|
|
232
225
|
current = ratchet_result.get("current_coverage", 0)
|
|
233
226
|
|
|
234
227
|
self.console.print(
|
|
235
|
-
f"[green]๐[/green] Coverage improved by {improvement
|
|
236
|
-
f"to {current
|
|
228
|
+
f"[green]๐[/ green] Coverage improved by {improvement: .2f}% "
|
|
229
|
+
f"to {current: .2f}%"
|
|
237
230
|
)
|
|
238
231
|
|
|
239
232
|
def _extract_failure_lines(self, output: str) -> list[str]:
|
|
240
|
-
"""Extract failure information from test output."""
|
|
241
233
|
failures = []
|
|
242
234
|
lines = output.split("\n")
|
|
243
235
|
|
|
@@ -247,12 +239,10 @@ class TestManager:
|
|
|
247
239
|
):
|
|
248
240
|
failures.append(line.strip())
|
|
249
241
|
|
|
250
|
-
return failures[:10]
|
|
242
|
+
return failures[:10]
|
|
251
243
|
|
|
252
244
|
def _get_timeout(self, options: OptionsProtocol) -> int:
|
|
253
|
-
"""Get timeout for test execution."""
|
|
254
245
|
return self.command_builder.get_test_timeout(options)
|
|
255
246
|
|
|
256
247
|
|
|
257
|
-
# For backward compatibility, also export the old class name
|
|
258
248
|
TestManagementImpl = TestManager
|