1- """Unit tests for the pure helpers behind the FP-stability cancellation pass and
2- its fypp macro-expansion flagging.
1+ """Unit tests for the pure helpers behind the FP-stability cancellation pass, its
2+ fypp macro-expansion flagging, scale-free pass/fail, and Verrou discovery/install .
33
44The Verrou subprocess machinery is exercised by the ./mfc.sh fp-stability CI job;
55here we test only the pure functions that decide what to instrument and how to
6- label results, so they can run without Verrou or built binaries.
6+ label results, so they can run without Verrou or built binaries. We keep the tests
7+ that pin a real behavioral contract or a subtle edge, not every micro-variation.
78"""
89
910from mfc .fp_stability_metrics import (
10- MIN_SIG_BITS ,
1111 _autodetect_compare ,
1212 _cancellation_severity ,
13- _digits_left ,
1413 _macro_context_in_lines ,
1514 _sig_bits ,
1615)
1716
18- # --- #2: fypp macro-expansion context detection ---
19-
20-
21- def test_macro_context_none_outside_any_block ():
22- lines = [
23- "subroutine s_foo()\n " ,
24- " a = b - c\n " ,
25- "end subroutine\n " ,
26- ]
27- assert _macro_context_in_lines (lines , 2 ) is None
17+ # --- fypp macro-expansion context detection (a #:for/#:def line maps to N instances) ---
2818
2919
3020def test_macro_context_inside_for_loop_body ():
@@ -37,6 +27,7 @@ def test_macro_context_inside_for_loop_body():
3727
3828
3929def test_macro_context_if_block_is_not_duplicating ():
30+ # #:if selects code but does not duplicate it, so it must NOT be flagged.
4031 lines = [
4132 "#:if FOO\n " ,
4233 " a = b - c\n " ,
@@ -45,50 +36,12 @@ def test_macro_context_if_block_is_not_duplicating():
4536 assert _macro_context_in_lines (lines , 2 ) is None
4637
4738
48- def test_macro_context_reports_innermost_duplicating_block ():
49- lines = [
50- "#:def MACRO(x)\n " ,
51- " #:if cond\n " ,
52- " #:for j in range(3)\n " ,
53- " y = ${x}$ - z\n " ,
54- " #:endfor\n " ,
55- " #:endif\n " ,
56- "#:enddef\n " ,
57- ]
58- assert _macro_context_in_lines (lines , 4 ) == "#:for"
59-
60-
61- def test_macro_context_balances_closers ():
62- lines = [
63- "#:for i in [1, 2]\n " ,
64- " a = b - c\n " ,
65- "#:endfor\n " ,
66- "d = e - f\n " ,
67- ]
68- # line 4 is after the loop closed -> not in any duplicating block
69- assert _macro_context_in_lines (lines , 4 ) is None
70-
71-
72- def test_macro_context_def_body_when_no_inner_loop ():
73- lines = [
74- "#:def GEOM(n)\n " ,
75- " r = x - y\n " ,
76- "#:enddef\n " ,
77- ]
78- assert _macro_context_in_lines (lines , 2 ) == "#:def"
79-
80-
81- def test_macro_context_block_and_call_are_duplicating ():
82- assert _macro_context_in_lines (["#:block B\n " , " a = b - c\n " , "#:endblock\n " ], 2 ) == "#:block"
83- assert _macro_context_in_lines (["#:call M()\n " , " a = b - c\n " , "#:endcall\n " ], 2 ) == "#:call"
84-
85-
8639def test_macro_context_unbalanced_close_is_safe ():
8740 # a stray #:endfor with an empty stack must not crash or misreport
8841 assert _macro_context_in_lines (["#:endfor\n " , " a = b - c\n " ], 2 ) is None
8942
9043
91- # --- per-site cancellation severity (bits lost), from a threshold sweep ---
44+ # --- per-site cancellation severity (highest bit-threshold a site survives) ---
9245
9346
9447def test_cancellation_severity_takes_highest_surviving_threshold ():
@@ -101,10 +54,6 @@ def test_cancellation_severity_takes_highest_surviving_threshold():
10154 assert _cancellation_severity (level_sites ) == {("a.fpp" , 1 ): 30 , ("b.fpp" , 2 ): 10 }
10255
10356
104- def test_cancellation_severity_empty ():
105- assert _cancellation_severity ([]) == {}
106-
107-
10857# --- auto-detect which output files to compare (for a user case) ---
10958
11059
@@ -123,47 +72,20 @@ def test_autodetect_compare_falls_back_to_prim_when_no_cons():
12372 assert _autodetect_compare (fns ) == ["prim.1.00.000010.dat" , "prim.3.00.000010.dat" ]
12473
12574
126- def test_autodetect_compare_empty_when_no_field_output ():
127- assert _autodetect_compare (["indices.dat" , "pre_time_data.dat" , "foo.txt" ]) == []
128-
129-
13075# --- scale-free pass/fail: significant bits retained ---
13176
13277
133- def test_sig_bits_relative_deviation ():
134- # max_dev/ref_scale = 1e-14 -> ~46.5 retained bits
135- assert 46 < _sig_bits (1e-14 , 1.0 ) < 47
136-
137-
13878def test_sig_bits_is_scale_free ():
13979 # same relative deviation -> same bits regardless of absolute magnitude
14080 assert abs (_sig_bits (1e-9 , 1.0 ) - _sig_bits (1e-4 , 1e5 )) < 1e-9
14181
14282
143- def test_sig_bits_zero_deviation_is_full_precision ():
144- assert _sig_bits (0.0 , 1.0 ) == 53.0
145-
146-
14783def test_sig_bits_zero_scale_is_safe ():
84+ # a zero/degenerate field scale must not divide-by-zero; report full precision
14885 assert _sig_bits (1e-12 , 0.0 ) == 53.0
14986
15087
151- def test_sig_bits_deviation_at_scale_is_unstable ():
152- # deviation as large as the field -> <= 0 retained bits
153- assert _sig_bits (1.0 , 1.0 ) <= 0.0
154-
155-
156- def test_min_sig_bits_is_single_precision_floor ():
157- assert MIN_SIG_BITS == 24
158-
159-
160- def test_digits_left_full_and_clamped ():
161- assert 15.5 < _digits_left (0 ) < 16.0 # full double ~ 16 sig digits
162- assert _digits_left (53 ) == 0.0
163- assert _digits_left (60 ) == 0.0 # clamp: never negative
164-
165-
166- # --- report emitters: must survive blank and populated result dicts (CI-only path) ---
88+ # --- report emitters: must survive the CI-only path without KeyError / regressions ---
16789
16890
16991def _emit_to_tmp (results , tmp_path , monkeypatch ):
@@ -185,26 +107,6 @@ def test_emit_summary_survives_blank_result(tmp_path, monkeypatch):
185107 assert "0 passed, 1 failed" in text
186108
187109
188- def test_emit_summary_populated_result (tmp_path , monkeypatch ):
189- from mfc .fp_stability import _blank_result
190-
191- r = _blank_result ("demo" )
192- r .update (
193- passed = False ,
194- max_dev = 1e-9 ,
195- sig_bits = 30.0 ,
196- float_proxy = 1e-6 ,
197- vprec = [(52 , 1e-14 ), (23 , float ("inf" ))], # exercises the "crash" branch
198- cancellation_locs = [("src/x/m_a.fpp" , 5 )],
199- cancellation_bits = {("src/x/m_a.fpp" , 5 ): 40 },
200- cancellation_macro = {("src/x/m_a.fpp" , 5 ): "#:for" },
201- float_max_locs = [("m_a.fpp" , 9 )],
202- )
203- text = _emit_to_tmp ([r ], tmp_path , monkeypatch )
204- assert "💥 crash" in text and "digits lost" in text
205- assert "may represent multiple instances" in text # fypp-ambiguous marker
206-
207-
208110def test_emit_annotations_cancellation_notes_fypp_ambiguity (tmp_path , monkeypatch , capsys ):
209111 from mfc import fp_stability_report as report
210112 from mfc .fp_stability import _blank_result
@@ -222,7 +124,7 @@ def test_emit_annotations_cancellation_notes_fypp_ambiguity(tmp_path, monkeypatc
222124 assert "multiple instances" in out # fypp-expanded cancellation site flagged
223125
224126
225- # --- Verrou discovery: a bare system valgrind must read as "Verrou absent" ---
127+ # --- Verrou discovery: a bare/broken valgrind must read as "Verrou absent" ---
226128
227129
228130def test_find_verrou_prefers_verrou_home_candidate (tmp_path , monkeypatch ):
@@ -264,15 +166,6 @@ def test_find_verrou_rejects_non_verrou_path_valgrind(tmp_path, monkeypatch):
264166 assert runners ._find_verrou () == ""
265167
266168
267- def test_find_verrou_accepts_verrou_enabled_path_valgrind (tmp_path , monkeypatch ):
268- from mfc import fp_stability_runners as runners
269-
270- monkeypatch .setenv ("VERROU_HOME" , str (tmp_path ))
271- monkeypatch .setattr (runners .shutil , "which" , lambda _name : "/opt/verrou/bin/valgrind" )
272- monkeypatch .setattr (runners , "_has_verrou_tool" , lambda _bin , _env = None : True )
273- assert runners ._find_verrou () == "/opt/verrou/bin/valgrind"
274-
275-
276169def test_has_verrou_tool_reflects_exit_code (monkeypatch ):
277170 from mfc import fp_stability_runners as runners
278171
@@ -304,14 +197,6 @@ def test_verrou_env_sets_valgrind_lib_when_libexec_present(tmp_path, monkeypatch
304197 assert env ["VALGRIND_LIB" ] == str (tmp_path / "libexec" / "valgrind" )
305198
306199
307- def test_verrou_env_omits_valgrind_lib_when_libexec_absent (tmp_path , monkeypatch ):
308- from mfc import fp_stability_runners as runners
309-
310- monkeypatch .delenv ("VALGRIND_LIB" , raising = False )
311- env = runners ._verrou_env (str (tmp_path / "bin" / "valgrind" ))
312- assert "VALGRIND_LIB" not in env
313-
314-
315200def test_verrou_env_preserves_user_valgrind_lib (tmp_path , monkeypatch ):
316201 from mfc import fp_stability_runners as runners
317202
@@ -321,17 +206,7 @@ def test_verrou_env_preserves_user_valgrind_lib(tmp_path, monkeypatch):
321206 assert env ["VALGRIND_LIB" ] == "/user/chosen/lib" # not clobbered
322207
323208
324- # --- auto-install hard-fail guards ---
325-
326-
327- def test_install_verrou_raises_when_bootstrap_fails (monkeypatch ):
328- import pytest
329-
330- from mfc import fp_stability as fps
331-
332- monkeypatch .setattr (fps .subprocess , "run" , lambda * a , ** k : type ("R" , (), {"returncode" : 1 })())
333- with pytest .raises (fps .MFCException , match = "Verrou install failed" ):
334- fps ._install_verrou ()
209+ # --- auto-install hard-fail guard (a green bootstrap that produced no binary) ---
335210
336211
337212def test_install_verrou_raises_when_no_binary_appears (monkeypatch ):
0 commit comments