Rust · 9193 bytes Raw Blame History
1 use std::path::PathBuf;
2 use std::process::{Command, Output, Stdio};
3 use std::sync::atomic::{AtomicUsize, Ordering};
4 use std::thread::sleep;
5 use std::time::{Duration, Instant};
6
7 static NEXT_TEMP_ID: AtomicUsize = AtomicUsize::new(0);
8
9 fn compiler(name: &str) -> PathBuf {
10 if let Some(path) = std::env::var_os(format!("CARGO_BIN_EXE_{}", name)) {
11 return PathBuf::from(path);
12 }
13 let candidate = PathBuf::from("target/debug").join(name);
14 if candidate.exists() {
15 return std::fs::canonicalize(candidate).expect("cannot canonicalize debug compiler path");
16 }
17 let candidate = PathBuf::from("target/release").join(name);
18 if candidate.exists() {
19 return std::fs::canonicalize(candidate)
20 .expect("cannot canonicalize release compiler path");
21 }
22 panic!(
23 "compiler binary '{}' not built — run `cargo build --bins` first",
24 name
25 );
26 }
27
28 fn unique_path(stem: &str, ext: &str) -> PathBuf {
29 let pid = std::process::id();
30 let id = NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed);
31 std::env::temp_dir().join(format!("afs_ctrl_{}_{}_{}.{}", stem, pid, id, ext))
32 }
33
34 fn write_program(text: &str, suffix: &str) -> PathBuf {
35 let path = unique_path("src", suffix);
36 std::fs::write(&path, text).expect("cannot write control-flow test source");
37 path
38 }
39
40 fn run_with_timeout(path: &std::path::Path) -> Output {
41 let mut child = Command::new(path)
42 .stdout(Stdio::piped())
43 .stderr(Stdio::piped())
44 .spawn()
45 .expect("failed to spawn control-flow test binary");
46 let deadline = Instant::now() + Duration::from_secs(3);
47 loop {
48 if let Some(_status) = child.try_wait().expect("failed to poll child status") {
49 return child
50 .wait_with_output()
51 .expect("failed to collect child output");
52 }
53 if Instant::now() >= deadline {
54 let _ = child.kill();
55 let _ = child.wait();
56 panic!("control-flow test binary hung");
57 }
58 sleep(Duration::from_millis(20));
59 }
60 }
61
62 #[test]
63 fn named_exit_and_cycle_target_nested_constructs() {
64 let src = write_program(
65 "program p\n implicit none\n integer :: i, j, sum\n sum = 0\nouter: do i = 1, 4\n inner: do j = 1, 4\n if (j == 2) cycle inner\n if (i == 3 .and. j == 4) exit outer\n sum = sum + i * 10 + j\n end do inner\nend do outer\nprint *, sum\nend program\n",
66 "f90",
67 );
68 let out = unique_path("named_exit_cycle", "bin");
69 let compile = Command::new(compiler("armfortas"))
70 .args([src.to_str().unwrap(), "-o", out.to_str().unwrap()])
71 .output()
72 .expect("named EXIT/CYCLE compile failed to spawn");
73 assert!(
74 compile.status.success(),
75 "named EXIT/CYCLE compile failed: {}",
76 String::from_utf8_lossy(&compile.stderr)
77 );
78
79 let run = Command::new(&out)
80 .output()
81 .expect("named EXIT/CYCLE run failed");
82 assert!(
83 run.status.success(),
84 "named EXIT/CYCLE run failed: status={:?}\nstdout:\n{}\nstderr:\n{}",
85 run.status,
86 String::from_utf8_lossy(&run.stdout),
87 String::from_utf8_lossy(&run.stderr)
88 );
89 let stdout = String::from_utf8_lossy(&run.stdout);
90 assert!(
91 stdout.contains("170"),
92 "unexpected named EXIT/CYCLE output: {}",
93 stdout
94 );
95
96 let _ = std::fs::remove_file(&out);
97 let _ = std::fs::remove_file(&src);
98 }
99
100 #[test]
101 fn character_select_case_matches_expected_arm() {
102 let src = write_program(
103 "program p\n implicit none\n integer :: code\n character(len=8) :: cmd\n cmd = 'help'\n code = dispatch(cmd)\n if (code /= 2) error stop 1\n cmd = 'exit'\n code = dispatch(cmd)\n if (code /= 1) error stop 2\n cmd = 'other'\n code = dispatch(cmd)\n if (code /= 3) error stop 3\n print *, 99\ncontains\n integer function dispatch(cmd) result(code)\n character(len=*), intent(in) :: cmd\n select case (trim(cmd))\n case ('quit', 'exit')\n code = 1\n case ('help')\n code = 2\n case default\n code = 3\n end select\n end function dispatch\nend program\n",
104 "f90",
105 );
106 let out = unique_path("char_select_case", "bin");
107 let compile = Command::new(compiler("armfortas"))
108 .args([src.to_str().unwrap(), "-o", out.to_str().unwrap()])
109 .output()
110 .expect("character SELECT CASE compile failed to spawn");
111 assert!(
112 compile.status.success(),
113 "character SELECT CASE compile failed: {}",
114 String::from_utf8_lossy(&compile.stderr)
115 );
116
117 let run = Command::new(&out)
118 .output()
119 .expect("character SELECT CASE run failed");
120 assert!(
121 run.status.success(),
122 "character SELECT CASE run failed: status={:?}\nstdout:\n{}\nstderr:\n{}",
123 run.status,
124 String::from_utf8_lossy(&run.stdout),
125 String::from_utf8_lossy(&run.stderr)
126 );
127 let stdout = String::from_utf8_lossy(&run.stdout);
128 assert!(
129 stdout.contains("99"),
130 "unexpected character SELECT CASE output: {}",
131 stdout
132 );
133
134 let _ = std::fs::remove_file(&out);
135 let _ = std::fs::remove_file(&src);
136 }
137
138 #[test]
139 fn logical_and_or_short_circuit_in_expression_values() {
140 let src = write_program(
141 "program p\n implicit none\n logical :: x, y\n x = .false. .and. boom()\n y = .true. .or. boom()\n if (x) error stop 1\n if (.not. y) error stop 2\n print *, 77\ncontains\n logical function boom()\n error stop 7\n end function boom\nend program\n",
142 "f90",
143 );
144 let out = unique_path("short_circuit_expr", "bin");
145 let compile = Command::new(compiler("armfortas"))
146 .args([src.to_str().unwrap(), "-o", out.to_str().unwrap()])
147 .output()
148 .expect("expression short-circuit compile failed to spawn");
149 assert!(
150 compile.status.success(),
151 "expression short-circuit compile failed: {}",
152 String::from_utf8_lossy(&compile.stderr)
153 );
154
155 let run = Command::new(&out)
156 .output()
157 .expect("expression short-circuit run failed");
158 assert!(
159 run.status.success(),
160 "expression short-circuit run failed: status={:?}\nstdout:\n{}\nstderr:\n{}",
161 run.status,
162 String::from_utf8_lossy(&run.stdout),
163 String::from_utf8_lossy(&run.stderr)
164 );
165 let stdout = String::from_utf8_lossy(&run.stdout);
166 assert!(
167 stdout.contains("77"),
168 "unexpected expression short-circuit output: {}",
169 stdout
170 );
171
172 let _ = std::fs::remove_file(&out);
173 let _ = std::fs::remove_file(&src);
174 }
175
176 #[test]
177 fn runtime_zero_step_do_fails_loudly_instead_of_hanging() {
178 let src = write_program(
179 "program p\n implicit none\n integer :: i, step\n step = 0\n do i = 1, 10, step\n print *, i\n end do\n print *, 88\nend program\n",
180 "f90",
181 );
182 let out = unique_path("runtime_zero_step", "bin");
183 let compile = Command::new(compiler("armfortas"))
184 .args([src.to_str().unwrap(), "-o", out.to_str().unwrap()])
185 .output()
186 .expect("runtime zero-step compile failed to spawn");
187 assert!(
188 compile.status.success(),
189 "runtime zero-step compile failed: {}",
190 String::from_utf8_lossy(&compile.stderr)
191 );
192
193 let run = run_with_timeout(&out);
194 assert!(
195 !run.status.success(),
196 "runtime zero-step DO should fail instead of succeeding"
197 );
198 let stderr = String::from_utf8_lossy(&run.stderr);
199 assert!(
200 stderr.contains("ERROR STOP"),
201 "expected runtime zero-step diagnostic, got stderr: {}",
202 stderr
203 );
204
205 let _ = std::fs::remove_file(&out);
206 let _ = std::fs::remove_file(&src);
207 }
208
209 #[test]
210 fn case_default_runs_only_after_other_cases_fail() {
211 let src = write_program(
212 "program p\n implicit none\n integer :: x\n x = 2\n select case (x)\n case default\n print *, 0\n case (2)\n print *, 2\n end select\nend program\n",
213 "f90",
214 );
215 let out = unique_path("select_default_fallback", "bin");
216 let compile = Command::new(compiler("armfortas"))
217 .args([src.to_str().unwrap(), "-o", out.to_str().unwrap()])
218 .output()
219 .expect("select default fallback compile failed to spawn");
220 assert!(
221 compile.status.success(),
222 "select default fallback compile failed: {}",
223 String::from_utf8_lossy(&compile.stderr)
224 );
225
226 let run = Command::new(&out)
227 .output()
228 .expect("select default fallback run failed");
229 assert!(
230 run.status.success(),
231 "select default fallback run failed: status={:?}\nstdout:\n{}\nstderr:\n{}",
232 run.status,
233 String::from_utf8_lossy(&run.stdout),
234 String::from_utf8_lossy(&run.stderr)
235 );
236 let stdout = String::from_utf8_lossy(&run.stdout);
237 assert!(
238 stdout.contains("2"),
239 "default arm should not swallow later matching case: {}",
240 stdout
241 );
242 assert!(
243 !stdout.contains("0"),
244 "default arm should only run as fallback: {}",
245 stdout
246 );
247
248 let _ = std::fs::remove_file(&out);
249 let _ = std::fs::remove_file(&src);
250 }
251