@@ -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+
1454def 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