claude-dev-env 7.0.0 → 7.1.0
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.
- package/.agents/skills/pull-request/scripts/pull_request.py +2 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
- package/.agents/skills/session-continuity/AGENTS.md +1 -0
- package/.agents/skills/session-continuity/CLAUDE.md +1 -0
- package/.agents/skills/session-continuity/README.md +189 -0
- package/.agents/skills/session-continuity/SKILL.md +114 -0
- package/.agents/skills/session-continuity/continuity.mjs +375 -0
- package/.agents/skills/session-continuity/continuity.test.mjs +303 -0
- package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
- package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
- package/bin/cde.mjs +36 -8
- package/bin/cde.test.mjs +48 -0
- package/bin/ever-shipped-skills.mjs +2 -20
- package/bin/ever-shipped-skills.test.mjs +8 -0
- package/bin/expand_home_directory_tokens.mjs +4 -5
- package/bin/expand_home_directory_tokens.test.mjs +128 -0
- package/bin/git_hooks_installer.mjs +22 -3
- package/bin/git_hooks_installer.test.mjs +159 -5
- package/bin/install-session-continuity.mjs +78 -0
- package/bin/install-session-continuity.test.mjs +42 -0
- package/bin/install.codex-rules.test.mjs +5 -2
- package/bin/install.cursor-rules.test.mjs +5 -2
- package/bin/install.mjs +478 -63
- package/bin/install.policy-lint.test.mjs +56 -4
- package/bin/install.profiles.test.mjs +10 -0
- package/bin/install.prune.test.mjs +227 -17
- package/bin/install.test.mjs +500 -120
- package/bin/install.transaction.test.mjs +3 -3
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +5 -8
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
- package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
- package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
- package/hooks/git-hooks/post_commit.py +33 -0
- package/hooks/git-hooks/pre_commit.py +18 -90
- package/hooks/git-hooks/pre_push.py +45 -541
- package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
- package/hooks/git-hooks/test_native_hook_support.py +129 -0
- package/hooks/git-hooks/test_native_policy_commit.py +39 -6
- package/hooks/git-hooks/test_post_commit.py +43 -0
- package/hooks/git-hooks/test_pre_commit.py +36 -392
- package/hooks/git-hooks/test_pre_push.py +48 -1631
- package/hooks/git-hooks/test_verification_notice.py +60 -0
- package/hooks/git-hooks/test_verification_start.py +173 -0
- package/hooks/git-hooks/verification_notice.py +192 -0
- package/hooks/git-hooks/verification_notice_context.py +253 -0
- package/hooks/git-hooks/verification_notice_state.py +237 -0
- package/hooks/git-hooks/verification_start.py +100 -0
- package/hooks/hooks.json +0 -119
- package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
- package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
- package/hooks/validators/run_all_validators.py +5 -1
- package/hooks/validators/security_checks.py +57 -2
- package/hooks/validators/test_hook_timing_harness.py +2 -5
- package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
- package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
- package/hooks/validators/test_security_checks.py +57 -0
- package/package.json +1 -1
- package/rules/confirm-implementation-forks.md +2 -0
- package/rules/git-workflow.md +12 -0
- package/rules/pstack-host-mapping.md +48 -0
- package/rules/vault-context.md +3 -5
- package/rules/verify-runtime-state.md +7 -0
- package/scripts/audit-hooks-cli.mjs +5 -0
- package/scripts/audit-hooks.mjs +72 -16
- package/scripts/automatic_advisory/__init__.py +1 -0
- package/scripts/automatic_advisory/checkout.py +51 -0
- package/scripts/automatic_advisory/cli.py +334 -0
- package/scripts/automatic_advisory/config/__init__.py +1 -0
- package/scripts/automatic_advisory/config/constants.py +93 -0
- package/scripts/automatic_advisory/config/timing.py +4 -0
- package/scripts/automatic_advisory/configuration.py +197 -0
- package/scripts/automatic_advisory/execution.py +350 -0
- package/scripts/automatic_advisory/git.py +69 -0
- package/scripts/automatic_advisory/model.py +107 -0
- package/scripts/automatic_advisory/process_host.py +32 -0
- package/scripts/automatic_advisory/publisher.py +84 -0
- package/scripts/automatic_advisory/runner.py +399 -0
- package/scripts/automatic_advisory/state.py +116 -0
- package/scripts/automatic_advisory/window_flags.py +41 -0
- package/scripts/automatic_advisory/windows_job.py +122 -0
- package/scripts/codex_compat_materializer.py +99 -13
- package/scripts/durable_post_lint.py +30 -2
- package/scripts/durable_post_lint_config/config/constants.py +10 -0
- package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
- package/scripts/local_report_cli.py +184 -0
- package/scripts/local_report_core.py +247 -0
- package/scripts/local_report_publisher.py +12 -0
- package/scripts/local_report_validation.py +357 -0
- package/scripts/local_verification/__init__.py +23 -0
- package/scripts/local_verification/check_runner.py +364 -0
- package/scripts/local_verification/check_support.py +99 -0
- package/scripts/local_verification/cli.py +158 -0
- package/scripts/local_verification/command_runner.py +84 -0
- package/scripts/local_verification/config/__init__.py +1 -0
- package/scripts/local_verification/config/constants.py +89 -0
- package/scripts/local_verification/config/timing.py +1 -0
- package/scripts/local_verification/git_state.py +146 -0
- package/scripts/local_verification/manifest.py +224 -0
- package/scripts/local_verification/model.py +174 -0
- package/scripts/local_verification/runner.py +234 -0
- package/scripts/policy_lint/adapter_pairing.py +58 -3
- package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
- package/scripts/pr_verification/__init__.py +1 -0
- package/scripts/pr_verification/branch_refs.py +114 -0
- package/scripts/pr_verification/config/__init__.py +1 -0
- package/scripts/pr_verification/config/constants.py +84 -0
- package/scripts/pr_verification/config/timing.py +5 -0
- package/scripts/pr_verification/github.py +137 -0
- package/scripts/pr_verification/github_api.py +376 -0
- package/scripts/pr_verification/github_labels.py +88 -0
- package/scripts/pr_verification/github_parsing.py +182 -0
- package/scripts/pr_verification/github_transport.py +91 -0
- package/scripts/pr_verification/lock.py +122 -0
- package/scripts/pr_verification/model.py +43 -0
- package/scripts/pr_verification/requirements.txt +1 -0
- package/scripts/test_durable_post_lint.py +97 -0
- package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
- package/scripts/tests/test-audit-hooks.mjs +94 -2
- package/scripts/tests/test_automatic_advisory.py +376 -0
- package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
- package/scripts/tests/test_automatic_advisory_polling.py +216 -0
- package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
- package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
- package/scripts/tests/test_automatic_advisory_settings.py +48 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
- package/scripts/tests/test_closed_pr_label.py +157 -0
- package/scripts/tests/test_codex_compat_materializer.py +189 -6
- package/scripts/tests/test_github_labels.py +101 -0
- package/scripts/tests/test_github_transport.py +125 -0
- package/scripts/tests/test_local_report_publisher.py +396 -0
- package/scripts/tests/test_local_report_publisher_cli.py +85 -0
- package/scripts/tests/test_local_verification.py +348 -0
- package/scripts/tests/test_local_verification_cli.py +98 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
- package/scripts/tests/test_pr_verification_github.py +392 -0
- package/scripts/tests/test_pr_verification_lock.py +168 -0
- package/scripts/tests/test_verification_notice.py +347 -0
- package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
- package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
- package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
- package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
- package/hooks/blocking/destructive_command_blocker.py +0 -2073
- package/hooks/blocking/fable_spawn_gate.py +0 -187
- package/hooks/blocking/luna_fast_mode_gate.py +0 -160
- package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
- package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
- package/hooks/blocking/pii_prevention_blocker.py +0 -365
- package/hooks/blocking/piped_pytest_blocker.py +0 -1223
- package/hooks/blocking/question_to_user_enforcer.py +0 -130
- package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
- package/hooks/blocking/sensitive_file_protector.py +0 -167
- package/hooks/blocking/session_edit_stage_gate.py +0 -684
- package/hooks/blocking/session_handoff_blocker.py +0 -182
- package/hooks/blocking/shell_substitution_blocker.py +0 -350
- package/hooks/blocking/test_ask_user_question_shape.py +0 -69
- package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
- package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
- package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
- package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
- package/hooks/blocking/test_fable_spawn_gate.py +0 -381
- package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
- package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
- package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
- package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
- package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
- package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
- package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
- package/hooks/blocking/test_sensitive_file_protector.py +0 -365
- package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
- package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
- package/hooks/blocking/test_session_handoff_blocker.py +0 -318
- package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
- package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
- package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
- package/hooks/blocking/unscoped_search_blocker.py +0 -391
- package/hooks/blocking/write_existing_file_blocker.py +0 -124
- package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
- package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
- package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
- package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TypeAlias
|
|
7
|
+
|
|
8
|
+
from .check_support import (
|
|
9
|
+
_prepare_check_invocation,
|
|
10
|
+
_record_missing_check,
|
|
11
|
+
_write_logs,
|
|
12
|
+
)
|
|
13
|
+
from .command_runner import run_command as _run_command
|
|
14
|
+
from .config import (
|
|
15
|
+
CLI_CHECK_FINISH_MESSAGE_TEMPLATE,
|
|
16
|
+
CLI_CHECK_START_MESSAGE_TEMPLATE,
|
|
17
|
+
COLLECTED_TESTS_PATTERN,
|
|
18
|
+
COLLECTION_ERROR_KIND,
|
|
19
|
+
COLLECTION_STDERR_LOG_SUFFIX,
|
|
20
|
+
COLLECTION_STDOUT_LOG_SUFFIX,
|
|
21
|
+
CRASH_ERROR_KIND,
|
|
22
|
+
FAILED_STATUS,
|
|
23
|
+
INCOMPLETE_STATUS,
|
|
24
|
+
MINIMUM_TESTS_ERROR_KIND,
|
|
25
|
+
NONZERO_EXIT_ERROR_KIND,
|
|
26
|
+
PASSED_STATUS,
|
|
27
|
+
PYTEST_COLLECTION_FLAG,
|
|
28
|
+
PYTEST_QUIET_FLAG,
|
|
29
|
+
SAFE_LOG_NAME_PATTERN,
|
|
30
|
+
SKIPPED_TESTS_ERROR_KIND,
|
|
31
|
+
SKIPPED_TESTS_PATTERN,
|
|
32
|
+
STDERR_LOG_SUFFIX,
|
|
33
|
+
STDOUT_LOG_SUFFIX,
|
|
34
|
+
SUCCESS_EXIT_CODE,
|
|
35
|
+
)
|
|
36
|
+
from .model import (
|
|
37
|
+
CheckLogPaths,
|
|
38
|
+
CheckRecord,
|
|
39
|
+
CheckSpec,
|
|
40
|
+
CommandCapture,
|
|
41
|
+
VerificationManifest,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
ProgressCallback: TypeAlias = Callable[[str], None]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def execute_all_checks(
|
|
48
|
+
manifest: VerificationManifest,
|
|
49
|
+
repository_path: Path,
|
|
50
|
+
base_revision: str,
|
|
51
|
+
logs_directory: Path,
|
|
52
|
+
progress_callback: ProgressCallback | None,
|
|
53
|
+
) -> tuple[CheckRecord, ...]:
|
|
54
|
+
"""Execute every check in the manifest.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
manifest: Checks to execute.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
One record for each check.
|
|
61
|
+
"""
|
|
62
|
+
return tuple(
|
|
63
|
+
_execute_check(
|
|
64
|
+
each_check,
|
|
65
|
+
repository_path,
|
|
66
|
+
base_revision,
|
|
67
|
+
logs_directory,
|
|
68
|
+
progress_callback,
|
|
69
|
+
)
|
|
70
|
+
for each_check in manifest.checks
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _execute_check(
|
|
75
|
+
check: CheckSpec,
|
|
76
|
+
repository_path: Path,
|
|
77
|
+
base_revision: str,
|
|
78
|
+
logs_directory: Path,
|
|
79
|
+
progress_callback: ProgressCallback | None,
|
|
80
|
+
) -> CheckRecord:
|
|
81
|
+
_emit_progress(
|
|
82
|
+
progress_callback,
|
|
83
|
+
CLI_CHECK_START_MESSAGE_TEMPLATE.format(check_id=check.check_id),
|
|
84
|
+
)
|
|
85
|
+
check_log_paths = _build_check_log_paths(check, logs_directory)
|
|
86
|
+
check_directory, all_arguments = _prepare_check_invocation(
|
|
87
|
+
check, repository_path, base_revision
|
|
88
|
+
)
|
|
89
|
+
if not check_directory.is_dir():
|
|
90
|
+
check_record = _record_missing_check(
|
|
91
|
+
check, all_arguments, check_directory, check_log_paths
|
|
92
|
+
)
|
|
93
|
+
return _finish_with_progress(progress_callback, check, check_record)
|
|
94
|
+
check_record = _run_existing_check(
|
|
95
|
+
check, all_arguments, check_directory, check_log_paths
|
|
96
|
+
)
|
|
97
|
+
return _finish_with_progress(progress_callback, check, check_record)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _build_check_log_paths(check: CheckSpec, logs_directory: Path) -> CheckLogPaths:
|
|
101
|
+
safe_check_id = SAFE_LOG_NAME_PATTERN.sub("_", check.check_id)
|
|
102
|
+
return CheckLogPaths(
|
|
103
|
+
logs_directory / (safe_check_id + STDOUT_LOG_SUFFIX),
|
|
104
|
+
logs_directory / (safe_check_id + STDERR_LOG_SUFFIX),
|
|
105
|
+
logs_directory / (safe_check_id + COLLECTION_STDOUT_LOG_SUFFIX),
|
|
106
|
+
logs_directory / (safe_check_id + COLLECTION_STDERR_LOG_SUFFIX),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _run_existing_check(
|
|
111
|
+
check: CheckSpec,
|
|
112
|
+
all_arguments: tuple[str, ...],
|
|
113
|
+
check_directory: Path,
|
|
114
|
+
check_log_paths: CheckLogPaths,
|
|
115
|
+
) -> CheckRecord:
|
|
116
|
+
all_collection_capture = _collect_minimum_tests(
|
|
117
|
+
check,
|
|
118
|
+
all_arguments,
|
|
119
|
+
check_directory,
|
|
120
|
+
check_log_paths.collection_stdout,
|
|
121
|
+
check_log_paths.collection_stderr,
|
|
122
|
+
)
|
|
123
|
+
started_at = time.monotonic()
|
|
124
|
+
command_capture = _run_command(
|
|
125
|
+
all_arguments, check_directory, check.timeout_seconds
|
|
126
|
+
)
|
|
127
|
+
duration_seconds = time.monotonic() - started_at
|
|
128
|
+
return _finish_check(
|
|
129
|
+
check,
|
|
130
|
+
all_arguments,
|
|
131
|
+
check_directory,
|
|
132
|
+
check_log_paths,
|
|
133
|
+
all_collection_capture,
|
|
134
|
+
command_capture,
|
|
135
|
+
duration_seconds,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _finish_with_progress(
|
|
140
|
+
progress_callback: ProgressCallback | None,
|
|
141
|
+
check: CheckSpec,
|
|
142
|
+
check_record: CheckRecord,
|
|
143
|
+
) -> CheckRecord:
|
|
144
|
+
_emit_progress(
|
|
145
|
+
progress_callback,
|
|
146
|
+
CLI_CHECK_FINISH_MESSAGE_TEMPLATE.format(
|
|
147
|
+
check_id=check.check_id, status=check_record.status
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
return check_record
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _emit_progress(
|
|
154
|
+
progress_callback: ProgressCallback | None, progress_message: str
|
|
155
|
+
) -> None:
|
|
156
|
+
if progress_callback is not None:
|
|
157
|
+
progress_callback(progress_message)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _finish_check(
|
|
161
|
+
check: CheckSpec,
|
|
162
|
+
all_arguments: tuple[str, ...],
|
|
163
|
+
check_directory: Path,
|
|
164
|
+
check_log_paths: CheckLogPaths,
|
|
165
|
+
all_collection_capture: tuple[CommandCapture, int | None] | None,
|
|
166
|
+
command_capture: CommandCapture,
|
|
167
|
+
duration_seconds: float,
|
|
168
|
+
) -> CheckRecord:
|
|
169
|
+
_write_logs(
|
|
170
|
+
check_log_paths.stdout,
|
|
171
|
+
check_log_paths.stderr,
|
|
172
|
+
command_capture.stdout_text,
|
|
173
|
+
command_capture.stderr_text,
|
|
174
|
+
)
|
|
175
|
+
return _build_finished_record(
|
|
176
|
+
check,
|
|
177
|
+
all_arguments,
|
|
178
|
+
check_directory,
|
|
179
|
+
check_log_paths,
|
|
180
|
+
all_collection_capture,
|
|
181
|
+
command_capture,
|
|
182
|
+
duration_seconds,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _build_finished_record(
|
|
187
|
+
check: CheckSpec,
|
|
188
|
+
all_arguments: tuple[str, ...],
|
|
189
|
+
check_directory: Path,
|
|
190
|
+
check_log_paths: CheckLogPaths,
|
|
191
|
+
all_collection_capture: tuple[CommandCapture, int | None] | None,
|
|
192
|
+
command_capture: CommandCapture,
|
|
193
|
+
duration_seconds: float,
|
|
194
|
+
) -> CheckRecord:
|
|
195
|
+
status, error_kind, error_message = _classify_record(
|
|
196
|
+
check, all_collection_capture, command_capture
|
|
197
|
+
)
|
|
198
|
+
return CheckRecord(
|
|
199
|
+
check.check_id,
|
|
200
|
+
status,
|
|
201
|
+
all_arguments,
|
|
202
|
+
str(check_directory),
|
|
203
|
+
command_capture.exit_code,
|
|
204
|
+
duration_seconds,
|
|
205
|
+
check_log_paths.stdout,
|
|
206
|
+
check_log_paths.stderr,
|
|
207
|
+
error_kind,
|
|
208
|
+
error_message,
|
|
209
|
+
check.minimum_tests,
|
|
210
|
+
all_collection_capture[1] if all_collection_capture is not None else None,
|
|
211
|
+
all_collection_capture[0].exit_code
|
|
212
|
+
if all_collection_capture is not None
|
|
213
|
+
else None,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _classify_record(
|
|
218
|
+
check: CheckSpec,
|
|
219
|
+
all_collection_capture: tuple[CommandCapture, int | None] | None,
|
|
220
|
+
command_capture: CommandCapture,
|
|
221
|
+
) -> tuple[str, str | None, str | None]:
|
|
222
|
+
status, error_kind, error_message = _classify_command(command_capture)
|
|
223
|
+
if all_collection_capture is None:
|
|
224
|
+
return status, error_kind, error_message
|
|
225
|
+
status, error_kind, error_message = _classify_collection(
|
|
226
|
+
check, all_collection_capture, status, error_kind, error_message
|
|
227
|
+
)
|
|
228
|
+
return _classify_skipped_tests(
|
|
229
|
+
check,
|
|
230
|
+
all_collection_capture,
|
|
231
|
+
command_capture,
|
|
232
|
+
status,
|
|
233
|
+
error_kind,
|
|
234
|
+
error_message,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _collect_minimum_tests(
|
|
239
|
+
check: CheckSpec,
|
|
240
|
+
all_arguments: tuple[str, ...],
|
|
241
|
+
check_directory: Path,
|
|
242
|
+
stdout_log_path: Path,
|
|
243
|
+
stderr_log_path: Path,
|
|
244
|
+
) -> tuple[CommandCapture, int | None] | None:
|
|
245
|
+
if check.minimum_tests is None:
|
|
246
|
+
return None
|
|
247
|
+
collection_arguments = _collection_arguments(all_arguments)
|
|
248
|
+
collection_capture = _run_command(
|
|
249
|
+
collection_arguments, check_directory, check.timeout_seconds
|
|
250
|
+
)
|
|
251
|
+
_write_logs(
|
|
252
|
+
stdout_log_path,
|
|
253
|
+
stderr_log_path,
|
|
254
|
+
collection_capture.stdout_text,
|
|
255
|
+
collection_capture.stderr_text,
|
|
256
|
+
)
|
|
257
|
+
collected_tests = _parse_collected_tests(collection_capture)
|
|
258
|
+
return collection_capture, collected_tests
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _collection_arguments(all_arguments: tuple[str, ...]) -> tuple[str, ...]:
|
|
262
|
+
all_collection_arguments = list(all_arguments)
|
|
263
|
+
if PYTEST_COLLECTION_FLAG not in all_collection_arguments:
|
|
264
|
+
all_collection_arguments.append(PYTEST_COLLECTION_FLAG)
|
|
265
|
+
if PYTEST_QUIET_FLAG not in all_collection_arguments:
|
|
266
|
+
all_collection_arguments.append(PYTEST_QUIET_FLAG)
|
|
267
|
+
return tuple(all_collection_arguments)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _parse_collected_tests(command_capture: CommandCapture) -> int | None:
|
|
271
|
+
all_text = command_capture.stdout_text + command_capture.stderr_text
|
|
272
|
+
test_match = COLLECTED_TESTS_PATTERN.search(all_text)
|
|
273
|
+
if test_match is None:
|
|
274
|
+
return 0 if command_capture.exit_code == 5 else None
|
|
275
|
+
return int(test_match.group("count"))
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _parse_skipped_tests(command_capture: CommandCapture) -> int:
|
|
279
|
+
all_text = command_capture.stdout_text + command_capture.stderr_text
|
|
280
|
+
skipped_match = SKIPPED_TESTS_PATTERN.search(all_text)
|
|
281
|
+
if skipped_match is None:
|
|
282
|
+
return 0
|
|
283
|
+
return int(skipped_match.group("count"))
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _classify_skipped_tests(
|
|
287
|
+
check: CheckSpec,
|
|
288
|
+
all_collection_capture: tuple[CommandCapture, int | None],
|
|
289
|
+
command_capture: CommandCapture,
|
|
290
|
+
status: str,
|
|
291
|
+
error_kind: str | None,
|
|
292
|
+
error_message: str | None,
|
|
293
|
+
) -> tuple[str, str | None, str | None]:
|
|
294
|
+
if status != PASSED_STATUS:
|
|
295
|
+
return status, error_kind, error_message
|
|
296
|
+
if not _has_skipped_required_tests(check, all_collection_capture, command_capture):
|
|
297
|
+
return status, error_kind, error_message
|
|
298
|
+
return INCOMPLETE_STATUS, SKIPPED_TESTS_ERROR_KIND, "Required tests were skipped"
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _has_skipped_required_tests(
|
|
302
|
+
check: CheckSpec,
|
|
303
|
+
all_collection_capture: tuple[CommandCapture, int | None],
|
|
304
|
+
command_capture: CommandCapture,
|
|
305
|
+
) -> bool:
|
|
306
|
+
if check.minimum_tests is None:
|
|
307
|
+
return False
|
|
308
|
+
collection_capture, collected_tests = all_collection_capture
|
|
309
|
+
if collection_capture.exit_code != SUCCESS_EXIT_CODE:
|
|
310
|
+
return False
|
|
311
|
+
if collected_tests is None or collected_tests == 0:
|
|
312
|
+
return False
|
|
313
|
+
return _parse_skipped_tests(command_capture) > 0
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _classify_collection(
|
|
317
|
+
check: CheckSpec,
|
|
318
|
+
all_collection_capture: tuple[CommandCapture, int | None],
|
|
319
|
+
status: str,
|
|
320
|
+
error_kind: str | None,
|
|
321
|
+
error_message: str | None,
|
|
322
|
+
) -> tuple[str, str | None, str | None]:
|
|
323
|
+
command_capture, collected_tests = all_collection_capture
|
|
324
|
+
if (
|
|
325
|
+
check.minimum_tests is not None
|
|
326
|
+
and collected_tests is not None
|
|
327
|
+
and collected_tests < check.minimum_tests
|
|
328
|
+
):
|
|
329
|
+
return FAILED_STATUS, MINIMUM_TESTS_ERROR_KIND, "Minimum test count was not met"
|
|
330
|
+
if status == FAILED_STATUS:
|
|
331
|
+
return status, error_kind, error_message
|
|
332
|
+
if command_capture.error_kind is not None:
|
|
333
|
+
return INCOMPLETE_STATUS, COLLECTION_ERROR_KIND, command_capture.error_message
|
|
334
|
+
if command_capture.exit_code != SUCCESS_EXIT_CODE:
|
|
335
|
+
return INCOMPLETE_STATUS, COLLECTION_ERROR_KIND, "Pytest collection failed"
|
|
336
|
+
if collected_tests is None:
|
|
337
|
+
return (
|
|
338
|
+
INCOMPLETE_STATUS,
|
|
339
|
+
COLLECTION_ERROR_KIND,
|
|
340
|
+
"Pytest collection count is missing",
|
|
341
|
+
)
|
|
342
|
+
return status, error_kind, error_message
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _classify_command(
|
|
346
|
+
command_capture: CommandCapture,
|
|
347
|
+
) -> tuple[str, str | None, str | None]:
|
|
348
|
+
if command_capture.error_kind is not None:
|
|
349
|
+
return (
|
|
350
|
+
INCOMPLETE_STATUS,
|
|
351
|
+
command_capture.error_kind,
|
|
352
|
+
command_capture.error_message,
|
|
353
|
+
)
|
|
354
|
+
if command_capture.exit_code is None:
|
|
355
|
+
return INCOMPLETE_STATUS, CRASH_ERROR_KIND, "Command ended without an exit code"
|
|
356
|
+
if command_capture.exit_code < 0:
|
|
357
|
+
return INCOMPLETE_STATUS, CRASH_ERROR_KIND, "Command ended from a signal"
|
|
358
|
+
if command_capture.exit_code != SUCCESS_EXIT_CODE:
|
|
359
|
+
return (
|
|
360
|
+
FAILED_STATUS,
|
|
361
|
+
NONZERO_EXIT_ERROR_KIND,
|
|
362
|
+
"Command returned a nonzero exit code",
|
|
363
|
+
)
|
|
364
|
+
return PASSED_STATUS, None, None
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .config import (
|
|
7
|
+
BASE_PLACEHOLDER,
|
|
8
|
+
CDE_LINT_PLACEHOLDER,
|
|
9
|
+
INCOMPLETE_STATUS,
|
|
10
|
+
PYTHON_PLACEHOLDER,
|
|
11
|
+
REPOSITORY_PLACEHOLDER,
|
|
12
|
+
REPOSITORY_POLICY_PLACEHOLDER,
|
|
13
|
+
START_ERROR_KIND,
|
|
14
|
+
UTF8_ENCODING,
|
|
15
|
+
)
|
|
16
|
+
from .model import CheckLogPaths, CheckRecord, CheckSpec
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _prepare_check_invocation(
|
|
20
|
+
check: CheckSpec, repository_path: Path, base_revision: str
|
|
21
|
+
) -> tuple[Path, tuple[str, ...]]:
|
|
22
|
+
check_directory = repository_path / check.cwd
|
|
23
|
+
all_arguments = _substitute_arguments(
|
|
24
|
+
check.command_arguments, repository_path, base_revision
|
|
25
|
+
)
|
|
26
|
+
return check_directory, all_arguments
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _record_missing_check(
|
|
30
|
+
check: CheckSpec,
|
|
31
|
+
all_arguments: tuple[str, ...],
|
|
32
|
+
check_directory: Path,
|
|
33
|
+
check_log_paths: CheckLogPaths,
|
|
34
|
+
) -> CheckRecord:
|
|
35
|
+
_write_logs(
|
|
36
|
+
check_log_paths.stdout, check_log_paths.stderr, "", "Missing check directory"
|
|
37
|
+
)
|
|
38
|
+
return _incomplete_record(
|
|
39
|
+
check,
|
|
40
|
+
all_arguments,
|
|
41
|
+
check_directory,
|
|
42
|
+
check_log_paths.stdout,
|
|
43
|
+
check_log_paths.stderr,
|
|
44
|
+
START_ERROR_KIND,
|
|
45
|
+
"Check cwd does not exist",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _substitute_arguments(
|
|
50
|
+
all_arguments: tuple[str, ...], repository_path: Path, base_revision: str
|
|
51
|
+
) -> tuple[str, ...]:
|
|
52
|
+
package_root = Path(__file__).resolve().parents[2]
|
|
53
|
+
replacement_by_placeholder = {
|
|
54
|
+
PYTHON_PLACEHOLDER: sys.executable,
|
|
55
|
+
REPOSITORY_PLACEHOLDER: str(repository_path),
|
|
56
|
+
BASE_PLACEHOLDER: base_revision,
|
|
57
|
+
CDE_LINT_PLACEHOLDER: str(package_root / "scripts" / "cde_lint.py"),
|
|
58
|
+
REPOSITORY_POLICY_PLACEHOLDER: str(
|
|
59
|
+
package_root / "scripts" / "repository_policy.py"
|
|
60
|
+
),
|
|
61
|
+
}
|
|
62
|
+
return tuple(
|
|
63
|
+
replacement_by_placeholder.get(each_argument, each_argument)
|
|
64
|
+
for each_argument in all_arguments
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _write_logs(
|
|
69
|
+
stdout_log_path: Path,
|
|
70
|
+
stderr_log_path: Path,
|
|
71
|
+
stdout_text: str,
|
|
72
|
+
stderr_text: str,
|
|
73
|
+
) -> None:
|
|
74
|
+
stdout_log_path.write_text(stdout_text, encoding=UTF8_ENCODING)
|
|
75
|
+
stderr_log_path.write_text(stderr_text, encoding=UTF8_ENCODING)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _incomplete_record(
|
|
79
|
+
check: CheckSpec,
|
|
80
|
+
all_arguments: tuple[str, ...],
|
|
81
|
+
check_directory: Path,
|
|
82
|
+
stdout_log_path: Path,
|
|
83
|
+
stderr_log_path: Path,
|
|
84
|
+
error_kind: str,
|
|
85
|
+
error_message: str,
|
|
86
|
+
) -> CheckRecord:
|
|
87
|
+
return CheckRecord(
|
|
88
|
+
check.check_id,
|
|
89
|
+
INCOMPLETE_STATUS,
|
|
90
|
+
all_arguments,
|
|
91
|
+
str(check_directory),
|
|
92
|
+
None,
|
|
93
|
+
0.0,
|
|
94
|
+
stdout_log_path,
|
|
95
|
+
stderr_log_path,
|
|
96
|
+
error_kind,
|
|
97
|
+
error_message,
|
|
98
|
+
check.minimum_tests,
|
|
99
|
+
)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Never, TextIO
|
|
8
|
+
|
|
9
|
+
scripts_directory = Path(__file__).resolve().parents[1]
|
|
10
|
+
if str(scripts_directory) not in sys.path:
|
|
11
|
+
sys.path.insert(0, str(scripts_directory))
|
|
12
|
+
|
|
13
|
+
from local_verification.config import (
|
|
14
|
+
CLI_AGGREGATE_MESSAGE_TEMPLATE,
|
|
15
|
+
CLI_ELIGIBILITY_MESSAGE_TEMPLATE,
|
|
16
|
+
CLI_FAILURE_MESSAGE_TEMPLATE,
|
|
17
|
+
CLI_REPORT_MESSAGE_TEMPLATE,
|
|
18
|
+
CLI_REVISION_MESSAGE_TEMPLATE,
|
|
19
|
+
CLI_START_MESSAGE_TEMPLATE,
|
|
20
|
+
INCOMPLETE_EXIT_CODE,
|
|
21
|
+
INVALID_INPUT_EXIT_CODE,
|
|
22
|
+
PASSED_STATUS,
|
|
23
|
+
REPORT_NEWLINE,
|
|
24
|
+
UNRESOLVED_REVISION,
|
|
25
|
+
)
|
|
26
|
+
from local_verification.manifest import ManifestRunFatal, load_manifest
|
|
27
|
+
from local_verification.model import VerificationManifest, VerificationReport
|
|
28
|
+
from local_verification.runner import run_verification
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _VerificationArgumentParser(argparse.ArgumentParser):
|
|
32
|
+
def error(self, message: str) -> Never:
|
|
33
|
+
raise ManifestRunFatal(message)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main(
|
|
37
|
+
all_arguments: Sequence[str],
|
|
38
|
+
*,
|
|
39
|
+
stdout: TextIO = sys.stdout,
|
|
40
|
+
stderr: TextIO = sys.stderr,
|
|
41
|
+
) -> int:
|
|
42
|
+
"""Run the local verification manifest.
|
|
43
|
+
Args:
|
|
44
|
+
all_arguments: Command arguments.
|
|
45
|
+
Returns:
|
|
46
|
+
The verifier exit code.
|
|
47
|
+
"""
|
|
48
|
+
parser = _build_parser()
|
|
49
|
+
try:
|
|
50
|
+
parsed_arguments = parser.parse_args(all_arguments)
|
|
51
|
+
except ManifestRunFatal as error:
|
|
52
|
+
stderr.write(f"{error}{REPORT_NEWLINE}")
|
|
53
|
+
return INVALID_INPUT_EXIT_CODE
|
|
54
|
+
report_path = Path(parsed_arguments.output)
|
|
55
|
+
if not _clear_previous_report(report_path, stderr):
|
|
56
|
+
return INCOMPLETE_EXIT_CODE
|
|
57
|
+
_write_progress(stdout, CLI_START_MESSAGE_TEMPLATE.format(report_path=report_path))
|
|
58
|
+
manifest = _load_manifest_for_cli(parsed_arguments.manifest, stderr)
|
|
59
|
+
if manifest is None:
|
|
60
|
+
return INVALID_INPUT_EXIT_CODE
|
|
61
|
+
report = _run_manifest_verification(manifest, parsed_arguments, stdout, stderr)
|
|
62
|
+
if report is None:
|
|
63
|
+
return INCOMPLETE_EXIT_CODE
|
|
64
|
+
_write_report_summary(report, report_path, stdout, stderr)
|
|
65
|
+
return report.exit_code
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
69
|
+
parser = _VerificationArgumentParser(prog="cde verify", add_help=True)
|
|
70
|
+
parser.add_argument("--manifest", required=True)
|
|
71
|
+
parser.add_argument("--repo", dest="repository", required=True)
|
|
72
|
+
parser.add_argument("--base", dest="base_revision", required=True)
|
|
73
|
+
parser.add_argument("--output", required=True)
|
|
74
|
+
return parser
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _run_manifest_verification(
|
|
78
|
+
manifest: VerificationManifest,
|
|
79
|
+
parsed_arguments: argparse.Namespace,
|
|
80
|
+
stdout: TextIO,
|
|
81
|
+
stderr: TextIO,
|
|
82
|
+
) -> VerificationReport | None:
|
|
83
|
+
try:
|
|
84
|
+
return run_verification(
|
|
85
|
+
manifest,
|
|
86
|
+
Path(parsed_arguments.repository),
|
|
87
|
+
parsed_arguments.base_revision,
|
|
88
|
+
Path(parsed_arguments.output),
|
|
89
|
+
progress_callback=lambda progress_message: _write_progress(
|
|
90
|
+
stdout, progress_message
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
except (OSError, ValueError) as error:
|
|
94
|
+
stderr.write(f"{error}{REPORT_NEWLINE}")
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _clear_previous_report(report_path: Path, stderr: TextIO) -> bool:
|
|
99
|
+
try:
|
|
100
|
+
report_path.unlink(missing_ok=True)
|
|
101
|
+
except OSError as error:
|
|
102
|
+
stderr.write(f"{error}{REPORT_NEWLINE}")
|
|
103
|
+
return False
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _write_progress(progress_stream: TextIO, progress_message: str) -> None:
|
|
108
|
+
progress_stream.write(f"{progress_message}{REPORT_NEWLINE}")
|
|
109
|
+
progress_stream.flush()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _write_report_summary(
|
|
113
|
+
report: VerificationReport,
|
|
114
|
+
report_path: Path,
|
|
115
|
+
stdout: TextIO,
|
|
116
|
+
stderr: TextIO,
|
|
117
|
+
) -> None:
|
|
118
|
+
_write_progress(
|
|
119
|
+
stdout, CLI_AGGREGATE_MESSAGE_TEMPLATE.format(status=report.aggregate_status)
|
|
120
|
+
)
|
|
121
|
+
_write_progress(
|
|
122
|
+
stdout,
|
|
123
|
+
CLI_REVISION_MESSAGE_TEMPLATE.format(
|
|
124
|
+
head=_format_revision(report.head_revision),
|
|
125
|
+
base=_format_revision(report.base_revision),
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
_write_progress(
|
|
129
|
+
stdout,
|
|
130
|
+
CLI_ELIGIBILITY_MESSAGE_TEMPLATE.format(
|
|
131
|
+
worktree_clean=str(report.worktree_clean).lower(),
|
|
132
|
+
inputs_unchanged=str(report.inputs_unchanged).lower(),
|
|
133
|
+
publishable=str(report.publishable).lower(),
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
_write_progress(stdout, CLI_REPORT_MESSAGE_TEMPLATE.format(report_path=report_path))
|
|
137
|
+
if report.aggregate_status != PASSED_STATUS:
|
|
138
|
+
stderr.write(
|
|
139
|
+
f"{CLI_FAILURE_MESSAGE_TEMPLATE.format(status=report.aggregate_status)}{REPORT_NEWLINE}"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _format_revision(revision: str | None) -> str:
|
|
144
|
+
return revision or UNRESOLVED_REVISION
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _load_manifest_for_cli(
|
|
148
|
+
manifest_name: str, stderr: TextIO
|
|
149
|
+
) -> VerificationManifest | None:
|
|
150
|
+
try:
|
|
151
|
+
return load_manifest(Path(manifest_name))
|
|
152
|
+
except (ManifestRunFatal, OSError) as error:
|
|
153
|
+
stderr.write(f"{error}{REPORT_NEWLINE}")
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .config import (
|
|
7
|
+
MISSING_TOOL_ERROR_KIND,
|
|
8
|
+
START_ERROR_KIND,
|
|
9
|
+
TIMEOUT_ERROR_KIND,
|
|
10
|
+
UTF8_ENCODING,
|
|
11
|
+
)
|
|
12
|
+
from .model import CommandCapture
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run_command(
|
|
16
|
+
all_arguments: tuple[str, ...], check_directory: Path, timeout_seconds: float
|
|
17
|
+
) -> CommandCapture:
|
|
18
|
+
"""Run a local command and return its captured state.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
all_arguments: Command and its arguments.
|
|
22
|
+
check_directory: Working directory for the command.
|
|
23
|
+
timeout_seconds: Wall-clock limit for the command.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
The captured exit code, output, and error kind.
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
completed_process = _run_subprocess(
|
|
30
|
+
all_arguments, check_directory, timeout_seconds
|
|
31
|
+
)
|
|
32
|
+
except FileNotFoundError as error:
|
|
33
|
+
return CommandCapture(None, "", "", MISSING_TOOL_ERROR_KIND, str(error))
|
|
34
|
+
except subprocess.TimeoutExpired as error:
|
|
35
|
+
return _timeout_capture(error)
|
|
36
|
+
except (OSError, ValueError) as error:
|
|
37
|
+
return CommandCapture(None, "", "", START_ERROR_KIND, str(error))
|
|
38
|
+
return _completed_capture(completed_process)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _timeout_capture(error: subprocess.TimeoutExpired) -> CommandCapture:
|
|
42
|
+
return CommandCapture(
|
|
43
|
+
None,
|
|
44
|
+
_decode_command_text(error.stdout),
|
|
45
|
+
_decode_command_text(error.stderr),
|
|
46
|
+
TIMEOUT_ERROR_KIND,
|
|
47
|
+
"Command exceeded its timeout",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _completed_capture(
|
|
52
|
+
completed_process: subprocess.CompletedProcess[str],
|
|
53
|
+
) -> CommandCapture:
|
|
54
|
+
return CommandCapture(
|
|
55
|
+
completed_process.returncode,
|
|
56
|
+
completed_process.stdout,
|
|
57
|
+
completed_process.stderr,
|
|
58
|
+
None,
|
|
59
|
+
None,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _run_subprocess(
|
|
64
|
+
all_arguments: tuple[str, ...], check_directory: Path, timeout_seconds: float
|
|
65
|
+
) -> subprocess.CompletedProcess[str]:
|
|
66
|
+
return subprocess.run(
|
|
67
|
+
all_arguments,
|
|
68
|
+
cwd=check_directory,
|
|
69
|
+
shell=False,
|
|
70
|
+
check=False,
|
|
71
|
+
capture_output=True,
|
|
72
|
+
text=True,
|
|
73
|
+
encoding=UTF8_ENCODING,
|
|
74
|
+
errors="replace",
|
|
75
|
+
timeout=timeout_seconds,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _decode_command_text(command_text: str | bytes | None) -> str:
|
|
80
|
+
if command_text is None:
|
|
81
|
+
return ""
|
|
82
|
+
if isinstance(command_text, bytes):
|
|
83
|
+
return command_text.decode(UTF8_ENCODING, errors="replace")
|
|
84
|
+
return command_text
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .constants import *
|