crackerjack 0.31.10__py3-none-any.whl → 0.31.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of crackerjack might be problematic. Click here for more details.
- crackerjack/CLAUDE.md +288 -705
- crackerjack/__main__.py +22 -8
- crackerjack/agents/__init__.py +0 -3
- crackerjack/agents/architect_agent.py +0 -43
- crackerjack/agents/base.py +1 -9
- crackerjack/agents/coordinator.py +2 -148
- crackerjack/agents/documentation_agent.py +109 -81
- crackerjack/agents/dry_agent.py +122 -97
- crackerjack/agents/formatting_agent.py +3 -16
- crackerjack/agents/import_optimization_agent.py +1174 -130
- crackerjack/agents/performance_agent.py +956 -188
- crackerjack/agents/performance_helpers.py +229 -0
- crackerjack/agents/proactive_agent.py +1 -48
- crackerjack/agents/refactoring_agent.py +516 -246
- crackerjack/agents/refactoring_helpers.py +282 -0
- crackerjack/agents/security_agent.py +393 -90
- crackerjack/agents/test_creation_agent.py +1776 -120
- crackerjack/agents/test_specialist_agent.py +59 -15
- crackerjack/agents/tracker.py +0 -102
- crackerjack/api.py +145 -37
- crackerjack/cli/handlers.py +48 -30
- crackerjack/cli/interactive.py +11 -11
- crackerjack/cli/options.py +66 -4
- crackerjack/code_cleaner.py +808 -148
- crackerjack/config/global_lock_config.py +110 -0
- crackerjack/config/hooks.py +43 -64
- crackerjack/core/async_workflow_orchestrator.py +247 -97
- crackerjack/core/autofix_coordinator.py +192 -109
- crackerjack/core/enhanced_container.py +46 -63
- crackerjack/core/file_lifecycle.py +549 -0
- crackerjack/core/performance.py +9 -8
- crackerjack/core/performance_monitor.py +395 -0
- crackerjack/core/phase_coordinator.py +281 -94
- crackerjack/core/proactive_workflow.py +9 -58
- crackerjack/core/resource_manager.py +501 -0
- crackerjack/core/service_watchdog.py +490 -0
- crackerjack/core/session_coordinator.py +4 -8
- crackerjack/core/timeout_manager.py +504 -0
- crackerjack/core/websocket_lifecycle.py +475 -0
- crackerjack/core/workflow_orchestrator.py +343 -209
- crackerjack/dynamic_config.py +47 -6
- crackerjack/errors.py +3 -4
- crackerjack/executors/async_hook_executor.py +63 -13
- crackerjack/executors/cached_hook_executor.py +14 -14
- crackerjack/executors/hook_executor.py +100 -37
- crackerjack/executors/hook_lock_manager.py +856 -0
- crackerjack/executors/individual_hook_executor.py +120 -86
- crackerjack/intelligence/__init__.py +0 -7
- crackerjack/intelligence/adaptive_learning.py +13 -86
- crackerjack/intelligence/agent_orchestrator.py +15 -78
- crackerjack/intelligence/agent_registry.py +12 -59
- crackerjack/intelligence/agent_selector.py +31 -92
- crackerjack/intelligence/integration.py +1 -41
- crackerjack/interactive.py +9 -9
- crackerjack/managers/async_hook_manager.py +25 -8
- crackerjack/managers/hook_manager.py +9 -9
- crackerjack/managers/publish_manager.py +57 -59
- crackerjack/managers/test_command_builder.py +6 -36
- crackerjack/managers/test_executor.py +9 -61
- crackerjack/managers/test_manager.py +17 -63
- crackerjack/managers/test_manager_backup.py +77 -127
- crackerjack/managers/test_progress.py +4 -23
- crackerjack/mcp/cache.py +5 -12
- crackerjack/mcp/client_runner.py +10 -10
- crackerjack/mcp/context.py +64 -6
- crackerjack/mcp/dashboard.py +14 -11
- crackerjack/mcp/enhanced_progress_monitor.py +55 -55
- crackerjack/mcp/file_monitor.py +72 -42
- crackerjack/mcp/progress_components.py +103 -84
- crackerjack/mcp/progress_monitor.py +122 -49
- crackerjack/mcp/rate_limiter.py +12 -12
- crackerjack/mcp/server_core.py +16 -22
- crackerjack/mcp/service_watchdog.py +26 -26
- crackerjack/mcp/state.py +15 -0
- crackerjack/mcp/tools/core_tools.py +95 -39
- crackerjack/mcp/tools/error_analyzer.py +6 -32
- crackerjack/mcp/tools/execution_tools.py +1 -56
- crackerjack/mcp/tools/execution_tools_backup.py +35 -131
- crackerjack/mcp/tools/intelligence_tool_registry.py +0 -36
- crackerjack/mcp/tools/intelligence_tools.py +2 -55
- crackerjack/mcp/tools/monitoring_tools.py +308 -145
- crackerjack/mcp/tools/proactive_tools.py +12 -42
- crackerjack/mcp/tools/progress_tools.py +23 -15
- crackerjack/mcp/tools/utility_tools.py +3 -40
- crackerjack/mcp/tools/workflow_executor.py +40 -60
- crackerjack/mcp/websocket/app.py +0 -3
- crackerjack/mcp/websocket/endpoints.py +206 -268
- crackerjack/mcp/websocket/jobs.py +213 -66
- crackerjack/mcp/websocket/server.py +84 -6
- crackerjack/mcp/websocket/websocket_handler.py +137 -29
- crackerjack/models/config_adapter.py +3 -16
- crackerjack/models/protocols.py +162 -3
- crackerjack/models/resource_protocols.py +454 -0
- crackerjack/models/task.py +3 -3
- crackerjack/monitoring/__init__.py +0 -0
- crackerjack/monitoring/ai_agent_watchdog.py +25 -71
- crackerjack/monitoring/regression_prevention.py +28 -87
- crackerjack/orchestration/advanced_orchestrator.py +44 -78
- crackerjack/orchestration/coverage_improvement.py +10 -60
- crackerjack/orchestration/execution_strategies.py +16 -16
- crackerjack/orchestration/test_progress_streamer.py +61 -53
- crackerjack/plugins/base.py +1 -1
- crackerjack/plugins/managers.py +22 -20
- crackerjack/py313.py +65 -21
- crackerjack/services/backup_service.py +467 -0
- crackerjack/services/bounded_status_operations.py +627 -0
- crackerjack/services/cache.py +7 -9
- crackerjack/services/config.py +35 -52
- crackerjack/services/config_integrity.py +5 -16
- crackerjack/services/config_merge.py +542 -0
- crackerjack/services/contextual_ai_assistant.py +17 -19
- crackerjack/services/coverage_ratchet.py +44 -73
- crackerjack/services/debug.py +25 -39
- crackerjack/services/dependency_monitor.py +52 -50
- crackerjack/services/enhanced_filesystem.py +14 -11
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/filesystem.py +1 -12
- crackerjack/services/git.py +71 -47
- crackerjack/services/health_metrics.py +31 -27
- crackerjack/services/initialization.py +276 -428
- crackerjack/services/input_validator.py +760 -0
- crackerjack/services/log_manager.py +16 -16
- crackerjack/services/logging.py +7 -6
- crackerjack/services/metrics.py +43 -43
- crackerjack/services/pattern_cache.py +2 -31
- crackerjack/services/pattern_detector.py +26 -63
- crackerjack/services/performance_benchmarks.py +20 -45
- crackerjack/services/regex_patterns.py +2887 -0
- crackerjack/services/regex_utils.py +537 -0
- crackerjack/services/secure_path_utils.py +683 -0
- crackerjack/services/secure_status_formatter.py +534 -0
- crackerjack/services/secure_subprocess.py +605 -0
- crackerjack/services/security.py +47 -10
- crackerjack/services/security_logger.py +492 -0
- crackerjack/services/server_manager.py +109 -50
- crackerjack/services/smart_scheduling.py +8 -25
- crackerjack/services/status_authentication.py +603 -0
- crackerjack/services/status_security_manager.py +442 -0
- crackerjack/services/thread_safe_status_collector.py +546 -0
- crackerjack/services/tool_version_service.py +1 -23
- crackerjack/services/unified_config.py +36 -58
- crackerjack/services/validation_rate_limiter.py +269 -0
- crackerjack/services/version_checker.py +9 -40
- crackerjack/services/websocket_resource_limiter.py +572 -0
- crackerjack/slash_commands/__init__.py +52 -2
- crackerjack/tools/__init__.py +0 -0
- crackerjack/tools/validate_input_validator_patterns.py +262 -0
- crackerjack/tools/validate_regex_patterns.py +198 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/METADATA +197 -12
- crackerjack-0.31.12.dist-info/RECORD +178 -0
- crackerjack/cli/facade.py +0 -104
- crackerjack-0.31.10.dist-info/RECORD +0 -149
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/WHEEL +0 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -30,8 +30,8 @@ class StreamingMode(str, Enum):
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class AICoordinationMode(str, Enum):
|
|
33
|
-
SINGLE_AGENT = "single
|
|
34
|
-
MULTI_AGENT = "multi
|
|
33
|
+
SINGLE_AGENT = "single-agent"
|
|
34
|
+
MULTI_AGENT = "multi-agent"
|
|
35
35
|
COORDINATOR = "coordinator"
|
|
36
36
|
|
|
37
37
|
|
|
@@ -138,13 +138,13 @@ class StrategySelector:
|
|
|
138
138
|
|
|
139
139
|
if len(context.previous_failures) > 5:
|
|
140
140
|
self.console.print(
|
|
141
|
-
"[yellow]🧠 Switching to individual execution due to multiple failures[/yellow]",
|
|
141
|
+
"[yellow]🧠 Switching to individual execution due to multiple failures[/ yellow]",
|
|
142
142
|
)
|
|
143
143
|
return ExecutionStrategy.INDIVIDUAL
|
|
144
144
|
|
|
145
145
|
if context.changed_files and len(context.changed_files) < 5:
|
|
146
146
|
self.console.print(
|
|
147
|
-
"[cyan]🎯 Using selective execution for targeted file changes[/cyan]",
|
|
147
|
+
"[cyan]🎯 Using selective execution for targeted file changes[/ cyan]",
|
|
148
148
|
)
|
|
149
149
|
return ExecutionStrategy.SELECTIVE
|
|
150
150
|
|
|
@@ -174,11 +174,11 @@ class StrategySelector:
|
|
|
174
174
|
if context.changed_files:
|
|
175
175
|
for file_path in context.changed_files:
|
|
176
176
|
if file_path.suffix == ".py":
|
|
177
|
-
priority_hooks.update(["pyright", "ruff
|
|
177
|
+
priority_hooks.update(["pyright", "ruff-check", "ruff-format"])
|
|
178
178
|
if "test" in str(file_path):
|
|
179
179
|
priority_hooks.update(["pytest", "coverage"])
|
|
180
180
|
if str(file_path).endswith(("setup.py", "pyproject.toml")):
|
|
181
|
-
priority_hooks.update(["bandit", "creosote", "detect
|
|
181
|
+
priority_hooks.update(["bandit", "creosote", "detect-secrets"])
|
|
182
182
|
|
|
183
183
|
selected_hooks = [
|
|
184
184
|
hook for hook in strategy.hooks if hook.name in priority_hooks
|
|
@@ -189,7 +189,7 @@ class StrategySelector:
|
|
|
189
189
|
|
|
190
190
|
self.console.print(
|
|
191
191
|
f"[cyan]🎯 Selected {len(selected_hooks)} hooks for targeted execution: "
|
|
192
|
-
f"{', '.join(h.name for h in selected_hooks)}[/cyan]",
|
|
192
|
+
f"{', '.join(h.name for h in selected_hooks)}[/ cyan]",
|
|
193
193
|
)
|
|
194
194
|
|
|
195
195
|
return HookStrategy(
|
|
@@ -303,35 +303,35 @@ class ExecutionPlan:
|
|
|
303
303
|
def print_plan_summary(self, console: Console) -> None:
|
|
304
304
|
console.print("\n" + "=" * 80)
|
|
305
305
|
console.print(
|
|
306
|
-
"[bold bright_blue]🎯 ORCHESTRATED EXECUTION PLAN[/bold bright_blue]",
|
|
306
|
+
"[bold bright_blue]🎯 ORCHESTRATED EXECUTION PLAN[/ bold bright_blue]",
|
|
307
307
|
)
|
|
308
308
|
console.print("=" * 80)
|
|
309
309
|
|
|
310
|
-
console.print(f"[bold]Strategy: [/bold] {self.execution_strategy.value}")
|
|
310
|
+
console.print(f"[bold]Strategy: [/ bold] {self.execution_strategy.value}")
|
|
311
311
|
console.print(
|
|
312
|
-
f"[bold]AI Mode: [/bold] {self.config.ai_coordination_mode.value}",
|
|
312
|
+
f"[bold]AI Mode: [/ bold] {self.config.ai_coordination_mode.value}",
|
|
313
313
|
)
|
|
314
314
|
console.print(
|
|
315
|
-
f"[bold]Progress Level: [/bold] {self.config.progress_level.value}",
|
|
315
|
+
f"[bold]Progress Level: [/ bold] {self.config.progress_level.value}",
|
|
316
316
|
)
|
|
317
317
|
console.print(
|
|
318
|
-
f"[bold]Estimated Duration: [/bold] {self.estimated_total_duration: .1f}s",
|
|
318
|
+
f"[bold]Estimated Duration: [/ bold] {self.estimated_total_duration: .1f}s",
|
|
319
319
|
)
|
|
320
320
|
|
|
321
|
-
console.print("\n[bold cyan]Hook Execution: [/bold cyan]")
|
|
321
|
+
console.print("\n[bold cyan]Hook Execution: [/ bold cyan]")
|
|
322
322
|
for i, plan in enumerate(self.hook_plans, 1):
|
|
323
323
|
strategy = plan["strategy"]
|
|
324
324
|
console.print(
|
|
325
|
-
f" {i}. {strategy.name}
|
|
325
|
+
f" {i}. {strategy.name}-{len(strategy.hooks)} hooks "
|
|
326
326
|
f"({plan['estimated_duration']: .1f}s)",
|
|
327
327
|
)
|
|
328
328
|
|
|
329
|
-
console.print("\n[bold green]Test Execution: [/bold green]")
|
|
329
|
+
console.print("\n[bold green]Test Execution: [/ bold green]")
|
|
330
330
|
console.print(f" Mode: {self.test_plan['mode']}")
|
|
331
331
|
console.print(f" Workers: {self.test_plan['parallel_workers']}")
|
|
332
332
|
console.print(f" Duration: {self.test_plan['estimated_duration']: .1f}s")
|
|
333
333
|
|
|
334
|
-
console.print("\n[bold magenta]AI Coordination: [/bold magenta]")
|
|
334
|
+
console.print("\n[bold magenta]AI Coordination: [/ bold magenta]")
|
|
335
335
|
console.print(f" Mode: {self.ai_plan['mode'].value}")
|
|
336
336
|
console.print(f" Intelligence: {self.ai_plan['intelligence_level'].value}")
|
|
337
337
|
console.print(
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import re
|
|
3
2
|
import subprocess
|
|
4
3
|
import time
|
|
5
4
|
import typing as t
|
|
@@ -9,6 +8,7 @@ from pathlib import Path
|
|
|
9
8
|
from rich.console import Console
|
|
10
9
|
|
|
11
10
|
from crackerjack.models.protocols import OptionsProtocol
|
|
11
|
+
from crackerjack.services.regex_patterns import SAFE_PATTERNS, CompiledPatternCache
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@dataclass
|
|
@@ -104,25 +104,31 @@ class TestSuiteProgress:
|
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
class PytestOutputParser:
|
|
107
|
-
TEST_START_PATTERN = re.compile(
|
|
108
|
-
r"^(.+?)::(.*)::(.*)(?:PASSED|FAILED|SKIPPED|ERROR)",
|
|
109
|
-
)
|
|
110
|
-
TEST_RESULT_PATTERN = re.compile(
|
|
111
|
-
r"^(.+?)(?:PASSED|FAILED|SKIPPED|ERROR)(?:\s+\[.*?\])?\s*$",
|
|
112
|
-
)
|
|
113
|
-
TEST_COLLECTION_PATTERN = re.compile(r"collected (\d+) items?")
|
|
114
|
-
TEST_SESSION_START = re.compile(r"test session starts")
|
|
115
|
-
COVERAGE_PATTERN = re.compile(r"TOTAL\s+\d+\s+\d+\s+(\d+)%")
|
|
116
|
-
|
|
117
|
-
DETAILED_TEST_PATTERN = re.compile(
|
|
118
|
-
r"^(.+?\.py)::(.*)(?:PASSED|FAILED|SKIPPED|ERROR)(?:\s+\[(\d+)%\])?\s*(?:\[(.*?)\])?\s*$",
|
|
119
|
-
)
|
|
120
|
-
|
|
121
107
|
def __init__(self) -> None:
|
|
122
108
|
self.current_test: str | None = None
|
|
123
109
|
self.test_traceback_buffer: list[str] = []
|
|
124
110
|
self.in_traceback = False
|
|
125
111
|
|
|
112
|
+
# Cache compiled patterns for optimal performance during test parsing
|
|
113
|
+
self._test_start_pattern = CompiledPatternCache.get_compiled_pattern(
|
|
114
|
+
SAFE_PATTERNS["pytest_test_start"].pattern
|
|
115
|
+
)
|
|
116
|
+
self._test_result_pattern = CompiledPatternCache.get_compiled_pattern(
|
|
117
|
+
SAFE_PATTERNS["pytest_test_result"].pattern
|
|
118
|
+
)
|
|
119
|
+
self._test_collection_pattern = CompiledPatternCache.get_compiled_pattern(
|
|
120
|
+
SAFE_PATTERNS["pytest_collection_count"].pattern
|
|
121
|
+
)
|
|
122
|
+
self._session_start_pattern = CompiledPatternCache.get_compiled_pattern(
|
|
123
|
+
SAFE_PATTERNS["pytest_session_start"].pattern
|
|
124
|
+
)
|
|
125
|
+
self._coverage_pattern = CompiledPatternCache.get_compiled_pattern(
|
|
126
|
+
SAFE_PATTERNS["pytest_coverage_total"].pattern
|
|
127
|
+
)
|
|
128
|
+
self._detailed_test_pattern = CompiledPatternCache.get_compiled_pattern(
|
|
129
|
+
SAFE_PATTERNS["pytest_detailed_test"].pattern
|
|
130
|
+
)
|
|
131
|
+
|
|
126
132
|
def parse_pytest_output(self, output_lines: list[str]) -> dict[str, t.Any]:
|
|
127
133
|
tests: dict[str, TestProgress] = {}
|
|
128
134
|
suite_info = TestSuiteProgress()
|
|
@@ -148,7 +154,7 @@ class PytestOutputParser:
|
|
|
148
154
|
line: str,
|
|
149
155
|
suite_info: TestSuiteProgress,
|
|
150
156
|
) -> None:
|
|
151
|
-
if match := self.
|
|
157
|
+
if match := self._test_collection_pattern.search(line):
|
|
152
158
|
suite_info.total_tests = int(match.group(1))
|
|
153
159
|
|
|
154
160
|
def _process_test_result_line(
|
|
@@ -157,9 +163,9 @@ class PytestOutputParser:
|
|
|
157
163
|
tests: dict[str, TestProgress],
|
|
158
164
|
suite_info: TestSuiteProgress,
|
|
159
165
|
) -> None:
|
|
160
|
-
if match := self.
|
|
161
|
-
file_path, test_name, status
|
|
162
|
-
test_id = f"{file_path}::{test_name}"
|
|
166
|
+
if match := self._detailed_test_pattern.match(line):
|
|
167
|
+
file_path, test_name, status = match.groups()
|
|
168
|
+
test_id = f"{file_path}:: {test_name}"
|
|
163
169
|
|
|
164
170
|
if test_id not in tests:
|
|
165
171
|
tests[test_id] = self._create_test_progress(
|
|
@@ -178,7 +184,7 @@ class PytestOutputParser:
|
|
|
178
184
|
test_id: str,
|
|
179
185
|
) -> TestProgress:
|
|
180
186
|
test_file = Path(file_path).name
|
|
181
|
-
test_parts = test_name.split("::")
|
|
187
|
+
test_parts = test_name.split(":: ")
|
|
182
188
|
test_class = test_parts[0] if len(test_parts) > 1 else None
|
|
183
189
|
test_method = test_parts[-1]
|
|
184
190
|
|
|
@@ -205,7 +211,7 @@ class PytestOutputParser:
|
|
|
205
211
|
suite_info.error_tests += 1
|
|
206
212
|
|
|
207
213
|
def _process_coverage_line(self, line: str, suite_info: TestSuiteProgress) -> None:
|
|
208
|
-
if match := self.
|
|
214
|
+
if match := self._coverage_pattern.search(line):
|
|
209
215
|
suite_info.coverage_percentage = float(match.group(1))
|
|
210
216
|
|
|
211
217
|
def _process_current_test_line(
|
|
@@ -213,7 +219,7 @@ class PytestOutputParser:
|
|
|
213
219
|
line: str,
|
|
214
220
|
suite_info: TestSuiteProgress,
|
|
215
221
|
) -> None:
|
|
216
|
-
if "::" in line and any(
|
|
222
|
+
if ":: " in line and any(
|
|
217
223
|
status in line for status in ("PASSED", "FAILED", "SKIPPED", "ERROR")
|
|
218
224
|
):
|
|
219
225
|
suite_info.current_test = line.split()[0] if line.split() else None
|
|
@@ -248,7 +254,7 @@ class PytestOutputParser:
|
|
|
248
254
|
return "FAILURES" in line or "ERRORS" in line
|
|
249
255
|
|
|
250
256
|
def _is_test_header(self, line: str) -> bool:
|
|
251
|
-
return line.startswith("_") and "::" in line
|
|
257
|
+
return line.startswith("_") and ":: " in line
|
|
252
258
|
|
|
253
259
|
def _should_add_to_traceback(self, current_test: str | None, line: str) -> bool:
|
|
254
260
|
return current_test is not None and bool(line.strip())
|
|
@@ -289,7 +295,7 @@ class TestProgressStreamer:
|
|
|
289
295
|
suite_progress = TestSuiteProgress(start_time=start_time)
|
|
290
296
|
|
|
291
297
|
self.console.print(
|
|
292
|
-
"\n[bold bright_green]🧪 RUNNING TESTS WITH STREAMING PROGRESS[/bold bright_green]",
|
|
298
|
+
"\n[bold bright_green]🧪 RUNNING TESTS WITH STREAMING PROGRESS[/ bold bright_green]",
|
|
293
299
|
)
|
|
294
300
|
|
|
295
301
|
cmd = self.build_pytest_command(options, execution_mode)
|
|
@@ -354,7 +360,7 @@ class TestProgressStreamer:
|
|
|
354
360
|
error: Exception,
|
|
355
361
|
suite_progress: TestSuiteProgress,
|
|
356
362
|
) -> dict[str, t.Any]:
|
|
357
|
-
self.console.print(f"[red]❌ Test execution failed: {error}[/red]")
|
|
363
|
+
self.console.print(f"[red]❌ Test execution failed: {error}[/ red]")
|
|
358
364
|
suite_progress.end_time = time.time()
|
|
359
365
|
suite_progress.duration = suite_progress.end_time - (
|
|
360
366
|
suite_progress.start_time or 0
|
|
@@ -375,23 +381,23 @@ class TestProgressStreamer:
|
|
|
375
381
|
) -> list[str]:
|
|
376
382
|
cmd = ["uv", "run", "pytest"]
|
|
377
383
|
|
|
378
|
-
cmd.extend(["-v", "--tb=short"])
|
|
384
|
+
cmd.extend(["- v", "--tb=short"])
|
|
379
385
|
|
|
380
386
|
if hasattr(options, "coverage") and options.coverage:
|
|
381
|
-
cmd.extend(["--cov=crackerjack", "
|
|
387
|
+
cmd.extend(["--cov=crackerjack", "- - cov - report=term-missing"])
|
|
382
388
|
|
|
383
389
|
if execution_mode == "individual_with_progress":
|
|
384
|
-
cmd.extend(["
|
|
390
|
+
cmd.extend(["- - no-header"])
|
|
385
391
|
elif execution_mode == "selective":
|
|
386
392
|
pass
|
|
387
393
|
else:
|
|
388
|
-
cmd.extend(["-q"])
|
|
394
|
+
cmd.extend(["- q"])
|
|
389
395
|
|
|
390
396
|
if hasattr(options, "test_timeout"):
|
|
391
|
-
cmd.extend([f"--timeout={options.test_timeout}"])
|
|
397
|
+
cmd.extend([f"--timeout ={options.test_timeout}"])
|
|
392
398
|
|
|
393
399
|
if hasattr(options, "test_workers") and options.test_workers > 1:
|
|
394
|
-
cmd.extend(["-n", str(options.test_workers)])
|
|
400
|
+
cmd.extend(["- n", str(options.test_workers)])
|
|
395
401
|
|
|
396
402
|
return cmd
|
|
397
403
|
|
|
@@ -400,7 +406,7 @@ class TestProgressStreamer:
|
|
|
400
406
|
cmd: list[str],
|
|
401
407
|
suite_progress: TestSuiteProgress,
|
|
402
408
|
) -> subprocess.CompletedProcess[str]:
|
|
403
|
-
self.console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
|
|
409
|
+
self.console.print(f"[dim]Running: {' '.join(cmd)}[/ dim]")
|
|
404
410
|
|
|
405
411
|
process = await self._create_subprocess(cmd)
|
|
406
412
|
stdout_lines: list[str] = []
|
|
@@ -517,14 +523,14 @@ class TestProgressStreamer:
|
|
|
517
523
|
line: str,
|
|
518
524
|
suite_progress: TestSuiteProgress,
|
|
519
525
|
) -> None:
|
|
520
|
-
if "::" in line and any(
|
|
526
|
+
if ":: " in line and any(
|
|
521
527
|
status in line for status in ("PASSED", "FAILED", "SKIPPED", "ERROR")
|
|
522
528
|
):
|
|
523
529
|
parts = line.split()
|
|
524
530
|
if parts:
|
|
525
531
|
suite_progress.current_test = parts[0]
|
|
526
532
|
|
|
527
|
-
if match := self.parser.
|
|
533
|
+
if match := self.parser._test_collection_pattern.search(line):
|
|
528
534
|
suite_progress.total_tests = int(match.group(1))
|
|
529
535
|
|
|
530
536
|
if "PASSED" in line:
|
|
@@ -542,17 +548,17 @@ class TestProgressStreamer:
|
|
|
542
548
|
|
|
543
549
|
def _print_test_line(self, line: str) -> None:
|
|
544
550
|
if "PASSED" in line:
|
|
545
|
-
self.console.print(f"[green]{line}[/green]")
|
|
551
|
+
self.console.print(f"[green]{line}[/ green]")
|
|
546
552
|
elif "FAILED" in line:
|
|
547
|
-
self.console.print(f"[red]{line}[/red]")
|
|
553
|
+
self.console.print(f"[red]{line}[/ red]")
|
|
548
554
|
elif "SKIPPED" in line:
|
|
549
|
-
self.console.print(f"[yellow]{line}[/yellow]")
|
|
555
|
+
self.console.print(f"[yellow]{line}[/ yellow]")
|
|
550
556
|
elif "ERROR" in line:
|
|
551
|
-
self.console.print(f"[bright_red]{line}[/bright_red]")
|
|
557
|
+
self.console.print(f"[bright_red]{line}[/ bright_red]")
|
|
552
558
|
elif line.startswith("="):
|
|
553
|
-
self.console.print(f"[bold cyan]{line}[/bold cyan]")
|
|
559
|
+
self.console.print(f"[bold cyan]{line}[/ bold cyan]")
|
|
554
560
|
else:
|
|
555
|
-
self.console.print(f"[dim]{line}[/dim]")
|
|
561
|
+
self.console.print(f"[dim]{line}[/ dim]")
|
|
556
562
|
|
|
557
563
|
def _print_test_summary(
|
|
558
564
|
self,
|
|
@@ -569,45 +575,47 @@ class TestProgressStreamer:
|
|
|
569
575
|
def _print_summary_header(self) -> None:
|
|
570
576
|
self.console.print("\n" + "=" * 80)
|
|
571
577
|
self.console.print(
|
|
572
|
-
"[bold bright_green]🧪 TEST EXECUTION SUMMARY[/bold bright_green]",
|
|
578
|
+
"[bold bright_green]🧪 TEST EXECUTION SUMMARY[/ bold bright_green]",
|
|
573
579
|
)
|
|
574
580
|
self.console.print("=" * 80)
|
|
575
581
|
|
|
576
582
|
def _print_test_counts(self, suite_progress: TestSuiteProgress) -> None:
|
|
577
|
-
self.console.print(f"[bold]Total Tests: [/bold] {suite_progress.total_tests}")
|
|
578
|
-
self.console.print(f"[green]✅ Passed: [/green] {suite_progress.passed_tests}")
|
|
583
|
+
self.console.print(f"[bold]Total Tests: [/ bold] {suite_progress.total_tests}")
|
|
584
|
+
self.console.print(f"[green]✅ Passed: [/ green] {suite_progress.passed_tests}")
|
|
579
585
|
|
|
580
586
|
if suite_progress.failed_tests > 0:
|
|
581
|
-
self.console.print(f"[red]❌ Failed: [/red] {suite_progress.failed_tests}")
|
|
587
|
+
self.console.print(f"[red]❌ Failed: [/ red] {suite_progress.failed_tests}")
|
|
582
588
|
|
|
583
589
|
if suite_progress.skipped_tests > 0:
|
|
584
590
|
self.console.print(
|
|
585
|
-
f"[yellow]⏭️ Skipped: [/yellow] {suite_progress.skipped_tests}",
|
|
591
|
+
f"[yellow]⏭️ Skipped: [/ yellow] {suite_progress.skipped_tests}",
|
|
586
592
|
)
|
|
587
593
|
|
|
588
594
|
if suite_progress.error_tests > 0:
|
|
589
595
|
self.console.print(
|
|
590
|
-
f"[bright_red]💥 Errors: [/bright_red] {suite_progress.error_tests}",
|
|
596
|
+
f"[bright_red]💥 Errors: [/ bright_red] {suite_progress.error_tests}",
|
|
591
597
|
)
|
|
592
598
|
|
|
593
599
|
def _print_timing_stats(self, suite_progress: TestSuiteProgress) -> None:
|
|
594
600
|
if not suite_progress.duration:
|
|
595
601
|
return
|
|
596
602
|
|
|
597
|
-
self.console.print(
|
|
603
|
+
self.console.print(
|
|
604
|
+
f"[bold]⏱️ Duration: [/ bold] {suite_progress.duration: .1f}s"
|
|
605
|
+
)
|
|
598
606
|
|
|
599
607
|
if suite_progress.total_tests > 0:
|
|
600
608
|
avg_time = suite_progress.duration / suite_progress.total_tests
|
|
601
|
-
self.console.print(f"[dim]Average per test: {avg_time
|
|
609
|
+
self.console.print(f"[dim]Average per test: {avg_time: .2f}s[/ dim]")
|
|
602
610
|
|
|
603
611
|
self.console.print(
|
|
604
|
-
f"[bold]📊 Success Rate: [/bold] {suite_progress.success_rate
|
|
612
|
+
f"[bold]📊 Success Rate: [/ bold] {suite_progress.success_rate: .1f}%",
|
|
605
613
|
)
|
|
606
614
|
|
|
607
615
|
def _print_coverage_stats(self, suite_progress: TestSuiteProgress) -> None:
|
|
608
616
|
if suite_progress.coverage_percentage is not None:
|
|
609
617
|
self.console.print(
|
|
610
|
-
f"[bold]📈 Coverage: [/bold] {suite_progress.coverage_percentage
|
|
618
|
+
f"[bold]📈 Coverage: [/ bold] {suite_progress.coverage_percentage: .1f}%",
|
|
611
619
|
)
|
|
612
620
|
|
|
613
621
|
def _print_failed_test_details(self, tests: list[TestProgress]) -> None:
|
|
@@ -616,16 +624,16 @@ class TestProgressStreamer:
|
|
|
616
624
|
return
|
|
617
625
|
|
|
618
626
|
self.console.print(
|
|
619
|
-
f"\n[bold red]Failed Tests ({len(failed_tests)}): [/bold red]",
|
|
627
|
+
f"\n[bold red]Failed Tests ({len(failed_tests)}): [/ bold red]",
|
|
620
628
|
)
|
|
621
629
|
for test in failed_tests[:5]:
|
|
622
630
|
self.console.print(f" ❌ {test.test_id}")
|
|
623
631
|
if test.error_message:
|
|
624
632
|
error_preview = self._format_error_preview(test.error_message)
|
|
625
|
-
self.console.print(f" [dim]{error_preview}[/dim]")
|
|
633
|
+
self.console.print(f" [dim]{error_preview}[/ dim]")
|
|
626
634
|
|
|
627
635
|
if len(failed_tests) > 5:
|
|
628
|
-
self.console.print(f" [dim]... and {len(failed_tests) - 5} more[/dim]")
|
|
636
|
+
self.console.print(f" [dim]... and {len(failed_tests) - 5} more[/ dim]")
|
|
629
637
|
|
|
630
638
|
def _format_error_preview(self, error_message: str) -> str:
|
|
631
639
|
return (
|
crackerjack/plugins/base.py
CHANGED
|
@@ -21,7 +21,7 @@ class PluginMetadata:
|
|
|
21
21
|
description: str
|
|
22
22
|
author: str = ""
|
|
23
23
|
license: str = ""
|
|
24
|
-
requires_python: str = "
|
|
24
|
+
requires_python: str = "> = 3.11"
|
|
25
25
|
dependencies: list[str] = field(default_factory=list)
|
|
26
26
|
entry_point: str = ""
|
|
27
27
|
config_schema: dict[str, t.Any] = field(default_factory=dict)
|
crackerjack/plugins/managers.py
CHANGED
|
@@ -44,7 +44,7 @@ class PluginManager:
|
|
|
44
44
|
|
|
45
45
|
if total_count > 0:
|
|
46
46
|
self.console.print(
|
|
47
|
-
f"[green]✅[/green] Loaded {loaded_count} / {total_count} plugins",
|
|
47
|
+
f"[green]✅[/ green] Loaded {loaded_count} / {total_count} plugins",
|
|
48
48
|
)
|
|
49
49
|
|
|
50
50
|
activation_results = self.registry.activate_all()
|
|
@@ -54,7 +54,7 @@ class PluginManager:
|
|
|
54
54
|
|
|
55
55
|
if activated_count > 0:
|
|
56
56
|
self.console.print(
|
|
57
|
-
f"[green]✅[/green] Activated {activated_count} plugins",
|
|
57
|
+
f"[green]✅[/ green] Activated {activated_count} plugins",
|
|
58
58
|
)
|
|
59
59
|
|
|
60
60
|
hook_plugins = self.registry.get_enabled(PluginType.HOOK)
|
|
@@ -69,7 +69,7 @@ class PluginManager:
|
|
|
69
69
|
except Exception as e:
|
|
70
70
|
self.logger.exception(f"Failed to initialize plugin system: {e}")
|
|
71
71
|
self.console.print(
|
|
72
|
-
f"[red]❌[/red] Plugin system initialization failed: {e}",
|
|
72
|
+
f"[red]❌[/ red] Plugin system initialization failed: {e}",
|
|
73
73
|
)
|
|
74
74
|
return False
|
|
75
75
|
|
|
@@ -85,7 +85,7 @@ class PluginManager:
|
|
|
85
85
|
|
|
86
86
|
if deactivated_count > 0:
|
|
87
87
|
self.console.print(
|
|
88
|
-
f"[yellow]⏹️[/yellow] Deactivated {deactivated_count} plugins",
|
|
88
|
+
f"[yellow]⏹️[/ yellow] Deactivated {deactivated_count} plugins",
|
|
89
89
|
)
|
|
90
90
|
|
|
91
91
|
self._initialized = False
|
|
@@ -128,12 +128,12 @@ class PluginManager:
|
|
|
128
128
|
def enable_plugin(self, plugin_name: str) -> bool:
|
|
129
129
|
plugin = self.registry.get(plugin_name)
|
|
130
130
|
if not plugin:
|
|
131
|
-
self.console.print(f"[red]❌[/red] Plugin not found: {plugin_name}")
|
|
131
|
+
self.console.print(f"[red]❌[/ red] Plugin not found: {plugin_name}")
|
|
132
132
|
return False
|
|
133
133
|
|
|
134
134
|
if plugin.enabled:
|
|
135
135
|
self.console.print(
|
|
136
|
-
f"[yellow]⚠️[/yellow] Plugin already enabled: {plugin_name}",
|
|
136
|
+
f"[yellow]⚠️[/ yellow] Plugin already enabled: {plugin_name}",
|
|
137
137
|
)
|
|
138
138
|
return True
|
|
139
139
|
|
|
@@ -142,7 +142,7 @@ class PluginManager:
|
|
|
142
142
|
success = plugin.activate()
|
|
143
143
|
|
|
144
144
|
if success:
|
|
145
|
-
self.console.print(f"[green]✅[/green] Enabled plugin: {plugin_name}")
|
|
145
|
+
self.console.print(f"[green]✅[/ green] Enabled plugin: {plugin_name}")
|
|
146
146
|
|
|
147
147
|
if plugin.plugin_type == PluginType.HOOK:
|
|
148
148
|
self.hook_registry.register_hook_plugin(plugin)
|
|
@@ -150,25 +150,25 @@ class PluginManager:
|
|
|
150
150
|
return True
|
|
151
151
|
plugin.disable()
|
|
152
152
|
self.console.print(
|
|
153
|
-
f"[red]❌[/red] Failed to activate plugin: {plugin_name}",
|
|
153
|
+
f"[red]❌[/ red] Failed to activate plugin: {plugin_name}",
|
|
154
154
|
)
|
|
155
155
|
return False
|
|
156
156
|
|
|
157
157
|
except Exception as e:
|
|
158
158
|
self.console.print(
|
|
159
|
-
f"[red]❌[/red] Error enabling plugin {plugin_name}: {e}",
|
|
159
|
+
f"[red]❌[/ red] Error enabling plugin {plugin_name}: {e}",
|
|
160
160
|
)
|
|
161
161
|
return False
|
|
162
162
|
|
|
163
163
|
def disable_plugin(self, plugin_name: str) -> bool:
|
|
164
164
|
plugin = self.registry.get(plugin_name)
|
|
165
165
|
if not plugin:
|
|
166
|
-
self.console.print(f"[red]❌[/red] Plugin not found: {plugin_name}")
|
|
166
|
+
self.console.print(f"[red]❌[/ red] Plugin not found: {plugin_name}")
|
|
167
167
|
return False
|
|
168
168
|
|
|
169
169
|
if not plugin.enabled:
|
|
170
170
|
self.console.print(
|
|
171
|
-
f"[yellow]⚠️[/yellow] Plugin already disabled: {plugin_name}",
|
|
171
|
+
f"[yellow]⚠️[/ yellow] Plugin already disabled: {plugin_name}",
|
|
172
172
|
)
|
|
173
173
|
return True
|
|
174
174
|
|
|
@@ -180,17 +180,19 @@ class PluginManager:
|
|
|
180
180
|
self.hook_registry.unregister_hook_plugin(plugin_name)
|
|
181
181
|
|
|
182
182
|
if success:
|
|
183
|
-
self.console.print(
|
|
183
|
+
self.console.print(
|
|
184
|
+
f"[yellow]⏹️[/ yellow] Disabled plugin: {plugin_name}"
|
|
185
|
+
)
|
|
184
186
|
else:
|
|
185
187
|
self.console.print(
|
|
186
|
-
f"[yellow]⚠️[/yellow] Plugin disabled with warnings: {plugin_name}",
|
|
188
|
+
f"[yellow]⚠️[/ yellow] Plugin disabled with warnings: {plugin_name}",
|
|
187
189
|
)
|
|
188
190
|
|
|
189
191
|
return True
|
|
190
192
|
|
|
191
193
|
except Exception as e:
|
|
192
194
|
self.console.print(
|
|
193
|
-
f"[red]❌[/red] Error disabling plugin {plugin_name}: {e}",
|
|
195
|
+
f"[red]❌[/ red] Error disabling plugin {plugin_name}: {e}",
|
|
194
196
|
)
|
|
195
197
|
return False
|
|
196
198
|
|
|
@@ -203,16 +205,16 @@ class PluginManager:
|
|
|
203
205
|
def configure_plugin(self, plugin_name: str, config: dict[str, t.Any]) -> bool:
|
|
204
206
|
plugin = self.registry.get(plugin_name)
|
|
205
207
|
if not plugin:
|
|
206
|
-
self.console.print(f"[red]❌[/red] Plugin not found: {plugin_name}")
|
|
208
|
+
self.console.print(f"[red]❌[/ red] Plugin not found: {plugin_name}")
|
|
207
209
|
return False
|
|
208
210
|
|
|
209
211
|
try:
|
|
210
212
|
plugin.configure(config)
|
|
211
|
-
self.console.print(f"[green]✅[/green] Configured plugin: {plugin_name}")
|
|
213
|
+
self.console.print(f"[green]✅[/ green] Configured plugin: {plugin_name}")
|
|
212
214
|
return True
|
|
213
215
|
except Exception as e:
|
|
214
216
|
self.console.print(
|
|
215
|
-
f"[red]❌[/red] Error configuring plugin {plugin_name}: {e}",
|
|
217
|
+
f"[red]❌[/ red] Error configuring plugin {plugin_name}: {e}",
|
|
216
218
|
)
|
|
217
219
|
return False
|
|
218
220
|
|
|
@@ -222,7 +224,7 @@ class PluginManager:
|
|
|
222
224
|
|
|
223
225
|
if success:
|
|
224
226
|
self.console.print(
|
|
225
|
-
f"[green]✅[/green] Installed plugin from: {plugin_file}",
|
|
227
|
+
f"[green]✅[/ green] Installed plugin from: {plugin_file}",
|
|
226
228
|
)
|
|
227
229
|
|
|
228
230
|
if self._initialized:
|
|
@@ -235,14 +237,14 @@ class PluginManager:
|
|
|
235
237
|
self.enable_plugin(latest_plugin_name)
|
|
236
238
|
else:
|
|
237
239
|
self.console.print(
|
|
238
|
-
f"[red]❌[/red] Failed to install plugin from: {plugin_file}",
|
|
240
|
+
f"[red]❌[/ red] Failed to install plugin from: {plugin_file}",
|
|
239
241
|
)
|
|
240
242
|
|
|
241
243
|
return success
|
|
242
244
|
|
|
243
245
|
except Exception as e:
|
|
244
246
|
self.console.print(
|
|
245
|
-
f"[red]❌[/red] Error installing plugin {plugin_file}: {e}",
|
|
247
|
+
f"[red]❌[/ red] Error installing plugin {plugin_file}: {e}",
|
|
246
248
|
)
|
|
247
249
|
return False
|
|
248
250
|
|
crackerjack/py313.py
CHANGED
|
@@ -86,7 +86,7 @@ def categorize_file(file_path: Path) -> str:
|
|
|
86
86
|
path_str = str(file_path)
|
|
87
87
|
name = file_path
|
|
88
88
|
match path_str:
|
|
89
|
-
case s if name.suffix == ".py" and "/tests/" in s:
|
|
89
|
+
case s if name.suffix == ".py" and "/ tests /" in s:
|
|
90
90
|
return "Python Test File"
|
|
91
91
|
case s if name.suffix == ".py" and "__init__.py" in name.name:
|
|
92
92
|
return "Python Module Init"
|
|
@@ -167,24 +167,68 @@ def clean_python_code(code: str) -> str:
|
|
|
167
167
|
lines = code.splitlines()
|
|
168
168
|
cleaned_lines: list[str] = []
|
|
169
169
|
for line in lines:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
cleaned_lines.append("")
|
|
174
|
-
case s if s.startswith(("import ", "from ")):
|
|
175
|
-
cleaned_lines.append(line)
|
|
176
|
-
case s if s.startswith("#"):
|
|
177
|
-
continue
|
|
178
|
-
case s if "#" in s and (
|
|
179
|
-
not any(
|
|
180
|
-
skip in s for skip in ("# noqa", "# type: ", "# pragma", "# skip")
|
|
181
|
-
)
|
|
182
|
-
):
|
|
183
|
-
code_part = line.split("#", 1)[0].rstrip()
|
|
184
|
-
if code_part:
|
|
185
|
-
cleaned_lines.append(code_part)
|
|
186
|
-
case s if s.startswith(('"""', "'''")):
|
|
187
|
-
continue
|
|
188
|
-
case _:
|
|
189
|
-
cleaned_lines.append(line)
|
|
170
|
+
processed_line = _process_line_for_cleaning(line, cleaned_lines)
|
|
171
|
+
if processed_line is not None:
|
|
172
|
+
cleaned_lines.append(processed_line)
|
|
190
173
|
return "\n".join(cleaned_lines)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _process_line_for_cleaning(line: str, cleaned_lines: list[str]) -> str | None:
|
|
177
|
+
"""Process a single line for Python code cleaning.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
The processed line to add, or None if the line should be skipped.
|
|
181
|
+
"""
|
|
182
|
+
stripped = line.strip()
|
|
183
|
+
|
|
184
|
+
if _should_handle_empty_line(stripped, cleaned_lines):
|
|
185
|
+
return ""
|
|
186
|
+
|
|
187
|
+
if _is_import_line(stripped):
|
|
188
|
+
return line
|
|
189
|
+
|
|
190
|
+
if _is_comment_to_skip(stripped):
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
if _has_inline_comment_to_process(stripped):
|
|
194
|
+
return _extract_code_part(line)
|
|
195
|
+
|
|
196
|
+
if _is_docstring_line(stripped):
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
return line
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _should_handle_empty_line(stripped: str, cleaned_lines: list[str]) -> bool:
|
|
203
|
+
"""Check if empty line should be preserved."""
|
|
204
|
+
return stripped == "" and (not cleaned_lines or bool(cleaned_lines[-1].strip()))
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _is_import_line(stripped: str) -> bool:
|
|
208
|
+
"""Check if line is an import statement."""
|
|
209
|
+
return stripped.startswith(("import ", "from "))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _is_comment_to_skip(stripped: str) -> bool:
|
|
213
|
+
"""Check if line is a comment that should be skipped."""
|
|
214
|
+
return stripped.startswith("#")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _has_inline_comment_to_process(stripped: str) -> bool:
|
|
218
|
+
"""Check if line has inline comment that should be processed."""
|
|
219
|
+
if "#" not in stripped:
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
skip_markers = ("# noqa", "# type: ", "# pragma", "# skip")
|
|
223
|
+
return not any(skip in stripped for skip in skip_markers)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _extract_code_part(line: str) -> str | None:
|
|
227
|
+
"""Extract code part from line with inline comment."""
|
|
228
|
+
code_part = line.split("#", 1)[0].rstrip()
|
|
229
|
+
return code_part or None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _is_docstring_line(stripped: str) -> bool:
|
|
233
|
+
"""Check if line starts a docstring."""
|
|
234
|
+
return stripped.startswith(('"""', "'''"))
|