Repository navigation
Expand file tree
/
Copy pathcheck_precommit_hooks_executed_test.py
More file actions
232 lines (173 loc) · 9.42 KB
/
Copy pathcheck_precommit_hooks_executed_test.py
File metadata and controls
232 lines (173 loc) · 9.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# Copyright 2025-2026 mrveiss
# SPDX-License-Identifier: Apache-2.0
"""Tests for the pre-commit could-not-run gate (#14181)."""
from __future__ import annotations
import importlib.util
from pathlib import Path
import pytest
_MODULE = Path(__file__).with_name("check_precommit_hooks_executed.py")
@pytest.fixture
def gate():
spec = importlib.util.spec_from_file_location("check_precommit_hooks_executed", _MODULE)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
# Hook display names are assembled, not written verbatim: several repo lint
# rules are line-based scans of raw source text, so a fixture quoting a real
# hook's name trips the rule that hook enforces. This file did exactly that on
# its first push -- `no-utcnow-isoformat` flagged a fixture line containing
# the tz-naive timestamp call it bans, which was sample *output*, not code. Same
# defect #14202 hit one PR earlier; see test_this_file_does_not_trip_a_sibling_lint.
_UTCNOW_HOOK_NAME = "Block datetime." + "utcnow()." + "isoformat() regressions"
_FINDINGS_ONLY = f"""\
black....................................................................Failed
- hook id: black
- files were modified by this hook
{_UTCNOW_HOOK_NAME}.....................................Failed
trim trailing whitespace.................................................Failed
Require encoding= on text-mode open()....................................Passed
"""
_CANNOT_RUN = f"""\
{_UTCNOW_HOOK_NAME}.....................................Failed
Executable `tools/lint/check_no_utcnow_isoformat.py` is not executable
"""
def test_findings_produce_no_unrunnable_hooks(gate):
"""Findings are not inability-to-run.
Renamed after review: the old name promised that findings "do not fail",
but the body only checked the helper. The end-to-end claim is covered by
`test_findings_only_output_passes_end_to_end`; this one covers the parse.
"""
assert gate.unrunnable_hooks(_FINDINGS_ONLY) == []
def test_a_hook_that_could_not_run_is_detected(gate):
assert gate.unrunnable_hooks(_CANNOT_RUN) == ["tools/lint/check_no_utcnow_isoformat.py"]
def test_the_not_found_wording_is_also_detected(gate):
"""pre-commit says `not found` when the entry is a bare name off $PATH."""
out = "myhook.................Failed\nExecutable `tools/lint/gone.py` not found\n"
assert gate.unrunnable_hooks(out) == ["tools/lint/gone.py"]
def test_a_known_dormant_hook_is_tolerated(gate, tmp_path, monkeypatch):
"""A baselined hook that cannot run is tolerated rather than blocking.
The fixture is synthetic on purpose. This used to seed itself from the live
``_KNOWN_DORMANT`` and broke with ``StopIteration`` when #15750 fixed the
last entry and the baseline emptied -- the suite failing because the guard
had SUCCEEDED, where the cheapest way back to green was to re-break an exec
bit. The tolerance behaviour is what this test is about, and it exists
whether or not the backlog currently has anyone in it. See #15762.
"""
dormant = "tools/lint/a_dormant_hook_for_this_test.py"
monkeypatch.setattr(gate, "_KNOWN_DORMANT", frozenset({dormant}))
log = tmp_path / "out.txt"
log.write_text(f"x...Failed\nExecutable `{dormant}` is not executable\n", encoding="utf-8")
assert gate.main([str(log)]) == 0
def test_an_unknown_hook_that_cannot_run_fails(gate, tmp_path):
"""The whole point: a hook outside the tracked backlog silently not running
is the fail-open #14181 exists to remove."""
log = tmp_path / "out.txt"
log.write_text("x...Failed\nExecutable `tools/lint/brand_new_hook.py` is not executable\n", encoding="utf-8")
assert gate.main([str(log)]) == 1
def test_findings_only_output_passes_end_to_end(gate, tmp_path):
log = tmp_path / "out.txt"
log.write_text(_FINDINGS_ONLY, encoding="utf-8")
assert gate.main([str(log)]) == 0
def test_the_tolerated_set_is_imported_not_duplicated(gate):
"""A second copy of the dormant list is a second thing to go stale (#14202)."""
import importlib.util as _ilu
spec = _ilu.spec_from_file_location("check_hook_exec_bits", _MODULE.with_name("check_hook_exec_bits.py"))
exec_bits = _ilu.module_from_spec(spec)
spec.loader.exec_module(exec_bits)
assert gate._KNOWN_DORMANT is exec_bits._KNOWN_DORMANT or gate._KNOWN_DORMANT == exec_bits._KNOWN_DORMANT
source = _MODULE.read_text(encoding="utf-8")
assert "from check_hook_exec_bits import _KNOWN_DORMANT" in source, "the list must be imported, not re-listed"
def test_this_file_does_not_trip_a_sibling_lint(gate) -> None:
"""A fixture quoting a banned pattern violates the rule that bans it.
#14202 shipped this defect and had it caught in review; this file then
reproduced it one PR later, with a fixture line containing
the tz-naive timestamp call that rule bans, as sample *output*. Line-based
checkers cannot tell a fixture from code, and the natural way to write
realistic sample output is the way that breaks.
Asserted rather than remembered.
"""
import subprocess # nosec B404 # fixed argv, no shell
checker = Path(__file__).resolve().parents[1] / "tools/lint/check_no_utcnow_isoformat.py"
if not checker.is_file(): # pragma: no cover - checker moved
pytest.skip("check_no_utcnow_isoformat.py not present")
result = subprocess.run( # nosec B603 B607 # fixed argv, no shell
["python3", str(checker), str(Path(__file__).resolve())],
capture_output=True,
text=True,
)
assert result.returncode == 0, (
"this test file trips no-utcnow-isoformat — assemble banned patterns "
f"from fragments instead of writing them verbatim:\n{result.stdout}{result.stderr}"
)
def test_empty_output_is_fatal_not_clean(gate, tmp_path):
"""No output means no run, and a pass over it is the fail-open this gate removes.
Found while reviewing this PR: with an empty capture the could-not-run scan
finds nothing and the gate reported "every other hook executed", exit 0.
Reachable whenever pre-commit dies before printing — a bootstrap failure, an
unreadable config, an OOM — and the preceding step swallows that too.
"""
log = tmp_path / "out.txt"
log.write_text("", encoding="utf-8")
assert gate.main([str(log)]) == 1
def test_a_crash_AFTER_some_hooks_ran_is_still_fatal(gate, tmp_path):
"""The case the run-happened check alone cannot catch.
pre-commit can print results for several hooks and then die. The output
then contains hook result lines, so the "did anything run" guard is
satisfied, while the run was truncated and the remaining hooks never
executed. Written this way deliberately: an earlier version of this test
passed a crash message with no hook lines at all, which the run-happened
guard caught on its own — so the fatal check was untested and could be
removed with every test still green.
"""
log = tmp_path / "out.txt"
log.write_text(
"black....................................................Passed\n"
"An unexpected error has occurred: KeyError('x')\n",
encoding="utf-8",
)
assert gate.main([str(log)]) == 1
def test_an_invalid_config_after_output_is_fatal(gate, tmp_path):
log = tmp_path / "out.txt"
log.write_text(
"black....................................................Passed\n"
"InvalidConfigError: .pre-commit-config.yaml is not valid\n",
encoding="utf-8",
)
assert gate.main([str(log)]) == 1
def test_output_with_only_passes_is_accepted(gate, tmp_path):
"""The run-happened check must not demand a failure to be satisfied."""
log = tmp_path / "out.txt"
log.write_text("black....................................................Passed\n", encoding="utf-8")
assert gate.main([str(log)]) == 0
def test_a_generic_pre_commit_fatal_after_output_is_caught(gate, tmp_path):
"""pre-commit prints "An error has occurred: <Class>" for its FatalError
subclasses — a repo clone failure, a venv bootstrap failure — and only
"An unexpected error has occurred" for unhandled exceptions.
Review finding: listing three literal strings missed the general prefix, so
a crash after some hooks had already printed satisfied the run-happened
guard and fell through the fatal check.
"""
log = tmp_path / "out.txt"
log.write_text(
"black....................................................Passed\n"
"An error has occurred: FatalError: git clone of hook repo failed\n",
encoding="utf-8",
)
assert gate.main([str(log)]) == 1
def test_the_same_phrase_inside_hook_output_does_not_fail(gate, tmp_path):
"""The fatal patterns are ordinary English, so they are anchored at line start.
pre-commit prints its own errors at column zero; hook output is indented or
prefixed. Unanchored, a lint message quoting the phrase would fail the gate
and block every PR — a false positive here is far worse than the false
negative it replaces.
"""
log = tmp_path / "out.txt"
log.write_text(
"black....................................................Passed\n"
# Capitalised and word-for-word identical to pre-commit's own wording,
# differing ONLY by indentation — otherwise this test passes whether or
# not the pattern is anchored, and the anchor goes untested.
" An error has occurred: while parsing foo.py\n",
encoding="utf-8",
)
assert gate.main([str(log)]) == 0