Rust · 11065 bytes Raw Blame History
1 //! Sprint 32 CLI driver tests.
2 //!
3 //! Each test exercises one user-visible behaviour of the `armfortas`
4 //! / `afs` driver via subprocess invocation. Subprocess use is
5 //! deliberate — we want to catch wrong-exit-code, wrong-stdout-vs-
6 //! stderr-routing, and missing-symbol-from-bin issues that an
7 //! in-process API call wouldn't see.
8
9 use std::path::PathBuf;
10 use std::process::Command;
11
12 fn compiler(name: &str) -> PathBuf {
13 let candidate = PathBuf::from("target/release").join(name);
14 if candidate.exists() {
15 return candidate;
16 }
17 let candidate = PathBuf::from("target/debug").join(name);
18 assert!(
19 candidate.exists(),
20 "compiler binary '{}' not built — run `cargo build --bins` first",
21 name
22 );
23 candidate
24 }
25
26 fn unique_path(stem: &str, ext: &str) -> PathBuf {
27 let pid = std::process::id();
28 let nanos = std::time::SystemTime::now()
29 .duration_since(std::time::UNIX_EPOCH)
30 .unwrap()
31 .as_nanos();
32 std::env::temp_dir().join(format!("afs_cli_{}_{}_{}.{}", stem, pid, nanos, ext))
33 }
34
35 fn write_program(text: &str, suffix: &str) -> PathBuf {
36 let path = unique_path("src", suffix);
37 std::fs::write(&path, text).expect("cannot write CLI test source");
38 path
39 }
40
41 #[test]
42 fn version_flag_prints_version_string_to_stdout() {
43 let out = Command::new(compiler("armfortas"))
44 .arg("--version")
45 .output()
46 .expect("failed to spawn armfortas");
47 assert!(out.status.success(), "exit code: {:?}", out.status);
48 let stdout = String::from_utf8_lossy(&out.stdout);
49 assert!(
50 stdout.contains("armfortas") && stdout.contains("0.1.0"),
51 "unexpected --version output: {}",
52 stdout
53 );
54 // The version string belongs on stdout (not stderr) per
55 // gfortran/clang convention; users shell-pipe it.
56 assert!(out.stderr.is_empty(), "stderr should be empty: {:?}", String::from_utf8_lossy(&out.stderr));
57 }
58
59 #[test]
60 fn help_flag_shows_usage_and_exits_zero() {
61 let out = Command::new(compiler("armfortas"))
62 .arg("--help")
63 .output()
64 .expect("failed to spawn armfortas");
65 assert!(out.status.success(), "--help should succeed");
66 let stdout = String::from_utf8_lossy(&out.stdout);
67 assert!(stdout.contains("USAGE"), "help missing USAGE line");
68 assert!(stdout.contains("--std="), "help missing --std= entry");
69 }
70
71 #[test]
72 fn dumpversion_prints_just_the_version_number() {
73 let out = Command::new(compiler("armfortas"))
74 .arg("-dumpversion")
75 .output()
76 .expect("failed to spawn armfortas");
77 assert!(out.status.success());
78 let stdout = String::from_utf8_lossy(&out.stdout);
79 assert_eq!(stdout.trim(), "0.1.0");
80 }
81
82 #[test]
83 fn afs_alias_runs_the_same_compiler() {
84 let out = Command::new(compiler("afs"))
85 .arg("--version")
86 .output()
87 .expect("failed to spawn afs alias");
88 assert!(out.status.success());
89 let stdout = String::from_utf8_lossy(&out.stdout);
90 // Both binaries are built from the same source so the version
91 // string is identical — that's the contract.
92 assert!(stdout.contains("armfortas"));
93 }
94
95 #[test]
96 fn no_args_prints_help_to_stderr_and_exits_nonzero() {
97 let out = Command::new(compiler("armfortas"))
98 .output()
99 .expect("failed to spawn armfortas");
100 assert!(!out.status.success(), "no-arg invocation should fail");
101 let stderr = String::from_utf8_lossy(&out.stderr);
102 assert!(
103 stderr.contains("USAGE"),
104 "no-arg invocation should print help to stderr: {}",
105 stderr
106 );
107 }
108
109 #[test]
110 fn dash_c_produces_object_file_only() {
111 let src = write_program(
112 "module foo\n integer :: x = 1\nend module\n",
113 "f90",
114 );
115 let out = unique_path("obj", "o");
116 let result = Command::new(compiler("armfortas"))
117 .args([
118 "-c",
119 src.to_str().unwrap(),
120 "-o",
121 out.to_str().unwrap(),
122 ])
123 .output()
124 .expect("compile failed to spawn");
125 assert!(
126 result.status.success(),
127 "-c compile failed: {}",
128 String::from_utf8_lossy(&result.stderr)
129 );
130 assert!(out.exists(), "-c should produce an object file");
131 let _ = std::fs::remove_file(&out);
132 let _ = std::fs::remove_file(&src);
133 }
134
135 #[test]
136 fn dash_capital_s_produces_assembly_text() {
137 let src = write_program(
138 "program p\n print *, 1\nend program\n",
139 "f90",
140 );
141 let out = unique_path("asm", "s");
142 let result = Command::new(compiler("armfortas"))
143 .args([
144 "-S",
145 src.to_str().unwrap(),
146 "-o",
147 out.to_str().unwrap(),
148 ])
149 .output()
150 .expect("spawn failed");
151 assert!(
152 result.status.success(),
153 "-S compile failed: {}",
154 String::from_utf8_lossy(&result.stderr)
155 );
156 let asm = std::fs::read_to_string(&out).expect("missing asm output");
157 assert!(asm.contains("__TEXT"), ".s output should contain section directive");
158 let _ = std::fs::remove_file(&out);
159 let _ = std::fs::remove_file(&src);
160 }
161
162 #[test]
163 fn dash_capital_e_preprocesses_only() {
164 let src = write_program(
165 "#define X 99\nprogram p\n print *, X\nend program\n",
166 "F90",
167 );
168 let out = unique_path("pp", "f90");
169 let result = Command::new(compiler("armfortas"))
170 .args([
171 "-E",
172 src.to_str().unwrap(),
173 "-o",
174 out.to_str().unwrap(),
175 ])
176 .output()
177 .expect("spawn failed");
178 assert!(
179 result.status.success(),
180 "-E preprocess failed: {}",
181 String::from_utf8_lossy(&result.stderr)
182 );
183 let pp = std::fs::read_to_string(&out).expect("missing preprocessed output");
184 assert!(
185 pp.contains(", 99"),
186 "preprocessed text should expand the macro: {}",
187 pp
188 );
189 let _ = std::fs::remove_file(&out);
190 let _ = std::fs::remove_file(&src);
191 }
192
193 #[test]
194 fn std_f95_rejects_f2008_error_stop() {
195 let src = write_program(
196 "program p\n error stop 'oops'\nend program\n",
197 "f90",
198 );
199 let out = unique_path("f95", "bin");
200 let result = Command::new(compiler("armfortas"))
201 .args([
202 "--std=f95",
203 src.to_str().unwrap(),
204 "-o",
205 out.to_str().unwrap(),
206 ])
207 .output()
208 .expect("spawn failed");
209 assert!(!result.status.success(), "--std=f95 should reject ERROR STOP");
210 let stderr = String::from_utf8_lossy(&result.stderr);
211 assert!(
212 stderr.contains("ERROR STOP") && stderr.contains("F2008"),
213 "expected ERROR STOP / F2008 error: {}",
214 stderr
215 );
216 let _ = std::fs::remove_file(&src);
217 }
218
219 #[test]
220 fn response_file_supplies_arguments() {
221 let src = write_program(
222 "program p\n print *, 7\nend program\n",
223 "f90",
224 );
225 let out = unique_path("resp", "bin");
226 let resp = unique_path("flags", "txt");
227 std::fs::write(
228 &resp,
229 format!(
230 "-O1\n-o\n{}\n{}\n",
231 out.display(),
232 src.display()
233 ),
234 )
235 .unwrap();
236 let result = Command::new(compiler("armfortas"))
237 .arg(format!("@{}", resp.display()))
238 .output()
239 .expect("spawn failed");
240 assert!(
241 result.status.success(),
242 "@response-file compile failed: {}",
243 String::from_utf8_lossy(&result.stderr)
244 );
245 assert!(out.exists(), "binary should exist after @file compile");
246 let _ = std::fs::remove_file(&out);
247 let _ = std::fs::remove_file(&src);
248 let _ = std::fs::remove_file(&resp);
249 }
250
251 #[test]
252 fn dash_j_writes_amod_to_chosen_directory() {
253 let src = write_program(
254 "module dashj_mod\n integer :: y = 5\nend module\n",
255 "f90",
256 );
257 let out = unique_path("dashjobj", "o");
258 let amod_dir = std::env::temp_dir().join(format!(
259 "afs_cli_amod_{}_{}",
260 std::process::id(),
261 std::time::SystemTime::now()
262 .duration_since(std::time::UNIX_EPOCH)
263 .unwrap()
264 .as_nanos(),
265 ));
266 std::fs::create_dir_all(&amod_dir).unwrap();
267 let result = Command::new(compiler("armfortas"))
268 .args([
269 "-c",
270 "-J",
271 amod_dir.to_str().unwrap(),
272 src.to_str().unwrap(),
273 "-o",
274 out.to_str().unwrap(),
275 ])
276 .output()
277 .expect("spawn failed");
278 assert!(
279 result.status.success(),
280 "-J compile failed: {}",
281 String::from_utf8_lossy(&result.stderr)
282 );
283 let amod = amod_dir.join("dashj_mod.amod");
284 assert!(amod.exists(), "-J should place .amod in the requested dir");
285 let _ = std::fs::remove_file(&out);
286 let _ = std::fs::remove_file(&src);
287 let _ = std::fs::remove_dir_all(&amod_dir);
288 }
289
290 #[test]
291 fn verbose_flag_streams_phase_lines_to_stderr() {
292 let src = write_program(
293 "program p\n print *, 1\nend program\n",
294 "f90",
295 );
296 let out = unique_path("verbose", "bin");
297 let result = Command::new(compiler("armfortas"))
298 .args([
299 "-v",
300 src.to_str().unwrap(),
301 "-o",
302 out.to_str().unwrap(),
303 ])
304 .output()
305 .expect("spawn failed");
306 assert!(result.status.success());
307 let stderr = String::from_utf8_lossy(&result.stderr);
308 assert!(stderr.contains("preprocessing:"), "verbose missing preprocessing line: {}", stderr);
309 assert!(stderr.contains("codegen:"), "verbose missing codegen line: {}", stderr);
310 let _ = std::fs::remove_file(&out);
311 let _ = std::fs::remove_file(&src);
312 }
313
314 #[test]
315 fn time_report_prints_phase_table() {
316 let src = write_program(
317 "program p\n print *, 1\nend program\n",
318 "f90",
319 );
320 let out = unique_path("timer", "bin");
321 let result = Command::new(compiler("armfortas"))
322 .args([
323 "--time-report",
324 src.to_str().unwrap(),
325 "-o",
326 out.to_str().unwrap(),
327 ])
328 .output()
329 .expect("spawn failed");
330 assert!(result.status.success());
331 let stderr = String::from_utf8_lossy(&result.stderr);
332 assert!(stderr.contains("Phase"), "missing time-report header: {}", stderr);
333 assert!(stderr.contains("Total"), "missing time-report total: {}", stderr);
334 let _ = std::fs::remove_file(&out);
335 let _ = std::fs::remove_file(&src);
336 }
337
338 #[test]
339 fn missing_input_file_reports_io_error() {
340 let result = Command::new(compiler("armfortas"))
341 .args(["/nonexistent/path/source.f90"])
342 .output()
343 .expect("spawn failed");
344 assert!(!result.status.success(), "missing input should fail");
345 // Per sprint 32 #6 exit-code spec: I/O errors (cannot read input)
346 // map to exit code 3. The driver categorises by error message
347 // text today; a structured error type is sprint 32 #507.
348 assert_eq!(
349 result.status.code(),
350 Some(3),
351 "missing input should map to exit code 3 (I/O error), got: {:?}",
352 result.status
353 );
354 }
355