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,278 @@
|
|
|
1
|
+
"""Targeted tests for watch command branches in src/main.py."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from contextlib import nullcontext
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_run_cli_watch_pid_file_sets_namespace(monkeypatch):
|
|
10
|
+
import src.lock_client as lc
|
|
11
|
+
import src.main as main_mod
|
|
12
|
+
|
|
13
|
+
class _Client:
|
|
14
|
+
def __init__(self, local_only=False):
|
|
15
|
+
self.local_only = local_only
|
|
16
|
+
|
|
17
|
+
def watch(self, **kwargs):
|
|
18
|
+
called["kwargs"] = kwargs
|
|
19
|
+
|
|
20
|
+
called = {"kwargs": None}
|
|
21
|
+
|
|
22
|
+
monkeypatch.setattr(
|
|
23
|
+
main_mod,
|
|
24
|
+
"setup_collab_logging",
|
|
25
|
+
lambda **_k: None,
|
|
26
|
+
raising=False,
|
|
27
|
+
)
|
|
28
|
+
monkeypatch.setattr(main_mod, "_quiet_console_loggers", lambda: None, raising=False)
|
|
29
|
+
monkeypatch.setattr(main_mod, "LockClient", _Client, raising=False)
|
|
30
|
+
|
|
31
|
+
# _run_cli lazily imports these symbols from lock_client each invocation.
|
|
32
|
+
monkeypatch.setattr(lc, "LockClient", _Client)
|
|
33
|
+
monkeypatch.setattr(lc, "_quiet_console_loggers", lambda: None)
|
|
34
|
+
monkeypatch.setattr(lc, "_COLLAB_ROOT", ".collab")
|
|
35
|
+
|
|
36
|
+
import src.logging_config as logging_config
|
|
37
|
+
|
|
38
|
+
monkeypatch.setattr(logging_config, "setup_collab_logging", lambda **_k: None)
|
|
39
|
+
|
|
40
|
+
monkeypatch.setattr(
|
|
41
|
+
sys,
|
|
42
|
+
"argv",
|
|
43
|
+
[
|
|
44
|
+
"collab",
|
|
45
|
+
"watch",
|
|
46
|
+
"--pid-file",
|
|
47
|
+
"tmp.daemon.pid",
|
|
48
|
+
"--interval",
|
|
49
|
+
"2",
|
|
50
|
+
"--timeout",
|
|
51
|
+
"1",
|
|
52
|
+
],
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
main_mod._run_cli()
|
|
56
|
+
assert lc.PID_FILE == "tmp.daemon.pid"
|
|
57
|
+
assert called["kwargs"] is not None
|
|
58
|
+
assert called["kwargs"]["interval"] == 2
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_run_cli_active_auto_starts_and_reconciles(monkeypatch, capsys):
|
|
62
|
+
import src.lock_client as lc
|
|
63
|
+
import src.main as main_mod
|
|
64
|
+
|
|
65
|
+
called = {"status": 0, "start": 0, "reconcile": 0}
|
|
66
|
+
|
|
67
|
+
class _Client:
|
|
68
|
+
def __init__(self, local_only=False):
|
|
69
|
+
self.local_only = local_only
|
|
70
|
+
|
|
71
|
+
def daemon_status(self):
|
|
72
|
+
called["status"] += 1
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
def daemon_start(self):
|
|
76
|
+
called["start"] += 1
|
|
77
|
+
|
|
78
|
+
def _reconcile(self):
|
|
79
|
+
called["reconcile"] += 1
|
|
80
|
+
|
|
81
|
+
def active(self):
|
|
82
|
+
return []
|
|
83
|
+
|
|
84
|
+
monkeypatch.setattr(
|
|
85
|
+
main_mod,
|
|
86
|
+
"setup_collab_logging",
|
|
87
|
+
lambda **_k: None,
|
|
88
|
+
raising=False,
|
|
89
|
+
)
|
|
90
|
+
monkeypatch.setattr(
|
|
91
|
+
main_mod,
|
|
92
|
+
"_quiet_console_loggers",
|
|
93
|
+
lambda: nullcontext(),
|
|
94
|
+
raising=False,
|
|
95
|
+
)
|
|
96
|
+
monkeypatch.setattr(main_mod, "LockClient", _Client, raising=False)
|
|
97
|
+
|
|
98
|
+
monkeypatch.setattr(lc, "LockClient", _Client)
|
|
99
|
+
monkeypatch.setattr(lc, "_quiet_console_loggers", lambda: nullcontext())
|
|
100
|
+
monkeypatch.setattr(lc, "_COLLAB_ROOT", ".collab")
|
|
101
|
+
|
|
102
|
+
import src.logging_config as logging_config
|
|
103
|
+
|
|
104
|
+
monkeypatch.setattr(logging_config, "setup_collab_logging", lambda **_k: None)
|
|
105
|
+
|
|
106
|
+
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
|
|
107
|
+
monkeypatch.setenv("COLLAB_AUTO_START_WATCHER", "1")
|
|
108
|
+
monkeypatch.setattr(sys, "argv", ["collab", "active"])
|
|
109
|
+
|
|
110
|
+
main_mod._run_cli()
|
|
111
|
+
out = capsys.readouterr().out
|
|
112
|
+
|
|
113
|
+
assert called["status"] == 1
|
|
114
|
+
assert called["start"] == 1
|
|
115
|
+
assert called["reconcile"] == 1
|
|
116
|
+
assert "No active locks." in out
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_run_cli_active_auto_start_can_be_disabled(monkeypatch, capsys):
|
|
120
|
+
import src.lock_client as lc
|
|
121
|
+
import src.main as main_mod
|
|
122
|
+
|
|
123
|
+
called = {"status": 0, "start": 0, "reconcile": 0}
|
|
124
|
+
|
|
125
|
+
class _Client:
|
|
126
|
+
def __init__(self, local_only=False):
|
|
127
|
+
self.local_only = local_only
|
|
128
|
+
|
|
129
|
+
def daemon_status(self):
|
|
130
|
+
called["status"] += 1
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
def daemon_start(self):
|
|
134
|
+
called["start"] += 1
|
|
135
|
+
|
|
136
|
+
def _reconcile(self):
|
|
137
|
+
called["reconcile"] += 1
|
|
138
|
+
|
|
139
|
+
def active(self):
|
|
140
|
+
return []
|
|
141
|
+
|
|
142
|
+
monkeypatch.setattr(
|
|
143
|
+
main_mod,
|
|
144
|
+
"setup_collab_logging",
|
|
145
|
+
lambda **_k: None,
|
|
146
|
+
raising=False,
|
|
147
|
+
)
|
|
148
|
+
monkeypatch.setattr(
|
|
149
|
+
main_mod,
|
|
150
|
+
"_quiet_console_loggers",
|
|
151
|
+
lambda: nullcontext(),
|
|
152
|
+
raising=False,
|
|
153
|
+
)
|
|
154
|
+
monkeypatch.setattr(main_mod, "LockClient", _Client, raising=False)
|
|
155
|
+
|
|
156
|
+
monkeypatch.setattr(lc, "LockClient", _Client)
|
|
157
|
+
monkeypatch.setattr(lc, "_quiet_console_loggers", lambda: nullcontext())
|
|
158
|
+
monkeypatch.setattr(lc, "_COLLAB_ROOT", ".collab")
|
|
159
|
+
|
|
160
|
+
import src.logging_config as logging_config
|
|
161
|
+
|
|
162
|
+
monkeypatch.setattr(logging_config, "setup_collab_logging", lambda **_k: None)
|
|
163
|
+
|
|
164
|
+
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
|
|
165
|
+
monkeypatch.setenv("COLLAB_AUTO_START_WATCHER", "0")
|
|
166
|
+
monkeypatch.setattr(sys, "argv", ["collab", "active"])
|
|
167
|
+
|
|
168
|
+
main_mod._run_cli()
|
|
169
|
+
out = capsys.readouterr().out
|
|
170
|
+
|
|
171
|
+
assert called["status"] == 0
|
|
172
|
+
assert called["start"] == 0
|
|
173
|
+
assert called["reconcile"] == 0
|
|
174
|
+
assert "No active locks." in out
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_is_truthy_env_uses_default_when_missing(monkeypatch):
|
|
178
|
+
import src.main as main_mod
|
|
179
|
+
|
|
180
|
+
monkeypatch.delenv("COLLAB_TEST_BOOL", raising=False)
|
|
181
|
+
assert main_mod._is_truthy_env("COLLAB_TEST_BOOL", default=True) is True
|
|
182
|
+
assert main_mod._is_truthy_env("COLLAB_TEST_BOOL", default=False) is False
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_ensure_watcher_running_non_target_command(monkeypatch):
|
|
186
|
+
import src.main as main_mod
|
|
187
|
+
|
|
188
|
+
class _Client:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
|
|
192
|
+
monkeypatch.setenv("COLLAB_AUTO_START_WATCHER", "1")
|
|
193
|
+
assert main_mod._ensure_watcher_running(_Client(), "release") is False
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_ensure_watcher_running_skips_when_daemon_alive(monkeypatch):
|
|
197
|
+
import src.main as main_mod
|
|
198
|
+
|
|
199
|
+
class _Client:
|
|
200
|
+
def daemon_status(self):
|
|
201
|
+
return True
|
|
202
|
+
|
|
203
|
+
def daemon_start(self):
|
|
204
|
+
raise AssertionError("daemon_start should not be called")
|
|
205
|
+
|
|
206
|
+
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
|
|
207
|
+
monkeypatch.setenv("COLLAB_AUTO_START_WATCHER", "1")
|
|
208
|
+
assert main_mod._ensure_watcher_running(_Client(), "active") is False
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_ensure_watcher_running_start_failure(monkeypatch):
|
|
212
|
+
import src.main as main_mod
|
|
213
|
+
|
|
214
|
+
class _Client:
|
|
215
|
+
def daemon_status(self):
|
|
216
|
+
raise RuntimeError("status failed")
|
|
217
|
+
|
|
218
|
+
def daemon_start(self):
|
|
219
|
+
raise RuntimeError("start failed")
|
|
220
|
+
|
|
221
|
+
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
|
|
222
|
+
monkeypatch.setenv("COLLAB_AUTO_START_WATCHER", "1")
|
|
223
|
+
assert main_mod._ensure_watcher_running(_Client(), "status") is False
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_run_cli_active_reconcile_exception_is_non_fatal(monkeypatch, capsys):
|
|
227
|
+
import src.lock_client as lc
|
|
228
|
+
import src.main as main_mod
|
|
229
|
+
|
|
230
|
+
called = {"start": 0}
|
|
231
|
+
|
|
232
|
+
class _Client:
|
|
233
|
+
def __init__(self, local_only=False):
|
|
234
|
+
self.local_only = local_only
|
|
235
|
+
|
|
236
|
+
def daemon_status(self):
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
def daemon_start(self):
|
|
240
|
+
called["start"] += 1
|
|
241
|
+
|
|
242
|
+
def _reconcile(self):
|
|
243
|
+
raise RuntimeError("reconcile boom")
|
|
244
|
+
|
|
245
|
+
def active(self):
|
|
246
|
+
return []
|
|
247
|
+
|
|
248
|
+
monkeypatch.setattr(
|
|
249
|
+
main_mod,
|
|
250
|
+
"setup_collab_logging",
|
|
251
|
+
lambda **_k: None,
|
|
252
|
+
raising=False,
|
|
253
|
+
)
|
|
254
|
+
monkeypatch.setattr(
|
|
255
|
+
main_mod,
|
|
256
|
+
"_quiet_console_loggers",
|
|
257
|
+
lambda: nullcontext(),
|
|
258
|
+
raising=False,
|
|
259
|
+
)
|
|
260
|
+
monkeypatch.setattr(main_mod, "LockClient", _Client, raising=False)
|
|
261
|
+
|
|
262
|
+
monkeypatch.setattr(lc, "LockClient", _Client)
|
|
263
|
+
monkeypatch.setattr(lc, "_quiet_console_loggers", lambda: nullcontext())
|
|
264
|
+
monkeypatch.setattr(lc, "_COLLAB_ROOT", ".collab")
|
|
265
|
+
|
|
266
|
+
import src.logging_config as logging_config
|
|
267
|
+
|
|
268
|
+
monkeypatch.setattr(logging_config, "setup_collab_logging", lambda **_k: None)
|
|
269
|
+
|
|
270
|
+
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
|
|
271
|
+
monkeypatch.setenv("COLLAB_AUTO_START_WATCHER", "1")
|
|
272
|
+
monkeypatch.setattr(sys, "argv", ["collab", "active"])
|
|
273
|
+
|
|
274
|
+
main_mod._run_cli()
|
|
275
|
+
out = capsys.readouterr().out
|
|
276
|
+
|
|
277
|
+
assert called["start"] == 1
|
|
278
|
+
assert "No active locks." in out
|
tests/conftest.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import importlib.util
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import signal
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
# ============================================================================
|
|
15
|
+
# EARLY ISOLATION (MUST HAPPEN BEFORE TEST COLLECTION)
|
|
16
|
+
# ============================================================================
|
|
17
|
+
# Pytest imports test modules during the test collection phase, which happens
|
|
18
|
+
# *before* session-scoped fixtures execute. Since lock_client and watcher
|
|
19
|
+
# modules eagerly evaluate os.getenv("COLLAB_PID_FILE") at module load time,
|
|
20
|
+
# we MUST set test isolation variables at the top level of this file.
|
|
21
|
+
|
|
22
|
+
_session_temp_dir = tempfile.mkdtemp(prefix="collab_test_")
|
|
23
|
+
|
|
24
|
+
os.environ["COLLAB_STATE_DIR"] = _session_temp_dir
|
|
25
|
+
os.environ["COLLAB_PID_FILE"] = os.path.join(_session_temp_dir, "daemon.pid")
|
|
26
|
+
os.environ["COLLAB_TEST_MODE"] = "1"
|
|
27
|
+
|
|
28
|
+
# We forcibly mock these for ALL tests to prevent accidental production leakage.
|
|
29
|
+
# Individual tests can still use monkeypatch if they need specific dummy values.
|
|
30
|
+
os.environ["SUPABASE_URL"] = "http://localhost:54321"
|
|
31
|
+
os.environ["SUPABASE_ANON_KEY"] = "test-anon-key-session"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _cleanup_session_temp():
|
|
35
|
+
try:
|
|
36
|
+
shutil.rmtree(_session_temp_dir)
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _is_test_watcher_cmdline(cmdline: str) -> bool:
|
|
42
|
+
"""Return True for watcher processes started by test runs only."""
|
|
43
|
+
text = (cmdline or "").lower().replace('"', "")
|
|
44
|
+
if "lock_client.py watch" not in text:
|
|
45
|
+
return False
|
|
46
|
+
if "--daemon" not in text:
|
|
47
|
+
return False
|
|
48
|
+
if "--pid-file" not in text:
|
|
49
|
+
return False
|
|
50
|
+
# Strictly match temp test namespaces; never touch production .collab/.daemon.pid.
|
|
51
|
+
return (
|
|
52
|
+
"pytest-of-" in text
|
|
53
|
+
or "collab_test_" in text
|
|
54
|
+
or "mockcmms_pytest_collab_" in text
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _terminate_orphan_test_watchers() -> None:
|
|
59
|
+
"""Best-effort kill for orphaned test watcher daemons.
|
|
60
|
+
|
|
61
|
+
Keeps scope narrow to test-mode watcher cmdlines so production daemons are never
|
|
62
|
+
affected.
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
if os.name == "nt":
|
|
66
|
+
proc = subprocess.run(
|
|
67
|
+
[
|
|
68
|
+
"powershell",
|
|
69
|
+
"-NoProfile",
|
|
70
|
+
"-Command",
|
|
71
|
+
(
|
|
72
|
+
"Get-CimInstance Win32_Process | "
|
|
73
|
+
"Where-Object { $_.Name -eq 'pythonw.exe' "
|
|
74
|
+
"-or $_.Name -eq 'python.exe' } | "
|
|
75
|
+
"Select-Object ProcessId,CommandLine | "
|
|
76
|
+
"ConvertTo-Json -Compress"
|
|
77
|
+
),
|
|
78
|
+
],
|
|
79
|
+
capture_output=True,
|
|
80
|
+
text=True,
|
|
81
|
+
check=False,
|
|
82
|
+
)
|
|
83
|
+
raw = (proc.stdout or "").strip()
|
|
84
|
+
if not raw:
|
|
85
|
+
return
|
|
86
|
+
import json
|
|
87
|
+
|
|
88
|
+
rows = json.loads(raw)
|
|
89
|
+
if isinstance(rows, dict):
|
|
90
|
+
rows = [rows]
|
|
91
|
+
for row in rows:
|
|
92
|
+
pid = int(row.get("ProcessId", 0) or 0)
|
|
93
|
+
cmdline = row.get("CommandLine") or ""
|
|
94
|
+
if pid > 0 and _is_test_watcher_cmdline(cmdline):
|
|
95
|
+
try:
|
|
96
|
+
os.kill(pid, signal.SIGTERM)
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
else:
|
|
100
|
+
proc = subprocess.run(
|
|
101
|
+
["ps", "-eo", "pid,args"],
|
|
102
|
+
capture_output=True,
|
|
103
|
+
text=True,
|
|
104
|
+
check=False,
|
|
105
|
+
)
|
|
106
|
+
for line in (proc.stdout or "").splitlines():
|
|
107
|
+
m = re.match(r"^\s*(\d+)\s+(.*)$", line)
|
|
108
|
+
if not m:
|
|
109
|
+
continue
|
|
110
|
+
pid = int(m.group(1))
|
|
111
|
+
cmdline = m.group(2)
|
|
112
|
+
if _is_test_watcher_cmdline(cmdline):
|
|
113
|
+
try:
|
|
114
|
+
os.kill(pid, signal.SIGTERM)
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# Ensure test mode is explicitly kept, do not clear it, so late-firing
|
|
122
|
+
# atexit hooks attached to test processes still skip network calls.
|
|
123
|
+
atexit.register(_cleanup_session_temp)
|
|
124
|
+
atexit.register(_terminate_orphan_test_watchers)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def pytest_sessionfinish(session, exitstatus):
|
|
128
|
+
"""Cleanup any orphaned test watcher daemons at end of a pytest session."""
|
|
129
|
+
_terminate_orphan_test_watchers()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _load_logging_config_module():
|
|
133
|
+
collab_root = Path(__file__).resolve().parents[2]
|
|
134
|
+
logging_config_path = collab_root / "logging_config.py"
|
|
135
|
+
spec = importlib.util.spec_from_file_location(
|
|
136
|
+
"collab.logging_config_test_cleanup", str(logging_config_path)
|
|
137
|
+
)
|
|
138
|
+
assert spec and spec.loader
|
|
139
|
+
module = importlib.util.module_from_spec(spec)
|
|
140
|
+
spec.loader.exec_module(module) # type: ignore[arg-type]
|
|
141
|
+
return module
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@pytest.fixture(autouse=True)
|
|
145
|
+
def _close_collab_logging_after_each_test():
|
|
146
|
+
yield
|
|
147
|
+
try:
|
|
148
|
+
_load_logging_config_module().close_collab_logging()
|
|
149
|
+
except Exception:
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ============================================================================
|
|
154
|
+
# MOCKS & FIXTURES
|
|
155
|
+
# ============================================================================
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class FakeNotification:
|
|
159
|
+
def notify(self, **kwargs):
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class FakePlyer:
|
|
164
|
+
notification = FakeNotification()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
sys.modules["plyer"] = FakePlyer() # type: ignore[assignment]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""CI-only packaging smoke test.
|
|
2
|
+
|
|
3
|
+
Builds an sdist+wheel and verifies the produced wheel can be installed into an ephemeral
|
|
4
|
+
venv and that the installed package exposes the `collab` import surface and a non-empty
|
|
5
|
+
`__version__` string.
|
|
6
|
+
|
|
7
|
+
This test is intentionally marked `packaging` and should only run in CI.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _find_repo_root(start: Path) -> Path:
|
|
21
|
+
cur = start.resolve()
|
|
22
|
+
for p in [cur] + list(cur.parents):
|
|
23
|
+
if (p / "pyproject.toml").exists() or (p / "setup.py").exists():
|
|
24
|
+
return p
|
|
25
|
+
raise RuntimeError("Could not find project root (pyproject.toml or setup.py)")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.mark.packaging
|
|
29
|
+
def test_smoke_install_build_and_import(tmp_path: Path) -> None:
|
|
30
|
+
repo_root = _find_repo_root(Path(__file__))
|
|
31
|
+
dist_dir = tmp_path / "dist"
|
|
32
|
+
dist_dir.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
|
|
34
|
+
# Build sdist+wheel
|
|
35
|
+
subprocess.check_call(
|
|
36
|
+
[
|
|
37
|
+
sys.executable,
|
|
38
|
+
"-m",
|
|
39
|
+
"build",
|
|
40
|
+
"--sdist",
|
|
41
|
+
"--wheel",
|
|
42
|
+
"--outdir",
|
|
43
|
+
str(dist_dir),
|
|
44
|
+
],
|
|
45
|
+
cwd=str(repo_root),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Create ephemeral venv
|
|
49
|
+
venv_dir = tmp_path / "venv"
|
|
50
|
+
subprocess.check_call([sys.executable, "-m", "venv", str(venv_dir)])
|
|
51
|
+
if os.name == "nt":
|
|
52
|
+
venv_dir / "Scripts" / "pip.exe"
|
|
53
|
+
py = venv_dir / "Scripts" / "python.exe"
|
|
54
|
+
else:
|
|
55
|
+
venv_dir / "bin" / "pip"
|
|
56
|
+
py = venv_dir / "bin" / "python"
|
|
57
|
+
|
|
58
|
+
# Install wheel (use `python -m pip` which is more reliable across platforms)
|
|
59
|
+
wheels = list(dist_dir.glob("*.whl"))
|
|
60
|
+
assert wheels, "no wheel produced"
|
|
61
|
+
subprocess.check_call(
|
|
62
|
+
[str(py), "-m", "pip", "install", "--upgrade", "pip"]
|
|
63
|
+
) # ensure modern pip
|
|
64
|
+
subprocess.check_call([str(py), "-m", "pip", "install", str(wheels[0])])
|
|
65
|
+
|
|
66
|
+
# Validate import surface
|
|
67
|
+
subprocess.check_call(
|
|
68
|
+
[
|
|
69
|
+
str(py),
|
|
70
|
+
"-c",
|
|
71
|
+
"import importlib;print(importlib.import_module('collab').__version__)",
|
|
72
|
+
]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Validate CLI surface via module execution (portable across platforms)
|
|
76
|
+
subprocess.check_call([str(py), "-m", "collab", "--help"])
|