Skip to content

Commit 8f2c5e7

Browse files
committed
test: add tests for new io func
1 parent bb54b6e commit 8f2c5e7

1 file changed

Lines changed: 219 additions & 0 deletions

File tree

tests/test_io.py

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
"""Tests for vesskel._io."""
2+
3+
import csv
4+
5+
import numpy as np
6+
import pytest
7+
8+
from vesskel._io import save_analysis_outputs
9+
from vesskel.config import OutputConfig
10+
from vesskel.pipeline import AnalysisResult
11+
12+
13+
class TestSaveAnalysisOutputs:
14+
"""Tests for save_analysis_outputs, the top-level writer."""
15+
16+
@staticmethod
17+
def _result(
18+
*,
19+
summary: bool = True,
20+
radius: bool = False,
21+
branches: bool = False,
22+
nodes: bool = False,
23+
) -> AnalysisResult:
24+
skel = np.eye(10, dtype=np.uint8)
25+
feat = {"n_branches": 4.0, "n_junctions": 1.0} if summary else {}
26+
rad = np.ones((10, 10), dtype=np.float64) if radius else None
27+
brecs = [{"id": i, "len": float(i * 2)} for i in range(2)] if branches else []
28+
nrecs = [{"id": i, "deg": i + 2} for i in range(2)] if nodes else []
29+
return AnalysisResult(
30+
skeleton=skel,
31+
layers=[],
32+
summary_features=feat,
33+
branch_records=brecs,
34+
node_records=nrecs,
35+
radius_matrix=rad,
36+
)
37+
38+
# -- skeleton output ---------------------------------------------------
39+
40+
def test_default_skeleton_npy_and_summary(self, tmp_path):
41+
save_analysis_outputs(tmp_path, "img", self._result(), OutputConfig())
42+
d = tmp_path / "img"
43+
assert d.is_dir()
44+
assert (d / "img_skeleton.npy").exists()
45+
assert (d / "img_summary.csv").exists()
46+
assert not (d / "img_skeleton.png").exists()
47+
48+
def test_skeleton_png_only(self, tmp_path):
49+
cfg = OutputConfig(write_skeleton_npy=False, write_skeleton_png=True)
50+
save_analysis_outputs(tmp_path, "img", self._result(), cfg)
51+
d = tmp_path / "img"
52+
assert (d / "img_skeleton.png").exists()
53+
assert not (d / "img_skeleton.npy").exists()
54+
55+
def test_skeleton_both_formats(self, tmp_path):
56+
cfg = OutputConfig(write_skeleton_npy=True, write_skeleton_png=True)
57+
save_analysis_outputs(tmp_path, "img", self._result(), cfg)
58+
d = tmp_path / "img"
59+
assert (d / "img_skeleton.npy").exists()
60+
assert (d / "img_skeleton.png").exists()
61+
62+
def test_skeleton_neither_format(self, tmp_path):
63+
cfg = OutputConfig(write_skeleton_npy=False, write_skeleton_png=False)
64+
save_analysis_outputs(tmp_path, "img", self._result(), cfg)
65+
d = tmp_path / "img"
66+
assert not (d / "img_skeleton.npy").exists()
67+
assert not (d / "img_skeleton.png").exists()
68+
69+
def test_3d_skeleton_with_png_raises(self, tmp_path):
70+
result = AnalysisResult(
71+
skeleton=np.ones((4, 4, 4), dtype=np.uint8),
72+
layers=[],
73+
summary_features={},
74+
branch_records=[],
75+
node_records=[],
76+
)
77+
cfg = OutputConfig(write_skeleton_npy=False, write_skeleton_png=True)
78+
with pytest.raises(ValueError, match="PNG skeleton output"):
79+
save_analysis_outputs(tmp_path, "vol", result, cfg)
80+
81+
# -- branch CSV --------------------------------------------------------
82+
83+
def test_saves_branch_csv(self, tmp_path):
84+
cfg = OutputConfig(write_branch_csv=True)
85+
save_analysis_outputs(tmp_path, "img", self._result(branches=True), cfg)
86+
rows = list(csv.DictReader(open(tmp_path / "img" / "img_branches.csv")))
87+
assert len(rows) == 2
88+
assert rows[0]["id"] == "0"
89+
90+
def test_skips_branch_csv_when_no_records(self, tmp_path):
91+
cfg = OutputConfig(write_branch_csv=True)
92+
save_analysis_outputs(tmp_path, "img", self._result(branches=False), cfg)
93+
assert not (tmp_path / "img" / "img_branches.csv").exists()
94+
95+
def test_skips_branch_csv_when_disabled(self, tmp_path):
96+
cfg = OutputConfig(write_branch_csv=False)
97+
save_analysis_outputs(tmp_path, "img", self._result(branches=True), cfg)
98+
assert not (tmp_path / "img" / "img_branches.csv").exists()
99+
100+
# -- node CSV ----------------------------------------------------------
101+
102+
def test_saves_node_csv(self, tmp_path):
103+
cfg = OutputConfig(write_node_csv=True)
104+
save_analysis_outputs(tmp_path, "img", self._result(nodes=True), cfg)
105+
rows = list(csv.DictReader(open(tmp_path / "img" / "img_nodes.csv")))
106+
assert len(rows) == 2
107+
assert rows[0]["deg"] == "2"
108+
109+
def test_skips_node_csv_when_no_records(self, tmp_path):
110+
cfg = OutputConfig(write_node_csv=True)
111+
save_analysis_outputs(tmp_path, "img", self._result(nodes=False), cfg)
112+
assert not (tmp_path / "img" / "img_nodes.csv").exists()
113+
114+
def test_skips_node_csv_when_disabled(self, tmp_path):
115+
cfg = OutputConfig(write_node_csv=False)
116+
save_analysis_outputs(tmp_path, "img", self._result(nodes=True), cfg)
117+
assert not (tmp_path / "img" / "img_nodes.csv").exists()
118+
119+
# -- radius ------------------------------------------------------------
120+
121+
def test_saves_radius(self, tmp_path):
122+
cfg = OutputConfig(write_radius=True)
123+
save_analysis_outputs(tmp_path, "img", self._result(radius=True), cfg)
124+
path = tmp_path / "img" / "img_radius.npy"
125+
assert path.exists()
126+
assert np.load(path).dtype == np.float64
127+
128+
def test_skips_radius_when_none(self, tmp_path):
129+
cfg = OutputConfig(write_radius=True)
130+
save_analysis_outputs(tmp_path, "img", self._result(radius=False), cfg)
131+
assert not (tmp_path / "img" / "img_radius.npy").exists()
132+
133+
def test_skips_radius_when_disabled(self, tmp_path):
134+
cfg = OutputConfig(write_radius=False)
135+
save_analysis_outputs(tmp_path, "img", self._result(radius=True), cfg)
136+
assert not (tmp_path / "img" / "img_radius.npy").exists()
137+
138+
# -- summary CSV -------------------------------------------------------
139+
140+
def test_summary_csv_content(self, tmp_path):
141+
save_analysis_outputs(
142+
tmp_path, "img", self._result(summary=True), OutputConfig()
143+
)
144+
rows = list(csv.DictReader(open(tmp_path / "img" / "img_summary.csv")))
145+
assert len(rows) == 1
146+
assert rows[0]["image"] == "img"
147+
assert rows[0]["n_branches"] == "4.0"
148+
149+
def test_skips_summary_when_empty_features(self, tmp_path):
150+
save_analysis_outputs(
151+
tmp_path, "img", self._result(summary=False), OutputConfig()
152+
)
153+
assert not (tmp_path / "img" / "img_summary.csv").exists()
154+
155+
def test_skips_summary_when_write_summary_false(self, tmp_path):
156+
save_analysis_outputs(
157+
tmp_path,
158+
"img",
159+
self._result(summary=True),
160+
OutputConfig(),
161+
write_summary=False,
162+
)
163+
assert not (tmp_path / "img" / "img_summary.csv").exists()
164+
165+
def test_skips_summary_when_config_disabled(self, tmp_path):
166+
cfg = OutputConfig(write_summary_csv=False)
167+
save_analysis_outputs(tmp_path, "img", self._result(summary=True), cfg)
168+
assert not (tmp_path / "img" / "img_summary.csv").exists()
169+
170+
# -- all outputs -------------------------------------------------------
171+
172+
def test_all_outputs_enabled(self, tmp_path):
173+
result = self._result(summary=True, radius=True, branches=True, nodes=True)
174+
cfg = OutputConfig(
175+
write_skeleton_npy=True,
176+
write_skeleton_png=True,
177+
write_branch_csv=True,
178+
write_node_csv=True,
179+
write_radius=True,
180+
write_summary_csv=True,
181+
)
182+
save_analysis_outputs(tmp_path, "img", result, cfg)
183+
d = tmp_path / "img"
184+
assert (d / "img_skeleton.npy").exists()
185+
assert (d / "img_skeleton.png").exists()
186+
assert (d / "img_branches.csv").exists()
187+
assert (d / "img_nodes.csv").exists()
188+
assert (d / "img_radius.npy").exists()
189+
assert (d / "img_summary.csv").exists()
190+
191+
def test_nothing_enabled_creates_empty_dir(self, tmp_path):
192+
cfg = OutputConfig(
193+
write_skeleton_npy=False,
194+
write_skeleton_png=False,
195+
write_branch_csv=False,
196+
write_node_csv=False,
197+
write_radius=False,
198+
write_summary_csv=False,
199+
)
200+
save_analysis_outputs(tmp_path, "img", self._result(summary=True), cfg)
201+
d = tmp_path / "img"
202+
assert d.is_dir()
203+
assert list(d.iterdir()) == []
204+
205+
# -- edge cases --------------------------------------------------------
206+
207+
def test_existing_dir_is_reused(self, tmp_path):
208+
d = tmp_path / "img"
209+
d.mkdir()
210+
(d / "stale.txt").touch()
211+
save_analysis_outputs(tmp_path, "img", self._result(), OutputConfig())
212+
assert (d / "stale.txt").exists()
213+
assert (d / "img_skeleton.npy").exists()
214+
215+
def test_base_name_with_spaces(self, tmp_path):
216+
save_analysis_outputs(tmp_path, "my img", self._result(), OutputConfig())
217+
d = tmp_path / "my img"
218+
assert d.is_dir()
219+
assert (d / "my img_skeleton.npy").exists()

0 commit comments

Comments
 (0)