collab-runtime 0.2.9__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.
- collab/__init__.py +77 -0
- collab/__main__.py +11 -0
- collab_runtime-0.2.9.dist-info/METADATA +218 -0
- collab_runtime-0.2.9.dist-info/RECORD +82 -0
- collab_runtime-0.2.9.dist-info/WHEEL +5 -0
- collab_runtime-0.2.9.dist-info/entry_points.txt +3 -0
- collab_runtime-0.2.9.dist-info/licenses/LICENSE +21 -0
- collab_runtime-0.2.9.dist-info/top_level.txt +10 -0
- scripts/cleanup.py +395 -0
- scripts/collab_git_hook.py +190 -0
- scripts/format_code.py +594 -0
- scripts/generate_tests.py +560 -0
- scripts/validate_code.py +1397 -0
- src/__init__.py +4 -0
- src/dashboard/index.html +1131 -0
- src/live_locks_watcher.py +1982 -0
- src/lock_client.py +4268 -0
- src/logging_config.py +259 -0
- src/main.py +436 -0
- tests/backend/__init__.py +0 -0
- tests/backend/functional/__init__.py +0 -0
- tests/backend/functional/test_package_imports.py +43 -0
- tests/backend/integration/__init__.py +0 -0
- tests/backend/integration/test_cli_contract_parity.py +220 -0
- tests/backend/performance/__init__.py +0 -0
- tests/backend/reliability/__init__.py +0 -0
- tests/backend/security/__init__.py +0 -0
- tests/backend/unit/live_locks_watcher/__init__.py +5 -0
- tests/backend/unit/live_locks_watcher/_helpers.py +123 -0
- tests/backend/unit/live_locks_watcher/conftest.py +18 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_dashboard.py +188 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_developer.py +56 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_graceful_shutdown.py +459 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_main.py +1925 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_module.py +187 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_multi_session.py +320 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_notify.py +67 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_parsing.py +155 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_process_helpers.py +684 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_processing.py +173 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_prompt_abort.py +71 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_reconcile.py +516 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_scan.py +296 -0
- tests/backend/unit/lock_client/__init__.py +1 -0
- tests/backend/unit/lock_client/_helpers.py +132 -0
- tests/backend/unit/lock_client/test_lock_client_acquire.py +214 -0
- tests/backend/unit/lock_client/test_lock_client_active.py +104 -0
- tests/backend/unit/lock_client/test_lock_client_api.py +63 -0
- tests/backend/unit/lock_client/test_lock_client_cli.py +682 -0
- tests/backend/unit/lock_client/test_lock_client_daemon.py +3730 -0
- tests/backend/unit/lock_client/test_lock_client_dashboard.py +438 -0
- tests/backend/unit/lock_client/test_lock_client_discover.py +241 -0
- tests/backend/unit/lock_client/test_lock_client_force_release.py +354 -0
- tests/backend/unit/lock_client/test_lock_client_helper_branches.py +1890 -0
- tests/backend/unit/lock_client/test_lock_client_history.py +301 -0
- tests/backend/unit/lock_client/test_lock_client_isolation.py +316 -0
- tests/backend/unit/lock_client/test_lock_client_pid.py +75 -0
- tests/backend/unit/lock_client/test_lock_client_reconcile.py +464 -0
- tests/backend/unit/lock_client/test_lock_client_release.py +77 -0
- tests/backend/unit/lock_client/test_lock_client_shutdown.py +1110 -0
- tests/backend/unit/lock_client/test_lock_client_utils.py +474 -0
- tests/backend/unit/lock_client/test_lock_client_watch.py +866 -0
- tests/backend/unit/scripts/__init__.py +1 -0
- tests/backend/unit/scripts/_helpers.py +42 -0
- tests/backend/unit/scripts/test_cleanup.py +285 -0
- tests/backend/unit/scripts/test_collab_git_hook.py +280 -0
- tests/backend/unit/scripts/test_collab_git_hook_ported.py +50 -0
- tests/backend/unit/scripts/test_format_code.py +368 -0
- tests/backend/unit/scripts/test_format_code_ported.py +177 -0
- tests/backend/unit/scripts/test_generate_tests.py +305 -0
- tests/backend/unit/scripts/test_hook_templates.py +357 -0
- tests/backend/unit/scripts/test_setup_hook_overlay.py +95 -0
- tests/backend/unit/scripts/test_validate_code.py +867 -0
- tests/backend/unit/scripts/test_validate_code_ported.py +237 -0
- tests/backend/unit/test_entrypoints_main_run.py +83 -0
- tests/backend/unit/test_logging_config.py +529 -0
- tests/backend/unit/test_main_watch_pid_file.py +278 -0
- tests/conftest.py +167 -0
- tests/frontend/__init__.py +0 -0
- tests/frontend/jest/__init__.py +0 -0
- tests/frontend/playwright/__init__.py +0 -0
- tests/packaging/test_smoke_install.py +76 -0
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
"""Tests for scripts/validate_code.py."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import io
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from types import SimpleNamespace
|
|
10
|
+
from unittest.mock import MagicMock
|
|
11
|
+
|
|
12
|
+
from tests.backend.unit.scripts._helpers import load_script_module
|
|
13
|
+
|
|
14
|
+
validate_code = load_script_module("validate_code.py", "validate_code_under_test")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_validate_backend_clears_mypy_cache_on_exists(monkeypatch, tmp_path):
|
|
18
|
+
"""Verify that validate_python_backend clears stale .mypy_cache."""
|
|
19
|
+
# Create a mock mypy_cache directory
|
|
20
|
+
cache_dir = tmp_path / ".mypy_cache"
|
|
21
|
+
cache_dir.mkdir()
|
|
22
|
+
(cache_dir / "test_marker.txt").touch()
|
|
23
|
+
|
|
24
|
+
# Mock Path(".mypy_cache") to return our test directory
|
|
25
|
+
original_path = Path
|
|
26
|
+
|
|
27
|
+
def mock_path(p):
|
|
28
|
+
if p == ".mypy_cache":
|
|
29
|
+
return cache_dir
|
|
30
|
+
return original_path(p)
|
|
31
|
+
|
|
32
|
+
monkeypatch.setattr(validate_code, "Path", mock_path)
|
|
33
|
+
|
|
34
|
+
# Mock run_command to succeed for all commands
|
|
35
|
+
commands_seen = []
|
|
36
|
+
|
|
37
|
+
def mock_run_command(cmd, *_a, **_k):
|
|
38
|
+
commands_seen.append(cmd)
|
|
39
|
+
return True, ""
|
|
40
|
+
|
|
41
|
+
monkeypatch.setattr(validate_code, "run_command", mock_run_command)
|
|
42
|
+
|
|
43
|
+
# Run with files=None to trigger full backend validation
|
|
44
|
+
validate_code.validate_python_backend(quick=False, files=None)
|
|
45
|
+
|
|
46
|
+
# Verify cache was removed (directory should not exist anymore)
|
|
47
|
+
assert not cache_dir.exists(), "mypy_cache should have been removed"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_validate_backend_handles_cache_cleanup_error(monkeypatch, capsys):
|
|
51
|
+
"""Verify graceful handling when cache cleanup fails."""
|
|
52
|
+
mock_cache = MagicMock()
|
|
53
|
+
mock_cache.exists.return_value = True
|
|
54
|
+
|
|
55
|
+
def mock_path(p):
|
|
56
|
+
if p == ".mypy_cache":
|
|
57
|
+
return mock_cache
|
|
58
|
+
return Path(p)
|
|
59
|
+
|
|
60
|
+
def mock_rmtree(path):
|
|
61
|
+
raise OSError("Permission denied")
|
|
62
|
+
|
|
63
|
+
monkeypatch.setattr(validate_code, "Path", mock_path)
|
|
64
|
+
monkeypatch.setattr(validate_code.shutil, "rmtree", mock_rmtree)
|
|
65
|
+
|
|
66
|
+
commands_seen = []
|
|
67
|
+
|
|
68
|
+
def mock_run_command(cmd, *_a, **_k):
|
|
69
|
+
commands_seen.append(cmd)
|
|
70
|
+
return True, ""
|
|
71
|
+
|
|
72
|
+
monkeypatch.setattr(validate_code, "run_command", mock_run_command)
|
|
73
|
+
|
|
74
|
+
# Should not crash even if cache removal fails
|
|
75
|
+
validate_code.validate_python_backend(quick=False, files=None)
|
|
76
|
+
|
|
77
|
+
# Verify warning was printed
|
|
78
|
+
captured = capsys.readouterr()
|
|
79
|
+
assert "Could not remove .mypy_cache" in captured.out or "Could not remove" in str(
|
|
80
|
+
captured
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_format_failure_output_pytest_sections():
|
|
85
|
+
noisy_stdout = "\n".join(
|
|
86
|
+
[f"test_{index:03d} PASSED" for index in range(60)]
|
|
87
|
+
+ [
|
|
88
|
+
"============================= FAILURES =============================",
|
|
89
|
+
"__________________________ test_example ___________________________",
|
|
90
|
+
"E AssertionError: assert 1 == 2",
|
|
91
|
+
"====================== short test summary info ======================",
|
|
92
|
+
"FAILED tests/backend/unit/test_example.py::test_example - AssertionError",
|
|
93
|
+
]
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
formatted = validate_code.format_failure_output(noisy_stdout, "")
|
|
97
|
+
assert "Pytest short summary" in formatted
|
|
98
|
+
assert "FAILED tests/backend/unit/test_example.py::test_example" in formatted
|
|
99
|
+
assert "AssertionError" in formatted
|
|
100
|
+
assert "test_000 PASSED" not in formatted
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_format_failure_output_generic_fallback():
|
|
104
|
+
stdout = "\n".join([f"line {index}" for index in range(220)])
|
|
105
|
+
formatted = validate_code.format_failure_output(stdout, "")
|
|
106
|
+
assert "First lines" in formatted
|
|
107
|
+
assert "Last lines" in formatted
|
|
108
|
+
assert "line 0" in formatted
|
|
109
|
+
assert "line 219" in formatted
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_python_module_fallback_command_maps_known_tools():
|
|
113
|
+
cmd = validate_code._python_module_fallback_command(["ruff", "check", "src"])
|
|
114
|
+
assert cmd is not None
|
|
115
|
+
assert cmd[0].lower() == validate_code.sys.executable.lower()
|
|
116
|
+
assert cmd[1:3] == ["-m", "ruff"]
|
|
117
|
+
|
|
118
|
+
diff_cmd = validate_code._python_module_fallback_command(
|
|
119
|
+
["diff-cover", "coverage.xml"]
|
|
120
|
+
)
|
|
121
|
+
assert diff_cmd is not None
|
|
122
|
+
assert diff_cmd[0].lower() == validate_code.sys.executable.lower()
|
|
123
|
+
assert diff_cmd[1:3] == ["-m", "diff_cover.diff_cover_tool"]
|
|
124
|
+
|
|
125
|
+
assert validate_code._python_module_fallback_command(["unknown-tool"]) is None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_run_command_merges_env_and_windows_roots(monkeypatch):
|
|
129
|
+
captured = {}
|
|
130
|
+
|
|
131
|
+
def _fake_run(*_args, **kwargs):
|
|
132
|
+
captured["env"] = kwargs["env"]
|
|
133
|
+
return SimpleNamespace(returncode=0, stdout="ok", stderr="")
|
|
134
|
+
|
|
135
|
+
monkeypatch.setattr(validate_code.subprocess, "run", _fake_run)
|
|
136
|
+
monkeypatch.setenv("SYSTEMDRIVE", "C:")
|
|
137
|
+
monkeypatch.setenv("PROGRAMDATA", r"C:\ProgramData")
|
|
138
|
+
|
|
139
|
+
success, _ = validate_code.run_command(
|
|
140
|
+
["python", "-V"],
|
|
141
|
+
"python version",
|
|
142
|
+
check=False,
|
|
143
|
+
env={"CUSTOM_FLAG": "enabled"},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
assert success is True
|
|
147
|
+
assert captured["env"]["CUSTOM_FLAG"] == "enabled"
|
|
148
|
+
assert captured["env"]["SYSTEMDRIVE"] == "C:"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_configure_coverage_data_file_skips_ci_environments(monkeypatch):
|
|
152
|
+
"""Verify that CI environment routing is skipped when CI env vars are set."""
|
|
153
|
+
# Clear COVERAGE_FILE if it exists
|
|
154
|
+
monkeypatch.delenv("COVERAGE_FILE", raising=False)
|
|
155
|
+
|
|
156
|
+
# Set GITHUB_ACTIONS to simulate GitHub Actions CI
|
|
157
|
+
monkeypatch.setenv("GITHUB_ACTIONS", "true")
|
|
158
|
+
|
|
159
|
+
# Call the function — should return early due to CI detection
|
|
160
|
+
validate_code._configure_coverage_data_file()
|
|
161
|
+
|
|
162
|
+
# Verify COVERAGE_FILE was NOT set (early return due to CI check)
|
|
163
|
+
assert validate_code.os.getenv("COVERAGE_FILE") is None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_configure_coverage_data_file_applies_temp_routing_locally(monkeypatch):
|
|
167
|
+
"""Verify temp directory routing is applied when NOT in CI."""
|
|
168
|
+
# Clear CI/GITHUB_ACTIONS and COVERAGE_FILE
|
|
169
|
+
monkeypatch.delenv("COVERAGE_FILE", raising=False)
|
|
170
|
+
monkeypatch.delenv("CI", raising=False)
|
|
171
|
+
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
|
172
|
+
|
|
173
|
+
# Call the function — should apply temp directory routing
|
|
174
|
+
validate_code._configure_coverage_data_file()
|
|
175
|
+
|
|
176
|
+
# Verify COVERAGE_FILE was set (temp directory routing applied)
|
|
177
|
+
assert validate_code.os.getenv("COVERAGE_FILE") is not None
|
|
178
|
+
assert "collab" in validate_code.os.getenv("COVERAGE_FILE")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_run_command_uses_python_module_resolution(monkeypatch):
|
|
182
|
+
calls = []
|
|
183
|
+
|
|
184
|
+
def _fake_run(cmd, **_kwargs):
|
|
185
|
+
calls.append(cmd)
|
|
186
|
+
return MagicMock(returncode=0, stdout="ok", stderr="")
|
|
187
|
+
|
|
188
|
+
monkeypatch.setattr(validate_code.subprocess, "run", _fake_run)
|
|
189
|
+
|
|
190
|
+
success, output = validate_code.run_command(
|
|
191
|
+
["ruff", "check", "src"],
|
|
192
|
+
"Ruff linting",
|
|
193
|
+
check=False,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
assert success is True
|
|
197
|
+
assert output == "ok"
|
|
198
|
+
assert len(calls) == 1
|
|
199
|
+
# Should use python -m resolution pre-emptively
|
|
200
|
+
assert calls[0][0].lower() == validate_code.sys.executable.lower()
|
|
201
|
+
assert calls[0][1:3] == ["-m", "ruff"]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_get_changed_files_collects_all_three_sources(monkeypatch):
|
|
205
|
+
payloads = [
|
|
206
|
+
"src/main.py\n",
|
|
207
|
+
"scripts/validate_code.py\n",
|
|
208
|
+
"new_file.py\n",
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
def _run(*_a, **_k):
|
|
212
|
+
return SimpleNamespace(returncode=0, stdout=payloads.pop(0), stderr="")
|
|
213
|
+
|
|
214
|
+
monkeypatch.setattr(validate_code.subprocess, "run", _run)
|
|
215
|
+
changed = validate_code._get_changed_files()
|
|
216
|
+
assert "src/main.py" in changed
|
|
217
|
+
assert "scripts/validate_code.py" in changed
|
|
218
|
+
assert "new_file.py" in changed
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_expand_input_paths_for_file_and_directory(tmp_path):
|
|
222
|
+
pkg = tmp_path / "pkg"
|
|
223
|
+
pkg.mkdir(parents=True)
|
|
224
|
+
f = pkg / "mod.py"
|
|
225
|
+
f.write_text("x = 1\n", encoding="utf-8")
|
|
226
|
+
|
|
227
|
+
expanded = validate_code._expand_input_paths([str(pkg), str(f), "raw\\x.py"])
|
|
228
|
+
assert any(p.endswith("pkg/mod.py") for p in expanded)
|
|
229
|
+
assert any(p.endswith("x.py") for p in expanded)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_detect_changed_scopes_branches(monkeypatch):
|
|
233
|
+
monkeypatch.setattr(validate_code, "_get_changed_files", lambda: [])
|
|
234
|
+
scopes = validate_code.detect_changed_scopes()
|
|
235
|
+
assert scopes["full_suite"] is True
|
|
236
|
+
assert scopes["changed_files"] == []
|
|
237
|
+
|
|
238
|
+
monkeypatch.setattr(validate_code, "_get_changed_files", lambda: ["pyproject.toml"])
|
|
239
|
+
scopes = validate_code.detect_changed_scopes()
|
|
240
|
+
assert scopes["full_suite"] is True
|
|
241
|
+
assert "Global config changed" in (scopes["reason"] or "")
|
|
242
|
+
|
|
243
|
+
monkeypatch.setattr(
|
|
244
|
+
validate_code,
|
|
245
|
+
"_get_changed_files",
|
|
246
|
+
lambda: ["scripts/validate_code.py"],
|
|
247
|
+
)
|
|
248
|
+
scopes = validate_code.detect_changed_scopes()
|
|
249
|
+
assert scopes["full_suite"] is True
|
|
250
|
+
assert "Infrastructure file changed" in (scopes["reason"] or "")
|
|
251
|
+
|
|
252
|
+
monkeypatch.setattr(
|
|
253
|
+
validate_code,
|
|
254
|
+
"_get_changed_files",
|
|
255
|
+
lambda: ["src/lock_client.py"],
|
|
256
|
+
)
|
|
257
|
+
scopes = validate_code.detect_changed_scopes()
|
|
258
|
+
assert scopes["full_suite"] is False
|
|
259
|
+
assert "tests/backend/unit/" in scopes["backend"]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_validate_python_backend_full_mode(monkeypatch):
|
|
263
|
+
commands = []
|
|
264
|
+
|
|
265
|
+
def _run_command(cmd, *_a, **_k):
|
|
266
|
+
commands.append(cmd)
|
|
267
|
+
return True, ""
|
|
268
|
+
|
|
269
|
+
monkeypatch.setattr(validate_code, "run_command", _run_command)
|
|
270
|
+
monkeypatch.setattr(validate_code.os.path, "exists", lambda p: p == "coverage.xml")
|
|
271
|
+
|
|
272
|
+
assert validate_code.validate_python_backend(quick=False, files=None) is True
|
|
273
|
+
assert any(cmd and cmd[0] == "isort" for cmd in commands)
|
|
274
|
+
assert any(cmd and cmd[0] == "diff-cover" for cmd in commands)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_validate_python_backend_quick_modes(monkeypatch):
|
|
278
|
+
monkeypatch.setattr(validate_code.os.path, "exists", lambda p: p == "coverage.xml")
|
|
279
|
+
|
|
280
|
+
seen = []
|
|
281
|
+
|
|
282
|
+
def _run_command(cmd, *_a, **_k):
|
|
283
|
+
seen.append(cmd)
|
|
284
|
+
# fail only diff-cover validation command
|
|
285
|
+
if cmd[:2] == ["diff-cover", "coverage.xml"]:
|
|
286
|
+
return False, "below threshold"
|
|
287
|
+
return True, ""
|
|
288
|
+
|
|
289
|
+
monkeypatch.setattr(validate_code, "run_command", _run_command)
|
|
290
|
+
monkeypatch.setattr(
|
|
291
|
+
validate_code,
|
|
292
|
+
"detect_changed_scopes",
|
|
293
|
+
lambda *a, **k: {
|
|
294
|
+
"full_suite": False,
|
|
295
|
+
"backend": ["tests/backend/unit/"],
|
|
296
|
+
"frontend": [],
|
|
297
|
+
"reason": None,
|
|
298
|
+
"changed_files": ["src/main.py", "tests/backend/unit/test_x.py"],
|
|
299
|
+
},
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
assert (
|
|
303
|
+
validate_code.validate_python_backend(
|
|
304
|
+
quick=True,
|
|
305
|
+
files=["src/main.py"],
|
|
306
|
+
)
|
|
307
|
+
is False
|
|
308
|
+
)
|
|
309
|
+
assert any("--include" in cmd for cmd in seen if cmd and cmd[0] == "diff-cover")
|
|
310
|
+
|
|
311
|
+
monkeypatch.setattr(
|
|
312
|
+
validate_code,
|
|
313
|
+
"detect_changed_scopes",
|
|
314
|
+
lambda *a, **k: {
|
|
315
|
+
"full_suite": False,
|
|
316
|
+
"backend": [],
|
|
317
|
+
"frontend": [],
|
|
318
|
+
"reason": None,
|
|
319
|
+
"changed_files": [],
|
|
320
|
+
},
|
|
321
|
+
)
|
|
322
|
+
monkeypatch.setattr(validate_code.os.path, "exists", lambda _p: False)
|
|
323
|
+
seen.clear()
|
|
324
|
+
assert (
|
|
325
|
+
validate_code.validate_python_backend(
|
|
326
|
+
quick=True,
|
|
327
|
+
files=["scripts/validate_code.py"],
|
|
328
|
+
)
|
|
329
|
+
is True
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def test_validate_python_backend_diff_cover_soft_skip(monkeypatch):
|
|
334
|
+
monkeypatch.setattr(validate_code.os.path, "exists", lambda p: p == "coverage.xml")
|
|
335
|
+
|
|
336
|
+
def _run_command(cmd, *_a, **_k):
|
|
337
|
+
if cmd == ["diff-cover", "--version"]:
|
|
338
|
+
return False, "missing"
|
|
339
|
+
return True, ""
|
|
340
|
+
|
|
341
|
+
monkeypatch.setattr(validate_code, "run_command", _run_command)
|
|
342
|
+
assert validate_code.validate_python_backend(quick=False, files=None) is True
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def test_validate_others_branches(monkeypatch):
|
|
346
|
+
monkeypatch.setattr(
|
|
347
|
+
validate_code.subprocess,
|
|
348
|
+
"run",
|
|
349
|
+
lambda *_a, **_k: SimpleNamespace(returncode=1),
|
|
350
|
+
)
|
|
351
|
+
assert validate_code.validate_others(files=["docs/readme.md"]) is True
|
|
352
|
+
|
|
353
|
+
calls = []
|
|
354
|
+
|
|
355
|
+
def _run(*_a, **_k):
|
|
356
|
+
return SimpleNamespace(returncode=0)
|
|
357
|
+
|
|
358
|
+
def _cmd(cmd, *_a, **_k):
|
|
359
|
+
calls.append(cmd)
|
|
360
|
+
return True, ""
|
|
361
|
+
|
|
362
|
+
monkeypatch.setattr(validate_code.subprocess, "run", _run)
|
|
363
|
+
monkeypatch.setattr(validate_code, "run_command", _cmd)
|
|
364
|
+
assert validate_code.validate_others(files=["docs/readme.md"]) is True
|
|
365
|
+
assert any(cmd and cmd[0] == "npx" for cmd in calls)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def test_validate_frontend_branches(monkeypatch):
|
|
369
|
+
monkeypatch.setattr(validate_code.shutil, "which", lambda name: None)
|
|
370
|
+
assert validate_code.validate_javascript_frontend(quick=False, files=None) is True
|
|
371
|
+
|
|
372
|
+
monkeypatch.setattr(validate_code.shutil, "which", lambda name: "/usr/bin/npm")
|
|
373
|
+
assert (
|
|
374
|
+
validate_code.validate_javascript_frontend(
|
|
375
|
+
quick=False,
|
|
376
|
+
files=["src/main.py"],
|
|
377
|
+
)
|
|
378
|
+
is True
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
calls = []
|
|
382
|
+
|
|
383
|
+
def _cmd(cmd, *_a, **_k):
|
|
384
|
+
calls.append(cmd)
|
|
385
|
+
return True, ""
|
|
386
|
+
|
|
387
|
+
monkeypatch.setattr(validate_code, "run_command", _cmd)
|
|
388
|
+
assert (
|
|
389
|
+
validate_code.validate_javascript_frontend(
|
|
390
|
+
quick=False,
|
|
391
|
+
files=["src/dashboard/app.js"],
|
|
392
|
+
)
|
|
393
|
+
is True
|
|
394
|
+
)
|
|
395
|
+
assert any(cmd and cmd[0] == "npx" for cmd in calls)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def test_main_exit_codes(monkeypatch):
|
|
399
|
+
monkeypatch.setattr(validate_code, "validate_python_backend", lambda **_k: True)
|
|
400
|
+
monkeypatch.setattr(
|
|
401
|
+
validate_code,
|
|
402
|
+
"validate_javascript_frontend",
|
|
403
|
+
lambda **_k: True,
|
|
404
|
+
)
|
|
405
|
+
monkeypatch.setattr(validate_code, "validate_others", lambda **_k: True)
|
|
406
|
+
monkeypatch.setattr(validate_code, "_run_cleanup", lambda: None)
|
|
407
|
+
|
|
408
|
+
monkeypatch.setattr(validate_code.sys, "argv", ["validate_code.py", "--backend"])
|
|
409
|
+
assert validate_code.main() == 0
|
|
410
|
+
|
|
411
|
+
monkeypatch.setattr(validate_code, "validate_python_backend", lambda **_k: False)
|
|
412
|
+
monkeypatch.setattr(validate_code.sys, "argv", ["validate_code.py", "--backend"])
|
|
413
|
+
assert validate_code.main() == 1
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def test_main_file_filtering_short_circuit(monkeypatch):
|
|
417
|
+
monkeypatch.setattr(validate_code, "_run_cleanup", lambda: None)
|
|
418
|
+
monkeypatch.setattr(
|
|
419
|
+
validate_code.sys,
|
|
420
|
+
"argv",
|
|
421
|
+
["validate_code.py", "README.txt"],
|
|
422
|
+
)
|
|
423
|
+
assert validate_code.main() == 0
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def test_run_cleanup_output_paths(monkeypatch, capsys):
|
|
427
|
+
monkeypatch.setattr(validate_code, "clean_default", lambda dry_run=False: 2)
|
|
428
|
+
monkeypatch.setattr(validate_code, "clean_packaging", lambda dry_run=False: 0)
|
|
429
|
+
validate_code._run_cleanup()
|
|
430
|
+
assert "Removed 2 generated artifact(s)" in capsys.readouterr().out
|
|
431
|
+
|
|
432
|
+
monkeypatch.setattr(validate_code, "clean_default", lambda dry_run=False: 0)
|
|
433
|
+
monkeypatch.setattr(validate_code, "clean_packaging", lambda dry_run=False: 0)
|
|
434
|
+
validate_code._run_cleanup()
|
|
435
|
+
assert "Nothing to clean" in capsys.readouterr().out
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def test_resolve_diff_compare_branch_paths(monkeypatch):
|
|
439
|
+
assert validate_code._resolve_diff_compare_branch(quick=True) == ("HEAD", None)
|
|
440
|
+
|
|
441
|
+
monkeypatch.setattr(validate_code, "_git_ref_exists", lambda ref: ref == "HEAD~1")
|
|
442
|
+
monkeypatch.setattr(validate_code, "_git_remote_origin_exists", lambda: False)
|
|
443
|
+
branch, warning = validate_code._resolve_diff_compare_branch(quick=False)
|
|
444
|
+
assert branch == "HEAD~1"
|
|
445
|
+
assert warning is not None and "HEAD~1" in warning
|
|
446
|
+
|
|
447
|
+
monkeypatch.setattr(validate_code, "_git_ref_exists", lambda _ref: False)
|
|
448
|
+
monkeypatch.setattr(validate_code, "_git_remote_origin_exists", lambda: False)
|
|
449
|
+
branch, warning = validate_code._resolve_diff_compare_branch(quick=False)
|
|
450
|
+
assert branch is None
|
|
451
|
+
assert warning is not None and "Unable to resolve" in warning
|
|
452
|
+
|
|
453
|
+
state: dict[str, bool] = {"fetched": False}
|
|
454
|
+
|
|
455
|
+
def _fake_ref(ref: str) -> bool:
|
|
456
|
+
if ref == "origin/main":
|
|
457
|
+
return state["fetched"]
|
|
458
|
+
if ref in {"main", "HEAD~1"}:
|
|
459
|
+
return False
|
|
460
|
+
return False
|
|
461
|
+
|
|
462
|
+
def _fake_run(cmd, **_kwargs):
|
|
463
|
+
if cmd[:3] == ["git", "fetch", "origin"]:
|
|
464
|
+
state["fetched"] = True
|
|
465
|
+
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
|
466
|
+
|
|
467
|
+
monkeypatch.setattr(validate_code, "_git_ref_exists", _fake_ref)
|
|
468
|
+
monkeypatch.setattr(validate_code, "_git_remote_origin_exists", lambda: True)
|
|
469
|
+
monkeypatch.setattr(validate_code.subprocess, "run", _fake_run)
|
|
470
|
+
branch, warning = validate_code._resolve_diff_compare_branch(quick=False)
|
|
471
|
+
assert branch == "origin/main"
|
|
472
|
+
assert warning is not None and "resolved after fetching" in warning
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def test_validate_backend_diff_cover_unresolved_compare_branch(monkeypatch):
|
|
476
|
+
commands = []
|
|
477
|
+
|
|
478
|
+
def _run_command(cmd, *_a, **_k):
|
|
479
|
+
commands.append(cmd)
|
|
480
|
+
return True, ""
|
|
481
|
+
|
|
482
|
+
monkeypatch.setattr(validate_code, "run_command", _run_command)
|
|
483
|
+
monkeypatch.setattr(validate_code.os.path, "exists", lambda p: p == "coverage.xml")
|
|
484
|
+
monkeypatch.setattr(
|
|
485
|
+
validate_code,
|
|
486
|
+
"_resolve_diff_compare_branch",
|
|
487
|
+
lambda _quick: (None, "missing compare branch"),
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
assert validate_code.validate_python_backend(quick=False, files=None) is False
|
|
491
|
+
assert not any(cmd[:2] == ["diff-cover", "coverage.xml"] for cmd in commands)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def test_validate_backend_diff_cover_warning_and_include_filter(monkeypatch):
|
|
495
|
+
seen = []
|
|
496
|
+
|
|
497
|
+
def _run_command(cmd, *_a, **_k):
|
|
498
|
+
seen.append(cmd)
|
|
499
|
+
return True, ""
|
|
500
|
+
|
|
501
|
+
monkeypatch.setattr(validate_code, "run_command", _run_command)
|
|
502
|
+
monkeypatch.setattr(validate_code.os.path, "exists", lambda p: p == "coverage.xml")
|
|
503
|
+
monkeypatch.setattr(
|
|
504
|
+
validate_code,
|
|
505
|
+
"detect_changed_scopes",
|
|
506
|
+
lambda *a, **k: {
|
|
507
|
+
"full_suite": False,
|
|
508
|
+
"backend": ["tests/backend/unit/"],
|
|
509
|
+
"frontend": [],
|
|
510
|
+
"reason": None,
|
|
511
|
+
"changed_files": ["scripts/validate_code.py", "README.md", "run.py"],
|
|
512
|
+
},
|
|
513
|
+
)
|
|
514
|
+
monkeypatch.setattr(
|
|
515
|
+
validate_code,
|
|
516
|
+
"_resolve_diff_compare_branch",
|
|
517
|
+
lambda _quick: ("HEAD", "branch warning"),
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
assert (
|
|
521
|
+
validate_code.validate_python_backend(
|
|
522
|
+
quick=True, files=["scripts/validate_code.py"]
|
|
523
|
+
)
|
|
524
|
+
is True
|
|
525
|
+
)
|
|
526
|
+
diff_cmd = next(cmd for cmd in seen if cmd[:2] == ["diff-cover", "coverage.xml"])
|
|
527
|
+
assert "--include" in diff_cmd
|
|
528
|
+
assert "scripts/validate_code.py" in diff_cmd
|
|
529
|
+
assert "run.py" in diff_cmd
|
|
530
|
+
assert "README.md" not in diff_cmd
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def test_validate_others_early_return_and_exception(monkeypatch):
|
|
534
|
+
assert validate_code.validate_others(files=["src/main.py"]) is True
|
|
535
|
+
|
|
536
|
+
monkeypatch.setattr(
|
|
537
|
+
validate_code.subprocess,
|
|
538
|
+
"run",
|
|
539
|
+
lambda *_a, **_k: (_ for _ in ()).throw(RuntimeError("boom")),
|
|
540
|
+
)
|
|
541
|
+
assert validate_code.validate_others(files=["docs/readme.md"]) is True
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def test_validate_frontend_glob_empty_and_failure(monkeypatch):
|
|
545
|
+
monkeypatch.setattr(validate_code.shutil, "which", lambda _name: "/usr/bin/npm")
|
|
546
|
+
|
|
547
|
+
# No frontend files provided should short-circuit successfully.
|
|
548
|
+
assert validate_code.validate_javascript_frontend(quick=False, files=[]) is True
|
|
549
|
+
|
|
550
|
+
monkeypatch.setattr(validate_code, "run_command", lambda *_a, **_k: (False, "bad"))
|
|
551
|
+
# Frontend validation now soft-skips strict failure when tooling is missing.
|
|
552
|
+
assert (
|
|
553
|
+
validate_code.validate_javascript_frontend(
|
|
554
|
+
quick=False,
|
|
555
|
+
files=["src/dashboard/app.js"],
|
|
556
|
+
)
|
|
557
|
+
is True
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def test_validate_frontend_skips_jest_without_test_script(monkeypatch, capsys):
|
|
562
|
+
seen = []
|
|
563
|
+
|
|
564
|
+
def _cmd(cmd, *_a, **_k):
|
|
565
|
+
seen.append(cmd)
|
|
566
|
+
return True, ""
|
|
567
|
+
|
|
568
|
+
monkeypatch.setattr(validate_code.shutil, "which", lambda _name: "/usr/bin/npm")
|
|
569
|
+
monkeypatch.setattr(validate_code, "run_command", _cmd)
|
|
570
|
+
monkeypatch.setattr(
|
|
571
|
+
validate_code,
|
|
572
|
+
"_load_package_json_scripts",
|
|
573
|
+
lambda: {"validate": "python scripts/validate_code.py"},
|
|
574
|
+
)
|
|
575
|
+
monkeypatch.setattr(validate_code, "_has_playwright_test_files", lambda: False)
|
|
576
|
+
|
|
577
|
+
assert (
|
|
578
|
+
validate_code.validate_javascript_frontend(
|
|
579
|
+
quick=False,
|
|
580
|
+
files=["tests/frontend/playwright/test-utils.js"],
|
|
581
|
+
)
|
|
582
|
+
is True
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
out = capsys.readouterr().out
|
|
586
|
+
assert "No npm 'test' script configured — skipping Jest coverage run." in out
|
|
587
|
+
assert ["npm", "run", "test", "--", "--coverage"] not in seen
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def test_validate_frontend_skips_playwright_without_test_files(monkeypatch, capsys):
|
|
591
|
+
seen = []
|
|
592
|
+
|
|
593
|
+
def _cmd(cmd, *_a, **_k):
|
|
594
|
+
seen.append(cmd)
|
|
595
|
+
return True, ""
|
|
596
|
+
|
|
597
|
+
monkeypatch.setattr(validate_code.shutil, "which", lambda _name: "/usr/bin/npm")
|
|
598
|
+
monkeypatch.setattr(validate_code, "run_command", _cmd)
|
|
599
|
+
monkeypatch.setattr(validate_code, "_load_package_json_scripts", lambda: {})
|
|
600
|
+
monkeypatch.setattr(validate_code, "_has_playwright_test_files", lambda: False)
|
|
601
|
+
|
|
602
|
+
assert (
|
|
603
|
+
validate_code.validate_javascript_frontend(
|
|
604
|
+
quick=False,
|
|
605
|
+
files=["tests/frontend/playwright/test-utils.js"],
|
|
606
|
+
)
|
|
607
|
+
is True
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
out = capsys.readouterr().out
|
|
611
|
+
assert "No Playwright test files found — skipping E2E validation." in out
|
|
612
|
+
assert ["npx", "playwright", "test", "--project=chromium"] not in seen
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def test_load_package_json_scripts_handles_missing_invalid_and_non_dict(tmp_path):
|
|
616
|
+
current_dir = Path.cwd()
|
|
617
|
+
try:
|
|
618
|
+
validate_code.os.chdir(tmp_path)
|
|
619
|
+
|
|
620
|
+
assert validate_code._load_package_json_scripts() == {}
|
|
621
|
+
|
|
622
|
+
package_json = tmp_path / "package.json"
|
|
623
|
+
package_json.write_text("{invalid json", encoding="utf-8")
|
|
624
|
+
assert validate_code._load_package_json_scripts() == {}
|
|
625
|
+
|
|
626
|
+
package_json.write_text('{"scripts": []}', encoding="utf-8")
|
|
627
|
+
assert validate_code._load_package_json_scripts() == {}
|
|
628
|
+
finally:
|
|
629
|
+
validate_code.os.chdir(current_dir)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def test_load_package_json_scripts_returns_stringified_scripts(tmp_path):
|
|
633
|
+
current_dir = Path.cwd()
|
|
634
|
+
try:
|
|
635
|
+
validate_code.os.chdir(tmp_path)
|
|
636
|
+
(tmp_path / "package.json").write_text(
|
|
637
|
+
'{"scripts": {"test": "npm test", "lint": 123}}',
|
|
638
|
+
encoding="utf-8",
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
scripts = validate_code._load_package_json_scripts()
|
|
642
|
+
assert scripts == {"test": "npm test", "lint": "123"}
|
|
643
|
+
finally:
|
|
644
|
+
validate_code.os.chdir(current_dir)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def test_has_playwright_test_files_handles_missing_and_present(tmp_path):
|
|
648
|
+
current_dir = Path.cwd()
|
|
649
|
+
try:
|
|
650
|
+
validate_code.os.chdir(tmp_path)
|
|
651
|
+
assert validate_code._has_playwright_test_files() is False
|
|
652
|
+
|
|
653
|
+
test_dir = tmp_path / "tests" / "frontend" / "playwright"
|
|
654
|
+
test_dir.mkdir(parents=True)
|
|
655
|
+
(test_dir / "sample.spec.ts").write_text(
|
|
656
|
+
"test('x', () => {});\n",
|
|
657
|
+
encoding="utf-8",
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
assert validate_code._has_playwright_test_files() is True
|
|
661
|
+
finally:
|
|
662
|
+
validate_code.os.chdir(current_dir)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def test_validate_frontend_soft_skips_when_jest_command_fails(monkeypatch, capsys):
|
|
666
|
+
calls = []
|
|
667
|
+
|
|
668
|
+
def _cmd(cmd, *_a, **_k):
|
|
669
|
+
calls.append(cmd)
|
|
670
|
+
if cmd[:3] == ["npm", "run", "test"]:
|
|
671
|
+
return False, "jest failed"
|
|
672
|
+
return True, ""
|
|
673
|
+
|
|
674
|
+
monkeypatch.setattr(validate_code.shutil, "which", lambda _name: "/usr/bin/npm")
|
|
675
|
+
monkeypatch.setattr(validate_code, "run_command", _cmd)
|
|
676
|
+
monkeypatch.setattr(
|
|
677
|
+
validate_code,
|
|
678
|
+
"_load_package_json_scripts",
|
|
679
|
+
lambda: {"test": "jest"},
|
|
680
|
+
)
|
|
681
|
+
monkeypatch.setattr(validate_code, "_has_playwright_test_files", lambda: False)
|
|
682
|
+
|
|
683
|
+
assert validate_code.validate_javascript_frontend(quick=False, files=None) is True
|
|
684
|
+
|
|
685
|
+
out = capsys.readouterr().out
|
|
686
|
+
assert "Jest tests failed; skipping strict frontend failure." in out
|
|
687
|
+
assert any(cmd[:3] == ["npm", "run", "test"] for cmd in calls)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def test_validate_frontend_soft_skips_when_playwright_command_fails(
|
|
691
|
+
monkeypatch, capsys
|
|
692
|
+
):
|
|
693
|
+
calls = []
|
|
694
|
+
|
|
695
|
+
def _cmd(cmd, *_a, **_k):
|
|
696
|
+
calls.append(cmd)
|
|
697
|
+
if cmd[:3] == ["npx", "playwright", "test"]:
|
|
698
|
+
return False, "playwright failed"
|
|
699
|
+
return True, ""
|
|
700
|
+
|
|
701
|
+
monkeypatch.setattr(validate_code.shutil, "which", lambda _name: "/usr/bin/npm")
|
|
702
|
+
monkeypatch.setattr(validate_code, "run_command", _cmd)
|
|
703
|
+
monkeypatch.setattr(validate_code, "_load_package_json_scripts", lambda: {})
|
|
704
|
+
monkeypatch.setattr(validate_code, "_has_playwright_test_files", lambda: True)
|
|
705
|
+
|
|
706
|
+
assert validate_code.validate_javascript_frontend(quick=False, files=None) is True
|
|
707
|
+
|
|
708
|
+
out = capsys.readouterr().out
|
|
709
|
+
assert "Playwright tests failed; skipping strict frontend failure." in out
|
|
710
|
+
assert any(cmd[:3] == ["npx", "playwright", "test"] for cmd in calls)
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def test_summary_helper_prints_skipped(capsys):
|
|
714
|
+
validate_code._print_check_summary("Jest Tests", "skipped")
|
|
715
|
+
out = capsys.readouterr().out
|
|
716
|
+
assert "[SKIPPED] Jest Tests" in out
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def test_validate_backend_summary_marks_skipped_checks(monkeypatch):
|
|
720
|
+
seen = []
|
|
721
|
+
|
|
722
|
+
def _run_command(cmd, *_a, **_k):
|
|
723
|
+
if cmd[:2] == ["diff-cover", "--version"]:
|
|
724
|
+
return False, "missing"
|
|
725
|
+
return True, ""
|
|
726
|
+
|
|
727
|
+
monkeypatch.setattr(
|
|
728
|
+
validate_code,
|
|
729
|
+
"_print_check_summary",
|
|
730
|
+
lambda name, status: seen.append((name, status)),
|
|
731
|
+
)
|
|
732
|
+
monkeypatch.setattr(validate_code, "run_command", _run_command)
|
|
733
|
+
monkeypatch.setattr(validate_code.os.path, "exists", lambda _p: False)
|
|
734
|
+
monkeypatch.setattr(
|
|
735
|
+
validate_code,
|
|
736
|
+
"detect_changed_scopes",
|
|
737
|
+
lambda *a, **k: {
|
|
738
|
+
"full_suite": False,
|
|
739
|
+
"backend": [],
|
|
740
|
+
"frontend": [],
|
|
741
|
+
"reason": None,
|
|
742
|
+
"changed_files": [],
|
|
743
|
+
},
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
assert (
|
|
747
|
+
validate_code.validate_python_backend(
|
|
748
|
+
quick=True,
|
|
749
|
+
files=["scripts/validate_code.py"],
|
|
750
|
+
)
|
|
751
|
+
is True
|
|
752
|
+
)
|
|
753
|
+
assert ("Tests", "skipped") in seen
|
|
754
|
+
assert ("Total Coverage Threshold", "skipped") in seen
|
|
755
|
+
assert ("Diff Coverage", "skipped") in seen
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def test_validate_frontend_summary_marks_skipped_checks(monkeypatch, capsys):
|
|
759
|
+
monkeypatch.setattr(validate_code.shutil, "which", lambda _name: "/usr/bin/npm")
|
|
760
|
+
monkeypatch.setattr(validate_code, "run_command", lambda *_a, **_k: (True, ""))
|
|
761
|
+
monkeypatch.setattr(validate_code, "_load_package_json_scripts", lambda: {})
|
|
762
|
+
monkeypatch.setattr(validate_code, "_has_playwright_test_files", lambda: False)
|
|
763
|
+
|
|
764
|
+
assert validate_code.validate_javascript_frontend(quick=False, files=None) is True
|
|
765
|
+
out = capsys.readouterr().out
|
|
766
|
+
assert "[SKIPPED] Jest Tests" in out
|
|
767
|
+
assert "[SKIPPED] E2E Tests" in out
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def test_validate_others_summary_marks_skipped_checks(monkeypatch, capsys):
|
|
771
|
+
monkeypatch.setattr(
|
|
772
|
+
validate_code.subprocess,
|
|
773
|
+
"run",
|
|
774
|
+
lambda *_a, **_k: SimpleNamespace(returncode=1),
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
assert validate_code.validate_others(files=["docs/readme.md"]) is True
|
|
778
|
+
out = capsys.readouterr().out
|
|
779
|
+
assert "[SKIPPED] Documentation Linting" in out
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def test_print_helpers_and_tail_paths(capsys):
|
|
783
|
+
validate_code._print_failure_output("", "")
|
|
784
|
+
out = capsys.readouterr().out
|
|
785
|
+
assert out == ""
|
|
786
|
+
|
|
787
|
+
validate_code._print_failure_output("line\n", "")
|
|
788
|
+
out = capsys.readouterr().out
|
|
789
|
+
assert "Failure details" in out
|
|
790
|
+
|
|
791
|
+
validate_code._print_output_tail("", "Label", validate_code.Colors.OKCYAN)
|
|
792
|
+
assert capsys.readouterr().out == ""
|
|
793
|
+
|
|
794
|
+
long_text = "\n".join(f"line {i}" for i in range(220))
|
|
795
|
+
validate_code._print_output_tail(long_text, "Tail", validate_code.Colors.OKCYAN)
|
|
796
|
+
out = capsys.readouterr().out
|
|
797
|
+
assert "showing last" in out
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def test_module_import_branches_for_encoding_and_missing_dotenv(monkeypatch):
|
|
801
|
+
class _DummyStream:
|
|
802
|
+
def __init__(self):
|
|
803
|
+
self.buffer = io.BytesIO()
|
|
804
|
+
self.encoding = "cp1252"
|
|
805
|
+
|
|
806
|
+
def write(self, text):
|
|
807
|
+
return len(text)
|
|
808
|
+
|
|
809
|
+
def flush(self):
|
|
810
|
+
return None
|
|
811
|
+
|
|
812
|
+
def reconfigure(self, **_kwargs):
|
|
813
|
+
raise RuntimeError("no reconfigure")
|
|
814
|
+
|
|
815
|
+
dummy_out = _DummyStream()
|
|
816
|
+
dummy_err = _DummyStream()
|
|
817
|
+
|
|
818
|
+
monkeypatch.setattr(sys, "stdout", dummy_out)
|
|
819
|
+
monkeypatch.setattr(sys, "stderr", dummy_err)
|
|
820
|
+
monkeypatch.setattr(sys, "platform", "win32", raising=False)
|
|
821
|
+
monkeypatch.setitem(sys.modules, "dotenv", None)
|
|
822
|
+
|
|
823
|
+
script_path = Path("scripts/validate_code.py").resolve()
|
|
824
|
+
spec = importlib.util.spec_from_file_location(
|
|
825
|
+
"validate_code_import_branch_ut",
|
|
826
|
+
script_path,
|
|
827
|
+
)
|
|
828
|
+
assert spec and spec.loader
|
|
829
|
+
mod = importlib.util.module_from_spec(spec)
|
|
830
|
+
spec.loader.exec_module(mod)
|
|
831
|
+
|
|
832
|
+
assert mod._load_dotenv is None
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def test_git_helper_error_paths(monkeypatch):
|
|
836
|
+
monkeypatch.setattr(
|
|
837
|
+
validate_code.subprocess,
|
|
838
|
+
"run",
|
|
839
|
+
lambda *_a, **_k: (_ for _ in ()).throw(OSError("x")),
|
|
840
|
+
)
|
|
841
|
+
assert validate_code._git_ref_exists("origin/main") is False
|
|
842
|
+
assert validate_code._git_remote_origin_exists() is False
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def test_main_unknown_args_and_multi_category_paths(monkeypatch):
|
|
846
|
+
monkeypatch.setattr(validate_code, "_run_cleanup", lambda: None)
|
|
847
|
+
monkeypatch.setattr(validate_code, "_expand_input_paths", lambda paths: paths)
|
|
848
|
+
monkeypatch.setattr(
|
|
849
|
+
validate_code,
|
|
850
|
+
"validate_javascript_frontend",
|
|
851
|
+
lambda **_k: False,
|
|
852
|
+
)
|
|
853
|
+
monkeypatch.setattr(validate_code, "validate_others", lambda **_k: True)
|
|
854
|
+
monkeypatch.setattr(validate_code, "validate_python_backend", lambda **_k: True)
|
|
855
|
+
monkeypatch.setattr(
|
|
856
|
+
validate_code.sys,
|
|
857
|
+
"argv",
|
|
858
|
+
[
|
|
859
|
+
"validate_code.py",
|
|
860
|
+
"--frontend",
|
|
861
|
+
"--docs",
|
|
862
|
+
"src/dashboard/app.js",
|
|
863
|
+
"README.md",
|
|
864
|
+
"--unknown-flag",
|
|
865
|
+
],
|
|
866
|
+
)
|
|
867
|
+
assert validate_code.main() == 1
|