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,368 @@
|
|
|
1
|
+
"""Tests for scripts/format_code.py."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from types import SimpleNamespace
|
|
9
|
+
from unittest import mock
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from tests.backend.unit.scripts._helpers import load_script_module
|
|
14
|
+
|
|
15
|
+
format_code = load_script_module("format_code.py", "format_code_under_test")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CaptureStdout:
|
|
19
|
+
def __enter__(self):
|
|
20
|
+
self._stdout = sys.stdout
|
|
21
|
+
sys.stdout = self._stringio = io.StringIO()
|
|
22
|
+
return self._stringio
|
|
23
|
+
|
|
24
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
25
|
+
sys.stdout = self._stdout
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _mock_completed(returncode=0, stdout="", stderr=""):
|
|
29
|
+
return SimpleNamespace(returncode=returncode, stdout=stdout, stderr=stderr)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_normalize_whitespace_preserves_crlf():
|
|
33
|
+
raw = b"a \r\n\tb\t\r\n\r\n"
|
|
34
|
+
normalized = format_code.CodeFormatter._normalize_whitespace(raw)
|
|
35
|
+
assert normalized == b"a\r\n\tb\r\n"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_exec_success_and_exceptions(monkeypatch):
|
|
39
|
+
formatter = format_code.CodeFormatter()
|
|
40
|
+
|
|
41
|
+
monkeypatch.setattr(
|
|
42
|
+
format_code.subprocess,
|
|
43
|
+
"run",
|
|
44
|
+
lambda *a, **k: _mock_completed(0, "ok", ""),
|
|
45
|
+
)
|
|
46
|
+
ok, result = formatter._exec(["tool", "arg"])
|
|
47
|
+
assert ok is True
|
|
48
|
+
assert result is not None
|
|
49
|
+
|
|
50
|
+
monkeypatch.setattr(
|
|
51
|
+
format_code.subprocess,
|
|
52
|
+
"run",
|
|
53
|
+
lambda *a, **k: (_ for _ in ()).throw(FileNotFoundError("missing")),
|
|
54
|
+
)
|
|
55
|
+
ok, result = formatter._exec(["missing"])
|
|
56
|
+
assert ok is False
|
|
57
|
+
assert result is None
|
|
58
|
+
|
|
59
|
+
monkeypatch.setattr(
|
|
60
|
+
format_code.subprocess,
|
|
61
|
+
"run",
|
|
62
|
+
lambda *a, **k: (_ for _ in ()).throw(RuntimeError("boom")),
|
|
63
|
+
)
|
|
64
|
+
ok, result = formatter._exec(["bad"])
|
|
65
|
+
assert ok is False
|
|
66
|
+
assert result is None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_run_tool_step_check_only_paths(monkeypatch):
|
|
70
|
+
formatter = format_code.CodeFormatter(check_only=True)
|
|
71
|
+
|
|
72
|
+
monkeypatch.setattr(formatter, "_exec", lambda *a, **k: (True, _mock_completed()))
|
|
73
|
+
assert formatter._run_tool_step("Tool", ["fix"], ["check"], "S", 1, 1) is True
|
|
74
|
+
|
|
75
|
+
monkeypatch.setattr(formatter, "_exec", lambda *a, **k: (False, _mock_completed(1)))
|
|
76
|
+
assert formatter._run_tool_step("Tool2", ["fix"], ["check"], "S", 1, 1) is False
|
|
77
|
+
assert any("Tool2" in x[1] for x in formatter.failed_tools)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_run_tool_step_fix_then_check_paths(monkeypatch):
|
|
81
|
+
formatter = format_code.CodeFormatter(check_only=False)
|
|
82
|
+
|
|
83
|
+
calls = {"n": 0}
|
|
84
|
+
|
|
85
|
+
def _exec(*_a, **_k):
|
|
86
|
+
calls["n"] += 1
|
|
87
|
+
if calls["n"] == 1:
|
|
88
|
+
return False, _mock_completed(1, "fail", "")
|
|
89
|
+
return True, _mock_completed(0, "", "")
|
|
90
|
+
|
|
91
|
+
monkeypatch.setattr(formatter, "_exec", _exec)
|
|
92
|
+
assert formatter._run_tool_step("Tool", ["fix"], ["check"], "S", 1, 1) is True
|
|
93
|
+
|
|
94
|
+
calls["n"] = 0
|
|
95
|
+
|
|
96
|
+
def _exec_fail(*_a, **_k):
|
|
97
|
+
calls["n"] += 1
|
|
98
|
+
if calls["n"] == 1:
|
|
99
|
+
return False, _mock_completed(1, "fail", "")
|
|
100
|
+
return False, _mock_completed(1, "still", "")
|
|
101
|
+
|
|
102
|
+
monkeypatch.setattr(formatter, "_exec", _exec_fail)
|
|
103
|
+
assert formatter._run_tool_step("ToolFail", ["fix"], ["check"], "S", 1, 1) is False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_normalize_whitespace_no_files(monkeypatch):
|
|
107
|
+
formatter = format_code.CodeFormatter(files=["not-python.bin"])
|
|
108
|
+
with CaptureStdout():
|
|
109
|
+
assert formatter.normalize_whitespace() is True
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_format_python_invokes_five_steps(monkeypatch):
|
|
113
|
+
formatter = format_code.CodeFormatter(files=["a.py"])
|
|
114
|
+
seen = []
|
|
115
|
+
monkeypatch.setattr(
|
|
116
|
+
formatter,
|
|
117
|
+
"_run_tool_step",
|
|
118
|
+
lambda desc, *_a, **_k: seen.append(desc) or True,
|
|
119
|
+
)
|
|
120
|
+
assert formatter.format_python() is True
|
|
121
|
+
assert len(seen) == 5
|
|
122
|
+
assert "Final linting (flake8)" in seen
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_check_prettier_and_filter_targets(monkeypatch, tmp_path):
|
|
126
|
+
formatter = format_code.CodeFormatter()
|
|
127
|
+
monkeypatch.setattr(formatter, "root_dir", tmp_path)
|
|
128
|
+
(tmp_path / "a.js").write_text("x", encoding="utf-8")
|
|
129
|
+
|
|
130
|
+
call_index = {"n": 0}
|
|
131
|
+
|
|
132
|
+
def _run(*_a, **_k):
|
|
133
|
+
call_index["n"] += 1
|
|
134
|
+
# prettier installed, plugin missing
|
|
135
|
+
if call_index["n"] == 1:
|
|
136
|
+
return _mock_completed(0)
|
|
137
|
+
return _mock_completed(1)
|
|
138
|
+
|
|
139
|
+
monkeypatch.setattr(format_code.subprocess, "run", _run)
|
|
140
|
+
assert formatter._check_prettier() is False
|
|
141
|
+
|
|
142
|
+
assert formatter._filter_glob_targets(["*.js", "*.css"]) == ["*.js"]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_format_frontend_docs_yaml(monkeypatch):
|
|
146
|
+
formatter = format_code.CodeFormatter(files=["x.js", "doc.md", "a.yaml"])
|
|
147
|
+
|
|
148
|
+
monkeypatch.setattr(formatter, "_check_prettier", lambda: True)
|
|
149
|
+
monkeypatch.setattr(formatter, "_run_tool_step", lambda *_a, **_k: True)
|
|
150
|
+
monkeypatch.setattr(
|
|
151
|
+
formatter,
|
|
152
|
+
"_filter_glob_targets",
|
|
153
|
+
lambda _patterns: ["src/**/*.js", "docs/**/*.md"],
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
assert formatter.format_frontend() is True
|
|
157
|
+
assert formatter.format_docs() is True
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_format_yaml_with_files(monkeypatch, tmp_path):
|
|
161
|
+
formatter = format_code.CodeFormatter()
|
|
162
|
+
monkeypatch.setattr(formatter, "root_dir", tmp_path)
|
|
163
|
+
y1 = tmp_path / "a.yaml"
|
|
164
|
+
y2 = tmp_path / "b.yml"
|
|
165
|
+
y1.write_text("k: v\n", encoding="utf-8")
|
|
166
|
+
y2.write_text("k: v\n", encoding="utf-8")
|
|
167
|
+
|
|
168
|
+
called = []
|
|
169
|
+
monkeypatch.setattr(
|
|
170
|
+
formatter,
|
|
171
|
+
"_run_tool_step",
|
|
172
|
+
lambda desc, *_a, **_k: called.append(desc) or True,
|
|
173
|
+
)
|
|
174
|
+
assert formatter.format_yaml() is True
|
|
175
|
+
assert called == ["YAML (prettier)", "YAML (yamllint)"]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@pytest.mark.parametrize("djlint_version_ok", [False, True])
|
|
179
|
+
def test_format_templates_paths(monkeypatch, djlint_version_ok):
|
|
180
|
+
formatter = format_code.CodeFormatter(files=["src/dashboard/index.html"])
|
|
181
|
+
|
|
182
|
+
exec_calls = {"n": 0}
|
|
183
|
+
|
|
184
|
+
def _exec(_cmd, suppress_output=False):
|
|
185
|
+
exec_calls["n"] += 1
|
|
186
|
+
# version check
|
|
187
|
+
if exec_calls["n"] == 1:
|
|
188
|
+
return djlint_version_ok, _mock_completed(0 if djlint_version_ok else 1)
|
|
189
|
+
# fix/check pass
|
|
190
|
+
return True, _mock_completed(0)
|
|
191
|
+
|
|
192
|
+
monkeypatch.setattr(formatter, "_exec", _exec)
|
|
193
|
+
assert formatter.format_templates() is True
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_format_templates_check_failure(monkeypatch):
|
|
197
|
+
formatter = format_code.CodeFormatter(files=["src/dashboard/index.html"])
|
|
198
|
+
|
|
199
|
+
exec_calls = {"n": 0}
|
|
200
|
+
|
|
201
|
+
def _exec(_cmd, suppress_output=False):
|
|
202
|
+
exec_calls["n"] += 1
|
|
203
|
+
if exec_calls["n"] == 1:
|
|
204
|
+
return True, _mock_completed(0) # version
|
|
205
|
+
if exec_calls["n"] == 2:
|
|
206
|
+
return False, _mock_completed(1) # fix failed
|
|
207
|
+
if exec_calls["n"] == 3:
|
|
208
|
+
return False, _mock_completed(1) # check failed
|
|
209
|
+
return False, _mock_completed(1)
|
|
210
|
+
|
|
211
|
+
monkeypatch.setattr(formatter, "_exec", _exec)
|
|
212
|
+
assert formatter.format_templates() is False
|
|
213
|
+
assert formatter.failed_tools
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_print_summary_success_and_failure(capsys):
|
|
217
|
+
formatter = format_code.CodeFormatter()
|
|
218
|
+
formatter.print_summary()
|
|
219
|
+
out = capsys.readouterr().out
|
|
220
|
+
assert "All formatting operations completed successfully" in out
|
|
221
|
+
|
|
222
|
+
formatter.failed_tools.append(("[X] step", "step", True))
|
|
223
|
+
formatter.print_summary()
|
|
224
|
+
out = capsys.readouterr().out
|
|
225
|
+
assert "operation(s) failed" in out
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_main_paths(monkeypatch):
|
|
229
|
+
monkeypatch.setattr(format_code, "clean_caches", lambda dry_run=False: 0)
|
|
230
|
+
|
|
231
|
+
formatter = format_code.CodeFormatter()
|
|
232
|
+
monkeypatch.setattr(format_code, "CodeFormatter", lambda **_k: formatter)
|
|
233
|
+
monkeypatch.setattr(formatter, "normalize_whitespace", lambda: True)
|
|
234
|
+
monkeypatch.setattr(formatter, "format_python", lambda: True)
|
|
235
|
+
monkeypatch.setattr(formatter, "format_frontend", lambda: True)
|
|
236
|
+
monkeypatch.setattr(formatter, "format_templates", lambda: True)
|
|
237
|
+
monkeypatch.setattr(formatter, "format_docs", lambda: True)
|
|
238
|
+
monkeypatch.setattr(formatter, "format_yaml", lambda: True)
|
|
239
|
+
monkeypatch.setattr(formatter, "print_summary", lambda: None)
|
|
240
|
+
|
|
241
|
+
with mock.patch("sys.argv", ["format_code.py", "--backend"]):
|
|
242
|
+
assert format_code.main() == 0
|
|
243
|
+
|
|
244
|
+
monkeypatch.setattr(formatter, "format_python", lambda: False)
|
|
245
|
+
with mock.patch("sys.argv", ["format_code.py", "--backend"]):
|
|
246
|
+
assert format_code.main() == 1
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_get_targets_and_python_executable_paths(monkeypatch, tmp_path):
|
|
250
|
+
formatter = format_code.CodeFormatter(files=None)
|
|
251
|
+
assert formatter._get_targets((".py",), ["src", "tests"]) == ["src", "tests"]
|
|
252
|
+
|
|
253
|
+
monkeypatch.setattr(formatter, "root_dir", tmp_path)
|
|
254
|
+
monkeypatch.setattr(format_code.sys, "platform", "win32")
|
|
255
|
+
scripts_dir = tmp_path / ".venv" / "Scripts"
|
|
256
|
+
scripts_dir.mkdir(parents=True, exist_ok=True)
|
|
257
|
+
exe = scripts_dir / "python.exe"
|
|
258
|
+
exe.write_text("", encoding="utf-8")
|
|
259
|
+
assert formatter._get_python_executable().endswith("python.exe")
|
|
260
|
+
|
|
261
|
+
exe.unlink()
|
|
262
|
+
assert formatter._get_python_executable() == format_code.sys.executable
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_exec_stderr_and_git_lsfiles_failure(monkeypatch, capsys):
|
|
266
|
+
formatter = format_code.CodeFormatter()
|
|
267
|
+
|
|
268
|
+
monkeypatch.setattr(
|
|
269
|
+
format_code.subprocess,
|
|
270
|
+
"run",
|
|
271
|
+
lambda *_a, **_k: _mock_completed(0, "", "warn\nerr"),
|
|
272
|
+
)
|
|
273
|
+
ok, _ = formatter._exec(["tool"])
|
|
274
|
+
assert ok is True
|
|
275
|
+
assert "warn" in capsys.readouterr().err
|
|
276
|
+
|
|
277
|
+
def _raise(*_a, **_k):
|
|
278
|
+
raise FileNotFoundError("git missing")
|
|
279
|
+
|
|
280
|
+
monkeypatch.setattr(format_code.subprocess, "run", _raise)
|
|
281
|
+
with CaptureStdout() as out:
|
|
282
|
+
assert formatter.normalize_whitespace() is True
|
|
283
|
+
assert "Could not list git files" in out.getvalue()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def test_normalize_whitespace_issues_and_skips(monkeypatch, tmp_path):
|
|
287
|
+
formatter = format_code.CodeFormatter(check_only=False)
|
|
288
|
+
monkeypatch.setattr(formatter, "root_dir", tmp_path)
|
|
289
|
+
|
|
290
|
+
raw_file = tmp_path / "bad.py"
|
|
291
|
+
raw_file.write_bytes(b"a \n")
|
|
292
|
+
|
|
293
|
+
bin_file = tmp_path / "binary.py"
|
|
294
|
+
bin_file.write_bytes(b"\x00\x01\x02")
|
|
295
|
+
|
|
296
|
+
tmp_path / "missing.py"
|
|
297
|
+
|
|
298
|
+
monkeypatch.setattr(
|
|
299
|
+
format_code.subprocess,
|
|
300
|
+
"run",
|
|
301
|
+
lambda *_a, **_k: _mock_completed(
|
|
302
|
+
0,
|
|
303
|
+
"bad.py\nbinary.py\nmissing.py\n",
|
|
304
|
+
"",
|
|
305
|
+
),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
original_read_bytes = Path.read_bytes
|
|
309
|
+
|
|
310
|
+
def _patched_read_bytes(path_obj):
|
|
311
|
+
if path_obj.name == "missing.py":
|
|
312
|
+
raise OSError("gone")
|
|
313
|
+
return original_read_bytes(path_obj)
|
|
314
|
+
|
|
315
|
+
monkeypatch.setattr(Path, "read_bytes", _patched_read_bytes)
|
|
316
|
+
|
|
317
|
+
assert formatter.normalize_whitespace() is True
|
|
318
|
+
assert raw_file.read_bytes().endswith(b"\n")
|
|
319
|
+
|
|
320
|
+
# Check-only path should report remaining issues and return False.
|
|
321
|
+
formatter_check = format_code.CodeFormatter(check_only=True)
|
|
322
|
+
monkeypatch.setattr(formatter_check, "root_dir", tmp_path)
|
|
323
|
+
raw_file.write_bytes(b"b \n")
|
|
324
|
+
monkeypatch.setattr(
|
|
325
|
+
format_code.subprocess,
|
|
326
|
+
"run",
|
|
327
|
+
lambda *_a, **_k: _mock_completed(0, "bad.py\n", ""),
|
|
328
|
+
)
|
|
329
|
+
assert formatter_check.normalize_whitespace() is False
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def test_prettier_and_target_early_returns(monkeypatch):
|
|
333
|
+
formatter = format_code.CodeFormatter(files=["x.py"]) # no frontend/docs targets
|
|
334
|
+
|
|
335
|
+
monkeypatch.setattr(formatter, "_filter_glob_targets", lambda _p: [])
|
|
336
|
+
assert formatter.format_frontend() is True
|
|
337
|
+
assert formatter.format_docs() is True
|
|
338
|
+
|
|
339
|
+
monkeypatch.setattr(formatter, "_check_prettier", lambda: False)
|
|
340
|
+
monkeypatch.setattr(formatter, "_filter_glob_targets", lambda _p: ["src/**/*.js"])
|
|
341
|
+
monkeypatch.setattr(
|
|
342
|
+
formatter,
|
|
343
|
+
"_get_targets",
|
|
344
|
+
lambda _ext, default: default,
|
|
345
|
+
)
|
|
346
|
+
assert formatter.format_frontend() is True
|
|
347
|
+
|
|
348
|
+
monkeypatch.setattr(formatter, "root_dir", Path("."))
|
|
349
|
+
monkeypatch.setattr(Path, "rglob", lambda self, _ext: iter(()))
|
|
350
|
+
assert formatter.format_yaml() is True
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def test_dunder_main_path(monkeypatch):
|
|
354
|
+
monkeypatch.setattr(format_code, "clean_caches", lambda dry_run=False: 0)
|
|
355
|
+
monkeypatch.setattr(
|
|
356
|
+
format_code.CodeFormatter, "normalize_whitespace", lambda self: True
|
|
357
|
+
)
|
|
358
|
+
monkeypatch.setattr(format_code.CodeFormatter, "format_python", lambda self: True)
|
|
359
|
+
monkeypatch.setattr(format_code.CodeFormatter, "format_frontend", lambda self: True)
|
|
360
|
+
monkeypatch.setattr(
|
|
361
|
+
format_code.CodeFormatter, "format_templates", lambda self: True
|
|
362
|
+
)
|
|
363
|
+
monkeypatch.setattr(format_code.CodeFormatter, "format_docs", lambda self: True)
|
|
364
|
+
monkeypatch.setattr(format_code.CodeFormatter, "format_yaml", lambda self: True)
|
|
365
|
+
monkeypatch.setattr(format_code.CodeFormatter, "print_summary", lambda self: None)
|
|
366
|
+
monkeypatch.setattr(sys, "argv", ["format_code.py"])
|
|
367
|
+
|
|
368
|
+
assert format_code.main() == 0
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Additional ported coverage tests for scripts/format_code.py."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import sys
|
|
7
|
+
from unittest import mock
|
|
8
|
+
|
|
9
|
+
from tests.backend.unit.scripts._helpers import load_script_module
|
|
10
|
+
|
|
11
|
+
format_code = load_script_module("format_code.py", "format_code_ported_under_test")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CaptureStdout:
|
|
15
|
+
def __enter__(self):
|
|
16
|
+
self._stdout = sys.stdout
|
|
17
|
+
sys.stdout = self._stringio = io.StringIO()
|
|
18
|
+
return self._stringio
|
|
19
|
+
|
|
20
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
21
|
+
sys.stdout = self._stdout
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def make_sequential_mock(call_map):
|
|
25
|
+
counters = {k: 0 for k in call_map}
|
|
26
|
+
|
|
27
|
+
def side_effect(cmd, *args, **kwargs):
|
|
28
|
+
cmd_str = " ".join(str(c) for c in cmd)
|
|
29
|
+
if "git" in cmd_str and "ls-files" in cmd_str:
|
|
30
|
+
m = mock.Mock()
|
|
31
|
+
m.returncode = 0
|
|
32
|
+
m.stdout = "src/main.py\n"
|
|
33
|
+
m.stderr = ""
|
|
34
|
+
return m
|
|
35
|
+
if "prettier" in cmd_str or "npm list" in cmd_str:
|
|
36
|
+
m = mock.Mock()
|
|
37
|
+
m.returncode = 0
|
|
38
|
+
m.stdout = ""
|
|
39
|
+
m.stderr = ""
|
|
40
|
+
return m
|
|
41
|
+
for tool, responses in call_map.items():
|
|
42
|
+
if tool in cmd_str:
|
|
43
|
+
idx = counters.get(tool, 0)
|
|
44
|
+
if idx < len(responses):
|
|
45
|
+
counters[tool] = idx + 1
|
|
46
|
+
rc, out, err = responses[idx]
|
|
47
|
+
else:
|
|
48
|
+
rc, out, err = responses[-1]
|
|
49
|
+
m = mock.Mock()
|
|
50
|
+
m.returncode = rc
|
|
51
|
+
m.stdout = out
|
|
52
|
+
m.stderr = err
|
|
53
|
+
return m
|
|
54
|
+
m = mock.Mock()
|
|
55
|
+
m.returncode = 0
|
|
56
|
+
m.stdout = ""
|
|
57
|
+
m.stderr = ""
|
|
58
|
+
return m
|
|
59
|
+
|
|
60
|
+
return side_effect
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
BACKEND_TOOLS = [
|
|
64
|
+
"Import sorting (isort)",
|
|
65
|
+
"Code formatting (black)",
|
|
66
|
+
"Docstring formatting (docformatter)",
|
|
67
|
+
"Ruff linting & fixing",
|
|
68
|
+
"Final linting (flake8)",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_scenario_clean(monkeypatch):
|
|
73
|
+
monkeypatch.setattr(
|
|
74
|
+
format_code.CodeFormatter,
|
|
75
|
+
"_get_targets",
|
|
76
|
+
lambda self, ext, default: ["src/main.py"] if ".py" in ext else [],
|
|
77
|
+
)
|
|
78
|
+
call_map = {
|
|
79
|
+
"ruff": [(0, "", "")],
|
|
80
|
+
"isort": [(0, "", "")],
|
|
81
|
+
"black": [(0, "", "")],
|
|
82
|
+
"docformatter": [(0, "", "")],
|
|
83
|
+
"flake8": [(0, "", "")],
|
|
84
|
+
"yamllint": [(0, "", "")],
|
|
85
|
+
}
|
|
86
|
+
with mock.patch("subprocess.run", side_effect=make_sequential_mock(call_map)):
|
|
87
|
+
with mock.patch("sys.argv", ["format_code.py", "--backend"]):
|
|
88
|
+
with CaptureStdout() as out:
|
|
89
|
+
format_code.main()
|
|
90
|
+
output = out.getvalue()
|
|
91
|
+
|
|
92
|
+
for tool in BACKEND_TOOLS:
|
|
93
|
+
assert f"✅ {tool} - SUCCESS" in output
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_scenario_fixable(monkeypatch):
|
|
97
|
+
monkeypatch.setattr(
|
|
98
|
+
format_code.CodeFormatter,
|
|
99
|
+
"_get_targets",
|
|
100
|
+
lambda self, ext, default: ["src/main.py"] if ".py" in ext else [],
|
|
101
|
+
)
|
|
102
|
+
call_map = {
|
|
103
|
+
"ruff": [(1, "fixed", ""), (0, "", "")],
|
|
104
|
+
"isort": [(1, "fixed", ""), (0, "", "")],
|
|
105
|
+
"black": [(1, "fixed", ""), (0, "", "")],
|
|
106
|
+
"docformatter": [(1, "fixed", ""), (0, "", "")],
|
|
107
|
+
"flake8": [(1, "fixed", ""), (0, "", "")],
|
|
108
|
+
"yamllint": [(0, "", "")],
|
|
109
|
+
}
|
|
110
|
+
with mock.patch("subprocess.run", side_effect=make_sequential_mock(call_map)):
|
|
111
|
+
with mock.patch("sys.argv", ["format_code.py", "--backend"]):
|
|
112
|
+
with CaptureStdout() as out:
|
|
113
|
+
format_code.main()
|
|
114
|
+
output = out.getvalue()
|
|
115
|
+
|
|
116
|
+
for tool in BACKEND_TOOLS:
|
|
117
|
+
assert f"❌ {tool} - ISSUES FOUND" in output
|
|
118
|
+
assert f"✅ {tool} (check) - All issues fixed" in output
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_scenario_unfixable(monkeypatch):
|
|
122
|
+
monkeypatch.setattr(
|
|
123
|
+
format_code.CodeFormatter,
|
|
124
|
+
"_get_targets",
|
|
125
|
+
lambda self, ext, default: ["src/main.py"] if ".py" in ext else [],
|
|
126
|
+
)
|
|
127
|
+
call_map = {
|
|
128
|
+
"ruff": [(1, "bad", ""), (1, "bad", "")],
|
|
129
|
+
"isort": [(1, "bad", ""), (1, "bad", "")],
|
|
130
|
+
"black": [(1, "bad", ""), (1, "bad", "")],
|
|
131
|
+
"docformatter": [(1, "bad", ""), (1, "bad", "")],
|
|
132
|
+
"flake8": [(1, "bad", ""), (1, "bad", "")],
|
|
133
|
+
"yamllint": [(0, "", "")],
|
|
134
|
+
}
|
|
135
|
+
with mock.patch("subprocess.run", side_effect=make_sequential_mock(call_map)):
|
|
136
|
+
with mock.patch("sys.argv", ["format_code.py", "--backend"]):
|
|
137
|
+
with CaptureStdout() as out:
|
|
138
|
+
try:
|
|
139
|
+
format_code.main()
|
|
140
|
+
except SystemExit:
|
|
141
|
+
pass
|
|
142
|
+
output = out.getvalue()
|
|
143
|
+
|
|
144
|
+
assert "operation(s) failed" in output
|
|
145
|
+
for tool in BACKEND_TOOLS:
|
|
146
|
+
assert f"❌ {tool} (check) - Issues remain" in output
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_exec_exception_branches():
|
|
150
|
+
formatter = format_code.CodeFormatter()
|
|
151
|
+
|
|
152
|
+
with mock.patch("subprocess.run", side_effect=FileNotFoundError("no")):
|
|
153
|
+
with CaptureStdout() as out:
|
|
154
|
+
ok, result = formatter._exec(["nonexistent_tool", "arg"])
|
|
155
|
+
assert ok is False
|
|
156
|
+
assert result is None
|
|
157
|
+
assert "Tool not found" in out.getvalue()
|
|
158
|
+
|
|
159
|
+
with mock.patch("subprocess.run", side_effect=RuntimeError("boom")):
|
|
160
|
+
with CaptureStdout() as out:
|
|
161
|
+
ok, result = formatter._exec(["bad_tool", "arg"])
|
|
162
|
+
assert ok is False
|
|
163
|
+
assert result is None
|
|
164
|
+
assert "Error: boom" in out.getvalue()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_run_tool_step_no_check_cmd():
|
|
168
|
+
formatter = format_code.CodeFormatter()
|
|
169
|
+
formatter.check_only = False
|
|
170
|
+
|
|
171
|
+
fail_result = mock.Mock(stdout="error", stderr="", returncode=1)
|
|
172
|
+
with mock.patch.object(formatter, "_exec", return_value=(False, fail_result)):
|
|
173
|
+
with CaptureStdout():
|
|
174
|
+
result = formatter._run_tool_step("Broken tool", ["fix"], None, "S", 1, 1)
|
|
175
|
+
|
|
176
|
+
assert result is False
|
|
177
|
+
assert any("Broken tool" in str(entry) for entry in formatter.failed_tools)
|