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.
- py_wlcli-0.8.0.dist-info/METADATA +139 -0
- py_wlcli-0.8.0.dist-info/RECORD +320 -0
- py_wlcli-0.8.0.dist-info/WHEEL +4 -0
- py_wlcli-0.8.0.dist-info/entry_points.txt +2 -0
- py_wlcli-0.8.0.dist-info/licenses/LICENSE +21 -0
- wl_cli/__init__.py +27 -0
- wl_cli/__main__.py +130 -0
- wl_cli/capabilities.py +79 -0
- wl_cli/cc/__init__.py +5 -0
- wl_cli/cc/checks.py +1019 -0
- wl_cli/cc/cli.py +370 -0
- wl_cli/cc/mutate.py +1350 -0
- wl_cli/cli_click.py +156 -0
- wl_cli/commands/__init__.py +84 -0
- wl_cli/commands/base.py +80 -0
- wl_cli/commands/buildcommands/__init__.py +1 -0
- wl_cli/commands/buildcommands/build.py +71 -0
- wl_cli/commands/buildcommands/build_utils.py +47 -0
- wl_cli/commands/buildcommands/builders/__init__.py +18 -0
- wl_cli/commands/buildcommands/builders/cleanup.py +35 -0
- wl_cli/commands/buildcommands/builders/coordinator.py +122 -0
- wl_cli/commands/buildcommands/builders/environment.py +81 -0
- wl_cli/commands/buildcommands/builders/rust_builder.py +39 -0
- wl_cli/commands/buildcommands/builders/stubs.py +58 -0
- wl_cli/commands/buildcommands/dist.py +326 -0
- wl_cli/commands/buildcommands/test.py +89 -0
- wl_cli/commands/clean/__init__.py +1 -0
- wl_cli/commands/clean/build.py +151 -0
- wl_cli/commands/clean/clean.py +294 -0
- wl_cli/commands/clean/clean_utils.py +77 -0
- wl_cli/commands/clean/core.py +236 -0
- wl_cli/commands/clean/dead_code/__init__.py +0 -0
- wl_cli/commands/clean/dead_code/dead_code_command.py +70 -0
- wl_cli/commands/clean/dead_code/detector.py +161 -0
- wl_cli/commands/clean/dead_code/fixer.py +176 -0
- wl_cli/commands/clean/dead_code/reporter.py +295 -0
- wl_cli/commands/clean/dead_code/utils.py +0 -0
- wl_cli/commands/clean/lfs.py +215 -0
- wl_cli/commands/clean/python.py +81 -0
- wl_cli/commands/clean/rust.py +91 -0
- wl_cli/commands/clean/utils/__init__.py +34 -0
- wl_cli/commands/clean/venv.py +91 -0
- wl_cli/commands/cleanup.py +73 -0
- wl_cli/commands/click_base.py +79 -0
- wl_cli/commands/config/__init__.py +1 -0
- wl_cli/commands/config/config.py +291 -0
- wl_cli/commands/debug/__init__.py +6 -0
- wl_cli/commands/debug/debug.py +309 -0
- wl_cli/commands/debug/debug_cache.py +86 -0
- wl_cli/commands/debug/debug_subagent.py +156 -0
- wl_cli/commands/debug/output_formatter.py +165 -0
- wl_cli/commands/debug/source_hasher.py +92 -0
- wl_cli/commands/format/__init__.py +13 -0
- wl_cli/commands/format/format_cache_adapter.py +125 -0
- wl_cli/commands/format/format_command.py +197 -0
- wl_cli/commands/format/format_coordinator.py +191 -0
- wl_cli/commands/format/format_executor.py +470 -0
- wl_cli/commands/format/format_outcome.py +44 -0
- wl_cli/commands/format/format_path_handler.py +1016 -0
- wl_cli/commands/format/format_progress.py +62 -0
- wl_cli/commands/format/format_reporter.py +31 -0
- wl_cli/commands/format/python_formatter.py +506 -0
- wl_cli/commands/format/rust_formatter.py +106 -0
- wl_cli/commands/gate/__init__.py +1 -0
- wl_cli/commands/gate/gate_command.py +862 -0
- wl_cli/commands/gate/preflight.py +445 -0
- wl_cli/commands/git/__init__.py +1 -0
- wl_cli/commands/git/config.py +169 -0
- wl_cli/commands/git/git_facts.py +60 -0
- wl_cli/commands/git/git_flow.py +86 -0
- wl_cli/commands/git/hook.py +112 -0
- wl_cli/commands/git/policy.py +216 -0
- wl_cli/commands/git/push_update.py +82 -0
- wl_cli/commands/initenv/__init__.py +1 -0
- wl_cli/commands/initenv/common/__init__.py +23 -0
- wl_cli/commands/initenv/common/check_git_bash.py +78 -0
- wl_cli/commands/initenv/common/error_handler.py +68 -0
- wl_cli/commands/initenv/common/exceptions.py +52 -0
- wl_cli/commands/initenv/components/__init__.py +4 -0
- wl_cli/commands/initenv/components/cargo_sync.py +240 -0
- wl_cli/commands/initenv/components/config_manager.py +106 -0
- wl_cli/commands/initenv/components/directory_utils.py +71 -0
- wl_cli/commands/initenv/components/git/__init__.py +9 -0
- wl_cli/commands/initenv/components/git/branch_creator.py +143 -0
- wl_cli/commands/initenv/components/git/git_flow_interface.py +24 -0
- wl_cli/commands/initenv/components/git/git_flow_manager.py +159 -0
- wl_cli/commands/initenv/components/git/git_lfs_manager.py +272 -0
- wl_cli/commands/initenv/components/git/git_utility.py +254 -0
- wl_cli/commands/initenv/components/git/gitignore_manager.py +53 -0
- wl_cli/commands/initenv/components/git_initializer.py +212 -0
- wl_cli/commands/initenv/components/i18n_manager.py +104 -0
- wl_cli/commands/initenv/components/log_manager.py +241 -0
- wl_cli/commands/initenv/components/project_structure/__init__.py +23 -0
- wl_cli/commands/initenv/components/project_structure/config_generator.py +360 -0
- wl_cli/commands/initenv/components/project_structure/directory_creator.py +184 -0
- wl_cli/commands/initenv/components/project_structure/hook_source.py +110 -0
- wl_cli/commands/initenv/components/project_structure/hooks_manager.py +995 -0
- wl_cli/commands/initenv/components/project_structure/readme_handler.py +56 -0
- wl_cli/commands/initenv/components/project_structure/rust_files_handler.py +30 -0
- wl_cli/commands/initenv/components/project_structure/setup_handler.py +103 -0
- wl_cli/commands/initenv/components/project_structure/sop_generator.py +73 -0
- wl_cli/commands/initenv/components/project_structure/template_copier.py +104 -0
- wl_cli/commands/initenv/components/project_structure/template_manager.py +195 -0
- wl_cli/commands/initenv/components/pyproject_generator.py +194 -0
- wl_cli/commands/initenv/components/rust_initializer.py +145 -0
- wl_cli/commands/initenv/components/symlink_cleaner.py +99 -0
- wl_cli/commands/initenv/components/symlink_manager.py +52 -0
- wl_cli/commands/initenv/components/venv_manager.py +118 -0
- wl_cli/commands/initenv/initenv.py +169 -0
- wl_cli/commands/initenv/services/__init__.py +11 -0
- wl_cli/commands/initenv/services/environment_initializer.py +288 -0
- wl_cli/commands/initenv/services/git_lfs_initializer.py +26 -0
- wl_cli/commands/initenv/services/project_structure_initializer.py +52 -0
- wl_cli/commands/lfs/README.md +221 -0
- wl_cli/commands/lfs/__init__.py +5 -0
- wl_cli/commands/lfs/cli.py +106 -0
- wl_cli/commands/lfs/config.py +164 -0
- wl_cli/commands/lfs/hook.py +61 -0
- wl_cli/commands/lfs/initializer.py +531 -0
- wl_cli/commands/lfs/manager.py +95 -0
- wl_cli/commands/lfs/policy.py +165 -0
- wl_cli/commands/lfs/watcher.py +294 -0
- wl_cli/commands/lint/__init__.py +1 -0
- wl_cli/commands/lint/lint_command.py +297 -0
- wl_cli/commands/lint/lint_executor.py +409 -0
- wl_cli/commands/lint/lint_reporter.py +579 -0
- wl_cli/commands/lint/lint_run.py +258 -0
- wl_cli/commands/lint/mypy_config.py +90 -0
- wl_cli/commands/registry.py +103 -0
- wl_cli/commands/self/__init__.py +7 -0
- wl_cli/commands/self/common/__init__.py +8 -0
- wl_cli/commands/self/common/capabilities.py +30 -0
- wl_cli/commands/self/common/config.py +27 -0
- wl_cli/commands/self/common/version_cache.py +85 -0
- wl_cli/commands/self/handlers/__init__.py +19 -0
- wl_cli/commands/self/handlers/base_handler.py +28 -0
- wl_cli/commands/self/handlers/install_handler.py +85 -0
- wl_cli/commands/self/handlers/status_handler.py +66 -0
- wl_cli/commands/self/handlers/uninstall_handler.py +58 -0
- wl_cli/commands/self/handlers/update_handler.py +102 -0
- wl_cli/commands/self/handlers/version_handler.py +16 -0
- wl_cli/commands/self/self_command.py +237 -0
- wl_cli/commands/self/services/__init__.py +15 -0
- wl_cli/commands/self/services/installation_manager.py +187 -0
- wl_cli/commands/self/services/local_installer.py +165 -0
- wl_cli/commands/self/services/pypi_updater.py +278 -0
- wl_cli/commands/self/services/windows_update.py +174 -0
- wl_cli/commands/testcommands/__init__.py +8 -0
- wl_cli/commands/testcommands/base_dependency_analyzer.py +198 -0
- wl_cli/commands/testcommands/batch_processor.py +278 -0
- wl_cli/commands/testcommands/cache_manager.py +1156 -0
- wl_cli/commands/testcommands/coverage_analyzer.py +955 -0
- wl_cli/commands/testcommands/coverage_cache_manager.py +183 -0
- wl_cli/commands/testcommands/progress_manager.py +79 -0
- wl_cli/commands/testcommands/python_env_detector.py +207 -0
- wl_cli/commands/testcommands/report_generator.py +101 -0
- wl_cli/commands/testcommands/single_test_executor.py +250 -0
- wl_cli/commands/testcommands/speed_checker.py +118 -0
- wl_cli/commands/testcommands/test_command_builder.py +165 -0
- wl_cli/commands/testcommands/test_commands.py +786 -0
- wl_cli/commands/testcommands/test_reporter.py +181 -0
- wl_cli/commands/testcommands/test_result_handler.py +96 -0
- wl_cli/commands/testcommands/test_runner.py +1343 -0
- wl_cli/commands/testcommands/test_utils.py +197 -0
- wl_cli/commands/testcommands/utils/__init__.py +17 -0
- wl_cli/commands/testcommands/utils/coverage_parser.py +98 -0
- wl_cli/commands/testcommands/utils/env_info.py +36 -0
- wl_cli/commands/testcommands/utils/junit_parser.py +75 -0
- wl_cli/commands/testcommands/utils/markdown_utils.py +20 -0
- wl_cli/commands/testcommands/utils/project_root.py +61 -0
- wl_cli/commands/update/__init__.py +9 -0
- wl_cli/commands/update/services/__init__.py +21 -0
- wl_cli/commands/update/services/backup_manager.py +207 -0
- wl_cli/commands/update/services/component_manager.py +83 -0
- wl_cli/commands/update/services/conflict_resolver.py +189 -0
- wl_cli/commands/update/services/vendors_updater.py +85 -0
- wl_cli/commands/update/services/version_tracker.py +127 -0
- wl_cli/commands/update/update_command.py +270 -0
- wl_cli/commands/update/utils/__init__.py +3 -0
- wl_cli/commands/version/__init__.py +9 -0
- wl_cli/commands/version/version_command.py +155 -0
- wl_cli/commands/workflow.py +477 -0
- wl_cli/env/__init__.py +5 -0
- wl_cli/env/cli.py +167 -0
- wl_cli/env_status.py +157 -0
- wl_cli/envelope.py +91 -0
- wl_cli/exceptions.py +85 -0
- wl_cli/lib/__init__.py +3 -0
- wl_cli/lib/rust_utils.py +35 -0
- wl_cli/mcp/__init__.py +6 -0
- wl_cli/mcp/capabilities.py +19 -0
- wl_cli/mcp/cli.py +22 -0
- wl_cli/mcp/gitlab.py +553 -0
- wl_cli/mcp/k8s.py +344 -0
- wl_cli/mcp/protocol.py +159 -0
- wl_cli/platform.py +139 -0
- wl_cli/quality/__init__.py +18 -0
- wl_cli/utils/__init__.py +7 -0
- wl_cli/utils/cache.py +491 -0
- wl_cli/utils/cache_ledger.py +355 -0
- wl_cli/utils/cache_store.py +546 -0
- wl_cli/utils/config/__init__.py +27 -0
- wl_cli/utils/config/config_loader.py +163 -0
- wl_cli/utils/config/core.py +862 -0
- wl_cli/utils/config/defaults.py +142 -0
- wl_cli/utils/config/env_exporter.py +86 -0
- wl_cli/utils/config/env_parser.py +108 -0
- wl_cli/utils/config/validation.py +280 -0
- wl_cli/utils/error_handler.py +165 -0
- wl_cli/utils/file_copier.py +56 -0
- wl_cli/utils/file_operations.py +506 -0
- wl_cli/utils/language_utils.py +122 -0
- wl_cli/utils/lazy_import.py +240 -0
- wl_cli/utils/log_levels.py +35 -0
- wl_cli/utils/log_rotators.py +74 -0
- wl_cli/utils/logging.py +458 -0
- wl_cli/utils/output_context.py +142 -0
- wl_cli/utils/performance/__init__.py +1 -0
- wl_cli/utils/performance/decorators.py +111 -0
- wl_cli/utils/performance/exporter.py +38 -0
- wl_cli/utils/performance/models.py +16 -0
- wl_cli/utils/performance/monitor.py +161 -0
- wl_cli/utils/performance_monitor.py +32 -0
- wl_cli/utils/platform_adapter.py +131 -0
- wl_cli/utils/pre_commit.py +266 -0
- wl_cli/utils/pre_commit_config.py +72 -0
- wl_cli/utils/root.py +521 -0
- wl_cli/utils/root_fallback.py +256 -0
- wl_cli/utils/run_evidence.py +236 -0
- wl_cli/utils/structured_logger.py +18 -0
- wl_cli/utils/structured_logging/__init__.py +19 -0
- wl_cli/utils/structured_logging/config.py +337 -0
- wl_cli/utils/structured_logging/core.py +177 -0
- wl_cli/utils/structured_logging/env.py +56 -0
- wl_cli/utils/structured_logging/filtering.py +75 -0
- wl_cli/utils/structured_logging/filters.py +55 -0
- wl_cli/utils/structured_logging/formatters.py +251 -0
- wl_cli/utils/structured_logging/handlers.py +308 -0
- wl_cli/utils/structured_logging/logger.py +354 -0
- wl_cli/utils/subprocess.py +71 -0
- wl_cli/utils/subprocess_utils.py +260 -0
- wl_cli/utils/test_utils/__init__.py +5 -0
- wl_cli/utils/test_utils/test_filter.py +36 -0
- wl_cli/utils/uv_run.py +79 -0
- wl_cli/utils/uv_tool.py +117 -0
- wl_cli/utils/uv_tree.py +71 -0
- wl_cli/utils/uv_workspace.py +174 -0
- wl_cli/utils/version/__init__.py +15 -0
- wl_cli/utils/version/pypi_version_checker.py +95 -0
- wl_cli/utils/version/version_comparator.py +133 -0
- wl_cli/utils/version/version_detectors.py +312 -0
- wl_cli/utils/version/version_service.py +131 -0
- wl_cli/utils/version/version_updater.py +198 -0
- wl_cli/utils/wl_dir_updater.py +1440 -0
- wl_cli/utils/workspace_detector/__init__.py +28 -0
- wl_cli/utils/workspace_detector/core.py +161 -0
- wl_cli/utils/workspace_detector/detection_strategies.py +107 -0
- wl_cli/utils/workspace_detector/exceptions.py +5 -0
- wl_cli/utils/workspace_detector/types.py +17 -0
- wl_cli/utils/workspace_detector/venv_resolver.py +84 -0
- wl_cli/vendors/config/.codespellrc +2 -0
- wl_cli/vendors/config/dependencies.toml +10 -0
- wl_cli/vendors/config/mypy.ini +62 -0
- wl_cli/vendors/config/pyproject_defaults.toml +28 -0
- wl_cli/vendors/config/template_config.toml +24 -0
- wl_cli/vendors/config/tool_configs.toml +76 -0
- wl_cli/vendors/git/.gitignore +122 -0
- wl_cli/vendors/git/.pre-commit-config.yaml +37 -0
- wl_cli/vendors/hooks/applypatch-msg.sample +15 -0
- wl_cli/vendors/hooks/commit-msg.sample +24 -0
- wl_cli/vendors/hooks/fsmonitor-watchman.sample +174 -0
- wl_cli/vendors/hooks/post-update.sample +8 -0
- wl_cli/vendors/hooks/pre-applypatch.sample +14 -0
- wl_cli/vendors/hooks/pre-commit +45 -0
- wl_cli/vendors/hooks/pre-commit.legacy +31 -0
- wl_cli/vendors/hooks/pre-commit.sample +49 -0
- wl_cli/vendors/hooks/pre-merge-commit.sample +13 -0
- wl_cli/vendors/hooks/pre-push.sample +60 -0
- wl_cli/vendors/hooks/pre-rebase.sample +169 -0
- wl_cli/vendors/hooks/pre-receive.sample +24 -0
- wl_cli/vendors/hooks/prepare-commit-msg.sample +42 -0
- wl_cli/vendors/hooks/push-to-checkout.sample +78 -0
- wl_cli/vendors/hooks/sendemail-validate.sample +77 -0
- wl_cli/vendors/hooks/unix/applypatch-msg.sample +15 -0
- wl_cli/vendors/hooks/unix/check_lockfile.py +58 -0
- wl_cli/vendors/hooks/unix/check_lockfile.sh +36 -0
- wl_cli/vendors/hooks/unix/commit-msg.sample +24 -0
- wl_cli/vendors/hooks/unix/fsmonitor-watchman.sample +174 -0
- wl_cli/vendors/hooks/unix/post-update.sample +8 -0
- wl_cli/vendors/hooks/unix/pre-applypatch.sample +14 -0
- wl_cli/vendors/hooks/unix/pre-commit +190 -0
- wl_cli/vendors/hooks/unix/pre-commit.legacy +31 -0
- wl_cli/vendors/hooks/unix/pre-commit.py +81 -0
- wl_cli/vendors/hooks/unix/pre-commit.sample +49 -0
- wl_cli/vendors/hooks/unix/pre-merge-commit.sample +13 -0
- wl_cli/vendors/hooks/unix/pre-push +417 -0
- wl_cli/vendors/hooks/unix/pre-push.sample +53 -0
- wl_cli/vendors/hooks/unix/pre-rebase.sample +169 -0
- wl_cli/vendors/hooks/unix/pre-receive.sample +24 -0
- wl_cli/vendors/hooks/unix/prepare-commit-msg.sample +42 -0
- wl_cli/vendors/hooks/update.sample +128 -0
- wl_cli/vendors/hooks/win/work.bat +63 -0
- wl_cli/vendors/project/.gitignore +90 -0
- wl_cli/vendors/project/.python-version +1 -0
- wl_cli/vendors/project/LICENSE +21 -0
- wl_cli/vendors/project/README.md +0 -0
- wl_cli/vendors/project/pyproject.toml +27 -0
- wl_cli/vendors/project/pyproject_no_workspace.toml +16 -0
- wl_cli/vendors/project/pyproject_workspace.toml +14 -0
- wl_cli/vendors/readme/README.md +120 -0
- wl_cli/vendors/rust/Cargo.toml +14 -0
- wl_cli/vendors/rust/lib/__init__.py +3 -0
- wl_cli/vendors/rust/lib/rust_utils.py +31 -0
- wl_cli/vendors/rust/src/lib.rs +13 -0
- wl_cli/vendors/rust/src/main.rs +3 -0
- wl_cli/vendors/rust/tests/test_rust_fallback.py +40 -0
- wl_cli/vendors/sop_doc/git-dev-sop.md +220 -0
- wl_cli/vendors/sop_doc/industrial-standard-sop.md +145 -0
- wl_cli/vendors/sop_doc/python-sop.md +342 -0
- 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)
|