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