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.
Files changed (82) hide show
  1. collab/__init__.py +77 -0
  2. collab/__main__.py +11 -0
  3. collab_runtime-0.2.9.dist-info/METADATA +218 -0
  4. collab_runtime-0.2.9.dist-info/RECORD +82 -0
  5. collab_runtime-0.2.9.dist-info/WHEEL +5 -0
  6. collab_runtime-0.2.9.dist-info/entry_points.txt +3 -0
  7. collab_runtime-0.2.9.dist-info/licenses/LICENSE +21 -0
  8. collab_runtime-0.2.9.dist-info/top_level.txt +10 -0
  9. scripts/cleanup.py +395 -0
  10. scripts/collab_git_hook.py +190 -0
  11. scripts/format_code.py +594 -0
  12. scripts/generate_tests.py +560 -0
  13. scripts/validate_code.py +1397 -0
  14. src/__init__.py +4 -0
  15. src/dashboard/index.html +1131 -0
  16. src/live_locks_watcher.py +1982 -0
  17. src/lock_client.py +4268 -0
  18. src/logging_config.py +259 -0
  19. src/main.py +436 -0
  20. tests/backend/__init__.py +0 -0
  21. tests/backend/functional/__init__.py +0 -0
  22. tests/backend/functional/test_package_imports.py +43 -0
  23. tests/backend/integration/__init__.py +0 -0
  24. tests/backend/integration/test_cli_contract_parity.py +220 -0
  25. tests/backend/performance/__init__.py +0 -0
  26. tests/backend/reliability/__init__.py +0 -0
  27. tests/backend/security/__init__.py +0 -0
  28. tests/backend/unit/live_locks_watcher/__init__.py +5 -0
  29. tests/backend/unit/live_locks_watcher/_helpers.py +123 -0
  30. tests/backend/unit/live_locks_watcher/conftest.py +18 -0
  31. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_dashboard.py +188 -0
  32. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_developer.py +56 -0
  33. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_graceful_shutdown.py +459 -0
  34. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_main.py +1925 -0
  35. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_module.py +187 -0
  36. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_multi_session.py +320 -0
  37. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_notify.py +67 -0
  38. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_parsing.py +155 -0
  39. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_process_helpers.py +684 -0
  40. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_processing.py +173 -0
  41. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_prompt_abort.py +71 -0
  42. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_reconcile.py +516 -0
  43. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_scan.py +296 -0
  44. tests/backend/unit/lock_client/__init__.py +1 -0
  45. tests/backend/unit/lock_client/_helpers.py +132 -0
  46. tests/backend/unit/lock_client/test_lock_client_acquire.py +214 -0
  47. tests/backend/unit/lock_client/test_lock_client_active.py +104 -0
  48. tests/backend/unit/lock_client/test_lock_client_api.py +63 -0
  49. tests/backend/unit/lock_client/test_lock_client_cli.py +682 -0
  50. tests/backend/unit/lock_client/test_lock_client_daemon.py +3730 -0
  51. tests/backend/unit/lock_client/test_lock_client_dashboard.py +438 -0
  52. tests/backend/unit/lock_client/test_lock_client_discover.py +241 -0
  53. tests/backend/unit/lock_client/test_lock_client_force_release.py +354 -0
  54. tests/backend/unit/lock_client/test_lock_client_helper_branches.py +1890 -0
  55. tests/backend/unit/lock_client/test_lock_client_history.py +301 -0
  56. tests/backend/unit/lock_client/test_lock_client_isolation.py +316 -0
  57. tests/backend/unit/lock_client/test_lock_client_pid.py +75 -0
  58. tests/backend/unit/lock_client/test_lock_client_reconcile.py +464 -0
  59. tests/backend/unit/lock_client/test_lock_client_release.py +77 -0
  60. tests/backend/unit/lock_client/test_lock_client_shutdown.py +1110 -0
  61. tests/backend/unit/lock_client/test_lock_client_utils.py +474 -0
  62. tests/backend/unit/lock_client/test_lock_client_watch.py +866 -0
  63. tests/backend/unit/scripts/__init__.py +1 -0
  64. tests/backend/unit/scripts/_helpers.py +42 -0
  65. tests/backend/unit/scripts/test_cleanup.py +285 -0
  66. tests/backend/unit/scripts/test_collab_git_hook.py +280 -0
  67. tests/backend/unit/scripts/test_collab_git_hook_ported.py +50 -0
  68. tests/backend/unit/scripts/test_format_code.py +368 -0
  69. tests/backend/unit/scripts/test_format_code_ported.py +177 -0
  70. tests/backend/unit/scripts/test_generate_tests.py +305 -0
  71. tests/backend/unit/scripts/test_hook_templates.py +357 -0
  72. tests/backend/unit/scripts/test_setup_hook_overlay.py +95 -0
  73. tests/backend/unit/scripts/test_validate_code.py +867 -0
  74. tests/backend/unit/scripts/test_validate_code_ported.py +237 -0
  75. tests/backend/unit/test_entrypoints_main_run.py +83 -0
  76. tests/backend/unit/test_logging_config.py +529 -0
  77. tests/backend/unit/test_main_watch_pid_file.py +278 -0
  78. tests/conftest.py +167 -0
  79. tests/frontend/__init__.py +0 -0
  80. tests/frontend/jest/__init__.py +0 -0
  81. tests/frontend/playwright/__init__.py +0 -0
  82. 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)