Skip to content

Commit de77007

Browse files
committed
Improve coverage
1 parent 797d1f7 commit de77007

12 files changed

Lines changed: 558 additions & 0 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# EnsureShapeMultiple
2+
3+
::: torchio.EnsureShapeMultiple
4+
options:
5+
show_root_heading: false

tests/test_batch.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import pytest
56
import torch
67

78
import torchio as tio
@@ -194,3 +195,104 @@ def test_batch_copy_preserves_original(self) -> None:
194195
tio.Noise(std=1.0)(batch)
195196
# Original should be unchanged (copy=True default)
196197
torch.testing.assert_close(batch.data, original)
198+
199+
200+
# ── Coverage gap tests ───────────────────────────────────────────────
201+
202+
203+
class TestImagesBatchValidation:
204+
def test_non_5d_raises(self) -> None:
205+
from torchio.data.batch import ImagesBatch
206+
207+
with pytest.raises(ValueError, match="5"):
208+
ImagesBatch(
209+
data=torch.rand(1, 10, 10),
210+
affines=[tio.AffineMatrix()],
211+
image_class=tio.ScalarImage,
212+
)
213+
214+
def test_affine_count_mismatch_raises(self) -> None:
215+
from torchio.data.batch import ImagesBatch
216+
217+
with pytest.raises(ValueError, match="affines"):
218+
ImagesBatch(
219+
data=torch.rand(2, 1, 5, 5, 5),
220+
affines=[tio.AffineMatrix()], # only 1 for batch of 2
221+
image_class=tio.ScalarImage,
222+
)
223+
224+
def test_from_images_empty_raises(self) -> None:
225+
from torchio.data.batch import ImagesBatch
226+
227+
with pytest.raises(ValueError, match="empty"):
228+
ImagesBatch.from_images([])
229+
230+
def test_data_setter_non_5d_raises(self) -> None:
231+
from torchio.data.batch import ImagesBatch
232+
233+
batch = ImagesBatch(
234+
data=torch.rand(1, 1, 5, 5, 5),
235+
affines=[tio.AffineMatrix()],
236+
image_class=tio.ScalarImage,
237+
)
238+
with pytest.raises(ValueError, match="5"):
239+
batch.data = torch.rand(1, 5, 5)
240+
241+
def test_device_property(self) -> None:
242+
from torchio.data.batch import ImagesBatch
243+
244+
batch = ImagesBatch(
245+
data=torch.rand(1, 1, 5, 5, 5),
246+
affines=[tio.AffineMatrix()],
247+
image_class=tio.ScalarImage,
248+
)
249+
assert batch.device.type == "cpu"
250+
251+
def test_len(self) -> None:
252+
from torchio.data.batch import ImagesBatch
253+
254+
batch = ImagesBatch(
255+
data=torch.rand(3, 1, 5, 5, 5),
256+
affines=[tio.AffineMatrix() for _ in range(3)],
257+
image_class=tio.ScalarImage,
258+
)
259+
assert len(batch) == 3
260+
261+
262+
class TestSubjectsBatchEdgeCases:
263+
def test_from_subjects_empty_raises(self) -> None:
264+
from torchio.data.batch import SubjectsBatch
265+
266+
with pytest.raises(ValueError, match="empty"):
267+
SubjectsBatch.from_subjects([])
268+
269+
def test_device_property(self) -> None:
270+
subject = tio.Subject(t1=tio.ScalarImage(torch.rand(1, 5, 5, 5)))
271+
from torchio.data.batch import SubjectsBatch
272+
273+
batch = SubjectsBatch.from_subjects([subject])
274+
assert batch.device.type == "cpu"
275+
276+
def test_getattr_invalid_raises(self) -> None:
277+
subject = tio.Subject(t1=tio.ScalarImage(torch.rand(1, 5, 5, 5)))
278+
from torchio.data.batch import SubjectsBatch
279+
280+
batch = SubjectsBatch.from_subjects([subject])
281+
with pytest.raises(AttributeError):
282+
_ = batch.nonexistent_image
283+
284+
def test_len(self) -> None:
285+
subject = tio.Subject(t1=tio.ScalarImage(torch.rand(1, 5, 5, 5)))
286+
from torchio.data.batch import SubjectsBatch
287+
288+
batch = SubjectsBatch.from_subjects([subject])
289+
assert len(batch) == 1
290+
291+
def test_repr(self) -> None:
292+
subject = tio.Subject(t1=tio.ScalarImage(torch.rand(1, 5, 5, 5)))
293+
from torchio.data.batch import SubjectsBatch
294+
295+
batch = SubjectsBatch.from_subjects([subject])
296+
r = repr(batch)
297+
assert "SubjectsBatch" in r
298+
assert "t1" in r

tests/test_compose.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Tests for Compose transform."""
2+
3+
from __future__ import annotations
4+
5+
import torch
6+
7+
import torchio as tio
8+
9+
10+
def _make_subject() -> tio.Subject:
11+
return tio.Subject(
12+
t1=tio.ScalarImage(torch.rand(1, 10, 10, 10)),
13+
seg=tio.LabelMap(torch.zeros(1, 10, 10, 10)),
14+
)
15+
16+
17+
class TestCompose:
18+
def test_identity_compose(self) -> None:
19+
subject = _make_subject()
20+
original = subject.t1.data.clone()
21+
result = tio.Compose([])(subject)
22+
torch.testing.assert_close(result.t1.data, original)
23+
24+
def test_single_transform(self) -> None:
25+
subject = _make_subject()
26+
result = tio.Compose([tio.Flip(axes=(0,))])(subject)
27+
assert result.t1.data.shape == subject.t1.data.shape
28+
29+
def test_multiple_transforms(self) -> None:
30+
subject = _make_subject()
31+
pipeline = tio.Compose(
32+
[
33+
tio.Flip(axes=(0,)),
34+
tio.Gamma(log_gamma=0.0),
35+
]
36+
)
37+
result = pipeline(subject)
38+
assert result.t1.data.shape == subject.t1.data.shape
39+
40+
def test_nested_compose(self) -> None:
41+
subject = _make_subject()
42+
inner = tio.Compose([tio.Flip(axes=(0,))], copy=False)
43+
outer = tio.Compose([inner])
44+
result = outer(subject)
45+
assert result.t1.data.shape == subject.t1.data.shape
46+
47+
def test_copy_default(self) -> None:
48+
"""Compose deep-copies input by default."""
49+
subject = _make_subject()
50+
original_data = subject.t1.data.clone()
51+
tio.Compose([tio.Gamma(log_gamma=0.5)])(subject)
52+
torch.testing.assert_close(subject.t1.data, original_data)
53+
54+
def test_no_copy(self) -> None:
55+
subject = _make_subject()
56+
result = tio.Compose([tio.Gamma(log_gamma=0.0)], copy=False)(subject)
57+
assert result.t1.data.shape == subject.t1.data.shape
58+
59+
def test_history_recorded(self) -> None:
60+
subject = _make_subject()
61+
result = tio.Compose([tio.Flip(axes=(0,))])(subject)
62+
assert len(result.applied_transforms) > 0

tests/test_ensure_shape_multiple.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,20 @@ def test_four_pooling_layers(self) -> None:
204204
result = tio.EnsureShapeMultiple(2**4)(subject)
205205
for s in result.t1.spatial_shape:
206206
assert s % 16 == 0
207+
208+
209+
# ── Coverage gap tests ───────────────────────────────────────────────
210+
211+
212+
class TestEnsureShapeMultipleValidation:
213+
def test_zero_multiple_raises(self) -> None:
214+
with pytest.raises(ValueError, match=">= 1"):
215+
tio.EnsureShapeMultiple(target_multiple=0)
216+
217+
def test_wrong_tuple_length_raises(self) -> None:
218+
with pytest.raises(ValueError, match="1 or 3"):
219+
tio.EnsureShapeMultiple(target_multiple=(2, 4))
220+
221+
def test_negative_in_tuple_raises(self) -> None:
222+
with pytest.raises(ValueError, match=">= 1"):
223+
tio.EnsureShapeMultiple(target_multiple=(2, -1, 4))

tests/test_histogram_standardization.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
from pathlib import Path
6+
57
import pytest
68
import torch
79

@@ -85,3 +87,52 @@ def test_too_few_quantiles_raises(self) -> None:
8587
images = self._make_images()
8688
with pytest.raises(ValueError, match="at least 2"):
8789
compute_histogram_landmarks(images, quantiles=(0.5,))
90+
91+
92+
class TestHistogramStandardizationEdgeCases:
93+
def test_quantiles_out_of_range_raises(self) -> None:
94+
images = [tio.ScalarImage(torch.randn(1, 5, 5, 5)) for _ in range(3)]
95+
with pytest.raises(ValueError, match="\\[0, 1\\]"):
96+
compute_histogram_landmarks(images, quantiles=(-0.1, 0.5, 1.1))
97+
98+
def test_cutoff_not_in_quantiles_raises(self) -> None:
99+
images = [tio.ScalarImage(torch.randn(1, 5, 5, 5)) for _ in range(3)]
100+
with pytest.raises(ValueError, match="Cutoff"):
101+
compute_histogram_landmarks(
102+
images, quantiles=(0.25, 0.5, 0.75), cutoff=(0.01, 0.99)
103+
)
104+
105+
def test_load_landmarks_from_npy(self, tmp_path: Path) -> None:
106+
import numpy as np
107+
108+
arr = np.linspace(0, 100, 13).astype(np.float32)
109+
npy_path = tmp_path / "landmarks.npy"
110+
np.save(npy_path, arr)
111+
subject = _make_subject(with_label=False)
112+
result = tio.HistogramStandardization(npy_path)(subject)
113+
assert result.t1.data.shape == subject.t1.data.shape
114+
115+
def test_load_landmarks_from_pt(self, tmp_path: Path) -> None:
116+
landmarks = torch.linspace(0, 100, 13)
117+
pt_path = tmp_path / "landmarks.pt"
118+
torch.save(landmarks, pt_path)
119+
subject = _make_subject(with_label=False)
120+
result = tio.HistogramStandardization(pt_path)(subject)
121+
assert result.t1.data.shape == subject.t1.data.shape
122+
123+
def test_unsupported_format_raises(self, tmp_path: Path) -> None:
124+
bad_path = tmp_path / "landmarks.csv"
125+
bad_path.write_text("1,2,3")
126+
with pytest.raises(ValueError, match="Unsupported"):
127+
tio.HistogramStandardization(bad_path)
128+
129+
def test_pt_with_wrong_type_raises(self, tmp_path: Path) -> None:
130+
pt_path = tmp_path / "landmarks.pt"
131+
torch.save({"not": "a tensor"}, pt_path)
132+
with pytest.raises(TypeError, match="Expected a Tensor"):
133+
tio.HistogramStandardization(pt_path)
134+
135+
def test_load_from_path_string(self) -> None:
136+
images = [tio.ScalarImage(torch.randn(1, 5, 5, 5)) for _ in range(3)]
137+
landmarks = compute_histogram_landmarks(images)
138+
assert landmarks.ndim == 1

tests/test_inverse.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""Tests for the inverse transform module."""
2+
3+
from __future__ import annotations
4+
5+
import torch
6+
7+
import torchio as tio
8+
9+
10+
def _make_subject() -> tio.Subject:
11+
return tio.Subject(
12+
t1=tio.ScalarImage(torch.rand(1, 10, 10, 10)),
13+
seg=tio.LabelMap(torch.zeros(1, 10, 10, 10)),
14+
)
15+
16+
17+
class TestApplyInverseTransform:
18+
def test_flip_inverse(self) -> None:
19+
subject = _make_subject()
20+
original = subject.t1.data.clone()
21+
transformed = tio.Flip(axes=(0,))(subject)
22+
restored = transformed.apply_inverse_transform()
23+
torch.testing.assert_close(restored.t1.data, original)
24+
25+
def test_compose_inverse(self) -> None:
26+
subject = _make_subject()
27+
original = subject.t1.data.clone()
28+
pipeline = tio.Compose(
29+
[
30+
tio.Flip(axes=(0,)),
31+
tio.Flip(axes=(1,)),
32+
]
33+
)
34+
transformed = pipeline(subject)
35+
restored = transformed.apply_inverse_transform()
36+
torch.testing.assert_close(restored.t1.data, original)
37+
38+
def test_ignore_intensity(self) -> None:
39+
subject = _make_subject()
40+
original = subject.t1.data.clone()
41+
pipeline = tio.Compose(
42+
[
43+
tio.Flip(axes=(0,)),
44+
tio.Noise(std=0.1),
45+
]
46+
)
47+
transformed = pipeline(subject)
48+
restored = transformed.apply_inverse_transform(
49+
ignore_intensity=True,
50+
)
51+
# Shape restored, flip inverted, noise skipped.
52+
assert restored.t1.data.shape == original.shape
53+
54+
def test_get_inverse_transform(self) -> None:
55+
subject = _make_subject()
56+
transformed = tio.Flip(axes=(0,))(subject)
57+
inverse = transformed.get_inverse_transform()
58+
assert inverse is not None
59+
60+
def test_standalone_function(self) -> None:
61+
subject = _make_subject()
62+
original = subject.t1.data.clone()
63+
transformed = tio.Flip(axes=(0,))(subject)
64+
restored = tio.apply_inverse_transform(transformed)
65+
torch.testing.assert_close(restored.t1.data, original)
66+
67+
def test_no_history(self) -> None:
68+
"""Subject with no transforms should return itself."""
69+
subject = _make_subject()
70+
original = subject.t1.data.clone()
71+
restored = subject.apply_inverse_transform()
72+
torch.testing.assert_close(restored.t1.data, original)

tests/test_one_of.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Tests for OneOf transform."""
2+
3+
from __future__ import annotations
4+
5+
import torch
6+
7+
import torchio as tio
8+
9+
10+
def _make_subject() -> tio.Subject:
11+
return tio.Subject(
12+
t1=tio.ScalarImage(torch.rand(1, 10, 10, 10)),
13+
)
14+
15+
16+
class TestOneOf:
17+
def test_applies_one(self) -> None:
18+
subject = _make_subject()
19+
transform = tio.OneOf(
20+
[
21+
tio.Flip(axes=(0,)),
22+
tio.Gamma(log_gamma=0.3),
23+
]
24+
)
25+
result = transform(subject)
26+
assert result.t1.data.shape == subject.t1.data.shape
27+
28+
def test_single_transform(self) -> None:
29+
subject = _make_subject()
30+
result = tio.OneOf([tio.Flip(axes=(0,))])(subject)
31+
assert result.t1.data.shape == subject.t1.data.shape
32+
33+
def test_with_weights(self) -> None:
34+
subject = _make_subject()
35+
transform = tio.OneOf(
36+
{
37+
tio.Flip(axes=(0,)): 1.0,
38+
tio.Gamma(log_gamma=0.0): 0.0,
39+
}
40+
)
41+
result = transform(subject)
42+
assert result.t1.data.shape == subject.t1.data.shape
43+
44+
def test_history_recorded(self) -> None:
45+
subject = _make_subject()
46+
result = tio.OneOf([tio.Flip(axes=(0,))])(subject)
47+
assert len(result.applied_transforms) > 0

0 commit comments

Comments
 (0)