Rust · 10920 bytes Raw Blame History
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