Skip to content

Commit cdf008f

Browse files
committed
Add tests for config_sync helpers
Add a comprehensive suite of unit tests for napari_deeplabcut.core.config_sync: small helper functions and higher-level behaviors. Tests cover _coerce_point_size (rounding/clamping/coercion), _layer_source_path (normal, missing, and failing path access), many resolve_config_path_from_layer scenarios (points meta inference, generic fallback hints, nearest-config fallback, error handling when reading points meta or find_nearest_config fails, passing dataset/anchor candidates to inference, and image-layer inference), and load_point_size_from_config / save_point_size_to_config behaviors (missing path, load/write failures, missing keys, coercion/clamping, unchanged values, and write error handling). Also add a pathlib.Path import used in tests.
1 parent b65148a commit cdf008f

1 file changed

Lines changed: 360 additions & 3 deletions

File tree

src/napari_deeplabcut/_tests/core/test_config_sync.py

Lines changed: 360 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,46 @@ def __init__(self, *, metadata=None, source_path=None):
1111
self.source = SimpleNamespace(path=source_path) if source_path is not None else None
1212

1313

14+
# -----------------------------------------------------------------------------
15+
# Small helpers
16+
# -----------------------------------------------------------------------------
17+
18+
19+
def test_coerce_point_size_rounds_and_clamps():
20+
assert cs._coerce_point_size(12) == 12
21+
assert cs._coerce_point_size(12.6) == 13
22+
assert cs._coerce_point_size("7") == 7
23+
assert cs._coerce_point_size(-5) == 1
24+
assert cs._coerce_point_size(999) == 100
25+
assert cs._coerce_point_size("not-a-number") == 6
26+
27+
28+
def test_layer_source_path_returns_string_when_available():
29+
layer = DummyLayer(source_path="/tmp/some/file.png")
30+
assert cs._layer_source_path(layer) == "/tmp/some/file.png"
31+
32+
33+
def test_layer_source_path_returns_none_when_source_missing():
34+
layer = DummyLayer()
35+
assert cs._layer_source_path(layer) is None
36+
37+
38+
def test_layer_source_path_returns_none_when_source_path_access_fails():
39+
class BadSource:
40+
@property
41+
def path(self):
42+
raise RuntimeError("boom")
43+
44+
layer = DummyLayer()
45+
layer.source = BadSource()
46+
assert cs._layer_source_path(layer) is None
47+
48+
49+
# -----------------------------------------------------------------------------
50+
# resolve_config_path_from_layer
51+
# -----------------------------------------------------------------------------
52+
53+
1454
def test_resolve_config_prefers_points_meta_inference(monkeypatch, tmp_path):
1555
config_path = tmp_path / "config.yaml"
1656
config_path.write_text("dotsize: 6\n", encoding="utf-8")
@@ -68,13 +108,330 @@ def test_resolve_config_uses_generic_fallback_hints(monkeypatch, tmp_path):
68108
layer = DummyLayer(metadata={})
69109

70110
monkeypatch.setattr(
71-
cs, "read_points_meta", lambda *args, **kwargs: SimpleNamespace(project=None, root=None, paths=[])
111+
cs,
112+
"read_points_meta",
113+
lambda *args, **kwargs: SimpleNamespace(project=None, root=None, paths=[]),
114+
)
115+
monkeypatch.setattr(
116+
cs,
117+
"infer_dlc_project_from_points_meta",
118+
lambda *args, **kwargs: SimpleNamespace(config_path=None),
72119
)
73120
monkeypatch.setattr(
74-
cs, "infer_dlc_project_from_points_meta", lambda *args, **kwargs: SimpleNamespace(config_path=None)
121+
cs,
122+
"infer_dlc_project",
123+
lambda *args, **kwargs: SimpleNamespace(config_path=config_path),
75124
)
76-
monkeypatch.setattr(cs, "infer_dlc_project", lambda *args, **kwargs: SimpleNamespace(config_path=config_path))
77125

78126
resolved = cs.resolve_config_path_from_layer(layer, fallback_project=str(tmp_path))
79127

80128
assert resolved == config_path
129+
130+
131+
def test_resolve_config_uses_find_nearest_config_as_last_resort(monkeypatch, tmp_path):
132+
config_path = tmp_path / "config.yaml"
133+
config_path.write_text("dotsize: 6\n", encoding="utf-8")
134+
135+
layer = DummyLayer(metadata={"root": str(tmp_path)})
136+
137+
monkeypatch.setattr(cs, "read_points_meta", lambda *args, **kwargs: None)
138+
monkeypatch.setattr(cs, "infer_dlc_project", lambda *args, **kwargs: SimpleNamespace(config_path=None))
139+
monkeypatch.setattr(cs, "find_nearest_config", lambda *args, **kwargs: config_path)
140+
141+
resolved = cs.resolve_config_path_from_layer(layer)
142+
143+
assert resolved == config_path
144+
145+
146+
def test_resolve_config_returns_none_when_everything_fails(monkeypatch):
147+
layer = DummyLayer(metadata={})
148+
149+
monkeypatch.setattr(cs, "read_points_meta", lambda *args, **kwargs: None)
150+
monkeypatch.setattr(cs, "infer_dlc_project", lambda *args, **kwargs: SimpleNamespace(config_path=None))
151+
monkeypatch.setattr(cs, "find_nearest_config", lambda *args, **kwargs: None)
152+
153+
resolved = cs.resolve_config_path_from_layer(layer)
154+
155+
assert resolved is None
156+
157+
158+
def test_resolve_config_ignores_points_meta_when_read_points_meta_raises(monkeypatch, tmp_path):
159+
config_path = tmp_path / "config.yaml"
160+
config_path.write_text("dotsize: 6\n", encoding="utf-8")
161+
162+
layer = DummyLayer(metadata={"root": str(tmp_path)})
163+
164+
monkeypatch.setattr(cs, "read_points_meta", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("boom")))
165+
monkeypatch.setattr(cs, "infer_dlc_project", lambda *args, **kwargs: SimpleNamespace(config_path=config_path))
166+
167+
resolved = cs.resolve_config_path_from_layer(layer)
168+
169+
assert resolved == config_path
170+
171+
172+
def test_resolve_config_skips_points_meta_when_errors_attribute_present(monkeypatch, tmp_path):
173+
config_path = tmp_path / "config.yaml"
174+
config_path.write_text("dotsize: 6\n", encoding="utf-8")
175+
176+
layer = DummyLayer(metadata={"root": str(tmp_path)})
177+
178+
monkeypatch.setattr(cs, "read_points_meta", lambda *args, **kwargs: SimpleNamespace(errors=["bad meta"]))
179+
monkeypatch.setattr(cs, "infer_dlc_project", lambda *args, **kwargs: SimpleNamespace(config_path=config_path))
180+
181+
resolved = cs.resolve_config_path_from_layer(layer)
182+
183+
assert resolved == config_path
184+
185+
186+
def test_resolve_config_skips_non_file_points_meta_config_and_falls_through(monkeypatch, tmp_path):
187+
missing_config = tmp_path / "missing_config.yaml"
188+
real_config = tmp_path / "config.yaml"
189+
real_config.write_text("dotsize: 6\n", encoding="utf-8")
190+
191+
layer = DummyLayer(metadata={"root": str(tmp_path)})
192+
193+
monkeypatch.setattr(
194+
cs,
195+
"read_points_meta",
196+
lambda *args, **kwargs: SimpleNamespace(project=None, root=None, paths=[]),
197+
)
198+
monkeypatch.setattr(
199+
cs,
200+
"infer_dlc_project_from_points_meta",
201+
lambda *args, **kwargs: SimpleNamespace(config_path=missing_config),
202+
)
203+
monkeypatch.setattr(
204+
cs,
205+
"infer_dlc_project",
206+
lambda *args, **kwargs: SimpleNamespace(config_path=real_config),
207+
)
208+
209+
resolved = cs.resolve_config_path_from_layer(layer)
210+
211+
assert resolved == real_config
212+
213+
214+
def test_resolve_config_passes_paths_into_generic_inference(monkeypatch, tmp_path):
215+
captured = {}
216+
217+
layer = DummyLayer(
218+
metadata={
219+
"project": str(tmp_path / "proj"),
220+
"root": str(tmp_path / "root"),
221+
"paths": [
222+
"labeled-data/session_001/img001.png",
223+
"labeled-data/session_001/img002.png",
224+
"labeled-data/session_001/img003.png",
225+
"labeled-data/session_001/img004.png",
226+
],
227+
},
228+
source_path=str(tmp_path / "video.mp4"),
229+
)
230+
231+
monkeypatch.setattr(cs, "read_points_meta", lambda *args, **kwargs: None)
232+
233+
def fake_infer_dlc_project(**kwargs):
234+
captured.update(kwargs)
235+
return SimpleNamespace(config_path=None)
236+
237+
monkeypatch.setattr(cs, "infer_dlc_project", fake_infer_dlc_project)
238+
monkeypatch.setattr(cs, "find_nearest_config", lambda *args, **kwargs: None)
239+
240+
resolved = cs.resolve_config_path_from_layer(
241+
layer,
242+
fallback_project=str(tmp_path / "fallback_project"),
243+
fallback_root=str(tmp_path / "fallback_root"),
244+
prefer_project_root=False,
245+
max_levels=7,
246+
)
247+
248+
assert resolved is None
249+
assert captured["dataset_candidates"] == ["labeled-data/session_001/img001.png"]
250+
assert captured["anchor_candidates"] == [
251+
str(tmp_path / "proj"),
252+
str(tmp_path / "root"),
253+
str(tmp_path / "video.mp4"),
254+
str(tmp_path / "fallback_project"),
255+
str(tmp_path / "fallback_root"),
256+
"labeled-data/session_001/img001.png",
257+
"labeled-data/session_001/img002.png",
258+
"labeled-data/session_001/img003.png",
259+
]
260+
assert captured["prefer_project_root"] is False
261+
assert captured["max_levels"] == 7
262+
263+
264+
def test_resolve_config_uses_image_inference_after_points_inference_exception(monkeypatch, tmp_path):
265+
config_path = tmp_path / "config.yaml"
266+
config_path.write_text("dotsize: 6\n", encoding="utf-8")
267+
268+
layer = DummyLayer(metadata={})
269+
image_layer = DummyLayer(metadata={"root": str(tmp_path)})
270+
271+
monkeypatch.setattr(
272+
cs,
273+
"read_points_meta",
274+
lambda *args, **kwargs: SimpleNamespace(project=None, root=None, paths=[]),
275+
)
276+
monkeypatch.setattr(
277+
cs,
278+
"infer_dlc_project_from_points_meta",
279+
lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("boom")),
280+
)
281+
monkeypatch.setattr(
282+
cs,
283+
"infer_dlc_project_from_image_layer",
284+
lambda *args, **kwargs: SimpleNamespace(config_path=config_path),
285+
)
286+
287+
resolved = cs.resolve_config_path_from_layer(layer, image_layer=image_layer)
288+
289+
assert resolved == config_path
290+
291+
292+
def test_resolve_config_continues_when_find_nearest_config_raises_for_one_candidate(monkeypatch, tmp_path):
293+
config_path = tmp_path / "config.yaml"
294+
config_path.write_text("dotsize: 6\n", encoding="utf-8")
295+
296+
layer = DummyLayer(
297+
metadata={
298+
"project": "bad-candidate",
299+
"root": str(tmp_path),
300+
}
301+
)
302+
303+
monkeypatch.setattr(cs, "read_points_meta", lambda *args, **kwargs: None)
304+
monkeypatch.setattr(cs, "infer_dlc_project", lambda *args, **kwargs: SimpleNamespace(config_path=None))
305+
306+
def fake_find(candidate, **kwargs):
307+
if candidate == "bad-candidate":
308+
raise RuntimeError("boom")
309+
return config_path
310+
311+
monkeypatch.setattr(cs, "find_nearest_config", fake_find)
312+
313+
resolved = cs.resolve_config_path_from_layer(layer)
314+
315+
assert resolved == config_path
316+
317+
318+
# -----------------------------------------------------------------------------
319+
# load_point_size_from_config
320+
# -----------------------------------------------------------------------------
321+
322+
323+
def test_load_point_size_from_config_returns_none_for_missing_path():
324+
assert cs.load_point_size_from_config(None) is None
325+
326+
327+
def test_load_point_size_from_config_returns_none_when_load_fails(monkeypatch, tmp_path):
328+
config_path = tmp_path / "config.yaml"
329+
330+
monkeypatch.setattr(cs.io, "load_config", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("boom")))
331+
332+
assert cs.load_point_size_from_config(config_path) is None
333+
334+
335+
def test_load_point_size_from_config_returns_none_when_key_missing(monkeypatch, tmp_path):
336+
config_path = tmp_path / "config.yaml"
337+
338+
monkeypatch.setattr(cs.io, "load_config", lambda *args, **kwargs: {"colormap": "rainbow"})
339+
340+
assert cs.load_point_size_from_config(config_path) is None
341+
342+
343+
def test_load_point_size_from_config_coerces_and_clamps(monkeypatch, tmp_path):
344+
config_path = tmp_path / "config.yaml"
345+
346+
monkeypatch.setattr(cs.io, "load_config", lambda *args, **kwargs: {"dotsize": "250"})
347+
348+
assert cs.load_point_size_from_config(config_path) == 100
349+
350+
351+
# -----------------------------------------------------------------------------
352+
# save_point_size_to_config
353+
# -----------------------------------------------------------------------------
354+
355+
356+
def test_save_point_size_to_config_returns_false_when_path_missing():
357+
assert cs.save_point_size_to_config(None, 12) is False
358+
359+
360+
def test_save_point_size_to_config_returns_false_when_load_fails(monkeypatch, tmp_path):
361+
config_path = tmp_path / "config.yaml"
362+
363+
monkeypatch.setattr(cs.io, "load_config", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("boom")))
364+
365+
assert cs.save_point_size_to_config(config_path, 12) is False
366+
367+
368+
def test_save_point_size_to_config_returns_false_when_value_unchanged(monkeypatch, tmp_path):
369+
config_path = tmp_path / "config.yaml"
370+
371+
written = []
372+
373+
monkeypatch.setattr(cs.io, "load_config", lambda *args, **kwargs: {"dotsize": 12})
374+
monkeypatch.setattr(cs.io, "write_config", lambda *args, **kwargs: written.append(True))
375+
376+
assert cs.save_point_size_to_config(config_path, 12) is False
377+
assert written == []
378+
379+
380+
def test_save_point_size_to_config_writes_updated_value(monkeypatch, tmp_path):
381+
config_path = tmp_path / "config.yaml"
382+
383+
written = {}
384+
385+
monkeypatch.setattr(cs.io, "load_config", lambda *args, **kwargs: {"dotsize": 6, "colormap": "rainbow"})
386+
387+
def fake_write(path, cfg):
388+
written["path"] = path
389+
written["cfg"] = cfg
390+
391+
monkeypatch.setattr(cs.io, "write_config", fake_write)
392+
393+
assert cs.save_point_size_to_config(config_path, 12) is True
394+
assert written["path"] == str(config_path)
395+
assert written["cfg"]["dotsize"] == 12
396+
assert written["cfg"]["colormap"] == "rainbow"
397+
398+
399+
def test_save_point_size_to_config_clamps_before_writing(monkeypatch, tmp_path):
400+
config_path = tmp_path / "config.yaml"
401+
402+
written = {}
403+
404+
monkeypatch.setattr(cs.io, "load_config", lambda *args, **kwargs: {})
405+
406+
def fake_write(path, cfg):
407+
written["cfg"] = cfg
408+
409+
monkeypatch.setattr(cs.io, "write_config", fake_write)
410+
411+
assert cs.save_point_size_to_config(config_path, 999) is True
412+
assert written["cfg"]["dotsize"] == 100
413+
414+
415+
def test_save_point_size_to_config_still_writes_when_old_value_is_not_coercible(monkeypatch, tmp_path):
416+
config_path = tmp_path / "config.yaml"
417+
418+
written = {}
419+
420+
monkeypatch.setattr(cs.io, "load_config", lambda *args, **kwargs: {"dotsize": object()})
421+
422+
def fake_write(path, cfg):
423+
written["cfg"] = cfg
424+
425+
monkeypatch.setattr(cs.io, "write_config", fake_write)
426+
427+
assert cs.save_point_size_to_config(config_path, 15) is True
428+
assert written["cfg"]["dotsize"] == 15
429+
430+
431+
def test_save_point_size_to_config_returns_false_when_write_fails(monkeypatch, tmp_path):
432+
config_path = tmp_path / "config.yaml"
433+
434+
monkeypatch.setattr(cs.io, "load_config", lambda *args, **kwargs: {"dotsize": 6})
435+
monkeypatch.setattr(cs.io, "write_config", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("boom")))
436+
437+
assert cs.save_point_size_to_config(config_path, 15) is False

0 commit comments

Comments
 (0)