arrayview 0.42.0__tar.gz → 0.42.1__tar.gz
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.
- {arrayview-0.42.0 → arrayview-0.42.1}/PKG-INFO +1 -1
- {arrayview-0.42.0 → arrayview-0.42.1}/pyproject.toml +1 -1
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_launch_trace.py +59 -2
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_query.py +26 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_server.py +22 -0
- arrayview-0.42.1/tests/test_launch_trace.py +231 -0
- arrayview-0.42.0/tests/test_launch_trace.py +0 -99
- {arrayview-0.42.0 → arrayview-0.42.1}/.gitignore +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/LICENSE +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/README.md +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/__init__.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/__main__.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_analysis.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_app.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_config.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_diagnostics.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_dicom.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_diff.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_icon.png +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_imaging.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_instance_registry.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_io.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_launch_plan.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_launcher.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_lifecycle.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_overlays.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_platform.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_render.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_analysis.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_drop.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_export.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_loading.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_persistence.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_preferences.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_preload.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_rendering.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_segmentation.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_state.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_vectorfield.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_websocket.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_session.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_session_spec.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_shell.html +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_source_safety.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_synthetic_mri.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_torch.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_tutorial.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_vectorfield.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_viewer.html +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_vscode.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_vscode_browser.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_vscode_extension.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_vscode_signal.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_watch.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/gsap.min.js +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/todo.md +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/capture_patient_loading.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/capture_v_animation.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/conftest.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/lifecycle_matrix.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_1d_arrays.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_api.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_backend_shared.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_browser.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_cli.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_colorbar_hover_highlight.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_config.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_dicom.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_display_opener_results.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_doctor_commands.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_drop_import.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_instance_registry.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_invocation_contract_matrix.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_io.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_large_arrays.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_launch_blackbox.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_launch_diagnostics.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_launch_execution_ownership.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_launch_plan.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_lifecycle_contract.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_loading_server.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_mode_consistency.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_mode_roundtrip.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_nifti_series.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_preferences_ui.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_public_invocation_regressions.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_render_failure_reporting.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_render_progress.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_safe_server_stop.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_server_identity_status.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_session_spec.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_source_safety.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_spawn_ownership.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_torch.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_tutorial.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_tutorial_browser.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_value_formatting.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_view_handle_lifecycle.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_view_launch_execution.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_vscode_ack_protocol.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_window_level_preview.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/ui_audit.py +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/v_anim_frames/.gitkeep +0 -0
- {arrayview-0.42.0 → arrayview-0.42.1}/tests/visual_smoke.py +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"""Best-effort, opt-in launch event tracing.
|
|
2
2
|
|
|
3
3
|
The trace is deliberately dependency-free and inert unless
|
|
4
|
-
``ARRAYVIEW_LAUNCH_TRACE`` names an absolute JSONL file.
|
|
5
|
-
|
|
4
|
+
``ARRAYVIEW_LAUNCH_TRACE`` names an absolute JSONL file. Integrated-browser
|
|
5
|
+
route tracing can also be enabled explicitly with
|
|
6
|
+
``~/.arrayview/enable-launch-trace``. Launch behavior must never depend on a
|
|
7
|
+
trace write succeeding.
|
|
6
8
|
"""
|
|
7
9
|
|
|
8
10
|
from __future__ import annotations
|
|
@@ -20,6 +22,8 @@ TRACE_PATH_ENV = "ARRAYVIEW_LAUNCH_TRACE"
|
|
|
20
22
|
TRACE_ID_ENV = "ARRAYVIEW_LAUNCH_ID"
|
|
21
23
|
TRACE_ROLE_ENV = "ARRAYVIEW_LAUNCH_ROLE"
|
|
22
24
|
TRACE_SCHEMA = 1
|
|
25
|
+
TRACE_MARKER_NAME = "enable-launch-trace"
|
|
26
|
+
TRACE_DEFAULT_NAME = "launch-trace.jsonl"
|
|
23
27
|
_MAX_EVENT_BYTES = 4096
|
|
24
28
|
|
|
25
29
|
_lock = threading.Lock()
|
|
@@ -70,6 +74,56 @@ def trace_tag(value: object) -> str:
|
|
|
70
74
|
return hashlib.sha256(str(value).encode("utf-8", "replace")).hexdigest()[:12]
|
|
71
75
|
|
|
72
76
|
|
|
77
|
+
def configure_route_launch_trace() -> bool:
|
|
78
|
+
"""Enable route tracing from an explicit environment or marker opt-in.
|
|
79
|
+
|
|
80
|
+
Route handlers call this directly so an already-running server can notice
|
|
81
|
+
the marker even when its process environment never contained the trace
|
|
82
|
+
variable. An explicit environment value always wins, including an invalid
|
|
83
|
+
relative value, which keeps the normal trace contract unambiguous.
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
explicit = os.environ.get(TRACE_PATH_ENV)
|
|
87
|
+
if explicit is not None:
|
|
88
|
+
if not explicit or not Path(explicit).is_absolute():
|
|
89
|
+
return False
|
|
90
|
+
if _path != explicit:
|
|
91
|
+
configure_launch_trace()
|
|
92
|
+
return _path == explicit
|
|
93
|
+
|
|
94
|
+
trace_dir = Path.home() / ".arrayview"
|
|
95
|
+
if not (trace_dir / TRACE_MARKER_NAME).is_file():
|
|
96
|
+
return False
|
|
97
|
+
candidate = str(trace_dir / TRACE_DEFAULT_NAME)
|
|
98
|
+
if _path != candidate:
|
|
99
|
+
configure_launch_trace(path=candidate, role="server")
|
|
100
|
+
return _path == candidate
|
|
101
|
+
except Exception:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def emit_route_launch_event(
|
|
106
|
+
event: str,
|
|
107
|
+
*,
|
|
108
|
+
navigation_key: str,
|
|
109
|
+
tab_key: str | None = None,
|
|
110
|
+
**attrs: object,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Emit a route event without exposing private route credentials."""
|
|
113
|
+
try:
|
|
114
|
+
if not configure_route_launch_trace():
|
|
115
|
+
return
|
|
116
|
+
safe_attrs = {
|
|
117
|
+
"navigation_key_tag": trace_tag(navigation_key),
|
|
118
|
+
**attrs,
|
|
119
|
+
}
|
|
120
|
+
if tab_key is not None:
|
|
121
|
+
safe_attrs["tab_key_tag"] = trace_tag(tab_key)
|
|
122
|
+
emit_launch_event(event, **safe_attrs)
|
|
123
|
+
except Exception:
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
|
|
73
127
|
def emit_launch_event(event: str, **attrs: object) -> None:
|
|
74
128
|
"""Append one compact event, swallowing every tracing-related failure."""
|
|
75
129
|
try:
|
|
@@ -113,10 +167,13 @@ def _ensure_environment_configuration() -> None:
|
|
|
113
167
|
|
|
114
168
|
__all__ = [
|
|
115
169
|
"TRACE_ID_ENV",
|
|
170
|
+
"TRACE_MARKER_NAME",
|
|
116
171
|
"TRACE_PATH_ENV",
|
|
117
172
|
"TRACE_ROLE_ENV",
|
|
118
173
|
"configure_launch_trace",
|
|
174
|
+
"configure_route_launch_trace",
|
|
119
175
|
"emit_launch_event",
|
|
176
|
+
"emit_route_launch_event",
|
|
120
177
|
"trace_child_environment",
|
|
121
178
|
"trace_tag",
|
|
122
179
|
]
|
|
@@ -140,6 +140,20 @@ def register_query_routes(app, *, get_session_or_404, pil_image, pil_imageops) -
|
|
|
140
140
|
previous_navigation_key,
|
|
141
141
|
None,
|
|
142
142
|
)
|
|
143
|
+
try:
|
|
144
|
+
from arrayview._launch_trace import emit_route_launch_event
|
|
145
|
+
|
|
146
|
+
emit_route_launch_event(
|
|
147
|
+
"page.route_retired",
|
|
148
|
+
navigation_key=previous_navigation_key,
|
|
149
|
+
tab_key=previous_journal.get("tab_key"),
|
|
150
|
+
request_id=request_id,
|
|
151
|
+
navigation_attempt=previous_journal.get(
|
|
152
|
+
"navigation_attempt"
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
143
157
|
journal = journals[request_id] = {
|
|
144
158
|
"token": token,
|
|
145
159
|
"window_id": window_id,
|
|
@@ -159,6 +173,18 @@ def register_query_routes(app, *, get_session_or_404, pil_image, pil_imageops) -
|
|
|
159
173
|
sid,
|
|
160
174
|
request_id,
|
|
161
175
|
)
|
|
176
|
+
try:
|
|
177
|
+
from arrayview._launch_trace import emit_route_launch_event
|
|
178
|
+
|
|
179
|
+
emit_route_launch_event(
|
|
180
|
+
"page.route_prepared",
|
|
181
|
+
navigation_key=navigation_key,
|
|
182
|
+
tab_key=tab_key,
|
|
183
|
+
request_id=request_id,
|
|
184
|
+
navigation_attempt=navigation_attempt,
|
|
185
|
+
)
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
162
188
|
return {
|
|
163
189
|
"sid": sid,
|
|
164
190
|
"request_id": request_id,
|
|
@@ -422,6 +422,16 @@ def get_short_viewer_ui(
|
|
|
422
422
|
navigation_key: str,
|
|
423
423
|
):
|
|
424
424
|
"""Resolve a private integrated-browser launch without exposing its query."""
|
|
425
|
+
try:
|
|
426
|
+
from arrayview._launch_trace import emit_route_launch_event
|
|
427
|
+
|
|
428
|
+
emit_route_launch_event(
|
|
429
|
+
"page.route_entered",
|
|
430
|
+
navigation_key=navigation_key,
|
|
431
|
+
tab_key=tab_key,
|
|
432
|
+
)
|
|
433
|
+
except Exception:
|
|
434
|
+
pass
|
|
425
435
|
owner = _session_mod.VIEWER_LAUNCH_ROUTES.get(navigation_key)
|
|
426
436
|
if owner is None:
|
|
427
437
|
raise HTTPException(status_code=404, detail="Viewer launch route expired")
|
|
@@ -437,6 +447,18 @@ def get_short_viewer_ui(
|
|
|
437
447
|
or journal.get("navigation_key") != navigation_key
|
|
438
448
|
):
|
|
439
449
|
raise HTTPException(status_code=409, detail="Viewer launch route changed")
|
|
450
|
+
try:
|
|
451
|
+
from arrayview._launch_trace import emit_route_launch_event
|
|
452
|
+
|
|
453
|
+
emit_route_launch_event(
|
|
454
|
+
"page.route_resolved",
|
|
455
|
+
navigation_key=navigation_key,
|
|
456
|
+
tab_key=tab_key,
|
|
457
|
+
request_id=request_id,
|
|
458
|
+
navigation_attempt=journal["navigation_attempt"],
|
|
459
|
+
)
|
|
460
|
+
except Exception:
|
|
461
|
+
pass
|
|
440
462
|
bootstrap_query = journal["viewer_query"] + "&" + urlencode(
|
|
441
463
|
{
|
|
442
464
|
"_av_integrated_browser": "1",
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import stat
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from fastapi.testclient import TestClient
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _fresh_trace_module():
|
|
13
|
+
import arrayview._launch_trace as trace
|
|
14
|
+
|
|
15
|
+
return importlib.reload(trace)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_trace_is_disabled_without_an_absolute_path(monkeypatch, tmp_path):
|
|
19
|
+
monkeypatch.delenv("ARRAYVIEW_LAUNCH_TRACE", raising=False)
|
|
20
|
+
trace = _fresh_trace_module()
|
|
21
|
+
|
|
22
|
+
assert trace.configure_launch_trace(path="relative.jsonl") is None
|
|
23
|
+
trace.emit_launch_event("ignored")
|
|
24
|
+
|
|
25
|
+
assert not (tmp_path / "relative.jsonl").exists()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_route_trace_is_inert_without_marker(monkeypatch, tmp_path):
|
|
29
|
+
monkeypatch.delenv("ARRAYVIEW_LAUNCH_TRACE", raising=False)
|
|
30
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
31
|
+
trace = _fresh_trace_module()
|
|
32
|
+
|
|
33
|
+
trace.emit_route_launch_event(
|
|
34
|
+
"page.route_entered",
|
|
35
|
+
navigation_key="navigation-secret",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
assert not (tmp_path / ".arrayview" / "launch-trace.jsonl").exists()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_route_trace_marker_enables_home_default(monkeypatch, tmp_path):
|
|
42
|
+
monkeypatch.delenv("ARRAYVIEW_LAUNCH_TRACE", raising=False)
|
|
43
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
44
|
+
trace_dir = tmp_path / ".arrayview"
|
|
45
|
+
trace_dir.mkdir()
|
|
46
|
+
(trace_dir / "enable-launch-trace").touch()
|
|
47
|
+
trace = _fresh_trace_module()
|
|
48
|
+
|
|
49
|
+
trace.emit_route_launch_event(
|
|
50
|
+
"page.route_entered",
|
|
51
|
+
navigation_key="navigation-secret",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
row = json.loads((trace_dir / "launch-trace.jsonl").read_text())
|
|
55
|
+
assert row["event"] == "page.route_entered"
|
|
56
|
+
assert row["attrs"] == {
|
|
57
|
+
"navigation_key_tag": trace.trace_tag("navigation-secret")
|
|
58
|
+
}
|
|
59
|
+
assert "navigation-secret" not in json.dumps(row)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_route_trace_explicit_environment_precedes_marker(monkeypatch, tmp_path):
|
|
63
|
+
trace_dir = tmp_path / ".arrayview"
|
|
64
|
+
trace_dir.mkdir()
|
|
65
|
+
(trace_dir / "enable-launch-trace").touch()
|
|
66
|
+
explicit_path = tmp_path / "explicit.jsonl"
|
|
67
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
68
|
+
monkeypatch.setenv("ARRAYVIEW_LAUNCH_TRACE", str(explicit_path))
|
|
69
|
+
trace = _fresh_trace_module()
|
|
70
|
+
|
|
71
|
+
trace.emit_route_launch_event(
|
|
72
|
+
"page.route_entered",
|
|
73
|
+
navigation_key="navigation-secret",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
assert explicit_path.exists()
|
|
77
|
+
assert not (trace_dir / "launch-trace.jsonl").exists()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_trace_writes_private_jsonl_with_stable_schema(monkeypatch, tmp_path):
|
|
81
|
+
path = tmp_path / "launch.jsonl"
|
|
82
|
+
monkeypatch.setenv("ARRAYVIEW_LAUNCH_TRACE", str(path))
|
|
83
|
+
trace = _fresh_trace_module()
|
|
84
|
+
|
|
85
|
+
assert trace.configure_launch_trace(launch_id="launch-1", role="parent") == "launch-1"
|
|
86
|
+
trace.emit_launch_event("plan.selected", primary_display="native")
|
|
87
|
+
|
|
88
|
+
row = json.loads(path.read_text())
|
|
89
|
+
assert row["schema"] == 1
|
|
90
|
+
assert row["launch_id"] == "launch-1"
|
|
91
|
+
assert row["role"] == "parent"
|
|
92
|
+
assert row["seq"] == 1
|
|
93
|
+
assert row["event"] == "plan.selected"
|
|
94
|
+
assert row["attrs"] == {"primary_display": "native"}
|
|
95
|
+
assert stat.S_IMODE(path.stat().st_mode) == 0o600
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_child_environment_is_copied_not_mutated(monkeypatch, tmp_path):
|
|
99
|
+
path = tmp_path / "launch.jsonl"
|
|
100
|
+
monkeypatch.setenv("ARRAYVIEW_LAUNCH_TRACE", str(path))
|
|
101
|
+
monkeypatch.delenv("ARRAYVIEW_LAUNCH_ID", raising=False)
|
|
102
|
+
trace = _fresh_trace_module()
|
|
103
|
+
trace.configure_launch_trace(launch_id="launch-2", role="parent")
|
|
104
|
+
original = {"PATH": "/test/bin"}
|
|
105
|
+
|
|
106
|
+
child = trace.trace_child_environment(original)
|
|
107
|
+
|
|
108
|
+
assert original == {"PATH": "/test/bin"}
|
|
109
|
+
assert child == {
|
|
110
|
+
"PATH": "/test/bin",
|
|
111
|
+
"ARRAYVIEW_LAUNCH_TRACE": str(path),
|
|
112
|
+
"ARRAYVIEW_LAUNCH_ID": "launch-2",
|
|
113
|
+
"ARRAYVIEW_LAUNCH_ROLE": "daemon",
|
|
114
|
+
}
|
|
115
|
+
assert "ARRAYVIEW_LAUNCH_ID" not in os.environ
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_trace_failure_cannot_escape(monkeypatch, tmp_path):
|
|
119
|
+
directory = tmp_path / "not-a-file"
|
|
120
|
+
directory.mkdir()
|
|
121
|
+
monkeypatch.setenv("ARRAYVIEW_LAUNCH_TRACE", str(directory))
|
|
122
|
+
trace = _fresh_trace_module()
|
|
123
|
+
trace.configure_launch_trace(launch_id="launch-3")
|
|
124
|
+
|
|
125
|
+
trace.emit_launch_event("bad-path")
|
|
126
|
+
trace.emit_launch_event("bad-json", value=object())
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_two_processes_append_parseable_events(tmp_path):
|
|
130
|
+
path = tmp_path / "concurrent.jsonl"
|
|
131
|
+
code = (
|
|
132
|
+
"from arrayview._launch_trace import emit_launch_event;"
|
|
133
|
+
"[emit_launch_event('worker.event', value=i) for i in range(20)]"
|
|
134
|
+
)
|
|
135
|
+
base_env = dict(os.environ)
|
|
136
|
+
base_env["ARRAYVIEW_LAUNCH_TRACE"] = str(path)
|
|
137
|
+
base_env["ARRAYVIEW_LAUNCH_ID"] = "shared-launch"
|
|
138
|
+
processes = []
|
|
139
|
+
for role in ("worker-a", "worker-b"):
|
|
140
|
+
env = dict(base_env)
|
|
141
|
+
env["ARRAYVIEW_LAUNCH_ROLE"] = role
|
|
142
|
+
processes.append(subprocess.Popen([sys.executable, "-c", code], env=env))
|
|
143
|
+
|
|
144
|
+
for process in processes:
|
|
145
|
+
assert process.wait(timeout=10) == 0
|
|
146
|
+
|
|
147
|
+
rows = [json.loads(line) for line in path.read_text().splitlines()]
|
|
148
|
+
assert len(rows) == 40
|
|
149
|
+
assert {row["launch_id"] for row in rows} == {"shared-launch"}
|
|
150
|
+
assert {row["role"] for row in rows} == {"worker-a", "worker-b"}
|
|
151
|
+
for role in ("worker-a", "worker-b"):
|
|
152
|
+
assert sorted(row["seq"] for row in rows if row["role"] == role) == list(
|
|
153
|
+
range(1, 21)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_short_route_traces_preparation_resolution_and_stale_entry(
|
|
158
|
+
monkeypatch,
|
|
159
|
+
tmp_path,
|
|
160
|
+
):
|
|
161
|
+
import arrayview._session as session_mod
|
|
162
|
+
from arrayview._app import app
|
|
163
|
+
from arrayview._lifecycle import release_session
|
|
164
|
+
|
|
165
|
+
monkeypatch.delenv("ARRAYVIEW_LAUNCH_TRACE", raising=False)
|
|
166
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
167
|
+
trace_dir = tmp_path / ".arrayview"
|
|
168
|
+
trace_dir.mkdir()
|
|
169
|
+
(trace_dir / "enable-launch-trace").touch()
|
|
170
|
+
trace = _fresh_trace_module()
|
|
171
|
+
|
|
172
|
+
session = session_mod.Session(np.array([[1.0, 2.0], [3.0, 4.0]]))
|
|
173
|
+
session_mod.SESSIONS[session.sid] = session
|
|
174
|
+
phase_path = f"/viewer-phase/{session.sid}/route-trace-request"
|
|
175
|
+
tab_key = "tabkey0123456789"
|
|
176
|
+
first_key = "navkey0123456789"
|
|
177
|
+
second_key = "navkey9876543210"
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
with TestClient(app) as client:
|
|
181
|
+
server_id = client.get("/ping").json()["instance_id"]
|
|
182
|
+
|
|
183
|
+
def prepare(key, attempt, token):
|
|
184
|
+
return client.post(
|
|
185
|
+
phase_path,
|
|
186
|
+
json={
|
|
187
|
+
"phase": "launch-prepared",
|
|
188
|
+
"server_id": server_id,
|
|
189
|
+
"window_id": "route-trace-window",
|
|
190
|
+
"token": token,
|
|
191
|
+
"viewer_query": f"?sid={session.sid}",
|
|
192
|
+
"tab_key": tab_key,
|
|
193
|
+
"navigation_key": key,
|
|
194
|
+
"navigation_attempt": attempt,
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
assert prepare(first_key, 0, "token-one").status_code == 200
|
|
199
|
+
assert client.get(f"/_av/{tab_key}/{first_key}").status_code == 200
|
|
200
|
+
assert prepare(second_key, 1, "token-two").status_code == 200
|
|
201
|
+
assert client.get(f"/_av/{tab_key}/{first_key}").status_code == 404
|
|
202
|
+
|
|
203
|
+
rows = [
|
|
204
|
+
json.loads(line)
|
|
205
|
+
for line in (trace_dir / "launch-trace.jsonl").read_text().splitlines()
|
|
206
|
+
]
|
|
207
|
+
route_rows = [row for row in rows if row["event"].startswith("page.route_")]
|
|
208
|
+
assert [row["event"] for row in route_rows] == [
|
|
209
|
+
"page.route_prepared",
|
|
210
|
+
"page.route_entered",
|
|
211
|
+
"page.route_resolved",
|
|
212
|
+
"page.route_retired",
|
|
213
|
+
"page.route_prepared",
|
|
214
|
+
"page.route_entered",
|
|
215
|
+
]
|
|
216
|
+
assert route_rows[0]["attrs"]["request_id"] == "route-trace-request"
|
|
217
|
+
assert route_rows[0]["attrs"]["navigation_attempt"] == 0
|
|
218
|
+
assert route_rows[2]["attrs"]["request_id"] == "route-trace-request"
|
|
219
|
+
assert route_rows[2]["attrs"]["navigation_attempt"] == 0
|
|
220
|
+
assert route_rows[3]["attrs"]["navigation_attempt"] == 0
|
|
221
|
+
assert route_rows[4]["attrs"]["navigation_attempt"] == 1
|
|
222
|
+
assert route_rows[-1]["attrs"] == {
|
|
223
|
+
"navigation_key_tag": trace.trace_tag(first_key),
|
|
224
|
+
"tab_key_tag": trace.trace_tag(tab_key),
|
|
225
|
+
}
|
|
226
|
+
encoded = "\n".join(json.dumps(row) for row in route_rows)
|
|
227
|
+
assert first_key not in encoded
|
|
228
|
+
assert second_key not in encoded
|
|
229
|
+
assert tab_key not in encoded
|
|
230
|
+
finally:
|
|
231
|
+
release_session(session.sid)
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import importlib
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
import stat
|
|
5
|
-
import subprocess
|
|
6
|
-
import sys
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def _fresh_trace_module():
|
|
10
|
-
import arrayview._launch_trace as trace
|
|
11
|
-
|
|
12
|
-
return importlib.reload(trace)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def test_trace_is_disabled_without_an_absolute_path(monkeypatch, tmp_path):
|
|
16
|
-
monkeypatch.delenv("ARRAYVIEW_LAUNCH_TRACE", raising=False)
|
|
17
|
-
trace = _fresh_trace_module()
|
|
18
|
-
|
|
19
|
-
assert trace.configure_launch_trace(path="relative.jsonl") is None
|
|
20
|
-
trace.emit_launch_event("ignored")
|
|
21
|
-
|
|
22
|
-
assert not (tmp_path / "relative.jsonl").exists()
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def test_trace_writes_private_jsonl_with_stable_schema(monkeypatch, tmp_path):
|
|
26
|
-
path = tmp_path / "launch.jsonl"
|
|
27
|
-
monkeypatch.setenv("ARRAYVIEW_LAUNCH_TRACE", str(path))
|
|
28
|
-
trace = _fresh_trace_module()
|
|
29
|
-
|
|
30
|
-
assert trace.configure_launch_trace(launch_id="launch-1", role="parent") == "launch-1"
|
|
31
|
-
trace.emit_launch_event("plan.selected", primary_display="native")
|
|
32
|
-
|
|
33
|
-
row = json.loads(path.read_text())
|
|
34
|
-
assert row["schema"] == 1
|
|
35
|
-
assert row["launch_id"] == "launch-1"
|
|
36
|
-
assert row["role"] == "parent"
|
|
37
|
-
assert row["seq"] == 1
|
|
38
|
-
assert row["event"] == "plan.selected"
|
|
39
|
-
assert row["attrs"] == {"primary_display": "native"}
|
|
40
|
-
assert stat.S_IMODE(path.stat().st_mode) == 0o600
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def test_child_environment_is_copied_not_mutated(monkeypatch, tmp_path):
|
|
44
|
-
path = tmp_path / "launch.jsonl"
|
|
45
|
-
monkeypatch.setenv("ARRAYVIEW_LAUNCH_TRACE", str(path))
|
|
46
|
-
monkeypatch.delenv("ARRAYVIEW_LAUNCH_ID", raising=False)
|
|
47
|
-
trace = _fresh_trace_module()
|
|
48
|
-
trace.configure_launch_trace(launch_id="launch-2", role="parent")
|
|
49
|
-
original = {"PATH": "/test/bin"}
|
|
50
|
-
|
|
51
|
-
child = trace.trace_child_environment(original)
|
|
52
|
-
|
|
53
|
-
assert original == {"PATH": "/test/bin"}
|
|
54
|
-
assert child == {
|
|
55
|
-
"PATH": "/test/bin",
|
|
56
|
-
"ARRAYVIEW_LAUNCH_TRACE": str(path),
|
|
57
|
-
"ARRAYVIEW_LAUNCH_ID": "launch-2",
|
|
58
|
-
"ARRAYVIEW_LAUNCH_ROLE": "daemon",
|
|
59
|
-
}
|
|
60
|
-
assert "ARRAYVIEW_LAUNCH_ID" not in os.environ
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def test_trace_failure_cannot_escape(monkeypatch, tmp_path):
|
|
64
|
-
directory = tmp_path / "not-a-file"
|
|
65
|
-
directory.mkdir()
|
|
66
|
-
monkeypatch.setenv("ARRAYVIEW_LAUNCH_TRACE", str(directory))
|
|
67
|
-
trace = _fresh_trace_module()
|
|
68
|
-
trace.configure_launch_trace(launch_id="launch-3")
|
|
69
|
-
|
|
70
|
-
trace.emit_launch_event("bad-path")
|
|
71
|
-
trace.emit_launch_event("bad-json", value=object())
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def test_two_processes_append_parseable_events(tmp_path):
|
|
75
|
-
path = tmp_path / "concurrent.jsonl"
|
|
76
|
-
code = (
|
|
77
|
-
"from arrayview._launch_trace import emit_launch_event;"
|
|
78
|
-
"[emit_launch_event('worker.event', value=i) for i in range(20)]"
|
|
79
|
-
)
|
|
80
|
-
base_env = dict(os.environ)
|
|
81
|
-
base_env["ARRAYVIEW_LAUNCH_TRACE"] = str(path)
|
|
82
|
-
base_env["ARRAYVIEW_LAUNCH_ID"] = "shared-launch"
|
|
83
|
-
processes = []
|
|
84
|
-
for role in ("worker-a", "worker-b"):
|
|
85
|
-
env = dict(base_env)
|
|
86
|
-
env["ARRAYVIEW_LAUNCH_ROLE"] = role
|
|
87
|
-
processes.append(subprocess.Popen([sys.executable, "-c", code], env=env))
|
|
88
|
-
|
|
89
|
-
for process in processes:
|
|
90
|
-
assert process.wait(timeout=10) == 0
|
|
91
|
-
|
|
92
|
-
rows = [json.loads(line) for line in path.read_text().splitlines()]
|
|
93
|
-
assert len(rows) == 40
|
|
94
|
-
assert {row["launch_id"] for row in rows} == {"shared-launch"}
|
|
95
|
-
assert {row["role"] for row in rows} == {"worker-a", "worker-b"}
|
|
96
|
-
for role in ("worker-a", "worker-b"):
|
|
97
|
-
assert sorted(row["seq"] for row in rows if row["role"] == role) == list(
|
|
98
|
-
range(1, 21)
|
|
99
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|