crackerjack 0.37.9__py3-none-any.whl → 0.45.2__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.
Files changed (425) hide show
  1. crackerjack/README.md +19 -0
  2. crackerjack/__init__.py +30 -1
  3. crackerjack/__main__.py +342 -1263
  4. crackerjack/adapters/README.md +18 -0
  5. crackerjack/adapters/__init__.py +27 -5
  6. crackerjack/adapters/_output_paths.py +167 -0
  7. crackerjack/adapters/_qa_adapter_base.py +309 -0
  8. crackerjack/adapters/_tool_adapter_base.py +706 -0
  9. crackerjack/adapters/ai/README.md +65 -0
  10. crackerjack/adapters/ai/__init__.py +5 -0
  11. crackerjack/adapters/ai/claude.py +853 -0
  12. crackerjack/adapters/complexity/README.md +53 -0
  13. crackerjack/adapters/complexity/__init__.py +10 -0
  14. crackerjack/adapters/complexity/complexipy.py +641 -0
  15. crackerjack/adapters/dependency/__init__.py +22 -0
  16. crackerjack/adapters/dependency/pip_audit.py +418 -0
  17. crackerjack/adapters/format/README.md +72 -0
  18. crackerjack/adapters/format/__init__.py +11 -0
  19. crackerjack/adapters/format/mdformat.py +313 -0
  20. crackerjack/adapters/format/ruff.py +516 -0
  21. crackerjack/adapters/lint/README.md +47 -0
  22. crackerjack/adapters/lint/__init__.py +11 -0
  23. crackerjack/adapters/lint/codespell.py +273 -0
  24. crackerjack/adapters/lsp/README.md +49 -0
  25. crackerjack/adapters/lsp/__init__.py +27 -0
  26. crackerjack/adapters/{rust_tool_manager.py → lsp/_manager.py} +3 -3
  27. crackerjack/adapters/{skylos_adapter.py → lsp/skylos.py} +59 -7
  28. crackerjack/adapters/{zuban_adapter.py → lsp/zuban.py} +3 -6
  29. crackerjack/adapters/refactor/README.md +59 -0
  30. crackerjack/adapters/refactor/__init__.py +12 -0
  31. crackerjack/adapters/refactor/creosote.py +318 -0
  32. crackerjack/adapters/refactor/refurb.py +406 -0
  33. crackerjack/adapters/refactor/skylos.py +494 -0
  34. crackerjack/adapters/sast/README.md +132 -0
  35. crackerjack/adapters/sast/__init__.py +32 -0
  36. crackerjack/adapters/sast/_base.py +201 -0
  37. crackerjack/adapters/sast/bandit.py +423 -0
  38. crackerjack/adapters/sast/pyscn.py +405 -0
  39. crackerjack/adapters/sast/semgrep.py +241 -0
  40. crackerjack/adapters/security/README.md +111 -0
  41. crackerjack/adapters/security/__init__.py +17 -0
  42. crackerjack/adapters/security/gitleaks.py +339 -0
  43. crackerjack/adapters/type/README.md +52 -0
  44. crackerjack/adapters/type/__init__.py +12 -0
  45. crackerjack/adapters/type/pyrefly.py +402 -0
  46. crackerjack/adapters/type/ty.py +402 -0
  47. crackerjack/adapters/type/zuban.py +522 -0
  48. crackerjack/adapters/utility/README.md +51 -0
  49. crackerjack/adapters/utility/__init__.py +10 -0
  50. crackerjack/adapters/utility/checks.py +884 -0
  51. crackerjack/agents/README.md +264 -0
  52. crackerjack/agents/__init__.py +40 -12
  53. crackerjack/agents/base.py +1 -0
  54. crackerjack/agents/claude_code_bridge.py +641 -0
  55. crackerjack/agents/coordinator.py +49 -53
  56. crackerjack/agents/dry_agent.py +187 -3
  57. crackerjack/agents/enhanced_coordinator.py +279 -0
  58. crackerjack/agents/enhanced_proactive_agent.py +185 -0
  59. crackerjack/agents/error_middleware.py +53 -0
  60. crackerjack/agents/formatting_agent.py +6 -8
  61. crackerjack/agents/helpers/__init__.py +9 -0
  62. crackerjack/agents/helpers/performance/__init__.py +22 -0
  63. crackerjack/agents/helpers/performance/performance_ast_analyzer.py +357 -0
  64. crackerjack/agents/helpers/performance/performance_pattern_detector.py +909 -0
  65. crackerjack/agents/helpers/performance/performance_recommender.py +572 -0
  66. crackerjack/agents/helpers/refactoring/__init__.py +22 -0
  67. crackerjack/agents/helpers/refactoring/code_transformer.py +536 -0
  68. crackerjack/agents/helpers/refactoring/complexity_analyzer.py +344 -0
  69. crackerjack/agents/helpers/refactoring/dead_code_detector.py +437 -0
  70. crackerjack/agents/helpers/test_creation/__init__.py +19 -0
  71. crackerjack/agents/helpers/test_creation/test_ast_analyzer.py +216 -0
  72. crackerjack/agents/helpers/test_creation/test_coverage_analyzer.py +643 -0
  73. crackerjack/agents/helpers/test_creation/test_template_generator.py +1031 -0
  74. crackerjack/agents/performance_agent.py +121 -1152
  75. crackerjack/agents/refactoring_agent.py +156 -655
  76. crackerjack/agents/semantic_agent.py +479 -0
  77. crackerjack/agents/semantic_helpers.py +356 -0
  78. crackerjack/agents/test_creation_agent.py +19 -1605
  79. crackerjack/api.py +5 -7
  80. crackerjack/cli/README.md +394 -0
  81. crackerjack/cli/__init__.py +1 -1
  82. crackerjack/cli/cache_handlers.py +23 -18
  83. crackerjack/cli/cache_handlers_enhanced.py +1 -4
  84. crackerjack/cli/facade.py +70 -8
  85. crackerjack/cli/formatting.py +13 -0
  86. crackerjack/cli/handlers/__init__.py +85 -0
  87. crackerjack/cli/handlers/advanced.py +103 -0
  88. crackerjack/cli/handlers/ai_features.py +62 -0
  89. crackerjack/cli/handlers/analytics.py +479 -0
  90. crackerjack/cli/handlers/changelog.py +271 -0
  91. crackerjack/cli/handlers/config_handlers.py +16 -0
  92. crackerjack/cli/handlers/coverage.py +84 -0
  93. crackerjack/cli/handlers/documentation.py +280 -0
  94. crackerjack/cli/handlers/main_handlers.py +497 -0
  95. crackerjack/cli/handlers/monitoring.py +371 -0
  96. crackerjack/cli/handlers.py +249 -49
  97. crackerjack/cli/interactive.py +8 -5
  98. crackerjack/cli/options.py +203 -110
  99. crackerjack/cli/semantic_handlers.py +292 -0
  100. crackerjack/cli/version.py +19 -0
  101. crackerjack/code_cleaner.py +60 -24
  102. crackerjack/config/README.md +472 -0
  103. crackerjack/config/__init__.py +256 -0
  104. crackerjack/config/global_lock_config.py +191 -54
  105. crackerjack/config/hooks.py +188 -16
  106. crackerjack/config/loader.py +239 -0
  107. crackerjack/config/settings.py +141 -0
  108. crackerjack/config/tool_commands.py +331 -0
  109. crackerjack/core/README.md +393 -0
  110. crackerjack/core/async_workflow_orchestrator.py +79 -53
  111. crackerjack/core/autofix_coordinator.py +22 -9
  112. crackerjack/core/container.py +10 -9
  113. crackerjack/core/enhanced_container.py +9 -9
  114. crackerjack/core/performance.py +1 -1
  115. crackerjack/core/performance_monitor.py +5 -3
  116. crackerjack/core/phase_coordinator.py +1018 -634
  117. crackerjack/core/proactive_workflow.py +3 -3
  118. crackerjack/core/retry.py +275 -0
  119. crackerjack/core/service_watchdog.py +167 -23
  120. crackerjack/core/session_coordinator.py +187 -382
  121. crackerjack/core/timeout_manager.py +161 -44
  122. crackerjack/core/workflow/__init__.py +21 -0
  123. crackerjack/core/workflow/workflow_ai_coordinator.py +863 -0
  124. crackerjack/core/workflow/workflow_event_orchestrator.py +1107 -0
  125. crackerjack/core/workflow/workflow_issue_parser.py +714 -0
  126. crackerjack/core/workflow/workflow_phase_executor.py +1158 -0
  127. crackerjack/core/workflow/workflow_security_gates.py +400 -0
  128. crackerjack/core/workflow_orchestrator.py +1247 -953
  129. crackerjack/data/README.md +11 -0
  130. crackerjack/data/__init__.py +8 -0
  131. crackerjack/data/models.py +79 -0
  132. crackerjack/data/repository.py +210 -0
  133. crackerjack/decorators/README.md +180 -0
  134. crackerjack/decorators/__init__.py +35 -0
  135. crackerjack/decorators/error_handling.py +649 -0
  136. crackerjack/decorators/error_handling_decorators.py +334 -0
  137. crackerjack/decorators/helpers.py +58 -0
  138. crackerjack/decorators/patterns.py +281 -0
  139. crackerjack/decorators/utils.py +58 -0
  140. crackerjack/docs/README.md +11 -0
  141. crackerjack/docs/generated/api/CLI_REFERENCE.md +1 -1
  142. crackerjack/documentation/README.md +11 -0
  143. crackerjack/documentation/ai_templates.py +1 -1
  144. crackerjack/documentation/dual_output_generator.py +11 -9
  145. crackerjack/documentation/reference_generator.py +104 -59
  146. crackerjack/dynamic_config.py +52 -61
  147. crackerjack/errors.py +1 -1
  148. crackerjack/events/README.md +11 -0
  149. crackerjack/events/__init__.py +16 -0
  150. crackerjack/events/telemetry.py +175 -0
  151. crackerjack/events/workflow_bus.py +346 -0
  152. crackerjack/exceptions/README.md +301 -0
  153. crackerjack/exceptions/__init__.py +5 -0
  154. crackerjack/exceptions/config.py +4 -0
  155. crackerjack/exceptions/tool_execution_error.py +245 -0
  156. crackerjack/executors/README.md +591 -0
  157. crackerjack/executors/__init__.py +2 -0
  158. crackerjack/executors/async_hook_executor.py +539 -77
  159. crackerjack/executors/cached_hook_executor.py +3 -3
  160. crackerjack/executors/hook_executor.py +967 -102
  161. crackerjack/executors/hook_lock_manager.py +31 -22
  162. crackerjack/executors/individual_hook_executor.py +66 -32
  163. crackerjack/executors/lsp_aware_hook_executor.py +136 -57
  164. crackerjack/executors/progress_hook_executor.py +282 -0
  165. crackerjack/executors/tool_proxy.py +23 -7
  166. crackerjack/hooks/README.md +485 -0
  167. crackerjack/hooks/lsp_hook.py +8 -9
  168. crackerjack/intelligence/README.md +557 -0
  169. crackerjack/interactive.py +37 -10
  170. crackerjack/managers/README.md +369 -0
  171. crackerjack/managers/async_hook_manager.py +41 -57
  172. crackerjack/managers/hook_manager.py +449 -79
  173. crackerjack/managers/publish_manager.py +81 -36
  174. crackerjack/managers/test_command_builder.py +290 -12
  175. crackerjack/managers/test_executor.py +93 -8
  176. crackerjack/managers/test_manager.py +1082 -75
  177. crackerjack/managers/test_progress.py +118 -26
  178. crackerjack/mcp/README.md +374 -0
  179. crackerjack/mcp/cache.py +25 -2
  180. crackerjack/mcp/client_runner.py +35 -18
  181. crackerjack/mcp/context.py +9 -9
  182. crackerjack/mcp/dashboard.py +24 -8
  183. crackerjack/mcp/enhanced_progress_monitor.py +34 -23
  184. crackerjack/mcp/file_monitor.py +27 -6
  185. crackerjack/mcp/progress_components.py +45 -34
  186. crackerjack/mcp/progress_monitor.py +6 -9
  187. crackerjack/mcp/rate_limiter.py +11 -7
  188. crackerjack/mcp/server.py +2 -0
  189. crackerjack/mcp/server_core.py +187 -55
  190. crackerjack/mcp/service_watchdog.py +12 -9
  191. crackerjack/mcp/task_manager.py +2 -2
  192. crackerjack/mcp/tools/README.md +27 -0
  193. crackerjack/mcp/tools/__init__.py +2 -0
  194. crackerjack/mcp/tools/core_tools.py +75 -52
  195. crackerjack/mcp/tools/execution_tools.py +87 -31
  196. crackerjack/mcp/tools/intelligence_tools.py +2 -2
  197. crackerjack/mcp/tools/proactive_tools.py +1 -1
  198. crackerjack/mcp/tools/semantic_tools.py +584 -0
  199. crackerjack/mcp/tools/utility_tools.py +180 -132
  200. crackerjack/mcp/tools/workflow_executor.py +87 -46
  201. crackerjack/mcp/websocket/README.md +31 -0
  202. crackerjack/mcp/websocket/app.py +11 -1
  203. crackerjack/mcp/websocket/event_bridge.py +188 -0
  204. crackerjack/mcp/websocket/jobs.py +27 -4
  205. crackerjack/mcp/websocket/monitoring/__init__.py +25 -0
  206. crackerjack/mcp/websocket/monitoring/api/__init__.py +19 -0
  207. crackerjack/mcp/websocket/monitoring/api/dependencies.py +141 -0
  208. crackerjack/mcp/websocket/monitoring/api/heatmap.py +154 -0
  209. crackerjack/mcp/websocket/monitoring/api/intelligence.py +199 -0
  210. crackerjack/mcp/websocket/monitoring/api/metrics.py +203 -0
  211. crackerjack/mcp/websocket/monitoring/api/telemetry.py +101 -0
  212. crackerjack/mcp/websocket/monitoring/dashboard.py +18 -0
  213. crackerjack/mcp/websocket/monitoring/factory.py +109 -0
  214. crackerjack/mcp/websocket/monitoring/filters.py +10 -0
  215. crackerjack/mcp/websocket/monitoring/metrics.py +64 -0
  216. crackerjack/mcp/websocket/monitoring/models.py +90 -0
  217. crackerjack/mcp/websocket/monitoring/utils.py +171 -0
  218. crackerjack/mcp/websocket/monitoring/websocket_manager.py +78 -0
  219. crackerjack/mcp/websocket/monitoring/websockets/__init__.py +17 -0
  220. crackerjack/mcp/websocket/monitoring/websockets/dependencies.py +126 -0
  221. crackerjack/mcp/websocket/monitoring/websockets/heatmap.py +176 -0
  222. crackerjack/mcp/websocket/monitoring/websockets/intelligence.py +291 -0
  223. crackerjack/mcp/websocket/monitoring/websockets/metrics.py +291 -0
  224. crackerjack/mcp/websocket/monitoring_endpoints.py +16 -2930
  225. crackerjack/mcp/websocket/server.py +1 -3
  226. crackerjack/mcp/websocket/websocket_handler.py +107 -6
  227. crackerjack/models/README.md +308 -0
  228. crackerjack/models/__init__.py +10 -1
  229. crackerjack/models/config.py +639 -22
  230. crackerjack/models/config_adapter.py +6 -6
  231. crackerjack/models/protocols.py +1167 -23
  232. crackerjack/models/pydantic_models.py +320 -0
  233. crackerjack/models/qa_config.py +145 -0
  234. crackerjack/models/qa_results.py +134 -0
  235. crackerjack/models/results.py +35 -0
  236. crackerjack/models/semantic_models.py +258 -0
  237. crackerjack/models/task.py +19 -3
  238. crackerjack/models/test_models.py +60 -0
  239. crackerjack/monitoring/README.md +11 -0
  240. crackerjack/monitoring/ai_agent_watchdog.py +5 -4
  241. crackerjack/monitoring/metrics_collector.py +4 -3
  242. crackerjack/monitoring/regression_prevention.py +4 -3
  243. crackerjack/monitoring/websocket_server.py +4 -241
  244. crackerjack/orchestration/README.md +340 -0
  245. crackerjack/orchestration/__init__.py +43 -0
  246. crackerjack/orchestration/advanced_orchestrator.py +20 -67
  247. crackerjack/orchestration/cache/README.md +312 -0
  248. crackerjack/orchestration/cache/__init__.py +37 -0
  249. crackerjack/orchestration/cache/memory_cache.py +338 -0
  250. crackerjack/orchestration/cache/tool_proxy_cache.py +340 -0
  251. crackerjack/orchestration/config.py +297 -0
  252. crackerjack/orchestration/coverage_improvement.py +13 -6
  253. crackerjack/orchestration/execution_strategies.py +6 -6
  254. crackerjack/orchestration/hook_orchestrator.py +1398 -0
  255. crackerjack/orchestration/strategies/README.md +401 -0
  256. crackerjack/orchestration/strategies/__init__.py +39 -0
  257. crackerjack/orchestration/strategies/adaptive_strategy.py +630 -0
  258. crackerjack/orchestration/strategies/parallel_strategy.py +237 -0
  259. crackerjack/orchestration/strategies/sequential_strategy.py +299 -0
  260. crackerjack/orchestration/test_progress_streamer.py +1 -1
  261. crackerjack/plugins/README.md +11 -0
  262. crackerjack/plugins/hooks.py +3 -2
  263. crackerjack/plugins/loader.py +3 -3
  264. crackerjack/plugins/managers.py +1 -1
  265. crackerjack/py313.py +191 -0
  266. crackerjack/security/README.md +11 -0
  267. crackerjack/services/README.md +374 -0
  268. crackerjack/services/__init__.py +8 -21
  269. crackerjack/services/ai/README.md +295 -0
  270. crackerjack/services/ai/__init__.py +7 -0
  271. crackerjack/services/ai/advanced_optimizer.py +878 -0
  272. crackerjack/services/{contextual_ai_assistant.py → ai/contextual_ai_assistant.py} +5 -3
  273. crackerjack/services/ai/embeddings.py +444 -0
  274. crackerjack/services/ai/intelligent_commit.py +328 -0
  275. crackerjack/services/ai/predictive_analytics.py +510 -0
  276. crackerjack/services/api_extractor.py +5 -3
  277. crackerjack/services/bounded_status_operations.py +45 -5
  278. crackerjack/services/cache.py +249 -318
  279. crackerjack/services/changelog_automation.py +7 -3
  280. crackerjack/services/command_execution_service.py +305 -0
  281. crackerjack/services/config_integrity.py +83 -39
  282. crackerjack/services/config_merge.py +9 -6
  283. crackerjack/services/config_service.py +198 -0
  284. crackerjack/services/config_template.py +13 -26
  285. crackerjack/services/coverage_badge_service.py +6 -4
  286. crackerjack/services/coverage_ratchet.py +53 -27
  287. crackerjack/services/debug.py +18 -7
  288. crackerjack/services/dependency_analyzer.py +4 -4
  289. crackerjack/services/dependency_monitor.py +13 -13
  290. crackerjack/services/documentation_generator.py +4 -2
  291. crackerjack/services/documentation_service.py +62 -33
  292. crackerjack/services/enhanced_filesystem.py +81 -27
  293. crackerjack/services/enterprise_optimizer.py +1 -1
  294. crackerjack/services/error_pattern_analyzer.py +10 -10
  295. crackerjack/services/file_filter.py +221 -0
  296. crackerjack/services/file_hasher.py +5 -7
  297. crackerjack/services/file_io_service.py +361 -0
  298. crackerjack/services/file_modifier.py +615 -0
  299. crackerjack/services/filesystem.py +80 -109
  300. crackerjack/services/git.py +99 -5
  301. crackerjack/services/health_metrics.py +4 -6
  302. crackerjack/services/heatmap_generator.py +12 -3
  303. crackerjack/services/incremental_executor.py +380 -0
  304. crackerjack/services/initialization.py +101 -49
  305. crackerjack/services/log_manager.py +2 -2
  306. crackerjack/services/logging.py +120 -68
  307. crackerjack/services/lsp_client.py +12 -12
  308. crackerjack/services/memory_optimizer.py +27 -22
  309. crackerjack/services/monitoring/README.md +30 -0
  310. crackerjack/services/monitoring/__init__.py +9 -0
  311. crackerjack/services/monitoring/dependency_monitor.py +678 -0
  312. crackerjack/services/monitoring/error_pattern_analyzer.py +676 -0
  313. crackerjack/services/monitoring/health_metrics.py +716 -0
  314. crackerjack/services/monitoring/metrics.py +587 -0
  315. crackerjack/services/{performance_benchmarks.py → monitoring/performance_benchmarks.py} +100 -14
  316. crackerjack/services/{performance_cache.py → monitoring/performance_cache.py} +21 -15
  317. crackerjack/services/{performance_monitor.py → monitoring/performance_monitor.py} +10 -6
  318. crackerjack/services/parallel_executor.py +166 -55
  319. crackerjack/services/patterns/__init__.py +142 -0
  320. crackerjack/services/patterns/agents.py +107 -0
  321. crackerjack/services/patterns/code/__init__.py +15 -0
  322. crackerjack/services/patterns/code/detection.py +118 -0
  323. crackerjack/services/patterns/code/imports.py +107 -0
  324. crackerjack/services/patterns/code/paths.py +159 -0
  325. crackerjack/services/patterns/code/performance.py +119 -0
  326. crackerjack/services/patterns/code/replacement.py +36 -0
  327. crackerjack/services/patterns/core.py +212 -0
  328. crackerjack/services/patterns/documentation/__init__.py +14 -0
  329. crackerjack/services/patterns/documentation/badges_markdown.py +96 -0
  330. crackerjack/services/patterns/documentation/comments_blocks.py +83 -0
  331. crackerjack/services/patterns/documentation/docstrings.py +89 -0
  332. crackerjack/services/patterns/formatting.py +226 -0
  333. crackerjack/services/patterns/operations.py +339 -0
  334. crackerjack/services/patterns/security/__init__.py +23 -0
  335. crackerjack/services/patterns/security/code_injection.py +122 -0
  336. crackerjack/services/patterns/security/credentials.py +190 -0
  337. crackerjack/services/patterns/security/path_traversal.py +221 -0
  338. crackerjack/services/patterns/security/unsafe_operations.py +216 -0
  339. crackerjack/services/patterns/templates.py +62 -0
  340. crackerjack/services/patterns/testing/__init__.py +18 -0
  341. crackerjack/services/patterns/testing/error_patterns.py +107 -0
  342. crackerjack/services/patterns/testing/pytest_output.py +126 -0
  343. crackerjack/services/patterns/tool_output/__init__.py +16 -0
  344. crackerjack/services/patterns/tool_output/bandit.py +72 -0
  345. crackerjack/services/patterns/tool_output/other.py +97 -0
  346. crackerjack/services/patterns/tool_output/pyright.py +67 -0
  347. crackerjack/services/patterns/tool_output/ruff.py +44 -0
  348. crackerjack/services/patterns/url_sanitization.py +114 -0
  349. crackerjack/services/patterns/utilities.py +42 -0
  350. crackerjack/services/patterns/utils.py +339 -0
  351. crackerjack/services/patterns/validation.py +46 -0
  352. crackerjack/services/patterns/versioning.py +62 -0
  353. crackerjack/services/predictive_analytics.py +21 -8
  354. crackerjack/services/profiler.py +280 -0
  355. crackerjack/services/quality/README.md +415 -0
  356. crackerjack/services/quality/__init__.py +11 -0
  357. crackerjack/services/quality/anomaly_detector.py +392 -0
  358. crackerjack/services/quality/pattern_cache.py +333 -0
  359. crackerjack/services/quality/pattern_detector.py +479 -0
  360. crackerjack/services/quality/qa_orchestrator.py +491 -0
  361. crackerjack/services/{quality_baseline.py → quality/quality_baseline.py} +163 -2
  362. crackerjack/services/{quality_baseline_enhanced.py → quality/quality_baseline_enhanced.py} +4 -1
  363. crackerjack/services/{quality_intelligence.py → quality/quality_intelligence.py} +180 -16
  364. crackerjack/services/regex_patterns.py +58 -2987
  365. crackerjack/services/regex_utils.py +55 -29
  366. crackerjack/services/secure_status_formatter.py +42 -15
  367. crackerjack/services/secure_subprocess.py +35 -2
  368. crackerjack/services/security.py +16 -8
  369. crackerjack/services/server_manager.py +40 -51
  370. crackerjack/services/smart_scheduling.py +46 -6
  371. crackerjack/services/status_authentication.py +3 -3
  372. crackerjack/services/thread_safe_status_collector.py +1 -0
  373. crackerjack/services/tool_filter.py +368 -0
  374. crackerjack/services/tool_version_service.py +9 -5
  375. crackerjack/services/unified_config.py +43 -351
  376. crackerjack/services/vector_store.py +689 -0
  377. crackerjack/services/version_analyzer.py +6 -4
  378. crackerjack/services/version_checker.py +14 -8
  379. crackerjack/services/zuban_lsp_service.py +5 -4
  380. crackerjack/slash_commands/README.md +11 -0
  381. crackerjack/slash_commands/init.md +2 -12
  382. crackerjack/slash_commands/run.md +84 -50
  383. crackerjack/tools/README.md +11 -0
  384. crackerjack/tools/__init__.py +30 -0
  385. crackerjack/tools/_git_utils.py +105 -0
  386. crackerjack/tools/check_added_large_files.py +139 -0
  387. crackerjack/tools/check_ast.py +105 -0
  388. crackerjack/tools/check_json.py +103 -0
  389. crackerjack/tools/check_jsonschema.py +297 -0
  390. crackerjack/tools/check_toml.py +103 -0
  391. crackerjack/tools/check_yaml.py +110 -0
  392. crackerjack/tools/codespell_wrapper.py +72 -0
  393. crackerjack/tools/end_of_file_fixer.py +202 -0
  394. crackerjack/tools/format_json.py +128 -0
  395. crackerjack/tools/mdformat_wrapper.py +114 -0
  396. crackerjack/tools/trailing_whitespace.py +198 -0
  397. crackerjack/tools/validate_regex_patterns.py +7 -3
  398. crackerjack/ui/README.md +11 -0
  399. crackerjack/ui/dashboard_renderer.py +28 -0
  400. crackerjack/ui/templates/README.md +11 -0
  401. crackerjack/utils/console_utils.py +13 -0
  402. crackerjack/utils/dependency_guard.py +230 -0
  403. crackerjack/utils/retry_utils.py +275 -0
  404. crackerjack/workflows/README.md +590 -0
  405. crackerjack/workflows/__init__.py +46 -0
  406. crackerjack/workflows/actions.py +811 -0
  407. crackerjack/workflows/auto_fix.py +444 -0
  408. crackerjack/workflows/container_builder.py +499 -0
  409. crackerjack/workflows/definitions.py +443 -0
  410. crackerjack/workflows/engine.py +177 -0
  411. crackerjack/workflows/event_bridge.py +242 -0
  412. {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/METADATA +678 -98
  413. crackerjack-0.45.2.dist-info/RECORD +478 -0
  414. {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/WHEEL +1 -1
  415. crackerjack/managers/test_manager_backup.py +0 -1075
  416. crackerjack/mcp/tools/execution_tools_backup.py +0 -1011
  417. crackerjack/mixins/__init__.py +0 -3
  418. crackerjack/mixins/error_handling.py +0 -145
  419. crackerjack/services/config.py +0 -358
  420. crackerjack/ui/server_panels.py +0 -125
  421. crackerjack-0.37.9.dist-info/RECORD +0 -231
  422. /crackerjack/adapters/{rust_tool_adapter.py → lsp/_base.py} +0 -0
  423. /crackerjack/adapters/{lsp_client.py → lsp/_client.py} +0 -0
  424. {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/entry_points.txt +0 -0
  425. {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,1075 +0,0 @@
1
- import re
2
- import subprocess
3
- import threading
4
- import time
5
- import typing as t
6
- from pathlib import Path
7
-
8
- from rich.align import Align
9
- from rich.console import Console
10
- from rich.live import Live
11
- from rich.table import Table
12
-
13
- from crackerjack.models.protocols import OptionsProtocol
14
- from crackerjack.services.coverage_ratchet import CoverageRatchetService
15
-
16
-
17
- class TestProgress:
18
- def __init__(self) -> None:
19
- self.total_tests: int = 0
20
- self.passed: int = 0
21
- self.failed: int = 0
22
- self.skipped: int = 0
23
- self.errors: int = 0
24
- self.current_test: str = ""
25
- self.start_time: float = 0
26
- self.is_complete: bool = False
27
- self.is_collecting: bool = True
28
- self.files_discovered: int = 0
29
- self.collection_status: str = "Starting collection..."
30
- self._lock = threading.Lock()
31
- self._seen_files: set[str] = set()
32
-
33
- @property
34
- def completed(self) -> int:
35
- return self.passed + self.failed + self.skipped + self.errors
36
-
37
- @property
38
- def elapsed_time(self) -> float:
39
- return time.time() - self.start_time if self.start_time else 0
40
-
41
- @property
42
- def eta_seconds(self) -> float | None:
43
- if self.completed <= 0 or self.total_tests <= 0:
44
- return None
45
- progress_rate = (
46
- self.completed / self.elapsed_time if self.elapsed_time > 0 else 0
47
- )
48
- remaining = self.total_tests - self.completed
49
- return remaining / progress_rate if progress_rate > 0 else None
50
-
51
- def update(self, **kwargs: t.Any) -> None:
52
- with self._lock:
53
- for key, value in kwargs.items():
54
- if hasattr(self, key):
55
- setattr(self, key, value)
56
-
57
- def format_progress(self) -> Align:
58
- with self._lock:
59
- if self.is_collecting:
60
- table = self._format_collection_progress()
61
- else:
62
- table = self._format_execution_progress()
63
-
64
- return Align.left(table)
65
-
66
- def _format_collection_progress(self) -> Table:
67
- table = Table(
68
- title="🔍 Test Collection",
69
- header_style="bold yellow",
70
- show_lines=True,
71
- border_style="yellow",
72
- title_style="bold yellow",
73
- expand=True,
74
- min_width=74,
75
- )
76
-
77
- table.add_column("Type", style="cyan", ratio=1)
78
- table.add_column("Details", style="white", ratio=3)
79
- table.add_column("Count", style="green", ratio=1)
80
-
81
- table.add_row("Status", self.collection_status, "")
82
-
83
- if self.files_discovered > 0:
84
- table.add_row("Files", "Test files discovered", str(self.files_discovered))
85
-
86
- if self.total_tests > 0:
87
- table.add_row("Tests", "Total tests found", str(self.total_tests))
88
-
89
- if self.files_discovered > 0:
90
- progress_chars = "▓" * min(self.files_discovered, 15) + "░" * max(
91
- 0, 15 - self.files_discovered
92
- )
93
- table.add_row(
94
- "Progress", f"[{progress_chars}]", f"{self.files_discovered}/ 15"
95
- )
96
-
97
- table.add_row("Duration", f"{self.elapsed_time: .1f} seconds", "")
98
-
99
- return table
100
-
101
- def _format_execution_progress(self) -> Table:
102
- table = Table(
103
- title="🧪 Test Execution",
104
- header_style="bold cyan",
105
- show_lines=True,
106
- border_style="cyan",
107
- title_style="bold cyan",
108
- expand=True,
109
- min_width=74,
110
- )
111
-
112
- table.add_column("Metric", style="cyan", ratio=1)
113
- table.add_column("Details", style="white", ratio=3)
114
- table.add_column("Count", style="green", ratio=1)
115
-
116
- if self.total_tests > 0:
117
- table.add_row("Total", "Total tests", str(self.total_tests))
118
- table.add_row("Passed", "Tests passed", f"[green]{self.passed}[/ green]")
119
-
120
- if self.failed > 0:
121
- table.add_row("Failed", "Tests failed", f"[red]{self.failed}[/ red]")
122
- if self.skipped > 0:
123
- table.add_row(
124
- "Skipped", "Tests skipped", f"[yellow]{self.skipped}[/ yellow]"
125
- )
126
- if self.errors > 0:
127
- table.add_row("Errors", "Test errors", f"[red]{self.errors}[/ red]")
128
-
129
- if self.total_tests > 0:
130
- percentage = (self.completed / self.total_tests) * 100
131
- filled = int((self.completed / self.total_tests) * 15)
132
- bar = "█" * filled + "░" * (15 - filled)
133
- table.add_row("Progress", f"[{bar}]", f"{percentage: .1f}%")
134
-
135
- if self.current_test:
136
- test_name = self.current_test
137
- if len(test_name) > 40:
138
- test_name = test_name[:37] + "..."
139
- table.add_row("Current", test_name, "")
140
-
141
- duration_text = f"{self.elapsed_time: .1f}s"
142
- if self.eta_seconds is not None and self.eta_seconds > 0:
143
- table.add_row("Duration", duration_text, f"ETA: ~{self.eta_seconds: .0f}s")
144
- else:
145
- table.add_row("Duration", duration_text, "")
146
-
147
- return table
148
-
149
-
150
- class TestManagementImpl:
151
- def __init__(self, console: Console, pkg_path: Path) -> None:
152
- self.console = console
153
- self.pkg_path = pkg_path
154
- self._last_test_failures: list[str] = []
155
- self._progress_callback: t.Callable[[dict[str, t.Any]], None] | None = None
156
- self.coverage_ratchet = CoverageRatchetService(pkg_path, console)
157
- self.coverage_ratchet_enabled = True
158
-
159
- def set_progress_callback(
160
- self,
161
- callback: t.Callable[[dict[str, t.Any]], None] | None,
162
- ) -> None:
163
- self._progress_callback = callback
164
-
165
- def set_coverage_ratchet_enabled(self, enabled: bool) -> None:
166
- self.coverage_ratchet_enabled = enabled
167
- if enabled:
168
- self.console.print(
169
- "[cyan]📊[/ cyan] Coverage ratchet enabled-targeting 100 % coverage"
170
- )
171
- else:
172
- self.console.print("[yellow]⚠️[/ yellow] Coverage ratchet disabled")
173
-
174
- def get_coverage_ratchet_status(self) -> dict[str, t.Any]:
175
- return self.coverage_ratchet.get_ratchet_data()
176
-
177
- def _run_test_command(
178
- self,
179
- cmd: list[str],
180
- timeout: int = 600,
181
- ) -> subprocess.CompletedProcess[str]:
182
- import os
183
- from pathlib import Path
184
-
185
- cache_dir = Path.home() / ".cache" / "crackerjack" / "coverage"
186
- cache_dir.mkdir(parents=True, exist_ok=True)
187
-
188
- env = os.environ.copy()
189
- env["COVERAGE_FILE"] = str(cache_dir / ".coverage")
190
-
191
- return subprocess.run(
192
- cmd,
193
- check=False,
194
- cwd=self.pkg_path,
195
- capture_output=True,
196
- text=True,
197
- timeout=timeout,
198
- env=env,
199
- )
200
-
201
- def _run_test_command_with_progress(
202
- self,
203
- cmd: list[str],
204
- timeout: int = 600,
205
- show_progress: bool = True,
206
- ) -> subprocess.CompletedProcess[str]:
207
- if not show_progress:
208
- return self._run_test_command(cmd, timeout)
209
-
210
- try:
211
- return self._execute_with_live_progress(cmd, timeout)
212
- except Exception as e:
213
- return self._handle_progress_error(e, cmd, timeout)
214
-
215
- def _execute_with_live_progress(
216
- self,
217
- cmd: list[str],
218
- timeout: int,
219
- ) -> subprocess.CompletedProcess[str]:
220
- progress = self._initialize_progress()
221
- stdout_lines: list[str] = []
222
- stderr_lines: list[str] = []
223
-
224
- activity_tracker = {"last_time": time.time()}
225
-
226
- with (
227
- Live(
228
- progress.format_progress(),
229
- refresh_per_second=2,
230
- console=self.console,
231
- auto_refresh=False,
232
- transient=True,
233
- ) as live,
234
- subprocess.Popen(
235
- cmd,
236
- cwd=self.pkg_path,
237
- stdout=subprocess.PIPE,
238
- stderr=subprocess.PIPE,
239
- text=True,
240
- env=self._setup_test_environment(),
241
- ) as process,
242
- ):
243
- threads = self._start_reader_threads(
244
- process,
245
- progress,
246
- stdout_lines,
247
- stderr_lines,
248
- live,
249
- activity_tracker,
250
- )
251
-
252
- returncode = self._wait_for_completion(process, progress, live, timeout)
253
- self._cleanup_threads(threads, progress, live)
254
-
255
- return subprocess.CompletedProcess(
256
- args=cmd,
257
- returncode=returncode,
258
- stdout="\n".join(stdout_lines),
259
- stderr="\n".join(stderr_lines),
260
- )
261
-
262
- def _initialize_progress(self) -> TestProgress:
263
- progress = TestProgress()
264
- progress.start_time = time.time()
265
- progress.collection_status = "Initializing test collection..."
266
- return progress
267
-
268
- def _setup_test_environment(self) -> dict[str, str]:
269
- import os
270
- from pathlib import Path
271
-
272
- cache_dir = Path.home() / ".cache" / "crackerjack" / "coverage"
273
- cache_dir.mkdir(parents=True, exist_ok=True)
274
-
275
- env = os.environ.copy()
276
- env["COVERAGE_FILE"] = str(cache_dir / ".coverage")
277
- return env
278
-
279
- def _start_reader_threads(
280
- self,
281
- process: subprocess.Popen[str],
282
- progress: TestProgress,
283
- stdout_lines: list[str],
284
- stderr_lines: list[str],
285
- live: Live,
286
- activity_tracker: dict[str, float],
287
- ) -> dict[str, threading.Thread]:
288
- read_output = self._create_stdout_reader(
289
- process,
290
- progress,
291
- stdout_lines,
292
- live,
293
- activity_tracker,
294
- )
295
- read_stderr = self._create_stderr_reader(process, stderr_lines)
296
- monitor_stuck = self._create_monitor_thread(
297
- process,
298
- progress,
299
- live,
300
- activity_tracker,
301
- )
302
-
303
- threads = {
304
- "stdout": threading.Thread(target=read_output, daemon=True),
305
- "stderr": threading.Thread(target=read_stderr, daemon=True),
306
- "monitor": threading.Thread(target=monitor_stuck, daemon=True),
307
- }
308
-
309
- for thread in threads.values():
310
- thread.start()
311
-
312
- return threads
313
-
314
- def _create_stdout_reader(
315
- self,
316
- process: subprocess.Popen[str],
317
- progress: TestProgress,
318
- stdout_lines: list[str],
319
- live: Live,
320
- activity_tracker: dict[str, float],
321
- ) -> t.Callable[[], None]:
322
- def read_output() -> None:
323
- refresh_state = {"last_refresh": 0, "last_content": ""}
324
-
325
- if process.stdout:
326
- for line in iter(process.stdout.readline, ""):
327
- if not line:
328
- break
329
-
330
- processed_line = line.rstrip()
331
- if processed_line.strip():
332
- self._process_test_output_line(
333
- processed_line, stdout_lines, progress, activity_tracker
334
- )
335
- self._update_display_if_needed(progress, live, refresh_state)
336
-
337
- return read_output
338
-
339
- def _process_test_output_line(
340
- self,
341
- line: str,
342
- stdout_lines: list[str],
343
- progress: TestProgress,
344
- activity_tracker: dict[str, float],
345
- ) -> None:
346
- stdout_lines.append(line)
347
- self._parse_test_line(line, progress)
348
- activity_tracker["last_time"] = time.time()
349
-
350
- def _update_display_if_needed(
351
- self,
352
- progress: TestProgress,
353
- live: Live,
354
- refresh_state: dict[str, t.Any],
355
- ) -> None:
356
- current_time = time.time()
357
- refresh_interval = self._get_refresh_interval(progress)
358
- current_content = self._get_current_content_signature(progress)
359
-
360
- if self._should_refresh_display(
361
- current_time, refresh_state, refresh_interval, current_content
362
- ):
363
- live.update(progress.format_progress())
364
- live.refresh()
365
- refresh_state["last_refresh"] = current_time
366
- refresh_state["last_content"] = current_content
367
-
368
- def _get_refresh_interval(self, progress: TestProgress) -> float:
369
- return 1.0 if progress.is_collecting else 0.25
370
-
371
- def _get_current_content_signature(self, progress: TestProgress) -> str:
372
- return f"{progress.collection_status}: {progress.files_discovered}: {progress.total_tests}"
373
-
374
- def _should_refresh_display(
375
- self,
376
- current_time: float,
377
- refresh_state: dict[str, t.Any],
378
- refresh_interval: float,
379
- current_content: str,
380
- ) -> bool:
381
- time_elapsed = current_time - refresh_state["last_refresh"] > refresh_interval
382
- content_changed = current_content != refresh_state["last_content"]
383
- return time_elapsed or content_changed
384
-
385
- def _create_stderr_reader(
386
- self,
387
- process: subprocess.Popen[str],
388
- stderr_lines: list[str],
389
- ) -> t.Callable[[], None]:
390
- def read_stderr() -> None:
391
- if process.stderr:
392
- for line in iter(process.stderr.readline, ""):
393
- if not line:
394
- break
395
- stderr_lines.append(line.rstrip())
396
-
397
- return read_stderr
398
-
399
- def _create_monitor_thread(
400
- self,
401
- process: subprocess.Popen[str],
402
- progress: TestProgress,
403
- live: Live,
404
- activity_tracker: dict[str, float],
405
- ) -> t.Callable[[], None]:
406
- def monitor_stuck_tests() -> None:
407
- while process.poll() is None:
408
- time.sleep(5)
409
- current_time = time.time()
410
- if current_time - activity_tracker["last_time"] > 30:
411
- self._mark_test_as_stuck(
412
- progress,
413
- current_time - activity_tracker["last_time"],
414
- live,
415
- )
416
-
417
- return monitor_stuck_tests
418
-
419
- def _mark_test_as_stuck(
420
- self,
421
- progress: TestProgress,
422
- stuck_time: float,
423
- live: Live,
424
- ) -> None:
425
- if progress.current_test and "stuck" not in progress.current_test.lower():
426
- progress.update(
427
- current_test=f"{progress.current_test} (possibly stuck-{stuck_time: .0f}s)",
428
- )
429
- live.update(progress.format_progress())
430
- live.refresh()
431
-
432
- def _wait_for_completion(
433
- self,
434
- process: subprocess.Popen[str],
435
- progress: TestProgress,
436
- live: Live,
437
- timeout: int,
438
- ) -> int:
439
- try:
440
- return process.wait(timeout=timeout)
441
- except subprocess.TimeoutExpired:
442
- process.kill()
443
- progress.update(current_test="TIMEOUT-Process killed")
444
- live.update(progress.format_progress())
445
- live.refresh()
446
- raise
447
-
448
- def _cleanup_threads(
449
- self,
450
- threads: dict[str, threading.Thread],
451
- progress: TestProgress,
452
- live: Live,
453
- ) -> None:
454
- threads["stdout"].join(timeout=1)
455
- threads["stderr"].join(timeout=1)
456
- progress.is_complete = True
457
- live.update(progress.format_progress())
458
- live.refresh()
459
-
460
- def _handle_progress_error(
461
- self,
462
- error: Exception,
463
- cmd: list[str],
464
- timeout: int,
465
- ) -> subprocess.CompletedProcess[str]:
466
- from contextlib import suppress
467
-
468
- with suppress(Exception):
469
- self.console.print(f"[red]❌ Progress display failed: {error}[/ red]")
470
- self.console.print("[yellow]⚠️ Falling back to standard mode[/ yellow]")
471
- return self._run_test_command(cmd, timeout)
472
-
473
- def _parse_test_line(self, line: str, progress: TestProgress) -> None:
474
- if self._handle_collection_completion(line, progress):
475
- return
476
- if self._handle_session_events(line, progress):
477
- return
478
- if self._handle_collection_progress(line, progress):
479
- return
480
- if self._handle_test_execution(line, progress):
481
- return
482
- self._handle_running_test(line, progress)
483
-
484
- def _handle_collection_completion(self, line: str, progress: TestProgress) -> bool:
485
- match = re.search(r"collected (\d+) items?", line)
486
- if match:
487
- progress.update(
488
- total_tests=int(match.group(1)),
489
- is_collecting=False,
490
- current_test="Starting test execution...",
491
- )
492
- return True
493
- return False
494
-
495
- def _handle_session_events(self, line: str, progress: TestProgress) -> bool:
496
- if "test session starts" in line.lower():
497
- progress.update(collection_status="Session starting...")
498
- return True
499
- if line.startswith("collecting") or "collecting" in line.lower():
500
- progress.update(collection_status="Collecting tests...")
501
- return True
502
- return False
503
-
504
- def _handle_collection_progress(self, line: str, progress: TestProgress) -> bool:
505
- if not progress.is_collecting:
506
- return False
507
-
508
- if line.strip().startswith("collecting") or "collecting" in line.lower():
509
- progress.update(collection_status="Collecting tests...")
510
- return True
511
-
512
- if (
513
- ":: " in line
514
- and ".py" in line
515
- and ("test_" in line or "tests /" in line)
516
- and not any(
517
- status in line for status in ("PASSED", "FAILED", "SKIPPED", "ERROR")
518
- )
519
- ):
520
- filename = line.split("/")[-1] if "/" in line else line.split(":: ")[0]
521
- if filename.endswith(".py") and filename not in progress._seen_files:
522
- progress._seen_files.add(filename)
523
- new_count = progress.files_discovered + 1
524
- progress.update(
525
- files_discovered=new_count,
526
- collection_status=f"Discovering tests... ({new_count} files)",
527
- )
528
- return True
529
-
530
- return False
531
-
532
- def _handle_test_execution(self, line: str, progress: TestProgress) -> bool:
533
- if not (
534
- ":: " in line
535
- and any(
536
- status in line for status in ("PASSED", "FAILED", "SKIPPED", "ERROR")
537
- )
538
- ):
539
- return False
540
-
541
- if "PASSED" in line:
542
- progress.update(passed=progress.passed + 1)
543
- elif "FAILED" in line:
544
- progress.update(failed=progress.failed + 1)
545
- elif "SKIPPED" in line:
546
- progress.update(skipped=progress.skipped + 1)
547
- elif "ERROR" in line:
548
- progress.update(errors=progress.errors + 1)
549
-
550
- self._extract_current_test(line, progress)
551
- return True
552
-
553
- def _handle_running_test(self, line: str, progress: TestProgress) -> None:
554
- if ":: " not in line or any(
555
- status in line for status in ("PASSED", "FAILED", "SKIPPED", "ERROR")
556
- ):
557
- return
558
-
559
- parts = line.split()
560
- if parts and ":: " in parts[0]:
561
- test_path = parts[0]
562
- if "/" in test_path:
563
- test_path = test_path.split("/")[-1]
564
- progress.update(current_test=f"Running: {test_path}")
565
-
566
- def _extract_current_test(self, line: str, progress: TestProgress) -> None:
567
- parts = line.split()
568
- if parts and ":: " in parts[0]:
569
- test_path = parts[0]
570
-
571
- if "/" in test_path:
572
- test_path = test_path.split("/")[-1]
573
- progress.update(current_test=test_path)
574
-
575
- def _run_test_command_with_ai_progress(
576
- self,
577
- cmd: list[str],
578
- timeout: int = 600,
579
- ) -> subprocess.CompletedProcess[str]:
580
- try:
581
- env = self._setup_coverage_env()
582
- progress = TestProgress()
583
- progress.start_time = time.time()
584
-
585
- return self._execute_test_process_with_progress(cmd, timeout, env, progress)
586
- except Exception:
587
- return self._run_test_command(cmd, timeout)
588
-
589
- def _setup_coverage_env(self) -> dict[str, str]:
590
- import os
591
- from pathlib import Path
592
-
593
- cache_dir = Path.home() / ".cache" / "crackerjack" / "coverage"
594
- cache_dir.mkdir(parents=True, exist_ok=True)
595
-
596
- env = os.environ.copy()
597
- env["COVERAGE_FILE"] = str(cache_dir / ".coverage")
598
- return env
599
-
600
- def _execute_test_process_with_progress(
601
- self,
602
- cmd: list[str],
603
- timeout: int,
604
- env: dict[str, str],
605
- progress: TestProgress,
606
- ) -> subprocess.CompletedProcess[str]:
607
- stdout_lines: list[str] = []
608
- stderr_lines: list[str] = []
609
- last_update_time = [time.time()]
610
-
611
- with subprocess.Popen(
612
- cmd,
613
- cwd=self.pkg_path,
614
- stdout=subprocess.PIPE,
615
- stderr=subprocess.PIPE,
616
- text=True,
617
- env=env,
618
- ) as process:
619
- stdout_thread = threading.Thread(
620
- target=self._read_stdout_with_progress,
621
- args=(process, stdout_lines, progress, last_update_time),
622
- daemon=True,
623
- )
624
- stderr_thread = threading.Thread(
625
- target=self._read_stderr_lines,
626
- args=(process, stderr_lines),
627
- daemon=True,
628
- )
629
-
630
- stdout_thread.start()
631
- stderr_thread.start()
632
-
633
- returncode = self._wait_for_process_completion(process, timeout)
634
-
635
- stdout_thread.join(timeout=1)
636
- stderr_thread.join(timeout=1)
637
-
638
- progress.is_complete = True
639
- self._emit_ai_progress(progress)
640
-
641
- return subprocess.CompletedProcess(
642
- args=cmd,
643
- returncode=returncode,
644
- stdout="\n".join(stdout_lines),
645
- stderr="\n".join(stderr_lines),
646
- )
647
-
648
- def _read_stdout_with_progress(
649
- self,
650
- process: subprocess.Popen[str],
651
- stdout_lines: list[str],
652
- progress: TestProgress,
653
- last_update_time: list[float],
654
- ) -> None:
655
- if not process.stdout:
656
- return
657
-
658
- for line in iter(process.stdout.readline, ""):
659
- if not line:
660
- break
661
- line = line.rstrip()
662
- stdout_lines.append(line)
663
- self._parse_test_line(line, progress)
664
-
665
- current_time = time.time()
666
- if current_time - last_update_time[0] >= 10:
667
- self._emit_ai_progress(progress)
668
- last_update_time[0] = current_time
669
-
670
- def _read_stderr_lines(
671
- self,
672
- process: subprocess.Popen[str],
673
- stderr_lines: list[str],
674
- ) -> None:
675
- if not process.stderr:
676
- return
677
-
678
- for line in iter(process.stderr.readline, ""):
679
- if not line:
680
- break
681
- stderr_lines.append(line.rstrip())
682
-
683
- def _wait_for_process_completion(
684
- self,
685
- process: subprocess.Popen[str],
686
- timeout: int,
687
- ) -> int:
688
- try:
689
- return process.wait(timeout=timeout)
690
- except subprocess.TimeoutExpired:
691
- process.kill()
692
- raise
693
-
694
- def _emit_ai_progress(self, progress: TestProgress) -> None:
695
- if not self._progress_callback:
696
- return
697
-
698
- progress_data = {
699
- "timestamp": progress.elapsed_time,
700
- "status": "complete" if progress.is_complete else "running",
701
- "progress_percentage": (progress.completed / progress.total_tests * 100)
702
- if progress.total_tests > 0
703
- else 0,
704
- "completed": progress.completed,
705
- "total": progress.total_tests,
706
- "passed": progress.passed,
707
- "failed": progress.failed,
708
- "skipped": progress.skipped,
709
- "errors": progress.errors,
710
- "current_test": progress.current_test,
711
- "elapsed_seconds": progress.elapsed_time,
712
- "eta_seconds": progress.eta_seconds,
713
- }
714
-
715
- if not progress.is_complete and progress.total_tests > 0:
716
- percentage = progress.completed / progress.total_tests * 100
717
- self.console.print(
718
- f"📊 Progress update ({progress.elapsed_time: .0f}s): "
719
- f"{progress.completed}/{progress.total_tests} tests completed ({percentage: .0f}%)",
720
- )
721
-
722
- self._progress_callback(progress_data)
723
-
724
- def _get_optimal_workers(self, options: OptionsProtocol) -> int:
725
- if options.test_workers > 0:
726
- return options.test_workers
727
- import os
728
-
729
- cpu_count = os.cpu_count() or 1
730
- test_files = list[t.Any](self.pkg_path.glob("tests/test_*.py"))
731
- if len(test_files) < 5:
732
- return min(2, cpu_count)
733
-
734
- return min(cpu_count, 8)
735
-
736
- def _get_test_timeout(self, options: OptionsProtocol) -> int:
737
- if options.test_timeout > 0:
738
- return options.test_timeout
739
- test_files = list[t.Any](self.pkg_path.glob("tests/test_*.py"))
740
- base_timeout = 300
741
-
742
- import math
743
-
744
- calculated_timeout = base_timeout + int(math.sqrt(len(test_files)) * 20)
745
- return min(calculated_timeout, 600)
746
-
747
- def run_tests(self, options: OptionsProtocol) -> bool:
748
- self._last_test_failures = []
749
- start_time = time.time()
750
-
751
- try:
752
- return self._execute_test_workflow(options, start_time)
753
- except subprocess.TimeoutExpired:
754
- return self._handle_test_timeout(start_time)
755
- except Exception as e:
756
- return self._handle_test_error(start_time, e)
757
-
758
- def _execute_test_workflow(
759
- self,
760
- options: OptionsProtocol,
761
- start_time: float,
762
- ) -> bool:
763
- cmd = self._build_test_command(options)
764
- timeout = self._get_test_timeout(options)
765
- result = self._execute_tests_with_appropriate_mode(cmd, timeout, options)
766
- duration = time.time() - start_time
767
- return self._process_test_results(result, duration)
768
-
769
- def _execute_tests_with_appropriate_mode(
770
- self,
771
- cmd: list[str],
772
- timeout: int,
773
- options: OptionsProtocol,
774
- ) -> subprocess.CompletedProcess[str]:
775
- execution_mode = self._determine_execution_mode(options)
776
- extended_timeout = timeout + 60
777
-
778
- if execution_mode == "ai_progress":
779
- self._print_test_start_message(cmd, timeout, options)
780
- return self._run_test_command_with_ai_progress(
781
- cmd,
782
- timeout=extended_timeout,
783
- )
784
- if execution_mode == "console_progress":
785
- return self._run_test_command_with_progress(cmd, timeout=extended_timeout)
786
-
787
- self._print_test_start_message(cmd, timeout, options)
788
- return self._run_test_command(cmd, timeout=extended_timeout)
789
-
790
- def _determine_execution_mode(self, options: OptionsProtocol) -> str:
791
- is_ai_mode = getattr(options, "ai_agent", False)
792
- is_benchmark = options.benchmark
793
-
794
- if is_ai_mode and self._progress_callback:
795
- return "ai_progress"
796
- if not is_ai_mode and not is_benchmark:
797
- return "console_progress"
798
- return "standard"
799
-
800
- def _handle_test_timeout(self, start_time: float) -> bool:
801
- duration = time.time() - start_time
802
- self.console.print(f"[red]⏰[/ red] Tests timed out after {duration: .1f}s")
803
- return False
804
-
805
- def _handle_test_error(self, start_time: float, error: Exception) -> bool:
806
- self.console.print(f"[red]💥[/ red] Test execution failed: {error}")
807
- return False
808
-
809
- def _build_test_command(self, options: OptionsProtocol) -> list[str]:
810
- cmd = ["python", "- m", "pytest"]
811
- self._add_coverage_options(cmd, options)
812
- self._add_worker_options(cmd, options)
813
- self._add_benchmark_options(cmd, options)
814
- self._add_timeout_options(cmd, options)
815
-
816
- is_ai_mode = getattr(options, "ai_agent", False)
817
- needs_verbose = (not is_ai_mode and not options.benchmark) or (
818
- is_ai_mode and self._progress_callback
819
- )
820
- self._add_verbosity_options(cmd, options, force_verbose=bool(needs_verbose))
821
- self._add_test_path(cmd)
822
-
823
- return cmd
824
-
825
- def _add_coverage_options(self, cmd: list[str], options: OptionsProtocol) -> None:
826
- if not options.benchmark:
827
- cmd.extend(["--cov=crackerjack", "--cov-report=term-missing"])
828
-
829
- def _add_worker_options(self, cmd: list[str], options: OptionsProtocol) -> None:
830
- if not options.benchmark:
831
- workers = self._get_optimal_workers(options)
832
- if workers > 1:
833
- cmd.extend(["- n", str(workers)])
834
- self.console.print(f"[cyan]🔧[/ cyan] Using {workers} test workers")
835
-
836
- def _add_benchmark_options(self, cmd: list[str], options: OptionsProtocol) -> None:
837
- if options.benchmark:
838
- self.console.print(
839
- "[cyan]📊[/ cyan] Running in benchmark mode (no parallelization)",
840
- )
841
- cmd.append("--benchmark-only")
842
-
843
- def _add_timeout_options(self, cmd: list[str], options: OptionsProtocol) -> None:
844
- timeout = self._get_test_timeout(options)
845
- cmd.extend(["--timeout", str(timeout)])
846
-
847
- def _add_verbosity_options(
848
- self,
849
- cmd: list[str],
850
- options: OptionsProtocol,
851
- force_verbose: bool = False,
852
- ) -> None:
853
- if options.verbose or force_verbose:
854
- cmd.append("- v")
855
-
856
- def _add_test_path(self, cmd: list[str]) -> None:
857
- test_path = self.pkg_path / "tests"
858
- if test_path.exists():
859
- cmd.append(str(test_path))
860
-
861
- def _print_test_start_message(
862
- self,
863
- cmd: list[str],
864
- timeout: int,
865
- options: OptionsProtocol,
866
- ) -> None:
867
- self.console.print(
868
- f"[yellow]🧪[/ yellow] Running tests... (timeout: {timeout}s)",
869
- )
870
- if options.verbose:
871
- self.console.print(f"[dim]Command: {' '.join(cmd)}[/ dim]")
872
-
873
- def _process_test_results(
874
- self,
875
- result: subprocess.CompletedProcess[str],
876
- duration: float,
877
- ) -> bool:
878
- output = result.stdout + result.stderr
879
- success = result.returncode == 0
880
-
881
- if self.coverage_ratchet_enabled and success:
882
- if not self._process_coverage_ratchet():
883
- return False
884
-
885
- if success:
886
- return self._handle_test_success(output, duration)
887
- return self._handle_test_failure(output, duration)
888
-
889
- def _process_coverage_ratchet(self) -> bool:
890
- coverage_data = self.get_coverage()
891
- if not coverage_data:
892
- return True
893
-
894
- current_coverage = coverage_data.get("total_coverage", 0)
895
- ratchet_result = self.coverage_ratchet.update_coverage(current_coverage)
896
-
897
- return self._handle_ratchet_result(ratchet_result)
898
-
899
- def _handle_ratchet_result(self, ratchet_result: dict[str, t.Any]) -> bool:
900
- status = ratchet_result["status"]
901
-
902
- if status == "improved":
903
- self._handle_coverage_improvement(ratchet_result)
904
- elif status == "regression":
905
- self.console.print(f"[red]📉 {ratchet_result['message']}[/ red]")
906
- return False
907
- elif status == "maintained":
908
- self.console.print(f"[cyan]📊 {ratchet_result['message']}[/ cyan]")
909
-
910
- self._display_progress_visualization()
911
- return True
912
-
913
- def _handle_coverage_improvement(self, ratchet_result: dict[str, t.Any]) -> None:
914
- self.console.print(f"[green]🎉 {ratchet_result['message']}[/ green]")
915
-
916
- if "milestones" in ratchet_result and ratchet_result["milestones"]:
917
- self.coverage_ratchet.display_milestone_celebration(
918
- ratchet_result["milestones"]
919
- )
920
-
921
- if "next_milestone" in ratchet_result and ratchet_result["next_milestone"]:
922
- next_milestone = ratchet_result["next_milestone"]
923
- points_needed = ratchet_result.get("points_to_next", 0)
924
- self.console.print(
925
- f"[cyan]🎯 Next milestone: {next_milestone: .0f}% (+{points_needed: .2f}% needed)[/ cyan]"
926
- )
927
-
928
- def _display_progress_visualization(self) -> None:
929
- progress_viz = self.coverage_ratchet.get_progress_visualization()
930
- for line in progress_viz.split("\n"):
931
- if line.strip():
932
- self.console.print(f"[dim]{line}[/ dim]")
933
-
934
- def _handle_test_success(self, output: str, duration: float) -> bool:
935
- self.console.print(f"[green]✅[/ green] Tests passed ({duration: .1f}s)")
936
- lines = output.split("\n")
937
- for line in lines:
938
- if "passed" in line and ("failed" in line or "error" in line):
939
- self.console.print(f"[cyan]📊[/ cyan] {line.strip()}")
940
- break
941
-
942
- return True
943
-
944
- def _handle_test_failure(self, output: str, duration: float) -> bool:
945
- self.console.print(f"[red]❌[/ red] Tests failed ({duration: .1f}s)")
946
- failure_lines = self._extract_failure_lines(output)
947
- if failure_lines:
948
- self.console.print("[red]💥[/ red] Failure summary: ")
949
- for line in failure_lines[:10]:
950
- if line.strip():
951
- self.console.print(f" [dim]{line}[/ dim]")
952
-
953
- self._last_test_failures = failure_lines or ["Test execution failed"]
954
-
955
- return False
956
-
957
- def _extract_failure_lines(self, output: str) -> list[str]:
958
- lines = output.split("\n")
959
- in_failure_section = False
960
- failure_lines: list[str] = []
961
- for line in lines:
962
- if "FAILURES" in line or "ERRORS" in line:
963
- in_failure_section = True
964
- elif in_failure_section and line.startswith(" = "):
965
- break
966
- elif in_failure_section:
967
- failure_lines.append(line)
968
-
969
- return failure_lines
970
-
971
- def get_coverage(self) -> dict[str, t.Any]:
972
- try:
973
- result = self._run_test_command(
974
- ["python", "- m", "coverage", "report", "--format=json"],
975
- )
976
- if result.returncode == 0:
977
- import json
978
-
979
- coverage_data = json.loads(result.stdout)
980
-
981
- return {
982
- "total_coverage": coverage_data.get("totals", {}).get(
983
- "percent_covered",
984
- 0,
985
- ),
986
- "files": coverage_data.get("files", {}),
987
- "summary": coverage_data.get("totals", {}),
988
- }
989
- self.console.print("[yellow]⚠️[/ yellow] Could not get coverage data")
990
- return {}
991
- except Exception as e:
992
- self.console.print(f"[yellow]⚠️[/ yellow] Error getting coverage: {e}")
993
- return {}
994
-
995
- def run_specific_tests(self, test_pattern: str) -> bool:
996
- try:
997
- cmd = ["python", "- m", "pytest", "- k", test_pattern, "- v"]
998
- self.console.print(
999
- f"[yellow]🎯[/ yellow] Running tests matching: {test_pattern}",
1000
- )
1001
- result = self._run_test_command(cmd)
1002
- if result.returncode == 0:
1003
- self.console.print("[green]✅[/ green] Specific tests passed")
1004
- return True
1005
- self.console.print("[red]❌[/ red] Specific tests failed")
1006
- return False
1007
- except Exception as e:
1008
- self.console.print(f"[red]💥[/ red] Error running specific tests: {e}")
1009
- return False
1010
-
1011
- def validate_test_environment(self) -> bool:
1012
- issues: list[str] = []
1013
- try:
1014
- result = self._run_test_command(["python", "- m", "pytest", "--version"])
1015
- if result.returncode != 0:
1016
- issues.append("pytest not available")
1017
- except (subprocess.SubprocessError, OSError, FileNotFoundError):
1018
- issues.append("pytest not accessible")
1019
- test_dir = self.pkg_path / "tests"
1020
- if not test_dir.exists():
1021
- issues.append("tests directory not found")
1022
- test_files = (
1023
- list[t.Any](test_dir.glob("test_ *.py")) if test_dir.exists() else []
1024
- )
1025
- if not test_files:
1026
- issues.append("no test files found")
1027
- if issues:
1028
- self.console.print("[red]❌[/ red] Test environment issues: ")
1029
- for issue in issues:
1030
- self.console.print(f"-{issue}")
1031
- return False
1032
- self.console.print("[green]✅[/ green] Test environment validated")
1033
- return True
1034
-
1035
- def get_test_stats(self) -> dict[str, t.Any]:
1036
- test_dir = self.pkg_path / "tests"
1037
- if not test_dir.exists():
1038
- return {"test_files": 0, "total_tests": 0, "test_lines": 0}
1039
- test_files = list[t.Any](test_dir.glob("test_ *.py"))
1040
- total_lines = 0
1041
- total_tests = 0
1042
- for test_file in test_files:
1043
- try:
1044
- content = test_file.read_text()
1045
- total_lines += len(content.split("\n"))
1046
- total_tests += content.count("def test_")
1047
- except (OSError, UnicodeDecodeError, PermissionError):
1048
- continue
1049
-
1050
- return {
1051
- "test_files": len(test_files),
1052
- "total_tests": total_tests,
1053
- "test_lines": total_lines,
1054
- "avg_tests_per_file": total_tests / len(test_files) if test_files else 0,
1055
- }
1056
-
1057
- def get_test_failures(self) -> list[str]:
1058
- return self._last_test_failures
1059
-
1060
- def get_test_command(self, options: OptionsProtocol) -> list[str]:
1061
- return self._build_test_command(options)
1062
-
1063
- def get_coverage_report(self) -> str | None:
1064
- try:
1065
- coverage_data = self.get_coverage()
1066
- if coverage_data:
1067
- total = coverage_data.get("total", 0)
1068
- return f"Total coverage: {total}%"
1069
- return None
1070
- except Exception:
1071
- return None
1072
-
1073
- def has_tests(self) -> bool:
1074
- test_files = list[t.Any](self.pkg_path.glob("tests/test_*.py"))
1075
- return len(test_files) > 0