| 1 | //! Generated multi-file dependency chain tests. |
| 2 | //! |
| 3 | //! Programmatically builds N-module chains, diamond patterns, and trees, |
| 4 | //! compiles each to .o in dependency order, links, runs, and verifies |
| 5 | //! that symbols propagate through deep dependency chains. |
| 6 | |
| 7 | use std::fs; |
| 8 | use std::path::{Path, PathBuf}; |
| 9 | use std::process::Command; |
| 10 | use std::sync::atomic::{AtomicU64, Ordering}; |
| 11 | |
| 12 | static NEXT_ID: AtomicU64 = AtomicU64::new(0); |
| 13 | |
| 14 | fn unique_dir(prefix: &str) -> PathBuf { |
| 15 | let id = NEXT_ID.fetch_add(1, Ordering::Relaxed); |
| 16 | let dir = |
| 17 | std::env::temp_dir().join(format!("afs_gen_{}_{}_{}", prefix, std::process::id(), id)); |
| 18 | fs::create_dir_all(&dir).unwrap(); |
| 19 | dir |
| 20 | } |
| 21 | |
| 22 | fn find_compiler() -> PathBuf { |
| 23 | for c in &["target/release/armfortas", "target/debug/armfortas"] { |
| 24 | let p = PathBuf::from(c); |
| 25 | if p.exists() { |
| 26 | return fs::canonicalize(&p).unwrap(); |
| 27 | } |
| 28 | } |
| 29 | panic!("armfortas binary not found"); |
| 30 | } |
| 31 | |
| 32 | fn find_runtime() -> PathBuf { |
| 33 | for dir in &["target/release", "target/debug"] { |
| 34 | let p = PathBuf::from(dir).join("libarmfortas_rt.a"); |
| 35 | if p.exists() { |
| 36 | return p; |
| 37 | } |
| 38 | } |
| 39 | panic!("libarmfortas_rt.a not found"); |
| 40 | } |
| 41 | |
| 42 | fn sdk_path() -> String { |
| 43 | let out = Command::new("xcrun") |
| 44 | .args(["--sdk", "macosx", "--show-sdk-path"]) |
| 45 | .output() |
| 46 | .expect("xcrun failed"); |
| 47 | String::from_utf8(out.stdout).unwrap().trim().to_string() |
| 48 | } |
| 49 | |
| 50 | fn compile_file(compiler: &Path, source: &Path, output: &Path, search_dir: &Path, opt: &str) { |
| 51 | let result = Command::new(compiler) |
| 52 | .args([ |
| 53 | source.to_str().unwrap(), |
| 54 | "-c", |
| 55 | opt, |
| 56 | "-o", |
| 57 | output.to_str().unwrap(), |
| 58 | &format!("-I{}", search_dir.display()), |
| 59 | ]) |
| 60 | .output() |
| 61 | .expect("compiler launch failed"); |
| 62 | assert!( |
| 63 | result.status.success(), |
| 64 | "compile {} failed:\n{}", |
| 65 | source.display(), |
| 66 | String::from_utf8_lossy(&result.stderr) |
| 67 | ); |
| 68 | } |
| 69 | |
| 70 | fn link_files(objects: &[PathBuf], output: &Path) { |
| 71 | let runtime = find_runtime(); |
| 72 | let sdk = sdk_path(); |
| 73 | let mut args: Vec<String> = vec!["-o".into(), output.to_str().unwrap().into()]; |
| 74 | for o in objects { |
| 75 | args.push(o.to_str().unwrap().into()); |
| 76 | } |
| 77 | args.push(runtime.to_str().unwrap().into()); |
| 78 | args.extend([ |
| 79 | "-lSystem".into(), |
| 80 | "-syslibroot".into(), |
| 81 | sdk, |
| 82 | "-arch".into(), |
| 83 | "arm64".into(), |
| 84 | ]); |
| 85 | let result = Command::new("ld") |
| 86 | .args(&args) |
| 87 | .output() |
| 88 | .expect("ld launch failed"); |
| 89 | assert!( |
| 90 | result.status.success(), |
| 91 | "link failed:\n{}", |
| 92 | String::from_utf8_lossy(&result.stderr) |
| 93 | ); |
| 94 | } |
| 95 | |
| 96 | fn run_binary(binary: &Path) -> String { |
| 97 | let result = Command::new(binary).output().expect("binary launch failed"); |
| 98 | assert!( |
| 99 | result.status.success(), |
| 100 | "{} exited with {:?}\nstderr: {}", |
| 101 | binary.display(), |
| 102 | result.status.code(), |
| 103 | String::from_utf8_lossy(&result.stderr) |
| 104 | ); |
| 105 | String::from_utf8_lossy(&result.stdout).into_owned() |
| 106 | } |
| 107 | |
| 108 | // ---- Generators ---- |
| 109 | |
| 110 | /// Generate a linear chain of N modules: |
| 111 | /// M1 uses M2, M2 uses M3, ..., M_N uses nothing. |
| 112 | /// Each module defines a PARAMETER that references the previous. |
| 113 | /// The main program prints the final accumulated value. |
| 114 | fn gen_chain(depth: usize) -> (Vec<(String, String)>, String, &'static str) { |
| 115 | let mut files = Vec::new(); |
| 116 | |
| 117 | // Module N (leaf) — no dependencies. |
| 118 | files.push(( |
| 119 | format!("mod_{}.f90", depth), |
| 120 | format!( |
| 121 | "module mod_{n}\n implicit none\n integer, parameter :: val_{n} = {n}\nend module\n", |
| 122 | n = depth |
| 123 | ), |
| 124 | )); |
| 125 | |
| 126 | // Modules N-1 down to 1 — each uses the next. |
| 127 | for i in (1..depth).rev() { |
| 128 | files.push(( |
| 129 | format!("mod_{}.f90", i), |
| 130 | format!( |
| 131 | "module mod_{i}\n use mod_{next}\n implicit none\n \ |
| 132 | integer, parameter :: val_{i} = val_{next} + {i}\nend module\n", |
| 133 | i = i, |
| 134 | next = i + 1 |
| 135 | ), |
| 136 | )); |
| 137 | } |
| 138 | |
| 139 | // Main program uses mod_1 and prints the accumulated value. |
| 140 | let expected: usize = (1..=depth).sum(); |
| 141 | let main_src = |
| 142 | format!("program p\n use mod_1\n implicit none\n print *, val_1\nend program\n"); |
| 143 | |
| 144 | // Files are already in compilation order: leaf first, then towards root. |
| 145 | let expected_str = Box::leak(format!("{}", expected).into_boxed_str()); |
| 146 | (files, main_src, expected_str) |
| 147 | } |
| 148 | |
| 149 | /// Generate a diamond: A uses B1..B_width, each Bi uses C. |
| 150 | /// C defines a base value, each Bi adds its index, A sums them. |
| 151 | fn gen_diamond(width: usize) -> (Vec<(String, String)>, String, &'static str) { |
| 152 | let mut files = Vec::new(); |
| 153 | |
| 154 | // C (leaf). |
| 155 | files.push(( |
| 156 | "mod_c.f90".into(), |
| 157 | "module mod_c\n implicit none\n integer, parameter :: base = 100\nend module\n".into(), |
| 158 | )); |
| 159 | |
| 160 | // B1..B_width. |
| 161 | for i in 1..=width { |
| 162 | files.push(( |
| 163 | format!("mod_b{}.f90", i), |
| 164 | format!( |
| 165 | "module mod_b{i}\n use mod_c\n implicit none\n \ |
| 166 | integer, parameter :: val_b{i} = base + {i}\nend module\n", |
| 167 | i = i |
| 168 | ), |
| 169 | )); |
| 170 | } |
| 171 | |
| 172 | // Main program uses all Bi and prints the sum. |
| 173 | let use_stmts: String = (1..=width) |
| 174 | .map(|i| format!(" use mod_b{}", i)) |
| 175 | .collect::<Vec<_>>() |
| 176 | .join("\n"); |
| 177 | let sum_expr: String = (1..=width) |
| 178 | .map(|i| format!("val_b{}", i)) |
| 179 | .collect::<Vec<_>>() |
| 180 | .join(" + "); |
| 181 | let main_src = format!( |
| 182 | "program p\n{}\n implicit none\n print *, {}\nend program\n", |
| 183 | use_stmts, sum_expr |
| 184 | ); |
| 185 | |
| 186 | let expected: usize = (1..=width).map(|i| 100 + i).sum(); |
| 187 | let expected_str = Box::leak(format!("{}", expected).into_boxed_str()); |
| 188 | (files, main_src, expected_str) |
| 189 | } |
| 190 | |
| 191 | fn run_generated_test( |
| 192 | files: Vec<(String, String)>, |
| 193 | main_src: String, |
| 194 | expected: &str, |
| 195 | opt: &str, |
| 196 | label: &str, |
| 197 | ) { |
| 198 | let compiler = find_compiler(); |
| 199 | let dir = unique_dir(label); |
| 200 | |
| 201 | // Write all module files. |
| 202 | for (name, src) in &files { |
| 203 | fs::write(dir.join(name), src).unwrap(); |
| 204 | } |
| 205 | fs::write(dir.join("main.f90"), &main_src).unwrap(); |
| 206 | |
| 207 | // Compile modules in order (they're already in dependency order). |
| 208 | let mut objects = Vec::new(); |
| 209 | for (name, _) in &files { |
| 210 | let f90 = dir.join(name); |
| 211 | let stem = name.trim_end_matches(".f90"); |
| 212 | let obj = dir.join(format!("{}.o", stem)); |
| 213 | compile_file(&compiler, &f90, &obj, &dir, opt); |
| 214 | objects.push(obj); |
| 215 | } |
| 216 | |
| 217 | // Compile main. |
| 218 | let main_o = dir.join("main.o"); |
| 219 | compile_file(&compiler, &dir.join("main.f90"), &main_o, &dir, opt); |
| 220 | objects.push(main_o); |
| 221 | |
| 222 | // Link and run. |
| 223 | let binary = dir.join("test_bin"); |
| 224 | link_files(&objects, &binary); |
| 225 | let output = run_binary(&binary); |
| 226 | |
| 227 | assert!( |
| 228 | output.contains(expected), |
| 229 | "{} [{}]: expected '{}' in output, got:\n{}", |
| 230 | label, |
| 231 | opt, |
| 232 | expected, |
| 233 | output |
| 234 | ); |
| 235 | |
| 236 | let _ = fs::remove_dir_all(&dir); |
| 237 | } |
| 238 | |
| 239 | // ---- Tests ---- |
| 240 | |
| 241 | #[test] |
| 242 | fn chain_depth_5() { |
| 243 | let (files, main_src, expected) = gen_chain(5); |
| 244 | run_generated_test(files, main_src, expected, "-O0", "chain5"); |
| 245 | } |
| 246 | |
| 247 | #[test] |
| 248 | fn chain_depth_10() { |
| 249 | let (files, main_src, expected) = gen_chain(10); |
| 250 | run_generated_test(files, main_src, expected, "-O0", "chain10"); |
| 251 | } |
| 252 | |
| 253 | #[test] |
| 254 | fn chain_depth_20() { |
| 255 | let (files, main_src, expected) = gen_chain(20); |
| 256 | run_generated_test(files, main_src, expected, "-O0", "chain20"); |
| 257 | } |
| 258 | |
| 259 | #[test] |
| 260 | fn chain_depth_10_o2() { |
| 261 | let (files, main_src, expected) = gen_chain(10); |
| 262 | run_generated_test(files, main_src, expected, "-O2", "chain10_o2"); |
| 263 | } |
| 264 | |
| 265 | #[test] |
| 266 | fn diamond_width_4() { |
| 267 | let (files, main_src, expected) = gen_diamond(4); |
| 268 | run_generated_test(files, main_src, expected, "-O0", "diamond4"); |
| 269 | } |
| 270 | |
| 271 | #[test] |
| 272 | fn diamond_width_8() { |
| 273 | let (files, main_src, expected) = gen_diamond(8); |
| 274 | run_generated_test(files, main_src, expected, "-O0", "diamond8"); |
| 275 | } |
| 276 | |
| 277 | #[test] |
| 278 | fn diamond_width_4_o2() { |
| 279 | let (files, main_src, expected) = gen_diamond(4); |
| 280 | run_generated_test(files, main_src, expected, "-O2", "diamond4_o2"); |
| 281 | } |
| 282 | |
| 283 | // ---- Cross-optimization ABI matrix tests ---- |
| 284 | |
| 285 | /// Compile modules at one opt level, main at another, link together. |
| 286 | /// Verifies ABI consistency across optimization levels. |
| 287 | fn run_cross_opt_test( |
| 288 | files: Vec<(String, String)>, |
| 289 | main_src: String, |
| 290 | expected: &str, |
| 291 | mod_opt: &str, |
| 292 | main_opt: &str, |
| 293 | label: &str, |
| 294 | ) { |
| 295 | let compiler = find_compiler(); |
| 296 | let dir = unique_dir(label); |
| 297 | |
| 298 | for (name, src) in &files { |
| 299 | fs::write(dir.join(name), src).unwrap(); |
| 300 | } |
| 301 | fs::write(dir.join("main.f90"), &main_src).unwrap(); |
| 302 | |
| 303 | // Compile modules at mod_opt. |
| 304 | let mut objects = Vec::new(); |
| 305 | for (name, _) in &files { |
| 306 | let f90 = dir.join(name); |
| 307 | let stem = name.trim_end_matches(".f90"); |
| 308 | let obj = dir.join(format!("{}.o", stem)); |
| 309 | compile_file(&compiler, &f90, &obj, &dir, mod_opt); |
| 310 | objects.push(obj); |
| 311 | } |
| 312 | |
| 313 | // Compile main at main_opt. |
| 314 | let main_o = dir.join("main.o"); |
| 315 | compile_file(&compiler, &dir.join("main.f90"), &main_o, &dir, main_opt); |
| 316 | objects.push(main_o); |
| 317 | |
| 318 | let binary = dir.join("test_bin"); |
| 319 | link_files(&objects, &binary); |
| 320 | let output = run_binary(&binary); |
| 321 | |
| 322 | assert!( |
| 323 | output.contains(expected), |
| 324 | "{}: mod@{} + main@{}: expected '{}' in output, got:\n{}", |
| 325 | label, |
| 326 | mod_opt, |
| 327 | main_opt, |
| 328 | expected, |
| 329 | output |
| 330 | ); |
| 331 | |
| 332 | let _ = fs::remove_dir_all(&dir); |
| 333 | } |
| 334 | |
| 335 | #[test] |
| 336 | fn abi_matrix_chain5_mod_o0_main_o2() { |
| 337 | let (files, main_src, expected) = gen_chain(5); |
| 338 | run_cross_opt_test(files, main_src, expected, "-O0", "-O2", "abi_chain5_0_2"); |
| 339 | } |
| 340 | |
| 341 | #[test] |
| 342 | fn abi_matrix_chain5_mod_o2_main_o0() { |
| 343 | let (files, main_src, expected) = gen_chain(5); |
| 344 | run_cross_opt_test(files, main_src, expected, "-O2", "-O0", "abi_chain5_2_0"); |
| 345 | } |
| 346 | |
| 347 | #[test] |
| 348 | fn abi_matrix_diamond4_mod_o0_main_o2() { |
| 349 | let (files, main_src, expected) = gen_diamond(4); |
| 350 | run_cross_opt_test(files, main_src, expected, "-O0", "-O2", "abi_dia4_0_2"); |
| 351 | } |
| 352 | |
| 353 | #[test] |
| 354 | fn abi_matrix_diamond4_mod_o2_main_o0() { |
| 355 | let (files, main_src, expected) = gen_diamond(4); |
| 356 | run_cross_opt_test(files, main_src, expected, "-O2", "-O0", "abi_dia4_2_0"); |
| 357 | } |
| 358 | |
| 359 | #[test] |
| 360 | fn abi_matrix_chain5_mod_o0_main_ofast() { |
| 361 | let (files, main_src, expected) = gen_chain(5); |
| 362 | run_cross_opt_test( |
| 363 | files, |
| 364 | main_src, |
| 365 | expected, |
| 366 | "-O0", |
| 367 | "-Ofast", |
| 368 | "abi_chain5_0_fast", |
| 369 | ); |
| 370 | } |
| 371 | |
| 372 | #[test] |
| 373 | fn abi_matrix_chain5_mod_ofast_main_o0() { |
| 374 | let (files, main_src, expected) = gen_chain(5); |
| 375 | run_cross_opt_test( |
| 376 | files, |
| 377 | main_src, |
| 378 | expected, |
| 379 | "-Ofast", |
| 380 | "-O0", |
| 381 | "abi_chain5_fast_0", |
| 382 | ); |
| 383 | } |
| 384 |