Skip to content

Commit 99fc8f8

Browse files
Donglai Weiclaude
andcommitted
ci: green the minimal-install test lanes (skip optional-dep tests; resolve split json)
PR #211's macOS / minimal-install CI lane is red on pre-existing failures unrelated to the malis change: several optional deps (numba, zarr, optuna, em_erl, waterz) and the ABISS `ws` binary are absent there, and config_io did not resolve per-split `json` paths. - config_io.resolve_data_paths: resolve per-split `json` like image/label/mask (fixes test_resolve_data_paths_resolves_test_json). Also drops an unused inference_cfg local and wraps a long message (flake8 F841/E501) so the file stays lint-clean now that it is in the diff. - guard optional-dependency tests with importorskip/skipif so they skip rather than error when the dep is absent, matching the existing zarr/tifffile idiom: waterz (both decoder test modules), optuna trial-timeout tests, zarr aux-cache test, em_erl nerl/skeleton tests, numba threshold-sensitivity, and the ABISS relative-script test (guarded on lib/abiss/build/ws). Verified: under a simulated minimal install the previously-failing tests now skip; with the deps installed they still pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c902373 commit 99fc8f8

8 files changed

Lines changed: 37 additions & 9 deletions

File tree

connectomics/config/pipeline/config_io.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,7 @@ def _raise_unconsumed_keys(yaml_conf: DictConfig) -> None:
153153
"(sibling of `monitor`, `inference`, `decoding`, `tune`)."
154154
),
155155
"use_timestamp": (
156-
"field removed. Train mode is always timestamped; "
157-
"test/tune modes are never timestamped."
156+
"field removed. Train mode is always timestamped; " "test/tune modes are never timestamped."
158157
),
159158
}
160159
_MONITOR_CHECKPOINT_ROOTS = (
@@ -201,8 +200,7 @@ def _reject_inference_runtime_alias_paths(explicit_field_paths: set[str]) -> Non
201200
alias_path = f"{root}.{alias}"
202201
if any(_path_is_or_descendant(path, alias_path) for path in explicit_field_paths):
203202
raise ValueError(
204-
f"`{alias_path}` was renamed. "
205-
f"Use `{root}.{canonical_tail}` instead."
203+
f"`{alias_path}` was renamed. " f"Use `{root}.{canonical_tail}` instead."
206204
)
207205

208206
for root in _MONITOR_CHECKPOINT_ROOTS:
@@ -212,8 +210,7 @@ def _reject_inference_runtime_alias_paths(explicit_field_paths: set[str]) -> Non
212210
if replacement.startswith("field "):
213211
raise ValueError(f"`{alias_path}` {replacement}")
214212
raise ValueError(
215-
f"`{alias_path}` was renamed. "
216-
f"Use `{root}.{replacement}` instead."
213+
f"`{alias_path}` was renamed. " f"Use `{root}.{replacement}` instead."
217214
)
218215

219216
# tune.output:* sub-block hoisted to tune.save_*
@@ -506,7 +503,6 @@ def validate_config(cfg: Config) -> None:
506503
if cfg.model.out_channels <= 0:
507504
raise ValueError("model.out_channels must be positive")
508505
model_heads = getattr(cfg.model, "heads", None) or {}
509-
inference_cfg = getattr(cfg, "inference", None)
510506
inference_head = get_inference_model_value(cfg, "head", None)
511507
images_cfg = getattr(getattr(getattr(cfg, "monitor", None), "logging", None), "images", None)
512508
visualization_head = getattr(images_cfg, "head", None) if images_cfg is not None else None
@@ -556,8 +552,8 @@ def validate_config(cfg: Config) -> None:
556552
missing = [h for h in inference_head_names if h not in model_heads]
557553
if missing:
558554
raise ValueError(
559-
f"inference.model.head={inference_head_names} references unknown heads {missing}; "
560-
f"available: {sorted(model_heads.keys())}."
555+
f"inference.model.head={inference_head_names} references unknown heads "
556+
f"{missing}; available: {sorted(model_heads.keys())}."
561557
)
562558
if (
563559
visualization_head is not None
@@ -909,6 +905,11 @@ def _resolve_split_paths(split_cfg):
909905
split_cfg.image = _combine_path(split_base, split_cfg.image)
910906
split_cfg.label = _combine_path(split_base, split_cfg.label)
911907
split_cfg.mask = _combine_path(split_base, split_cfg.mask)
908+
split_json_resolved = _combine_path(split_base, split_cfg.json)
909+
if isinstance(split_json_resolved, list):
910+
split_cfg.json = split_json_resolved[0] if split_json_resolved else None
911+
else:
912+
split_cfg.json = split_json_resolved
912913

913914
# Resolve inference/test paths from merged runtime cfg.data.
914915
if getattr(cfg.data, "test", None) is not None:

tests/integration/test_affinity_cc3d.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ def test_basic_functionality(self, simple_affinities):
109109
), f"Expected 3 labels (bg + 2 objects), got {len(unique_labels)}"
110110
assert 0 in unique_labels, "Background label 0 should be present"
111111

112+
@pytest.mark.skipif(not NUMBA_AVAILABLE, reason="numba required")
112113
def test_threshold_sensitivity(self, simple_affinities):
113114
"""Test that threshold parameter affects segmentation."""
114115
# Low threshold - more connected

tests/test_decode_waterz.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@
33
from __future__ import annotations
44

55
import numpy as np
6+
import pytest
67

78
from connectomics.decoding.decoders import waterz as waterz_decoder
89

10+
# decode_waterz calls into the real waterz package (waterz._uint8,
11+
# merge_function_to_scoring); the fakes below only stub waterz.waterz(). Skip
12+
# the whole module when waterz is not installed (e.g. minimal CI).
13+
pytest.importorskip("waterz")
14+
915

1016
class _FakeWaterzModule:
1117
"""Minimal waterz stub for testing wrapper behavior."""

tests/unit/test_data_factory_zarr_aux.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from pathlib import Path
22

33
import numpy as np
4+
import pytest
45

56
from connectomics.config import Config
67
from connectomics.data.io import save_volume
78
from connectomics.training.lightning.data_factory import _maybe_precompute_label_aux
89

910

1011
def test_maybe_precompute_label_aux_reuses_existing_zarr_cache(monkeypatch, tmp_path: Path):
12+
pytest.importorskip("zarr")
1113
label_path = tmp_path / "data.zarr" / "seg"
1214
aux_path = tmp_path / "data.zarr" / "seg_skeleton"
1315
save_volume(str(label_path), np.zeros((2, 2, 2), dtype=np.uint16), file_format="zarr")

tests/unit/test_decode_abiss_wrapper.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,21 @@
22

33
from __future__ import annotations
44

5+
import shutil
56
import sys
7+
from pathlib import Path
68

79
import numpy as np
810
import pytest
911

1012
from connectomics.decoding import decode_abiss
1113

14+
# The relative-script test below shells out to scripts/run_abiss_single.py, which
15+
# requires the compiled ABISS `ws` binary (lib/abiss/build/ws or on PATH). That
16+
# binary is absent in minimal installs (e.g. CI), so guard that test on it.
17+
_WS_BINARY = Path(__file__).resolve().parents[2] / "lib" / "abiss" / "build" / "ws"
18+
_ABISS_WS_AVAILABLE = _WS_BINARY.exists() or shutil.which("ws") is not None
19+
1220

1321
def test_decode_abiss_with_list_command_writes_npy_output():
1422
pred = np.zeros((3, 6, 8, 10), dtype=np.float32)
@@ -61,6 +69,7 @@ def test_decode_abiss_raises_if_output_missing():
6169
decode_abiss(pred, command=command)
6270

6371

72+
@pytest.mark.skipif(not _ABISS_WS_AVAILABLE, reason="ABISS ws binary not built (lib/abiss/build/ws)")
6473
def test_decode_abiss_with_relative_script_command_outside_repo_cwd(monkeypatch, tmp_path):
6574
pred = np.zeros((3, 6, 8, 10), dtype=np.float32)
6675
pred[:, 1:5, 2:7, 3:9] = 1.0

tests/unit/test_decode_waterz.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77

88
from connectomics.decoding.decoders import waterz as waterz_decoder
99

10+
# decode_waterz calls into the real waterz package (waterz._uint8,
11+
# merge_function_to_scoring); the fakes below only stub waterz.waterz(). Skip
12+
# the whole module when waterz is not installed (e.g. minimal CI).
13+
pytest.importorskip("waterz")
14+
1015

1116
class _FakeWaterzModule:
1217
"""Minimal waterz stub for testing wrapper behavior."""

tests/unit/test_optuna_tuner.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,7 @@ def _fake_decoder(predictions, **_kwargs):
562562

563563

564564
def test_objective_returns_bad_value_when_standard_trial_times_out(monkeypatch):
565+
pytest.importorskip("optuna")
565566
cfg = Config()
566567
cfg.tune = TuneConfig()
567568
cfg.tune.trial_timeout = 12
@@ -588,6 +589,7 @@ def _raise_timeout(_evaluation_kind, _payload):
588589

589590

590591
def test_objective_returns_bad_value_when_waterz_batch_trial_times_out(monkeypatch):
592+
pytest.importorskip("optuna")
591593
cfg = Config()
592594
cfg.tune = TuneConfig()
593595
cfg.tune.trial_timeout = 30

tests/unit/test_test_pipeline_multi_volume_eval.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ def test_run_evaluation_stage_accepts_plain_context_and_arrays():
181181

182182

183183
def test_compute_test_metrics_supports_nerl_without_dense_labels(tmp_path):
184+
pytest.importorskip("em_erl")
184185
ERLGraph, _, _ = import_em_erl()
185186
graph_path = tmp_path / "gt_graph.npz"
186187
graph = ERLGraph(
@@ -233,6 +234,7 @@ def test_compute_test_metrics_supports_nerl_without_dense_labels(tmp_path):
233234

234235

235236
def test_compute_test_metrics_supports_banis_skeleton_pickle(tmp_path):
237+
pytest.importorskip("em_erl")
236238
nx = pytest.importorskip("networkx")
237239
import pickle
238240

0 commit comments

Comments
 (0)