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,183 @@
|
|
|
1
|
+
"""Coverage cache management module (the coverage cache adapter).
|
|
2
|
+
|
|
3
|
+
This is one of the three adapters over the single cache seam
|
|
4
|
+
:class:`~wl_cli.utils.cache_ledger.CacheLedger` (#213, ADR-0011). It owns what
|
|
5
|
+
this cache decides for itself -- the key inputs, the user-level location and the
|
|
6
|
+
7-day TTL. Key spelling, storage, freshness and the error policy are the
|
|
7
|
+
ledger's.
|
|
8
|
+
|
|
9
|
+
The key stays on file **stat** metadata (``mtime_ns`` / ``ctime_ns`` / ``size``)
|
|
10
|
+
rather than file content: ``ctime`` cannot be restored by the user, so a content
|
|
11
|
+
change always moves the key, and the coverage pass does not have to re-read every
|
|
12
|
+
source and test file just to build a key (the performance trade-off recorded in
|
|
13
|
+
#15). What #213 changed is the failure path: when a file cannot be ``stat``-ed
|
|
14
|
+
there is no truthful key, so **nothing is cached** for that run instead of
|
|
15
|
+
silently keying on the path alone.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, cast
|
|
20
|
+
|
|
21
|
+
from ...utils.cache_ledger import CacheInputs, CacheLedger
|
|
22
|
+
from ...utils.config import get_config
|
|
23
|
+
|
|
24
|
+
_DEFAULT_CACHE_ROOT = Path.home() / ".cache" / "wl"
|
|
25
|
+
|
|
26
|
+
#: Returned by :meth:`CoverageCacheManager.get_cache_key` when a file's
|
|
27
|
+
#: ``stat`` failed, so no truthful key exists. ``KeyedStore`` refuses the empty
|
|
28
|
+
#: string as a key, and the two cache operations below guard on it explicitly:
|
|
29
|
+
#: a read is a miss and a write is a no-op. The call sites therefore need no
|
|
30
|
+
#: special case -- an unkeyed run simply never caches.
|
|
31
|
+
UNKEYED = ""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _default_coverage_cache_dir() -> Path:
|
|
35
|
+
"""Resolve the default coverage cache directory.
|
|
36
|
+
|
|
37
|
+
Mirrors the test-cache root resolution (the ``test_cache.cache_dir`` config
|
|
38
|
+
key, falling back to ``~/.cache/wl``) and appends ``test-cache/coverage``.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Path to the default coverage cache directory.
|
|
42
|
+
"""
|
|
43
|
+
configured = get_config("test_cache.cache_dir", None)
|
|
44
|
+
root = (
|
|
45
|
+
Path(configured).expanduser()
|
|
46
|
+
if isinstance(configured, str) and configured
|
|
47
|
+
else _DEFAULT_CACHE_ROOT
|
|
48
|
+
)
|
|
49
|
+
return root / "test-cache" / "coverage"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CoverageCacheManager:
|
|
53
|
+
"""Cache whole ``coverage.json`` payloads, keyed by the files they describe.
|
|
54
|
+
|
|
55
|
+
Responsibilities:
|
|
56
|
+
- Answer the key inputs: every source and test file's path plus its stat
|
|
57
|
+
metadata, in sorted path order
|
|
58
|
+
- Read and write coverage payloads through the ledger
|
|
59
|
+
- Own the location (user-level) and the TTL (7 days); the ledger owns the
|
|
60
|
+
mechanism
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
CACHE_EXPIRY_SECONDS = 7 * 86400 # 7 days
|
|
64
|
+
NAMESPACE = "coverage"
|
|
65
|
+
|
|
66
|
+
def __init__(self, cache_dir: Path | None = None) -> None:
|
|
67
|
+
"""Initialize CoverageCacheManager.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
cache_dir: Directory holding coverage cache entries. Defaults to
|
|
71
|
+
``<test_cache.cache_dir>/test-cache/coverage``.
|
|
72
|
+
"""
|
|
73
|
+
self._ledger = CacheLedger(
|
|
74
|
+
cache_dir if cache_dir is not None else _default_coverage_cache_dir(),
|
|
75
|
+
self.CACHE_EXPIRY_SECONDS,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def get_cache_key(self, source_files: list[Path], test_files: list[Path]) -> str:
|
|
79
|
+
"""Build the coverage cache key from the files the coverage describes.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
source_files: List of source code files
|
|
83
|
+
test_files: List of corresponding test files
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The ledger key (``coverage_`` plus the full SHA-256 digest), or
|
|
87
|
+
:data:`UNKEYED` when any file's ``stat`` failed. In that case the
|
|
88
|
+
caller gets a miss on read and a no-op on write: a key that ignored
|
|
89
|
+
a file's identity would serve one run's coverage to another run.
|
|
90
|
+
"""
|
|
91
|
+
parts: list[str] = []
|
|
92
|
+
for file_path in (
|
|
93
|
+
*sorted(source_files, key=str),
|
|
94
|
+
*sorted(test_files, key=str),
|
|
95
|
+
):
|
|
96
|
+
part = self._file_part(file_path)
|
|
97
|
+
if part is None:
|
|
98
|
+
return UNKEYED
|
|
99
|
+
parts.append(part)
|
|
100
|
+
|
|
101
|
+
return self._ledger.key(CacheInputs(self.NAMESPACE, tuple(parts)))
|
|
102
|
+
|
|
103
|
+
def get_cached_coverage(self, cache_key: str) -> dict[str, Any] | None:
|
|
104
|
+
"""Get cached coverage data from the cache.
|
|
105
|
+
|
|
106
|
+
A read failure of any kind -- missing entry, unreadable file, corrupt
|
|
107
|
+
JSON, an expired entry -- is a miss, never an exception.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
cache_key: The cache key to look up; :data:`UNKEYED` always misses.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Cached coverage data as dictionary, or None if there is no usable
|
|
114
|
+
entry.
|
|
115
|
+
"""
|
|
116
|
+
if not cache_key:
|
|
117
|
+
return None
|
|
118
|
+
entry = self._ledger.get(cache_key)
|
|
119
|
+
if entry is None:
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
coverage_data = entry.get("coverage_data")
|
|
123
|
+
return cast("dict[str, Any] | None", coverage_data) if coverage_data else None
|
|
124
|
+
|
|
125
|
+
def save_cached_coverage(
|
|
126
|
+
self, cache_key: str, coverage_data: dict[str, Any]
|
|
127
|
+
) -> bool:
|
|
128
|
+
"""Save coverage data to the cache, atomically.
|
|
129
|
+
|
|
130
|
+
A failed write never breaks the coverage run: the ledger logs it and
|
|
131
|
+
returns ``False``.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
cache_key: The cache key to use; :data:`UNKEYED` writes nothing.
|
|
135
|
+
coverage_data: Coverage data to cache.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
True if the entry was written; False when the key is
|
|
139
|
+
:data:`UNKEYED` or the write failed.
|
|
140
|
+
"""
|
|
141
|
+
if not cache_key:
|
|
142
|
+
return False
|
|
143
|
+
return self._ledger.put(
|
|
144
|
+
cache_key,
|
|
145
|
+
{"coverage_data": coverage_data, "version": "2.0"},
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def _file_part(file_path: Path) -> str | None:
|
|
150
|
+
"""One file's contribution to the key, or ``None`` when it is unknown.
|
|
151
|
+
|
|
152
|
+
The part is the normalized path plus the stat metadata the key has
|
|
153
|
+
always used (``mtime_ns`` / ``ctime_ns`` / ``size``). A ``stat``
|
|
154
|
+
failure returns ``None`` rather than a path-only part: the file's
|
|
155
|
+
identity is then unknown, and an entry keyed without it could be served
|
|
156
|
+
to a later run that is not this run (#213, replacing the old
|
|
157
|
+
``except Exception: pass``).
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
file_path: The file to describe.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
The part, or ``None`` if ``stat`` failed.
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
stat_result = file_path.stat()
|
|
167
|
+
except (OSError, ValueError):
|
|
168
|
+
# ``OSError``: the file vanished or cannot be inspected. ``ValueError``:
|
|
169
|
+
# the path itself is unusable (an embedded NUL), which can name no
|
|
170
|
+
# file and so can yield no truthful key either. Both mean the same
|
|
171
|
+
# thing here, and nothing else is expected from ``stat`` (#213
|
|
172
|
+
# review NIT-7).
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
path_str = str(file_path).replace("\\", "/")
|
|
176
|
+
return "\0".join(
|
|
177
|
+
(
|
|
178
|
+
path_str,
|
|
179
|
+
str(stat_result.st_mtime_ns),
|
|
180
|
+
str(stat_result.st_ctime_ns),
|
|
181
|
+
str(stat_result.st_size),
|
|
182
|
+
)
|
|
183
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Progress manager module for test commands."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ProgressManager:
|
|
8
|
+
"""Progress manager for displaying test progress."""
|
|
9
|
+
|
|
10
|
+
def _truncate_filename(self, filename):
|
|
11
|
+
"""Truncate filename to fit in progress bar."""
|
|
12
|
+
max_len = 28
|
|
13
|
+
if len(filename) > max_len:
|
|
14
|
+
return filename[: max_len - 3] + "..."
|
|
15
|
+
return filename
|
|
16
|
+
|
|
17
|
+
def _update_test_progress(self, pbar, truncated_name, test_file, cache_hits):
|
|
18
|
+
"""Update progress bar for test processing with cache details."""
|
|
19
|
+
# Check if this file used cached result
|
|
20
|
+
if test_file.name in cache_hits:
|
|
21
|
+
# Show cached status in progress bar
|
|
22
|
+
pbar.set_description(f"Testing {truncated_name:<28} [cached]")
|
|
23
|
+
else:
|
|
24
|
+
# Normal testing status
|
|
25
|
+
pbar.set_description(f"Testing {truncated_name:<28}")
|
|
26
|
+
|
|
27
|
+
pbar.update(1)
|
|
28
|
+
|
|
29
|
+
def _log_report_mode(self, report_mode: bool, concise: bool = False) -> None:
|
|
30
|
+
try:
|
|
31
|
+
from ...utils.logging import log_info_pair
|
|
32
|
+
|
|
33
|
+
if not concise:
|
|
34
|
+
log_info_pair(
|
|
35
|
+
f"Report mode: {report_mode}",
|
|
36
|
+
f"报告模式: {report_mode}",
|
|
37
|
+
)
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
def _log_project_paths(
|
|
42
|
+
self, project_root: str, todo_dir: Path, concise: bool = False
|
|
43
|
+
) -> None:
|
|
44
|
+
try:
|
|
45
|
+
from ...utils.logging import log_info_pair
|
|
46
|
+
|
|
47
|
+
if not concise:
|
|
48
|
+
log_info_pair(
|
|
49
|
+
f"Project root: {project_root}",
|
|
50
|
+
f"项目根目录: {project_root}",
|
|
51
|
+
)
|
|
52
|
+
log_info_pair(
|
|
53
|
+
f"Todos dir: {todo_dir}",
|
|
54
|
+
f"报告目录: {todo_dir}",
|
|
55
|
+
)
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
def _build_env(self, project_root: str) -> dict:
|
|
60
|
+
env = os.environ.copy()
|
|
61
|
+
src_path = os.path.join(project_root, "src")
|
|
62
|
+
if "PYTHONPATH" in env:
|
|
63
|
+
env["PYTHONPATH"] = f"{src_path}{os.pathsep}{env['PYTHONPATH']}"
|
|
64
|
+
else:
|
|
65
|
+
env["PYTHONPATH"] = src_path
|
|
66
|
+
return env
|
|
67
|
+
|
|
68
|
+
def project_root(self) -> str:
|
|
69
|
+
"""The project root the test run is anchored to, as a string.
|
|
70
|
+
|
|
71
|
+
A plain forwarder to the test subsystem's policy entry point
|
|
72
|
+
(:func:`~wl_cli.commands.testcommands.utils.project_root.project_root_for`),
|
|
73
|
+
which owns the ``tests/``/``src/`` pre-check and the CWD fallback
|
|
74
|
+
(ADR 0006: the fallback is the caller's policy, not the root module's).
|
|
75
|
+
It stays a method so callers keep a single, mockable seam.
|
|
76
|
+
"""
|
|
77
|
+
from .utils.project_root import project_root_for
|
|
78
|
+
|
|
79
|
+
return project_root_for()
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python environment detection utilities for test command.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ...utils.logging import log_debug
|
|
11
|
+
from ...utils.platform_adapter import PlatformAdapter
|
|
12
|
+
from ...utils.workspace_detector import WorkspaceDetector
|
|
13
|
+
from .utils.project_root import project_root_for
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PythonEnvDetector:
|
|
17
|
+
"""Detect Python executable in different environments.
|
|
18
|
+
|
|
19
|
+
``root`` is the directory the detection is anchored to; ``None`` -- the
|
|
20
|
+
default -- keeps the historical behaviour of resolving against the process
|
|
21
|
+
working directory, which is what ``wl test`` relies on (it is invoked *in*
|
|
22
|
+
the directory it is asked about). A caller whose subject runs somewhere else
|
|
23
|
+
must say so explicitly: ``wl gate``'s pytest leg is ``wl test`` launched in
|
|
24
|
+
the chain's anchor root, so its preflight has to ask about *that* root
|
|
25
|
+
rather than wherever the user happened to stand (issue #110).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, root: Path | None = None):
|
|
29
|
+
"""Initialize Python environment detector.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
root: Directory to anchor every lookup to. ``None`` means "the
|
|
33
|
+
process working directory", resolved lazily on each use so a
|
|
34
|
+
caller that never passes a root behaves exactly as before.
|
|
35
|
+
"""
|
|
36
|
+
self.detector = WorkspaceDetector()
|
|
37
|
+
self._root = root
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def root(self) -> Path:
|
|
41
|
+
"""The directory this detector resolves against."""
|
|
42
|
+
return Path.cwd() if self._root is None else self._root
|
|
43
|
+
|
|
44
|
+
def get_python_executable(self) -> str:
|
|
45
|
+
"""
|
|
46
|
+
Get the Python executable path, using the same logic as wl build dist.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
str: Path to the Python executable
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
python_exe, _ = self._resolve_python()
|
|
53
|
+
return python_exe or sys.executable
|
|
54
|
+
except Exception:
|
|
55
|
+
# 如果出现问题,回退到系统Python
|
|
56
|
+
return sys.executable
|
|
57
|
+
|
|
58
|
+
def _resolve_python(self) -> tuple[str | None, bool]:
|
|
59
|
+
"""
|
|
60
|
+
Resolve Python executable path, with enhanced virtual environment detection.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
tuple: (python_path, is_virtual_env)
|
|
64
|
+
"""
|
|
65
|
+
# 1. 检查是否在uv工作区环境中
|
|
66
|
+
is_workspace = self.detector.detect(self.root)
|
|
67
|
+
log_debug(f"uv workspace detected: {is_workspace}")
|
|
68
|
+
|
|
69
|
+
# 如果不在工作区,使用当前运行的Python解释器
|
|
70
|
+
if not is_workspace:
|
|
71
|
+
return sys.executable, False
|
|
72
|
+
|
|
73
|
+
# 2. 尝试通过不同方式查找虚拟环境中的Python
|
|
74
|
+
python_path = self._find_python_in_uv_workspace()
|
|
75
|
+
if python_path:
|
|
76
|
+
return python_path, True
|
|
77
|
+
|
|
78
|
+
python_path = self._find_python_in_custom_venv()
|
|
79
|
+
if python_path:
|
|
80
|
+
return python_path, True
|
|
81
|
+
|
|
82
|
+
python_path = self._find_python_in_environment_venv()
|
|
83
|
+
if python_path:
|
|
84
|
+
return python_path, True
|
|
85
|
+
|
|
86
|
+
# 3. 最后回退到系统Python
|
|
87
|
+
log_debug(f"Falling back to system Python: {sys.executable}")
|
|
88
|
+
return sys.executable, False
|
|
89
|
+
|
|
90
|
+
def _find_python_in_uv_workspace(self) -> str | None:
|
|
91
|
+
"""查找uv工作区中的Python可执行文件。"""
|
|
92
|
+
venv_root = self.detector.get_venv_path(self.root)
|
|
93
|
+
if not venv_root:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
log_debug(f"get_venv_path result: {venv_root}")
|
|
97
|
+
|
|
98
|
+
# 尝试标准Python路径
|
|
99
|
+
python_exe = self._get_python_path_from_venv(venv_root)
|
|
100
|
+
if self._is_valid_python_executable(python_exe):
|
|
101
|
+
return python_exe
|
|
102
|
+
|
|
103
|
+
# 尝试python3路径
|
|
104
|
+
if not PlatformAdapter.is_windows():
|
|
105
|
+
python_exe_alt = str((venv_root / "bin" / "python3").absolute())
|
|
106
|
+
if self._is_valid_python_executable(python_exe_alt):
|
|
107
|
+
return python_exe_alt
|
|
108
|
+
|
|
109
|
+
# 尝试使用uv run
|
|
110
|
+
return self._get_python_using_uv_run()
|
|
111
|
+
|
|
112
|
+
def _find_python_in_custom_venv(self) -> str | None:
|
|
113
|
+
"""使用_find_venv方法查找Python。"""
|
|
114
|
+
venv_path = self._find_venv()
|
|
115
|
+
if not venv_path:
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
venv_root = Path(venv_path)
|
|
119
|
+
print(f"Debug: Found venv using _find_venv: {venv_root}")
|
|
120
|
+
|
|
121
|
+
python_exe = self._get_python_path_from_venv(venv_root)
|
|
122
|
+
if self._is_valid_python_executable(python_exe):
|
|
123
|
+
return python_exe
|
|
124
|
+
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
def _find_python_in_environment_venv(self) -> str | None:
|
|
128
|
+
"""通过VIRTUAL_ENV环境变量查找Python。"""
|
|
129
|
+
virtual_env = os.environ.get("VIRTUAL_ENV")
|
|
130
|
+
if not virtual_env:
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
venv_root = Path(virtual_env)
|
|
134
|
+
print(f"Debug: Using VIRTUAL_ENV: {venv_root}")
|
|
135
|
+
|
|
136
|
+
python_exe = self._get_python_path_from_venv(venv_root)
|
|
137
|
+
if self._is_valid_python_executable(python_exe):
|
|
138
|
+
return python_exe
|
|
139
|
+
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
def _get_python_path_from_venv(self, venv_root: Path) -> str:
|
|
143
|
+
"""从虚拟环境根目录构造Python可执行文件路径。"""
|
|
144
|
+
if PlatformAdapter.is_windows():
|
|
145
|
+
return str((venv_root / "Scripts" / "python.exe").absolute())
|
|
146
|
+
else:
|
|
147
|
+
return str((venv_root / "bin" / "python").absolute())
|
|
148
|
+
|
|
149
|
+
def _is_valid_python_executable(self, python_path: str) -> bool:
|
|
150
|
+
"""检查Python可执行文件是否存在。"""
|
|
151
|
+
log_debug(f"Checking Python executable at: {python_path}")
|
|
152
|
+
if Path(python_path).exists():
|
|
153
|
+
log_debug(f"Successfully found Python executable: {python_path}")
|
|
154
|
+
return True
|
|
155
|
+
log_debug(f"Python executable does not exist at: {python_path}")
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
def _get_python_using_uv_run(self) -> str | None:
|
|
159
|
+
"""使用uv run获取Python可执行文件路径。
|
|
160
|
+
|
|
161
|
+
``cwd`` 与其它查找同源:``uv run`` 解析的是**运行目录**所属的项目,
|
|
162
|
+
所以锚定 root 的调用必须在这里也用它,否则会问到另一个项目(#110)。
|
|
163
|
+
``None`` 仍然是「继承进程工作目录」,与改动前逐字节相同。
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
result = subprocess.run(
|
|
167
|
+
[
|
|
168
|
+
"uv",
|
|
169
|
+
"run",
|
|
170
|
+
"python",
|
|
171
|
+
"-c",
|
|
172
|
+
"import sys; print(sys.executable)",
|
|
173
|
+
],
|
|
174
|
+
cwd=self._root,
|
|
175
|
+
capture_output=True,
|
|
176
|
+
text=True,
|
|
177
|
+
check=True,
|
|
178
|
+
)
|
|
179
|
+
uv_python = result.stdout.strip()
|
|
180
|
+
log_debug(f"Using uv run python result: {uv_python}")
|
|
181
|
+
if Path(uv_python).exists():
|
|
182
|
+
return uv_python
|
|
183
|
+
except Exception as e:
|
|
184
|
+
log_debug(f"uv run python failed: {e}")
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
def _find_venv(self) -> str | None:
|
|
188
|
+
"""
|
|
189
|
+
Find virtual environment path if it exists.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
str | None: Path to virtual environment or None if not found
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
project_root = project_root_for(self._root)
|
|
196
|
+
|
|
197
|
+
# Check common virtual environment locations
|
|
198
|
+
possible_venv_paths = [
|
|
199
|
+
os.path.join(project_root, ".venv"),
|
|
200
|
+
os.path.join(project_root, "venv"),
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
for venv_path in possible_venv_paths:
|
|
204
|
+
if os.path.exists(os.path.join(venv_path, "pyvenv.cfg")):
|
|
205
|
+
return venv_path
|
|
206
|
+
|
|
207
|
+
return None
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Report generator for test results."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ...utils.logging import log_info_pair
|
|
7
|
+
from .test_reporter import generate_reports
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ReportGenerator:
|
|
11
|
+
"""Generates test reports after test completion."""
|
|
12
|
+
|
|
13
|
+
def generate(
|
|
14
|
+
self,
|
|
15
|
+
todo_dir: Path,
|
|
16
|
+
project_root: str,
|
|
17
|
+
verbose: bool = False,
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Generate coverage and error reports."""
|
|
20
|
+
try:
|
|
21
|
+
cov_json = str(todo_dir / "coverage.json")
|
|
22
|
+
junit_xml = str(todo_dir / "junit.xml")
|
|
23
|
+
cov_md = str(todo_dir / "coverage.md")
|
|
24
|
+
err_md = str(todo_dir / "error.md")
|
|
25
|
+
|
|
26
|
+
self._log_report_paths(cov_json, junit_xml, verbose)
|
|
27
|
+
|
|
28
|
+
summary = generate_reports(
|
|
29
|
+
coverage_json_path=cov_json,
|
|
30
|
+
junit_xml_path=junit_xml,
|
|
31
|
+
out_cov=cov_md,
|
|
32
|
+
out_err=err_md,
|
|
33
|
+
project_root=project_root,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
self._log_output_paths(cov_md, err_md, verbose)
|
|
37
|
+
|
|
38
|
+
if summary and verbose:
|
|
39
|
+
print(summary)
|
|
40
|
+
|
|
41
|
+
# Ensure files are fully written
|
|
42
|
+
time.sleep(0.5)
|
|
43
|
+
self._cleanup_temp_files(cov_json, junit_xml)
|
|
44
|
+
except Exception as e:
|
|
45
|
+
print(f"Failed to generate markdown reports: {e}")
|
|
46
|
+
|
|
47
|
+
def _log_report_paths(
|
|
48
|
+
self,
|
|
49
|
+
cov_json: str,
|
|
50
|
+
junit_xml: str,
|
|
51
|
+
verbose: bool = False,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Log report file paths."""
|
|
54
|
+
try:
|
|
55
|
+
if verbose:
|
|
56
|
+
log_info_pair(
|
|
57
|
+
f"Coverage JSON path: {cov_json}",
|
|
58
|
+
f"Coverage JSON路径: {cov_json}",
|
|
59
|
+
)
|
|
60
|
+
log_info_pair(
|
|
61
|
+
f"JUnit XML path: {junit_xml}",
|
|
62
|
+
f"JUnit XML路径: {junit_xml}",
|
|
63
|
+
)
|
|
64
|
+
log_info_pair(
|
|
65
|
+
f"Coverage JSON exists: {Path(cov_json).exists()}",
|
|
66
|
+
f"覆盖率JSON存在: {Path(cov_json).exists()}",
|
|
67
|
+
)
|
|
68
|
+
log_info_pair(
|
|
69
|
+
f"JUnit XML exists: {Path(junit_xml).exists()}",
|
|
70
|
+
f"JUnit XML存在: {Path(junit_xml).exists()}",
|
|
71
|
+
)
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
def _log_output_paths(
|
|
76
|
+
self,
|
|
77
|
+
cov_md: str,
|
|
78
|
+
err_md: str,
|
|
79
|
+
verbose: bool = False,
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Log output report paths."""
|
|
82
|
+
try:
|
|
83
|
+
if verbose:
|
|
84
|
+
log_info_pair(
|
|
85
|
+
f"Coverage MD path: {cov_md}",
|
|
86
|
+
f"Coverage MD存在: {Path(cov_md).exists()}",
|
|
87
|
+
)
|
|
88
|
+
log_info_pair(
|
|
89
|
+
f"Error MD path: {err_md}",
|
|
90
|
+
f"错误MD存在: {Path(err_md).exists()}",
|
|
91
|
+
)
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
def _cleanup_temp_files(self, cov_json: str, junit_xml: str) -> None:
|
|
96
|
+
"""Clean up temporary files after report generation."""
|
|
97
|
+
try:
|
|
98
|
+
Path(cov_json).unlink(missing_ok=True)
|
|
99
|
+
Path(junit_xml).unlink(missing_ok=True)
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|