crackerjack 0.29.0__py3-none-any.whl → 0.31.4__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.

Files changed (158) hide show
  1. crackerjack/CLAUDE.md +1005 -0
  2. crackerjack/RULES.md +380 -0
  3. crackerjack/__init__.py +42 -13
  4. crackerjack/__main__.py +225 -253
  5. crackerjack/agents/__init__.py +41 -0
  6. crackerjack/agents/architect_agent.py +281 -0
  7. crackerjack/agents/base.py +169 -0
  8. crackerjack/agents/coordinator.py +512 -0
  9. crackerjack/agents/documentation_agent.py +498 -0
  10. crackerjack/agents/dry_agent.py +388 -0
  11. crackerjack/agents/formatting_agent.py +245 -0
  12. crackerjack/agents/import_optimization_agent.py +281 -0
  13. crackerjack/agents/performance_agent.py +669 -0
  14. crackerjack/agents/proactive_agent.py +104 -0
  15. crackerjack/agents/refactoring_agent.py +788 -0
  16. crackerjack/agents/security_agent.py +529 -0
  17. crackerjack/agents/test_creation_agent.py +652 -0
  18. crackerjack/agents/test_specialist_agent.py +486 -0
  19. crackerjack/agents/tracker.py +212 -0
  20. crackerjack/api.py +560 -0
  21. crackerjack/cli/__init__.py +24 -0
  22. crackerjack/cli/facade.py +104 -0
  23. crackerjack/cli/handlers.py +267 -0
  24. crackerjack/cli/interactive.py +471 -0
  25. crackerjack/cli/options.py +401 -0
  26. crackerjack/cli/utils.py +18 -0
  27. crackerjack/code_cleaner.py +670 -0
  28. crackerjack/config/__init__.py +19 -0
  29. crackerjack/config/hooks.py +218 -0
  30. crackerjack/core/__init__.py +0 -0
  31. crackerjack/core/async_workflow_orchestrator.py +406 -0
  32. crackerjack/core/autofix_coordinator.py +200 -0
  33. crackerjack/core/container.py +104 -0
  34. crackerjack/core/enhanced_container.py +542 -0
  35. crackerjack/core/performance.py +243 -0
  36. crackerjack/core/phase_coordinator.py +561 -0
  37. crackerjack/core/proactive_workflow.py +316 -0
  38. crackerjack/core/session_coordinator.py +289 -0
  39. crackerjack/core/workflow_orchestrator.py +640 -0
  40. crackerjack/dynamic_config.py +577 -0
  41. crackerjack/errors.py +263 -41
  42. crackerjack/executors/__init__.py +11 -0
  43. crackerjack/executors/async_hook_executor.py +431 -0
  44. crackerjack/executors/cached_hook_executor.py +242 -0
  45. crackerjack/executors/hook_executor.py +345 -0
  46. crackerjack/executors/individual_hook_executor.py +669 -0
  47. crackerjack/intelligence/__init__.py +44 -0
  48. crackerjack/intelligence/adaptive_learning.py +751 -0
  49. crackerjack/intelligence/agent_orchestrator.py +551 -0
  50. crackerjack/intelligence/agent_registry.py +414 -0
  51. crackerjack/intelligence/agent_selector.py +502 -0
  52. crackerjack/intelligence/integration.py +290 -0
  53. crackerjack/interactive.py +576 -315
  54. crackerjack/managers/__init__.py +11 -0
  55. crackerjack/managers/async_hook_manager.py +135 -0
  56. crackerjack/managers/hook_manager.py +137 -0
  57. crackerjack/managers/publish_manager.py +411 -0
  58. crackerjack/managers/test_command_builder.py +151 -0
  59. crackerjack/managers/test_executor.py +435 -0
  60. crackerjack/managers/test_manager.py +258 -0
  61. crackerjack/managers/test_manager_backup.py +1124 -0
  62. crackerjack/managers/test_progress.py +144 -0
  63. crackerjack/mcp/__init__.py +0 -0
  64. crackerjack/mcp/cache.py +336 -0
  65. crackerjack/mcp/client_runner.py +104 -0
  66. crackerjack/mcp/context.py +615 -0
  67. crackerjack/mcp/dashboard.py +636 -0
  68. crackerjack/mcp/enhanced_progress_monitor.py +479 -0
  69. crackerjack/mcp/file_monitor.py +336 -0
  70. crackerjack/mcp/progress_components.py +569 -0
  71. crackerjack/mcp/progress_monitor.py +949 -0
  72. crackerjack/mcp/rate_limiter.py +332 -0
  73. crackerjack/mcp/server.py +22 -0
  74. crackerjack/mcp/server_core.py +244 -0
  75. crackerjack/mcp/service_watchdog.py +501 -0
  76. crackerjack/mcp/state.py +395 -0
  77. crackerjack/mcp/task_manager.py +257 -0
  78. crackerjack/mcp/tools/__init__.py +17 -0
  79. crackerjack/mcp/tools/core_tools.py +249 -0
  80. crackerjack/mcp/tools/error_analyzer.py +308 -0
  81. crackerjack/mcp/tools/execution_tools.py +370 -0
  82. crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
  83. crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
  84. crackerjack/mcp/tools/intelligence_tools.py +314 -0
  85. crackerjack/mcp/tools/monitoring_tools.py +502 -0
  86. crackerjack/mcp/tools/proactive_tools.py +384 -0
  87. crackerjack/mcp/tools/progress_tools.py +141 -0
  88. crackerjack/mcp/tools/utility_tools.py +341 -0
  89. crackerjack/mcp/tools/workflow_executor.py +360 -0
  90. crackerjack/mcp/websocket/__init__.py +14 -0
  91. crackerjack/mcp/websocket/app.py +39 -0
  92. crackerjack/mcp/websocket/endpoints.py +559 -0
  93. crackerjack/mcp/websocket/jobs.py +253 -0
  94. crackerjack/mcp/websocket/server.py +116 -0
  95. crackerjack/mcp/websocket/websocket_handler.py +78 -0
  96. crackerjack/mcp/websocket_server.py +10 -0
  97. crackerjack/models/__init__.py +31 -0
  98. crackerjack/models/config.py +93 -0
  99. crackerjack/models/config_adapter.py +230 -0
  100. crackerjack/models/protocols.py +118 -0
  101. crackerjack/models/task.py +154 -0
  102. crackerjack/monitoring/ai_agent_watchdog.py +450 -0
  103. crackerjack/monitoring/regression_prevention.py +638 -0
  104. crackerjack/orchestration/__init__.py +0 -0
  105. crackerjack/orchestration/advanced_orchestrator.py +970 -0
  106. crackerjack/orchestration/execution_strategies.py +341 -0
  107. crackerjack/orchestration/test_progress_streamer.py +636 -0
  108. crackerjack/plugins/__init__.py +15 -0
  109. crackerjack/plugins/base.py +200 -0
  110. crackerjack/plugins/hooks.py +246 -0
  111. crackerjack/plugins/loader.py +335 -0
  112. crackerjack/plugins/managers.py +259 -0
  113. crackerjack/py313.py +8 -3
  114. crackerjack/services/__init__.py +22 -0
  115. crackerjack/services/cache.py +314 -0
  116. crackerjack/services/config.py +347 -0
  117. crackerjack/services/config_integrity.py +99 -0
  118. crackerjack/services/contextual_ai_assistant.py +516 -0
  119. crackerjack/services/coverage_ratchet.py +347 -0
  120. crackerjack/services/debug.py +736 -0
  121. crackerjack/services/dependency_monitor.py +617 -0
  122. crackerjack/services/enhanced_filesystem.py +439 -0
  123. crackerjack/services/file_hasher.py +151 -0
  124. crackerjack/services/filesystem.py +395 -0
  125. crackerjack/services/git.py +165 -0
  126. crackerjack/services/health_metrics.py +611 -0
  127. crackerjack/services/initialization.py +847 -0
  128. crackerjack/services/log_manager.py +286 -0
  129. crackerjack/services/logging.py +174 -0
  130. crackerjack/services/metrics.py +578 -0
  131. crackerjack/services/pattern_cache.py +362 -0
  132. crackerjack/services/pattern_detector.py +515 -0
  133. crackerjack/services/performance_benchmarks.py +653 -0
  134. crackerjack/services/security.py +163 -0
  135. crackerjack/services/server_manager.py +234 -0
  136. crackerjack/services/smart_scheduling.py +144 -0
  137. crackerjack/services/tool_version_service.py +61 -0
  138. crackerjack/services/unified_config.py +437 -0
  139. crackerjack/services/version_checker.py +248 -0
  140. crackerjack/slash_commands/__init__.py +14 -0
  141. crackerjack/slash_commands/init.md +122 -0
  142. crackerjack/slash_commands/run.md +163 -0
  143. crackerjack/slash_commands/status.md +127 -0
  144. crackerjack-0.31.4.dist-info/METADATA +742 -0
  145. crackerjack-0.31.4.dist-info/RECORD +148 -0
  146. crackerjack-0.31.4.dist-info/entry_points.txt +2 -0
  147. crackerjack/.gitignore +0 -34
  148. crackerjack/.libcst.codemod.yaml +0 -18
  149. crackerjack/.pdm.toml +0 -1
  150. crackerjack/.pre-commit-config-ai.yaml +0 -149
  151. crackerjack/.pre-commit-config-fast.yaml +0 -69
  152. crackerjack/.pre-commit-config.yaml +0 -114
  153. crackerjack/crackerjack.py +0 -4140
  154. crackerjack/pyproject.toml +0 -285
  155. crackerjack-0.29.0.dist-info/METADATA +0 -1289
  156. crackerjack-0.29.0.dist-info/RECORD +0 -17
  157. {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/WHEEL +0 -0
  158. {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,151 @@
1
+ """Test command building and configuration.
2
+
3
+ This module handles pytest command construction with various options and configurations.
4
+ Split from test_manager.py for better separation of concerns.
5
+ """
6
+
7
+ from pathlib import Path
8
+
9
+ from crackerjack.models.protocols import OptionsProtocol
10
+
11
+
12
+ class TestCommandBuilder:
13
+ """Builds pytest commands with appropriate options and configurations."""
14
+
15
+ def __init__(self, pkg_path: Path) -> None:
16
+ self.pkg_path = pkg_path
17
+
18
+ def build_command(self, options: OptionsProtocol) -> list[str]:
19
+ """Build complete pytest command with all options."""
20
+ cmd = ["python", "-m", "pytest"]
21
+
22
+ self._add_coverage_options(cmd, options)
23
+ self._add_worker_options(cmd, options)
24
+ self._add_benchmark_options(cmd, options)
25
+ self._add_timeout_options(cmd, options)
26
+ self._add_verbosity_options(cmd, options)
27
+ self._add_test_path(cmd)
28
+
29
+ return cmd
30
+
31
+ def get_optimal_workers(self, options: OptionsProtocol) -> int:
32
+ """Calculate optimal number of pytest workers based on system and configuration."""
33
+ if hasattr(options, "test_workers") and options.test_workers:
34
+ return options.test_workers
35
+
36
+ # Auto-detect based on CPU count
37
+ import multiprocessing
38
+
39
+ cpu_count = multiprocessing.cpu_count()
40
+
41
+ # Conservative worker count to avoid overwhelming the system
42
+ if cpu_count <= 2:
43
+ return 1
44
+ elif cpu_count <= 4:
45
+ return 2
46
+ elif cpu_count <= 8:
47
+ return 3
48
+ return 4
49
+
50
+ def get_test_timeout(self, options: OptionsProtocol) -> int:
51
+ """Get test timeout based on options or default."""
52
+ if hasattr(options, "test_timeout") and options.test_timeout:
53
+ return options.test_timeout
54
+
55
+ # Default timeout based on test configuration
56
+ if hasattr(options, "benchmark") and options.benchmark:
57
+ return 900 # 15 minutes for benchmarks
58
+ return 300 # 5 minutes for regular tests
59
+
60
+ def _add_coverage_options(self, cmd: list[str], options: OptionsProtocol) -> None:
61
+ """Add coverage-related options to command."""
62
+ # Always include coverage for comprehensive testing
63
+ cmd.extend(
64
+ [
65
+ "--cov=crackerjack",
66
+ "--cov-report=term-missing",
67
+ "--cov-report=html",
68
+ "--cov-fail-under=0", # Don't fail on low coverage, let ratchet handle it
69
+ ]
70
+ )
71
+
72
+ def _add_worker_options(self, cmd: list[str], options: OptionsProtocol) -> None:
73
+ """Add parallel execution options to command."""
74
+ workers = self.get_optimal_workers(options)
75
+ if workers > 1:
76
+ cmd.extend(["-n", str(workers)])
77
+
78
+ def _add_benchmark_options(self, cmd: list[str], options: OptionsProtocol) -> None:
79
+ """Add benchmark-specific options to command."""
80
+ if hasattr(options, "benchmark") and options.benchmark:
81
+ cmd.extend(
82
+ [
83
+ "--benchmark-only",
84
+ "--benchmark-sort=mean",
85
+ "--benchmark-columns=min,max,mean,stddev",
86
+ ]
87
+ )
88
+
89
+ def _add_timeout_options(self, cmd: list[str], options: OptionsProtocol) -> None:
90
+ """Add timeout options to command."""
91
+ timeout = self.get_test_timeout(options)
92
+ cmd.extend(["--timeout", str(timeout)])
93
+
94
+ def _add_verbosity_options(self, cmd: list[str], options: OptionsProtocol) -> None:
95
+ """Add verbosity and output formatting options."""
96
+ # Always use verbose output for better progress tracking
97
+ cmd.append("-v")
98
+
99
+ # Add useful output options
100
+ cmd.extend(
101
+ [
102
+ "--tb=short", # Shorter traceback format
103
+ "--strict-markers", # Ensure all markers are defined
104
+ "--strict-config", # Ensure configuration is valid
105
+ ]
106
+ )
107
+
108
+ def _add_test_path(self, cmd: list[str]) -> None:
109
+ """Add test path to command."""
110
+ # Add tests directory if it exists, otherwise current directory
111
+ test_paths = ["tests", "test"]
112
+
113
+ for test_path in test_paths:
114
+ full_path = self.pkg_path / test_path
115
+ if full_path.exists() and full_path.is_dir():
116
+ cmd.append(str(full_path))
117
+ return
118
+
119
+ # Fallback to current directory
120
+ cmd.append(str(self.pkg_path))
121
+
122
+ def build_specific_test_command(self, test_pattern: str) -> list[str]:
123
+ """Build command for running specific tests matching a pattern."""
124
+ cmd = ["python", "-m", "pytest", "-v"]
125
+
126
+ # Add basic coverage
127
+ cmd.extend(
128
+ [
129
+ "--cov=crackerjack",
130
+ "--cov-report=term-missing",
131
+ ]
132
+ )
133
+
134
+ # Add the test pattern
135
+ cmd.extend(["-k", test_pattern])
136
+
137
+ # Add test path
138
+ self._add_test_path(cmd)
139
+
140
+ return cmd
141
+
142
+ def build_validation_command(self) -> list[str]:
143
+ """Build command for test environment validation."""
144
+ return [
145
+ "python",
146
+ "-m",
147
+ "pytest",
148
+ "--collect-only",
149
+ "--quiet",
150
+ "tests" if (self.pkg_path / "tests").exists() else ".",
151
+ ]
@@ -0,0 +1,435 @@
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:
238
+ progress.update(collection_status="Session starting...")
239
+ return True
240
+ elif "test session starts" in line:
241
+ progress.update(collection_status="Starting test collection...")
242
+ return True
243
+ return False
244
+
245
+ def _handle_collection_progress(self, line: str, progress: TestProgress) -> bool:
246
+ """Handle collection progress updates."""
247
+ if progress.is_collecting:
248
+ # Look for file discovery patterns
249
+ if line.endswith(".py") and ("test_" in line or "_test.py" in line):
250
+ with progress._lock:
251
+ if line not in progress._seen_files:
252
+ progress._seen_files.add(line)
253
+ progress.files_discovered += 1
254
+ progress.collection_status = (
255
+ f"Found {progress.files_discovered} test files..."
256
+ )
257
+ return True
258
+ return False
259
+
260
+ def _handle_test_execution(self, line: str, progress: TestProgress) -> bool:
261
+ """Handle test execution progress."""
262
+ # Test result patterns
263
+ if " PASSED " in line:
264
+ progress.update(passed=progress.passed + 1)
265
+ self._extract_current_test(line, progress)
266
+ return True
267
+ elif " FAILED " in line:
268
+ progress.update(failed=progress.failed + 1)
269
+ self._extract_current_test(line, progress)
270
+ return True
271
+ elif " SKIPPED " in line:
272
+ progress.update(skipped=progress.skipped + 1)
273
+ self._extract_current_test(line, progress)
274
+ return True
275
+ elif " ERROR " in line:
276
+ progress.update(errors=progress.errors + 1)
277
+ self._extract_current_test(line, progress)
278
+ return True
279
+ elif "::" in line and any(x in line for x in ("RUNNING", "test_")):
280
+ self._handle_running_test(line, progress)
281
+ return True
282
+
283
+ return False
284
+
285
+ def _handle_running_test(self, line: str, progress: TestProgress) -> None:
286
+ """Handle currently running test indicator."""
287
+ if "::" in line:
288
+ # Extract test name from line
289
+ test_parts = line.split("::")
290
+ if len(test_parts) >= 2:
291
+ test_name = "::".join(test_parts[-2:])
292
+ progress.update(current_test=test_name)
293
+
294
+ def _extract_current_test(self, line: str, progress: TestProgress) -> None:
295
+ """Extract current test name from output line."""
296
+ if "::" in line:
297
+ # Extract test identifier
298
+ parts = line.split(" ")
299
+ for part in parts:
300
+ if "::" in part:
301
+ progress.update(current_test=part)
302
+ break
303
+
304
+ def _update_display_if_needed(self, progress: TestProgress, live: Live) -> None:
305
+ """Update display if enough time has passed or significant change occurred."""
306
+ if self._should_refresh_display(progress):
307
+ live.update(progress.format_progress())
308
+
309
+ def _should_refresh_display(self, progress: TestProgress) -> bool:
310
+ """Determine if display should be refreshed."""
311
+ return True # Simplified - let Rich handle refresh rate
312
+
313
+ def _mark_test_as_stuck(self, progress: TestProgress, test_name: str) -> None:
314
+ """Mark a test as potentially stuck."""
315
+ if test_name:
316
+ progress.update(current_test=f"🐌 {test_name} (slow)")
317
+
318
+ def _cleanup_threads(self, threads: list[threading.Thread]) -> None:
319
+ """Clean up reader threads."""
320
+ for thread in threads:
321
+ if thread.is_alive():
322
+ thread.join(timeout=1.0)
323
+
324
+ def _handle_progress_error(
325
+ self, process: subprocess.Popen[str], progress: TestProgress, error_msg: str
326
+ ) -> None:
327
+ """Handle progress tracking errors."""
328
+ process.terminate()
329
+ progress.update(is_complete=True, current_test=f"❌ {error_msg}")
330
+
331
+ def _execute_test_process_with_progress(
332
+ self,
333
+ cmd: list[str],
334
+ env: dict[str, str],
335
+ progress: TestProgress,
336
+ progress_callback: t.Callable[[dict[str, t.Any]], None],
337
+ timeout: int,
338
+ ) -> subprocess.CompletedProcess[str]:
339
+ """Execute test process with AI progress tracking."""
340
+ process = subprocess.Popen(
341
+ cmd,
342
+ cwd=self.pkg_path,
343
+ stdout=subprocess.PIPE,
344
+ stderr=subprocess.PIPE,
345
+ text=True,
346
+ env=env,
347
+ )
348
+
349
+ stdout_lines = self._read_stdout_with_progress(
350
+ process, progress, progress_callback
351
+ )
352
+ stderr_lines = self._read_stderr_lines(process)
353
+
354
+ return_code = self._wait_for_process_completion(process, timeout)
355
+
356
+ return subprocess.CompletedProcess(
357
+ cmd, return_code, "\n".join(stdout_lines), "\n".join(stderr_lines)
358
+ )
359
+
360
+ def _read_stdout_with_progress(
361
+ self,
362
+ process: subprocess.Popen[str],
363
+ progress: TestProgress,
364
+ progress_callback: t.Callable[[dict[str, t.Any]], None],
365
+ ) -> list[str]:
366
+ """Read stdout with progress updates."""
367
+ stdout_lines = []
368
+
369
+ if process.stdout:
370
+ for line in iter(process.stdout.readline, ""):
371
+ if not line:
372
+ break
373
+
374
+ line = line.strip()
375
+ if line:
376
+ stdout_lines.append(line)
377
+ self._process_test_output_line(line, progress)
378
+ self._emit_ai_progress(progress, progress_callback)
379
+
380
+ return stdout_lines
381
+
382
+ def _read_stderr_lines(self, process: subprocess.Popen[str]) -> list[str]:
383
+ """Read stderr lines."""
384
+ stderr_lines = []
385
+
386
+ if process.stderr:
387
+ for line in iter(process.stderr.readline, ""):
388
+ if not line:
389
+ break
390
+ line = line.strip()
391
+ if line:
392
+ stderr_lines.append(line)
393
+
394
+ return stderr_lines
395
+
396
+ def _wait_for_process_completion(
397
+ self, process: subprocess.Popen[str], timeout: int
398
+ ) -> int:
399
+ """Wait for process completion with timeout."""
400
+ try:
401
+ process.wait(timeout=timeout)
402
+ return process.returncode
403
+ except subprocess.TimeoutExpired:
404
+ process.terminate()
405
+ return -1
406
+
407
+ def _emit_ai_progress(
408
+ self,
409
+ progress: TestProgress,
410
+ progress_callback: t.Callable[[dict[str, t.Any]], None],
411
+ ) -> None:
412
+ """Emit progress update for AI consumption."""
413
+ progress_data = {
414
+ "type": "test_progress",
415
+ "total_tests": progress.total_tests,
416
+ "completed": progress.completed,
417
+ "passed": progress.passed,
418
+ "failed": progress.failed,
419
+ "skipped": progress.skipped,
420
+ "errors": progress.errors,
421
+ "current_test": progress.current_test,
422
+ "elapsed_time": progress.elapsed_time,
423
+ "is_collecting": progress.is_collecting,
424
+ "is_complete": progress.is_complete,
425
+ "collection_status": progress.collection_status,
426
+ }
427
+
428
+ if progress.eta_seconds:
429
+ progress_data["eta_seconds"] = progress.eta_seconds
430
+
431
+ from contextlib import suppress
432
+
433
+ with suppress(Exception):
434
+ # Don't let progress callback errors affect test execution
435
+ progress_callback(progress_data)