Skip to content

Commit c73a149

Browse files
committed
test: cover M0 contradiction paths, TSV validation, and BS warnings
1 parent 7f628e5 commit c73a149

1 file changed

Lines changed: 183 additions & 7 deletions

File tree

Lines changed: 183 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,188 @@
1-
"""Tests for M0 contradiction and TSV validation branches in ASLProcessor."""
1+
"""Tests for M0 contradiction paths, TSV validation, and BS warnings."""
22

33
from typing import Callable
44

5-
from pyaslreport.modalities.asl.processor import ProcessingContext
5+
from pyaslreport.modalities.asl.processor import ASLProcessor, ProcessingContext
66

7+
# ---------- _validate_m0_data ----------
78

8-
def test_make_context_factory(make_context: Callable[..., ProcessingContext]) -> None:
9-
"""The make_context fixture produces a ProcessingContext with overrides applied."""
10-
ctx = make_context(m0_type="Separate")
11-
assert ctx.m0_type == "Separate"
12-
assert ctx.errors == []
9+
10+
class TestValidateM0Data:
11+
def test_separate_with_no_m0_file_errors(
12+
self,
13+
make_processor: Callable[..., ASLProcessor],
14+
make_context: Callable[..., ProcessingContext],
15+
) -> None:
16+
"""M0Type=Separate but m0_json missing -> error appended."""
17+
proc = make_processor()
18+
ctx = make_context(m0_type="Separate")
19+
group = {
20+
"asl_json": ("asl.json", {"M0Type": "Separate"}),
21+
"m0_json": None,
22+
"tsv": None,
23+
}
24+
proc._validate_m0_data(group, ctx, "asl.json", group["asl_json"][1])
25+
assert any("Separate" in e and "not provided" in e for e in ctx.errors)
26+
27+
def test_absent_with_m0_file_errors(
28+
self,
29+
make_processor: Callable[..., ASLProcessor],
30+
make_context: Callable[..., ProcessingContext],
31+
) -> None:
32+
"""M0Type=Absent but m0_json present -> error appended."""
33+
proc = make_processor()
34+
ctx = make_context(m0_type="Absent")
35+
m0_data = {"EchoTime": 0.012}
36+
group = {
37+
"asl_json": ("asl.json", {"M0Type": "Absent"}),
38+
"m0_json": ("m0.json", m0_data),
39+
"tsv": None,
40+
}
41+
proc._validate_m0_data(group, ctx, "asl.json", group["asl_json"][1])
42+
assert any("Absent" in e and "is present" in e for e in ctx.errors)
43+
44+
def test_included_with_separate_m0_file_errors(
45+
self,
46+
make_processor: Callable[..., ASLProcessor],
47+
make_context: Callable[..., ProcessingContext],
48+
) -> None:
49+
"""M0Type=Included but separate m0_json provided -> error."""
50+
proc = make_processor()
51+
ctx = make_context(m0_type="Included")
52+
m0_data = {"EchoTime": 0.012}
53+
group = {
54+
"asl_json": ("asl.json", {"M0Type": "Included"}),
55+
"m0_json": ("m0.json", m0_data),
56+
"tsv": None,
57+
}
58+
proc._validate_m0_data(group, ctx, "asl.json", group["asl_json"][1])
59+
assert any("Included" in e for e in ctx.errors)
60+
61+
def test_separate_with_m0_file_no_error(
62+
self,
63+
make_processor: Callable[..., ASLProcessor],
64+
make_context: Callable[..., ProcessingContext],
65+
) -> None:
66+
"""M0Type=Separate with m0_json present -> no contradiction.
67+
68+
Both ASL and M0 dicts agree on the five compare_params fields to avoid
69+
spurious errors from that helper.
70+
"""
71+
proc = make_processor()
72+
ctx = make_context(m0_type="Separate")
73+
m0_data = {
74+
"EchoTime": 0.012,
75+
"FlipAngle": 90,
76+
"MagneticFieldStrength": 3,
77+
"MRAcquisitionType": "3D",
78+
"PulseSequenceType": "GRASE",
79+
}
80+
asl_data = dict(m0_data, M0Type="Separate")
81+
group = {
82+
"asl_json": ("asl.json", asl_data),
83+
"m0_json": ("m0.json", m0_data),
84+
"tsv": None,
85+
}
86+
proc._validate_m0_data(group, ctx, "asl.json", asl_data)
87+
m0_type_errors = [
88+
e for e in ctx.errors if "M0 type" in e or "specified as" in e
89+
]
90+
assert m0_type_errors == []
91+
92+
93+
# ---------- TSV: _analyze_tsv_volume_types and _validate_m0scan_consistency ----------
94+
95+
96+
class TestTSVValidation:
97+
def test_absent_with_m0scan_in_tsv_errors(
98+
self,
99+
make_processor: Callable[..., ASLProcessor],
100+
make_context: Callable[..., ProcessingContext],
101+
) -> None:
102+
"""M0Type=Absent but TSV contains 'm0scan' -> error."""
103+
proc = make_processor()
104+
ctx = make_context(m0_type="Absent")
105+
asl_data = {"M0Type": "Absent"}
106+
tsv_data = ["m0scan", "control", "label"]
107+
proc._analyze_tsv_volume_types(
108+
tsv_data, ctx, "asl.json", asl_data, "context.tsv"
109+
)
110+
assert any("Absent" in e and "m0scan" in e for e in ctx.errors)
111+
112+
def test_separate_with_m0scan_in_tsv_errors(
113+
self,
114+
make_processor: Callable[..., ASLProcessor],
115+
make_context: Callable[..., ProcessingContext],
116+
) -> None:
117+
"""M0Type=Separate but TSV contains 'm0scan' -> error."""
118+
proc = make_processor()
119+
ctx = make_context(m0_type="Separate")
120+
asl_data = {"M0Type": "Separate"}
121+
tsv_data = ["m0scan", "control", "label"]
122+
proc._analyze_tsv_volume_types(
123+
tsv_data, ctx, "asl.json", asl_data, "context.tsv"
124+
)
125+
assert any("Separate" in e and "m0scan" in e for e in ctx.errors)
126+
127+
def test_total_acquired_pairs_set(
128+
self,
129+
make_processor: Callable[..., ASLProcessor],
130+
make_context: Callable[..., ProcessingContext],
131+
) -> None:
132+
"""A clean TSV populates TotalAcquiredPairs."""
133+
proc = make_processor()
134+
ctx = make_context(m0_type="Included")
135+
asl_data = {
136+
"M0Type": "Included",
137+
"RepetitionTimePreparation": 4.0,
138+
"BackgroundSuppression": False,
139+
}
140+
tsv_data = ["m0scan", "control", "label", "control", "label"]
141+
proc._analyze_tsv_volume_types(
142+
tsv_data, ctx, "asl.json", asl_data, "context.tsv"
143+
)
144+
assert asl_data["TotalAcquiredPairs"] == 2 # two control-label pairs
145+
146+
147+
# ---------- _handle_no_m0scan_warnings ----------
148+
149+
150+
class TestBackgroundSuppressionWarnings:
151+
def test_bs_off_no_warning(
152+
self,
153+
make_processor: Callable[..., ASLProcessor],
154+
make_context: Callable[..., ProcessingContext],
155+
) -> None:
156+
"""BackgroundSuppression off -> no warnings."""
157+
proc = make_processor()
158+
ctx = make_context()
159+
asl_data = {"BackgroundSuppression": False}
160+
proc._handle_no_m0scan_warnings(ctx, "asl.json", asl_data)
161+
assert ctx.warnings == []
162+
163+
def test_bs_on_with_pulse_time_warns_about_efficiency(
164+
self,
165+
make_processor: Callable[..., ASLProcessor],
166+
make_context: Callable[..., ProcessingContext],
167+
) -> None:
168+
"""BS on with pulse times -> efficiency warning."""
169+
proc = make_processor()
170+
ctx = make_context()
171+
asl_data = {
172+
"BackgroundSuppression": True,
173+
"BackgroundSuppressionPulseTime": [0.15, 0.5],
174+
}
175+
proc._handle_no_m0scan_warnings(ctx, "asl.json", asl_data)
176+
assert any("BS-pulse efficiency" in w for w in ctx.warnings)
177+
178+
def test_bs_on_no_pulse_time_warns_about_relative_quantification(
179+
self,
180+
make_processor: Callable[..., ASLProcessor],
181+
make_context: Callable[..., ProcessingContext],
182+
) -> None:
183+
"""BS on without pulse times -> relative-quantification warning."""
184+
proc = make_processor()
185+
ctx = make_context()
186+
asl_data = {"BackgroundSuppression": True}
187+
proc._handle_no_m0scan_warnings(ctx, "asl.json", asl_data)
188+
assert any("relative quantification" in w for w in ctx.warnings)

0 commit comments

Comments
 (0)