py-wlcli 0.8.0__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 (320) hide show
  1. py_wlcli-0.8.0.dist-info/METADATA +139 -0
  2. py_wlcli-0.8.0.dist-info/RECORD +320 -0
  3. py_wlcli-0.8.0.dist-info/WHEEL +4 -0
  4. py_wlcli-0.8.0.dist-info/entry_points.txt +2 -0
  5. py_wlcli-0.8.0.dist-info/licenses/LICENSE +21 -0
  6. wl_cli/__init__.py +27 -0
  7. wl_cli/__main__.py +130 -0
  8. wl_cli/capabilities.py +79 -0
  9. wl_cli/cc/__init__.py +5 -0
  10. wl_cli/cc/checks.py +1019 -0
  11. wl_cli/cc/cli.py +370 -0
  12. wl_cli/cc/mutate.py +1350 -0
  13. wl_cli/cli_click.py +156 -0
  14. wl_cli/commands/__init__.py +84 -0
  15. wl_cli/commands/base.py +80 -0
  16. wl_cli/commands/buildcommands/__init__.py +1 -0
  17. wl_cli/commands/buildcommands/build.py +71 -0
  18. wl_cli/commands/buildcommands/build_utils.py +47 -0
  19. wl_cli/commands/buildcommands/builders/__init__.py +18 -0
  20. wl_cli/commands/buildcommands/builders/cleanup.py +35 -0
  21. wl_cli/commands/buildcommands/builders/coordinator.py +122 -0
  22. wl_cli/commands/buildcommands/builders/environment.py +81 -0
  23. wl_cli/commands/buildcommands/builders/rust_builder.py +39 -0
  24. wl_cli/commands/buildcommands/builders/stubs.py +58 -0
  25. wl_cli/commands/buildcommands/dist.py +326 -0
  26. wl_cli/commands/buildcommands/test.py +89 -0
  27. wl_cli/commands/clean/__init__.py +1 -0
  28. wl_cli/commands/clean/build.py +151 -0
  29. wl_cli/commands/clean/clean.py +294 -0
  30. wl_cli/commands/clean/clean_utils.py +77 -0
  31. wl_cli/commands/clean/core.py +236 -0
  32. wl_cli/commands/clean/dead_code/__init__.py +0 -0
  33. wl_cli/commands/clean/dead_code/dead_code_command.py +70 -0
  34. wl_cli/commands/clean/dead_code/detector.py +161 -0
  35. wl_cli/commands/clean/dead_code/fixer.py +176 -0
  36. wl_cli/commands/clean/dead_code/reporter.py +295 -0
  37. wl_cli/commands/clean/dead_code/utils.py +0 -0
  38. wl_cli/commands/clean/lfs.py +215 -0
  39. wl_cli/commands/clean/python.py +81 -0
  40. wl_cli/commands/clean/rust.py +91 -0
  41. wl_cli/commands/clean/utils/__init__.py +34 -0
  42. wl_cli/commands/clean/venv.py +91 -0
  43. wl_cli/commands/cleanup.py +73 -0
  44. wl_cli/commands/click_base.py +79 -0
  45. wl_cli/commands/config/__init__.py +1 -0
  46. wl_cli/commands/config/config.py +291 -0
  47. wl_cli/commands/debug/__init__.py +6 -0
  48. wl_cli/commands/debug/debug.py +309 -0
  49. wl_cli/commands/debug/debug_cache.py +86 -0
  50. wl_cli/commands/debug/debug_subagent.py +156 -0
  51. wl_cli/commands/debug/output_formatter.py +165 -0
  52. wl_cli/commands/debug/source_hasher.py +92 -0
  53. wl_cli/commands/format/__init__.py +13 -0
  54. wl_cli/commands/format/format_cache_adapter.py +125 -0
  55. wl_cli/commands/format/format_command.py +197 -0
  56. wl_cli/commands/format/format_coordinator.py +191 -0
  57. wl_cli/commands/format/format_executor.py +470 -0
  58. wl_cli/commands/format/format_outcome.py +44 -0
  59. wl_cli/commands/format/format_path_handler.py +1016 -0
  60. wl_cli/commands/format/format_progress.py +62 -0
  61. wl_cli/commands/format/format_reporter.py +31 -0
  62. wl_cli/commands/format/python_formatter.py +506 -0
  63. wl_cli/commands/format/rust_formatter.py +106 -0
  64. wl_cli/commands/gate/__init__.py +1 -0
  65. wl_cli/commands/gate/gate_command.py +862 -0
  66. wl_cli/commands/gate/preflight.py +445 -0
  67. wl_cli/commands/git/__init__.py +1 -0
  68. wl_cli/commands/git/config.py +169 -0
  69. wl_cli/commands/git/git_facts.py +60 -0
  70. wl_cli/commands/git/git_flow.py +86 -0
  71. wl_cli/commands/git/hook.py +112 -0
  72. wl_cli/commands/git/policy.py +216 -0
  73. wl_cli/commands/git/push_update.py +82 -0
  74. wl_cli/commands/initenv/__init__.py +1 -0
  75. wl_cli/commands/initenv/common/__init__.py +23 -0
  76. wl_cli/commands/initenv/common/check_git_bash.py +78 -0
  77. wl_cli/commands/initenv/common/error_handler.py +68 -0
  78. wl_cli/commands/initenv/common/exceptions.py +52 -0
  79. wl_cli/commands/initenv/components/__init__.py +4 -0
  80. wl_cli/commands/initenv/components/cargo_sync.py +240 -0
  81. wl_cli/commands/initenv/components/config_manager.py +106 -0
  82. wl_cli/commands/initenv/components/directory_utils.py +71 -0
  83. wl_cli/commands/initenv/components/git/__init__.py +9 -0
  84. wl_cli/commands/initenv/components/git/branch_creator.py +143 -0
  85. wl_cli/commands/initenv/components/git/git_flow_interface.py +24 -0
  86. wl_cli/commands/initenv/components/git/git_flow_manager.py +159 -0
  87. wl_cli/commands/initenv/components/git/git_lfs_manager.py +272 -0
  88. wl_cli/commands/initenv/components/git/git_utility.py +254 -0
  89. wl_cli/commands/initenv/components/git/gitignore_manager.py +53 -0
  90. wl_cli/commands/initenv/components/git_initializer.py +212 -0
  91. wl_cli/commands/initenv/components/i18n_manager.py +104 -0
  92. wl_cli/commands/initenv/components/log_manager.py +241 -0
  93. wl_cli/commands/initenv/components/project_structure/__init__.py +23 -0
  94. wl_cli/commands/initenv/components/project_structure/config_generator.py +360 -0
  95. wl_cli/commands/initenv/components/project_structure/directory_creator.py +184 -0
  96. wl_cli/commands/initenv/components/project_structure/hook_source.py +110 -0
  97. wl_cli/commands/initenv/components/project_structure/hooks_manager.py +995 -0
  98. wl_cli/commands/initenv/components/project_structure/readme_handler.py +56 -0
  99. wl_cli/commands/initenv/components/project_structure/rust_files_handler.py +30 -0
  100. wl_cli/commands/initenv/components/project_structure/setup_handler.py +103 -0
  101. wl_cli/commands/initenv/components/project_structure/sop_generator.py +73 -0
  102. wl_cli/commands/initenv/components/project_structure/template_copier.py +104 -0
  103. wl_cli/commands/initenv/components/project_structure/template_manager.py +195 -0
  104. wl_cli/commands/initenv/components/pyproject_generator.py +194 -0
  105. wl_cli/commands/initenv/components/rust_initializer.py +145 -0
  106. wl_cli/commands/initenv/components/symlink_cleaner.py +99 -0
  107. wl_cli/commands/initenv/components/symlink_manager.py +52 -0
  108. wl_cli/commands/initenv/components/venv_manager.py +118 -0
  109. wl_cli/commands/initenv/initenv.py +169 -0
  110. wl_cli/commands/initenv/services/__init__.py +11 -0
  111. wl_cli/commands/initenv/services/environment_initializer.py +288 -0
  112. wl_cli/commands/initenv/services/git_lfs_initializer.py +26 -0
  113. wl_cli/commands/initenv/services/project_structure_initializer.py +52 -0
  114. wl_cli/commands/lfs/README.md +221 -0
  115. wl_cli/commands/lfs/__init__.py +5 -0
  116. wl_cli/commands/lfs/cli.py +106 -0
  117. wl_cli/commands/lfs/config.py +164 -0
  118. wl_cli/commands/lfs/hook.py +61 -0
  119. wl_cli/commands/lfs/initializer.py +531 -0
  120. wl_cli/commands/lfs/manager.py +95 -0
  121. wl_cli/commands/lfs/policy.py +165 -0
  122. wl_cli/commands/lfs/watcher.py +294 -0
  123. wl_cli/commands/lint/__init__.py +1 -0
  124. wl_cli/commands/lint/lint_command.py +297 -0
  125. wl_cli/commands/lint/lint_executor.py +409 -0
  126. wl_cli/commands/lint/lint_reporter.py +579 -0
  127. wl_cli/commands/lint/lint_run.py +258 -0
  128. wl_cli/commands/lint/mypy_config.py +90 -0
  129. wl_cli/commands/registry.py +103 -0
  130. wl_cli/commands/self/__init__.py +7 -0
  131. wl_cli/commands/self/common/__init__.py +8 -0
  132. wl_cli/commands/self/common/capabilities.py +30 -0
  133. wl_cli/commands/self/common/config.py +27 -0
  134. wl_cli/commands/self/common/version_cache.py +85 -0
  135. wl_cli/commands/self/handlers/__init__.py +19 -0
  136. wl_cli/commands/self/handlers/base_handler.py +28 -0
  137. wl_cli/commands/self/handlers/install_handler.py +85 -0
  138. wl_cli/commands/self/handlers/status_handler.py +66 -0
  139. wl_cli/commands/self/handlers/uninstall_handler.py +58 -0
  140. wl_cli/commands/self/handlers/update_handler.py +102 -0
  141. wl_cli/commands/self/handlers/version_handler.py +16 -0
  142. wl_cli/commands/self/self_command.py +237 -0
  143. wl_cli/commands/self/services/__init__.py +15 -0
  144. wl_cli/commands/self/services/installation_manager.py +187 -0
  145. wl_cli/commands/self/services/local_installer.py +165 -0
  146. wl_cli/commands/self/services/pypi_updater.py +278 -0
  147. wl_cli/commands/self/services/windows_update.py +174 -0
  148. wl_cli/commands/testcommands/__init__.py +8 -0
  149. wl_cli/commands/testcommands/base_dependency_analyzer.py +198 -0
  150. wl_cli/commands/testcommands/batch_processor.py +278 -0
  151. wl_cli/commands/testcommands/cache_manager.py +1156 -0
  152. wl_cli/commands/testcommands/coverage_analyzer.py +955 -0
  153. wl_cli/commands/testcommands/coverage_cache_manager.py +183 -0
  154. wl_cli/commands/testcommands/progress_manager.py +79 -0
  155. wl_cli/commands/testcommands/python_env_detector.py +207 -0
  156. wl_cli/commands/testcommands/report_generator.py +101 -0
  157. wl_cli/commands/testcommands/single_test_executor.py +250 -0
  158. wl_cli/commands/testcommands/speed_checker.py +118 -0
  159. wl_cli/commands/testcommands/test_command_builder.py +165 -0
  160. wl_cli/commands/testcommands/test_commands.py +786 -0
  161. wl_cli/commands/testcommands/test_reporter.py +181 -0
  162. wl_cli/commands/testcommands/test_result_handler.py +96 -0
  163. wl_cli/commands/testcommands/test_runner.py +1343 -0
  164. wl_cli/commands/testcommands/test_utils.py +197 -0
  165. wl_cli/commands/testcommands/utils/__init__.py +17 -0
  166. wl_cli/commands/testcommands/utils/coverage_parser.py +98 -0
  167. wl_cli/commands/testcommands/utils/env_info.py +36 -0
  168. wl_cli/commands/testcommands/utils/junit_parser.py +75 -0
  169. wl_cli/commands/testcommands/utils/markdown_utils.py +20 -0
  170. wl_cli/commands/testcommands/utils/project_root.py +61 -0
  171. wl_cli/commands/update/__init__.py +9 -0
  172. wl_cli/commands/update/services/__init__.py +21 -0
  173. wl_cli/commands/update/services/backup_manager.py +207 -0
  174. wl_cli/commands/update/services/component_manager.py +83 -0
  175. wl_cli/commands/update/services/conflict_resolver.py +189 -0
  176. wl_cli/commands/update/services/vendors_updater.py +85 -0
  177. wl_cli/commands/update/services/version_tracker.py +127 -0
  178. wl_cli/commands/update/update_command.py +270 -0
  179. wl_cli/commands/update/utils/__init__.py +3 -0
  180. wl_cli/commands/version/__init__.py +9 -0
  181. wl_cli/commands/version/version_command.py +155 -0
  182. wl_cli/commands/workflow.py +477 -0
  183. wl_cli/env/__init__.py +5 -0
  184. wl_cli/env/cli.py +167 -0
  185. wl_cli/env_status.py +157 -0
  186. wl_cli/envelope.py +91 -0
  187. wl_cli/exceptions.py +85 -0
  188. wl_cli/lib/__init__.py +3 -0
  189. wl_cli/lib/rust_utils.py +35 -0
  190. wl_cli/mcp/__init__.py +6 -0
  191. wl_cli/mcp/capabilities.py +19 -0
  192. wl_cli/mcp/cli.py +22 -0
  193. wl_cli/mcp/gitlab.py +553 -0
  194. wl_cli/mcp/k8s.py +344 -0
  195. wl_cli/mcp/protocol.py +159 -0
  196. wl_cli/platform.py +139 -0
  197. wl_cli/quality/__init__.py +18 -0
  198. wl_cli/utils/__init__.py +7 -0
  199. wl_cli/utils/cache.py +491 -0
  200. wl_cli/utils/cache_ledger.py +355 -0
  201. wl_cli/utils/cache_store.py +546 -0
  202. wl_cli/utils/config/__init__.py +27 -0
  203. wl_cli/utils/config/config_loader.py +163 -0
  204. wl_cli/utils/config/core.py +862 -0
  205. wl_cli/utils/config/defaults.py +142 -0
  206. wl_cli/utils/config/env_exporter.py +86 -0
  207. wl_cli/utils/config/env_parser.py +108 -0
  208. wl_cli/utils/config/validation.py +280 -0
  209. wl_cli/utils/error_handler.py +165 -0
  210. wl_cli/utils/file_copier.py +56 -0
  211. wl_cli/utils/file_operations.py +506 -0
  212. wl_cli/utils/language_utils.py +122 -0
  213. wl_cli/utils/lazy_import.py +240 -0
  214. wl_cli/utils/log_levels.py +35 -0
  215. wl_cli/utils/log_rotators.py +74 -0
  216. wl_cli/utils/logging.py +458 -0
  217. wl_cli/utils/output_context.py +142 -0
  218. wl_cli/utils/performance/__init__.py +1 -0
  219. wl_cli/utils/performance/decorators.py +111 -0
  220. wl_cli/utils/performance/exporter.py +38 -0
  221. wl_cli/utils/performance/models.py +16 -0
  222. wl_cli/utils/performance/monitor.py +161 -0
  223. wl_cli/utils/performance_monitor.py +32 -0
  224. wl_cli/utils/platform_adapter.py +131 -0
  225. wl_cli/utils/pre_commit.py +266 -0
  226. wl_cli/utils/pre_commit_config.py +72 -0
  227. wl_cli/utils/root.py +521 -0
  228. wl_cli/utils/root_fallback.py +256 -0
  229. wl_cli/utils/run_evidence.py +236 -0
  230. wl_cli/utils/structured_logger.py +18 -0
  231. wl_cli/utils/structured_logging/__init__.py +19 -0
  232. wl_cli/utils/structured_logging/config.py +337 -0
  233. wl_cli/utils/structured_logging/core.py +177 -0
  234. wl_cli/utils/structured_logging/env.py +56 -0
  235. wl_cli/utils/structured_logging/filtering.py +75 -0
  236. wl_cli/utils/structured_logging/filters.py +55 -0
  237. wl_cli/utils/structured_logging/formatters.py +251 -0
  238. wl_cli/utils/structured_logging/handlers.py +308 -0
  239. wl_cli/utils/structured_logging/logger.py +354 -0
  240. wl_cli/utils/subprocess.py +71 -0
  241. wl_cli/utils/subprocess_utils.py +260 -0
  242. wl_cli/utils/test_utils/__init__.py +5 -0
  243. wl_cli/utils/test_utils/test_filter.py +36 -0
  244. wl_cli/utils/uv_run.py +79 -0
  245. wl_cli/utils/uv_tool.py +117 -0
  246. wl_cli/utils/uv_tree.py +71 -0
  247. wl_cli/utils/uv_workspace.py +174 -0
  248. wl_cli/utils/version/__init__.py +15 -0
  249. wl_cli/utils/version/pypi_version_checker.py +95 -0
  250. wl_cli/utils/version/version_comparator.py +133 -0
  251. wl_cli/utils/version/version_detectors.py +312 -0
  252. wl_cli/utils/version/version_service.py +131 -0
  253. wl_cli/utils/version/version_updater.py +198 -0
  254. wl_cli/utils/wl_dir_updater.py +1440 -0
  255. wl_cli/utils/workspace_detector/__init__.py +28 -0
  256. wl_cli/utils/workspace_detector/core.py +161 -0
  257. wl_cli/utils/workspace_detector/detection_strategies.py +107 -0
  258. wl_cli/utils/workspace_detector/exceptions.py +5 -0
  259. wl_cli/utils/workspace_detector/types.py +17 -0
  260. wl_cli/utils/workspace_detector/venv_resolver.py +84 -0
  261. wl_cli/vendors/config/.codespellrc +2 -0
  262. wl_cli/vendors/config/dependencies.toml +10 -0
  263. wl_cli/vendors/config/mypy.ini +62 -0
  264. wl_cli/vendors/config/pyproject_defaults.toml +28 -0
  265. wl_cli/vendors/config/template_config.toml +24 -0
  266. wl_cli/vendors/config/tool_configs.toml +76 -0
  267. wl_cli/vendors/git/.gitignore +122 -0
  268. wl_cli/vendors/git/.pre-commit-config.yaml +37 -0
  269. wl_cli/vendors/hooks/applypatch-msg.sample +15 -0
  270. wl_cli/vendors/hooks/commit-msg.sample +24 -0
  271. wl_cli/vendors/hooks/fsmonitor-watchman.sample +174 -0
  272. wl_cli/vendors/hooks/post-update.sample +8 -0
  273. wl_cli/vendors/hooks/pre-applypatch.sample +14 -0
  274. wl_cli/vendors/hooks/pre-commit +45 -0
  275. wl_cli/vendors/hooks/pre-commit.legacy +31 -0
  276. wl_cli/vendors/hooks/pre-commit.sample +49 -0
  277. wl_cli/vendors/hooks/pre-merge-commit.sample +13 -0
  278. wl_cli/vendors/hooks/pre-push.sample +60 -0
  279. wl_cli/vendors/hooks/pre-rebase.sample +169 -0
  280. wl_cli/vendors/hooks/pre-receive.sample +24 -0
  281. wl_cli/vendors/hooks/prepare-commit-msg.sample +42 -0
  282. wl_cli/vendors/hooks/push-to-checkout.sample +78 -0
  283. wl_cli/vendors/hooks/sendemail-validate.sample +77 -0
  284. wl_cli/vendors/hooks/unix/applypatch-msg.sample +15 -0
  285. wl_cli/vendors/hooks/unix/check_lockfile.py +58 -0
  286. wl_cli/vendors/hooks/unix/check_lockfile.sh +36 -0
  287. wl_cli/vendors/hooks/unix/commit-msg.sample +24 -0
  288. wl_cli/vendors/hooks/unix/fsmonitor-watchman.sample +174 -0
  289. wl_cli/vendors/hooks/unix/post-update.sample +8 -0
  290. wl_cli/vendors/hooks/unix/pre-applypatch.sample +14 -0
  291. wl_cli/vendors/hooks/unix/pre-commit +190 -0
  292. wl_cli/vendors/hooks/unix/pre-commit.legacy +31 -0
  293. wl_cli/vendors/hooks/unix/pre-commit.py +81 -0
  294. wl_cli/vendors/hooks/unix/pre-commit.sample +49 -0
  295. wl_cli/vendors/hooks/unix/pre-merge-commit.sample +13 -0
  296. wl_cli/vendors/hooks/unix/pre-push +417 -0
  297. wl_cli/vendors/hooks/unix/pre-push.sample +53 -0
  298. wl_cli/vendors/hooks/unix/pre-rebase.sample +169 -0
  299. wl_cli/vendors/hooks/unix/pre-receive.sample +24 -0
  300. wl_cli/vendors/hooks/unix/prepare-commit-msg.sample +42 -0
  301. wl_cli/vendors/hooks/update.sample +128 -0
  302. wl_cli/vendors/hooks/win/work.bat +63 -0
  303. wl_cli/vendors/project/.gitignore +90 -0
  304. wl_cli/vendors/project/.python-version +1 -0
  305. wl_cli/vendors/project/LICENSE +21 -0
  306. wl_cli/vendors/project/README.md +0 -0
  307. wl_cli/vendors/project/pyproject.toml +27 -0
  308. wl_cli/vendors/project/pyproject_no_workspace.toml +16 -0
  309. wl_cli/vendors/project/pyproject_workspace.toml +14 -0
  310. wl_cli/vendors/readme/README.md +120 -0
  311. wl_cli/vendors/rust/Cargo.toml +14 -0
  312. wl_cli/vendors/rust/lib/__init__.py +3 -0
  313. wl_cli/vendors/rust/lib/rust_utils.py +31 -0
  314. wl_cli/vendors/rust/src/lib.rs +13 -0
  315. wl_cli/vendors/rust/src/main.rs +3 -0
  316. wl_cli/vendors/rust/tests/test_rust_fallback.py +40 -0
  317. wl_cli/vendors/sop_doc/git-dev-sop.md +220 -0
  318. wl_cli/vendors/sop_doc/industrial-standard-sop.md +145 -0
  319. wl_cli/vendors/sop_doc/python-sop.md +342 -0
  320. wl_cli/vendors/sop_doc/rust-readme.md +219 -0
@@ -0,0 +1,250 @@
1
+ """Single test file executor with caching integration."""
2
+
3
+ import shutil
4
+ import subprocess
5
+ import tempfile
6
+ from pathlib import Path
7
+
8
+ from .cache_manager import CacheManager
9
+ from .speed_checker import check_test_speeds, format_timeout_message, load_speed_config
10
+ from .test_utils import _probe_pytest_cov
11
+
12
+
13
+ class SingleTestExecutor:
14
+ """Executes single test file with caching support."""
15
+
16
+ def __init__(self, cache_manager: CacheManager):
17
+ """Initialize with cache manager dependency."""
18
+ self.cache_manager = cache_manager
19
+ # 每个解释器的 pytest-cov 可达性只探一次:判定结果按解释器缓存,避免
20
+ # `wl test -s` 逐文件重复起探针进程。
21
+ self._pytest_cov_cache: dict[str, bool] = {}
22
+
23
+ def execute(
24
+ self,
25
+ python_executable: str,
26
+ test_file: Path,
27
+ project_root: str,
28
+ env: dict[str, str],
29
+ debug_mode: bool,
30
+ no_cache: bool,
31
+ step_by_step: bool = False,
32
+ concise: bool = False,
33
+ no_skip: bool = False,
34
+ extra_args: list[str] | None = None,
35
+ ) -> tuple[bool, str]:
36
+ """Execute a single test file with caching check.
37
+
38
+ Args:
39
+ extra_args: Already-filtered passthrough pytest args to append to
40
+ this one file's command (see
41
+ ``test_runner._attribution_args``). The attribution pass uses
42
+ it so a file is judged under the same selection the batch run
43
+ used; ``None`` keeps every existing caller's command unchanged.
44
+ """
45
+ # Check if we can use cached result
46
+ if not no_cache:
47
+ cache_hit, cache_reason, _, _ = self._check_cache_for_single_file(
48
+ test_file, step_by_step, concise, python_executable
49
+ )
50
+ if cache_hit:
51
+ return True, ""
52
+ else:
53
+ # Skip cache
54
+ cache_reason = "Cache skipped - --no-cache option used"
55
+ if not concise and not step_by_step:
56
+ print(f"[Cache] {cache_reason} for {test_file.name}")
57
+
58
+ # Build pytest command
59
+ cmd_args = self._build_test_command(
60
+ python_executable, test_file, debug_mode, step_by_step, env, extra_args
61
+ )
62
+
63
+ # Execute pytest
64
+ result = self._execute_test(cmd_args, project_root, env)
65
+
66
+ # Process result
67
+ test_success, error_message = self._process_test_result(result)
68
+
69
+ # 归因转发了用户选择(rv183 P1):pytest 退出码 5 表示「本文件没有跑任何
70
+ # 用例」,在**带选择**的重跑里这只是「用户的 `-k`/`-m` 没命中」——它不是
71
+ # 该文件通过的证据。若照 rc==5 写成功缓存,之后的运行会拿着这份假绿命中
72
+ # 缓存、exit 0 报「using cache」,而测试依旧是坏的。故此时按「未归因」处理:
73
+ # 不写 pass,落到下面的失败分支把该文件的缓存剔掉,下一轮必须重跑。
74
+ if extra_args and result.returncode == 5:
75
+ test_success = False
76
+ error_message = (
77
+ "no tests matched the forwarded selection (pytest exit 5); "
78
+ "file left unattributed"
79
+ )
80
+
81
+ # Check for slow test cases in step-by-step mode
82
+ if step_by_step and test_success:
83
+ config = load_speed_config()
84
+ if config.enabled and result.stdout:
85
+ timeout_info = check_test_speeds(
86
+ result.stdout, config.threshold, no_skip
87
+ )
88
+ if timeout_info and config.fail_on_timeout:
89
+ error_message = format_timeout_message(timeout_info)
90
+ test_success = False
91
+
92
+ # Save result to cache only if not in no-cache mode
93
+ # If test failed, delete the cache instead of saving failed result
94
+ if not no_cache:
95
+ if test_success:
96
+ self.cache_manager.record_passed_single(
97
+ test_file, step_by_step, concise, python_executable
98
+ )
99
+ else:
100
+ # Delete cache for failed test
101
+ self.cache_manager.invalidate_file(
102
+ test_file, step_by_step, concise, python_executable
103
+ )
104
+
105
+ return test_success, error_message
106
+
107
+ def _check_cache_for_single_file(
108
+ self,
109
+ test_file: Path,
110
+ step_by_step: bool = False,
111
+ concise: bool = False,
112
+ python_executable: str | None = None,
113
+ ) -> tuple[bool, str, bool, str | None]:
114
+ """Check cache for a single test file.
115
+
116
+ ``python_executable`` is the interpreter this file is about to run
117
+ under: the cache key fingerprints it, so a hit can only come from a
118
+ run under the same interpreter (#191).
119
+ """
120
+ return self.cache_manager.cached_result(
121
+ test_file, step_by_step, concise, python_executable
122
+ )
123
+
124
+ def _build_test_command(
125
+ self,
126
+ python_executable: str,
127
+ test_file: Path,
128
+ debug_mode: bool,
129
+ step_by_step: bool = False,
130
+ env: dict[str, str] | None = None,
131
+ extra_args: list[str] | None = None,
132
+ ) -> list[str]:
133
+ """Build pytest command for a single test file.
134
+
135
+ ``extra_args`` goes **before** the file path so a value-taking option
136
+ (``-k expr``) can never swallow the path as its value.
137
+ """
138
+ cmd_args = [python_executable, "-m", "pytest"]
139
+
140
+ if debug_mode:
141
+ cmd_args.extend(["-v", "--tb=short"])
142
+ else:
143
+ cmd_args.extend(["-q"])
144
+
145
+ # Disable coverage for single file tests —— 但**只在 pytest-cov 存在时**传。
146
+ # `--no-cov` 是 pytest-cov 提供的 flag;未装该插件时 pytest 会以
147
+ # "unrecognized arguments: --no-cov" 直接失败,而不是忽略它。
148
+ # 这正是 `wl test -s` 在缺 pytest-cov 的环境里跑不起来的原因。
149
+ if self._check_pytest_cov(python_executable, env):
150
+ cmd_args.extend(["--no-cov"])
151
+
152
+ # In step-by-step mode, enable duration reporting for all test cases
153
+ if step_by_step:
154
+ cmd_args.extend(["--durations", "0", "--durations-min", "0"])
155
+
156
+ if extra_args:
157
+ cmd_args.extend(extra_args)
158
+
159
+ cmd_args.append(str(test_file))
160
+ return cmd_args
161
+
162
+ def _check_pytest_cov(
163
+ self, python_executable: str, env: dict[str, str] | None = None
164
+ ) -> bool:
165
+ """pytest-cov 是否可被**子进程解释器**导入。
166
+
167
+ 判定必须落在真正执行 pytest 的解释器上,而不是 wl 进程本身:两者可以
168
+ 是两个解释器(例如 wl 由 uv tool 环境提供、测试却跑在项目 .venv 里)。
169
+ 只看 wl 进程会让 `--no-cov` 误判 —— 漏判时子进程会转而吃 pyproject 的
170
+ `--cov=wl_cli`,于是 `wl test -s` 的并发子进程在同一份未隔离的 coverage
171
+ 数据文件上初始化/写入,抛 DataError 并在项目根留下
172
+ `.coverage.<host>.<pid>.<rand>` 残片。
173
+
174
+ 探针本体是 `test_utils._probe_pytest_cov`(#112 起三处入口共用同一实现,
175
+ 不再各持一份);此处只负责**按解释器缓存**。`env` 是执行该解释器时用的
176
+ 环境,探针必须拿到同一份,否则探针与子进程看的环境不同,「同一判定」无从
177
+ 谈起(子进程还多注入一个隔离用的 `COVERAGE_FILE`,那只决定 coverage 往哪
178
+ 写,不影响 import 判定)。
179
+
180
+ 缓存以解释器为键:一次运行里执行用 env 是固定的,故无需并入键。
181
+ """
182
+ cached = self._pytest_cov_cache.get(python_executable)
183
+ if cached is not None:
184
+ return cached
185
+ available = _probe_pytest_cov(python_executable, env)
186
+ self._pytest_cov_cache[python_executable] = available
187
+ return available
188
+
189
+ def _execute_test(
190
+ self,
191
+ cmd_args: list[str],
192
+ project_root: str,
193
+ env: dict[str, str],
194
+ ) -> subprocess.CompletedProcess:
195
+ """Execute pytest and return the result.
196
+
197
+ 每个子进程都拿到**私有**的 coverage 数据命名空间:`COVERAGE_FILE` 指向
198
+ 一次性的临时目录。pytest-cov 的 `Central` 控制器总是以 `data_suffix=True`
199
+ 写 `.coverage.<host>.<pid>.<rand>`,收尾时再按 basename 通配 combine ——
200
+ `wl test -s` 会并发跑多个 pytest,共用同一 basename 时它们会彼此读到尚未
201
+ 建表的半成品 SQLite 文件(`DataError: no such table: file`),并把残片留在
202
+ 项目根。隔离后各进程互不可见,项目根也不再产生残片。
203
+
204
+ 注意 `env` 由 16 路并发线程共享,绝不能就地修改,只能拷贝。
205
+ """
206
+ coverage_dir = tempfile.mkdtemp(prefix="wl-test-cov-")
207
+ child_env = dict(env)
208
+ child_env["COVERAGE_FILE"] = str(Path(coverage_dir) / ".coverage")
209
+ try:
210
+ try:
211
+ result = subprocess.run(
212
+ cmd_args,
213
+ cwd=project_root,
214
+ check=False,
215
+ env=child_env,
216
+ capture_output=True,
217
+ text=True,
218
+ errors="replace",
219
+ timeout=300, # 5 minute timeout per test file
220
+ )
221
+ except subprocess.TimeoutExpired as e:
222
+ result = subprocess.CompletedProcess(
223
+ args=cmd_args,
224
+ returncode=-1,
225
+ stdout=e.stdout if isinstance(e.stdout, str) else "",
226
+ stderr=(e.stderr if isinstance(e.stderr, str) else "")
227
+ + "\n[Timeout] Test execution exceeded 300 seconds",
228
+ )
229
+ finally:
230
+ shutil.rmtree(coverage_dir, ignore_errors=True)
231
+ return result
232
+
233
+ def _process_test_result(
234
+ self,
235
+ result: subprocess.CompletedProcess,
236
+ ) -> tuple[bool, str]:
237
+ """Process test result and return success status and error message."""
238
+ test_success = False
239
+ error_message = ""
240
+
241
+ if result.returncode == 0:
242
+ test_success = True
243
+ elif result.returncode == 5:
244
+ # No tests in this file, continue
245
+ test_success = True
246
+ else:
247
+ error_message = f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
248
+ test_success = False
249
+
250
+ return test_success, error_message
@@ -0,0 +1,118 @@
1
+ """Test speed checking for step-by-step mode."""
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+
6
+ from wl_cli.utils.config import get_config
7
+
8
+
9
+ @dataclass
10
+ class SpeedConfig:
11
+ """Speed check configuration."""
12
+
13
+ enabled: bool
14
+ threshold: float
15
+ fail_on_timeout: bool
16
+
17
+
18
+ def load_speed_config() -> SpeedConfig:
19
+ """Load speed check configuration from global config."""
20
+ enabled = get_config("test_speed.enabled", True)
21
+ threshold = get_config("test_speed.threshold", 5.0)
22
+ fail_on_timeout = get_config("test_speed.fail_on_timeout", True)
23
+ return SpeedConfig(
24
+ enabled=enabled,
25
+ threshold=threshold,
26
+ fail_on_timeout=fail_on_timeout,
27
+ )
28
+
29
+
30
+ @dataclass
31
+ class TimeoutInfo:
32
+ """Information about a timed-out test case."""
33
+
34
+ test_name: str
35
+ duration: float
36
+ threshold: float
37
+
38
+
39
+ # Regex pattern to match pytest --durations output
40
+ # Example: "0.12s call tests/file.py::test_func"
41
+ # Example: "10.00s setup tests/file.py::test_func"
42
+ _DURATION_PATTERN = re.compile(r"^\s*([\d\.]+)s\s+\w+\s+(.+)$", re.MULTILINE)
43
+
44
+
45
+ def check_test_speeds(
46
+ stdout: str,
47
+ threshold: float,
48
+ no_skip: bool = False,
49
+ ) -> TimeoutInfo | None:
50
+ """
51
+ Check pytest output for slow test cases.
52
+
53
+ Args:
54
+ stdout: pytest stdout output
55
+ threshold: timeout threshold in seconds
56
+
57
+ Returns:
58
+ TimeoutInfo if first slow test found, None otherwise
59
+ """
60
+ # Patterns to skip - these tests are intentionally slow
61
+ skip_patterns = [
62
+ "performance",
63
+ "benchmark",
64
+ "profile",
65
+ "expiration",
66
+ "expire",
67
+ "timeout",
68
+ "sleep",
69
+ "concurrency",
70
+ "format",
71
+ "lock",
72
+ "uv",
73
+ "docstring",
74
+ "style",
75
+ "build",
76
+ "workspace",
77
+ "integration",
78
+ "comprehensive",
79
+ "initializer",
80
+ "initialize",
81
+ "fetch",
82
+ "git",
83
+ "self",
84
+ "slow",
85
+ "collection",
86
+ "cache",
87
+ "publish",
88
+ ]
89
+
90
+ for match in _DURATION_PATTERN.finditer(stdout):
91
+ duration_str = match.group(1)
92
+ test_name = match.group(2).strip()
93
+ try:
94
+ duration = float(duration_str)
95
+ if duration > threshold:
96
+ # Skip intentionally slow tests by name pattern
97
+ lower_name = test_name.lower()
98
+ if not no_skip and any(
99
+ pattern in lower_name for pattern in skip_patterns
100
+ ):
101
+ continue
102
+ return TimeoutInfo(
103
+ test_name=test_name,
104
+ duration=duration,
105
+ threshold=threshold,
106
+ )
107
+ except ValueError:
108
+ continue
109
+ return None
110
+
111
+
112
+ def format_timeout_message(info: TimeoutInfo) -> str:
113
+ """Format timeout error message for display."""
114
+ return (
115
+ f"\n⏱️ 测试用例耗时过长 ({info.duration:.2f}s > {info.threshold:.2f}s)\n"
116
+ f" 测试用例: {info.test_name}\n"
117
+ f" 测试用例速度过慢,请尝试优化测试逻辑。\n"
118
+ )
@@ -0,0 +1,165 @@
1
+ """
2
+ Test command builder utilities for test command.
3
+ """
4
+
5
+ from pathlib import Path
6
+
7
+ from .coverage_analyzer import coverage_targets
8
+ from .test_utils import _probe_pytest_cov
9
+
10
+
11
+ class TestCommandBuilder:
12
+ """Build pytest command arguments based on configuration."""
13
+
14
+ def __init__(self, python_detector):
15
+ """Initialize test command builder.
16
+
17
+ Args:
18
+ python_detector: Instance of PythonEnvDetector to get Python executable
19
+ """
20
+ self.python_detector = python_detector
21
+
22
+ def build_command_args(
23
+ self,
24
+ report_mode: bool,
25
+ args: list[str],
26
+ debug_mode: bool = False,
27
+ verbose: bool = False,
28
+ python_executable: str | None = None,
29
+ env: dict[str, str] | None = None,
30
+ *,
31
+ project_root: Path,
32
+ ) -> list[str]:
33
+ """Build the command arguments for pytest.
34
+
35
+ Args:
36
+ report_mode: Whether to generate detailed reports
37
+ args: Additional pytest arguments
38
+ debug_mode: Whether to run in debug mode
39
+ verbose: Whether to show verbose output
40
+ python_executable: Interpreter that will actually execute pytest.
41
+ Defaults to the detector's current answer. Callers that already
42
+ resolved a specific interpreter must pass it here, so the
43
+ pytest-cov probe below judges the very interpreter that runs.
44
+ env: Environment the interpreter will be exec'd with. The probe is
45
+ given the same env, so judge and executor see one environment.
46
+ project_root: Project root whose package ``--cov`` should measure.
47
+ Required: the caller resolved this root once already (the
48
+ same value it hands pytest as its cwd), so ``--cov`` names the
49
+ package of the project that actually runs (rv192 MINOR-3).
50
+ Resolving it again here could name a different project than
51
+ the run itself (rv192b NIT).
52
+
53
+ Returns:
54
+ List of command arguments
55
+ """
56
+ if python_executable is None:
57
+ python_executable = self.python_detector.get_python_executable()
58
+ cmd_args: list[str] = [python_executable, "-m", "pytest"]
59
+
60
+ # Configure warning behavior based on mode
61
+ if report_mode or debug_mode or verbose:
62
+ # In report/debug/verbose mode, show all warning details
63
+ cmd_args.append("-W")
64
+ cmd_args.append("always")
65
+ cmd_args.append("-v")
66
+ if report_mode:
67
+ cmd_args.append("--tb=short") # Show shorter tracebacks for failures
68
+ else:
69
+ cmd_args.append("--tb=long") # Show full tracebacks in debug mode
70
+ else:
71
+ # In default concise mode, keep quiet and concise
72
+ # Use --tb=short to only show short tracebacks and final short test summary info
73
+ cmd_args.append(
74
+ "--tb=short"
75
+ ) # Only show short tracebacks + short test summary info
76
+ cmd_args.append("-q") # Quiet mode - suppress progress characters
77
+ cmd_args.append("--no-header") # Don't show pytest header
78
+ # In concise mode, only show warning categories that should fail the test
79
+ cmd_args.append("-W")
80
+ cmd_args.append("error::DeprecationWarning")
81
+ cmd_args.append("-W")
82
+ cmd_args.append("ignore::UserWarning")
83
+ cmd_args.append("-W")
84
+ cmd_args.append("ignore::RuntimeWarning")
85
+
86
+ # Add debug markers if in debug mode
87
+ if debug_mode and "-xvs" not in args:
88
+ cmd_args.extend(
89
+ ["-xvs"]
90
+ ) # Stop at first failure, verbose, show print output
91
+
92
+ has_user_cov = any(arg.startswith("--cov") for arg in args)
93
+
94
+ # Add coverage-related arguments
95
+ # By default: do NOT check coverage - only enable coverage in report mode
96
+ has_pytest_cov = self._check_pytest_cov(python_executable, env)
97
+ if has_pytest_cov and not has_user_cov:
98
+ if report_mode:
99
+ # Only enable coverage collection in report mode. The target is
100
+ # the project's own package (rv192 MINOR-3): `--cov=wl_cli`
101
+ # measured nothing for any other project.
102
+ cmd_args.extend(
103
+ [
104
+ *[
105
+ f"--cov={target}"
106
+ for target in coverage_targets(project_root)
107
+ ],
108
+ "--cov-branch",
109
+ "--cov-context=test",
110
+ ]
111
+ )
112
+ # Control coverage report output based on verbosity
113
+ if verbose or report_mode:
114
+ cmd_args.append("--cov-report=term-missing:skip-covered")
115
+ else:
116
+ # In concise mode, disable terminal coverage report completely
117
+ cmd_args.append("--cov-report=")
118
+ cmd_args.append("--no-cov-on-fail")
119
+ else:
120
+ # Default mode: disable coverage completely
121
+ cmd_args.append("--no-cov")
122
+
123
+ if report_mode:
124
+ # In report mode, generate additional coverage outputs
125
+ # Create todos directory in current working directory
126
+ todo_dir = Path("todos")
127
+ todo_dir.mkdir(exist_ok=True)
128
+ cov_json = str(todo_dir / "coverage.json")
129
+ junit_xml = str(todo_dir / "junit.xml")
130
+ # `--cov-report` 由 pytest-cov 提供:缺失时必须**省略**,否则 pytest 以
131
+ # "unrecognized arguments" 失败(`wl test --report` 曾恒传它)。
132
+ # `--junitxml` 是 pytest 核心 flag,始终可传。
133
+ if has_pytest_cov:
134
+ cmd_args.append(f"--cov-report=json:{cov_json}")
135
+ cmd_args.append(f"--junitxml={junit_xml}")
136
+
137
+ # Add any additional user arguments
138
+ if args:
139
+ # Check if any argument looks like a test file/directory path (doesn't start with --)
140
+ has_path_arg = any(not arg.startswith("--") for arg in args)
141
+ cmd_args.extend(args)
142
+
143
+ # Add default test path if no path arguments provided (regardless of mode)
144
+ if not has_path_arg:
145
+ cmd_args.append("tests/")
146
+ else:
147
+ # If no args provided, add default test path
148
+ cmd_args.append("tests/")
149
+
150
+ return cmd_args
151
+
152
+ def _check_pytest_cov(
153
+ self, python_executable: str, env: dict[str, str] | None = None
154
+ ) -> bool:
155
+ """pytest-cov 是否可被**执行 pytest 的那个解释器**导入。
156
+
157
+ 判定必须落在真正执行 pytest 的解释器上,而不是 wl 进程本身:`wl test`
158
+ 全量路径由 `test_runner._run_all_tests` 用调用方解析出的 `python_executable`
159
+ 执行 pytest,可能是与 wl 进程不同的解释器。只在 wl 进程内 `__import__`
160
+ 会让 `--no-cov` 误判:漏判 ⇒ 子进程转而吃 pyproject 的 `--cov=wl_cli`;
161
+ 多判 ⇒ 子进程收到 pytest 认不出的 `--no-cov` 直接失败。
162
+
163
+ 探针本体是 `test_utils._probe_pytest_cov`(#112 起三处入口共用)。
164
+ """
165
+ return _probe_pytest_cov(python_executable, env)