Skip to content

Commit 12506ca

Browse files
committed
test: cover normalization, unit conversion, and rounding
1 parent c7d1195 commit 12506ca

1 file changed

Lines changed: 157 additions & 5 deletions

File tree

Lines changed: 157 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,160 @@
1-
"""Tests for normalization logic: _rename_fields, _convert_units_to_milliseconds."""
1+
"""Tests for normalization logic in ASLProcessor."""
22

3+
from typing import Callable
34

4-
def test_module_imports() -> None:
5-
"""The processor module imports cleanly."""
6-
from pyaslreport.modalities.asl.processor import ASLProcessor
5+
import pytest
76

8-
assert ASLProcessor is not None
7+
from pyaslreport.modalities.asl.processor import ASLProcessor
8+
from pyaslreport.utils.math_utils import MathUtils
9+
from pyaslreport.utils.unit_conversion_utils import UnitConverterUtils
10+
11+
# ---------- _rename_fields ----------
12+
13+
14+
@pytest.mark.parametrize(
15+
"old_key,new_key,value",
16+
[
17+
("RepetitionTime", "RepetitionTimePreparation", 4.5),
18+
("InversionTime", "PostLabelingDelay", 1.8),
19+
("BolusDuration", "BolusCutOffDelayTime", 0.7),
20+
("InitialPostLabelDelay", "PostLabelingDelay", 1.8),
21+
],
22+
)
23+
def test_rename_fields_renames_and_deletes_legacy(
24+
make_processor: Callable[..., ASLProcessor],
25+
old_key: str,
26+
new_key: str,
27+
value: float,
28+
) -> None:
29+
"""Each of the four mappings copies old->new and deletes the old key."""
30+
proc = make_processor()
31+
session = {old_key: value}
32+
proc._rename_fields(session)
33+
assert session[new_key] == value
34+
assert old_key not in session
35+
36+
37+
def test_rename_fields_numrfblocks_derives_labeling_duration(
38+
make_processor: Callable[..., ASLProcessor],
39+
) -> None:
40+
"""NumRFBlocks=100 derives LabelingDuration=1.84 (pins the numeric contract)."""
41+
proc = make_processor()
42+
session = {"NumRFBlocks": 100}
43+
proc._rename_fields(session)
44+
assert session["LabelingDuration"] == pytest.approx(1.84)
45+
46+
47+
def test_rename_fields_numrfblocks_retains_source(
48+
make_processor: Callable[..., ASLProcessor],
49+
) -> None:
50+
"""CURRENT behavior: NumRFBlocks is NOT deleted after deriving LabelingDuration.
51+
52+
Retention vs removal is an open contract question with mentors (provenance).
53+
If they decide to remove it, change this to `assert "NumRFBlocks" not in session`.
54+
"""
55+
proc = make_processor()
56+
session = {"NumRFBlocks": 100}
57+
proc._rename_fields(session)
58+
assert "NumRFBlocks" in session
59+
60+
61+
def test_rename_fields_no_legacy_keys_is_noop(
62+
make_processor: Callable[..., ASLProcessor],
63+
) -> None:
64+
"""A modern session with no legacy keys is unchanged."""
65+
proc = make_processor()
66+
session = {"PostLabelingDelay": 1.8, "RepetitionTimePreparation": 4.0}
67+
original = dict(session)
68+
proc._rename_fields(session)
69+
assert session == original
70+
71+
72+
# ---------- _convert_units_to_milliseconds ----------
73+
74+
TIME_FIELDS = [
75+
"EchoTime",
76+
"RepetitionTimePreparation",
77+
"LabelingDuration",
78+
"BolusCutOffDelayTime",
79+
"BackgroundSuppressionPulseTime",
80+
"PostLabelingDelay",
81+
]
82+
83+
84+
@pytest.mark.parametrize("field", TIME_FIELDS)
85+
def test_convert_scalar(
86+
make_processor: Callable[..., ASLProcessor], field: str
87+
) -> None:
88+
"""Each time field scalar is multiplied by 1000."""
89+
proc = make_processor()
90+
session = {field: 1.5}
91+
proc._convert_units_to_milliseconds(session)
92+
assert session[field] == 1500
93+
94+
95+
@pytest.mark.parametrize("field", TIME_FIELDS)
96+
def test_convert_list(make_processor: Callable[..., ASLProcessor], field: str) -> None:
97+
"""Each time field list maps element-wise."""
98+
proc = make_processor()
99+
session = {field: [1.0, 2.0, 3.0]}
100+
proc._convert_units_to_milliseconds(session)
101+
assert session[field] == [1000, 2000, 3000]
102+
103+
104+
def test_convert_leaves_non_time_fields(
105+
make_processor: Callable[..., ASLProcessor],
106+
) -> None:
107+
"""Non-time fields untouched; time field converted."""
108+
proc = make_processor()
109+
session = {"FlipAngle": 90, "EchoTime": 0.012}
110+
proc._convert_units_to_milliseconds(session)
111+
assert session["FlipAngle"] == 90
112+
assert session["EchoTime"] == 12
113+
114+
115+
# ---------- MathUtils.round_if_close ----------
116+
117+
118+
@pytest.mark.parametrize(
119+
"input_val,expected",
120+
[
121+
(2000.0, 2000),
122+
(2000.0000001, 2000),
123+
(2000.5, 2000.5),
124+
(1999.9999999, 2000),
125+
(2000.123456, 2000.123),
126+
(0.0, 0),
127+
(-2000.0, -2000),
128+
],
129+
)
130+
def test_round_if_close(input_val: float, expected: int | float) -> None:
131+
"""Snaps to int within 1e-6 of an integer; else rounds to 3 decimals."""
132+
result = MathUtils.round_if_close(input_val)
133+
assert result == expected
134+
if isinstance(expected, int):
135+
assert isinstance(result, int)
136+
137+
138+
# ---------- UnitConverterUtils.convert_to_milliseconds ----------
139+
140+
141+
def test_convert_to_ms_scalar() -> None:
142+
"""A scalar in seconds becomes milliseconds (int when close to integer)."""
143+
assert UnitConverterUtils.convert_to_milliseconds(2.0) == 2000
144+
145+
146+
def test_convert_to_ms_list() -> None:
147+
"""A list maps element-wise."""
148+
assert UnitConverterUtils.convert_to_milliseconds([1.0, 2.0]) == [1000, 2000]
149+
150+
151+
def test_convert_to_ms_rejects_string() -> None:
152+
"""A non-numeric scalar raises TypeError."""
153+
with pytest.raises(TypeError):
154+
UnitConverterUtils.convert_to_milliseconds("not a number")
155+
156+
157+
def test_convert_to_ms_rejects_list_with_string() -> None:
158+
"""A list containing a non-number raises TypeError."""
159+
with pytest.raises(TypeError):
160+
UnitConverterUtils.convert_to_milliseconds([1.0, "two"])

0 commit comments

Comments
 (0)