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