crackerjack 0.30.3__py3-none-any.whl โ 0.31.7__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 +1005 -0
- crackerjack/RULES.md +380 -0
- crackerjack/__init__.py +42 -13
- crackerjack/__main__.py +227 -299
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +170 -0
- crackerjack/agents/coordinator.py +512 -0
- crackerjack/agents/documentation_agent.py +498 -0
- crackerjack/agents/dry_agent.py +388 -0
- crackerjack/agents/formatting_agent.py +245 -0
- crackerjack/agents/import_optimization_agent.py +281 -0
- crackerjack/agents/performance_agent.py +669 -0
- crackerjack/agents/proactive_agent.py +104 -0
- crackerjack/agents/refactoring_agent.py +788 -0
- crackerjack/agents/security_agent.py +529 -0
- crackerjack/agents/test_creation_agent.py +657 -0
- crackerjack/agents/test_specialist_agent.py +486 -0
- crackerjack/agents/tracker.py +212 -0
- crackerjack/api.py +560 -0
- crackerjack/cli/__init__.py +24 -0
- crackerjack/cli/facade.py +104 -0
- crackerjack/cli/handlers.py +267 -0
- crackerjack/cli/interactive.py +471 -0
- crackerjack/cli/options.py +409 -0
- crackerjack/cli/utils.py +18 -0
- crackerjack/code_cleaner.py +618 -928
- crackerjack/config/__init__.py +19 -0
- crackerjack/config/hooks.py +218 -0
- crackerjack/core/__init__.py +0 -0
- crackerjack/core/async_workflow_orchestrator.py +406 -0
- crackerjack/core/autofix_coordinator.py +200 -0
- crackerjack/core/container.py +104 -0
- crackerjack/core/enhanced_container.py +542 -0
- crackerjack/core/performance.py +243 -0
- crackerjack/core/phase_coordinator.py +585 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +826 -0
- crackerjack/dynamic_config.py +94 -103
- crackerjack/errors.py +263 -41
- crackerjack/executors/__init__.py +11 -0
- crackerjack/executors/async_hook_executor.py +431 -0
- crackerjack/executors/cached_hook_executor.py +242 -0
- crackerjack/executors/hook_executor.py +345 -0
- crackerjack/executors/individual_hook_executor.py +669 -0
- crackerjack/intelligence/__init__.py +44 -0
- crackerjack/intelligence/adaptive_learning.py +751 -0
- crackerjack/intelligence/agent_orchestrator.py +551 -0
- crackerjack/intelligence/agent_registry.py +414 -0
- crackerjack/intelligence/agent_selector.py +502 -0
- crackerjack/intelligence/integration.py +290 -0
- crackerjack/interactive.py +576 -315
- crackerjack/managers/__init__.py +11 -0
- crackerjack/managers/async_hook_manager.py +135 -0
- crackerjack/managers/hook_manager.py +137 -0
- crackerjack/managers/publish_manager.py +433 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +443 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +114 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +621 -0
- crackerjack/mcp/dashboard.py +636 -0
- crackerjack/mcp/enhanced_progress_monitor.py +479 -0
- crackerjack/mcp/file_monitor.py +336 -0
- crackerjack/mcp/progress_components.py +569 -0
- crackerjack/mcp/progress_monitor.py +949 -0
- crackerjack/mcp/rate_limiter.py +332 -0
- crackerjack/mcp/server.py +22 -0
- crackerjack/mcp/server_core.py +244 -0
- crackerjack/mcp/service_watchdog.py +501 -0
- crackerjack/mcp/state.py +395 -0
- crackerjack/mcp/task_manager.py +257 -0
- crackerjack/mcp/tools/__init__.py +17 -0
- crackerjack/mcp/tools/core_tools.py +249 -0
- crackerjack/mcp/tools/error_analyzer.py +308 -0
- crackerjack/mcp/tools/execution_tools.py +372 -0
- crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
- crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
- crackerjack/mcp/tools/intelligence_tools.py +314 -0
- crackerjack/mcp/tools/monitoring_tools.py +502 -0
- crackerjack/mcp/tools/proactive_tools.py +384 -0
- crackerjack/mcp/tools/progress_tools.py +217 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +565 -0
- crackerjack/mcp/websocket/__init__.py +14 -0
- crackerjack/mcp/websocket/app.py +39 -0
- crackerjack/mcp/websocket/endpoints.py +559 -0
- crackerjack/mcp/websocket/jobs.py +253 -0
- crackerjack/mcp/websocket/server.py +116 -0
- crackerjack/mcp/websocket/websocket_handler.py +78 -0
- crackerjack/mcp/websocket_server.py +10 -0
- crackerjack/models/__init__.py +31 -0
- crackerjack/models/config.py +93 -0
- crackerjack/models/config_adapter.py +230 -0
- crackerjack/models/protocols.py +118 -0
- crackerjack/models/task.py +154 -0
- crackerjack/monitoring/ai_agent_watchdog.py +450 -0
- crackerjack/monitoring/regression_prevention.py +638 -0
- crackerjack/orchestration/__init__.py +0 -0
- crackerjack/orchestration/advanced_orchestrator.py +970 -0
- crackerjack/orchestration/coverage_improvement.py +223 -0
- crackerjack/orchestration/execution_strategies.py +341 -0
- crackerjack/orchestration/test_progress_streamer.py +636 -0
- crackerjack/plugins/__init__.py +15 -0
- crackerjack/plugins/base.py +200 -0
- crackerjack/plugins/hooks.py +246 -0
- crackerjack/plugins/loader.py +335 -0
- crackerjack/plugins/managers.py +259 -0
- crackerjack/py313.py +8 -3
- crackerjack/services/__init__.py +22 -0
- crackerjack/services/cache.py +314 -0
- crackerjack/services/config.py +358 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +356 -0
- crackerjack/services/debug.py +736 -0
- crackerjack/services/dependency_monitor.py +617 -0
- crackerjack/services/enhanced_filesystem.py +439 -0
- crackerjack/services/file_hasher.py +151 -0
- crackerjack/services/filesystem.py +421 -0
- crackerjack/services/git.py +176 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +873 -0
- crackerjack/services/log_manager.py +286 -0
- crackerjack/services/logging.py +174 -0
- crackerjack/services/metrics.py +578 -0
- crackerjack/services/pattern_cache.py +362 -0
- crackerjack/services/pattern_detector.py +515 -0
- crackerjack/services/performance_benchmarks.py +653 -0
- crackerjack/services/security.py +163 -0
- crackerjack/services/server_manager.py +234 -0
- crackerjack/services/smart_scheduling.py +144 -0
- crackerjack/services/tool_version_service.py +61 -0
- crackerjack/services/unified_config.py +437 -0
- crackerjack/services/version_checker.py +248 -0
- crackerjack/slash_commands/__init__.py +14 -0
- crackerjack/slash_commands/init.md +122 -0
- crackerjack/slash_commands/run.md +163 -0
- crackerjack/slash_commands/status.md +127 -0
- crackerjack-0.31.7.dist-info/METADATA +742 -0
- crackerjack-0.31.7.dist-info/RECORD +149 -0
- crackerjack-0.31.7.dist-info/entry_points.txt +2 -0
- crackerjack/.gitignore +0 -34
- crackerjack/.libcst.codemod.yaml +0 -18
- crackerjack/.pdm.toml +0 -1
- crackerjack/crackerjack.py +0 -3805
- crackerjack/pyproject.toml +0 -286
- crackerjack-0.30.3.dist-info/METADATA +0 -1290
- crackerjack-0.30.3.dist-info/RECORD +0 -16
- {crackerjack-0.30.3.dist-info โ crackerjack-0.31.7.dist-info}/WHEEL +0 -0
- {crackerjack-0.30.3.dist-info โ crackerjack-0.31.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,443 @@
|
|
|
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
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
import typing as t
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.live import Live
|
|
16
|
+
|
|
17
|
+
from .test_progress import TestProgress
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestExecutor:
|
|
21
|
+
"""Handles test execution with real-time progress tracking."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, console: Console, pkg_path: Path) -> None:
|
|
24
|
+
self.console = console
|
|
25
|
+
self.pkg_path = pkg_path
|
|
26
|
+
|
|
27
|
+
def execute_with_progress(
|
|
28
|
+
self,
|
|
29
|
+
cmd: list[str],
|
|
30
|
+
timeout: int = 600,
|
|
31
|
+
) -> subprocess.CompletedProcess[str]:
|
|
32
|
+
"""Execute test command with live progress display."""
|
|
33
|
+
return self._execute_with_live_progress(cmd, timeout)
|
|
34
|
+
|
|
35
|
+
def execute_with_ai_progress(
|
|
36
|
+
self,
|
|
37
|
+
cmd: list[str],
|
|
38
|
+
progress_callback: t.Callable[[dict[str, t.Any]], None],
|
|
39
|
+
timeout: int = 600,
|
|
40
|
+
) -> subprocess.CompletedProcess[str]:
|
|
41
|
+
"""Execute test command with AI-compatible progress callbacks."""
|
|
42
|
+
return self._run_test_command_with_ai_progress(cmd, progress_callback, timeout)
|
|
43
|
+
|
|
44
|
+
def _execute_with_live_progress(
|
|
45
|
+
self, cmd: list[str], timeout: int
|
|
46
|
+
) -> subprocess.CompletedProcess[str]:
|
|
47
|
+
"""Execute tests with Rich live progress display."""
|
|
48
|
+
progress = self._initialize_progress()
|
|
49
|
+
|
|
50
|
+
with Live(progress.format_progress(), console=self.console) as live:
|
|
51
|
+
env = self._setup_test_environment()
|
|
52
|
+
|
|
53
|
+
process = subprocess.Popen(
|
|
54
|
+
cmd,
|
|
55
|
+
cwd=self.pkg_path,
|
|
56
|
+
stdout=subprocess.PIPE,
|
|
57
|
+
stderr=subprocess.PIPE,
|
|
58
|
+
text=True,
|
|
59
|
+
bufsize=1,
|
|
60
|
+
universal_newlines=True,
|
|
61
|
+
env=env,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Start reader threads
|
|
65
|
+
stdout_thread, stderr_thread, monitor_thread = self._start_reader_threads(
|
|
66
|
+
process, progress, live
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Wait for completion
|
|
70
|
+
try:
|
|
71
|
+
process.wait(timeout=timeout)
|
|
72
|
+
except subprocess.TimeoutExpired:
|
|
73
|
+
self._handle_progress_error(
|
|
74
|
+
process, progress, "Test execution timed out"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Cleanup
|
|
78
|
+
self._cleanup_threads([stdout_thread, stderr_thread, monitor_thread])
|
|
79
|
+
|
|
80
|
+
# Return completed process with string output
|
|
81
|
+
stdout_str = process.stdout.read() if process.stdout else ""
|
|
82
|
+
stderr_str = process.stderr.read() if process.stderr else ""
|
|
83
|
+
return subprocess.CompletedProcess(
|
|
84
|
+
cmd, process.returncode, stdout_str, stderr_str
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def _run_test_command_with_ai_progress(
|
|
88
|
+
self,
|
|
89
|
+
cmd: list[str],
|
|
90
|
+
progress_callback: t.Callable[[dict[str, t.Any]], None],
|
|
91
|
+
timeout: int = 600,
|
|
92
|
+
) -> subprocess.CompletedProcess[str]:
|
|
93
|
+
"""Execute test command with AI progress callbacks."""
|
|
94
|
+
progress = self._initialize_progress()
|
|
95
|
+
env = self._setup_coverage_env()
|
|
96
|
+
|
|
97
|
+
result = self._execute_test_process_with_progress(
|
|
98
|
+
cmd, env, progress, progress_callback, timeout
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
def _initialize_progress(self) -> TestProgress:
|
|
104
|
+
"""Initialize progress tracker."""
|
|
105
|
+
progress = TestProgress()
|
|
106
|
+
progress.start_time = time.time()
|
|
107
|
+
return progress
|
|
108
|
+
|
|
109
|
+
def _setup_test_environment(self) -> dict[str, str]:
|
|
110
|
+
"""Set up environment variables for test execution."""
|
|
111
|
+
import os
|
|
112
|
+
|
|
113
|
+
cache_dir = Path.home() / ".cache" / "crackerjack" / "coverage"
|
|
114
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
|
|
116
|
+
env = os.environ.copy()
|
|
117
|
+
env["COVERAGE_FILE"] = str(cache_dir / ".coverage")
|
|
118
|
+
env["PYTEST_CURRENT_TEST"] = ""
|
|
119
|
+
return env
|
|
120
|
+
|
|
121
|
+
def _setup_coverage_env(self) -> dict[str, str]:
|
|
122
|
+
"""Set up coverage environment for AI mode."""
|
|
123
|
+
import os
|
|
124
|
+
from pathlib import Path
|
|
125
|
+
|
|
126
|
+
cache_dir = Path.home() / ".cache" / "crackerjack" / "coverage"
|
|
127
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
|
|
129
|
+
env = os.environ.copy()
|
|
130
|
+
env["COVERAGE_FILE"] = str(cache_dir / ".coverage")
|
|
131
|
+
return env
|
|
132
|
+
|
|
133
|
+
def _start_reader_threads(
|
|
134
|
+
self, process: subprocess.Popen[str], progress: TestProgress, live: Live
|
|
135
|
+
) -> tuple[threading.Thread, threading.Thread, threading.Thread]:
|
|
136
|
+
"""Start threads for reading stdout, stderr, and monitoring."""
|
|
137
|
+
stdout_thread = self._create_stdout_reader(process, progress, live)
|
|
138
|
+
stderr_thread = self._create_stderr_reader(process, progress, live)
|
|
139
|
+
monitor_thread = self._create_monitor_thread(progress)
|
|
140
|
+
|
|
141
|
+
stdout_thread.start()
|
|
142
|
+
stderr_thread.start()
|
|
143
|
+
monitor_thread.start()
|
|
144
|
+
|
|
145
|
+
return stdout_thread, stderr_thread, monitor_thread
|
|
146
|
+
|
|
147
|
+
def _create_stdout_reader(
|
|
148
|
+
self, process: subprocess.Popen[str], progress: TestProgress, live: Live
|
|
149
|
+
) -> threading.Thread:
|
|
150
|
+
"""Create thread for reading stdout."""
|
|
151
|
+
|
|
152
|
+
def read_output() -> None:
|
|
153
|
+
if process.stdout:
|
|
154
|
+
for line in iter(process.stdout.readline, ""):
|
|
155
|
+
if line.strip():
|
|
156
|
+
self._process_test_output_line(line.strip(), progress)
|
|
157
|
+
self._update_display_if_needed(progress, live)
|
|
158
|
+
if progress.is_complete:
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
return threading.Thread(target=read_output, daemon=True)
|
|
162
|
+
|
|
163
|
+
def _create_stderr_reader(
|
|
164
|
+
self, process: subprocess.Popen[str], progress: TestProgress, live: Live
|
|
165
|
+
) -> threading.Thread:
|
|
166
|
+
"""Create thread for reading stderr."""
|
|
167
|
+
|
|
168
|
+
def read_stderr() -> None:
|
|
169
|
+
if process.stderr:
|
|
170
|
+
for line in iter(process.stderr.readline, ""):
|
|
171
|
+
if line.strip() and "warning" not in line.lower():
|
|
172
|
+
progress.update(current_test=f"โ ๏ธ {line.strip()}")
|
|
173
|
+
self._update_display_if_needed(progress, live)
|
|
174
|
+
|
|
175
|
+
return threading.Thread(target=read_stderr, daemon=True)
|
|
176
|
+
|
|
177
|
+
def _create_monitor_thread(self, progress: TestProgress) -> threading.Thread:
|
|
178
|
+
"""Create thread for monitoring stuck tests."""
|
|
179
|
+
|
|
180
|
+
def monitor_stuck_tests() -> None:
|
|
181
|
+
last_update = time.time()
|
|
182
|
+
last_test = ""
|
|
183
|
+
|
|
184
|
+
while not progress.is_complete:
|
|
185
|
+
time.sleep(5)
|
|
186
|
+
current_time = time.time()
|
|
187
|
+
|
|
188
|
+
if (
|
|
189
|
+
progress.current_test == last_test
|
|
190
|
+
and current_time - last_update > 30
|
|
191
|
+
):
|
|
192
|
+
self._mark_test_as_stuck(progress, progress.current_test)
|
|
193
|
+
last_update = current_time
|
|
194
|
+
elif progress.current_test != last_test:
|
|
195
|
+
last_test = progress.current_test
|
|
196
|
+
last_update = current_time
|
|
197
|
+
|
|
198
|
+
return threading.Thread(target=monitor_stuck_tests, daemon=True)
|
|
199
|
+
|
|
200
|
+
def _process_test_output_line(self, line: str, progress: TestProgress) -> None:
|
|
201
|
+
"""Process a single line of test output."""
|
|
202
|
+
self._parse_test_line(line, progress)
|
|
203
|
+
|
|
204
|
+
def _parse_test_line(self, line: str, progress: TestProgress) -> None:
|
|
205
|
+
"""Parse test output line and update progress."""
|
|
206
|
+
# Handle collection completion
|
|
207
|
+
if self._handle_collection_completion(line, progress):
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
# Handle session events
|
|
211
|
+
if self._handle_session_events(line, progress):
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
# Handle collection progress
|
|
215
|
+
if self._handle_collection_progress(line, progress):
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
# Handle test execution
|
|
219
|
+
if self._handle_test_execution(line, progress):
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
def _handle_collection_completion(self, line: str, progress: TestProgress) -> bool:
|
|
223
|
+
"""Handle test collection completion."""
|
|
224
|
+
if "collected" in line and ("item" in line or "test" in line):
|
|
225
|
+
match = re.search(r"(\d+) (?:item|test)", line)
|
|
226
|
+
if match:
|
|
227
|
+
progress.update(
|
|
228
|
+
total_tests=int(match.group(1)),
|
|
229
|
+
is_collecting=False,
|
|
230
|
+
collection_status="Collection complete",
|
|
231
|
+
)
|
|
232
|
+
return True
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
def _handle_session_events(self, line: str, progress: TestProgress) -> bool:
|
|
236
|
+
"""Handle pytest session events."""
|
|
237
|
+
if "session starts" in line and progress.collection_status != "Session started":
|
|
238
|
+
progress.update(collection_status="Session started")
|
|
239
|
+
return True
|
|
240
|
+
elif (
|
|
241
|
+
"test session starts" in line
|
|
242
|
+
and progress.collection_status != "Test collection started"
|
|
243
|
+
):
|
|
244
|
+
progress.update(collection_status="Test collection started")
|
|
245
|
+
return True
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
def _handle_collection_progress(self, line: str, progress: TestProgress) -> bool:
|
|
249
|
+
"""Handle collection progress updates."""
|
|
250
|
+
if progress.is_collecting:
|
|
251
|
+
# Look for file discovery patterns
|
|
252
|
+
if line.endswith(".py") and ("test_" in line or "_test.py" in line):
|
|
253
|
+
with progress._lock:
|
|
254
|
+
if line not in progress._seen_files:
|
|
255
|
+
progress._seen_files.add(line)
|
|
256
|
+
progress.files_discovered += 1
|
|
257
|
+
progress.collection_status = (
|
|
258
|
+
f"Found {progress.files_discovered} test files..."
|
|
259
|
+
)
|
|
260
|
+
return True
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
def _handle_test_execution(self, line: str, progress: TestProgress) -> bool:
|
|
264
|
+
"""Handle test execution progress."""
|
|
265
|
+
# Test result patterns
|
|
266
|
+
if " PASSED " in line:
|
|
267
|
+
progress.update(passed=progress.passed + 1)
|
|
268
|
+
self._extract_current_test(line, progress)
|
|
269
|
+
return True
|
|
270
|
+
elif " FAILED " in line:
|
|
271
|
+
progress.update(failed=progress.failed + 1)
|
|
272
|
+
self._extract_current_test(line, progress)
|
|
273
|
+
return True
|
|
274
|
+
elif " SKIPPED " in line:
|
|
275
|
+
progress.update(skipped=progress.skipped + 1)
|
|
276
|
+
self._extract_current_test(line, progress)
|
|
277
|
+
return True
|
|
278
|
+
elif " ERROR " in line:
|
|
279
|
+
progress.update(errors=progress.errors + 1)
|
|
280
|
+
self._extract_current_test(line, progress)
|
|
281
|
+
return True
|
|
282
|
+
elif "::" in line and any(x in line for x in ("RUNNING", "test_")):
|
|
283
|
+
self._handle_running_test(line, progress)
|
|
284
|
+
return True
|
|
285
|
+
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
def _handle_running_test(self, line: str, progress: TestProgress) -> None:
|
|
289
|
+
"""Handle currently running test indicator."""
|
|
290
|
+
if "::" in line:
|
|
291
|
+
# Extract test name from line
|
|
292
|
+
test_parts = line.split("::")
|
|
293
|
+
if len(test_parts) >= 2:
|
|
294
|
+
test_name = "::".join(test_parts[-2:])
|
|
295
|
+
progress.update(current_test=test_name)
|
|
296
|
+
|
|
297
|
+
def _extract_current_test(self, line: str, progress: TestProgress) -> None:
|
|
298
|
+
"""Extract current test name from output line."""
|
|
299
|
+
if "::" in line:
|
|
300
|
+
# Extract test identifier
|
|
301
|
+
parts = line.split(" ")
|
|
302
|
+
for part in parts:
|
|
303
|
+
if "::" in part:
|
|
304
|
+
progress.update(current_test=part)
|
|
305
|
+
break
|
|
306
|
+
|
|
307
|
+
def _update_display_if_needed(self, progress: TestProgress, live: Live) -> None:
|
|
308
|
+
"""Update display if enough time has passed or significant change occurred."""
|
|
309
|
+
if self._should_refresh_display(progress):
|
|
310
|
+
live.update(progress.format_progress())
|
|
311
|
+
|
|
312
|
+
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
|
+
return (
|
|
316
|
+
progress.is_complete
|
|
317
|
+
or progress.total_tests > 0
|
|
318
|
+
or len(progress.current_test) > 0
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def _mark_test_as_stuck(self, progress: TestProgress, test_name: str) -> None:
|
|
322
|
+
"""Mark a test as potentially stuck."""
|
|
323
|
+
if test_name:
|
|
324
|
+
progress.update(current_test=f"๐ {test_name} (slow)")
|
|
325
|
+
|
|
326
|
+
def _cleanup_threads(self, threads: list[threading.Thread]) -> None:
|
|
327
|
+
"""Clean up reader threads."""
|
|
328
|
+
for thread in threads:
|
|
329
|
+
if thread.is_alive():
|
|
330
|
+
thread.join(timeout=1.0)
|
|
331
|
+
|
|
332
|
+
def _handle_progress_error(
|
|
333
|
+
self, process: subprocess.Popen[str], progress: TestProgress, error_msg: str
|
|
334
|
+
) -> None:
|
|
335
|
+
"""Handle progress tracking errors."""
|
|
336
|
+
process.terminate()
|
|
337
|
+
progress.update(is_complete=True, current_test=f"โ {error_msg}")
|
|
338
|
+
|
|
339
|
+
def _execute_test_process_with_progress(
|
|
340
|
+
self,
|
|
341
|
+
cmd: list[str],
|
|
342
|
+
env: dict[str, str],
|
|
343
|
+
progress: TestProgress,
|
|
344
|
+
progress_callback: t.Callable[[dict[str, t.Any]], None],
|
|
345
|
+
timeout: int,
|
|
346
|
+
) -> subprocess.CompletedProcess[str]:
|
|
347
|
+
"""Execute test process with AI progress tracking."""
|
|
348
|
+
process = subprocess.Popen(
|
|
349
|
+
cmd,
|
|
350
|
+
cwd=self.pkg_path,
|
|
351
|
+
stdout=subprocess.PIPE,
|
|
352
|
+
stderr=subprocess.PIPE,
|
|
353
|
+
text=True,
|
|
354
|
+
env=env,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
stdout_lines = self._read_stdout_with_progress(
|
|
358
|
+
process, progress, progress_callback
|
|
359
|
+
)
|
|
360
|
+
stderr_lines = self._read_stderr_lines(process)
|
|
361
|
+
|
|
362
|
+
return_code = self._wait_for_process_completion(process, timeout)
|
|
363
|
+
|
|
364
|
+
return subprocess.CompletedProcess(
|
|
365
|
+
cmd, return_code, "\n".join(stdout_lines), "\n".join(stderr_lines)
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
def _read_stdout_with_progress(
|
|
369
|
+
self,
|
|
370
|
+
process: subprocess.Popen[str],
|
|
371
|
+
progress: TestProgress,
|
|
372
|
+
progress_callback: t.Callable[[dict[str, t.Any]], None],
|
|
373
|
+
) -> list[str]:
|
|
374
|
+
"""Read stdout with progress updates."""
|
|
375
|
+
stdout_lines = []
|
|
376
|
+
|
|
377
|
+
if process.stdout:
|
|
378
|
+
for line in iter(process.stdout.readline, ""):
|
|
379
|
+
if not line:
|
|
380
|
+
break
|
|
381
|
+
|
|
382
|
+
line = line.strip()
|
|
383
|
+
if line:
|
|
384
|
+
stdout_lines.append(line)
|
|
385
|
+
self._process_test_output_line(line, progress)
|
|
386
|
+
self._emit_ai_progress(progress, progress_callback)
|
|
387
|
+
|
|
388
|
+
return stdout_lines
|
|
389
|
+
|
|
390
|
+
def _read_stderr_lines(self, process: subprocess.Popen[str]) -> list[str]:
|
|
391
|
+
"""Read stderr lines."""
|
|
392
|
+
stderr_lines = []
|
|
393
|
+
|
|
394
|
+
if process.stderr:
|
|
395
|
+
for line in iter(process.stderr.readline, ""):
|
|
396
|
+
if not line:
|
|
397
|
+
break
|
|
398
|
+
line = line.strip()
|
|
399
|
+
if line:
|
|
400
|
+
stderr_lines.append(line)
|
|
401
|
+
|
|
402
|
+
return stderr_lines
|
|
403
|
+
|
|
404
|
+
def _wait_for_process_completion(
|
|
405
|
+
self, process: subprocess.Popen[str], timeout: int
|
|
406
|
+
) -> int:
|
|
407
|
+
"""Wait for process completion with timeout."""
|
|
408
|
+
try:
|
|
409
|
+
process.wait(timeout=timeout)
|
|
410
|
+
return process.returncode
|
|
411
|
+
except subprocess.TimeoutExpired:
|
|
412
|
+
process.terminate()
|
|
413
|
+
return -1
|
|
414
|
+
|
|
415
|
+
def _emit_ai_progress(
|
|
416
|
+
self,
|
|
417
|
+
progress: TestProgress,
|
|
418
|
+
progress_callback: t.Callable[[dict[str, t.Any]], None],
|
|
419
|
+
) -> None:
|
|
420
|
+
"""Emit progress update for AI consumption."""
|
|
421
|
+
progress_data = {
|
|
422
|
+
"type": "test_progress",
|
|
423
|
+
"total_tests": progress.total_tests,
|
|
424
|
+
"completed": progress.completed,
|
|
425
|
+
"passed": progress.passed,
|
|
426
|
+
"failed": progress.failed,
|
|
427
|
+
"skipped": progress.skipped,
|
|
428
|
+
"errors": progress.errors,
|
|
429
|
+
"current_test": progress.current_test,
|
|
430
|
+
"elapsed_time": progress.elapsed_time,
|
|
431
|
+
"is_collecting": progress.is_collecting,
|
|
432
|
+
"is_complete": progress.is_complete,
|
|
433
|
+
"collection_status": progress.collection_status,
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if progress.eta_seconds:
|
|
437
|
+
progress_data["eta_seconds"] = progress.eta_seconds
|
|
438
|
+
|
|
439
|
+
from contextlib import suppress
|
|
440
|
+
|
|
441
|
+
with suppress(Exception):
|
|
442
|
+
# Don't let progress callback errors affect test execution
|
|
443
|
+
progress_callback(progress_data)
|
|
@@ -0,0 +1,258 @@
|
|
|
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
|
+
import subprocess
|
|
14
|
+
import time
|
|
15
|
+
import typing as t
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
|
|
20
|
+
from crackerjack.models.protocols import OptionsProtocol
|
|
21
|
+
from crackerjack.services.coverage_ratchet import CoverageRatchetService
|
|
22
|
+
|
|
23
|
+
from .test_command_builder import TestCommandBuilder
|
|
24
|
+
from .test_executor import TestExecutor
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestManager:
|
|
28
|
+
"""Refactored test manager with modular architecture."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, console: Console, pkg_path: Path) -> None:
|
|
31
|
+
self.console = console
|
|
32
|
+
self.pkg_path = pkg_path
|
|
33
|
+
|
|
34
|
+
# Initialize specialized components
|
|
35
|
+
self.executor = TestExecutor(console, pkg_path)
|
|
36
|
+
self.command_builder = TestCommandBuilder(pkg_path)
|
|
37
|
+
self.coverage_ratchet = CoverageRatchetService(pkg_path, console)
|
|
38
|
+
|
|
39
|
+
# State
|
|
40
|
+
self._last_test_failures: list[str] = []
|
|
41
|
+
self._progress_callback: t.Callable[[dict[str, t.Any]], None] | None = None
|
|
42
|
+
self.coverage_ratchet_enabled = True
|
|
43
|
+
|
|
44
|
+
def set_progress_callback(
|
|
45
|
+
self,
|
|
46
|
+
callback: t.Callable[[dict[str, t.Any]], None] | None,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Set callback for AI mode structured progress updates."""
|
|
49
|
+
self._progress_callback = callback
|
|
50
|
+
|
|
51
|
+
def set_coverage_ratchet_enabled(self, enabled: bool) -> None:
|
|
52
|
+
"""Enable or disable the coverage ratchet system."""
|
|
53
|
+
self.coverage_ratchet_enabled = enabled
|
|
54
|
+
if enabled:
|
|
55
|
+
self.console.print(
|
|
56
|
+
"[cyan]๐[/cyan] Coverage ratchet enabled - targeting 100% coverage"
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
self.console.print("[yellow]โ ๏ธ[/yellow] Coverage ratchet disabled")
|
|
60
|
+
|
|
61
|
+
def run_tests(self, options: OptionsProtocol) -> bool:
|
|
62
|
+
"""Run tests with comprehensive progress tracking and coverage analysis."""
|
|
63
|
+
start_time = time.time()
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
result = self._execute_test_workflow(options)
|
|
67
|
+
duration = time.time() - start_time
|
|
68
|
+
|
|
69
|
+
if result:
|
|
70
|
+
return self._handle_test_success(result.stdout, duration)
|
|
71
|
+
else:
|
|
72
|
+
return self._handle_test_failure(
|
|
73
|
+
result.stderr if result else "", duration
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
except Exception as e:
|
|
77
|
+
return self._handle_test_error(start_time, e)
|
|
78
|
+
|
|
79
|
+
def run_specific_tests(self, test_pattern: str) -> bool:
|
|
80
|
+
"""Run tests matching a specific pattern."""
|
|
81
|
+
self.console.print(f"[cyan]๐งช[/cyan] Running tests matching: {test_pattern}")
|
|
82
|
+
|
|
83
|
+
cmd = self.command_builder.build_specific_test_command(test_pattern)
|
|
84
|
+
result = self.executor.execute_with_progress(cmd)
|
|
85
|
+
|
|
86
|
+
success = result.returncode == 0
|
|
87
|
+
if success:
|
|
88
|
+
self.console.print("[green]โ
[/green] Specific tests passed")
|
|
89
|
+
else:
|
|
90
|
+
self.console.print("[red]โ[/red] Some specific tests failed")
|
|
91
|
+
|
|
92
|
+
return success
|
|
93
|
+
|
|
94
|
+
def validate_test_environment(self) -> bool:
|
|
95
|
+
"""Validate test environment and configuration."""
|
|
96
|
+
if not self.has_tests():
|
|
97
|
+
self.console.print("[yellow]โ ๏ธ[/yellow] No tests found")
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
# Test basic pytest collection
|
|
101
|
+
cmd = self.command_builder.build_validation_command()
|
|
102
|
+
result = subprocess.run(cmd, cwd=self.pkg_path, capture_output=True, text=True)
|
|
103
|
+
|
|
104
|
+
if result.returncode != 0:
|
|
105
|
+
self.console.print("[red]โ[/red] Test environment validation failed")
|
|
106
|
+
self.console.print(result.stderr)
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
self.console.print("[green]โ
[/green] Test environment validated")
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
def get_coverage_ratchet_status(self) -> dict[str, t.Any]:
|
|
113
|
+
"""Get comprehensive coverage ratchet status."""
|
|
114
|
+
return self.coverage_ratchet.get_status_report()
|
|
115
|
+
|
|
116
|
+
def get_test_stats(self) -> dict[str, t.Any]:
|
|
117
|
+
"""Get comprehensive test execution statistics."""
|
|
118
|
+
return {
|
|
119
|
+
"has_tests": self.has_tests(),
|
|
120
|
+
"coverage_ratchet_enabled": self.coverage_ratchet_enabled,
|
|
121
|
+
"last_failures_count": len(self._last_test_failures),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
def get_test_failures(self) -> list[str]:
|
|
125
|
+
"""Get list of recent test failures."""
|
|
126
|
+
return self._last_test_failures.copy()
|
|
127
|
+
|
|
128
|
+
def get_test_command(self, options: OptionsProtocol) -> list[str]:
|
|
129
|
+
"""Get the test command that would be executed."""
|
|
130
|
+
return self.command_builder.build_command(options)
|
|
131
|
+
|
|
132
|
+
def get_coverage_report(self) -> str | None:
|
|
133
|
+
"""Get coverage report if available."""
|
|
134
|
+
try:
|
|
135
|
+
return self.coverage_ratchet.get_coverage_report()
|
|
136
|
+
except Exception:
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
def has_tests(self) -> bool:
|
|
140
|
+
"""Check if project has tests."""
|
|
141
|
+
test_directories = ["tests", "test"]
|
|
142
|
+
test_files = ["test_*.py", "*_test.py"]
|
|
143
|
+
|
|
144
|
+
for test_dir in test_directories:
|
|
145
|
+
test_path = self.pkg_path / test_dir
|
|
146
|
+
if test_path.exists() and test_path.is_dir():
|
|
147
|
+
for test_file_pattern in test_files:
|
|
148
|
+
if list(test_path.glob(f"**/{test_file_pattern}")):
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
# Check for test files in root directory
|
|
152
|
+
for test_file_pattern in test_files:
|
|
153
|
+
if list(self.pkg_path.glob(test_file_pattern)):
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
# Private implementation methods
|
|
159
|
+
|
|
160
|
+
def _execute_test_workflow(
|
|
161
|
+
self, options: OptionsProtocol
|
|
162
|
+
) -> subprocess.CompletedProcess[str]:
|
|
163
|
+
"""Execute the complete test workflow."""
|
|
164
|
+
self._print_test_start_message(options)
|
|
165
|
+
|
|
166
|
+
cmd = self.command_builder.build_command(options)
|
|
167
|
+
|
|
168
|
+
if self._progress_callback:
|
|
169
|
+
return self.executor.execute_with_ai_progress(
|
|
170
|
+
cmd, self._progress_callback, self._get_timeout(options)
|
|
171
|
+
)
|
|
172
|
+
return self.executor.execute_with_progress(cmd, self._get_timeout(options))
|
|
173
|
+
|
|
174
|
+
def _print_test_start_message(self, options: OptionsProtocol) -> None:
|
|
175
|
+
"""Print test execution start message."""
|
|
176
|
+
workers = self.command_builder.get_optimal_workers(options)
|
|
177
|
+
timeout = self.command_builder.get_test_timeout(options)
|
|
178
|
+
|
|
179
|
+
self.console.print(
|
|
180
|
+
f"[cyan]๐งช[/cyan] Running tests (workers: {workers}, timeout: {timeout}s)"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def _handle_test_success(self, output: str, duration: float) -> bool:
|
|
184
|
+
"""Handle successful test execution."""
|
|
185
|
+
self.console.print(f"[green]โ
[/green] Tests passed in {duration:.1f}s")
|
|
186
|
+
|
|
187
|
+
if self.coverage_ratchet_enabled:
|
|
188
|
+
return self._process_coverage_ratchet()
|
|
189
|
+
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
def _handle_test_failure(self, output: str, duration: float) -> bool:
|
|
193
|
+
"""Handle failed test execution."""
|
|
194
|
+
self.console.print(f"[red]โ[/red] Tests failed in {duration:.1f}s")
|
|
195
|
+
|
|
196
|
+
self._last_test_failures = self._extract_failure_lines(output)
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
def _handle_test_error(self, start_time: float, error: Exception) -> bool:
|
|
200
|
+
"""Handle test execution errors."""
|
|
201
|
+
duration = time.time() - start_time
|
|
202
|
+
self.console.print(
|
|
203
|
+
f"[red]๐ฅ[/red] Test execution error after {duration:.1f}s: {error}"
|
|
204
|
+
)
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
def _process_coverage_ratchet(self) -> bool:
|
|
208
|
+
"""Process coverage ratchet checks."""
|
|
209
|
+
if not self.coverage_ratchet_enabled:
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
ratchet_result = self.coverage_ratchet.check_and_update_coverage()
|
|
213
|
+
return self._handle_ratchet_result(ratchet_result)
|
|
214
|
+
|
|
215
|
+
def _handle_ratchet_result(self, ratchet_result: dict[str, t.Any]) -> bool:
|
|
216
|
+
"""Handle coverage ratchet results."""
|
|
217
|
+
if ratchet_result.get("success", False):
|
|
218
|
+
if ratchet_result.get("improved", False):
|
|
219
|
+
self._handle_coverage_improvement(ratchet_result)
|
|
220
|
+
return True
|
|
221
|
+
else:
|
|
222
|
+
self.console.print(
|
|
223
|
+
f"[red]๐[/red] Coverage regression: "
|
|
224
|
+
f"{ratchet_result.get('current_coverage', 0):.2f}% < "
|
|
225
|
+
f"{ratchet_result.get('previous_coverage', 0):.2f}%"
|
|
226
|
+
)
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
def _handle_coverage_improvement(self, ratchet_result: dict[str, t.Any]) -> None:
|
|
230
|
+
"""Handle coverage improvements."""
|
|
231
|
+
improvement = ratchet_result.get("improvement", 0)
|
|
232
|
+
current = ratchet_result.get("current_coverage", 0)
|
|
233
|
+
|
|
234
|
+
self.console.print(
|
|
235
|
+
f"[green]๐[/green] Coverage improved by {improvement:.2f}% "
|
|
236
|
+
f"to {current:.2f}%"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def _extract_failure_lines(self, output: str) -> list[str]:
|
|
240
|
+
"""Extract failure information from test output."""
|
|
241
|
+
failures = []
|
|
242
|
+
lines = output.split("\n")
|
|
243
|
+
|
|
244
|
+
for line in lines:
|
|
245
|
+
if any(
|
|
246
|
+
keyword in line for keyword in ("FAILED", "ERROR", "AssertionError")
|
|
247
|
+
):
|
|
248
|
+
failures.append(line.strip())
|
|
249
|
+
|
|
250
|
+
return failures[:10] # Limit to first 10 failures
|
|
251
|
+
|
|
252
|
+
def _get_timeout(self, options: OptionsProtocol) -> int:
|
|
253
|
+
"""Get timeout for test execution."""
|
|
254
|
+
return self.command_builder.get_test_timeout(options)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# For backward compatibility, also export the old class name
|
|
258
|
+
TestManagementImpl = TestManager
|