Skip to content

Commit f8bf1d3

Browse files
authored
Merge pull request #156 from DeepLabCut/cy/expand-writer-tests
Expand test coverage (writer and widgets)
2 parents b2df5dd + 0ec0c9c commit f8bf1d3

7 files changed

Lines changed: 560 additions & 1 deletion

File tree

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ napari.manifest =
5656

5757
[options.extras_require]
5858
testing =
59+
Pillow
5960
pytest
6061
pytest-cov
6162
pytest-qt

src/napari_deeplabcut/_reader.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,16 @@ def _populate_metadata(
156156

157157
def _load_superkeypoints_diagram(super_animal: str):
158158
path = str(Path(__file__).parent / "assets" / f"{super_animal}.jpg")
159-
return imread(path), {"root": ""}, "images"
159+
try:
160+
return imread(path), {"root": ""}, "images"
161+
except Exception as e:
162+
raise FileNotFoundError(f"Superkeypoints diagram not found for {super_animal}.") from e
160163

161164

162165
def _load_superkeypoints(super_animal: str):
163166
path = str(Path(__file__).parent / "assets" / f"{super_animal}.json")
167+
if not Path(path).is_file():
168+
raise FileNotFoundError(f"Superkeypoints JSON file not found for {super_animal}.")
164169
with open(path) as f:
165170
return json.load(f)
166171

src/napari_deeplabcut/_tests/conftest.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import json
12
import os
3+
from pathlib import Path
24

35
import cv2
46
import numpy as np
57
import pandas as pd
68
import pytest
9+
from PIL import Image
710
from qtpy.QtWidgets import QDockWidget
811
from skimage.io import imsave
912

@@ -151,3 +154,78 @@ def video_path(tmp_path_factory):
151154
writer.write(frame)
152155
writer.release()
153156
return output_path
157+
158+
159+
@pytest.fixture
160+
def superkeypoints_assets(tmp_path, monkeypatch):
161+
"""
162+
Create a fake module dir with the expected assets layout:
163+
164+
module_dir/_reader_fake.py -> patched as __file__
165+
module_dir/assets/fake.json
166+
module_dir/assets/fake.jpg
167+
168+
This mirrors the code under test:
169+
Path(__file__).parent / "assets" / f"{super_animal}.json|.jpg"
170+
"""
171+
module_dir = tmp_path / "module"
172+
assets_dir = module_dir / "assets"
173+
assets_dir.mkdir(parents=True)
174+
175+
super_animal = "fake"
176+
data = {
177+
"SK1": [10.0, 20.0],
178+
"SK2": [40.0, 60.0],
179+
}
180+
181+
# JSON with superkeypoints coordinates
182+
(assets_dir / f"{super_animal}.json").write_text(json.dumps(data))
183+
184+
# Small 10x10 RGB diagram
185+
Image.new("RGB", (10, 10), "white").save(assets_dir / f"{super_animal}.jpg")
186+
187+
# Patch the module's __file__ so that Path(__file__).parent == module_dir
188+
fake_module_file = module_dir / "_reader_fake.py"
189+
fake_module_file.write_text("# fake")
190+
monkeypatch.setattr("napari_deeplabcut._reader.__file__", str(fake_module_file))
191+
192+
return {
193+
"module_dir": module_dir,
194+
"assets_dir": assets_dir,
195+
"super_animal": super_animal,
196+
"data": data,
197+
}
198+
199+
200+
@pytest.fixture
201+
def mapped_points(points, superkeypoints_assets, config_path):
202+
"""
203+
Return a DLC Points layer that is ready for _map_keypoints():
204+
- metadata['project'] is set (so the widget can write config.yaml)
205+
- metadata['tables'] contains a mapping for two real bodyparts -> SK1/SK2
206+
- at least two rows have coordinates exactly on the SK1/SK2 positions
207+
and their labels are set to those bodyparts, guaranteeing a neighbor match.
208+
"""
209+
layer = points # DLC layer created via viewer.open(..., plugin="napari-deeplabcut")
210+
super_animal = superkeypoints_assets["super_animal"]
211+
superkpts = superkeypoints_assets["data"]
212+
213+
# Required by _map_keypoints to locate and write config.yaml
214+
# NOTE: This relies on config_path pointing to a file directly under the
215+
# project directory, so that Path(config_path).parent is the project root.
216+
layer.metadata["project"] = str(Path(config_path).parent)
217+
header = layer.metadata["header"]
218+
bp1, bp2 = header.bodyparts[:2]
219+
220+
# Inject a conversion table into metadata
221+
layer.metadata["tables"] = {super_animal: {bp1: "SK1", bp2: "SK2"}}
222+
223+
# Ensure _map_keypoints finds matches:
224+
# Put the first two rows exactly on SK1/SK2 and set their labels accordingly.
225+
layer.data[0, 1:] = np.array(superkpts["SK1"], dtype=float)
226+
layer.properties["label"][0] = bp1
227+
228+
layer.data[1, 1:] = np.array(superkpts["SK2"], dtype=float)
229+
layer.properties["label"][1] = bp2
230+
231+
return layer, super_animal, bp1, bp2

src/napari_deeplabcut/_tests/test_reader.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import numpy as np
22
import pandas as pd
33
import pytest
4+
from PIL import Image
45
from skimage.io import imsave
56

67
from napari_deeplabcut import _reader
@@ -110,3 +111,68 @@ def test_read_video(video_path):
110111
assert dict_["metadata"].get("root")
111112
assert array.shape[0] == 5
112113
assert array[0].compute().shape == (50, 50, 3)
114+
115+
116+
@pytest.mark.parametrize(
117+
"exists",
118+
[
119+
True,
120+
False,
121+
],
122+
)
123+
def test_load_superkeypoints(monkeypatch, tmp_path, exists):
124+
"""Test loading of superkeypoints JSON with and without the file present."""
125+
module_dir = tmp_path / "module"
126+
assets_dir = module_dir / "assets"
127+
assets_dir.mkdir(parents=True)
128+
129+
super_animal = "fake"
130+
json_path = assets_dir / f"{super_animal}.json"
131+
132+
if exists:
133+
json_path.write_text('{"SK1": [1, 2]}')
134+
135+
# Patch module __file__
136+
fake_file = module_dir / "_reader_fake.py"
137+
fake_file.write_text("# fake module")
138+
monkeypatch.setattr("napari_deeplabcut._reader.__file__", str(fake_file))
139+
140+
if exists:
141+
assert _reader._load_superkeypoints(super_animal) == {"SK1": [1, 2]}
142+
else:
143+
with pytest.raises(FileNotFoundError):
144+
_reader._load_superkeypoints(super_animal)
145+
146+
147+
@pytest.mark.parametrize(
148+
"exists",
149+
[
150+
True,
151+
False,
152+
],
153+
)
154+
def test_load_superkeypoints_diagram(monkeypatch, tmp_path, exists):
155+
"""Test loading of superkeypoints diagram with and without the file present."""
156+
module_dir = tmp_path / "module"
157+
assets_dir = module_dir / "assets"
158+
assets_dir.mkdir(parents=True)
159+
160+
super_animal = "fake"
161+
jpg_path = assets_dir / f"{super_animal}.jpg"
162+
163+
if exists:
164+
Image.new("RGB", (10, 10), "white").save(jpg_path)
165+
166+
# Patch module __file__
167+
fake_file = module_dir / "_reader_fake.py"
168+
fake_file.write_text("# fake")
169+
monkeypatch.setattr("napari_deeplabcut._reader.__file__", str(fake_file))
170+
171+
if exists:
172+
array, meta, layer_type = _reader._load_superkeypoints_diagram(super_animal)
173+
assert layer_type == "images"
174+
assert meta == {"root": ""}
175+
assert tuple(array.shape[-3:-1]) == (10, 10)
176+
else:
177+
with pytest.raises(FileNotFoundError):
178+
_reader._load_superkeypoints_diagram(super_animal)

src/napari_deeplabcut/_tests/test_widgets.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
import numpy as np
55
import pytest
6+
import yaml
7+
from qtpy.QtSvgWidgets import QSvgWidget
68
from vispy import keys
79

810
from napari_deeplabcut import _widgets
@@ -310,3 +312,65 @@ def add_dock_widget(self, *args, **kwargs):
310312

311313
# Docking failed → remains undocked
312314
assert controls._mpl_docked is False
315+
316+
317+
def test_display_shortcuts_dialog(viewer, qtbot):
318+
"""Ensure that the Shortcuts dialog can be created and shown without errors."""
319+
controls = _widgets.KeypointControls(viewer)
320+
qtbot.add_widget(controls)
321+
322+
# Create the dialog directly
323+
dlg = _widgets.Shortcuts(controls)
324+
qtbot.add_widget(dlg)
325+
326+
# Show it non-modally
327+
dlg.show()
328+
qtbot.waitExposed(dlg)
329+
330+
# Verify it is visible
331+
assert dlg.isVisible()
332+
333+
# Ensure the SVG widget is present
334+
found_svg = False
335+
for child in dlg.children():
336+
if isinstance(child, QSvgWidget):
337+
found_svg = True
338+
break
339+
340+
assert found_svg, "Shortcuts dialog should contain a QSvgWidget with the shortcuts image."
341+
342+
343+
# NOTE SuperAnimal keypoints functionality and testing may need an overhaul in the future:
344+
# these tests currently exercise only a narrow "everything fine" path and rely on specific metadata
345+
# layout and SuperAnimal conversion-table conventions, which makes them susceptible to API changes
346+
def test_widget_load_superkeypoints_diagram(viewer, qtbot, points, superkeypoints_assets):
347+
controls = _widgets.KeypointControls(viewer)
348+
qtbot.add_widget(controls)
349+
350+
# Inject conversion table into the existing Points layer
351+
layer = points
352+
super_animal = superkeypoints_assets["super_animal"]
353+
layer.metadata["tables"] = {super_animal: {"kp1": "SK1", "kp2": "SK2"}}
354+
355+
n_layers_before = len(viewer.layers)
356+
controls.load_superkeypoints_diagram()
357+
358+
assert len(viewer.layers) == n_layers_before + 1
359+
assert list(layer.properties["label"]) == ["kp1", "kp2"]
360+
assert controls._keypoint_mapping_button.text() == "Map keypoints"
361+
362+
363+
def test_widget_map_keypoints_writes_to_config(viewer, qtbot, mapped_points, config_path):
364+
controls = _widgets.KeypointControls(viewer)
365+
qtbot.add_widget(controls)
366+
367+
_, super_animal, bp1, bp2 = mapped_points
368+
controls._map_keypoints(super_animal)
369+
370+
with open(config_path, encoding="utf-8") as fh:
371+
cfg = yaml.safe_load(fh)
372+
assert "SuperAnimalConversionTables" in cfg
373+
assert cfg["SuperAnimalConversionTables"][super_animal] == {
374+
bp1: "SK1",
375+
bp2: "SK2",
376+
}

0 commit comments

Comments
 (0)