Rust · 9672 bytes Raw Blame History
1 //! Cross-TU (multi-file) compilation tests.
2 //!
3 //! Each test compiles a module .f90 and a consumer .f90 separately
4 //! with `-c`, links the .o files with the runtime, runs the binary,
5 //! and checks the output.
6
7 use std::path::{Path, PathBuf};
8 use std::process::Command;
9 use std::sync::atomic::{AtomicU64, Ordering};
10
11 static NEXT_ID: AtomicU64 = AtomicU64::new(0);
12
13 fn unique_dir() -> PathBuf {
14 let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
15 let dir = std::env::temp_dir().join(format!("afs_multifile_{}_{}", std::process::id(), id));
16 std::fs::create_dir_all(&dir).unwrap();
17 dir
18 }
19
20 fn find_compiler() -> PathBuf {
21 for c in &["target/release/armfortas", "target/debug/armfortas"] {
22 let p = PathBuf::from(c);
23 if p.exists() {
24 return std::fs::canonicalize(&p).unwrap();
25 }
26 }
27 panic!("armfortas binary not found");
28 }
29
30 fn find_runtime() -> PathBuf {
31 for dir in &["target/release", "target/debug"] {
32 let p = PathBuf::from(dir).join("libarmfortas_rt.a");
33 if p.exists() {
34 return p;
35 }
36 }
37 panic!("libarmfortas_rt.a not found");
38 }
39
40 fn sdk_path() -> String {
41 let out = Command::new("xcrun")
42 .args(["--sdk", "macosx", "--show-sdk-path"])
43 .output()
44 .expect("xcrun failed");
45 String::from_utf8(out.stdout).unwrap().trim().to_string()
46 }
47
48 /// Compile a .f90 file with -c, producing .o and optionally .amod.
49 fn compile_file(compiler: &Path, source: &Path, output: &Path, search_dir: Option<&Path>) {
50 let mut cmd = Command::new(compiler);
51 if let Some(parent) = source.parent() {
52 cmd.current_dir(parent);
53 }
54 cmd.args([
55 source.to_str().unwrap(),
56 "-c",
57 "-o",
58 output.to_str().unwrap(),
59 ]);
60 if let Some(dir) = search_dir {
61 cmd.arg(format!("-I{}", dir.display()));
62 }
63 let result = cmd.output().expect("compiler launch failed");
64 assert!(
65 result.status.success(),
66 "compile {} failed:\n{}",
67 source.display(),
68 String::from_utf8_lossy(&result.stderr)
69 );
70 }
71
72 /// Link .o files into a binary.
73 fn link_files(objects: &[&Path], output: &Path) {
74 let runtime = find_runtime();
75 let sdk = sdk_path();
76 let mut args: Vec<String> = vec!["-o".into(), output.to_str().unwrap().into()];
77 for o in objects {
78 args.push(o.to_str().unwrap().into());
79 }
80 args.push(runtime.to_str().unwrap().into());
81 args.extend([
82 "-lSystem".into(),
83 "-syslibroot".into(),
84 sdk,
85 "-arch".into(),
86 "arm64".into(),
87 ]);
88 let result = Command::new("ld")
89 .args(&args)
90 .output()
91 .expect("ld launch failed");
92 assert!(
93 result.status.success(),
94 "link failed:\n{}",
95 String::from_utf8_lossy(&result.stderr)
96 );
97 }
98
99 /// Run a binary and return its stdout.
100 fn run_binary(binary: &Path) -> String {
101 let result = Command::new(binary).output().expect("binary launch failed");
102 assert!(
103 result.status.success(),
104 "{} exited with {:?}\nstderr: {}",
105 binary.display(),
106 result.status.code(),
107 String::from_utf8_lossy(&result.stderr)
108 );
109 String::from_utf8_lossy(&result.stdout).into_owned()
110 }
111
112 /// Full multi-file test: write sources, compile, link, run, check.
113 fn multifile_test(mod_source: &str, main_source: &str, expected_substring: &str) {
114 let compiler = find_compiler();
115 let dir = unique_dir();
116 let mod_f90 = dir.join("mod.f90");
117 let main_f90 = dir.join("main.f90");
118 let mod_o = dir.join("mod.o");
119 let main_o = dir.join("main.o");
120 let binary = dir.join("test_bin");
121
122 std::fs::write(&mod_f90, mod_source).unwrap();
123 std::fs::write(&main_f90, main_source).unwrap();
124
125 compile_file(&compiler, &mod_f90, &mod_o, None);
126 compile_file(&compiler, &main_f90, &main_o, Some(&dir));
127 link_files(&[&mod_o, &main_o], &binary);
128 let output = run_binary(&binary);
129
130 assert!(
131 output.contains(expected_substring),
132 "expected '{}' in output, got:\n{}",
133 expected_substring,
134 output
135 );
136
137 let _ = std::fs::remove_dir_all(&dir);
138 }
139
140 // ---- Tests ----
141
142 #[test]
143 fn basic_module_variable_and_subroutine() {
144 multifile_test(
145 "module m\n implicit none\n integer :: counter = 0\ncontains\n subroutine bump()\n counter = counter + 1\n end subroutine\n integer function get() result(r)\n r = counter\n end function\nend module\n",
146 "program p\n use m\n call bump(); call bump(); call bump()\n print *, get()\nend program\n",
147 "3",
148 );
149 }
150
151 #[test]
152 fn module_with_allocatable_array() {
153 multifile_test(
154 "module arr_mod\n implicit none\n integer, allocatable :: buf(:)\ncontains\n subroutine init()\n allocate(buf(3))\n buf(1) = 10; buf(2) = 20; buf(3) = 30\n end subroutine\nend module\n",
155 "program p\n use arr_mod\n call init()\n print *, buf(1), buf(2), buf(3)\nend program\n",
156 "10 20 30",
157 );
158 }
159
160 #[test]
161 fn module_with_derived_type() {
162 multifile_test(
163 "module dt_mod\n implicit none\n type :: point\n real :: x, y\n end type\ncontains\n subroutine set_pt(p, a, b)\n type(point), intent(out) :: p\n real, intent(in) :: a, b\n p%x = a; p%y = b\n end subroutine\nend module\n",
164 "program p\n use dt_mod\n type(point) :: pt\n call set_pt(pt, 1.5, 2.5)\n print *, pt%x, pt%y\nend program\n",
165 "1.5",
166 );
167 }
168
169 #[test]
170 fn module_parameter_constants() {
171 multifile_test(
172 "module consts\n implicit none\n integer, parameter :: MAX_N = 1024\n integer, parameter :: HALF = MAX_N / 2\nend module\n",
173 "program p\n use consts\n print *, MAX_N, HALF\nend program\n",
174 "1024",
175 );
176 }
177
178 #[test]
179 fn use_only_filtering() {
180 multifile_test(
181 "module big_mod\n implicit none\n integer :: alpha = 10\n integer :: beta = 20\n integer :: gamma = 30\nend module\n",
182 "program p\n use big_mod, only: beta\n print *, beta\nend program\n",
183 "20",
184 );
185 }
186
187 #[test]
188 fn use_rename() {
189 multifile_test(
190 "module rename_mod\n implicit none\n integer :: original = 99\nend module\n",
191 "program p\n use rename_mod, renamed => original\n print *, renamed\nend program\n",
192 "99",
193 );
194 }
195
196 /// Generic interface resolved across .amod boundaries: the consumer
197 /// reconstructs the NamedInterface from the @interface block and
198 /// dispatches each specific at the call site.
199 #[test]
200 fn generic_interface_cross_module() {
201 multifile_test(
202 "module mgen\n implicit none\n interface add\n module procedure add_int, add_real\n end interface\ncontains\n integer function add_int(a, b)\n integer, intent(in) :: a, b\n add_int = a + b\n end function\n real function add_real(a, b)\n real, intent(in) :: a, b\n add_real = a + b\n end function\nend module\n",
203 "program p\n use mgen\n print *, add(1, 2)\n print *, add(1.5, 2.5)\nend program\n",
204 "3",
205 );
206 }
207
208 /// Generic interface reachable transitively through an intermediate
209 /// module that re-exports via `USE`. The middle module's .amod has
210 /// only `@uses base`; the consumer must recursively load base and
211 /// re-expose its symbols (including the NamedInterface) so generic
212 /// dispatch walks the chain.
213 #[test]
214 fn generic_interface_transitive_use() {
215 let compiler = find_compiler();
216 let dir = unique_dir();
217 let base_f90 = dir.join("base.f90");
218 let middle_f90 = dir.join("middle.f90");
219 let main_f90 = dir.join("main.f90");
220 let base_o = dir.join("base.o");
221 let middle_o = dir.join("middle.o");
222 let main_o = dir.join("main.o");
223 let binary = dir.join("test_bin");
224
225 std::fs::write(&base_f90, "module base\n implicit none\n interface add\n module procedure add_int, add_real\n end interface\ncontains\n integer function add_int(a, b)\n integer, intent(in) :: a, b\n add_int = a + b\n end function\n real function add_real(a, b)\n real, intent(in) :: a, b\n add_real = a + b\n end function\nend module\n").unwrap();
226 std::fs::write(&middle_f90, "module middle\n use base\nend module\n").unwrap();
227 std::fs::write(
228 &main_f90,
229 "program p\n use middle\n print *, add(1, 2)\n print *, add(1.5, 2.5)\nend program\n",
230 )
231 .unwrap();
232
233 compile_file(&compiler, &base_f90, &base_o, None);
234 compile_file(&compiler, &middle_f90, &middle_o, Some(&dir));
235 compile_file(&compiler, &main_f90, &main_o, Some(&dir));
236 link_files(&[&main_o, &middle_o, &base_o], &binary);
237 let output = run_binary(&binary);
238 assert!(
239 output.contains("3"),
240 "expected '3' in output, got:\n{}",
241 output
242 );
243 assert!(
244 output.contains("4.0000000E0"),
245 "expected real add result in output, got:\n{}",
246 output
247 );
248
249 let _ = std::fs::remove_dir_all(&dir);
250 }
251
252 #[test]
253 fn module_private_default() {
254 // priv_val should not be accessible; only pub_val.
255 let compiler = find_compiler();
256 let dir = unique_dir();
257 let mod_f90 = dir.join("mod.f90");
258 let mod_o = dir.join("mod.o");
259
260 std::fs::write(&mod_f90,
261 "module priv_mod\n implicit none\n private\n integer, public :: pub_val = 42\n integer :: priv_val = 99\nend module\n"
262 ).unwrap();
263 compile_file(&compiler, &mod_f90, &mod_o, None);
264
265 // Check .amod only has pub_val.
266 let amod = std::fs::read_to_string(dir.join("priv_mod.amod")).unwrap();
267 assert!(amod.contains("pub_val"), "pub_val should be in .amod");
268 assert!(
269 !amod.contains("priv_val"),
270 "priv_val should NOT be in .amod"
271 );
272
273 let _ = std::fs::remove_dir_all(&dir);
274 }
275