| 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 |