arrayview 0.37.1__tar.gz → 0.38.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.37.1 → arrayview-0.38.1}/PKG-INFO +1 -1
- {arrayview-0.37.1 → arrayview-0.38.1}/pyproject.toml +1 -2
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_launch_plan.py +0 -1
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_platform.py +51 -2
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_vscode_browser.py +6 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_vscode_extension.py +31 -15
- arrayview-0.38.1/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_api.py +130 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_invocation_contract_matrix.py +0 -4
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_launch_plan.py +0 -1
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_lifecycle_contract.py +14 -0
- arrayview-0.37.1/src/arrayview/_codex_open.py +0 -137
- arrayview-0.37.1/src/arrayview/arrayview-opener.vsix +0 -0
- arrayview-0.37.1/tests/test_codex_open.py +0 -160
- {arrayview-0.37.1 → arrayview-0.38.1}/.gitignore +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/LICENSE +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/README.md +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/__init__.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/__main__.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_analysis.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_app.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_config.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_dicom.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_diff.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_icon.png +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_imaging.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_instance_registry.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_io.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_launch_trace.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_launcher.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_lifecycle.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_overlays.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_render.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_routes_analysis.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_routes_drop.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_routes_export.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_routes_loading.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_routes_persistence.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_routes_preferences.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_routes_preload.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_routes_query.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_routes_rendering.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_routes_segmentation.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_routes_state.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_routes_vectorfield.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_routes_websocket.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_server.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_session.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_session_spec.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_shell.html +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_synthetic_mri.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_torch.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_vectorfield.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_viewer.html +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_vscode.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/_vscode_signal.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/gsap.min.js +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/src/arrayview/todo.md +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/capture_patient_loading.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/capture_v_animation.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/conftest.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/lifecycle_matrix.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_backend_shared.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_browser.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_cli.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_colorbar_hover_highlight.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_command_reachability.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_config.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_cross_mode_parametrized.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_dicom.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_display_opener_results.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_doctor_commands.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_drop_import.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_instance_registry.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_interactions.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_io.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_large_arrays.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_launch_blackbox.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_launch_diagnostics.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_launch_execution_ownership.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_launch_trace.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_loading_server.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_mode_consistency.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_mode_roundtrip.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_nifti_series.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_preferences_ui.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_public_invocation_regressions.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_safe_server_stop.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_server_identity_status.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_session_spec.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_spawn_ownership.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_torch.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_value_formatting.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_view_component_integration.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_view_component_unit.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_view_handle_lifecycle.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_view_launch_execution.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/test_vscode_ack_protocol.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/ui_audit.py +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/v_anim_frames/.gitkeep +0 -0
- {arrayview-0.37.1 → arrayview-0.38.1}/tests/visual_smoke.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "arrayview"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.38.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"
|
|
@@ -43,7 +43,6 @@ dependencies = [
|
|
|
43
43
|
|
|
44
44
|
[project.scripts]
|
|
45
45
|
arrayview = "arrayview._launcher:arrayview"
|
|
46
|
-
arrayview-codex = "arrayview._codex_open:main"
|
|
47
46
|
|
|
48
47
|
[tool.hatch.build.targets.wheel]
|
|
49
48
|
packages = ["src/arrayview"]
|
|
@@ -6,6 +6,7 @@ import importlib.util
|
|
|
6
6
|
import hashlib
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
|
+
from collections.abc import Callable
|
|
9
10
|
import subprocess
|
|
10
11
|
import sys
|
|
11
12
|
|
|
@@ -450,6 +451,42 @@ def _process_is_alive(pid: object) -> bool:
|
|
|
450
451
|
return False
|
|
451
452
|
|
|
452
453
|
|
|
454
|
+
def _superseding_window_registration(
|
|
455
|
+
claimed_ids: list[str],
|
|
456
|
+
signal_dir: str,
|
|
457
|
+
read: Callable[[str], tuple[str, dict] | None],
|
|
458
|
+
) -> tuple[str, dict] | None:
|
|
459
|
+
"""Return the live window that replaced one of ``claimed_ids``, if unique.
|
|
460
|
+
|
|
461
|
+
Each registration lists the window ids it took over from, so a terminal left
|
|
462
|
+
holding a pre-reload id resolves to its own window rather than to a sibling
|
|
463
|
+
that merely shares a server root. Two live windows claiming one id would be a
|
|
464
|
+
contradiction, so an ambiguous answer is refused instead of guessed.
|
|
465
|
+
"""
|
|
466
|
+
if not claimed_ids:
|
|
467
|
+
return None
|
|
468
|
+
wanted = set(claimed_ids)
|
|
469
|
+
try:
|
|
470
|
+
filenames = os.listdir(signal_dir)
|
|
471
|
+
except OSError:
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
matches: list[tuple[str, dict]] = []
|
|
475
|
+
for filename in filenames:
|
|
476
|
+
if not filename.startswith("window-") or not filename.endswith(".json"):
|
|
477
|
+
continue
|
|
478
|
+
candidate = read(filename[len("window-") : -len(".json")])
|
|
479
|
+
if candidate is None:
|
|
480
|
+
continue
|
|
481
|
+
superseded = candidate[1].get("supersedes")
|
|
482
|
+
if not isinstance(superseded, list):
|
|
483
|
+
continue
|
|
484
|
+
if wanted.intersection(item for item in superseded if isinstance(item, str)):
|
|
485
|
+
matches.append(candidate)
|
|
486
|
+
|
|
487
|
+
return matches[0] if len(matches) == 1 else None
|
|
488
|
+
|
|
489
|
+
|
|
453
490
|
def _exact_vscode_window_registration(
|
|
454
491
|
ipc: str | None = None,
|
|
455
492
|
) -> tuple[str, dict] | None:
|
|
@@ -461,6 +498,11 @@ def _exact_vscode_window_registration(
|
|
|
461
498
|
remote registration is unsafe. When direct window evidence is unavailable,
|
|
462
499
|
match the live extension-host executable to the exact server root named by
|
|
463
500
|
the terminal's NLS configuration, and only accept a unique match.
|
|
501
|
+
|
|
502
|
+
Reloading a window rotates its IPC socket and re-injects ``ARRAYVIEW_WINDOW_ID``
|
|
503
|
+
into new terminals only, so a terminal that predates the reload keeps naming a
|
|
504
|
+
window id that no longer registers. The extension records the ids it replaced,
|
|
505
|
+
which turns that stale name back into an exact match on the window that owns it.
|
|
464
506
|
"""
|
|
465
507
|
signal_dir = os.path.expanduser("~/.arrayview")
|
|
466
508
|
|
|
@@ -479,16 +521,23 @@ def _exact_vscode_window_registration(
|
|
|
479
521
|
return None
|
|
480
522
|
return window_id, registration
|
|
481
523
|
|
|
524
|
+
claimed_ids: list[str] = []
|
|
482
525
|
registration = None
|
|
483
526
|
if ipc:
|
|
484
527
|
window_id = hashlib.sha256(ipc.encode()).hexdigest()[:16]
|
|
485
528
|
if reg := _read(window_id):
|
|
486
529
|
return reg
|
|
530
|
+
claimed_ids.append(window_id)
|
|
487
531
|
registration = reg
|
|
488
532
|
if registration is None:
|
|
489
533
|
window_id = os.environ.get("ARRAYVIEW_WINDOW_ID")
|
|
490
|
-
if window_id
|
|
491
|
-
|
|
534
|
+
if window_id:
|
|
535
|
+
if reg := _read(window_id):
|
|
536
|
+
return reg
|
|
537
|
+
claimed_ids.append(window_id)
|
|
538
|
+
|
|
539
|
+
if reg := _superseding_window_registration(claimed_ids, signal_dir, _read):
|
|
540
|
+
return reg
|
|
492
541
|
|
|
493
542
|
exact_cli = _current_vscode_remote_cli()
|
|
494
543
|
if exact_cli is None:
|
|
@@ -275,6 +275,12 @@ def _open_browser(
|
|
|
275
275
|
"ArrayView could not safely install its VS Code opener in "
|
|
276
276
|
"this exact remote window; no viewer request was sent."
|
|
277
277
|
)
|
|
278
|
+
elif _extension_state._VSCODE_EXT_NO_LIVE_WINDOW:
|
|
279
|
+
detail = (
|
|
280
|
+
"This terminal was opened before its VS Code window reloaded, "
|
|
281
|
+
"so it still points at a window that no longer exists. Open a "
|
|
282
|
+
"new terminal in the target window and retry."
|
|
283
|
+
)
|
|
278
284
|
elif _extension_state._VSCODE_EXT_RELOAD_REQUIRED:
|
|
279
285
|
detail = (
|
|
280
286
|
"ArrayView's VS Code opener was installed; reload this exact "
|
|
@@ -54,7 +54,8 @@ _VSCODE_EXT_INSTALLED = False # cached so we only check once per process
|
|
|
54
54
|
_VSCODE_EXT_FRESH_INSTALL = False # True if we just installed it this session
|
|
55
55
|
_VSCODE_EXT_RELOAD_REQUIRED = False # installed files are newer than the live host
|
|
56
56
|
_VSCODE_EXT_INSTALL_FAILED = False # automatic install could not complete safely
|
|
57
|
-
|
|
57
|
+
_VSCODE_EXT_NO_LIVE_WINDOW = False # no live host claims this terminal's window id
|
|
58
|
+
_VSCODE_EXT_VERSION = "0.14.89" # current bundled extension version
|
|
58
59
|
_VSCODE_CONFIGURED_PORTS: set[int] = set()
|
|
59
60
|
|
|
60
61
|
def _bundled_vscode_vsix_version(vsix_path: str) -> str | None:
|
|
@@ -471,9 +472,11 @@ def _ensure_vscode_extension(*, is_remote: bool | None = None) -> bool:
|
|
|
471
472
|
"""
|
|
472
473
|
global _VSCODE_EXT_INSTALLED, _VSCODE_EXT_FRESH_INSTALL
|
|
473
474
|
global _VSCODE_EXT_RELOAD_REQUIRED, _VSCODE_EXT_INSTALL_FAILED
|
|
475
|
+
global _VSCODE_EXT_NO_LIVE_WINDOW
|
|
474
476
|
_VSCODE_EXT_FRESH_INSTALL = False
|
|
475
477
|
_VSCODE_EXT_RELOAD_REQUIRED = False
|
|
476
478
|
_VSCODE_EXT_INSTALL_FAILED = False
|
|
479
|
+
_VSCODE_EXT_NO_LIVE_WINDOW = False
|
|
477
480
|
|
|
478
481
|
vsix_path = str(_pkg_files("arrayview").joinpath("arrayview-opener.vsix"))
|
|
479
482
|
if not os.path.isfile(vsix_path):
|
|
@@ -496,6 +499,31 @@ def _ensure_vscode_extension(*, is_remote: bool | None = None) -> bool:
|
|
|
496
499
|
and active_registration.get("extensionVersion") == active_version
|
|
497
500
|
else None
|
|
498
501
|
)
|
|
502
|
+
def _reject_unmatched_host(installed_version: str) -> bool:
|
|
503
|
+
"""Record why the live host was rejected, so callers can advise correctly.
|
|
504
|
+
|
|
505
|
+
A version disagreement is fixed by reloading; an unidentifiable window is
|
|
506
|
+
not. Conflating them tells the user to reload when the reload is what
|
|
507
|
+
stranded their terminal in the first place.
|
|
508
|
+
"""
|
|
509
|
+
global _VSCODE_EXT_RELOAD_REQUIRED, _VSCODE_EXT_NO_LIVE_WINDOW
|
|
510
|
+
if active_version is None:
|
|
511
|
+
_VSCODE_EXT_NO_LIVE_WINDOW = True
|
|
512
|
+
_vprint(
|
|
513
|
+
f"[ArrayView] opener v{installed_version} is installed, but no live "
|
|
514
|
+
"VS Code window claims this terminal; open a new terminal in the "
|
|
515
|
+
"target window",
|
|
516
|
+
flush=True,
|
|
517
|
+
)
|
|
518
|
+
return False
|
|
519
|
+
_VSCODE_EXT_RELOAD_REQUIRED = True
|
|
520
|
+
_vprint(
|
|
521
|
+
f"[ArrayView] opener v{installed_version} is installed, but this VS Code "
|
|
522
|
+
"window is still running an older extension host; reload this window",
|
|
523
|
+
flush=True,
|
|
524
|
+
)
|
|
525
|
+
return False
|
|
526
|
+
|
|
499
527
|
if _extension_on_disk(ext_version, vsix_path, remote=is_remote):
|
|
500
528
|
active_matches = (
|
|
501
529
|
active_version == ext_version
|
|
@@ -503,13 +531,7 @@ def _ensure_vscode_extension(*, is_remote: bool | None = None) -> bool:
|
|
|
503
531
|
else active_version in (None, ext_version)
|
|
504
532
|
)
|
|
505
533
|
if not active_matches:
|
|
506
|
-
|
|
507
|
-
_vprint(
|
|
508
|
-
f"[ArrayView] opener v{ext_version} is installed, but this VS Code "
|
|
509
|
-
"window is still running an older extension host; reload this window",
|
|
510
|
-
flush=True,
|
|
511
|
-
)
|
|
512
|
-
return False
|
|
534
|
+
return _reject_unmatched_host(ext_version)
|
|
513
535
|
if not is_remote:
|
|
514
536
|
_remove_old_extension_versions(ext_version, remote=False)
|
|
515
537
|
_VSCODE_EXT_INSTALLED = True
|
|
@@ -527,13 +549,7 @@ def _ensure_vscode_extension(*, is_remote: bool | None = None) -> bool:
|
|
|
527
549
|
else active_version in (None, newer_version)
|
|
528
550
|
)
|
|
529
551
|
if not active_matches:
|
|
530
|
-
|
|
531
|
-
_vprint(
|
|
532
|
-
f"[ArrayView] newer opener v{newer_version} is installed, but this "
|
|
533
|
-
"VS Code window is still running an older extension host; reload this window",
|
|
534
|
-
flush=True,
|
|
535
|
-
)
|
|
536
|
-
return False
|
|
552
|
+
return _reject_unmatched_host(newer_version)
|
|
537
553
|
_VSCODE_EXT_INSTALLED = True
|
|
538
554
|
_vprint(
|
|
539
555
|
f"[ArrayView] newer extension v{newer_version} is installed — keeping it",
|
|
Binary file
|
|
@@ -5667,6 +5667,136 @@ class TestPortAndTunnelHelpers:
|
|
|
5667
5667
|
is None
|
|
5668
5668
|
)
|
|
5669
5669
|
|
|
5670
|
+
def _write_reload_survivor_home(self, monkeypatch, tmp_path):
|
|
5671
|
+
"""Home whose only live window replaced the one this terminal remembers."""
|
|
5672
|
+
import hashlib
|
|
5673
|
+
|
|
5674
|
+
home = tmp_path / "home"
|
|
5675
|
+
signal_dir = home / ".arrayview"
|
|
5676
|
+
signal_dir.mkdir(parents=True)
|
|
5677
|
+
monkeypatch.setenv("HOME", str(home))
|
|
5678
|
+
monkeypatch.delenv("ARRAYVIEW_WINDOW_ID", raising=False)
|
|
5679
|
+
stale_ipc = "/run/user/1/vscode-ipc-before-reload.sock"
|
|
5680
|
+
return signal_dir, stale_ipc, hashlib.sha256(stale_ipc.encode()).hexdigest()[:16]
|
|
5681
|
+
|
|
5682
|
+
def test_terminal_stranded_by_window_reload_resolves_via_supersedes(
|
|
5683
|
+
self, monkeypatch, tmp_path
|
|
5684
|
+
):
|
|
5685
|
+
import os
|
|
5686
|
+
import arrayview._platform as platform
|
|
5687
|
+
|
|
5688
|
+
signal_dir, stale_ipc, stale_id = self._write_reload_survivor_home(
|
|
5689
|
+
monkeypatch, tmp_path
|
|
5690
|
+
)
|
|
5691
|
+
(signal_dir / "window-after-reload.json").write_text(
|
|
5692
|
+
json.dumps(
|
|
5693
|
+
{
|
|
5694
|
+
"pid": os.getpid(),
|
|
5695
|
+
"hookTag": "after-reload",
|
|
5696
|
+
"remoteName": "tunnel",
|
|
5697
|
+
"extensionVersion": "0.14.89",
|
|
5698
|
+
"supersedes": [stale_id],
|
|
5699
|
+
}
|
|
5700
|
+
)
|
|
5701
|
+
)
|
|
5702
|
+
|
|
5703
|
+
resolved = platform._exact_vscode_window_registration(stale_ipc)
|
|
5704
|
+
|
|
5705
|
+
assert resolved is not None
|
|
5706
|
+
assert resolved[0] == "after-reload"
|
|
5707
|
+
assert resolved[1]["extensionVersion"] == "0.14.89"
|
|
5708
|
+
|
|
5709
|
+
def test_stranded_terminal_resolves_stale_injected_window_id(
|
|
5710
|
+
self, monkeypatch, tmp_path
|
|
5711
|
+
):
|
|
5712
|
+
import os
|
|
5713
|
+
import arrayview._platform as platform
|
|
5714
|
+
|
|
5715
|
+
home = tmp_path / "home"
|
|
5716
|
+
signal_dir = home / ".arrayview"
|
|
5717
|
+
signal_dir.mkdir(parents=True)
|
|
5718
|
+
monkeypatch.setenv("HOME", str(home))
|
|
5719
|
+
monkeypatch.setenv("ARRAYVIEW_WINDOW_ID", "pre-reload-id")
|
|
5720
|
+
(signal_dir / "window-after-reload.json").write_text(
|
|
5721
|
+
json.dumps(
|
|
5722
|
+
{
|
|
5723
|
+
"pid": os.getpid(),
|
|
5724
|
+
"remoteName": "tunnel",
|
|
5725
|
+
"supersedes": ["older-still", "pre-reload-id"],
|
|
5726
|
+
}
|
|
5727
|
+
)
|
|
5728
|
+
)
|
|
5729
|
+
|
|
5730
|
+
resolved = platform._exact_vscode_window_registration(None)
|
|
5731
|
+
|
|
5732
|
+
assert resolved is not None and resolved[0] == "after-reload"
|
|
5733
|
+
|
|
5734
|
+
def test_stranded_terminal_never_borrows_an_unrelated_window(
|
|
5735
|
+
self, monkeypatch, tmp_path
|
|
5736
|
+
):
|
|
5737
|
+
"""A sibling window that never owned this terminal must not claim it."""
|
|
5738
|
+
import os
|
|
5739
|
+
import arrayview._platform as platform
|
|
5740
|
+
|
|
5741
|
+
signal_dir, stale_ipc, _stale_id = self._write_reload_survivor_home(
|
|
5742
|
+
monkeypatch, tmp_path
|
|
5743
|
+
)
|
|
5744
|
+
(signal_dir / "window-unrelated.json").write_text(
|
|
5745
|
+
json.dumps(
|
|
5746
|
+
{
|
|
5747
|
+
"pid": os.getpid(),
|
|
5748
|
+
"remoteName": "tunnel",
|
|
5749
|
+
"supersedes": ["some-other-window"],
|
|
5750
|
+
}
|
|
5751
|
+
)
|
|
5752
|
+
)
|
|
5753
|
+
monkeypatch.setattr(platform, "_current_vscode_remote_cli", lambda: None)
|
|
5754
|
+
|
|
5755
|
+
assert platform._exact_vscode_window_registration(stale_ipc) is None
|
|
5756
|
+
|
|
5757
|
+
def test_contested_superseded_id_is_refused_rather_than_guessed(
|
|
5758
|
+
self, monkeypatch, tmp_path
|
|
5759
|
+
):
|
|
5760
|
+
import os
|
|
5761
|
+
import arrayview._platform as platform
|
|
5762
|
+
|
|
5763
|
+
signal_dir, stale_ipc, stale_id = self._write_reload_survivor_home(
|
|
5764
|
+
monkeypatch, tmp_path
|
|
5765
|
+
)
|
|
5766
|
+
for name in ("window-first.json", "window-second.json"):
|
|
5767
|
+
(signal_dir / name).write_text(
|
|
5768
|
+
json.dumps(
|
|
5769
|
+
{
|
|
5770
|
+
"pid": os.getpid(),
|
|
5771
|
+
"remoteName": "tunnel",
|
|
5772
|
+
"supersedes": [stale_id],
|
|
5773
|
+
}
|
|
5774
|
+
)
|
|
5775
|
+
)
|
|
5776
|
+
monkeypatch.setattr(platform, "_current_vscode_remote_cli", lambda: None)
|
|
5777
|
+
|
|
5778
|
+
assert platform._exact_vscode_window_registration(stale_ipc) is None
|
|
5779
|
+
|
|
5780
|
+
def test_dead_superseding_window_does_not_resolve(self, monkeypatch, tmp_path):
|
|
5781
|
+
"""Lineage is not enough — the claiming host must still be running."""
|
|
5782
|
+
import arrayview._platform as platform
|
|
5783
|
+
|
|
5784
|
+
signal_dir, stale_ipc, stale_id = self._write_reload_survivor_home(
|
|
5785
|
+
monkeypatch, tmp_path
|
|
5786
|
+
)
|
|
5787
|
+
(signal_dir / "window-dead.json").write_text(
|
|
5788
|
+
json.dumps(
|
|
5789
|
+
{
|
|
5790
|
+
"pid": 2**31 - 1,
|
|
5791
|
+
"remoteName": "tunnel",
|
|
5792
|
+
"supersedes": [stale_id],
|
|
5793
|
+
}
|
|
5794
|
+
)
|
|
5795
|
+
)
|
|
5796
|
+
monkeypatch.setattr(platform, "_current_vscode_remote_cli", lambda: None)
|
|
5797
|
+
|
|
5798
|
+
assert platform._exact_vscode_window_registration(stale_ipc) is None
|
|
5799
|
+
|
|
5670
5800
|
def test_code_cli_selection_uses_captured_placement(self, monkeypatch):
|
|
5671
5801
|
import glob
|
|
5672
5802
|
import shutil
|
|
@@ -73,10 +73,6 @@ CONTRACT_CASES = (
|
|
|
73
73
|
"python_plain_ssh", Invocation.PYTHON, Environment.SSH,
|
|
74
74
|
ServerOwner.IN_PROCESS, Registration.IN_PROCESS_SESSION, Display.BROWSER,
|
|
75
75
|
),
|
|
76
|
-
ContractCase(
|
|
77
|
-
"codex_local", Invocation.CODEX, Environment.TERMINAL,
|
|
78
|
-
ServerOwner.IN_PROCESS, Registration.IN_PROCESS_SESSION, Display.BROWSER,
|
|
79
|
-
),
|
|
80
76
|
)
|
|
81
77
|
|
|
82
78
|
|
|
@@ -1701,6 +1701,20 @@ def test_vscode_open_ack_requires_requested_session_metadata():
|
|
|
1701
1701
|
assert "Viewer session did not become ready" in source
|
|
1702
1702
|
|
|
1703
1703
|
|
|
1704
|
+
def test_vscode_registration_publishes_superseded_window_ids():
|
|
1705
|
+
"""Terminals older than the last reload are only reachable via this chain."""
|
|
1706
|
+
source = (Path(__file__).resolve().parents[1] / "vscode-extension" / "extension.js").read_text()
|
|
1707
|
+
|
|
1708
|
+
# The previous id must be read before replace() overwrites the collection.
|
|
1709
|
+
assert source.index("_readEnvCollection('ARRAYVIEW_WINDOW_ID')") < source.index(
|
|
1710
|
+
"envCollection.replace('ARRAYVIEW_WINDOW_ID', windowId)"
|
|
1711
|
+
)
|
|
1712
|
+
assert "_readEnvCollection('ARRAYVIEW_WINDOW_CHAIN')" in source
|
|
1713
|
+
assert "envCollection.replace('ARRAYVIEW_WINDOW_CHAIN', supersededIds.join(','))" in source
|
|
1714
|
+
assert "supersedes: supersededIds," in source
|
|
1715
|
+
assert "supersedes.slice(0, MAX_SUPERSEDED_WINDOW_IDS)" in source
|
|
1716
|
+
|
|
1717
|
+
|
|
1704
1718
|
def test_vscode_tunnel_resolution_reuses_verified_routes_and_retries_fresh():
|
|
1705
1719
|
source = (Path(__file__).resolve().parents[1] / "vscode-extension" / "extension.js").read_text()
|
|
1706
1720
|
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
"""Codex-app helper for safely loading one file into ArrayView.
|
|
2
|
-
|
|
3
|
-
Usage:
|
|
4
|
-
uv run arrayview-codex path/to/file.nii
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import argparse
|
|
10
|
-
import os
|
|
11
|
-
import subprocess
|
|
12
|
-
import sys
|
|
13
|
-
import uuid
|
|
14
|
-
|
|
15
|
-
from arrayview._instance_registry import InstanceRegistry
|
|
16
|
-
from arrayview._launch_plan import (
|
|
17
|
-
Invocation,
|
|
18
|
-
LaunchIntent,
|
|
19
|
-
Registration,
|
|
20
|
-
create_launch_context,
|
|
21
|
-
)
|
|
22
|
-
from arrayview._launcher import (
|
|
23
|
-
_find_server_port,
|
|
24
|
-
_load_session_from_filepath,
|
|
25
|
-
_port_in_use,
|
|
26
|
-
_revalidate_launch_server,
|
|
27
|
-
_server_alive,
|
|
28
|
-
_server_pid,
|
|
29
|
-
_wait_for_port,
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def _start_loaded_server(
|
|
34
|
-
filepath: str, port: int, sid: str, name: str
|
|
35
|
-
) -> subprocess.Popen[bytes]:
|
|
36
|
-
script = (
|
|
37
|
-
"from arrayview._launcher import _serve_daemon; "
|
|
38
|
-
f"_serve_daemon({filepath!r}, {port}, {sid!r}, name={name!r}, persist=True)"
|
|
39
|
-
)
|
|
40
|
-
proc = subprocess.Popen(
|
|
41
|
-
[sys.executable, "-c", script],
|
|
42
|
-
stdin=subprocess.DEVNULL,
|
|
43
|
-
stdout=subprocess.DEVNULL,
|
|
44
|
-
stderr=subprocess.DEVNULL,
|
|
45
|
-
start_new_session=True,
|
|
46
|
-
)
|
|
47
|
-
if not _wait_for_port(port, timeout=15.0, tcp_only=True):
|
|
48
|
-
if proc.poll() is None:
|
|
49
|
-
proc.terminate()
|
|
50
|
-
raise RuntimeError(f"ArrayView server failed to start on port {port}")
|
|
51
|
-
if not _server_alive(port, timeout=2.0):
|
|
52
|
-
if proc.poll() is None:
|
|
53
|
-
proc.terminate()
|
|
54
|
-
raise RuntimeError(f"ArrayView server on port {port} did not become healthy")
|
|
55
|
-
actual_pid = _server_pid(port)
|
|
56
|
-
if actual_pid != proc.pid:
|
|
57
|
-
if proc.poll() is None:
|
|
58
|
-
proc.terminate()
|
|
59
|
-
raise RuntimeError(
|
|
60
|
-
f"Port {port} was claimed by another process while ArrayView was starting"
|
|
61
|
-
)
|
|
62
|
-
return proc
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def _open_codex_file(filepath: str, requested_port: int) -> str:
|
|
66
|
-
context = create_launch_context(
|
|
67
|
-
LaunchIntent(
|
|
68
|
-
Invocation.CODEX,
|
|
69
|
-
requested_port,
|
|
70
|
-
requested_window="browser",
|
|
71
|
-
persistent=True,
|
|
72
|
-
)
|
|
73
|
-
)
|
|
74
|
-
if not context.plan.ok:
|
|
75
|
-
raise ValueError(f"Invalid launch request: {context.plan.failure.value}")
|
|
76
|
-
|
|
77
|
-
name = os.path.basename(filepath)
|
|
78
|
-
expected_server_id = None
|
|
79
|
-
with InstanceRegistry().startup_lock(timeout=20.0):
|
|
80
|
-
port = context.plan.effective_port
|
|
81
|
-
already_running = context.plan.registration is Registration.HTTP_LOAD
|
|
82
|
-
if already_running:
|
|
83
|
-
port = _revalidate_launch_server(context, port)
|
|
84
|
-
expected_server_id = context.evidence.server.server_instance_id
|
|
85
|
-
else:
|
|
86
|
-
if _port_in_use(port):
|
|
87
|
-
port, appeared_running = _find_server_port(port + 1)
|
|
88
|
-
if appeared_running or _port_in_use(port):
|
|
89
|
-
raise RuntimeError("Could not find a free port for ArrayView")
|
|
90
|
-
if not 1 <= port <= 65535 or _port_in_use(port):
|
|
91
|
-
raise RuntimeError("Could not find a free port for ArrayView")
|
|
92
|
-
sid = uuid.uuid4().hex
|
|
93
|
-
_start_loaded_server(filepath, port, sid, name)
|
|
94
|
-
|
|
95
|
-
if already_running:
|
|
96
|
-
result = _load_session_from_filepath(
|
|
97
|
-
port,
|
|
98
|
-
filepath,
|
|
99
|
-
name,
|
|
100
|
-
expected_server_id=expected_server_id,
|
|
101
|
-
release_on_disconnect=True,
|
|
102
|
-
)
|
|
103
|
-
if "error" in result:
|
|
104
|
-
raise RuntimeError(f"ArrayView server rejected the file: {result['error']}")
|
|
105
|
-
sid = str(result["sid"])
|
|
106
|
-
|
|
107
|
-
return f"http://localhost:{port}/?sid={sid}"
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def main() -> int:
|
|
111
|
-
parser = argparse.ArgumentParser(
|
|
112
|
-
prog="arrayview-codex",
|
|
113
|
-
description=(
|
|
114
|
-
"Load one file into a compatible ArrayView server, starting one on "
|
|
115
|
-
"a free port when needed, and print the Codex app browser URL."
|
|
116
|
-
),
|
|
117
|
-
)
|
|
118
|
-
parser.add_argument("file", help="Array file to load")
|
|
119
|
-
parser.add_argument("--port", type=int, default=8000, help="Preferred port")
|
|
120
|
-
args = parser.parse_args()
|
|
121
|
-
|
|
122
|
-
filepath = os.path.abspath(args.file)
|
|
123
|
-
if not os.path.isfile(filepath):
|
|
124
|
-
print(f"Error: file not found: {filepath}", file=sys.stderr)
|
|
125
|
-
return 1
|
|
126
|
-
|
|
127
|
-
try:
|
|
128
|
-
url = _open_codex_file(filepath, args.port)
|
|
129
|
-
except (OSError, RuntimeError, TimeoutError, ValueError) as exc:
|
|
130
|
-
print(f"Error: {exc}", file=sys.stderr)
|
|
131
|
-
return 1
|
|
132
|
-
print(url)
|
|
133
|
-
return 0
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if __name__ == "__main__":
|
|
137
|
-
raise SystemExit(main())
|
|
Binary file
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from contextlib import contextmanager
|
|
4
|
-
from types import SimpleNamespace
|
|
5
|
-
|
|
6
|
-
from arrayview._launch_plan import Registration
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def _context(port: int, *, existing: bool = False):
|
|
10
|
-
return SimpleNamespace(
|
|
11
|
-
plan=SimpleNamespace(
|
|
12
|
-
ok=True,
|
|
13
|
-
effective_port=port,
|
|
14
|
-
failure=None,
|
|
15
|
-
registration=(
|
|
16
|
-
Registration.HTTP_LOAD
|
|
17
|
-
if existing
|
|
18
|
-
else Registration.DAEMON_STARTUP
|
|
19
|
-
),
|
|
20
|
-
),
|
|
21
|
-
evidence=SimpleNamespace(
|
|
22
|
-
server=SimpleNamespace(
|
|
23
|
-
server_instance_id="existing-server" if existing else None
|
|
24
|
-
)
|
|
25
|
-
)
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class _Registry:
|
|
30
|
-
def __init__(self, events: list[str]):
|
|
31
|
-
self.events = events
|
|
32
|
-
|
|
33
|
-
@contextmanager
|
|
34
|
-
def startup_lock(self, *, timeout: float):
|
|
35
|
-
self.events.append(f"lock:{timeout}")
|
|
36
|
-
try:
|
|
37
|
-
yield
|
|
38
|
-
finally:
|
|
39
|
-
self.events.append("unlock")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def test_codex_reuses_compatible_server_without_killing_or_spawning(
|
|
43
|
-
monkeypatch, tmp_path
|
|
44
|
-
):
|
|
45
|
-
import arrayview._codex_open as codex
|
|
46
|
-
|
|
47
|
-
filepath = tmp_path / "array.npy"
|
|
48
|
-
filepath.write_bytes(b"data")
|
|
49
|
-
events: list[str] = []
|
|
50
|
-
intents = []
|
|
51
|
-
monkeypatch.setattr(
|
|
52
|
-
codex,
|
|
53
|
-
"create_launch_context",
|
|
54
|
-
lambda intent: intents.append(intent) or _context(8000, existing=True),
|
|
55
|
-
)
|
|
56
|
-
monkeypatch.setattr(codex, "InstanceRegistry", lambda: _Registry(events))
|
|
57
|
-
monkeypatch.setattr(
|
|
58
|
-
codex,
|
|
59
|
-
"_revalidate_launch_server",
|
|
60
|
-
lambda context, port: port,
|
|
61
|
-
)
|
|
62
|
-
monkeypatch.setattr(
|
|
63
|
-
codex,
|
|
64
|
-
"_load_session_from_filepath",
|
|
65
|
-
lambda port, path, name, **kwargs: events.append(
|
|
66
|
-
f"load:{port}:{name}:{kwargs['expected_server_id']}:"
|
|
67
|
-
f"release={kwargs['release_on_disconnect']}"
|
|
68
|
-
)
|
|
69
|
-
or {"sid": "existing-sid"},
|
|
70
|
-
)
|
|
71
|
-
monkeypatch.setattr(
|
|
72
|
-
codex,
|
|
73
|
-
"_start_loaded_server",
|
|
74
|
-
lambda *args: (_ for _ in ()).throw(AssertionError("must reuse the server")),
|
|
75
|
-
)
|
|
76
|
-
monkeypatch.setattr(
|
|
77
|
-
codex.os,
|
|
78
|
-
"kill",
|
|
79
|
-
lambda *args: (_ for _ in ()).throw(AssertionError("must not kill a listener")),
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
url = codex._open_codex_file(str(filepath), 8000)
|
|
83
|
-
|
|
84
|
-
assert url == "http://localhost:8000/?sid=existing-sid"
|
|
85
|
-
assert events == [
|
|
86
|
-
"lock:20.0",
|
|
87
|
-
"unlock",
|
|
88
|
-
"load:8000:array.npy:existing-server:release=True",
|
|
89
|
-
]
|
|
90
|
-
assert intents[0].invocation.value == "codex"
|
|
91
|
-
assert intents[0].requested_window == "browser"
|
|
92
|
-
assert intents[0].persistent is True
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def test_codex_uses_free_port_and_starts_under_startup_lock(monkeypatch, tmp_path):
|
|
96
|
-
import arrayview._codex_open as codex
|
|
97
|
-
|
|
98
|
-
filepath = tmp_path / "array.npy"
|
|
99
|
-
filepath.write_bytes(b"data")
|
|
100
|
-
events: list[str] = []
|
|
101
|
-
monkeypatch.setattr(codex, "create_launch_context", lambda intent: _context(8001))
|
|
102
|
-
monkeypatch.setattr(codex, "InstanceRegistry", lambda: _Registry(events))
|
|
103
|
-
monkeypatch.setattr(codex, "_port_in_use", lambda port: False)
|
|
104
|
-
|
|
105
|
-
def start(path, port, sid, name):
|
|
106
|
-
assert events == ["lock:20.0"]
|
|
107
|
-
events.append(f"start:{port}:{name}")
|
|
108
|
-
return SimpleNamespace(pid=123)
|
|
109
|
-
|
|
110
|
-
monkeypatch.setattr(codex, "_start_loaded_server", start)
|
|
111
|
-
monkeypatch.setattr(codex.uuid, "uuid4", lambda: SimpleNamespace(hex="new-sid"))
|
|
112
|
-
|
|
113
|
-
url = codex._open_codex_file(str(filepath), 8000)
|
|
114
|
-
|
|
115
|
-
assert url == "http://localhost:8001/?sid=new-sid"
|
|
116
|
-
assert events == ["lock:20.0", "start:8001:array.npy", "unlock"]
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def test_spawned_server_must_match_child_pid(monkeypatch):
|
|
120
|
-
import arrayview._codex_open as codex
|
|
121
|
-
|
|
122
|
-
terminated: list[bool] = []
|
|
123
|
-
proc = SimpleNamespace(
|
|
124
|
-
pid=123,
|
|
125
|
-
poll=lambda: None,
|
|
126
|
-
terminate=lambda: terminated.append(True),
|
|
127
|
-
)
|
|
128
|
-
monkeypatch.setattr(codex.subprocess, "Popen", lambda *args, **kwargs: proc)
|
|
129
|
-
monkeypatch.setattr(codex, "_wait_for_port", lambda *args, **kwargs: True)
|
|
130
|
-
monkeypatch.setattr(codex, "_server_alive", lambda *args, **kwargs: True)
|
|
131
|
-
monkeypatch.setattr(codex, "_server_pid", lambda port: 456)
|
|
132
|
-
|
|
133
|
-
try:
|
|
134
|
-
codex._start_loaded_server("/tmp/array.npy", 8000, "sid", "array.npy")
|
|
135
|
-
except RuntimeError as exc:
|
|
136
|
-
assert "claimed by another process" in str(exc)
|
|
137
|
-
else:
|
|
138
|
-
raise AssertionError("PID mismatch must fail")
|
|
139
|
-
|
|
140
|
-
assert terminated == [True]
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def test_codex_main_reports_actual_selected_port(monkeypatch, tmp_path, capsys):
|
|
144
|
-
import arrayview._codex_open as codex
|
|
145
|
-
|
|
146
|
-
filepath = tmp_path / "array.npy"
|
|
147
|
-
filepath.write_bytes(b"data")
|
|
148
|
-
monkeypatch.setattr(
|
|
149
|
-
codex.sys,
|
|
150
|
-
"argv",
|
|
151
|
-
["arrayview-codex", str(filepath), "--port", "8000"],
|
|
152
|
-
)
|
|
153
|
-
monkeypatch.setattr(
|
|
154
|
-
codex,
|
|
155
|
-
"_open_codex_file",
|
|
156
|
-
lambda path, port: "http://localhost:8010/?sid=selected",
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
assert codex.main() == 0
|
|
160
|
-
assert capsys.readouterr().out.strip() == "http://localhost:8010/?sid=selected"
|
|
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
|