Skip to content

Commit db2f8ce

Browse files
committed
test: cover file grouping for both modes plus TSV header
1 parent c73a149 commit db2f8ce

1 file changed

Lines changed: 148 additions & 5 deletions

File tree

Lines changed: 148 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,154 @@
11
"""Tests for _group_files and FileReader behavior."""
22

3+
import json
4+
from pathlib import Path
35
from typing import Callable
46

5-
from pyaslreport.modalities.asl.processor import ASLProcessor
7+
import pytest
68

9+
from pyaslreport.modalities.asl.processor import ASLProcessor, ProcessingContext
710

8-
def test_make_processor_factory(make_processor: Callable[..., ASLProcessor]) -> None:
9-
"""The make_processor fixture produces an ASLProcessor with the given files."""
10-
proc = make_processor(files=[])
11-
assert proc.data["files"] == []
11+
# ---------- _group_files: NIfTI mode (exact suffix matching) ----------
12+
13+
14+
class TestGroupFilesNiftiMode:
15+
def test_groups_asl_with_tsv_and_m0(
16+
self, make_processor: Callable[..., ASLProcessor], tmp_path: Path
17+
) -> None:
18+
"""A canonical BIDS triple groups together correctly."""
19+
asl_json = tmp_path / "sub-01_asl.json"
20+
asl_json.write_text(json.dumps({"M0Type": "Separate"}))
21+
tsv = tmp_path / "sub-01_aslcontext.tsv"
22+
tsv.write_text("volume_type\ncontrol\nlabel\n")
23+
m0_json = tmp_path / "sub-01_m0scan.json"
24+
m0_json.write_text(json.dumps({"EchoTime": 0.012}))
25+
26+
proc = make_processor(files=[str(asl_json), str(tsv), str(m0_json)])
27+
groups = proc._group_files("nifti")
28+
29+
assert len(groups) == 1
30+
g = groups[0]
31+
assert g["asl_json"][0] == "sub-01_asl.json"
32+
assert g["tsv"][0] == "sub-01_aslcontext.tsv"
33+
assert g["m0_json"][0] == "sub-01_m0scan.json"
34+
35+
def test_two_sessions_produce_two_groups(
36+
self, make_processor: Callable[..., ASLProcessor], tmp_path: Path
37+
) -> None:
38+
"""Two BIDS sessions in the same directory produce two groups."""
39+
# Build the list explicitly. Do NOT scan tmp_path with iterdir(): the
40+
# make_processor -> minimal_nifti_path fixture also writes asl.nii.gz
41+
# into tmp_path, and _group_files rejects unknown extensions.
42+
files: list[str] = []
43+
for i in [1, 2]:
44+
asl_json = tmp_path / f"sub-0{i}_asl.json"
45+
asl_json.write_text(json.dumps({"M0Type": "Separate"}))
46+
tsv = tmp_path / f"sub-0{i}_aslcontext.tsv"
47+
tsv.write_text("volume_type\ncontrol\nlabel\n")
48+
files.extend([str(asl_json), str(tsv)])
49+
proc = make_processor(files=files)
50+
groups = proc._group_files("nifti")
51+
assert len(groups) == 2
52+
53+
def test_unsupported_extension_raises(
54+
self, make_processor: Callable[..., ASLProcessor], tmp_path: Path
55+
) -> None:
56+
"""An unsupported extension raises ValueError during grouping."""
57+
bad = tmp_path / "weird.xml"
58+
bad.write_text("<x/>")
59+
proc = make_processor(files=[str(bad)])
60+
with pytest.raises(ValueError, match="Unsupported file format"):
61+
proc._group_files("nifti")
62+
63+
64+
# ---------- _group_files: DICOM mode (substring matching for m0) ----------
65+
66+
67+
class TestGroupFilesDicomMode:
68+
def test_dicom_mode_uses_substring_for_m0(
69+
self, make_processor: Callable[..., ASLProcessor], tmp_path: Path
70+
) -> None:
71+
"""In DICOM mode, any filename containing 'm0' counts as M0."""
72+
asl_json = tmp_path / "scan_dump.json"
73+
asl_json.write_text(json.dumps({"M0Type": "Separate"}))
74+
m0_json = tmp_path / "scan_m0_dump.json"
75+
m0_json.write_text(json.dumps({"EchoTime": 0.012}))
76+
77+
proc = make_processor(files=[str(asl_json), str(m0_json)])
78+
groups = proc._group_files("dicom")
79+
assert len(groups) == 1
80+
assert groups[0]["asl_json"][0] == "scan_dump.json"
81+
assert groups[0]["m0_json"][0] == "scan_m0_dump.json"
82+
83+
84+
# ---------- _validate_tsv_data: missing TSV behavior ----------
85+
86+
87+
class TestMissingTSV:
88+
def test_missing_tsv_in_nifti_mode_errors(
89+
self,
90+
make_processor: Callable[..., ASLProcessor],
91+
make_context: Callable[..., ProcessingContext],
92+
) -> None:
93+
"""Missing aslcontext.tsv in NIfTI mode produces a missing-file error."""
94+
proc = make_processor()
95+
ctx = make_context()
96+
group = {
97+
"asl_json": ("asl.json", {"M0Type": "Absent"}),
98+
"m0_json": None,
99+
"tsv": None,
100+
}
101+
proc._validate_tsv_data(group, ctx, "asl.json", group["asl_json"][1], "nifti")
102+
assert any("aslcontext.tsv" in e and "missing" in e for e in ctx.errors)
103+
104+
def test_missing_tsv_in_dicom_mode_falls_through_to_dicom_repetitions(
105+
self,
106+
make_processor: Callable[..., ASLProcessor],
107+
make_context: Callable[..., ProcessingContext],
108+
) -> None:
109+
"""Missing TSV in DICOM mode delegates to _analyze_dicom_repetitions."""
110+
proc = make_processor()
111+
ctx = make_context()
112+
asl_data = {"lRepetitions": 10}
113+
group = {"asl_json": ("asl.json", asl_data), "m0_json": None, "tsv": None}
114+
proc._validate_tsv_data(group, ctx, "asl.json", asl_data, "dicom")
115+
# _analyze_dicom_repetitions sets total_acquired_pairs from lRepetitions/2
116+
assert ctx.total_acquired_pairs == 5
117+
# No TSV-missing error in DICOM mode
118+
assert not any("aslcontext.tsv" in e for e in ctx.errors)
119+
120+
121+
# ---------- FileReader: TSV header enforcement ----------
122+
123+
124+
class TestFileReaderTSVHeader:
125+
def test_valid_header_returns_data(self, tmp_path: Path) -> None:
126+
"""A 'volume_type' header with rows returns the rows as a list."""
127+
from pyaslreport.io.readers.file_reader import FileReader
128+
129+
f = tmp_path / "valid.tsv"
130+
f.write_text("volume_type\ncontrol\nlabel\n")
131+
result = FileReader.read(str(f))
132+
assert result == ["control", "label"]
133+
134+
def test_invalid_header_raises(self, tmp_path: Path) -> None:
135+
"""A header that isn't exactly 'volume_type' raises RuntimeError.
136+
137+
NOTE: FileReader.read re-wraps the inner error as
138+
'Error reading file: Invalid TSV header, ...'. The substring match below
139+
still matches; do NOT anchor this regex with '^'.
140+
"""
141+
from pyaslreport.io.readers.file_reader import FileReader
142+
143+
f = tmp_path / "bad.tsv"
144+
f.write_text("volume_types\ncontrol\nlabel\n") # plural, wrong
145+
with pytest.raises(RuntimeError, match="Invalid TSV header"):
146+
FileReader.read(str(f))
147+
148+
def test_empty_file_returns_none(self, tmp_path: Path) -> None:
149+
"""A truly empty TSV returns None rather than raising."""
150+
from pyaslreport.io.readers.file_reader import FileReader
151+
152+
f = tmp_path / "empty.tsv"
153+
f.write_text("")
154+
assert FileReader.read(str(f)) is None

0 commit comments

Comments
 (0)