crackerjack 0.30.3__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.
- crackerjack/CLAUDE.md +1005 -0
- crackerjack/RULES.md +380 -0
- crackerjack/__init__.py +42 -13
- crackerjack/__main__.py +225 -299
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +169 -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 +652 -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 +401 -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 +561 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +640 -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 +411 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +435 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +144 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +615 -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 +370 -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 +141 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +360 -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/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 +347 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +347 -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 +395 -0
- crackerjack/services/git.py +165 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +847 -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.4.dist-info/METADATA +742 -0
- crackerjack-0.31.4.dist-info/RECORD +148 -0
- crackerjack-0.31.4.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.4.dist-info}/WHEEL +0 -0
- {crackerjack-0.30.3.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)
|