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.
Files changed (111) hide show
  1. {arrayview-0.42.0 → arrayview-0.42.1}/PKG-INFO +1 -1
  2. {arrayview-0.42.0 → arrayview-0.42.1}/pyproject.toml +1 -1
  3. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_launch_trace.py +59 -2
  4. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_query.py +26 -0
  5. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_server.py +22 -0
  6. arrayview-0.42.1/tests/test_launch_trace.py +231 -0
  7. arrayview-0.42.0/tests/test_launch_trace.py +0 -99
  8. {arrayview-0.42.0 → arrayview-0.42.1}/.gitignore +0 -0
  9. {arrayview-0.42.0 → arrayview-0.42.1}/LICENSE +0 -0
  10. {arrayview-0.42.0 → arrayview-0.42.1}/README.md +0 -0
  11. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/ARCHITECTURE.md +0 -0
  12. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/__init__.py +0 -0
  13. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/__main__.py +0 -0
  14. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_analysis.py +0 -0
  15. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_app.py +0 -0
  16. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_config.py +0 -0
  17. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_diagnostics.py +0 -0
  18. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_dicom.py +0 -0
  19. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_diff.py +0 -0
  20. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_icon.png +0 -0
  21. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_imaging.py +0 -0
  22. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_instance_registry.py +0 -0
  23. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_io.py +0 -0
  24. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_launch_plan.py +0 -0
  25. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_launcher.py +0 -0
  26. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_lifecycle.py +0 -0
  27. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_overlays.py +0 -0
  28. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_platform.py +0 -0
  29. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_render.py +0 -0
  30. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_analysis.py +0 -0
  31. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_drop.py +0 -0
  32. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_export.py +0 -0
  33. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_loading.py +0 -0
  34. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_persistence.py +0 -0
  35. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_preferences.py +0 -0
  36. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_preload.py +0 -0
  37. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_rendering.py +0 -0
  38. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_segmentation.py +0 -0
  39. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_state.py +0 -0
  40. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_vectorfield.py +0 -0
  41. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_routes_websocket.py +0 -0
  42. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_segmentation.py +0 -0
  43. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_session.py +0 -0
  44. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_session_spec.py +0 -0
  45. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_shell.html +0 -0
  46. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_source_safety.py +0 -0
  47. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_synthetic_mri.py +0 -0
  48. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_torch.py +0 -0
  49. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_tutorial.py +0 -0
  50. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_vectorfield.py +0 -0
  51. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_viewer.html +0 -0
  52. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_vscode.py +0 -0
  53. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_vscode_browser.py +0 -0
  54. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_vscode_extension.py +0 -0
  55. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_vscode_signal.py +0 -0
  56. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/_watch.py +0 -0
  57. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/arrayview-opener.vsix +0 -0
  58. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/gsap.min.js +0 -0
  59. {arrayview-0.42.0 → arrayview-0.42.1}/src/arrayview/todo.md +0 -0
  60. {arrayview-0.42.0 → arrayview-0.42.1}/tests/capture_patient_loading.py +0 -0
  61. {arrayview-0.42.0 → arrayview-0.42.1}/tests/capture_v_animation.py +0 -0
  62. {arrayview-0.42.0 → arrayview-0.42.1}/tests/conftest.py +0 -0
  63. {arrayview-0.42.0 → arrayview-0.42.1}/tests/lifecycle_matrix.py +0 -0
  64. {arrayview-0.42.0 → arrayview-0.42.1}/tests/make_vectorfield_test_arrays.py +0 -0
  65. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_1d_arrays.py +0 -0
  66. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_api.py +0 -0
  67. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_backend_shared.py +0 -0
  68. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_browser.py +0 -0
  69. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_cli.py +0 -0
  70. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_colorbar_hover_highlight.py +0 -0
  71. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_config.py +0 -0
  72. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_dicom.py +0 -0
  73. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_display_opener_results.py +0 -0
  74. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_doctor_commands.py +0 -0
  75. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_drop_import.py +0 -0
  76. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_instance_registry.py +0 -0
  77. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_invocation_contract_matrix.py +0 -0
  78. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_io.py +0 -0
  79. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_large_arrays.py +0 -0
  80. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_launch_blackbox.py +0 -0
  81. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_launch_diagnostics.py +0 -0
  82. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_launch_execution_ownership.py +0 -0
  83. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_launch_plan.py +0 -0
  84. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_lifecycle_contract.py +0 -0
  85. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_loading_server.py +0 -0
  86. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_mode_consistency.py +0 -0
  87. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_mode_matrix.py +0 -0
  88. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_mode_roundtrip.py +0 -0
  89. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_nifti_meta.py +0 -0
  90. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_nifti_series.py +0 -0
  91. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_preferences_ui.py +0 -0
  92. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_public_invocation_regressions.py +0 -0
  93. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_render_failure_reporting.py +0 -0
  94. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_render_progress.py +0 -0
  95. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_rgb_pixel_art.py +0 -0
  96. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_safe_server_stop.py +0 -0
  97. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_server_identity_status.py +0 -0
  98. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_session_spec.py +0 -0
  99. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_source_safety.py +0 -0
  100. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_spawn_ownership.py +0 -0
  101. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_torch.py +0 -0
  102. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_tutorial.py +0 -0
  103. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_tutorial_browser.py +0 -0
  104. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_value_formatting.py +0 -0
  105. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_view_handle_lifecycle.py +0 -0
  106. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_view_launch_execution.py +0 -0
  107. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_vscode_ack_protocol.py +0 -0
  108. {arrayview-0.42.0 → arrayview-0.42.1}/tests/test_window_level_preview.py +0 -0
  109. {arrayview-0.42.0 → arrayview-0.42.1}/tests/ui_audit.py +0 -0
  110. {arrayview-0.42.0 → arrayview-0.42.1}/tests/v_anim_frames/.gitkeep +0 -0
  111. {arrayview-0.42.0 → arrayview-0.42.1}/tests/visual_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arrayview
3
- Version: 0.42.0
3
+ Version: 0.42.1
4
4
  Summary: Fast multi-dimensional array viewer
5
5
  Project-URL: Home, https://github.com/oscarvanderheide/arrayview
6
6
  Project-URL: Source, https://github.com/oscarvanderheide/arrayview
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arrayview"
7
- version = "0.42.0"
7
+ version = "0.42.1"
8
8
  description = "Fast multi-dimensional array viewer"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.12"
@@ -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. Launch behavior must
5
- never depend on a trace write succeeding.
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