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,438 @@
|
|
|
1
|
+
"""Dashboard-related tests for LockClient.
|
|
2
|
+
|
|
3
|
+
Moved from the main `test_lock_client.py` for clearer organization.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import socket
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
import types
|
|
13
|
+
from unittest import mock
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
from ._helpers import FakeResponse, load_lock_client_module, make_create_client
|
|
18
|
+
|
|
19
|
+
mod = load_lock_client_module()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_dashboard_opens_browser(monkeypatch):
|
|
23
|
+
"""Test dashboard() opens a browser."""
|
|
24
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
25
|
+
monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
|
|
26
|
+
|
|
27
|
+
monkeypatch.setattr(
|
|
28
|
+
mod, "_get_create_client", lambda: make_create_client(FakeResponse())
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
opened_urls = []
|
|
32
|
+
|
|
33
|
+
def mock_prepare(self):
|
|
34
|
+
_tmp = os.path.join(tempfile.gettempdir(), "dash.html")
|
|
35
|
+
return "http://127.0.0.1:9999/dash.html", _tmp
|
|
36
|
+
|
|
37
|
+
monkeypatch.setattr(mod.LockClient, "_prepare_dashboard_server", mock_prepare)
|
|
38
|
+
|
|
39
|
+
import webbrowser
|
|
40
|
+
|
|
41
|
+
monkeypatch.setattr(webbrowser, "open", lambda url: opened_urls.append(url))
|
|
42
|
+
|
|
43
|
+
lc = mod.LockClient(developer_id="test_user")
|
|
44
|
+
lc.dashboard()
|
|
45
|
+
assert len(opened_urls) == 1
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_dashboard_no_url(monkeypatch):
|
|
49
|
+
"""Test dashboard() when _prepare_dashboard_server returns None."""
|
|
50
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
51
|
+
monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
|
|
52
|
+
|
|
53
|
+
monkeypatch.setattr(
|
|
54
|
+
mod, "_get_create_client", lambda: make_create_client(FakeResponse())
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def mock_prepare(self):
|
|
58
|
+
return None, None
|
|
59
|
+
|
|
60
|
+
monkeypatch.setattr(mod.LockClient, "_prepare_dashboard_server", mock_prepare)
|
|
61
|
+
|
|
62
|
+
lc = mod.LockClient(developer_id="test_user")
|
|
63
|
+
lc.dashboard() # Should return early without error
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_dashboard_browser_exception(monkeypatch, capsys):
|
|
67
|
+
"""Test dashboard() handles browser open failure."""
|
|
68
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
69
|
+
monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
|
|
70
|
+
|
|
71
|
+
monkeypatch.setattr(
|
|
72
|
+
mod, "_get_create_client", lambda: make_create_client(FakeResponse())
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def mock_prepare(self):
|
|
76
|
+
_tmp = os.path.join(tempfile.gettempdir(), "dash.html")
|
|
77
|
+
return "http://127.0.0.1:9999/dash.html", _tmp
|
|
78
|
+
|
|
79
|
+
monkeypatch.setattr(mod.LockClient, "_prepare_dashboard_server", mock_prepare)
|
|
80
|
+
|
|
81
|
+
import webbrowser
|
|
82
|
+
|
|
83
|
+
monkeypatch.setattr(
|
|
84
|
+
webbrowser, "open", mock.Mock(side_effect=Exception("No browser"))
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
lc = mod.LockClient(developer_id="test_user")
|
|
88
|
+
lc.dashboard()
|
|
89
|
+
captured = capsys.readouterr()
|
|
90
|
+
assert "open in browser" in captured.out.lower() or "http" in captured.out.lower()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_prepare_dashboard_server_missing_html(monkeypatch):
|
|
94
|
+
"""Test _prepare_dashboard_server when index.html is missing."""
|
|
95
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
96
|
+
monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
|
|
97
|
+
|
|
98
|
+
monkeypatch.setattr(
|
|
99
|
+
mod, "_get_create_client", lambda: make_create_client(FakeResponse())
|
|
100
|
+
)
|
|
101
|
+
monkeypatch.setattr(mod, "_RESOURCE_ROOT", "/nonexistent/path")
|
|
102
|
+
|
|
103
|
+
lc = mod.LockClient(developer_id="test_user")
|
|
104
|
+
url, tmp_path = lc._prepare_dashboard_server()
|
|
105
|
+
assert url is None
|
|
106
|
+
assert tmp_path is None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_prepare_dashboard_server_success(monkeypatch, tmp_path):
|
|
110
|
+
"""Test _prepare_dashboard_server creates server and returns URL."""
|
|
111
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
112
|
+
monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
|
|
113
|
+
|
|
114
|
+
# Create fake dashboard directory with index.html
|
|
115
|
+
dash_dir = tmp_path / "dashboard"
|
|
116
|
+
dash_dir.mkdir()
|
|
117
|
+
(dash_dir / "index.html").write_text("<html><body>Test</body></html>")
|
|
118
|
+
|
|
119
|
+
monkeypatch.setattr(mod, "_RESOURCE_ROOT", str(tmp_path))
|
|
120
|
+
monkeypatch.setattr(
|
|
121
|
+
mod, "_get_create_client", lambda: make_create_client(FakeResponse())
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
lc = mod.LockClient(developer_id="test_user")
|
|
125
|
+
url, tmp_file = lc._prepare_dashboard_server()
|
|
126
|
+
|
|
127
|
+
# Should return a valid URL
|
|
128
|
+
if url:
|
|
129
|
+
assert "http://127.0.0.1" in url
|
|
130
|
+
# Clean up temp file if created
|
|
131
|
+
if tmp_file and os.path.exists(tmp_file):
|
|
132
|
+
os.unlink(tmp_file)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_prepare_dashboard_server_read_error(monkeypatch, tmp_path):
|
|
136
|
+
"""Test _prepare_dashboard_server when reading HTML file fails."""
|
|
137
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
138
|
+
monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
|
|
139
|
+
|
|
140
|
+
# Create dashboard dir with index.html, but make it unreadable
|
|
141
|
+
dash_dir = tmp_path / "dashboard"
|
|
142
|
+
dash_dir.mkdir()
|
|
143
|
+
html_file = dash_dir / "index.html"
|
|
144
|
+
html_file.write_text("<html></html>")
|
|
145
|
+
|
|
146
|
+
monkeypatch.setattr(mod, "_RESOURCE_ROOT", str(tmp_path))
|
|
147
|
+
monkeypatch.setattr(
|
|
148
|
+
mod, "_get_create_client", lambda: make_create_client(FakeResponse())
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Mock open to raise IOError on the specific file
|
|
152
|
+
original_open = open
|
|
153
|
+
|
|
154
|
+
def failing_open(path, *args, **kwargs):
|
|
155
|
+
if "index.html" in str(path):
|
|
156
|
+
raise IOError("Permission denied")
|
|
157
|
+
return original_open(path, *args, **kwargs)
|
|
158
|
+
|
|
159
|
+
monkeypatch.setattr("builtins.open", failing_open)
|
|
160
|
+
|
|
161
|
+
lc = mod.LockClient(developer_id="test_user")
|
|
162
|
+
url, tmp_file = lc._prepare_dashboard_server()
|
|
163
|
+
assert url is None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_prepare_dashboard_server_tmpfile_error(monkeypatch, tmp_path):
|
|
167
|
+
"""Test _prepare_dashboard_server when tmpfile creation fails."""
|
|
168
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
169
|
+
monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
|
|
170
|
+
|
|
171
|
+
dash_dir = tmp_path / "dashboard"
|
|
172
|
+
dash_dir.mkdir()
|
|
173
|
+
(dash_dir / "index.html").write_text("<html></html>")
|
|
174
|
+
|
|
175
|
+
monkeypatch.setattr(mod, "_RESOURCE_ROOT", str(tmp_path))
|
|
176
|
+
monkeypatch.setattr(
|
|
177
|
+
mod, "_get_create_client", lambda: make_create_client(FakeResponse())
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Mock tempfile to raise
|
|
181
|
+
import tempfile
|
|
182
|
+
|
|
183
|
+
monkeypatch.setattr(
|
|
184
|
+
tempfile,
|
|
185
|
+
"NamedTemporaryFile",
|
|
186
|
+
mock.Mock(side_effect=OSError("Disk full")),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
lc = mod.LockClient(developer_id="test_user")
|
|
190
|
+
url, tmp_file = lc._prepare_dashboard_server()
|
|
191
|
+
assert url is None
|
|
192
|
+
assert tmp_file is None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_prepare_dashboard_server_http_error(monkeypatch, tmp_path):
|
|
196
|
+
"""Test _prepare_dashboard_server when HTTP server fails."""
|
|
197
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
198
|
+
monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
|
|
199
|
+
|
|
200
|
+
dash_dir = tmp_path / "dashboard"
|
|
201
|
+
dash_dir.mkdir()
|
|
202
|
+
(dash_dir / "index.html").write_text("<html></html>")
|
|
203
|
+
|
|
204
|
+
monkeypatch.setattr(mod, "_RESOURCE_ROOT", str(tmp_path))
|
|
205
|
+
monkeypatch.setattr(
|
|
206
|
+
mod, "_get_create_client", lambda: make_create_client(FakeResponse())
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Mock http.server.ThreadingHTTPServer to raise
|
|
210
|
+
import http.server
|
|
211
|
+
|
|
212
|
+
monkeypatch.setattr(
|
|
213
|
+
http.server,
|
|
214
|
+
"ThreadingHTTPServer",
|
|
215
|
+
mock.Mock(side_effect=OSError("Port error")),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
lc = mod.LockClient(developer_id="test_user")
|
|
219
|
+
url, tmp_file = lc._prepare_dashboard_server()
|
|
220
|
+
assert url is None
|
|
221
|
+
assert tmp_file is None
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_prepare_dashboard_server_socket_probe_failure(monkeypatch, tmp_path):
|
|
225
|
+
"""Test _prepare_dashboard_server socket probe retry."""
|
|
226
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
227
|
+
monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
|
|
228
|
+
|
|
229
|
+
dash_dir = tmp_path / "dashboard"
|
|
230
|
+
dash_dir.mkdir()
|
|
231
|
+
(dash_dir / "index.html").write_text("<html></html>")
|
|
232
|
+
|
|
233
|
+
monkeypatch.setattr(mod, "_RESOURCE_ROOT", str(tmp_path))
|
|
234
|
+
monkeypatch.setattr(
|
|
235
|
+
mod, "_get_create_client", lambda: make_create_client(FakeResponse())
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
import http.server
|
|
239
|
+
import socket as _socket
|
|
240
|
+
|
|
241
|
+
# Create a real-ish server mock that binds but returns a port
|
|
242
|
+
# where sockets will initially fail then succeed
|
|
243
|
+
probe_count = [0]
|
|
244
|
+
original_create_connection = _socket.create_connection
|
|
245
|
+
|
|
246
|
+
def flaky_connection(addr, timeout=None):
|
|
247
|
+
probe_count[0] += 1
|
|
248
|
+
if probe_count[0] <= 2:
|
|
249
|
+
raise ConnectionRefusedError("not ready yet")
|
|
250
|
+
return original_create_connection(addr, timeout=timeout)
|
|
251
|
+
|
|
252
|
+
class FakeServerForProbe:
|
|
253
|
+
def __init__(self, addr, handler):
|
|
254
|
+
self.server_address = ("127.0.0.1", 19876)
|
|
255
|
+
|
|
256
|
+
def serve_forever(self):
|
|
257
|
+
pass
|
|
258
|
+
|
|
259
|
+
def shutdown(self):
|
|
260
|
+
pass
|
|
261
|
+
|
|
262
|
+
monkeypatch.setattr(http.server, "ThreadingHTTPServer", FakeServerForProbe)
|
|
263
|
+
monkeypatch.setattr(_socket, "create_connection", flaky_connection)
|
|
264
|
+
monkeypatch.setattr(mod.time, "sleep", lambda x: None)
|
|
265
|
+
|
|
266
|
+
lc = mod.LockClient(developer_id="test_user")
|
|
267
|
+
url, tmp_file = lc._prepare_dashboard_server()
|
|
268
|
+
|
|
269
|
+
# Should succeed after retries
|
|
270
|
+
if url:
|
|
271
|
+
assert "http://127.0.0.1" in url
|
|
272
|
+
if tmp_file and os.path.exists(tmp_file):
|
|
273
|
+
os.unlink(tmp_file)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_prepare_dashboard_server_unlink_error(monkeypatch, tmp_path):
|
|
277
|
+
"""Test _prepare_dashboard_server handles unlink error."""
|
|
278
|
+
monkeypatch.setenv("SUPABASE_URL", "https://test.supabase.co")
|
|
279
|
+
monkeypatch.setenv("SUPABASE_ANON_KEY", "test_key")
|
|
280
|
+
|
|
281
|
+
dash_dir = tmp_path / "dashboard"
|
|
282
|
+
dash_dir.mkdir()
|
|
283
|
+
(dash_dir / "index.html").write_text("<html></html>")
|
|
284
|
+
|
|
285
|
+
monkeypatch.setattr(mod, "_RESOURCE_ROOT", str(tmp_path))
|
|
286
|
+
monkeypatch.setattr(
|
|
287
|
+
mod, "_get_create_client", lambda: make_create_client(FakeResponse())
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
import http.server
|
|
291
|
+
|
|
292
|
+
# Server creation raises, triggering the except block
|
|
293
|
+
def raise_on_create(addr, handler):
|
|
294
|
+
raise OSError("Cannot bind")
|
|
295
|
+
|
|
296
|
+
monkeypatch.setattr(http.server, "ThreadingHTTPServer", raise_on_create)
|
|
297
|
+
|
|
298
|
+
# Also mock os.unlink to raise, covering unlink error
|
|
299
|
+
original_unlink = os.unlink
|
|
300
|
+
|
|
301
|
+
def failing_unlink(path):
|
|
302
|
+
if path.endswith(".html"):
|
|
303
|
+
raise OSError("Permission denied on unlink")
|
|
304
|
+
return original_unlink(path)
|
|
305
|
+
|
|
306
|
+
monkeypatch.setattr(os, "unlink", failing_unlink)
|
|
307
|
+
|
|
308
|
+
lc = mod.LockClient(developer_id="test_user")
|
|
309
|
+
url, tmp_file = lc._prepare_dashboard_server()
|
|
310
|
+
assert url is None
|
|
311
|
+
assert tmp_file is None
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# --- Appended from test_lock_client_dashboard_cli_cleanup.py ---
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def test_prepare_dashboard_server_cli_migrated(monkeypatch, tmp_path):
|
|
318
|
+
mod = load_lock_client_module()
|
|
319
|
+
|
|
320
|
+
# Use a temporary .collab/dashboard directory
|
|
321
|
+
monkeypatch.setattr(mod, "_RESOURCE_ROOT", str(tmp_path))
|
|
322
|
+
d = tmp_path / "dashboard"
|
|
323
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
324
|
+
html = d / "index.html"
|
|
325
|
+
html.write_text("<html>dashboard</html>")
|
|
326
|
+
|
|
327
|
+
# Fake ThreadingHTTPServer and Thread so we don't actually bind a port
|
|
328
|
+
import http.server as _http
|
|
329
|
+
import threading as _threading
|
|
330
|
+
|
|
331
|
+
class FakeServer:
|
|
332
|
+
def __init__(self, addr, handler):
|
|
333
|
+
self.server_address = ("127.0.0.1", 54321)
|
|
334
|
+
|
|
335
|
+
def serve_forever(self):
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
def shutdown(self):
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
class FakeThread:
|
|
342
|
+
def __init__(self, target, daemon=True):
|
|
343
|
+
self._target = target
|
|
344
|
+
|
|
345
|
+
def start(self):
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
monkeypatch.setattr(_http, "ThreadingHTTPServer", FakeServer)
|
|
349
|
+
monkeypatch.setattr(_threading, "Thread", FakeThread)
|
|
350
|
+
|
|
351
|
+
# Make socket.create_connection succeed immediately
|
|
352
|
+
class DummyConn:
|
|
353
|
+
def __enter__(self):
|
|
354
|
+
return self
|
|
355
|
+
|
|
356
|
+
def __exit__(self, exc_type, exc, tb):
|
|
357
|
+
return False
|
|
358
|
+
|
|
359
|
+
monkeypatch.setattr(socket, "create_connection", lambda *a, **k: DummyConn())
|
|
360
|
+
|
|
361
|
+
url, tmpfile = mod.LockClient(local_only=True)._prepare_dashboard_server()
|
|
362
|
+
assert url and tmpfile
|
|
363
|
+
txt = open(tmpfile, "r", encoding="utf-8").read()
|
|
364
|
+
assert "window.__SUPABASE_CONFIG__" in txt
|
|
365
|
+
try:
|
|
366
|
+
os.unlink(tmpfile)
|
|
367
|
+
except Exception:
|
|
368
|
+
pass
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def test_cleanup_orphaned_processes_unix_cli(monkeypatch, capsys):
|
|
372
|
+
mod = load_lock_client_module()
|
|
373
|
+
# Force UNIX branch
|
|
374
|
+
monkeypatch.setattr(sys, "platform", "linux", raising=False)
|
|
375
|
+
monkeypatch.setenv("COLLAB_TEST_MODE", "1")
|
|
376
|
+
|
|
377
|
+
out_line = "root 9999 0.0 0 0 ? S 0:00 python collab_test_lock_client"
|
|
378
|
+
|
|
379
|
+
def fake_run(*a, **k):
|
|
380
|
+
return types.SimpleNamespace(stdout=out_line)
|
|
381
|
+
|
|
382
|
+
monkeypatch.setattr("subprocess.run", fake_run)
|
|
383
|
+
|
|
384
|
+
killed = {"n": 0}
|
|
385
|
+
|
|
386
|
+
def fake_kill(pid, sig):
|
|
387
|
+
killed["n"] += 1
|
|
388
|
+
|
|
389
|
+
monkeypatch.setattr("os.kill", fake_kill)
|
|
390
|
+
|
|
391
|
+
client = mod.LockClient(local_only=True)
|
|
392
|
+
client._remove_pid = lambda: None
|
|
393
|
+
client.cleanup_orphaned_processes()
|
|
394
|
+
captured = capsys.readouterr()
|
|
395
|
+
assert killed["n"] >= 1
|
|
396
|
+
assert "Killing orphaned" in captured.out or "Killed" in captured.out
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def test_cli_force_release_all_and_cleanup_migrated(monkeypatch):
|
|
400
|
+
mod = load_lock_client_module()
|
|
401
|
+
|
|
402
|
+
class FakeLockClient:
|
|
403
|
+
last = None
|
|
404
|
+
|
|
405
|
+
def __init__(self, local_only=False):
|
|
406
|
+
FakeLockClient.last = self
|
|
407
|
+
self.is_admin = True
|
|
408
|
+
|
|
409
|
+
def force_release_all(self):
|
|
410
|
+
return 3
|
|
411
|
+
|
|
412
|
+
def cleanup_orphaned_processes(self):
|
|
413
|
+
self.cleaned = True
|
|
414
|
+
|
|
415
|
+
monkeypatch.setattr(mod, "LockClient", FakeLockClient)
|
|
416
|
+
|
|
417
|
+
# force-release-all should exit 0 when admin
|
|
418
|
+
monkeypatch.setattr( # type: ignore[arg-type]
|
|
419
|
+
sys, "argv", ["prog", "force-release-all"]
|
|
420
|
+
)
|
|
421
|
+
with pytest.raises(SystemExit) as exc:
|
|
422
|
+
mod._run_cli()
|
|
423
|
+
assert exc.value.code == 0
|
|
424
|
+
|
|
425
|
+
# cleanup should call the instance method (no SystemExit expected)
|
|
426
|
+
monkeypatch.setattr(sys, "argv", ["prog", "cleanup"]) # type: ignore[arg-type]
|
|
427
|
+
mod._run_cli()
|
|
428
|
+
assert getattr(FakeLockClient.last, "cleaned", False) is True
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def test_get_parent_ide_pid_vscode_cli_migrated(monkeypatch):
|
|
432
|
+
mod = load_lock_client_module()
|
|
433
|
+
monkeypatch.setenv("VSCODE_PID", "4321")
|
|
434
|
+
client = mod.LockClient(local_only=True)
|
|
435
|
+
monkeypatch.setattr(client, "_is_process_alive", lambda pid: True)
|
|
436
|
+
pid, method = client._get_parent_ide_pid()
|
|
437
|
+
assert pid == 4321
|
|
438
|
+
assert method == "vscode_pid"
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import types
|
|
4
|
+
|
|
5
|
+
from ._helpers import load_lock_client_module
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_discover_running_watchers_with_psutil_workspace_match(monkeypatch):
|
|
9
|
+
mod = load_lock_client_module()
|
|
10
|
+
|
|
11
|
+
class FakeProc:
|
|
12
|
+
def __init__(self, pid, cmdline):
|
|
13
|
+
self.info = {"pid": pid, "cmdline": cmdline}
|
|
14
|
+
|
|
15
|
+
def fake_process_iter(attrs=("pid", "cmdline")):
|
|
16
|
+
# One matching watcher (references project root), one ignored (current pid)
|
|
17
|
+
return [
|
|
18
|
+
FakeProc(
|
|
19
|
+
4242,
|
|
20
|
+
[
|
|
21
|
+
mod._PROJECT_ROOT,
|
|
22
|
+
".collab/pycharm/live_locks_watcher.py",
|
|
23
|
+
"--pid-file",
|
|
24
|
+
mod.PID_FILE,
|
|
25
|
+
],
|
|
26
|
+
),
|
|
27
|
+
FakeProc(os.getpid(), ["python", "other"]),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
fake_psutil = types.SimpleNamespace(process_iter=fake_process_iter)
|
|
31
|
+
monkeypatch.setitem(sys.modules, "psutil", fake_psutil)
|
|
32
|
+
|
|
33
|
+
client = mod.LockClient(local_only=True)
|
|
34
|
+
found = client._discover_running_watchers()
|
|
35
|
+
assert isinstance(found, list)
|
|
36
|
+
assert 4242 in found
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# -- appended migrated process-helper tests from extra split --
|
|
40
|
+
mod = load_lock_client_module()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_is_process_alive_win32_no_psutil_ctypes_fallback(monkeypatch):
|
|
44
|
+
import sys as _sys
|
|
45
|
+
|
|
46
|
+
_sys.platform = "win32"
|
|
47
|
+
monkeypatch.delitem(sys.modules, "psutil", raising=False)
|
|
48
|
+
|
|
49
|
+
fake_ctypes = types.SimpleNamespace(
|
|
50
|
+
windll=types.SimpleNamespace(
|
|
51
|
+
kernel32=types.SimpleNamespace(
|
|
52
|
+
OpenProcess=lambda a, b, c: 123,
|
|
53
|
+
GetExitCodeProcess=lambda h, ec: (setattr(ec, "value", 259) or True),
|
|
54
|
+
CloseHandle=lambda h: None,
|
|
55
|
+
GetLastError=lambda: 0,
|
|
56
|
+
)
|
|
57
|
+
),
|
|
58
|
+
c_ulong=lambda v: type("ULong", (), {"value": v})(),
|
|
59
|
+
byref=lambda x: x,
|
|
60
|
+
Structure=type("Structure", (), {}),
|
|
61
|
+
)
|
|
62
|
+
monkeypatch.setitem(sys.modules, "ctypes", fake_ctypes)
|
|
63
|
+
|
|
64
|
+
result = mod.LockClient._is_process_alive(os.getpid())
|
|
65
|
+
assert result is True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_is_process_alive_win32_process_exited(monkeypatch):
|
|
69
|
+
import sys as _sys
|
|
70
|
+
|
|
71
|
+
_sys.platform = "win32"
|
|
72
|
+
monkeypatch.delitem(sys.modules, "psutil", raising=False)
|
|
73
|
+
|
|
74
|
+
fake_ctypes = types.SimpleNamespace(
|
|
75
|
+
windll=types.SimpleNamespace(
|
|
76
|
+
kernel32=types.SimpleNamespace(
|
|
77
|
+
OpenProcess=lambda a, b, c: 123,
|
|
78
|
+
GetExitCodeProcess=lambda h, ec: (setattr(ec, "value", 1) or True),
|
|
79
|
+
CloseHandle=lambda h: None,
|
|
80
|
+
GetLastError=lambda: 0,
|
|
81
|
+
)
|
|
82
|
+
),
|
|
83
|
+
c_ulong=lambda v: type("ULong", (), {"value": v})(),
|
|
84
|
+
byref=lambda x: x,
|
|
85
|
+
Structure=type("Structure", (), {}),
|
|
86
|
+
)
|
|
87
|
+
monkeypatch.setitem(sys.modules, "ctypes", fake_ctypes)
|
|
88
|
+
|
|
89
|
+
result = mod.LockClient._is_process_alive(99999)
|
|
90
|
+
assert result is False
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_is_process_alive_win32_access_denied(monkeypatch):
|
|
94
|
+
import sys as _sys
|
|
95
|
+
|
|
96
|
+
_sys.platform = "win32"
|
|
97
|
+
monkeypatch.delitem(sys.modules, "psutil", raising=False)
|
|
98
|
+
|
|
99
|
+
class FakeKernel32:
|
|
100
|
+
def OpenProcess(self, a, b, c):
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
def GetLastError(self):
|
|
104
|
+
return 5
|
|
105
|
+
|
|
106
|
+
def CloseHandle(self, h):
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
def GetExitCodeProcess(self, h, ec):
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
fake_ctypes = types.SimpleNamespace(
|
|
113
|
+
windll=types.SimpleNamespace(kernel32=FakeKernel32()),
|
|
114
|
+
c_ulong=lambda v: type("ULong", (), {"value": v})(),
|
|
115
|
+
byref=lambda x: x,
|
|
116
|
+
Structure=type("Structure", (), {}),
|
|
117
|
+
)
|
|
118
|
+
monkeypatch.setitem(sys.modules, "ctypes", fake_ctypes)
|
|
119
|
+
|
|
120
|
+
result = mod.LockClient._is_process_alive(4)
|
|
121
|
+
assert result is True
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_is_process_alive_win32_tasklist_fallback(monkeypatch):
|
|
125
|
+
import sys as _sys
|
|
126
|
+
|
|
127
|
+
_sys.platform = "win32"
|
|
128
|
+
monkeypatch.delitem(sys.modules, "psutil", raising=False)
|
|
129
|
+
|
|
130
|
+
class FailKernel32:
|
|
131
|
+
def OpenProcess(self, a, b, c):
|
|
132
|
+
raise Exception("ctypes failing")
|
|
133
|
+
|
|
134
|
+
def GetLastError(self):
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
fake_ctypes = types.SimpleNamespace(
|
|
138
|
+
windll=types.SimpleNamespace(kernel32=FailKernel32()),
|
|
139
|
+
c_ulong=lambda v: type("ULong", (), {"value": v})(),
|
|
140
|
+
byref=lambda x: x,
|
|
141
|
+
Structure=type("Structure", (), {}),
|
|
142
|
+
)
|
|
143
|
+
monkeypatch.setitem(sys.modules, "ctypes", fake_ctypes)
|
|
144
|
+
|
|
145
|
+
def fake_check_output(cmd, **kw):
|
|
146
|
+
if "tasklist" in str(cmd):
|
|
147
|
+
return f"python.exe {os.getpid()} Console 1 12345 KB"
|
|
148
|
+
return b""
|
|
149
|
+
|
|
150
|
+
monkeypatch.setattr(mod.subprocess, "check_output", fake_check_output)
|
|
151
|
+
|
|
152
|
+
result = mod.LockClient._is_process_alive(os.getpid())
|
|
153
|
+
assert result is True
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_is_process_alive_unix(monkeypatch):
|
|
157
|
+
monkeypatch.setattr(sys, "platform", "linux")
|
|
158
|
+
monkeypatch.setattr(mod.os, "kill", lambda pid, sig: None)
|
|
159
|
+
result = mod.LockClient._is_process_alive(12345)
|
|
160
|
+
assert result is True
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_is_process_alive_unix_dead(monkeypatch):
|
|
164
|
+
monkeypatch.setattr(sys, "platform", "linux")
|
|
165
|
+
|
|
166
|
+
def raise_lookup(pid, sig):
|
|
167
|
+
raise ProcessLookupError()
|
|
168
|
+
|
|
169
|
+
monkeypatch.setattr(mod.os, "kill", raise_lookup)
|
|
170
|
+
result = mod.LockClient._is_process_alive(99999)
|
|
171
|
+
assert result is False
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_discover_running_watchers_no_psutil_win32(monkeypatch):
|
|
175
|
+
monkeypatch.setattr(sys, "platform", "win32")
|
|
176
|
+
monkeypatch.delitem(sys.modules, "psutil", raising=False)
|
|
177
|
+
|
|
178
|
+
def fake_subprocess_run(cmd, **kw):
|
|
179
|
+
if "python.exe" in str(cmd):
|
|
180
|
+
return types.SimpleNamespace(
|
|
181
|
+
stdout='"python.exe","%d","Console","1","12345 K"\n' % os.getpid(),
|
|
182
|
+
returncode=0,
|
|
183
|
+
)
|
|
184
|
+
return types.SimpleNamespace(stdout="", returncode=0)
|
|
185
|
+
|
|
186
|
+
monkeypatch.setattr(mod.subprocess, "run", fake_subprocess_run)
|
|
187
|
+
monkeypatch.setattr(
|
|
188
|
+
mod.LockClient,
|
|
189
|
+
"_get_cmdline_for_pid",
|
|
190
|
+
lambda self, pid: "python lock_client.py",
|
|
191
|
+
)
|
|
192
|
+
monkeypatch.setattr(
|
|
193
|
+
mod.LockClient, "_cmdline_matches_watcher", staticmethod(lambda cmd: True)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
client = mod.LockClient(local_only=True)
|
|
197
|
+
result = client._discover_running_watchers()
|
|
198
|
+
assert isinstance(result, list)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_discover_running_watchers_unix_no_psutil(monkeypatch):
|
|
202
|
+
monkeypatch.setattr(sys, "platform", "linux")
|
|
203
|
+
monkeypatch.delitem(sys.modules, "psutil", raising=False)
|
|
204
|
+
|
|
205
|
+
def fake_run(cmd, **kw):
|
|
206
|
+
stdout = "12345 python /path/.collab/core/lock_client.py watch\n"
|
|
207
|
+
return types.SimpleNamespace(stdout=stdout, returncode=0)
|
|
208
|
+
|
|
209
|
+
monkeypatch.setattr(mod.subprocess, "run", fake_run)
|
|
210
|
+
monkeypatch.setattr(
|
|
211
|
+
mod.LockClient, "_cmdline_matches_watcher", staticmethod(lambda cmd: True)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
client = mod.LockClient(local_only=True)
|
|
215
|
+
result = client._discover_running_watchers()
|
|
216
|
+
assert isinstance(result, list)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_get_process_info_local_non_windows(monkeypatch):
|
|
220
|
+
monkeypatch.setattr(sys, "platform", "linux")
|
|
221
|
+
client = mod.LockClient(local_only=True)
|
|
222
|
+
name, ppid = client._get_process_info_local(12345)
|
|
223
|
+
assert name is None and ppid is None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_get_process_info_local_no_wmic_tasklist_fallback(monkeypatch):
|
|
227
|
+
monkeypatch.setattr(sys, "platform", "win32")
|
|
228
|
+
monkeypatch.delitem(sys.modules, "psutil", raising=False)
|
|
229
|
+
monkeypatch.setattr(mod.shutil, "which", lambda cmd: None if cmd == "wmic" else cmd)
|
|
230
|
+
|
|
231
|
+
def fake_run(cmd, **kw):
|
|
232
|
+
return types.SimpleNamespace(
|
|
233
|
+
stdout='"python.exe","12345","Console","1","12345 K"\n',
|
|
234
|
+
returncode=0,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
monkeypatch.setattr(mod.subprocess, "run", fake_run)
|
|
238
|
+
|
|
239
|
+
client = mod.LockClient(local_only=True)
|
|
240
|
+
name = client._get_process_name_via_tasklist(12345)
|
|
241
|
+
assert name == "python.exe"
|