@@ -106,7 +106,7 @@ |
| 106 | 106 | //! should use stable substrings that are intentionally expected |
| 107 | 107 | //! across the requested matrix. |
| 108 | 108 | //! |
| 109 | | -//! ## FILE_CHECK / FILE_NOT / FILE_EXISTS / FILE_MISSING / FILE_LINE_COUNT / FILE_RERUN_MODE annotations |
| 109 | +//! ## FILE_CHECK / FILE_NOT / FILE_EXISTS / FILE_MISSING / FILE_LINE_COUNT / FILE_RERUN_MODE / FILE_SET_EXACT annotations |
| 110 | 110 | //! |
| 111 | 111 | //! Runtime tests can assert on files created inside their per-test |
| 112 | 112 | //! sandbox: |
@@ -127,6 +127,9 @@ |
| 127 | 127 | //! program is executed twice in the same sandbox, the named file must |
| 128 | 128 | //! either be byte-identical after both runs (`stable`) or grow by |
| 129 | 129 | //! strict append (`append`). |
| 130 | +//! * `! FILE_SET_EXACT: <relative-path>[,<relative-path>...]` — the |
| 131 | +//! final sandbox file set must match exactly, with no extra side |
| 132 | +//! effects beyond the listed relative paths. |
| 130 | 133 | //! |
| 131 | 134 | //! Paths are sandbox-relative on purpose. The harness runs each binary |
| 132 | 135 | //! in a private temp directory, so file assertions pin side effects |
@@ -169,6 +172,7 @@ |
| 169 | 172 | //! |
| 170 | 173 | //! * `! PHASE_TRIANGULATE: ir|asm|obj` |
| 171 | 174 | //! * `! PHASE_TRIANGULATE: ir|asm|obj|clean` |
| 175 | +//! * `! PHASE_TRIANGULATE: ir|asm|obj|repro` |
| 172 | 176 | //! |
| 173 | 177 | //! The linked runtime path is the anchor. If the program runs correctly but |
| 174 | 178 | //! one of the requested extra surfaces fails to compile or produces an empty |
@@ -180,6 +184,11 @@ |
| 180 | 184 | //! only their explicit output artifact in a private phase sandbox. That lets a |
| 181 | 185 | //! runtime-side-effecting program assert that `--emit-ir`, `-S`, and `-c` do |
| 182 | 186 | //! not accidentally create the files that only linked execution should create. |
| 187 | +//! |
| 188 | +//! `repro` strengthens it in a different direction: each requested compile-only |
| 189 | +//! phase must produce byte-identical output across two independent compilations. |
| 190 | +//! This keeps pipeline oracles from checking only "exists" when what we really |
| 191 | +//! need is "exists, stays clean, and stays deterministic". |
| 183 | 192 | |
| 184 | 193 | use std::collections::BTreeMap; |
| 185 | 194 | use std::fs; |
@@ -227,6 +236,11 @@ struct FileRerunModeCheck { |
| 227 | 236 | mode: FileRerunMode, |
| 228 | 237 | } |
| 229 | 238 | |
| 239 | +struct FileSetExactCheck { |
| 240 | + line_num: usize, |
| 241 | + rel_paths: Vec<String>, |
| 242 | +} |
| 243 | + |
| 230 | 244 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 231 | 245 | enum ReproStage { |
| 232 | 246 | Asm, |
@@ -256,6 +270,7 @@ enum PhaseSurface { |
| 256 | 270 | Asm, |
| 257 | 271 | Obj, |
| 258 | 272 | Clean, |
| 273 | + Repro, |
| 259 | 274 | } |
| 260 | 275 | |
| 261 | 276 | #[derive(Debug, Clone, PartialEq, Eq)] |
@@ -631,6 +646,62 @@ fn extract_file_rerun_mode_checks( |
| 631 | 646 | Ok(checks) |
| 632 | 647 | } |
| 633 | 648 | |
| 649 | +fn extract_file_set_exact( |
| 650 | + source: &str, |
| 651 | + filename: &str, |
| 652 | +) -> Result<Option<FileSetExactCheck>, String> { |
| 653 | + let mut found: Option<FileSetExactCheck> = None; |
| 654 | + for (i, line) in source.lines().enumerate() { |
| 655 | + let line_num = i + 1; |
| 656 | + let trimmed = line.trim(); |
| 657 | + let Some(rest) = trimmed.strip_prefix("! FILE_SET_EXACT:") else { |
| 658 | + continue; |
| 659 | + }; |
| 660 | + if let Some(existing) = &found { |
| 661 | + return Err(format!( |
| 662 | + "{}:{}: multiple FILE_SET_EXACT annotations are not allowed (another at line {})", |
| 663 | + filename, line_num, existing.line_num |
| 664 | + )); |
| 665 | + } |
| 666 | + |
| 667 | + let mut rel_paths = Vec::new(); |
| 668 | + for token in rest.trim().split(',') { |
| 669 | + let rel_path = token.trim(); |
| 670 | + if rel_path.is_empty() { |
| 671 | + return Err(format!( |
| 672 | + "{}:{}: FILE_SET_EXACT paths cannot be empty", |
| 673 | + filename, line_num |
| 674 | + )); |
| 675 | + } |
| 676 | + if Path::new(rel_path).is_absolute() { |
| 677 | + return Err(format!( |
| 678 | + "{}:{}: FILE_SET_EXACT paths must be relative, got '{}'", |
| 679 | + filename, line_num, rel_path |
| 680 | + )); |
| 681 | + } |
| 682 | + if !rel_paths |
| 683 | + .iter() |
| 684 | + .any(|existing: &String| existing == rel_path) |
| 685 | + { |
| 686 | + rel_paths.push(rel_path.to_string()); |
| 687 | + } |
| 688 | + } |
| 689 | + if rel_paths.is_empty() { |
| 690 | + return Err(format!( |
| 691 | + "{}:{}: FILE_SET_EXACT needs at least one relative path", |
| 692 | + filename, line_num |
| 693 | + )); |
| 694 | + } |
| 695 | + rel_paths.sort(); |
| 696 | + |
| 697 | + found = Some(FileSetExactCheck { |
| 698 | + line_num, |
| 699 | + rel_paths, |
| 700 | + }); |
| 701 | + } |
| 702 | + Ok(found) |
| 703 | +} |
| 704 | + |
| 634 | 705 | fn extract_repro_checks(source: &str, filename: &str) -> Result<Vec<ReproStage>, String> { |
| 635 | 706 | let mut stages = Vec::new(); |
| 636 | 707 | for (i, line) in source.lines().enumerate() { |
@@ -753,9 +824,10 @@ fn extract_phase_triangulation( |
| 753 | 824 | "asm" => PhaseSurface::Asm, |
| 754 | 825 | "obj" => PhaseSurface::Obj, |
| 755 | 826 | "clean" => PhaseSurface::Clean, |
| 827 | + "repro" => PhaseSurface::Repro, |
| 756 | 828 | other => { |
| 757 | 829 | return Err(format!( |
| 758 | | - "{}:{}: PHASE_TRIANGULATE surfaces must be ir, asm, obj, or clean; got '{}'", |
| 830 | + "{}:{}: PHASE_TRIANGULATE surfaces must be ir, asm, obj, clean, or repro; got '{}'", |
| 759 | 831 | filename, line_num, other |
| 760 | 832 | )) |
| 761 | 833 | } |
@@ -772,10 +844,10 @@ fn extract_phase_triangulation( |
| 772 | 844 | } |
| 773 | 845 | if surfaces |
| 774 | 846 | .iter() |
| 775 | | - .all(|surface| *surface == PhaseSurface::Clean) |
| 847 | + .all(|surface| matches!(surface, PhaseSurface::Clean | PhaseSurface::Repro)) |
| 776 | 848 | { |
| 777 | 849 | return Err(format!( |
| 778 | | - "{}:{}: PHASE_TRIANGULATE(clean) needs at least one of ir, asm, or obj", |
| 850 | + "{}:{}: PHASE_TRIANGULATE policy-only annotations need at least one of ir, asm, or obj", |
| 779 | 851 | filename, line_num |
| 780 | 852 | )); |
| 781 | 853 | } |
@@ -1048,11 +1120,27 @@ fn match_file_rerun_mode_checks( |
| 1048 | 1120 | Ok(()) |
| 1049 | 1121 | } |
| 1050 | 1122 | |
| 1123 | +fn match_file_set_exact( |
| 1124 | + check: &FileSetExactCheck, |
| 1125 | + files: &BTreeMap<String, Vec<u8>>, |
| 1126 | + filename: &str, |
| 1127 | +) -> Result<(), String> { |
| 1128 | + let actual = files.keys().cloned().collect::<Vec<_>>(); |
| 1129 | + if actual != check.rel_paths { |
| 1130 | + return Err(format!( |
| 1131 | + "{}:{}: FILE_SET_EXACT failed: expected {:?}, got {:?}", |
| 1132 | + filename, check.line_num, check.rel_paths, actual |
| 1133 | + )); |
| 1134 | + } |
| 1135 | + Ok(()) |
| 1136 | +} |
| 1137 | + |
| 1051 | 1138 | fn collect_declared_runtime_paths( |
| 1052 | 1139 | file_checks: &[FileCheck], |
| 1053 | 1140 | file_presence_checks: &[FilePresenceCheck], |
| 1054 | 1141 | file_line_count_checks: &[FileLineCountCheck], |
| 1055 | 1142 | file_rerun_mode_checks: &[FileRerunModeCheck], |
| 1143 | + file_set_exact: Option<&FileSetExactCheck>, |
| 1056 | 1144 | ) -> BTreeMap<String, String> { |
| 1057 | 1145 | let mut paths = BTreeMap::new(); |
| 1058 | 1146 | for check in file_checks { |
@@ -1083,6 +1171,13 @@ fn collect_declared_runtime_paths( |
| 1083 | 1171 | .entry(check.rel_path.clone()) |
| 1084 | 1172 | .or_insert_with(|| "FILE_RERUN_MODE".to_string()); |
| 1085 | 1173 | } |
| 1174 | + if let Some(check) = file_set_exact { |
| 1175 | + for rel_path in &check.rel_paths { |
| 1176 | + paths |
| 1177 | + .entry(rel_path.clone()) |
| 1178 | + .or_insert_with(|| "FILE_SET_EXACT".to_string()); |
| 1179 | + } |
| 1180 | + } |
| 1086 | 1181 | paths |
| 1087 | 1182 | } |
| 1088 | 1183 | |
@@ -1144,6 +1239,7 @@ fn compile_phase_artifact( |
| 1144 | 1239 | PhaseSurface::Asm => "asm", |
| 1145 | 1240 | PhaseSurface::Obj => "obj", |
| 1146 | 1241 | PhaseSurface::Clean => unreachable!("clean is a triangulation policy, not an artifact"), |
| 1242 | + PhaseSurface::Repro => unreachable!("repro is a triangulation policy, not an artifact"), |
| 1147 | 1243 | }; |
| 1148 | 1244 | let phase_sandbox = unique_temp_path( |
| 1149 | 1245 | "phase_sandbox", |
@@ -1164,6 +1260,7 @@ fn compile_phase_artifact( |
| 1164 | 1260 | PhaseSurface::Asm => ("phase-output.s", &["-S"]), |
| 1165 | 1261 | PhaseSurface::Obj => ("phase-output.o", &["-c"]), |
| 1166 | 1262 | PhaseSurface::Clean => unreachable!(), |
| 1263 | + PhaseSurface::Repro => unreachable!(), |
| 1167 | 1264 | }; |
| 1168 | 1265 | let output_path = phase_sandbox.join(output_name); |
| 1169 | 1266 | |
@@ -1486,6 +1583,7 @@ fn render_phase_surfaces(surfaces: &[PhaseSurface]) -> String { |
| 1486 | 1583 | PhaseSurface::Asm => "asm", |
| 1487 | 1584 | PhaseSurface::Obj => "obj", |
| 1488 | 1585 | PhaseSurface::Clean => "clean", |
| 1586 | + PhaseSurface::Repro => "repro", |
| 1489 | 1587 | }) |
| 1490 | 1588 | .collect::<Vec<_>>() |
| 1491 | 1589 | .join("|") |
@@ -1500,6 +1598,7 @@ fn run_phase_triangulation( |
| 1500 | 1598 | declared_runtime_paths: &BTreeMap<String, String>, |
| 1501 | 1599 | ) -> Result<(), String> { |
| 1502 | 1600 | let require_clean = triangulation.surfaces.contains(&PhaseSurface::Clean); |
| 1601 | + let require_repro = triangulation.surfaces.contains(&PhaseSurface::Repro); |
| 1503 | 1602 | for surface in &triangulation.surfaces { |
| 1504 | 1603 | match surface { |
| 1505 | 1604 | PhaseSurface::Ir | PhaseSurface::Asm | PhaseSurface::Obj => { |
@@ -1511,6 +1610,7 @@ fn run_phase_triangulation( |
| 1511 | 1610 | PhaseSurface::Asm => "assembly", |
| 1512 | 1611 | PhaseSurface::Obj => "object", |
| 1513 | 1612 | PhaseSurface::Clean => unreachable!(), |
| 1613 | + PhaseSurface::Repro => unreachable!(), |
| 1514 | 1614 | }; |
| 1515 | 1615 | return Err(format!( |
| 1516 | 1616 | "{}:{}: PHASE_TRIANGULATE({}) produced empty {} output at {}", |
@@ -1555,8 +1655,47 @@ fn run_phase_triangulation( |
| 1555 | 1655 | )); |
| 1556 | 1656 | } |
| 1557 | 1657 | } |
| 1658 | + if require_repro { |
| 1659 | + let second_artifact = |
| 1660 | + compile_phase_artifact(compiler, source, opt_flag, *surface, filename)?; |
| 1661 | + if require_clean { |
| 1662 | + let second_file_keys: Vec<&str> = second_artifact |
| 1663 | + .sandbox_files |
| 1664 | + .keys() |
| 1665 | + .map(|key| key.as_str()) |
| 1666 | + .collect(); |
| 1667 | + if second_file_keys != vec![second_artifact.output_rel_path.as_str()] { |
| 1668 | + return Err(format!( |
| 1669 | + "{}:{}: PHASE_TRIANGULATE({}) failed at {}: repeated compile-only phase left unexpected files {:?} (expected only '{}')", |
| 1670 | + filename, |
| 1671 | + triangulation.line_num, |
| 1672 | + render_phase_surfaces(&triangulation.surfaces), |
| 1673 | + opt_flag, |
| 1674 | + second_file_keys, |
| 1675 | + second_artifact.output_rel_path, |
| 1676 | + )); |
| 1677 | + } |
| 1678 | + } |
| 1679 | + if artifact.output_bytes != second_artifact.output_bytes { |
| 1680 | + let surface_name = match surface { |
| 1681 | + PhaseSurface::Ir => "IR", |
| 1682 | + PhaseSurface::Asm => "assembly", |
| 1683 | + PhaseSurface::Obj => "object", |
| 1684 | + PhaseSurface::Clean => unreachable!(), |
| 1685 | + PhaseSurface::Repro => unreachable!(), |
| 1686 | + }; |
| 1687 | + return Err(format!( |
| 1688 | + "{}:{}: PHASE_TRIANGULATE({}) failed at {}: {} output changed across repeated compile-only runs", |
| 1689 | + filename, |
| 1690 | + triangulation.line_num, |
| 1691 | + render_phase_surfaces(&triangulation.surfaces), |
| 1692 | + opt_flag, |
| 1693 | + surface_name, |
| 1694 | + )); |
| 1695 | + } |
| 1696 | + } |
| 1558 | 1697 | } |
| 1559 | | - PhaseSurface::Clean => {} |
| 1698 | + PhaseSurface::Clean | PhaseSurface::Repro => {} |
| 1560 | 1699 | } |
| 1561 | 1700 | } |
| 1562 | 1701 | Ok(()) |
@@ -1654,6 +1793,10 @@ fn run_test(compiler: &Path, source: &Path, opt_flag: &str) -> TestOutcome { |
| 1654 | 1793 | Ok(checks) => checks, |
| 1655 | 1794 | Err(e) => return TestOutcome::Fail(e), |
| 1656 | 1795 | }; |
| 1796 | + let file_set_exact = match extract_file_set_exact(&source_text, filename) { |
| 1797 | + Ok(check) => check, |
| 1798 | + Err(e) => return TestOutcome::Fail(e), |
| 1799 | + }; |
| 1657 | 1800 | let repro_checks = match extract_repro_checks(&source_text, filename) { |
| 1658 | 1801 | Ok(checks) => checks, |
| 1659 | 1802 | Err(e) => return TestOutcome::Fail(e), |
@@ -1674,6 +1817,7 @@ fn run_test(compiler: &Path, source: &Path, opt_flag: &str) -> TestOutcome { |
| 1674 | 1817 | && file_presence_checks.is_empty() |
| 1675 | 1818 | && file_line_count_checks.is_empty() |
| 1676 | 1819 | && file_rerun_mode_checks.is_empty() |
| 1820 | + && file_set_exact.is_none() |
| 1677 | 1821 | && repro_checks.is_empty() |
| 1678 | 1822 | && opt_eq_rules.is_empty() |
| 1679 | 1823 | && phase_triangulation.is_none() |
@@ -1685,7 +1829,7 @@ fn run_test(compiler: &Path, source: &Path, opt_flag: &str) -> TestOutcome { |
| 1685 | 1829 | // Programs with no runtime or shape assertions, no XFAIL marker, |
| 1686 | 1830 | // and no ERROR marker are mis-configured tests, not test failures. |
| 1687 | 1831 | return TestOutcome::Fail(format!( |
| 1688 | | - "{}: no CHECK / STDERR_CHECK / EXIT_CODE / IR_CHECK / ASM_CHECK / FILE_CHECK / FILE_EXISTS / FILE_MISSING / FILE_LINE_COUNT / FILE_RERUN_MODE / REPRO_CHECK / XFAIL / ERROR_EXPECTED / ERROR_SPAN annotations", |
| 1832 | + "{}: no CHECK / STDERR_CHECK / EXIT_CODE / IR_CHECK / ASM_CHECK / FILE_CHECK / FILE_EXISTS / FILE_MISSING / FILE_LINE_COUNT / FILE_RERUN_MODE / FILE_SET_EXACT / REPRO_CHECK / XFAIL / ERROR_EXPECTED / ERROR_SPAN annotations", |
| 1689 | 1833 | filename, |
| 1690 | 1834 | )); |
| 1691 | 1835 | } |
@@ -1823,6 +1967,13 @@ fn run_test(compiler: &Path, source: &Path, opt_flag: &str) -> TestOutcome { |
| 1823 | 1967 | let _ = fs::remove_dir_all(&sandbox); |
| 1824 | 1968 | return Err(e); |
| 1825 | 1969 | } |
| 1970 | + if let Some(check) = &file_set_exact { |
| 1971 | + if let Err(e) = match_file_set_exact(check, &snapshot.files, &label) { |
| 1972 | + let _ = fs::remove_file(&binary); |
| 1973 | + let _ = fs::remove_dir_all(&sandbox); |
| 1974 | + return Err(e); |
| 1975 | + } |
| 1976 | + } |
| 1826 | 1977 | if !file_rerun_mode_checks.is_empty() { |
| 1827 | 1978 | let second = run_binary_in_sandbox(&binary, &sandbox, filename)?; |
| 1828 | 1979 | if snapshot.exit_code != second.exit_code { |
@@ -1987,6 +2138,7 @@ fn run_test(compiler: &Path, source: &Path, opt_flag: &str) -> TestOutcome { |
| 1987 | 2138 | &file_presence_checks, |
| 1988 | 2139 | &file_line_count_checks, |
| 1989 | 2140 | &file_rerun_mode_checks, |
| 2141 | + file_set_exact.as_ref(), |
| 1990 | 2142 | ); |
| 1991 | 2143 | run_phase_triangulation( |
| 1992 | 2144 | compiler, |
@@ -2251,6 +2403,14 @@ fn extract_file_rerun_mode_checks_accepts_stable_and_append() { |
| 2251 | 2403 | assert_eq!(checks[1].mode, FileRerunMode::Append); |
| 2252 | 2404 | } |
| 2253 | 2405 | |
| 2406 | +#[test] |
| 2407 | +fn extract_file_set_exact_accepts_relative_paths() { |
| 2408 | + let check = extract_file_set_exact("! FILE_SET_EXACT: out.txt, log.txt\n", "inline.f90") |
| 2409 | + .unwrap() |
| 2410 | + .unwrap(); |
| 2411 | + assert_eq!(check.rel_paths, vec!["log.txt", "out.txt"]); |
| 2412 | +} |
| 2413 | + |
| 2254 | 2414 | #[test] |
| 2255 | 2415 | fn file_rerun_mode_matcher_accepts_strict_append_growth() { |
| 2256 | 2416 | let checks = vec![FileRerunModeCheck { |
@@ -2313,8 +2473,8 @@ fn extract_opt_eq_rules_rejects_unknown_component() { |
| 2313 | 2473 | } |
| 2314 | 2474 | |
| 2315 | 2475 | #[test] |
| 2316 | | -fn extract_phase_triangulation_accepts_ir_asm_obj_and_clean() { |
| 2317 | | - let source = "! PHASE_TRIANGULATE: ir|asm|obj|clean\n"; |
| 2476 | +fn extract_phase_triangulation_accepts_ir_asm_obj_clean_and_repro() { |
| 2477 | + let source = "! PHASE_TRIANGULATE: ir|asm|obj|clean|repro\n"; |
| 2318 | 2478 | let rule = extract_phase_triangulation(source, "inline.f90") |
| 2319 | 2479 | .unwrap() |
| 2320 | 2480 | .unwrap(); |
@@ -2324,7 +2484,8 @@ fn extract_phase_triangulation_accepts_ir_asm_obj_and_clean() { |
| 2324 | 2484 | PhaseSurface::Ir, |
| 2325 | 2485 | PhaseSurface::Asm, |
| 2326 | 2486 | PhaseSurface::Obj, |
| 2327 | | - PhaseSurface::Clean |
| 2487 | + PhaseSurface::Clean, |
| 2488 | + PhaseSurface::Repro |
| 2328 | 2489 | ] |
| 2329 | 2490 | ); |
| 2330 | 2491 | } |
@@ -2333,14 +2494,14 @@ fn extract_phase_triangulation_accepts_ir_asm_obj_and_clean() { |
| 2333 | 2494 | fn extract_phase_triangulation_rejects_unknown_surface() { |
| 2334 | 2495 | let source = "! PHASE_TRIANGULATE: run\n"; |
| 2335 | 2496 | let err = extract_phase_triangulation(source, "inline.f90").unwrap_err(); |
| 2336 | | - assert!(err.contains("ir, asm, obj, or clean")); |
| 2497 | + assert!(err.contains("ir, asm, obj, clean, or repro")); |
| 2337 | 2498 | } |
| 2338 | 2499 | |
| 2339 | 2500 | #[test] |
| 2340 | 2501 | fn extract_phase_triangulation_rejects_clean_only() { |
| 2341 | | - let source = "! PHASE_TRIANGULATE: clean\n"; |
| 2502 | + let source = "! PHASE_TRIANGULATE: clean|repro\n"; |
| 2342 | 2503 | let err = extract_phase_triangulation(source, "inline.f90").unwrap_err(); |
| 2343 | | - assert!(err.contains("needs at least one of ir, asm, or obj")); |
| 2504 | + assert!(err.contains("policy-only annotations")); |
| 2344 | 2505 | } |
| 2345 | 2506 | |
| 2346 | 2507 | #[test] |
@@ -2452,6 +2613,25 @@ fn file_presence_checks_allow_rewind_side_effects() { |
| 2452 | 2613 | } |
| 2453 | 2614 | } |
| 2454 | 2615 | |
| 2616 | +#[test] |
| 2617 | +fn file_set_exact_allows_rewind_single_output() { |
| 2618 | + let compiler = find_compiler(); |
| 2619 | + let test_dir = find_test_programs(); |
| 2620 | + let source = test_dir.join("io_rewind.f90"); |
| 2621 | + assert!( |
| 2622 | + source.exists(), |
| 2623 | + "io_rewind.f90 missing — needed for FILE_SET_EXACT coverage" |
| 2624 | + ); |
| 2625 | + |
| 2626 | + match run_test(&compiler, &source, "-O0") { |
| 2627 | + TestOutcome::Pass => {} |
| 2628 | + other => panic!( |
| 2629 | + "io_rewind.f90 should pass with FILE_SET_EXACT coverage, got {:?}", |
| 2630 | + other |
| 2631 | + ), |
| 2632 | + } |
| 2633 | +} |
| 2634 | + |
| 2455 | 2635 | #[test] |
| 2456 | 2636 | fn file_rerun_mode_append_fixture_tracks_current_compiler_gap() { |
| 2457 | 2637 | let compiler = find_compiler(); |
@@ -2547,6 +2727,25 @@ fn phase_triangulation_allows_function_call_pipeline_surfaces() { |
| 2547 | 2727 | } |
| 2548 | 2728 | } |
| 2549 | 2729 | |
| 2730 | +#[test] |
| 2731 | +fn phase_triangulation_repro_keeps_function_call_compile_surfaces_stable() { |
| 2732 | + let compiler = find_compiler(); |
| 2733 | + let test_dir = find_test_programs(); |
| 2734 | + let source = test_dir.join("function_call.f90"); |
| 2735 | + assert!( |
| 2736 | + source.exists(), |
| 2737 | + "function_call.f90 missing — needed for PHASE_TRIANGULATE(repro) coverage" |
| 2738 | + ); |
| 2739 | + |
| 2740 | + match run_test(&compiler, &source, "-O0") { |
| 2741 | + TestOutcome::Pass => {} |
| 2742 | + other => panic!( |
| 2743 | + "function_call.f90 should pass with PHASE_TRIANGULATE(repro) coverage, got {:?}", |
| 2744 | + other |
| 2745 | + ), |
| 2746 | + } |
| 2747 | +} |
| 2748 | + |
| 2550 | 2749 | #[test] |
| 2551 | 2750 | fn phase_triangulation_clean_keeps_compile_phases_free_of_runtime_files() { |
| 2552 | 2751 | let compiler = find_compiler(); |