| 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 | if let Some(path) = std::env::var_os(format!("CARGO_BIN_EXE_{}", name)) { |
| 14 | return PathBuf::from(path); |
| 15 | } |
| 16 | let candidate = PathBuf::from("target/debug").join(name); |
| 17 | if candidate.exists() { |
| 18 | return std::fs::canonicalize(candidate).expect("cannot canonicalize debug compiler path"); |
| 19 | } |
| 20 | let candidate = PathBuf::from("target/release").join(name); |
| 21 | if candidate.exists() { |
| 22 | return std::fs::canonicalize(candidate) |
| 23 | .expect("cannot canonicalize release compiler path"); |
| 24 | } |
| 25 | panic!( |
| 26 | "compiler binary '{}' not built — run `cargo build --bins` first", |
| 27 | name |
| 28 | ); |
| 29 | } |
| 30 | |
| 31 | fn unique_path(stem: &str, ext: &str) -> PathBuf { |
| 32 | let pid = std::process::id(); |
| 33 | let nanos = std::time::SystemTime::now() |
| 34 | .duration_since(std::time::UNIX_EPOCH) |
| 35 | .unwrap() |
| 36 | .as_nanos(); |
| 37 | std::env::temp_dir().join(format!("afs_cli_{}_{}_{}.{}", stem, pid, nanos, ext)) |
| 38 | } |
| 39 | |
| 40 | fn unique_dir(stem: &str) -> PathBuf { |
| 41 | let dir = unique_path(stem, "dir"); |
| 42 | std::fs::create_dir_all(&dir).expect("cannot create CLI test directory"); |
| 43 | dir |
| 44 | } |
| 45 | |
| 46 | fn write_program(text: &str, suffix: &str) -> PathBuf { |
| 47 | let path = unique_path("src", suffix); |
| 48 | std::fs::write(&path, text).expect("cannot write CLI test source"); |
| 49 | path |
| 50 | } |
| 51 | |
| 52 | fn write_program_in(dir: &std::path::Path, name: &str, text: &str) -> PathBuf { |
| 53 | let path = dir.join(name); |
| 54 | std::fs::write(&path, text).expect("cannot write CLI test source"); |
| 55 | path |
| 56 | } |
| 57 | |
| 58 | fn undefined_symbols(path: &std::path::Path) -> Vec<String> { |
| 59 | let out = Command::new("nm") |
| 60 | .args(["-u", "-j", path.to_str().unwrap()]) |
| 61 | .output() |
| 62 | .expect("failed to spawn nm"); |
| 63 | assert!( |
| 64 | out.status.success(), |
| 65 | "nm failed for {}: {}", |
| 66 | path.display(), |
| 67 | String::from_utf8_lossy(&out.stderr) |
| 68 | ); |
| 69 | String::from_utf8_lossy(&out.stdout) |
| 70 | .lines() |
| 71 | .map(str::trim) |
| 72 | .filter(|line| !line.is_empty()) |
| 73 | .map(ToOwned::to_owned) |
| 74 | .collect() |
| 75 | } |
| 76 | |
| 77 | #[test] |
| 78 | fn version_flag_prints_version_string_to_stdout() { |
| 79 | let out = Command::new(compiler("armfortas")) |
| 80 | .arg("--version") |
| 81 | .output() |
| 82 | .expect("failed to spawn armfortas"); |
| 83 | assert!(out.status.success(), "exit code: {:?}", out.status); |
| 84 | let stdout = String::from_utf8_lossy(&out.stdout); |
| 85 | assert!( |
| 86 | stdout.contains("armfortas") && stdout.contains("0.1.0"), |
| 87 | "unexpected --version output: {}", |
| 88 | stdout |
| 89 | ); |
| 90 | // The version string belongs on stdout (not stderr) per |
| 91 | // gfortran/clang convention; users shell-pipe it. |
| 92 | assert!( |
| 93 | out.stderr.is_empty(), |
| 94 | "stderr should be empty: {:?}", |
| 95 | String::from_utf8_lossy(&out.stderr) |
| 96 | ); |
| 97 | } |
| 98 | |
| 99 | #[test] |
| 100 | fn help_flag_shows_usage_and_exits_zero() { |
| 101 | let out = Command::new(compiler("armfortas")) |
| 102 | .arg("--help") |
| 103 | .output() |
| 104 | .expect("failed to spawn armfortas"); |
| 105 | assert!(out.status.success(), "--help should succeed"); |
| 106 | let stdout = String::from_utf8_lossy(&out.stdout); |
| 107 | assert!(stdout.contains("USAGE"), "help missing USAGE line"); |
| 108 | assert!(stdout.contains("--std="), "help missing --std= entry"); |
| 109 | } |
| 110 | |
| 111 | #[test] |
| 112 | fn dumpversion_prints_just_the_version_number() { |
| 113 | let out = Command::new(compiler("armfortas")) |
| 114 | .arg("-dumpversion") |
| 115 | .output() |
| 116 | .expect("failed to spawn armfortas"); |
| 117 | assert!(out.status.success()); |
| 118 | let stdout = String::from_utf8_lossy(&out.stdout); |
| 119 | assert_eq!(stdout.trim(), "0.1.0"); |
| 120 | } |
| 121 | |
| 122 | #[test] |
| 123 | fn afs_alias_runs_the_same_compiler() { |
| 124 | let out = Command::new(compiler("afs")) |
| 125 | .arg("--version") |
| 126 | .output() |
| 127 | .expect("failed to spawn afs alias"); |
| 128 | assert!(out.status.success()); |
| 129 | let stdout = String::from_utf8_lossy(&out.stdout); |
| 130 | assert!( |
| 131 | stdout.starts_with("afs "), |
| 132 | "afs --version should identify itself as afs: {}", |
| 133 | stdout |
| 134 | ); |
| 135 | } |
| 136 | |
| 137 | #[test] |
| 138 | fn no_args_prints_help_to_stdout_and_exits_zero() { |
| 139 | let out = Command::new(compiler("armfortas")) |
| 140 | .output() |
| 141 | .expect("failed to spawn armfortas"); |
| 142 | assert!( |
| 143 | out.status.success(), |
| 144 | "no-arg invocation should show usage help" |
| 145 | ); |
| 146 | let stdout = String::from_utf8_lossy(&out.stdout); |
| 147 | assert!( |
| 148 | stdout.contains("USAGE"), |
| 149 | "no-arg invocation should print help to stdout: {}", |
| 150 | stdout |
| 151 | ); |
| 152 | assert!( |
| 153 | out.stderr.is_empty(), |
| 154 | "no-arg invocation should not print usage to stderr: {}", |
| 155 | String::from_utf8_lossy(&out.stderr) |
| 156 | ); |
| 157 | } |
| 158 | |
| 159 | #[test] |
| 160 | fn no_input_after_flags_prints_help_and_mentions_missing_input() { |
| 161 | let out = Command::new(compiler("armfortas")) |
| 162 | .arg("-Wall") |
| 163 | .output() |
| 164 | .expect("failed to spawn armfortas"); |
| 165 | assert!( |
| 166 | out.status.success(), |
| 167 | "flag-only no-input invocation should exit zero" |
| 168 | ); |
| 169 | let stdout = String::from_utf8_lossy(&out.stdout); |
| 170 | let stderr = String::from_utf8_lossy(&out.stderr); |
| 171 | assert!(stdout.contains("USAGE"), "missing help text: {}", stdout); |
| 172 | assert!( |
| 173 | stderr.contains("no input file"), |
| 174 | "expected missing-input note on stderr: {}", |
| 175 | stderr |
| 176 | ); |
| 177 | } |
| 178 | |
| 179 | #[test] |
| 180 | fn dash_c_produces_object_file_only() { |
| 181 | let src = write_program("module foo\n integer :: x = 1\nend module\n", "f90"); |
| 182 | let out = unique_path("obj", "o"); |
| 183 | let result = Command::new(compiler("armfortas")) |
| 184 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 185 | .output() |
| 186 | .expect("compile failed to spawn"); |
| 187 | assert!( |
| 188 | result.status.success(), |
| 189 | "-c compile failed: {}", |
| 190 | String::from_utf8_lossy(&result.stderr) |
| 191 | ); |
| 192 | assert!(out.exists(), "-c should produce an object file"); |
| 193 | let _ = std::fs::remove_file(&out); |
| 194 | let _ = std::fs::remove_file(&src); |
| 195 | } |
| 196 | |
| 197 | #[test] |
| 198 | fn fixed_form_program_compiles_and_runs() { |
| 199 | let src = write_program( |
| 200 | " PROGRAM P\n INTEGER I, S\n S = 0\n DO 10 I = 1, 3\n S = S + I\n 10 CONTINUE\n PRINT *, S\n END\n", |
| 201 | "f", |
| 202 | ); |
| 203 | let out = unique_path("fixed_form", "bin"); |
| 204 | let compile = Command::new(compiler("armfortas")) |
| 205 | .args([src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 206 | .output() |
| 207 | .expect("fixed-form compile failed to spawn"); |
| 208 | assert!( |
| 209 | compile.status.success(), |
| 210 | "fixed-form compile failed: {}", |
| 211 | String::from_utf8_lossy(&compile.stderr) |
| 212 | ); |
| 213 | |
| 214 | let run = Command::new(&out).output().expect("fixed-form run failed"); |
| 215 | assert!( |
| 216 | run.status.success(), |
| 217 | "fixed-form run failed: {:?}", |
| 218 | run.status |
| 219 | ); |
| 220 | let stdout = String::from_utf8_lossy(&run.stdout); |
| 221 | assert!( |
| 222 | stdout.trim().ends_with('6'), |
| 223 | "unexpected fixed-form output: {}", |
| 224 | stdout |
| 225 | ); |
| 226 | |
| 227 | let _ = std::fs::remove_file(&out); |
| 228 | let _ = std::fs::remove_file(&src); |
| 229 | } |
| 230 | |
| 231 | #[test] |
| 232 | fn select_lowering_coerces_mixed_width_branch_values() { |
| 233 | let src = write_program( |
| 234 | "program p\n implicit none\n integer :: x\n integer(8) :: y\n y = 7_8\n if (y > 0_8) then\n x = 1\n else\n x = y\n end if\n print *, x\nend program\n", |
| 235 | "f90", |
| 236 | ); |
| 237 | let out = unique_path("select_mixed_width", "o"); |
| 238 | let compile = Command::new(compiler("armfortas")) |
| 239 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 240 | .output() |
| 241 | .expect("mixed-width select compile failed to spawn"); |
| 242 | assert!( |
| 243 | compile.status.success(), |
| 244 | "mixed-width select compile failed: {}", |
| 245 | String::from_utf8_lossy(&compile.stderr) |
| 246 | ); |
| 247 | assert!( |
| 248 | out.exists(), |
| 249 | "mixed-width select should produce an object file" |
| 250 | ); |
| 251 | |
| 252 | let _ = std::fs::remove_file(&out); |
| 253 | let _ = std::fs::remove_file(&src); |
| 254 | } |
| 255 | |
| 256 | #[test] |
| 257 | fn max_intrinsic_coerces_mixed_width_integer_args() { |
| 258 | let src = write_program( |
| 259 | "program p\n implicit none\n integer :: x\n integer(8) :: y\n y = 7_8\n x = max(1, y)\n print *, x\nend program\n", |
| 260 | "f90", |
| 261 | ); |
| 262 | let out = unique_path("max_mixed_width", "o"); |
| 263 | let compile = Command::new(compiler("armfortas")) |
| 264 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 265 | .output() |
| 266 | .expect("mixed-width max compile failed to spawn"); |
| 267 | assert!( |
| 268 | compile.status.success(), |
| 269 | "mixed-width max compile failed: {}", |
| 270 | String::from_utf8_lossy(&compile.stderr) |
| 271 | ); |
| 272 | assert!( |
| 273 | out.exists(), |
| 274 | "mixed-width max should produce an object file" |
| 275 | ); |
| 276 | |
| 277 | let _ = std::fs::remove_file(&out); |
| 278 | let _ = std::fs::remove_file(&src); |
| 279 | } |
| 280 | |
| 281 | #[test] |
| 282 | fn counted_do_coerces_mixed_width_bounds() { |
| 283 | let src = write_program( |
| 284 | "program p\n implicit none\n character(len=5) :: s\n integer :: i, total\n s = 'abc '\n total = 0\n do i = len_trim(s), 1, -1\n total = total + i\n end do\n print *, total\nend program\n", |
| 285 | "f90", |
| 286 | ); |
| 287 | let out = unique_path("do_mixed_width", "o"); |
| 288 | let compile = Command::new(compiler("armfortas")) |
| 289 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 290 | .output() |
| 291 | .expect("mixed-width DO compile failed to spawn"); |
| 292 | assert!( |
| 293 | compile.status.success(), |
| 294 | "mixed-width DO compile failed: {}", |
| 295 | String::from_utf8_lossy(&compile.stderr) |
| 296 | ); |
| 297 | assert!(out.exists(), "mixed-width DO should produce an object file"); |
| 298 | |
| 299 | let _ = std::fs::remove_file(&out); |
| 300 | let _ = std::fs::remove_file(&src); |
| 301 | } |
| 302 | |
| 303 | #[test] |
| 304 | fn runtime_sized_local_character_uses_runtime_string_support() { |
| 305 | let src = write_program( |
| 306 | "subroutine f(input, trimmed)\n implicit none\n character(len=*), intent(in) :: input\n integer, intent(out) :: trimmed\n character(len=len(input)) :: working_input\n working_input = input\n trimmed = len_trim(working_input)\nend subroutine\n", |
| 307 | "f90", |
| 308 | ); |
| 309 | let out = unique_path("runtime_char_local", "o"); |
| 310 | let compile = Command::new(compiler("armfortas")) |
| 311 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 312 | .output() |
| 313 | .expect("runtime-sized local character compile failed to spawn"); |
| 314 | assert!( |
| 315 | compile.status.success(), |
| 316 | "runtime-sized local character compile failed: {}", |
| 317 | String::from_utf8_lossy(&compile.stderr) |
| 318 | ); |
| 319 | |
| 320 | let undefined = undefined_symbols(&out); |
| 321 | assert!( |
| 322 | undefined.iter().any(|sym| sym == "_afs_len_trim"), |
| 323 | "runtime-sized local character should call afs_len_trim, undefineds were: {:?}", |
| 324 | undefined |
| 325 | ); |
| 326 | assert!( |
| 327 | !undefined.iter().any(|sym| sym == "_working_input"), |
| 328 | "runtime-sized local character should not lower to an external working_input call: {:?}", |
| 329 | undefined |
| 330 | ); |
| 331 | assert!( |
| 332 | !undefined.iter().any(|sym| sym == "_len_trim"), |
| 333 | "runtime-sized local character should not lower to a raw len_trim symbol: {:?}", |
| 334 | undefined |
| 335 | ); |
| 336 | |
| 337 | let _ = std::fs::remove_file(&out); |
| 338 | let _ = std::fs::remove_file(&src); |
| 339 | } |
| 340 | |
| 341 | #[test] |
| 342 | fn assumed_length_character_dummy_keeps_hidden_length_abi() { |
| 343 | let src = write_program( |
| 344 | "subroutine f(prompt_str, first)\n implicit none\n character(len=*), intent(in) :: prompt_str\n character(len=1), intent(out) :: first\n first = prompt_str(1:1)\nend subroutine\n", |
| 345 | "f90", |
| 346 | ); |
| 347 | let out = unique_path("assumed_len_dummy", "o"); |
| 348 | let compile = Command::new(compiler("armfortas")) |
| 349 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 350 | .output() |
| 351 | .expect("assumed-length dummy compile failed to spawn"); |
| 352 | assert!( |
| 353 | compile.status.success(), |
| 354 | "assumed-length dummy compile failed: {}", |
| 355 | String::from_utf8_lossy(&compile.stderr) |
| 356 | ); |
| 357 | |
| 358 | let undefined = undefined_symbols(&out); |
| 359 | assert!( |
| 360 | !undefined.iter().any(|sym| sym == "_prompt_str"), |
| 361 | "assumed-length dummy should not become an external prompt_str call: {:?}", |
| 362 | undefined |
| 363 | ); |
| 364 | |
| 365 | let _ = std::fs::remove_file(&out); |
| 366 | let _ = std::fs::remove_file(&src); |
| 367 | } |
| 368 | |
| 369 | #[test] |
| 370 | fn bind_c_name_call_uses_declared_c_symbol() { |
| 371 | let src = write_program( |
| 372 | "program p\n use iso_c_binding, only: c_int\n implicit none\n interface\n function getpid_c() bind(c, name='getpid') result(pid)\n import :: c_int\n integer(c_int) :: pid\n end function getpid_c\n end interface\n integer(c_int) :: pid\n pid = getpid_c()\nend program\n", |
| 373 | "f90", |
| 374 | ); |
| 375 | let out = unique_path("bind_c_name_call", "o"); |
| 376 | let compile = Command::new(compiler("armfortas")) |
| 377 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 378 | .output() |
| 379 | .expect("bind(c) name compile failed to spawn"); |
| 380 | assert!( |
| 381 | compile.status.success(), |
| 382 | "bind(c) name compile failed: {}", |
| 383 | String::from_utf8_lossy(&compile.stderr) |
| 384 | ); |
| 385 | |
| 386 | let undefined = undefined_symbols(&out); |
| 387 | assert!( |
| 388 | undefined.iter().any(|sym| sym == "_getpid"), |
| 389 | "bind(c, name=...) should call the declared C symbol: {:?}", |
| 390 | undefined |
| 391 | ); |
| 392 | assert!( |
| 393 | !undefined.iter().any(|sym| sym == "_getpid_c"), |
| 394 | "bind(c, name=...) should not call the local Fortran alias: {:?}", |
| 395 | undefined |
| 396 | ); |
| 397 | |
| 398 | let _ = std::fs::remove_file(&out); |
| 399 | let _ = std::fs::remove_file(&src); |
| 400 | } |
| 401 | |
| 402 | #[test] |
| 403 | fn module_procedure_case_and_bind_label_survive_amod_import() { |
| 404 | let dir = unique_dir("amod_case_bind"); |
| 405 | let mod_src = write_program_in( |
| 406 | &dir, |
| 407 | "m.f90", |
| 408 | "module m\n use iso_c_binding, only: c_int\n implicit none\n interface\n function C_CLOSE(fd) bind(c, name='close') result(ret)\n import :: c_int\n integer(c_int), value :: fd\n integer(c_int) :: ret\n end function C_CLOSE\n end interface\ncontains\n function WEXITSTATUS(status) result(exit_status)\n integer(c_int), intent(in) :: status\n integer :: exit_status\n exit_status = status + 1\n end function WEXITSTATUS\nend module\n", |
| 409 | ); |
| 410 | let use_src = write_program_in( |
| 411 | &dir, |
| 412 | "use_m.f90", |
| 413 | "program p\n use iso_c_binding, only: c_int\n use m\n implicit none\n integer(c_int) :: status, closed\n status = WEXITSTATUS(1_c_int)\n closed = C_CLOSE(0_c_int)\nend program\n", |
| 414 | ); |
| 415 | |
| 416 | let mod_obj = dir.join("m.o"); |
| 417 | let compile_mod = Command::new(compiler("armfortas")) |
| 418 | .current_dir(&dir) |
| 419 | .args([ |
| 420 | "-c", |
| 421 | "-J", |
| 422 | dir.to_str().unwrap(), |
| 423 | mod_src.to_str().unwrap(), |
| 424 | "-o", |
| 425 | mod_obj.to_str().unwrap(), |
| 426 | ]) |
| 427 | .env("NO_COLOR", "1") |
| 428 | .output() |
| 429 | .expect("module compile failed to spawn"); |
| 430 | assert!( |
| 431 | compile_mod.status.success(), |
| 432 | "module compile failed: {}", |
| 433 | String::from_utf8_lossy(&compile_mod.stderr) |
| 434 | ); |
| 435 | |
| 436 | let use_obj = dir.join("use_m.o"); |
| 437 | let compile_use = Command::new(compiler("armfortas")) |
| 438 | .current_dir(&dir) |
| 439 | .args([ |
| 440 | "-c", |
| 441 | "-I", |
| 442 | dir.to_str().unwrap(), |
| 443 | "-J", |
| 444 | dir.to_str().unwrap(), |
| 445 | use_src.to_str().unwrap(), |
| 446 | "-o", |
| 447 | use_obj.to_str().unwrap(), |
| 448 | ]) |
| 449 | .env("NO_COLOR", "1") |
| 450 | .output() |
| 451 | .expect("consumer compile failed to spawn"); |
| 452 | assert!( |
| 453 | compile_use.status.success(), |
| 454 | "consumer compile failed: {}", |
| 455 | String::from_utf8_lossy(&compile_use.stderr) |
| 456 | ); |
| 457 | |
| 458 | let undefined = undefined_symbols(&use_obj); |
| 459 | assert!( |
| 460 | undefined |
| 461 | .iter() |
| 462 | .any(|sym| sym == "_afs_modproc_m_WEXITSTATUS"), |
| 463 | "mixed-case module procedures should retain case across .amod import: {:?}", |
| 464 | undefined |
| 465 | ); |
| 466 | assert!( |
| 467 | !undefined |
| 468 | .iter() |
| 469 | .any(|sym| sym == "_afs_modproc_m_wexitstatus"), |
| 470 | "imported mixed-case module procedures should not be downcased: {:?}", |
| 471 | undefined |
| 472 | ); |
| 473 | assert!( |
| 474 | undefined.iter().any(|sym| sym == "_close"), |
| 475 | "bind(c, name=...) procedures should keep binding labels across .amod import: {:?}", |
| 476 | undefined |
| 477 | ); |
| 478 | assert!( |
| 479 | !undefined.iter().any(|sym| sym == "_c_close"), |
| 480 | "bind(c, name=...) procedures should not fall back to Fortran aliases: {:?}", |
| 481 | undefined |
| 482 | ); |
| 483 | |
| 484 | let _ = std::fs::remove_dir_all(&dir); |
| 485 | } |
| 486 | |
| 487 | #[test] |
| 488 | fn repeat_intrinsic_lowers_to_runtime_symbol() { |
| 489 | let src = write_program( |
| 490 | "program p\n implicit none\n character(len=:), allocatable :: s\n s = repeat('ab', 3)\n print *, len_trim(s)\nend program\n", |
| 491 | "f90", |
| 492 | ); |
| 493 | let out = unique_path("repeat_runtime", "o"); |
| 494 | let compile = Command::new(compiler("armfortas")) |
| 495 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 496 | .output() |
| 497 | .expect("repeat intrinsic compile failed to spawn"); |
| 498 | assert!( |
| 499 | compile.status.success(), |
| 500 | "repeat intrinsic compile failed: {}", |
| 501 | String::from_utf8_lossy(&compile.stderr) |
| 502 | ); |
| 503 | |
| 504 | let undefined = undefined_symbols(&out); |
| 505 | assert!( |
| 506 | undefined.iter().any(|sym| sym == "_afs_repeat"), |
| 507 | "repeat intrinsic should lower to afs_repeat, undefineds were: {:?}", |
| 508 | undefined |
| 509 | ); |
| 510 | assert!( |
| 511 | !undefined.iter().any(|sym| sym == "_repeat"), |
| 512 | "repeat intrinsic should not lower to a raw repeat symbol: {:?}", |
| 513 | undefined |
| 514 | ); |
| 515 | |
| 516 | let _ = std::fs::remove_file(&out); |
| 517 | let _ = std::fs::remove_file(&src); |
| 518 | } |
| 519 | |
| 520 | #[test] |
| 521 | fn pointer_dummy_associated_lowers_without_raw_symbol() { |
| 522 | let src = write_program( |
| 523 | "module m\n implicit none\n type :: node_t\n integer :: value = 0\n end type node_t\ncontains\n logical function present(node) result(ok)\n type(node_t), pointer, intent(in) :: node\n ok = associated(node)\n end function present\nend module m\n", |
| 524 | "f90", |
| 525 | ); |
| 526 | let out = unique_path("associated_pointer_dummy", "o"); |
| 527 | let compile = Command::new(compiler("armfortas")) |
| 528 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 529 | .env("NO_COLOR", "1") |
| 530 | .output() |
| 531 | .expect("pointer associated compile failed to spawn"); |
| 532 | assert!( |
| 533 | compile.status.success(), |
| 534 | "pointer associated compile failed: {}", |
| 535 | String::from_utf8_lossy(&compile.stderr) |
| 536 | ); |
| 537 | |
| 538 | let undefined = undefined_symbols(&out); |
| 539 | assert!( |
| 540 | !undefined.iter().any(|sym| sym == "_associated"), |
| 541 | "pointer dummy associated() should not escape as a raw symbol: {:?}", |
| 542 | undefined |
| 543 | ); |
| 544 | |
| 545 | let _ = std::fs::remove_file(&out); |
| 546 | let _ = std::fs::remove_file(&src); |
| 547 | } |
| 548 | |
| 549 | #[test] |
| 550 | fn pointer_function_result_associated_lowers_without_raw_symbol() { |
| 551 | let src = write_program( |
| 552 | "module m\n implicit none\n type :: node_t\n integer :: value = 0\n end type node_t\ncontains\n recursive function parse() result(node)\n type(node_t), pointer :: node, right_node\n nullify(node)\n if (.not. associated(node)) return\n if (.not. associated(right_node)) return\n end function parse\nend module m\n", |
| 553 | "f90", |
| 554 | ); |
| 555 | let out = unique_path("associated_pointer_result", "o"); |
| 556 | let compile = Command::new(compiler("armfortas")) |
| 557 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 558 | .env("NO_COLOR", "1") |
| 559 | .output() |
| 560 | .expect("pointer result associated compile failed to spawn"); |
| 561 | assert!( |
| 562 | compile.status.success(), |
| 563 | "pointer result associated compile failed: {}", |
| 564 | String::from_utf8_lossy(&compile.stderr) |
| 565 | ); |
| 566 | |
| 567 | let undefined = undefined_symbols(&out); |
| 568 | assert!( |
| 569 | !undefined.iter().any(|sym| sym == "_associated"), |
| 570 | "pointer function-result associated() should not escape as a raw symbol: {:?}", |
| 571 | undefined |
| 572 | ); |
| 573 | |
| 574 | let _ = std::fs::remove_file(&out); |
| 575 | let _ = std::fs::remove_file(&src); |
| 576 | } |
| 577 | |
| 578 | #[test] |
| 579 | fn component_array_intrinsics_survive_logical_condition_lowering() { |
| 580 | let src = write_program( |
| 581 | "module m\n implicit none\n type :: cmd_t\n character(:), allocatable :: tokens(:)\n integer, allocatable :: token_lengths(:)\n end type cmd_t\ncontains\n integer function f(cmd, i) result(strip_len)\n type(cmd_t), intent(in) :: cmd\n integer, intent(in) :: i\n if (allocated(cmd%token_lengths) .and. i <= size(cmd%token_lengths) .and. cmd%token_lengths(i) > 0) then\n strip_len = cmd%token_lengths(i)\n else\n strip_len = len_trim(cmd%tokens(i))\n end if\n end function f\nend module m\n", |
| 582 | "f90", |
| 583 | ); |
| 584 | let out = unique_path("component_array_condition", "o"); |
| 585 | let compile = Command::new(compiler("armfortas")) |
| 586 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 587 | .output() |
| 588 | .expect("component array condition compile failed to spawn"); |
| 589 | assert!( |
| 590 | compile.status.success(), |
| 591 | "component array condition compile failed: {}", |
| 592 | String::from_utf8_lossy(&compile.stderr) |
| 593 | ); |
| 594 | |
| 595 | let undefined = undefined_symbols(&out); |
| 596 | assert!( |
| 597 | undefined.iter().any(|sym| sym == "_afs_array_allocated"), |
| 598 | "component array condition should lower allocated() to afs_array_allocated: {:?}", |
| 599 | undefined |
| 600 | ); |
| 601 | assert!( |
| 602 | undefined.iter().any(|sym| sym == "_afs_array_size"), |
| 603 | "component array condition should lower size() to afs_array_size: {:?}", |
| 604 | undefined |
| 605 | ); |
| 606 | assert!( |
| 607 | undefined.iter().any(|sym| sym == "_afs_len_trim"), |
| 608 | "component array condition should lower len_trim() to afs_len_trim: {:?}", |
| 609 | undefined |
| 610 | ); |
| 611 | assert!( |
| 612 | !undefined |
| 613 | .iter() |
| 614 | .any(|sym| sym == "_allocated" || sym == "_size"), |
| 615 | "component array condition should not call raw allocated/size symbols: {:?}", |
| 616 | undefined |
| 617 | ); |
| 618 | |
| 619 | let _ = std::fs::remove_file(&out); |
| 620 | let _ = std::fs::remove_file(&src); |
| 621 | } |
| 622 | |
| 623 | #[test] |
| 624 | fn allocatable_array_element_component_intrinsics_do_not_escape() { |
| 625 | let src = write_program( |
| 626 | "module m\n implicit none\n integer, parameter :: max_token_len = 32\n type :: command_t\n character(len=:), allocatable :: tokens(:)\n character(len=max_token_len), allocatable :: prefix_assignments(:)\n character(len=:), allocatable :: heredoc_delimiter\n end type command_t\ncontains\n subroutine f()\n type(command_t), allocatable :: temp_commands(:)\n integer :: i\n allocate(temp_commands(2))\n i = 1\n if (allocated(temp_commands(i)%prefix_assignments)) print *, 1\n if (allocated(temp_commands(i)%tokens)) print *, 2\n if (allocated(temp_commands(i)%heredoc_delimiter)) print *, 3\n end subroutine f\nend module m\n", |
| 627 | "f90", |
| 628 | ); |
| 629 | let out = unique_path("allocatable_base_component_intrinsics", "o"); |
| 630 | let compile = Command::new(compiler("armfortas")) |
| 631 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 632 | .env("NO_COLOR", "1") |
| 633 | .output() |
| 634 | .expect("allocatable base component intrinsic compile failed to spawn"); |
| 635 | assert!( |
| 636 | compile.status.success(), |
| 637 | "allocatable base component intrinsic compile failed: {}", |
| 638 | String::from_utf8_lossy(&compile.stderr) |
| 639 | ); |
| 640 | |
| 641 | let undefined = undefined_symbols(&out); |
| 642 | assert!( |
| 643 | undefined.iter().any(|sym| sym == "_afs_array_allocated"), |
| 644 | "allocatable component arrays should lower allocated() to afs_array_allocated: {:?}", |
| 645 | undefined |
| 646 | ); |
| 647 | assert!( |
| 648 | undefined.iter().any(|sym| sym == "_afs_string_allocated"), |
| 649 | "allocatable character components should lower allocated() to afs_string_allocated: {:?}", |
| 650 | undefined |
| 651 | ); |
| 652 | assert!( |
| 653 | !undefined.iter().any(|sym| sym == "_allocated"), |
| 654 | "allocatable array-element component allocated() should not escape as a raw symbol: {:?}", |
| 655 | undefined |
| 656 | ); |
| 657 | |
| 658 | let _ = std::fs::remove_file(&out); |
| 659 | let _ = std::fs::remove_file(&src); |
| 660 | } |
| 661 | |
| 662 | #[test] |
| 663 | fn fixed_component_array_size_lowers_without_raw_symbol() { |
| 664 | let src = write_program( |
| 665 | "module m\n implicit none\n type :: shell_t\n integer :: vars(4)\n end type shell_t\ncontains\n integer function f(shell) result(n)\n type(shell_t), intent(in) :: shell\n n = size(shell%vars)\n end function f\nend module m\n", |
| 666 | "f90", |
| 667 | ); |
| 668 | let out = unique_path("fixed_component_size", "o"); |
| 669 | let compile = Command::new(compiler("armfortas")) |
| 670 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 671 | .env("NO_COLOR", "1") |
| 672 | .output() |
| 673 | .expect("fixed component size compile failed to spawn"); |
| 674 | assert!( |
| 675 | compile.status.success(), |
| 676 | "fixed component size compile failed: {}", |
| 677 | String::from_utf8_lossy(&compile.stderr) |
| 678 | ); |
| 679 | |
| 680 | let undefined = undefined_symbols(&out); |
| 681 | assert!( |
| 682 | !undefined.iter().any(|sym| sym == "_size"), |
| 683 | "fixed-size component array SIZE() should not escape as a raw symbol: {:?}", |
| 684 | undefined |
| 685 | ); |
| 686 | |
| 687 | let _ = std::fs::remove_file(&out); |
| 688 | let _ = std::fs::remove_file(&src); |
| 689 | } |
| 690 | |
| 691 | #[test] |
| 692 | fn allocate_bounds_size_intrinsic_lowers_without_raw_symbol() { |
| 693 | let src = write_program( |
| 694 | "module m\n implicit none\n type :: string_t\n character(:), allocatable :: str\n end type string_t\n type :: shell_t\n type(string_t), allocatable :: positional_params(:)\n end type shell_t\ncontains\n subroutine f(shell)\n type(shell_t), intent(inout) :: shell\n type(string_t), allocatable :: saved(:)\n integer :: i\n if (allocated(shell%positional_params)) then\n allocate(saved(size(shell%positional_params)))\n do i = 1, size(shell%positional_params)\n saved(i)%str = shell%positional_params(i)%str\n end do\n end if\n end subroutine f\nend module m\n", |
| 695 | "f90", |
| 696 | ); |
| 697 | let out = unique_path("allocate_bounds_size", "o"); |
| 698 | let compile = Command::new(compiler("armfortas")) |
| 699 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 700 | .env("NO_COLOR", "1") |
| 701 | .output() |
| 702 | .expect("allocate-bounds size compile failed to spawn"); |
| 703 | assert!( |
| 704 | compile.status.success(), |
| 705 | "allocate-bounds size compile failed: {}", |
| 706 | String::from_utf8_lossy(&compile.stderr) |
| 707 | ); |
| 708 | |
| 709 | let undefined = undefined_symbols(&out); |
| 710 | assert!( |
| 711 | undefined.iter().any(|sym| sym == "_afs_array_size"), |
| 712 | "allocate bounds should still lower size() to afs_array_size: {:?}", |
| 713 | undefined |
| 714 | ); |
| 715 | assert!( |
| 716 | !undefined.iter().any(|sym| sym == "_size"), |
| 717 | "allocate bounds size() should not escape as a raw symbol: {:?}", |
| 718 | undefined |
| 719 | ); |
| 720 | |
| 721 | let _ = std::fs::remove_file(&out); |
| 722 | let _ = std::fs::remove_file(&src); |
| 723 | } |
| 724 | |
| 725 | #[test] |
| 726 | fn fixed_component_array_element_assignment_compiles() { |
| 727 | let src = write_program( |
| 728 | "module m\n implicit none\n type :: command_t\n integer :: code = 0\n end type command_t\n type :: trap_table_t\n type(command_t) :: commands(3)\n end type trap_table_t\ncontains\n subroutine set_code(tab, i, v)\n type(trap_table_t), intent(inout) :: tab\n integer, intent(in) :: i, v\n tab%commands(i)%code = v\n end subroutine set_code\nend module m\n", |
| 729 | "f90", |
| 730 | ); |
| 731 | let out = unique_path("fixed_component_array", "o"); |
| 732 | let compile = Command::new(compiler("armfortas")) |
| 733 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 734 | .output() |
| 735 | .expect("fixed component array compile failed to spawn"); |
| 736 | assert!( |
| 737 | compile.status.success(), |
| 738 | "fixed component array compile failed: {}", |
| 739 | String::from_utf8_lossy(&compile.stderr) |
| 740 | ); |
| 741 | |
| 742 | let _ = std::fs::remove_file(&out); |
| 743 | let _ = std::fs::remove_file(&src); |
| 744 | } |
| 745 | |
| 746 | #[test] |
| 747 | fn scalar_char_component_ops_and_achar_compile() { |
| 748 | let src = write_program( |
| 749 | "module m\n implicit none\n type :: shell_t\n character(len=8) :: ifs = ''\n end type shell_t\ncontains\n subroutine f(shell, sep)\n type(shell_t), intent(in) :: shell\n character(len=1), intent(out) :: sep\n if (len_trim(shell%ifs) > 0) then\n sep = shell%ifs(1:1)\n else\n sep = achar(0)\n end if\n end subroutine f\nend module m\n", |
| 750 | "f90", |
| 751 | ); |
| 752 | let out = unique_path("scalar_char_component", "o"); |
| 753 | let compile = Command::new(compiler("armfortas")) |
| 754 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 755 | .output() |
| 756 | .expect("scalar char component compile failed to spawn"); |
| 757 | assert!( |
| 758 | compile.status.success(), |
| 759 | "scalar char component compile failed: {}", |
| 760 | String::from_utf8_lossy(&compile.stderr) |
| 761 | ); |
| 762 | |
| 763 | let undefined = undefined_symbols(&out); |
| 764 | assert!( |
| 765 | undefined.iter().any(|sym| sym == "_afs_len_trim"), |
| 766 | "scalar char component should lower len_trim() to afs_len_trim: {:?}", |
| 767 | undefined |
| 768 | ); |
| 769 | assert!( |
| 770 | undefined.iter().any(|sym| sym == "_afs_char"), |
| 771 | "ACHAR should lower to afs_char: {:?}", |
| 772 | undefined |
| 773 | ); |
| 774 | assert!( |
| 775 | !undefined.iter().any(|sym| sym == "_achar" || sym == "_ifs"), |
| 776 | "scalar char component lowering should not introduce raw achar/ifs symbols: {:?}", |
| 777 | undefined |
| 778 | ); |
| 779 | |
| 780 | let _ = std::fs::remove_file(&out); |
| 781 | let _ = std::fs::remove_file(&src); |
| 782 | } |
| 783 | |
| 784 | #[test] |
| 785 | fn scalar_char_substring_argument_avoids_raw_local_symbol() { |
| 786 | let src = write_program( |
| 787 | "module m\n implicit none\ncontains\n integer function visual_length(s)\n character(len=*), intent(in) :: s\n visual_length = len_trim(s)\n end function visual_length\n\n integer function run(input) result(n)\n character(len=*), intent(in) :: input\n character(len=len(input)) :: working_input\n working_input = input\n n = visual_length(working_input(2:3))\n end function run\nend module m\n", |
| 788 | "f90", |
| 789 | ); |
| 790 | let out = unique_path("char_substring_arg", "o"); |
| 791 | let compile = Command::new(compiler("armfortas")) |
| 792 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 793 | .output() |
| 794 | .expect("char substring argument compile failed to spawn"); |
| 795 | assert!( |
| 796 | compile.status.success(), |
| 797 | "char substring argument compile failed: {}", |
| 798 | String::from_utf8_lossy(&compile.stderr) |
| 799 | ); |
| 800 | |
| 801 | let undefined = undefined_symbols(&out); |
| 802 | assert!( |
| 803 | undefined.iter().any(|sym| sym == "_afs_len_trim"), |
| 804 | "character dummy call should still route len_trim through the runtime: {:?}", |
| 805 | undefined |
| 806 | ); |
| 807 | assert!( |
| 808 | !undefined.iter().any(|sym| sym == "_working_input"), |
| 809 | "character substring argument should not lower as an external local symbol: {:?}", |
| 810 | undefined |
| 811 | ); |
| 812 | |
| 813 | let _ = std::fs::remove_file(&out); |
| 814 | let _ = std::fs::remove_file(&src); |
| 815 | } |
| 816 | |
| 817 | #[test] |
| 818 | fn allocated_on_derived_array_element_component_uses_descriptor_runtime() { |
| 819 | let src = write_program( |
| 820 | "program p\n implicit none\n type :: cmd_t\n character(:), allocatable :: tokens(:)\n end type cmd_t\n type(cmd_t) :: cmds(2)\n logical :: ok\n ok = allocated(cmds(1)%tokens)\n if (ok) print *, size(cmds(1)%tokens)\nend program\n", |
| 821 | "f90", |
| 822 | ); |
| 823 | let out = unique_path("derived_array_component_allocated", "o"); |
| 824 | let compile = Command::new(compiler("armfortas")) |
| 825 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 826 | .output() |
| 827 | .expect("derived array component allocated compile failed to spawn"); |
| 828 | assert!( |
| 829 | compile.status.success(), |
| 830 | "derived array component allocated compile failed: {}", |
| 831 | String::from_utf8_lossy(&compile.stderr) |
| 832 | ); |
| 833 | |
| 834 | let undefined = undefined_symbols(&out); |
| 835 | assert!( |
| 836 | undefined.iter().any(|sym| sym == "_afs_array_allocated"), |
| 837 | "allocated(cmds(i)%tokens) should lower to afs_array_allocated: {:?}", |
| 838 | undefined |
| 839 | ); |
| 840 | assert!( |
| 841 | undefined.iter().any(|sym| sym == "_afs_array_size"), |
| 842 | "size(cmds(i)%tokens) should lower to afs_array_size: {:?}", |
| 843 | undefined |
| 844 | ); |
| 845 | assert!( |
| 846 | !undefined.iter().any(|sym| sym == "_allocated" || sym == "_size"), |
| 847 | "derived array element component intrinsics should not call raw allocated/size symbols: {:?}", |
| 848 | undefined |
| 849 | ); |
| 850 | |
| 851 | let _ = std::fs::remove_file(&out); |
| 852 | let _ = std::fs::remove_file(&src); |
| 853 | } |
| 854 | |
| 855 | #[test] |
| 856 | fn named_len_char_component_substring_and_trim_compile() { |
| 857 | let src = write_program( |
| 858 | "module m\n implicit none\n integer, parameter :: max_token_len = 8\n type :: token_t\n character(len=max_token_len) :: value\n end type token_t\ncontains\n subroutine f(tok, i, is_bang, trimmed)\n type(token_t), intent(in) :: tok\n integer, intent(in) :: i\n logical, intent(out) :: is_bang\n character(len=max_token_len), intent(out) :: trimmed\n is_bang = (tok%value(i:i) == '!')\n trimmed = trim(tok%value)\n end subroutine f\nend module m\n", |
| 859 | "f90", |
| 860 | ); |
| 861 | let out = unique_path("named_len_char_component", "o"); |
| 862 | let compile = Command::new(compiler("armfortas")) |
| 863 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 864 | .output() |
| 865 | .expect("named-len char component compile failed to spawn"); |
| 866 | assert!( |
| 867 | compile.status.success(), |
| 868 | "named-len char component compile failed: {}", |
| 869 | String::from_utf8_lossy(&compile.stderr) |
| 870 | ); |
| 871 | assert!( |
| 872 | out.exists(), |
| 873 | "named-len char component should produce an object file" |
| 874 | ); |
| 875 | |
| 876 | let _ = std::fs::remove_file(&out); |
| 877 | let _ = std::fs::remove_file(&src); |
| 878 | } |
| 879 | |
| 880 | #[test] |
| 881 | fn imported_derived_array_global_component_access_compiles() { |
| 882 | let dir = unique_dir("derived_array_global"); |
| 883 | let dep = write_program_in( |
| 884 | &dir, |
| 885 | "dep.f90", |
| 886 | "module dep\n implicit none\n type :: item_t\n logical :: active = .false.\n end type item_t\n type(item_t), save :: items(2)\ncontains\n subroutine init_items()\n items(1)%active = .true.\n end subroutine init_items\nend module dep\n", |
| 887 | ); |
| 888 | let user = write_program_in( |
| 889 | &dir, |
| 890 | "user.f90", |
| 891 | "module user_mod\n use dep, only: items\n implicit none\ncontains\n logical function item_active(i)\n integer, intent(in) :: i\n item_active = items(i)%active\n end function item_active\nend module user_mod\n", |
| 892 | ); |
| 893 | let dep_obj = dir.join("dep.o"); |
| 894 | let user_obj = dir.join("user.o"); |
| 895 | |
| 896 | let dep_compile = Command::new(compiler("armfortas")) |
| 897 | .args([ |
| 898 | "-c", |
| 899 | dep.to_str().unwrap(), |
| 900 | "-J", |
| 901 | dir.to_str().unwrap(), |
| 902 | "-o", |
| 903 | dep_obj.to_str().unwrap(), |
| 904 | ]) |
| 905 | .output() |
| 906 | .expect("dep module compile failed to spawn"); |
| 907 | assert!( |
| 908 | dep_compile.status.success(), |
| 909 | "dep module compile failed: {}", |
| 910 | String::from_utf8_lossy(&dep_compile.stderr) |
| 911 | ); |
| 912 | |
| 913 | let user_compile = Command::new(compiler("armfortas")) |
| 914 | .args([ |
| 915 | "-c", |
| 916 | user.to_str().unwrap(), |
| 917 | "-I", |
| 918 | dir.to_str().unwrap(), |
| 919 | "-J", |
| 920 | dir.to_str().unwrap(), |
| 921 | "-o", |
| 922 | user_obj.to_str().unwrap(), |
| 923 | ]) |
| 924 | .output() |
| 925 | .expect("user module compile failed to spawn"); |
| 926 | assert!( |
| 927 | user_compile.status.success(), |
| 928 | "user module compile failed: {}", |
| 929 | String::from_utf8_lossy(&user_compile.stderr) |
| 930 | ); |
| 931 | |
| 932 | let _ = std::fs::remove_file(&dep_obj); |
| 933 | let _ = std::fs::remove_file(&user_obj); |
| 934 | let _ = std::fs::remove_file(dir.join("dep.amod")); |
| 935 | let _ = std::fs::remove_file(&dep); |
| 936 | let _ = std::fs::remove_file(&user); |
| 937 | let _ = std::fs::remove_dir_all(&dir); |
| 938 | } |
| 939 | |
| 940 | #[test] |
| 941 | fn derived_array_element_assignment_with_pointer_component_compiles() { |
| 942 | let src = write_program( |
| 943 | "module m\n implicit none\n type :: node_t\n integer :: x = 0\n end type node_t\n type :: entry_t\n character(len=256) :: name\n type(node_t), pointer :: body => null()\n end type entry_t\n type(entry_t), save :: entries(4)\ncontains\n subroutine shift(i)\n integer, intent(in) :: i\n entries(i) = entries(i + 1)\n end subroutine shift\nend module m\n", |
| 944 | "f90", |
| 945 | ); |
| 946 | let out = unique_path("derived_array_shift_ptr", "o"); |
| 947 | let compile = Command::new(compiler("armfortas")) |
| 948 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 949 | .output() |
| 950 | .expect("derived array shift compile failed to spawn"); |
| 951 | assert!( |
| 952 | compile.status.success(), |
| 953 | "derived array shift compile failed: {}", |
| 954 | String::from_utf8_lossy(&compile.stderr) |
| 955 | ); |
| 956 | |
| 957 | let _ = std::fs::remove_file(&out); |
| 958 | let _ = std::fs::remove_file(&src); |
| 959 | } |
| 960 | |
| 961 | #[test] |
| 962 | fn dash_o_equals_form_sets_output_path() { |
| 963 | let src = write_program("program p\n print *, 1\nend program\n", "f90"); |
| 964 | let out = unique_path("oeq", "o"); |
| 965 | let arg = format!("-o={}", out.display()); |
| 966 | let result = Command::new(compiler("armfortas")) |
| 967 | .args(["-c", src.to_str().unwrap(), &arg]) |
| 968 | .output() |
| 969 | .expect("compile failed to spawn"); |
| 970 | assert!( |
| 971 | result.status.success(), |
| 972 | "-o=path compile failed: {}", |
| 973 | String::from_utf8_lossy(&result.stderr) |
| 974 | ); |
| 975 | assert!(out.exists(), "-o=path should produce the requested output"); |
| 976 | let _ = std::fs::remove_file(&out); |
| 977 | let _ = std::fs::remove_file(&src); |
| 978 | } |
| 979 | |
| 980 | #[test] |
| 981 | fn duplicate_o_is_rejected() { |
| 982 | let src = write_program("program p\n print *, 1\nend program\n", "f90"); |
| 983 | let out_a = unique_path("dup_a", "bin"); |
| 984 | let out_b = unique_path("dup_b", "bin"); |
| 985 | let result = Command::new(compiler("armfortas")) |
| 986 | .args([ |
| 987 | src.to_str().unwrap(), |
| 988 | "-o", |
| 989 | out_a.to_str().unwrap(), |
| 990 | "-o", |
| 991 | out_b.to_str().unwrap(), |
| 992 | ]) |
| 993 | .output() |
| 994 | .expect("compile failed to spawn"); |
| 995 | assert!(!result.status.success(), "duplicate -o should fail"); |
| 996 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 997 | assert!( |
| 998 | stderr.contains("duplicate -o"), |
| 999 | "expected duplicate -o diagnostic: {}", |
| 1000 | stderr |
| 1001 | ); |
| 1002 | assert!(!out_a.exists(), "first output should not be produced"); |
| 1003 | assert!(!out_b.exists(), "second output should not be produced"); |
| 1004 | let _ = std::fs::remove_file(&src); |
| 1005 | } |
| 1006 | |
| 1007 | #[test] |
| 1008 | fn multi_input_dash_c_produces_one_object_per_source() { |
| 1009 | let dir = unique_dir("multi_c_ok"); |
| 1010 | write_program_in(&dir, "m.f90", "module m\n integer :: x = 7\nend module\n"); |
| 1011 | write_program_in( |
| 1012 | &dir, |
| 1013 | "user.f90", |
| 1014 | "program p\n use m\n print *, x\nend program\n", |
| 1015 | ); |
| 1016 | let result = Command::new(compiler("armfortas")) |
| 1017 | .current_dir(&dir) |
| 1018 | .args(["-c", "m.f90", "user.f90"]) |
| 1019 | .output() |
| 1020 | .expect("compile failed to spawn"); |
| 1021 | assert!( |
| 1022 | result.status.success(), |
| 1023 | "multi-input -c failed: {}", |
| 1024 | String::from_utf8_lossy(&result.stderr) |
| 1025 | ); |
| 1026 | assert!(dir.join("m.o").exists(), "module object was not written"); |
| 1027 | assert!(dir.join("user.o").exists(), "user object was not written"); |
| 1028 | assert!( |
| 1029 | dir.join("m.amod").exists(), |
| 1030 | "module interface was not written" |
| 1031 | ); |
| 1032 | let _ = std::fs::remove_dir_all(&dir); |
| 1033 | } |
| 1034 | |
| 1035 | #[test] |
| 1036 | fn multi_input_dash_c_with_o_is_rejected() { |
| 1037 | let dir = unique_dir("multi_c_err"); |
| 1038 | write_program_in(&dir, "a.f90", "program a\n print *, 1\nend program\n"); |
| 1039 | write_program_in(&dir, "b.f90", "program b\n print *, 2\nend program\n"); |
| 1040 | let result = Command::new(compiler("armfortas")) |
| 1041 | .current_dir(&dir) |
| 1042 | .args(["-c", "a.f90", "b.f90", "-o", "multi.o"]) |
| 1043 | .output() |
| 1044 | .expect("compile failed to spawn"); |
| 1045 | assert!( |
| 1046 | !result.status.success(), |
| 1047 | "multi-input -c with -o should fail" |
| 1048 | ); |
| 1049 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 1050 | assert!( |
| 1051 | stderr.contains("-o") && stderr.contains("multiple input files"), |
| 1052 | "expected -c/-o multi-input diagnostic: {}", |
| 1053 | stderr |
| 1054 | ); |
| 1055 | assert!( |
| 1056 | !dir.join("multi.o").exists(), |
| 1057 | "no linked or object output should be produced" |
| 1058 | ); |
| 1059 | let _ = std::fs::remove_dir_all(&dir); |
| 1060 | } |
| 1061 | |
| 1062 | #[test] |
| 1063 | fn prebuilt_object_input_links_cleanly() { |
| 1064 | let src = write_program("program p\n print *, 9\nend program\n", "f90"); |
| 1065 | let obj = unique_path("link_only_obj", "o"); |
| 1066 | let compile = Command::new(compiler("armfortas")) |
| 1067 | .args(["-c", src.to_str().unwrap(), "-o", obj.to_str().unwrap()]) |
| 1068 | .output() |
| 1069 | .expect("object compile failed to spawn"); |
| 1070 | assert!( |
| 1071 | compile.status.success(), |
| 1072 | "object compile failed: {}", |
| 1073 | String::from_utf8_lossy(&compile.stderr) |
| 1074 | ); |
| 1075 | |
| 1076 | let exe = unique_path("link_only_obj", "bin"); |
| 1077 | let link = Command::new(compiler("armfortas")) |
| 1078 | .args([obj.to_str().unwrap(), "-o", exe.to_str().unwrap()]) |
| 1079 | .output() |
| 1080 | .expect("link-only spawn failed"); |
| 1081 | assert!( |
| 1082 | link.status.success(), |
| 1083 | "prebuilt object link failed: {}", |
| 1084 | String::from_utf8_lossy(&link.stderr) |
| 1085 | ); |
| 1086 | assert!(exe.exists(), "prebuilt object link should write the binary"); |
| 1087 | |
| 1088 | let _ = std::fs::remove_file(&exe); |
| 1089 | let _ = std::fs::remove_file(&obj); |
| 1090 | let _ = std::fs::remove_file(&src); |
| 1091 | } |
| 1092 | |
| 1093 | #[test] |
| 1094 | fn prebuilt_archive_input_links_after_objects() { |
| 1095 | let dir = unique_dir("link_only_archive"); |
| 1096 | let helper_src = write_program_in( |
| 1097 | &dir, |
| 1098 | "helper.f90", |
| 1099 | "subroutine helper()\n print *, 7\nend subroutine helper\n", |
| 1100 | ); |
| 1101 | let main_src = write_program_in( |
| 1102 | &dir, |
| 1103 | "main.f90", |
| 1104 | "program p\n call helper()\nend program p\n", |
| 1105 | ); |
| 1106 | |
| 1107 | let helper_obj = dir.join("helper.o"); |
| 1108 | let compile_helper = Command::new(compiler("armfortas")) |
| 1109 | .current_dir(&dir) |
| 1110 | .args([ |
| 1111 | "-c", |
| 1112 | helper_src.to_str().unwrap(), |
| 1113 | "-o", |
| 1114 | helper_obj.to_str().unwrap(), |
| 1115 | ]) |
| 1116 | .output() |
| 1117 | .expect("helper compile spawn failed"); |
| 1118 | assert!( |
| 1119 | compile_helper.status.success(), |
| 1120 | "helper compile failed: {}", |
| 1121 | String::from_utf8_lossy(&compile_helper.stderr) |
| 1122 | ); |
| 1123 | |
| 1124 | let archive = dir.join("libhelper.a"); |
| 1125 | let ar = Command::new("ar") |
| 1126 | .current_dir(&dir) |
| 1127 | .args([ |
| 1128 | "rcs", |
| 1129 | archive.to_str().unwrap(), |
| 1130 | helper_obj.to_str().unwrap(), |
| 1131 | ]) |
| 1132 | .output() |
| 1133 | .expect("archive spawn failed"); |
| 1134 | assert!( |
| 1135 | ar.status.success(), |
| 1136 | "archive creation failed: {}", |
| 1137 | String::from_utf8_lossy(&ar.stderr) |
| 1138 | ); |
| 1139 | |
| 1140 | let main_obj = dir.join("main.o"); |
| 1141 | let compile_main = Command::new(compiler("armfortas")) |
| 1142 | .current_dir(&dir) |
| 1143 | .args([ |
| 1144 | "-c", |
| 1145 | main_src.to_str().unwrap(), |
| 1146 | "-o", |
| 1147 | main_obj.to_str().unwrap(), |
| 1148 | ]) |
| 1149 | .output() |
| 1150 | .expect("main compile spawn failed"); |
| 1151 | assert!( |
| 1152 | compile_main.status.success(), |
| 1153 | "main compile failed: {}", |
| 1154 | String::from_utf8_lossy(&compile_main.stderr) |
| 1155 | ); |
| 1156 | |
| 1157 | let exe = dir.join("linked_archive"); |
| 1158 | let link = Command::new(compiler("armfortas")) |
| 1159 | .current_dir(&dir) |
| 1160 | .args([ |
| 1161 | main_obj.to_str().unwrap(), |
| 1162 | archive.to_str().unwrap(), |
| 1163 | "-o", |
| 1164 | exe.to_str().unwrap(), |
| 1165 | ]) |
| 1166 | .output() |
| 1167 | .expect("archive link spawn failed"); |
| 1168 | assert!( |
| 1169 | link.status.success(), |
| 1170 | "prebuilt archive link failed: {}", |
| 1171 | String::from_utf8_lossy(&link.stderr) |
| 1172 | ); |
| 1173 | assert!( |
| 1174 | exe.exists(), |
| 1175 | "prebuilt archive link should write the binary" |
| 1176 | ); |
| 1177 | |
| 1178 | let _ = std::fs::remove_dir_all(&dir); |
| 1179 | } |
| 1180 | |
| 1181 | #[test] |
| 1182 | fn dash_capital_s_produces_assembly_text() { |
| 1183 | let src = write_program("program p\n print *, 1\nend program\n", "f90"); |
| 1184 | let out = unique_path("asm", "s"); |
| 1185 | let result = Command::new(compiler("armfortas")) |
| 1186 | .args(["-S", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 1187 | .output() |
| 1188 | .expect("spawn failed"); |
| 1189 | assert!( |
| 1190 | result.status.success(), |
| 1191 | "-S compile failed: {}", |
| 1192 | String::from_utf8_lossy(&result.stderr) |
| 1193 | ); |
| 1194 | let asm = std::fs::read_to_string(&out).expect("missing asm output"); |
| 1195 | assert!( |
| 1196 | asm.contains("__TEXT"), |
| 1197 | ".s output should contain section directive" |
| 1198 | ); |
| 1199 | let _ = std::fs::remove_file(&out); |
| 1200 | let _ = std::fs::remove_file(&src); |
| 1201 | } |
| 1202 | |
| 1203 | #[test] |
| 1204 | fn dash_capital_e_preprocesses_only() { |
| 1205 | let src = write_program( |
| 1206 | "#define X 99\nprogram p\n print *, X\nend program\n", |
| 1207 | "F90", |
| 1208 | ); |
| 1209 | let out = unique_path("pp", "f90"); |
| 1210 | let result = Command::new(compiler("armfortas")) |
| 1211 | .args(["-E", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 1212 | .output() |
| 1213 | .expect("spawn failed"); |
| 1214 | assert!( |
| 1215 | result.status.success(), |
| 1216 | "-E preprocess failed: {}", |
| 1217 | String::from_utf8_lossy(&result.stderr) |
| 1218 | ); |
| 1219 | let pp = std::fs::read_to_string(&out).expect("missing preprocessed output"); |
| 1220 | assert!( |
| 1221 | pp.contains(", 99"), |
| 1222 | "preprocessed text should expand the macro: {}", |
| 1223 | pp |
| 1224 | ); |
| 1225 | let _ = std::fs::remove_file(&out); |
| 1226 | let _ = std::fs::remove_file(&src); |
| 1227 | } |
| 1228 | |
| 1229 | #[test] |
| 1230 | fn dash_capital_e_without_o_writes_to_stdout() { |
| 1231 | let dir = unique_dir("pp_stdout"); |
| 1232 | write_program_in( |
| 1233 | &dir, |
| 1234 | "hello.F90", |
| 1235 | "#define X 99\nprogram p\n print *, X\nend program\n", |
| 1236 | ); |
| 1237 | let result = Command::new(compiler("armfortas")) |
| 1238 | .current_dir(&dir) |
| 1239 | .args(["-E", "hello.F90"]) |
| 1240 | .output() |
| 1241 | .expect("spawn failed"); |
| 1242 | assert!( |
| 1243 | result.status.success(), |
| 1244 | "-E preprocess failed: {}", |
| 1245 | String::from_utf8_lossy(&result.stderr) |
| 1246 | ); |
| 1247 | let stdout = String::from_utf8_lossy(&result.stdout); |
| 1248 | assert!( |
| 1249 | stdout.contains(", 99"), |
| 1250 | "preprocessed output should be written to stdout: {}", |
| 1251 | stdout |
| 1252 | ); |
| 1253 | assert!( |
| 1254 | !dir.join("hello").exists(), |
| 1255 | "default -E output should not create a bare-stem file" |
| 1256 | ); |
| 1257 | let _ = std::fs::remove_dir_all(&dir); |
| 1258 | } |
| 1259 | |
| 1260 | #[test] |
| 1261 | fn dash_capital_d_defines_preprocessor_macro() { |
| 1262 | let src = write_program( |
| 1263 | "#ifdef USE_C_STRINGS\n#define X 1\n#else\n#define X 0\n#endif\nprogram p\n print *, X\nend program\n", |
| 1264 | "F90", |
| 1265 | ); |
| 1266 | let out = unique_path("pp_define", "f90"); |
| 1267 | let result = Command::new(compiler("armfortas")) |
| 1268 | .args([ |
| 1269 | "-DUSE_C_STRINGS", |
| 1270 | "-E", |
| 1271 | src.to_str().unwrap(), |
| 1272 | "-o", |
| 1273 | out.to_str().unwrap(), |
| 1274 | ]) |
| 1275 | .output() |
| 1276 | .expect("spawn failed"); |
| 1277 | assert!( |
| 1278 | result.status.success(), |
| 1279 | "-D preprocess failed: {}", |
| 1280 | String::from_utf8_lossy(&result.stderr) |
| 1281 | ); |
| 1282 | let pp = std::fs::read_to_string(&out).expect("missing preprocessed output"); |
| 1283 | assert!( |
| 1284 | pp.contains(", 1"), |
| 1285 | "preprocessed text should take the defined branch: {}", |
| 1286 | pp |
| 1287 | ); |
| 1288 | let _ = std::fs::remove_file(&out); |
| 1289 | let _ = std::fs::remove_file(&src); |
| 1290 | } |
| 1291 | |
| 1292 | #[test] |
| 1293 | fn dash_capital_d_rejects_invalid_macro_name() { |
| 1294 | let src = write_program("program p\n print *, 1\nend program\n", "f90"); |
| 1295 | let result = Command::new(compiler("armfortas")) |
| 1296 | .args(["-D1BAD", src.to_str().unwrap(), "-c"]) |
| 1297 | .output() |
| 1298 | .expect("spawn failed"); |
| 1299 | assert!( |
| 1300 | !result.status.success(), |
| 1301 | "-D with an invalid macro name should fail" |
| 1302 | ); |
| 1303 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 1304 | assert!( |
| 1305 | stderr.contains("invalid macro definition"), |
| 1306 | "expected invalid macro diagnostic, got: {}", |
| 1307 | stderr |
| 1308 | ); |
| 1309 | let _ = std::fs::remove_file(&src); |
| 1310 | } |
| 1311 | |
| 1312 | #[test] |
| 1313 | fn std_f95_rejects_f2008_error_stop() { |
| 1314 | let src = write_program("program p\n error stop 'oops'\nend program\n", "f90"); |
| 1315 | let out = unique_path("f95", "bin"); |
| 1316 | let result = Command::new(compiler("armfortas")) |
| 1317 | .args([ |
| 1318 | "--std=f95", |
| 1319 | src.to_str().unwrap(), |
| 1320 | "-o", |
| 1321 | out.to_str().unwrap(), |
| 1322 | ]) |
| 1323 | .output() |
| 1324 | .expect("spawn failed"); |
| 1325 | assert!( |
| 1326 | !result.status.success(), |
| 1327 | "--std=f95 should reject ERROR STOP" |
| 1328 | ); |
| 1329 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 1330 | assert!( |
| 1331 | stderr.contains("ERROR STOP") && stderr.contains("F2008"), |
| 1332 | "expected ERROR STOP / F2008 error: {}", |
| 1333 | stderr |
| 1334 | ); |
| 1335 | let _ = std::fs::remove_file(&src); |
| 1336 | } |
| 1337 | |
| 1338 | #[test] |
| 1339 | fn std_space_form_is_accepted() { |
| 1340 | let src = write_program("program p\n print *, 7\nend program\n", "f90"); |
| 1341 | let out = unique_path("std_space", "bin"); |
| 1342 | let result = Command::new(compiler("armfortas")) |
| 1343 | .args([ |
| 1344 | "--std", |
| 1345 | "f2018", |
| 1346 | src.to_str().unwrap(), |
| 1347 | "-o", |
| 1348 | out.to_str().unwrap(), |
| 1349 | ]) |
| 1350 | .output() |
| 1351 | .expect("spawn failed"); |
| 1352 | assert!( |
| 1353 | result.status.success(), |
| 1354 | "--std f2018 should compile: {}", |
| 1355 | String::from_utf8_lossy(&result.stderr) |
| 1356 | ); |
| 1357 | assert!( |
| 1358 | out.exists(), |
| 1359 | "space-form --std should preserve the input path" |
| 1360 | ); |
| 1361 | let _ = std::fs::remove_file(&out); |
| 1362 | let _ = std::fs::remove_file(&src); |
| 1363 | } |
| 1364 | |
| 1365 | #[test] |
| 1366 | fn std_f77_rejects_free_form_source() { |
| 1367 | let src = write_program("program p\n print *, 1\nend program\n", "f90"); |
| 1368 | let out = unique_path("std_f77_free", "o"); |
| 1369 | let result = Command::new(compiler("armfortas")) |
| 1370 | .args([ |
| 1371 | "--std=f77", |
| 1372 | "-c", |
| 1373 | src.to_str().unwrap(), |
| 1374 | "-o", |
| 1375 | out.to_str().unwrap(), |
| 1376 | ]) |
| 1377 | .output() |
| 1378 | .expect("spawn failed"); |
| 1379 | assert!( |
| 1380 | !result.status.success(), |
| 1381 | "--std=f77 should reject free-form source" |
| 1382 | ); |
| 1383 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 1384 | assert!( |
| 1385 | stderr.contains("--std=f77 requires fixed-form source"), |
| 1386 | "expected fixed-form requirement: {}", |
| 1387 | stderr |
| 1388 | ); |
| 1389 | let _ = std::fs::remove_file(&out); |
| 1390 | let _ = std::fs::remove_file(&src); |
| 1391 | } |
| 1392 | |
| 1393 | #[test] |
| 1394 | fn std_f95_rejects_impure_prefix() { |
| 1395 | let src = write_program("impure subroutine s()\nend subroutine\n", "f90"); |
| 1396 | let out = unique_path("std_impure", "o"); |
| 1397 | let result = Command::new(compiler("armfortas")) |
| 1398 | .args([ |
| 1399 | "--std=f95", |
| 1400 | "-c", |
| 1401 | src.to_str().unwrap(), |
| 1402 | "-o", |
| 1403 | out.to_str().unwrap(), |
| 1404 | ]) |
| 1405 | .env("NO_COLOR", "1") |
| 1406 | .output() |
| 1407 | .expect("spawn failed"); |
| 1408 | assert!(!result.status.success(), "--std=f95 should reject IMPURE"); |
| 1409 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 1410 | assert!( |
| 1411 | stderr.contains("IMPURE") && stderr.contains("F2008"), |
| 1412 | "expected IMPURE / F2008 error: {}", |
| 1413 | stderr |
| 1414 | ); |
| 1415 | let _ = std::fs::remove_file(&out); |
| 1416 | let _ = std::fs::remove_file(&src); |
| 1417 | } |
| 1418 | |
| 1419 | #[test] |
| 1420 | fn std_f95_rejects_abstract_type() { |
| 1421 | let src = write_program( |
| 1422 | "module m\n type, abstract :: t\n end type t\nend module m\n", |
| 1423 | "f90", |
| 1424 | ); |
| 1425 | let out = unique_path("std_abstract", "o"); |
| 1426 | let result = Command::new(compiler("armfortas")) |
| 1427 | .args([ |
| 1428 | "--std=f95", |
| 1429 | "-c", |
| 1430 | src.to_str().unwrap(), |
| 1431 | "-o", |
| 1432 | out.to_str().unwrap(), |
| 1433 | ]) |
| 1434 | .env("NO_COLOR", "1") |
| 1435 | .output() |
| 1436 | .expect("spawn failed"); |
| 1437 | assert!( |
| 1438 | !result.status.success(), |
| 1439 | "--std=f95 should reject ABSTRACT type" |
| 1440 | ); |
| 1441 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 1442 | assert!( |
| 1443 | stderr.contains("ABSTRACT type") && stderr.contains("F2003"), |
| 1444 | "expected ABSTRACT type / F2003 error: {}", |
| 1445 | stderr |
| 1446 | ); |
| 1447 | let _ = std::fs::remove_file(&out); |
| 1448 | let _ = std::fs::remove_file(&src); |
| 1449 | } |
| 1450 | |
| 1451 | #[test] |
| 1452 | fn std_f77_rejects_module_in_fixed_form() { |
| 1453 | let src = write_program( |
| 1454 | " module m\n implicit none\n end module m\n", |
| 1455 | "f", |
| 1456 | ); |
| 1457 | let out = unique_path("std_f77_module", "o"); |
| 1458 | let result = Command::new(compiler("armfortas")) |
| 1459 | .args([ |
| 1460 | "--std=f77", |
| 1461 | "-c", |
| 1462 | src.to_str().unwrap(), |
| 1463 | "-o", |
| 1464 | out.to_str().unwrap(), |
| 1465 | ]) |
| 1466 | .env("NO_COLOR", "1") |
| 1467 | .output() |
| 1468 | .expect("spawn failed"); |
| 1469 | assert!(!result.status.success(), "--std=f77 should reject MODULE"); |
| 1470 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 1471 | assert!( |
| 1472 | stderr.contains("MODULE") && stderr.contains("F90"), |
| 1473 | "expected MODULE / F90 error: {}", |
| 1474 | stderr |
| 1475 | ); |
| 1476 | let _ = std::fs::remove_file(&out); |
| 1477 | let _ = std::fs::remove_file(&src); |
| 1478 | } |
| 1479 | |
| 1480 | #[test] |
| 1481 | fn help_and_version_use_last_flag_wins_precedence() { |
| 1482 | let help_then_version = Command::new(compiler("armfortas")) |
| 1483 | .args(["--help", "--version"]) |
| 1484 | .output() |
| 1485 | .expect("spawn failed"); |
| 1486 | assert!(help_then_version.status.success()); |
| 1487 | let hv_stdout = String::from_utf8_lossy(&help_then_version.stdout); |
| 1488 | assert!( |
| 1489 | hv_stdout.trim_start().starts_with("armfortas "), |
| 1490 | "expected trailing --version to win: {}", |
| 1491 | hv_stdout |
| 1492 | ); |
| 1493 | |
| 1494 | let version_then_help = Command::new(compiler("armfortas")) |
| 1495 | .args(["--version", "--help"]) |
| 1496 | .output() |
| 1497 | .expect("spawn failed"); |
| 1498 | assert!(version_then_help.status.success()); |
| 1499 | let vh_stdout = String::from_utf8_lossy(&version_then_help.stdout); |
| 1500 | assert!( |
| 1501 | vh_stdout.contains("USAGE"), |
| 1502 | "expected trailing --help to win: {}", |
| 1503 | vh_stdout |
| 1504 | ); |
| 1505 | } |
| 1506 | |
| 1507 | #[test] |
| 1508 | fn response_file_supplies_arguments() { |
| 1509 | let src = write_program("program p\n print *, 7\nend program\n", "f90"); |
| 1510 | let out = unique_path("resp", "bin"); |
| 1511 | let resp = unique_path("flags", "txt"); |
| 1512 | std::fs::write( |
| 1513 | &resp, |
| 1514 | format!("-O1\n-o\n{}\n{}\n", out.display(), src.display()), |
| 1515 | ) |
| 1516 | .unwrap(); |
| 1517 | let result = Command::new(compiler("armfortas")) |
| 1518 | .arg(format!("@{}", resp.display())) |
| 1519 | .output() |
| 1520 | .expect("spawn failed"); |
| 1521 | assert!( |
| 1522 | result.status.success(), |
| 1523 | "@response-file compile failed: {}", |
| 1524 | String::from_utf8_lossy(&result.stderr) |
| 1525 | ); |
| 1526 | assert!(out.exists(), "binary should exist after @file compile"); |
| 1527 | let _ = std::fs::remove_file(&out); |
| 1528 | let _ = std::fs::remove_file(&src); |
| 1529 | let _ = std::fs::remove_file(&resp); |
| 1530 | } |
| 1531 | |
| 1532 | #[test] |
| 1533 | fn diagnostics_format_json_is_rejected_until_implemented() { |
| 1534 | let src = write_program("program p\n print *, 7\nend program\n", "f90"); |
| 1535 | let out = unique_path("diag_json", "bin"); |
| 1536 | let result = Command::new(compiler("armfortas")) |
| 1537 | .args([ |
| 1538 | "--diagnostics-format=json", |
| 1539 | src.to_str().unwrap(), |
| 1540 | "-o", |
| 1541 | out.to_str().unwrap(), |
| 1542 | ]) |
| 1543 | .output() |
| 1544 | .expect("spawn failed"); |
| 1545 | assert!( |
| 1546 | !result.status.success(), |
| 1547 | "--diagnostics-format=json should be rejected until implemented" |
| 1548 | ); |
| 1549 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 1550 | assert!( |
| 1551 | stderr.contains("JSON diagnostics are not yet implemented"), |
| 1552 | "expected explicit json-format diagnostic: {}", |
| 1553 | stderr |
| 1554 | ); |
| 1555 | let _ = std::fs::remove_file(&src); |
| 1556 | let _ = std::fs::remove_file(&out); |
| 1557 | } |
| 1558 | |
| 1559 | #[test] |
| 1560 | fn nested_response_files_support_quotes_and_relative_paths() { |
| 1561 | let dir = unique_dir("rsp nested"); |
| 1562 | let src = write_program_in( |
| 1563 | &dir, |
| 1564 | "file with spaces.f90", |
| 1565 | "program p\n print *, 7\nend program\n", |
| 1566 | ); |
| 1567 | let out = dir.join("binary with spaces"); |
| 1568 | let inner = dir.join("inner.rsp"); |
| 1569 | let outer = dir.join("outer.rsp"); |
| 1570 | std::fs::write( |
| 1571 | &inner, |
| 1572 | format!("\"{}\"\n-o\n\"{}\"\n", src.display(), out.display()), |
| 1573 | ) |
| 1574 | .unwrap(); |
| 1575 | std::fs::write(&outer, "@inner.rsp\n").unwrap(); |
| 1576 | |
| 1577 | let result = Command::new(compiler("armfortas")) |
| 1578 | .current_dir(&dir) |
| 1579 | .arg("@outer.rsp") |
| 1580 | .output() |
| 1581 | .expect("spawn failed"); |
| 1582 | assert!( |
| 1583 | result.status.success(), |
| 1584 | "nested quoted response files should compile: {}", |
| 1585 | String::from_utf8_lossy(&result.stderr) |
| 1586 | ); |
| 1587 | assert!(out.exists(), "nested response file should produce output"); |
| 1588 | let _ = std::fs::remove_dir_all(&dir); |
| 1589 | } |
| 1590 | |
| 1591 | #[test] |
| 1592 | fn accepted_but_unimplemented_flags_emit_warnings() { |
| 1593 | let src = write_program("program p\n print *, 7\nend program\n", "f90"); |
| 1594 | let out = unique_path("warn_flags", "o"); |
| 1595 | let result = Command::new(compiler("armfortas")) |
| 1596 | .args([ |
| 1597 | "-c", |
| 1598 | "-g", |
| 1599 | "-fcheck=bounds", |
| 1600 | "-fmax-stack-var-size=64", |
| 1601 | "-frecursive", |
| 1602 | "-fbackslash", |
| 1603 | "-Wall", |
| 1604 | "-Wextra", |
| 1605 | "-Wpedantic", |
| 1606 | "-Wdeprecated", |
| 1607 | src.to_str().unwrap(), |
| 1608 | "-o", |
| 1609 | out.to_str().unwrap(), |
| 1610 | ]) |
| 1611 | .output() |
| 1612 | .expect("spawn failed"); |
| 1613 | assert!( |
| 1614 | result.status.success(), |
| 1615 | "compile with accepted flags should still succeed: {}", |
| 1616 | String::from_utf8_lossy(&result.stderr) |
| 1617 | ); |
| 1618 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 1619 | for needle in [ |
| 1620 | "-g is accepted, but debug info emission is not yet implemented", |
| 1621 | "-fcheck=bounds currently has no effect", |
| 1622 | "-fmax-stack-var-size is recognized but not yet implemented", |
| 1623 | "-frecursive is recognized but not yet implemented", |
| 1624 | "-fbackslash is recognized but string escape processing is not yet implemented", |
| 1625 | "-Wall is recognized but warning-group emission is not yet implemented", |
| 1626 | "-Wextra is recognized but warning-group emission is not yet implemented", |
| 1627 | ] { |
| 1628 | assert!( |
| 1629 | stderr.contains(needle), |
| 1630 | "missing warning `{}` in {}", |
| 1631 | needle, |
| 1632 | stderr |
| 1633 | ); |
| 1634 | } |
| 1635 | assert!( |
| 1636 | !stderr |
| 1637 | .contains("-Wpedantic is recognized but warning-group emission is not yet implemented"), |
| 1638 | "pedantic should now be a real semantic warning group: {}", |
| 1639 | stderr |
| 1640 | ); |
| 1641 | assert!( |
| 1642 | !stderr.contains( |
| 1643 | "-Wdeprecated is recognized but warning-group emission is not yet implemented" |
| 1644 | ), |
| 1645 | "deprecated should now be a real semantic warning group: {}", |
| 1646 | stderr |
| 1647 | ); |
| 1648 | let _ = std::fs::remove_file(&src); |
| 1649 | let _ = std::fs::remove_file(&out); |
| 1650 | } |
| 1651 | |
| 1652 | #[test] |
| 1653 | fn fcheck_all_warns_about_partial_support() { |
| 1654 | let src = write_program("program p\n print *, 7\nend program\n", "f90"); |
| 1655 | let out = unique_path("warn_fcheck_all", "o"); |
| 1656 | let result = Command::new(compiler("armfortas")) |
| 1657 | .args([ |
| 1658 | "-c", |
| 1659 | "-fcheck=all", |
| 1660 | src.to_str().unwrap(), |
| 1661 | "-o", |
| 1662 | out.to_str().unwrap(), |
| 1663 | ]) |
| 1664 | .output() |
| 1665 | .expect("spawn failed"); |
| 1666 | assert!(result.status.success()); |
| 1667 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 1668 | assert!( |
| 1669 | stderr.contains("-fcheck=all is accepted, but only array bounds checks exist today"), |
| 1670 | "expected -fcheck=all warning: {}", |
| 1671 | stderr |
| 1672 | ); |
| 1673 | let _ = std::fs::remove_file(&src); |
| 1674 | let _ = std::fs::remove_file(&out); |
| 1675 | } |
| 1676 | |
| 1677 | #[test] |
| 1678 | fn werror_promotes_cli_warnings_to_errors() { |
| 1679 | let src = write_program("program p\n print *, 7\nend program\n", "f90"); |
| 1680 | let out = unique_path("werror_warn", "o"); |
| 1681 | let result = Command::new(compiler("armfortas")) |
| 1682 | .args([ |
| 1683 | "-c", |
| 1684 | "-Wall", |
| 1685 | "-Werror", |
| 1686 | src.to_str().unwrap(), |
| 1687 | "-o", |
| 1688 | out.to_str().unwrap(), |
| 1689 | ]) |
| 1690 | .output() |
| 1691 | .expect("spawn failed"); |
| 1692 | assert!( |
| 1693 | !result.status.success(), |
| 1694 | "-Werror should promote CLI warnings" |
| 1695 | ); |
| 1696 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 1697 | assert!( |
| 1698 | stderr.contains( |
| 1699 | "error: -Wall is recognized but warning-group emission is not yet implemented" |
| 1700 | ), |
| 1701 | "expected promoted CLI warning: {}", |
| 1702 | stderr |
| 1703 | ); |
| 1704 | let _ = std::fs::remove_file(&src); |
| 1705 | let _ = std::fs::remove_file(&out); |
| 1706 | } |
| 1707 | |
| 1708 | #[test] |
| 1709 | fn unknown_warning_flag_emits_warning() { |
| 1710 | let src = write_program("program p\n print *, 7\nend program\n", "f90"); |
| 1711 | let out = unique_path("wunknown", "o"); |
| 1712 | let result = Command::new(compiler("armfortas")) |
| 1713 | .args([ |
| 1714 | "-c", |
| 1715 | "-Weverything", |
| 1716 | src.to_str().unwrap(), |
| 1717 | "-o", |
| 1718 | out.to_str().unwrap(), |
| 1719 | ]) |
| 1720 | .output() |
| 1721 | .expect("spawn failed"); |
| 1722 | assert!( |
| 1723 | result.status.success(), |
| 1724 | "unknown -W should warn but compile" |
| 1725 | ); |
| 1726 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 1727 | assert!( |
| 1728 | stderr.contains("unrecognized warning option '-Weverything'"), |
| 1729 | "expected unknown-warning diagnostic: {}", |
| 1730 | stderr |
| 1731 | ); |
| 1732 | let _ = std::fs::remove_file(&src); |
| 1733 | let _ = std::fs::remove_file(&out); |
| 1734 | } |
| 1735 | |
| 1736 | #[test] |
| 1737 | fn wpedantic_warns_on_arithmetic_if() { |
| 1738 | let src = write_program( |
| 1739 | "program p\n integer :: i\n i = 0\n if (i) 10, 20, 30\n10 continue\n20 continue\n30 continue\nend program\n", |
| 1740 | "f90", |
| 1741 | ); |
| 1742 | let out = unique_path("wpedantic", "o"); |
| 1743 | let result = Command::new(compiler("armfortas")) |
| 1744 | .args([ |
| 1745 | "-c", |
| 1746 | "-Wpedantic", |
| 1747 | src.to_str().unwrap(), |
| 1748 | "-o", |
| 1749 | out.to_str().unwrap(), |
| 1750 | ]) |
| 1751 | .env("NO_COLOR", "1") |
| 1752 | .output() |
| 1753 | .expect("spawn failed"); |
| 1754 | assert!(result.status.success(), "pedantic compile failed"); |
| 1755 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 1756 | assert!( |
| 1757 | stderr.contains("warning: arithmetic IF is an obsolescent feature"), |
| 1758 | "expected arithmetic IF warning: {}", |
| 1759 | stderr |
| 1760 | ); |
| 1761 | let _ = std::fs::remove_file(&src); |
| 1762 | let _ = std::fs::remove_file(&out); |
| 1763 | } |
| 1764 | |
| 1765 | #[test] |
| 1766 | fn wdeprecated_warns_on_common_block() { |
| 1767 | let src = write_program( |
| 1768 | "program p\n integer :: x\n common /blk/ x\nend program\n", |
| 1769 | "f90", |
| 1770 | ); |
| 1771 | let out = unique_path("wdeprecated", "o"); |
| 1772 | let result = Command::new(compiler("armfortas")) |
| 1773 | .args([ |
| 1774 | "-c", |
| 1775 | "-Wdeprecated", |
| 1776 | src.to_str().unwrap(), |
| 1777 | "-o", |
| 1778 | out.to_str().unwrap(), |
| 1779 | ]) |
| 1780 | .env("NO_COLOR", "1") |
| 1781 | .output() |
| 1782 | .expect("spawn failed"); |
| 1783 | assert!(result.status.success(), "deprecated compile failed"); |
| 1784 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 1785 | assert!( |
| 1786 | stderr.contains("warning: COMMON block is an obsolescent feature"), |
| 1787 | "expected COMMON warning: {}", |
| 1788 | stderr |
| 1789 | ); |
| 1790 | let _ = std::fs::remove_file(&src); |
| 1791 | let _ = std::fs::remove_file(&out); |
| 1792 | } |
| 1793 | |
| 1794 | #[test] |
| 1795 | fn unknown_warning_flag_can_be_suppressed() { |
| 1796 | let src = write_program("program p\n print *, 7\nend program\n", "f90"); |
| 1797 | let out = unique_path("wunknown_suppressed", "o"); |
| 1798 | let result = Command::new(compiler("armfortas")) |
| 1799 | .args([ |
| 1800 | "-c", |
| 1801 | "-Weverything", |
| 1802 | "-Wno-unknown-warning-option", |
| 1803 | src.to_str().unwrap(), |
| 1804 | "-o", |
| 1805 | out.to_str().unwrap(), |
| 1806 | ]) |
| 1807 | .output() |
| 1808 | .expect("spawn failed"); |
| 1809 | assert!( |
| 1810 | result.status.success(), |
| 1811 | "suppressed unknown -W should compile" |
| 1812 | ); |
| 1813 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 1814 | assert!( |
| 1815 | !stderr.contains("unrecognized warning option"), |
| 1816 | "unknown-warning suppression should silence the warning: {}", |
| 1817 | stderr |
| 1818 | ); |
| 1819 | let _ = std::fs::remove_file(&src); |
| 1820 | let _ = std::fs::remove_file(&out); |
| 1821 | } |
| 1822 | |
| 1823 | #[test] |
| 1824 | fn missing_response_file_uses_io_exit_code() { |
| 1825 | let result = Command::new(compiler("armfortas")) |
| 1826 | .arg("@/definitely/missing/armfortas_cli.rsp") |
| 1827 | .output() |
| 1828 | .expect("spawn failed"); |
| 1829 | assert!( |
| 1830 | !result.status.success(), |
| 1831 | "missing response file should fail" |
| 1832 | ); |
| 1833 | assert_eq!( |
| 1834 | result.status.code(), |
| 1835 | Some(3), |
| 1836 | "response-file read failures should map to I/O exit code" |
| 1837 | ); |
| 1838 | } |
| 1839 | |
| 1840 | #[test] |
| 1841 | fn escaped_at_prefixed_input_is_treated_as_literal_filename() { |
| 1842 | let dir = unique_dir("at_input"); |
| 1843 | write_program_in(&dir, "@file.f90", "program p\n print *, 7\nend program\n"); |
| 1844 | let out = dir.join("at_file.o"); |
| 1845 | let result = Command::new(compiler("armfortas")) |
| 1846 | .current_dir(&dir) |
| 1847 | .args(["-c", "@@file.f90", "-o", "at_file.o"]) |
| 1848 | .output() |
| 1849 | .expect("spawn failed"); |
| 1850 | assert!( |
| 1851 | result.status.success(), |
| 1852 | "escaped @ input should compile: {}", |
| 1853 | String::from_utf8_lossy(&result.stderr) |
| 1854 | ); |
| 1855 | assert!( |
| 1856 | out.exists(), |
| 1857 | "escaped @ input should produce the object file" |
| 1858 | ); |
| 1859 | let _ = std::fs::remove_dir_all(&dir); |
| 1860 | } |
| 1861 | |
| 1862 | #[test] |
| 1863 | fn dash_j_writes_amod_to_chosen_directory() { |
| 1864 | let src = write_program("module dashj_mod\n integer :: y = 5\nend module\n", "f90"); |
| 1865 | let out = unique_path("dashjobj", "o"); |
| 1866 | let amod_dir = std::env::temp_dir().join(format!( |
| 1867 | "afs_cli_amod_{}_{}", |
| 1868 | std::process::id(), |
| 1869 | std::time::SystemTime::now() |
| 1870 | .duration_since(std::time::UNIX_EPOCH) |
| 1871 | .unwrap() |
| 1872 | .as_nanos(), |
| 1873 | )); |
| 1874 | std::fs::create_dir_all(&amod_dir).unwrap(); |
| 1875 | let result = Command::new(compiler("armfortas")) |
| 1876 | .args([ |
| 1877 | "-c", |
| 1878 | "-J", |
| 1879 | amod_dir.to_str().unwrap(), |
| 1880 | src.to_str().unwrap(), |
| 1881 | "-o", |
| 1882 | out.to_str().unwrap(), |
| 1883 | ]) |
| 1884 | .output() |
| 1885 | .expect("spawn failed"); |
| 1886 | assert!( |
| 1887 | result.status.success(), |
| 1888 | "-J compile failed: {}", |
| 1889 | String::from_utf8_lossy(&result.stderr) |
| 1890 | ); |
| 1891 | let amod = amod_dir.join("dashj_mod.amod"); |
| 1892 | assert!(amod.exists(), "-J should place .amod in the requested dir"); |
| 1893 | let _ = std::fs::remove_file(&out); |
| 1894 | let _ = std::fs::remove_file(&src); |
| 1895 | let _ = std::fs::remove_dir_all(&amod_dir); |
| 1896 | } |
| 1897 | |
| 1898 | #[test] |
| 1899 | fn dash_j_nonexistent_dir_is_hard_error() { |
| 1900 | let dir = unique_dir("dashj_bad"); |
| 1901 | let src = write_program_in( |
| 1902 | &dir, |
| 1903 | "m.f90", |
| 1904 | "module dashj_mod\n integer :: y = 5\nend module\n", |
| 1905 | ); |
| 1906 | let out = dir.join("m.o"); |
| 1907 | let missing = dir.join("missing_modules"); |
| 1908 | let result = Command::new(compiler("armfortas")) |
| 1909 | .args([ |
| 1910 | "-c", |
| 1911 | "-J", |
| 1912 | missing.to_str().unwrap(), |
| 1913 | src.to_str().unwrap(), |
| 1914 | "-o", |
| 1915 | out.to_str().unwrap(), |
| 1916 | ]) |
| 1917 | .output() |
| 1918 | .expect("spawn failed"); |
| 1919 | assert!(!result.status.success(), "-J to missing dir should fail"); |
| 1920 | assert_eq!(result.status.code(), Some(3), "expected I/O exit code"); |
| 1921 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 1922 | assert!( |
| 1923 | stderr.contains("cannot write"), |
| 1924 | "expected cannot-write diagnostic: {}", |
| 1925 | stderr |
| 1926 | ); |
| 1927 | let _ = std::fs::remove_dir_all(&dir); |
| 1928 | } |
| 1929 | |
| 1930 | #[test] |
| 1931 | fn dash_i_equals_form_finds_modules() { |
| 1932 | let dir = unique_dir("ieq_mod"); |
| 1933 | let mod_src = write_program_in( |
| 1934 | &dir, |
| 1935 | "mymod.f90", |
| 1936 | "module mymod\n integer :: x = 7\nend module\n", |
| 1937 | ); |
| 1938 | let user_src = write_program_in( |
| 1939 | &dir, |
| 1940 | "use_mod.f90", |
| 1941 | "program p\n use mymod\n print *, x\nend program\n", |
| 1942 | ); |
| 1943 | let mod_obj = dir.join("mymod.o"); |
| 1944 | let compile_mod = Command::new(compiler("armfortas")) |
| 1945 | .args([ |
| 1946 | "-c", |
| 1947 | mod_src.to_str().unwrap(), |
| 1948 | "-o", |
| 1949 | mod_obj.to_str().unwrap(), |
| 1950 | ]) |
| 1951 | .output() |
| 1952 | .expect("module compile spawn failed"); |
| 1953 | assert!( |
| 1954 | compile_mod.status.success(), |
| 1955 | "module compile failed: {}", |
| 1956 | String::from_utf8_lossy(&compile_mod.stderr) |
| 1957 | ); |
| 1958 | |
| 1959 | let user_obj = dir.join("use_mod.o"); |
| 1960 | let include_arg = format!("-I={}", dir.display()); |
| 1961 | let compile_user = Command::new(compiler("armfortas")) |
| 1962 | .args([ |
| 1963 | &include_arg, |
| 1964 | "-c", |
| 1965 | user_src.to_str().unwrap(), |
| 1966 | "-o", |
| 1967 | user_obj.to_str().unwrap(), |
| 1968 | ]) |
| 1969 | .output() |
| 1970 | .expect("user compile spawn failed"); |
| 1971 | assert!( |
| 1972 | compile_user.status.success(), |
| 1973 | "-I=dir should find module interfaces: {}", |
| 1974 | String::from_utf8_lossy(&compile_user.stderr) |
| 1975 | ); |
| 1976 | let _ = std::fs::remove_dir_all(&dir); |
| 1977 | } |
| 1978 | |
| 1979 | #[test] |
| 1980 | fn block_use_imports_module_values_and_procedures() { |
| 1981 | let dir = unique_dir("block_use_mod"); |
| 1982 | let mod_src = write_program_in( |
| 1983 | &dir, |
| 1984 | "expansion.f90", |
| 1985 | "module expansion\n implicit none\n integer, save :: base_value = 7\ncontains\n function arithmetic_expansion_shell(expr, shell) result(r)\n character(len=*), intent(in) :: expr\n integer, intent(inout) :: shell\n character(len=:), allocatable :: r\n r = trim(expr)\n shell = shell + 1\n end function\nend module\n", |
| 1986 | ); |
| 1987 | let user_src = write_program_in( |
| 1988 | &dir, |
| 1989 | "user.f90", |
| 1990 | "program p\n implicit none\n integer :: shell, total\n character(len=32) :: var_value\n integer :: actual_value_len\n shell = 0\n total = 0\n var_value = '123'\n actual_value_len = 3\n block\n use expansion, only: arithmetic_expansion_shell, base_value\n character(len=:), allocatable :: arith_expr, arith_result\n arith_expr = '$((' // var_value(:actual_value_len) // '))'\n arith_result = arithmetic_expansion_shell(trim(arith_expr), shell)\n total = base_value + len_trim(arith_result)\n end block\n if (shell /= 1) error stop 1\n if (total /= 14) error stop 2\nend program\n", |
| 1991 | ); |
| 1992 | let mod_obj = dir.join("expansion.o"); |
| 1993 | let compile_mod = Command::new(compiler("armfortas")) |
| 1994 | .current_dir(&dir) |
| 1995 | .args([ |
| 1996 | "-c", |
| 1997 | "-J", |
| 1998 | dir.to_str().unwrap(), |
| 1999 | mod_src.to_str().unwrap(), |
| 2000 | "-o", |
| 2001 | mod_obj.to_str().unwrap(), |
| 2002 | ]) |
| 2003 | .output() |
| 2004 | .expect("module compile spawn failed"); |
| 2005 | assert!( |
| 2006 | compile_mod.status.success(), |
| 2007 | "module compile failed: {}", |
| 2008 | String::from_utf8_lossy(&compile_mod.stderr) |
| 2009 | ); |
| 2010 | |
| 2011 | let user_obj = dir.join("user.o"); |
| 2012 | let compile_user = Command::new(compiler("armfortas")) |
| 2013 | .current_dir(&dir) |
| 2014 | .args([ |
| 2015 | "-c", |
| 2016 | user_src.to_str().unwrap(), |
| 2017 | "-o", |
| 2018 | user_obj.to_str().unwrap(), |
| 2019 | ]) |
| 2020 | .output() |
| 2021 | .expect("user compile spawn failed"); |
| 2022 | assert!( |
| 2023 | compile_user.status.success(), |
| 2024 | "BLOCK-local USE imports should compile: {}", |
| 2025 | String::from_utf8_lossy(&compile_user.stderr) |
| 2026 | ); |
| 2027 | |
| 2028 | let _ = std::fs::remove_dir_all(&dir); |
| 2029 | } |
| 2030 | |
| 2031 | #[test] |
| 2032 | fn block_interface_declares_callable_under_implicit_none() { |
| 2033 | let src = write_program( |
| 2034 | "subroutine s(acc_status)\n use iso_c_binding, only: c_char, c_int\n implicit none\n integer, intent(out) :: acc_status\n character(kind=c_char), target :: c_path(2)\n block\n interface\n function cache_access(pathname, mode) bind(C, name=\"access\")\n import :: c_char, c_int\n character(kind=c_char), intent(in) :: pathname(*)\n integer(c_int), value :: mode\n integer(c_int) :: cache_access\n end function\n end interface\n acc_status = cache_access(c_path, int(1, c_int))\n end block\nend subroutine\n", |
| 2035 | "f90", |
| 2036 | ); |
| 2037 | let out = unique_path("block_interface_decl", "o"); |
| 2038 | let result = Command::new(compiler("armfortas")) |
| 2039 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 2040 | .env("NO_COLOR", "1") |
| 2041 | .output() |
| 2042 | .expect("spawn failed"); |
| 2043 | assert!( |
| 2044 | result.status.success(), |
| 2045 | "BLOCK-local interface procedures should count as declared: {}", |
| 2046 | String::from_utf8_lossy(&result.stderr) |
| 2047 | ); |
| 2048 | let _ = std::fs::remove_file(&out); |
| 2049 | let _ = std::fs::remove_file(&src); |
| 2050 | } |
| 2051 | |
| 2052 | #[test] |
| 2053 | fn public_derived_type_in_private_module_is_emitted_and_importable() { |
| 2054 | let dir = unique_dir("public_type_mod"); |
| 2055 | let mod_src = write_program_in( |
| 2056 | &dir, |
| 2057 | "m.f90", |
| 2058 | "module m\n implicit none\n private\n public :: make_t\n type, public :: result_t\n integer :: source = 0\n integer :: length = 0\n end type\ncontains\n function make_t() result(res)\n type(result_t) :: res\n res%source = 1\n res%length = 2\n end function\nend module\n", |
| 2059 | ); |
| 2060 | let user_src = write_program_in( |
| 2061 | &dir, |
| 2062 | "user.f90", |
| 2063 | "program p\n use m, only: make_t, result_t\n implicit none\n type(result_t) :: x\n x = make_t()\n if (x%source /= 1) error stop 1\n if (x%length /= 2) error stop 2\nend program\n", |
| 2064 | ); |
| 2065 | let mod_obj = dir.join("m.o"); |
| 2066 | let compile_mod = Command::new(compiler("armfortas")) |
| 2067 | .current_dir(&dir) |
| 2068 | .args([ |
| 2069 | "-c", |
| 2070 | "-J", |
| 2071 | dir.to_str().unwrap(), |
| 2072 | mod_src.to_str().unwrap(), |
| 2073 | "-o", |
| 2074 | mod_obj.to_str().unwrap(), |
| 2075 | ]) |
| 2076 | .output() |
| 2077 | .expect("module compile spawn failed"); |
| 2078 | assert!( |
| 2079 | compile_mod.status.success(), |
| 2080 | "module compile failed: {}", |
| 2081 | String::from_utf8_lossy(&compile_mod.stderr) |
| 2082 | ); |
| 2083 | |
| 2084 | let amod = dir.join("m.amod"); |
| 2085 | let amod_text = std::fs::read_to_string(&amod).expect("module .amod should exist"); |
| 2086 | assert!( |
| 2087 | amod_text.contains("@type result_t"), |
| 2088 | "public derived type should be exported to .amod: {}", |
| 2089 | amod_text |
| 2090 | ); |
| 2091 | assert!( |
| 2092 | amod_text.contains("@field source") && amod_text.contains("@field length"), |
| 2093 | "derived type layout should be exported to .amod: {}", |
| 2094 | amod_text |
| 2095 | ); |
| 2096 | |
| 2097 | let user_obj = dir.join("user.o"); |
| 2098 | let compile_user = Command::new(compiler("armfortas")) |
| 2099 | .current_dir(&dir) |
| 2100 | .args([ |
| 2101 | "-c", |
| 2102 | user_src.to_str().unwrap(), |
| 2103 | "-o", |
| 2104 | user_obj.to_str().unwrap(), |
| 2105 | ]) |
| 2106 | .output() |
| 2107 | .expect("user compile spawn failed"); |
| 2108 | assert!( |
| 2109 | compile_user.status.success(), |
| 2110 | "consumer compile should import the public derived type layout: {}", |
| 2111 | String::from_utf8_lossy(&compile_user.stderr) |
| 2112 | ); |
| 2113 | |
| 2114 | let _ = std::fs::remove_dir_all(&dir); |
| 2115 | } |
| 2116 | |
| 2117 | #[test] |
| 2118 | fn shared_compile_emits_amod_and_links_cleanly() { |
| 2119 | let dir = unique_dir("shared_mod"); |
| 2120 | let lib_src = write_program_in( |
| 2121 | &dir, |
| 2122 | "mylib.f90", |
| 2123 | "module m\ncontains\n integer function answer()\n answer = 42\n end function\nend module\n", |
| 2124 | ); |
| 2125 | let user_src = write_program_in( |
| 2126 | &dir, |
| 2127 | "user.f90", |
| 2128 | "program p\n use m\n print *, answer()\nend program\n", |
| 2129 | ); |
| 2130 | let dylib = dir.join("libmylib.dylib"); |
| 2131 | let shared = Command::new(compiler("armfortas")) |
| 2132 | .args([ |
| 2133 | "-shared", |
| 2134 | lib_src.to_str().unwrap(), |
| 2135 | "-o", |
| 2136 | dylib.to_str().unwrap(), |
| 2137 | ]) |
| 2138 | .output() |
| 2139 | .expect("shared compile spawn failed"); |
| 2140 | assert!( |
| 2141 | shared.status.success(), |
| 2142 | "shared compile failed: {}", |
| 2143 | String::from_utf8_lossy(&shared.stderr) |
| 2144 | ); |
| 2145 | assert!( |
| 2146 | dir.join("m.amod").exists(), |
| 2147 | "shared compile should emit m.amod" |
| 2148 | ); |
| 2149 | |
| 2150 | let exe = dir.join("use_m"); |
| 2151 | let dir_str = dir.to_str().unwrap(); |
| 2152 | let user = Command::new(compiler("armfortas")) |
| 2153 | .args([ |
| 2154 | "-I", |
| 2155 | dir_str, |
| 2156 | "-L", |
| 2157 | dir_str, |
| 2158 | "-rpath", |
| 2159 | dir_str, |
| 2160 | "-lmylib", |
| 2161 | user_src.to_str().unwrap(), |
| 2162 | "-o", |
| 2163 | exe.to_str().unwrap(), |
| 2164 | ]) |
| 2165 | .output() |
| 2166 | .expect("user compile spawn failed"); |
| 2167 | assert!( |
| 2168 | user.status.success(), |
| 2169 | "consumer link failed: {}", |
| 2170 | String::from_utf8_lossy(&user.stderr) |
| 2171 | ); |
| 2172 | |
| 2173 | let run = Command::new(&exe).output().expect("consumer run failed"); |
| 2174 | assert!( |
| 2175 | run.status.success(), |
| 2176 | "consumer run failed: {:?}", |
| 2177 | run.status |
| 2178 | ); |
| 2179 | let stdout = String::from_utf8_lossy(&run.stdout); |
| 2180 | assert!( |
| 2181 | stdout.trim().ends_with("42"), |
| 2182 | "unexpected output: {}", |
| 2183 | stdout |
| 2184 | ); |
| 2185 | let _ = std::fs::remove_dir_all(&dir); |
| 2186 | } |
| 2187 | |
| 2188 | #[test] |
| 2189 | fn verbose_flag_streams_phase_lines_to_stderr() { |
| 2190 | let src = write_program("program p\n print *, 1\nend program\n", "f90"); |
| 2191 | let out = unique_path("verbose", "bin"); |
| 2192 | let result = Command::new(compiler("armfortas")) |
| 2193 | .args(["-v", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 2194 | .output() |
| 2195 | .expect("spawn failed"); |
| 2196 | assert!(result.status.success()); |
| 2197 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 2198 | assert!( |
| 2199 | stderr.contains("preprocessing:"), |
| 2200 | "verbose missing preprocessing line: {}", |
| 2201 | stderr |
| 2202 | ); |
| 2203 | assert!( |
| 2204 | stderr.contains("codegen:"), |
| 2205 | "verbose missing codegen line: {}", |
| 2206 | stderr |
| 2207 | ); |
| 2208 | let _ = std::fs::remove_file(&out); |
| 2209 | let _ = std::fs::remove_file(&src); |
| 2210 | } |
| 2211 | |
| 2212 | #[test] |
| 2213 | fn time_report_prints_phase_table() { |
| 2214 | let src = write_program("program p\n print *, 1\nend program\n", "f90"); |
| 2215 | let out = unique_path("timer", "bin"); |
| 2216 | let result = Command::new(compiler("armfortas")) |
| 2217 | .args([ |
| 2218 | "--time-report", |
| 2219 | src.to_str().unwrap(), |
| 2220 | "-o", |
| 2221 | out.to_str().unwrap(), |
| 2222 | ]) |
| 2223 | .output() |
| 2224 | .expect("spawn failed"); |
| 2225 | assert!(result.status.success()); |
| 2226 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 2227 | assert!( |
| 2228 | stderr.contains("Phase"), |
| 2229 | "missing time-report header: {}", |
| 2230 | stderr |
| 2231 | ); |
| 2232 | assert!( |
| 2233 | stderr.contains("Total"), |
| 2234 | "missing time-report total: {}", |
| 2235 | stderr |
| 2236 | ); |
| 2237 | let _ = std::fs::remove_file(&out); |
| 2238 | let _ = std::fs::remove_file(&src); |
| 2239 | } |
| 2240 | |
| 2241 | #[test] |
| 2242 | fn time_report_prints_phase_table_on_error() { |
| 2243 | let src = write_program("program p\n error stop 'oops'\nend program\n", "f90"); |
| 2244 | let out = unique_path("timer_err", "bin"); |
| 2245 | let result = Command::new(compiler("armfortas")) |
| 2246 | .args([ |
| 2247 | "--time-report", |
| 2248 | "--std=f95", |
| 2249 | src.to_str().unwrap(), |
| 2250 | "-o", |
| 2251 | out.to_str().unwrap(), |
| 2252 | ]) |
| 2253 | .env("NO_COLOR", "1") |
| 2254 | .output() |
| 2255 | .expect("spawn failed"); |
| 2256 | assert!( |
| 2257 | !result.status.success(), |
| 2258 | "compile should fail under --std=f95" |
| 2259 | ); |
| 2260 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 2261 | assert!( |
| 2262 | stderr.contains("Phase"), |
| 2263 | "missing time-report header: {}", |
| 2264 | stderr |
| 2265 | ); |
| 2266 | assert!( |
| 2267 | stderr.contains("Total"), |
| 2268 | "missing time-report total: {}", |
| 2269 | stderr |
| 2270 | ); |
| 2271 | assert!( |
| 2272 | stderr.contains("sema"), |
| 2273 | "expected failing phase in report: {}", |
| 2274 | stderr |
| 2275 | ); |
| 2276 | let _ = std::fs::remove_file(&out); |
| 2277 | let _ = std::fs::remove_file(&src); |
| 2278 | } |
| 2279 | |
| 2280 | #[test] |
| 2281 | fn diagnostic_renders_source_line_and_caret() { |
| 2282 | let src = write_program("program p\n error stop 'oops'\nend program\n", "f90"); |
| 2283 | let out = unique_path("diag", "bin"); |
| 2284 | let result = Command::new(compiler("armfortas")) |
| 2285 | .args([ |
| 2286 | "--std=f95", |
| 2287 | src.to_str().unwrap(), |
| 2288 | "-o", |
| 2289 | out.to_str().unwrap(), |
| 2290 | ]) |
| 2291 | .env("NO_COLOR", "1") |
| 2292 | .output() |
| 2293 | .expect("spawn failed"); |
| 2294 | assert!(!result.status.success()); |
| 2295 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 2296 | // Header line uses the gfortran/clang gutter format. |
| 2297 | assert!( |
| 2298 | stderr.contains(":2:3: error:"), |
| 2299 | "missing standard error header: {}", |
| 2300 | stderr |
| 2301 | ); |
| 2302 | // Source line is shown with a numbered gutter (` 2 |`). |
| 2303 | assert!( |
| 2304 | stderr.contains("| error stop"), |
| 2305 | "missing source-line snippet: {}", |
| 2306 | stderr |
| 2307 | ); |
| 2308 | // Caret underline lives on a ` |` line. |
| 2309 | assert!( |
| 2310 | stderr.contains(" |"), |
| 2311 | "missing caret gutter: {}", |
| 2312 | stderr |
| 2313 | ); |
| 2314 | assert!(stderr.contains("^"), "missing caret marker: {}", stderr); |
| 2315 | let _ = std::fs::remove_file(&src); |
| 2316 | let _ = std::fs::remove_file(&out); |
| 2317 | } |
| 2318 | |
| 2319 | #[test] |
| 2320 | fn garbage_text_is_rejected_as_parse_error() { |
| 2321 | let src = write_program("this is garbage\n", "f90"); |
| 2322 | let out = unique_path("garbage", "bin"); |
| 2323 | let result = Command::new(compiler("armfortas")) |
| 2324 | .args([src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 2325 | .env("NO_COLOR", "1") |
| 2326 | .output() |
| 2327 | .expect("spawn failed"); |
| 2328 | assert!( |
| 2329 | !result.status.success(), |
| 2330 | "garbage text should fail to parse" |
| 2331 | ); |
| 2332 | assert_eq!( |
| 2333 | result.status.code(), |
| 2334 | Some(1), |
| 2335 | "garbage text should be a compile-time parse error" |
| 2336 | ); |
| 2337 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 2338 | assert!( |
| 2339 | stderr.contains("parse error:"), |
| 2340 | "expected parse-error header: {}", |
| 2341 | stderr |
| 2342 | ); |
| 2343 | assert!( |
| 2344 | stderr.contains("| this is garbage"), |
| 2345 | "expected source snippet for parse error: {}", |
| 2346 | stderr |
| 2347 | ); |
| 2348 | assert!( |
| 2349 | stderr.contains("^"), |
| 2350 | "expected parse-error caret: {}", |
| 2351 | stderr |
| 2352 | ); |
| 2353 | assert!( |
| 2354 | !stderr.contains("linker failed"), |
| 2355 | "garbage text should not reach the linker: {}", |
| 2356 | stderr |
| 2357 | ); |
| 2358 | let _ = std::fs::remove_file(&src); |
| 2359 | let _ = std::fs::remove_file(&out); |
| 2360 | } |
| 2361 | |
| 2362 | #[test] |
| 2363 | fn utf8_lexer_error_reports_character_and_caret() { |
| 2364 | let src = write_program("program p\n café = 1\nend program\n", "f90"); |
| 2365 | let out = unique_path("utf8", "bin"); |
| 2366 | let result = Command::new(compiler("armfortas")) |
| 2367 | .args([src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 2368 | .env("NO_COLOR", "1") |
| 2369 | .output() |
| 2370 | .expect("spawn failed"); |
| 2371 | assert!(!result.status.success(), "UTF-8 lexer error should fail"); |
| 2372 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 2373 | assert!( |
| 2374 | stderr.contains("lexer error: unexpected character: 'é'"), |
| 2375 | "expected full UTF-8 character in lexer diagnostic: {}", |
| 2376 | stderr |
| 2377 | ); |
| 2378 | assert!( |
| 2379 | stderr.contains("| café = 1"), |
| 2380 | "expected lexer source snippet: {}", |
| 2381 | stderr |
| 2382 | ); |
| 2383 | assert!(stderr.contains("^"), "expected lexer caret: {}", stderr); |
| 2384 | let _ = std::fs::remove_file(&src); |
| 2385 | let _ = std::fs::remove_file(&out); |
| 2386 | } |
| 2387 | |
| 2388 | #[test] |
| 2389 | fn bom_prefixed_source_compiles_cleanly() { |
| 2390 | let src = write_program("\u{feff}program p\n print *, 1\nend program\n", "f90"); |
| 2391 | let out = unique_path("bom", "o"); |
| 2392 | let result = Command::new(compiler("armfortas")) |
| 2393 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 2394 | .output() |
| 2395 | .expect("spawn failed"); |
| 2396 | assert!( |
| 2397 | result.status.success(), |
| 2398 | "BOM-prefixed source should compile: {}", |
| 2399 | String::from_utf8_lossy(&result.stderr) |
| 2400 | ); |
| 2401 | assert!( |
| 2402 | out.exists(), |
| 2403 | "BOM-prefixed compile should produce an object" |
| 2404 | ); |
| 2405 | let _ = std::fs::remove_file(&src); |
| 2406 | let _ = std::fs::remove_file(&out); |
| 2407 | } |
| 2408 | |
| 2409 | #[test] |
| 2410 | fn deeply_nested_expression_fails_gracefully() { |
| 2411 | let expr = format!("{}1{}", "(".repeat(1500), ")".repeat(1500)); |
| 2412 | let src = write_program( |
| 2413 | &format!("program p\n integer :: x\n x = {expr}\nend program\n"), |
| 2414 | "f90", |
| 2415 | ); |
| 2416 | let out = unique_path("deep_expr", "o"); |
| 2417 | let result = Command::new(compiler("armfortas")) |
| 2418 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 2419 | .env("NO_COLOR", "1") |
| 2420 | .output() |
| 2421 | .expect("spawn failed"); |
| 2422 | assert!( |
| 2423 | !result.status.success(), |
| 2424 | "deeply nested expression should be rejected" |
| 2425 | ); |
| 2426 | assert_eq!( |
| 2427 | result.status.code(), |
| 2428 | Some(1), |
| 2429 | "deep-expression overflow should stay a compile error" |
| 2430 | ); |
| 2431 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 2432 | assert!( |
| 2433 | stderr.contains("expression nesting exceeds parser limit"), |
| 2434 | "expected parser depth diagnostic: {}", |
| 2435 | stderr |
| 2436 | ); |
| 2437 | assert!( |
| 2438 | !stderr.contains("INTERNAL COMPILER ERROR"), |
| 2439 | "depth guard should avoid ICE path: {}", |
| 2440 | stderr |
| 2441 | ); |
| 2442 | let _ = std::fs::remove_file(&src); |
| 2443 | let _ = std::fs::remove_file(&out); |
| 2444 | } |
| 2445 | |
| 2446 | #[test] |
| 2447 | fn diagnostic_gutter_stays_aligned_for_six_digit_line_numbers() { |
| 2448 | let mut src_text = "! filler\n".repeat(100_000); |
| 2449 | src_text.push_str("program p\n error stop 'oops'\nend program\n"); |
| 2450 | let src = write_program(&src_text, "f90"); |
| 2451 | let out = unique_path("bigline", "bin"); |
| 2452 | let result = Command::new(compiler("armfortas")) |
| 2453 | .args([ |
| 2454 | "--std=f95", |
| 2455 | src.to_str().unwrap(), |
| 2456 | "-o", |
| 2457 | out.to_str().unwrap(), |
| 2458 | ]) |
| 2459 | .env("NO_COLOR", "1") |
| 2460 | .output() |
| 2461 | .expect("spawn failed"); |
| 2462 | assert!( |
| 2463 | !result.status.success(), |
| 2464 | "compile should fail under --std=f95" |
| 2465 | ); |
| 2466 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 2467 | let lines: Vec<_> = stderr.lines().collect(); |
| 2468 | let idx = lines |
| 2469 | .iter() |
| 2470 | .position(|line| line.contains("100002 |")) |
| 2471 | .expect("missing six-digit source gutter"); |
| 2472 | let source_line = lines[idx]; |
| 2473 | let caret_line = *lines.get(idx + 1).expect("missing caret line"); |
| 2474 | assert_eq!( |
| 2475 | source_line.find('|'), |
| 2476 | caret_line.find('|'), |
| 2477 | "source and caret gutters should stay aligned:\n{}", |
| 2478 | stderr |
| 2479 | ); |
| 2480 | let _ = std::fs::remove_file(&src); |
| 2481 | let _ = std::fs::remove_file(&out); |
| 2482 | } |
| 2483 | |
| 2484 | #[test] |
| 2485 | fn integer_pow_overflow_is_diagnosed() { |
| 2486 | let src = write_program("program p\n print *, 2**200\nend program\n", "f90"); |
| 2487 | let out = unique_path("pow_overflow", "bin"); |
| 2488 | let result = Command::new(compiler("armfortas")) |
| 2489 | .args([src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 2490 | .env("NO_COLOR", "1") |
| 2491 | .output() |
| 2492 | .expect("spawn failed"); |
| 2493 | assert!( |
| 2494 | !result.status.success(), |
| 2495 | "constant integer overflow should be rejected" |
| 2496 | ); |
| 2497 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 2498 | assert!( |
| 2499 | stderr.contains("compile-time integer overflow"), |
| 2500 | "expected integer overflow diagnostic: {}", |
| 2501 | stderr |
| 2502 | ); |
| 2503 | let _ = std::fs::remove_file(&src); |
| 2504 | let _ = std::fs::remove_file(&out); |
| 2505 | } |
| 2506 | |
| 2507 | #[test] |
| 2508 | fn parameter_integer_literal_overflow_is_diagnosed() { |
| 2509 | let src = write_program( |
| 2510 | "program p\n integer, parameter :: x = -2147483649\n print *, x\nend program\n", |
| 2511 | "f90", |
| 2512 | ); |
| 2513 | let out = unique_path("param_overflow", "bin"); |
| 2514 | let result = Command::new(compiler("armfortas")) |
| 2515 | .args([src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 2516 | .env("NO_COLOR", "1") |
| 2517 | .output() |
| 2518 | .expect("spawn failed"); |
| 2519 | assert!( |
| 2520 | !result.status.success(), |
| 2521 | "parameter overflow should be rejected" |
| 2522 | ); |
| 2523 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 2524 | assert!( |
| 2525 | stderr.contains("compile-time integer overflow"), |
| 2526 | "expected parameter overflow diagnostic: {}", |
| 2527 | stderr |
| 2528 | ); |
| 2529 | let _ = std::fs::remove_file(&src); |
| 2530 | let _ = std::fs::remove_file(&out); |
| 2531 | } |
| 2532 | |
| 2533 | #[test] |
| 2534 | fn integer_division_by_zero_is_diagnosed() { |
| 2535 | let src = write_program( |
| 2536 | "program p\n integer, parameter :: x = 1 / 0\n print *, x\nend program\n", |
| 2537 | "f90", |
| 2538 | ); |
| 2539 | let out = unique_path("div_zero", "bin"); |
| 2540 | let result = Command::new(compiler("armfortas")) |
| 2541 | .args([src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 2542 | .env("NO_COLOR", "1") |
| 2543 | .output() |
| 2544 | .expect("spawn failed"); |
| 2545 | assert!( |
| 2546 | !result.status.success(), |
| 2547 | "compile-time integer division by zero should be rejected" |
| 2548 | ); |
| 2549 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 2550 | assert!( |
| 2551 | stderr.contains("compile-time integer division by zero"), |
| 2552 | "expected division-by-zero diagnostic: {}", |
| 2553 | stderr |
| 2554 | ); |
| 2555 | let _ = std::fs::remove_file(&src); |
| 2556 | let _ = std::fs::remove_file(&out); |
| 2557 | } |
| 2558 | |
| 2559 | #[test] |
| 2560 | fn no_color_env_suppresses_ansi_escapes() { |
| 2561 | let src = write_program("program p\n error stop 'x'\nend program\n", "f90"); |
| 2562 | let out = unique_path("nocolor", "bin"); |
| 2563 | let result = Command::new(compiler("armfortas")) |
| 2564 | .args([ |
| 2565 | "--std=f95", |
| 2566 | src.to_str().unwrap(), |
| 2567 | "-o", |
| 2568 | out.to_str().unwrap(), |
| 2569 | ]) |
| 2570 | .env("NO_COLOR", "1") |
| 2571 | .env_remove("CLICOLOR_FORCE") |
| 2572 | .output() |
| 2573 | .expect("spawn failed"); |
| 2574 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 2575 | assert!( |
| 2576 | !stderr.contains('\x1b'), |
| 2577 | "NO_COLOR must suppress ANSI escapes: {:?}", |
| 2578 | stderr |
| 2579 | ); |
| 2580 | let _ = std::fs::remove_file(&src); |
| 2581 | let _ = std::fs::remove_file(&out); |
| 2582 | } |
| 2583 | |
| 2584 | #[test] |
| 2585 | fn clicolor_force_enables_ansi_even_off_a_tty() { |
| 2586 | let src = write_program("program p\n error stop 'x'\nend program\n", "f90"); |
| 2587 | let out = unique_path("forcecolor", "bin"); |
| 2588 | let result = Command::new(compiler("armfortas")) |
| 2589 | .args([ |
| 2590 | "--std=f95", |
| 2591 | src.to_str().unwrap(), |
| 2592 | "-o", |
| 2593 | out.to_str().unwrap(), |
| 2594 | ]) |
| 2595 | .env("CLICOLOR_FORCE", "1") |
| 2596 | .env_remove("NO_COLOR") |
| 2597 | .output() |
| 2598 | .expect("spawn failed"); |
| 2599 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 2600 | assert!( |
| 2601 | stderr.contains('\x1b'), |
| 2602 | "CLICOLOR_FORCE must produce ANSI escapes: {:?}", |
| 2603 | stderr |
| 2604 | ); |
| 2605 | let _ = std::fs::remove_file(&src); |
| 2606 | let _ = std::fs::remove_file(&out); |
| 2607 | } |
| 2608 | |
| 2609 | #[test] |
| 2610 | fn fimplicit_none_rejects_implicitly_typed_use() { |
| 2611 | let src = write_program("program p\n i = 5\n print *, i\nend program\n", "f90"); |
| 2612 | let out = unique_path("fimplicit", "bin"); |
| 2613 | let result = Command::new(compiler("armfortas")) |
| 2614 | .args([ |
| 2615 | "-fimplicit-none", |
| 2616 | src.to_str().unwrap(), |
| 2617 | "-o", |
| 2618 | out.to_str().unwrap(), |
| 2619 | ]) |
| 2620 | .env("NO_COLOR", "1") |
| 2621 | .output() |
| 2622 | .expect("spawn failed"); |
| 2623 | assert!( |
| 2624 | !result.status.success(), |
| 2625 | "-fimplicit-none should reject undeclared 'i'" |
| 2626 | ); |
| 2627 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 2628 | assert!( |
| 2629 | stderr.contains("'i'") && stderr.contains("IMPLICIT NONE is active"), |
| 2630 | "expected implicit-none diagnostic: {}", |
| 2631 | stderr |
| 2632 | ); |
| 2633 | let _ = std::fs::remove_file(&src); |
| 2634 | } |
| 2635 | |
| 2636 | #[test] |
| 2637 | fn fimplicit_none_respects_explicit_implicit_rules() { |
| 2638 | let src = write_program( |
| 2639 | "program p\n implicit integer (i-n)\n i = 5\n print *, i\nend program\n", |
| 2640 | "f90", |
| 2641 | ); |
| 2642 | let out = unique_path("fimplicit_explicit", "bin"); |
| 2643 | let result = Command::new(compiler("armfortas")) |
| 2644 | .args([ |
| 2645 | "-fimplicit-none", |
| 2646 | src.to_str().unwrap(), |
| 2647 | "-o", |
| 2648 | out.to_str().unwrap(), |
| 2649 | ]) |
| 2650 | .output() |
| 2651 | .expect("spawn failed"); |
| 2652 | assert!( |
| 2653 | result.status.success(), |
| 2654 | "explicit IMPLICIT should win over -fimplicit-none: {}", |
| 2655 | String::from_utf8_lossy(&result.stderr) |
| 2656 | ); |
| 2657 | let run = Command::new(&out).output().expect("run failed"); |
| 2658 | let stdout = String::from_utf8_lossy(&run.stdout); |
| 2659 | assert!( |
| 2660 | stdout.trim().ends_with('5'), |
| 2661 | "expected explicit implicit typing to remain active: {}", |
| 2662 | stdout |
| 2663 | ); |
| 2664 | let _ = std::fs::remove_file(&out); |
| 2665 | let _ = std::fs::remove_file(&src); |
| 2666 | } |
| 2667 | |
| 2668 | #[test] |
| 2669 | fn fdefault_integer_8_changes_default_kind() { |
| 2670 | let src = write_program( |
| 2671 | "program p\n integer :: x\n print *, kind(x)\nend program\n", |
| 2672 | "f90", |
| 2673 | ); |
| 2674 | let out = unique_path("defint", "bin"); |
| 2675 | let result = Command::new(compiler("armfortas")) |
| 2676 | .args([ |
| 2677 | "-fdefault-integer-8", |
| 2678 | src.to_str().unwrap(), |
| 2679 | "-o", |
| 2680 | out.to_str().unwrap(), |
| 2681 | ]) |
| 2682 | .output() |
| 2683 | .expect("spawn failed"); |
| 2684 | assert!( |
| 2685 | result.status.success(), |
| 2686 | "-fdefault-integer-8 compile failed: {}", |
| 2687 | String::from_utf8_lossy(&result.stderr) |
| 2688 | ); |
| 2689 | let run = Command::new(&out).output().expect("run failed"); |
| 2690 | let stdout = String::from_utf8_lossy(&run.stdout); |
| 2691 | assert!( |
| 2692 | stdout.trim().ends_with('8'), |
| 2693 | "expected kind 8: {:?}", |
| 2694 | stdout |
| 2695 | ); |
| 2696 | let _ = std::fs::remove_file(&out); |
| 2697 | let _ = std::fs::remove_file(&src); |
| 2698 | } |
| 2699 | |
| 2700 | #[test] |
| 2701 | fn fdefault_real_8_changes_default_kind() { |
| 2702 | let src = write_program( |
| 2703 | "program p\n real :: y\n print *, kind(y)\nend program\n", |
| 2704 | "f90", |
| 2705 | ); |
| 2706 | let out = unique_path("defreal", "bin"); |
| 2707 | let result = Command::new(compiler("armfortas")) |
| 2708 | .args([ |
| 2709 | "-fdefault-real-8", |
| 2710 | src.to_str().unwrap(), |
| 2711 | "-o", |
| 2712 | out.to_str().unwrap(), |
| 2713 | ]) |
| 2714 | .output() |
| 2715 | .expect("spawn failed"); |
| 2716 | assert!(result.status.success()); |
| 2717 | let run = Command::new(&out).output().expect("run failed"); |
| 2718 | let stdout = String::from_utf8_lossy(&run.stdout); |
| 2719 | assert!( |
| 2720 | stdout.trim().ends_with('8'), |
| 2721 | "expected kind 8: {:?}", |
| 2722 | stdout |
| 2723 | ); |
| 2724 | let _ = std::fs::remove_file(&out); |
| 2725 | let _ = std::fs::remove_file(&src); |
| 2726 | } |
| 2727 | |
| 2728 | #[test] |
| 2729 | fn afs_runtime_path_env_overrides_runtime_discovery() { |
| 2730 | // Point $AFS_RUNTIME_PATH at a directory that DOES contain the |
| 2731 | // real runtime and verify compilation still succeeds — exercises |
| 2732 | // the override branch end-to-end without hiding the real runtime. |
| 2733 | let rt = PathBuf::from("target/release/libarmfortas_rt.a"); |
| 2734 | if !rt.exists() { |
| 2735 | // Skip silently when running off a tree that only has a |
| 2736 | // debug runtime — CI has both; a contributor's fresh clone |
| 2737 | // with only `cargo build` will hit release. |
| 2738 | return; |
| 2739 | } |
| 2740 | let rt_dir = rt.parent().unwrap().to_path_buf(); |
| 2741 | let src = write_program("program p\n print *, 11\nend program\n", "f90"); |
| 2742 | let out = unique_path("rtpath", "bin"); |
| 2743 | let result = Command::new(compiler("armfortas")) |
| 2744 | .args([src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 2745 | .env("AFS_RUNTIME_PATH", &rt_dir) |
| 2746 | .output() |
| 2747 | .expect("spawn failed"); |
| 2748 | assert!( |
| 2749 | result.status.success(), |
| 2750 | "AFS_RUNTIME_PATH-directed compile failed: {}", |
| 2751 | String::from_utf8_lossy(&result.stderr) |
| 2752 | ); |
| 2753 | let _ = std::fs::remove_file(&out); |
| 2754 | let _ = std::fs::remove_file(&src); |
| 2755 | } |
| 2756 | |
| 2757 | #[test] |
| 2758 | fn missing_input_file_reports_io_error() { |
| 2759 | let result = Command::new(compiler("armfortas")) |
| 2760 | .args(["/nonexistent/path/source.f90"]) |
| 2761 | .output() |
| 2762 | .expect("spawn failed"); |
| 2763 | assert!(!result.status.success(), "missing input should fail"); |
| 2764 | // Per sprint 32 #6 exit-code spec: I/O errors (cannot read input) |
| 2765 | // map to exit code 3. The driver categorises by error message |
| 2766 | // text today; a structured error type is sprint 32 #507. |
| 2767 | assert_eq!( |
| 2768 | result.status.code(), |
| 2769 | Some(3), |
| 2770 | "missing input should map to exit code 3 (I/O error), got: {:?}", |
| 2771 | result.status |
| 2772 | ); |
| 2773 | } |
| 2774 | |
| 2775 | #[test] |
| 2776 | fn entry_statement_reports_not_implemented() { |
| 2777 | let src = write_program( |
| 2778 | "subroutine f(x)\n integer :: x\n entry g(y)\nend subroutine\n", |
| 2779 | "f90", |
| 2780 | ); |
| 2781 | let out = unique_path("entry_stmt", "o"); |
| 2782 | let result = Command::new(compiler("armfortas")) |
| 2783 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 2784 | .env("NO_COLOR", "1") |
| 2785 | .output() |
| 2786 | .expect("spawn failed"); |
| 2787 | assert!(!result.status.success(), "ENTRY should not compile yet"); |
| 2788 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 2789 | assert!( |
| 2790 | stderr.contains("ENTRY statements are recognized but not yet implemented"), |
| 2791 | "expected explicit ENTRY diagnostic: {}", |
| 2792 | stderr |
| 2793 | ); |
| 2794 | let _ = std::fs::remove_file(&out); |
| 2795 | let _ = std::fs::remove_file(&src); |
| 2796 | } |
| 2797 | |
| 2798 | #[test] |
| 2799 | fn coarray_declaration_reports_not_implemented() { |
| 2800 | let src = write_program("program p\n integer :: x[*]\nend program\n", "f90"); |
| 2801 | let out = unique_path("coarray_decl", "o"); |
| 2802 | let result = Command::new(compiler("armfortas")) |
| 2803 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 2804 | .env("NO_COLOR", "1") |
| 2805 | .output() |
| 2806 | .expect("spawn failed"); |
| 2807 | assert!( |
| 2808 | !result.status.success(), |
| 2809 | "coarray declarations should fail honestly" |
| 2810 | ); |
| 2811 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 2812 | assert!( |
| 2813 | stderr.contains("coarray declarations are recognized but not yet implemented"), |
| 2814 | "expected explicit coarray declaration diagnostic: {}", |
| 2815 | stderr |
| 2816 | ); |
| 2817 | let _ = std::fs::remove_file(&out); |
| 2818 | let _ = std::fs::remove_file(&src); |
| 2819 | } |
| 2820 | |
| 2821 | #[test] |
| 2822 | fn coarray_sync_reports_not_implemented() { |
| 2823 | let src = write_program("program p\n sync all\nend program\n", "f90"); |
| 2824 | let out = unique_path("coarray_sync", "o"); |
| 2825 | let result = Command::new(compiler("armfortas")) |
| 2826 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 2827 | .env("NO_COLOR", "1") |
| 2828 | .output() |
| 2829 | .expect("spawn failed"); |
| 2830 | assert!( |
| 2831 | !result.status.success(), |
| 2832 | "coarray SYNC should fail honestly" |
| 2833 | ); |
| 2834 | let stderr = String::from_utf8_lossy(&result.stderr); |
| 2835 | assert!( |
| 2836 | stderr.contains("coarray SYNC statements are recognized but not yet implemented"), |
| 2837 | "expected explicit coarray SYNC diagnostic: {}", |
| 2838 | stderr |
| 2839 | ); |
| 2840 | let _ = std::fs::remove_file(&out); |
| 2841 | let _ = std::fs::remove_file(&src); |
| 2842 | } |
| 2843 | |
| 2844 | #[test] |
| 2845 | fn procedure_pointer_decl_compiles_through_wrapper_calls() { |
| 2846 | let src = write_program( |
| 2847 | "module m\n implicit none\n abstract interface\n logical function pred(x)\n integer, intent(in) :: x\n end function pred\n subroutine act(x)\n integer, intent(in) :: x\n end subroutine act\n end interface\n procedure(pred), pointer :: p => null()\n procedure(act), pointer :: q => null()\ncontains\n logical function ok(x)\n integer, intent(in) :: x\n ok = .false.\n if (associated(p)) ok = p(x)\n end function ok\n\n subroutine run(x)\n integer, intent(in) :: x\n if (associated(q)) call q(x)\n end subroutine run\nend module\n", |
| 2848 | "f90", |
| 2849 | ); |
| 2850 | let out = unique_path("procedure_ptr_decl", "o"); |
| 2851 | let result = Command::new(compiler("armfortas")) |
| 2852 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 2853 | .env("NO_COLOR", "1") |
| 2854 | .output() |
| 2855 | .expect("spawn failed"); |
| 2856 | assert!( |
| 2857 | result.status.success(), |
| 2858 | "procedure-pointer declarations should compile: {}", |
| 2859 | String::from_utf8_lossy(&result.stderr) |
| 2860 | ); |
| 2861 | let _ = std::fs::remove_file(&out); |
| 2862 | let _ = std::fs::remove_file(&src); |
| 2863 | } |
| 2864 | |
| 2865 | #[test] |
| 2866 | fn procedure_pointer_calls_and_assignment_run_indirectly() { |
| 2867 | let src = write_program( |
| 2868 | "module m\n implicit none\n abstract interface\n integer function pred(x)\n integer, intent(in) :: x\n end function pred\n subroutine act(x)\n integer, intent(inout) :: x\n end subroutine act\n end interface\n procedure(pred), pointer :: p => null()\n procedure(act), pointer :: q => null()\ncontains\n integer function twice(x)\n integer, intent(in) :: x\n twice = x * 2\n end function twice\n\n subroutine bump(x)\n integer, intent(inout) :: x\n x = x + 1\n end subroutine bump\n\n subroutine init()\n p => twice\n q => bump\n end subroutine init\nend module\n\nprogram main\n use m\n implicit none\n integer :: x\n call init()\n x = p(3)\n call q(x)\n print *, x\nend program main\n", |
| 2869 | "f90", |
| 2870 | ); |
| 2871 | let out = unique_path("procedure_ptr_run", "s"); |
| 2872 | let compile = Command::new(compiler("armfortas")) |
| 2873 | .args(["-S", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 2874 | .env("NO_COLOR", "1") |
| 2875 | .output() |
| 2876 | .expect("spawn failed"); |
| 2877 | assert!( |
| 2878 | compile.status.success(), |
| 2879 | "procedure-pointer indirect call program should lower to assembly: {}", |
| 2880 | String::from_utf8_lossy(&compile.stderr) |
| 2881 | ); |
| 2882 | let asm = std::fs::read_to_string(&out).expect("cannot read indirect-call assembly"); |
| 2883 | assert!( |
| 2884 | asm.contains("blr "), |
| 2885 | "procedure-pointer calls should lower to BLR: {}", |
| 2886 | asm |
| 2887 | ); |
| 2888 | assert!( |
| 2889 | asm.contains("_twice@PAGE") && asm.contains("_bump@PAGE"), |
| 2890 | "procedure-pointer assignment should materialize callee addresses: {}", |
| 2891 | asm |
| 2892 | ); |
| 2893 | assert!( |
| 2894 | !asm.contains("bl _p") && !asm.contains("bl _q"), |
| 2895 | "procedure-pointer calls should not lower as direct symbol calls: {}", |
| 2896 | asm |
| 2897 | ); |
| 2898 | |
| 2899 | let _ = std::fs::remove_file(&out); |
| 2900 | let _ = std::fs::remove_file(&src); |
| 2901 | } |
| 2902 | |
| 2903 | #[test] |
| 2904 | fn procedure_pointer_module_export_survives_amod_import() { |
| 2905 | let dir = unique_dir("procptr_amod"); |
| 2906 | let mod_src = write_program_in( |
| 2907 | &dir, |
| 2908 | "control_flow.f90", |
| 2909 | "module control_flow\n implicit none\n abstract interface\n subroutine evaluate_condition_interface(n)\n integer, intent(inout) :: n\n end subroutine\n end interface\n procedure(evaluate_condition_interface), pointer, public :: evaluate_condition => null()\nend module\n", |
| 2910 | ); |
| 2911 | let user_src = write_program_in( |
| 2912 | &dir, |
| 2913 | "executor.f90", |
| 2914 | "module executor\n implicit none\ncontains\n subroutine init_control_flow_callbacks()\n use control_flow\n evaluate_condition => evaluate_condition_impl\n end subroutine\n\n subroutine evaluate_condition_impl(n)\n integer, intent(inout) :: n\n n = n + 1\n end subroutine\nend module\n", |
| 2915 | ); |
| 2916 | |
| 2917 | let mod_obj = dir.join("control_flow.o"); |
| 2918 | let compile_mod = Command::new(compiler("armfortas")) |
| 2919 | .current_dir(&dir) |
| 2920 | .args([ |
| 2921 | "-c", |
| 2922 | "-J", |
| 2923 | dir.to_str().unwrap(), |
| 2924 | mod_src.to_str().unwrap(), |
| 2925 | "-o", |
| 2926 | mod_obj.to_str().unwrap(), |
| 2927 | ]) |
| 2928 | .output() |
| 2929 | .expect("module compile spawn failed"); |
| 2930 | assert!( |
| 2931 | compile_mod.status.success(), |
| 2932 | "procedure-pointer module should compile: {}", |
| 2933 | String::from_utf8_lossy(&compile_mod.stderr) |
| 2934 | ); |
| 2935 | |
| 2936 | let user_obj = dir.join("executor.o"); |
| 2937 | let compile_user = Command::new(compiler("armfortas")) |
| 2938 | .current_dir(&dir) |
| 2939 | .args([ |
| 2940 | "-c", |
| 2941 | "-I", |
| 2942 | dir.to_str().unwrap(), |
| 2943 | "-J", |
| 2944 | dir.to_str().unwrap(), |
| 2945 | user_src.to_str().unwrap(), |
| 2946 | "-o", |
| 2947 | user_obj.to_str().unwrap(), |
| 2948 | ]) |
| 2949 | .output() |
| 2950 | .expect("user compile spawn failed"); |
| 2951 | assert!( |
| 2952 | compile_user.status.success(), |
| 2953 | "imported module procedure pointers should survive .amod export/import: {}", |
| 2954 | String::from_utf8_lossy(&compile_user.stderr) |
| 2955 | ); |
| 2956 | |
| 2957 | let _ = std::fs::remove_dir_all(&dir); |
| 2958 | } |
| 2959 | |
| 2960 | #[test] |
| 2961 | fn char_intrinsics_and_transfer_lower_without_raw_symbols() { |
| 2962 | let src = write_program( |
| 2963 | "module m\n use iso_c_binding, only: c_funptr, c_intptr_t\ncontains\n subroutine s(buf, mask, ok)\n character(len=:), allocatable, intent(inout) :: buf\n logical, intent(in) :: mask\n logical, intent(out) :: ok\n type(c_funptr) :: sig_ign\n if (allocated(buf)) then\n ok = lgt(trim(buf), 'a')\n else\n ok = .false.\n end if\n ok = ok .or. any(buf(1:1) == ['!', '?'])\n buf = merge(buf // new_line('a'), '?', mask)\n sig_ign = transfer(1_c_intptr_t, sig_ign)\n end subroutine s\nend module m\n", |
| 2964 | "f90", |
| 2965 | ); |
| 2966 | let out = unique_path("char_intrinsics_link", "o"); |
| 2967 | let compile = Command::new(compiler("armfortas")) |
| 2968 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 2969 | .env("NO_COLOR", "1") |
| 2970 | .output() |
| 2971 | .expect("char intrinsic compile failed to spawn"); |
| 2972 | assert!( |
| 2973 | compile.status.success(), |
| 2974 | "char intrinsic compile failed: {}", |
| 2975 | String::from_utf8_lossy(&compile.stderr) |
| 2976 | ); |
| 2977 | |
| 2978 | let undefined = undefined_symbols(&out); |
| 2979 | assert!( |
| 2980 | undefined.iter().any(|sym| sym == "_afs_string_allocated"), |
| 2981 | "deferred-char ALLOCATED() should lower to the string runtime: {:?}", |
| 2982 | undefined |
| 2983 | ); |
| 2984 | assert!( |
| 2985 | undefined.iter().any(|sym| sym == "_afs_lgt"), |
| 2986 | "LGT should lower to the string runtime: {:?}", |
| 2987 | undefined |
| 2988 | ); |
| 2989 | assert!( |
| 2990 | !undefined.iter().any(|sym| { |
| 2991 | matches!( |
| 2992 | sym.as_str(), |
| 2993 | "_allocated" | "_any" | "_merge" | "_new_line" | "_transfer" | "_lgt" |
| 2994 | ) |
| 2995 | }), |
| 2996 | "char/link intrinsics should not escape as raw symbols: {:?}", |
| 2997 | undefined |
| 2998 | ); |
| 2999 | |
| 3000 | let _ = std::fs::remove_file(&out); |
| 3001 | let _ = std::fs::remove_file(&src); |
| 3002 | } |
| 3003 | |
| 3004 | #[test] |
| 3005 | fn deferred_char_allocatable_dummy_uses_descriptor_abi() { |
| 3006 | let src = write_program( |
| 3007 | "module m\ncontains\n subroutine grow(buf, cap, content_len)\n character(len=:), allocatable, intent(inout) :: buf\n integer, intent(inout) :: cap\n integer, intent(in) :: content_len\n character(len=:), allocatable :: tmp\n integer :: new_cap\n new_cap = cap * 2\n allocate(character(len=new_cap) :: tmp)\n if (content_len > 0) tmp(1:content_len) = buf(1:content_len)\n call move_alloc(tmp, buf)\n cap = new_cap\n end subroutine\nend module\n", |
| 3008 | "f90", |
| 3009 | ); |
| 3010 | let out = unique_path("deferred_char_dummy", "s"); |
| 3011 | let compile = Command::new(compiler("armfortas")) |
| 3012 | .args(["-S", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 3013 | .env("NO_COLOR", "1") |
| 3014 | .output() |
| 3015 | .expect("spawn failed"); |
| 3016 | assert!( |
| 3017 | compile.status.success(), |
| 3018 | "deferred-length allocatable character dummy should lower cleanly: {}", |
| 3019 | String::from_utf8_lossy(&compile.stderr) |
| 3020 | ); |
| 3021 | let asm = std::fs::read_to_string(&out).expect("cannot read deferred-char dummy assembly"); |
| 3022 | assert!( |
| 3023 | asm.contains("bl _afs_move_alloc_string"), |
| 3024 | "MOVE_ALLOC on deferred-length character dummies should call the string runtime: {}", |
| 3025 | asm |
| 3026 | ); |
| 3027 | assert!( |
| 3028 | !asm.contains("bl _move_alloc"), |
| 3029 | "deferred-length character MOVE_ALLOC should not escape as a raw external call: {}", |
| 3030 | asm |
| 3031 | ); |
| 3032 | assert!( |
| 3033 | !asm.contains("bl _buf"), |
| 3034 | "substringing a deferred-length dummy should not lower as a fake function call: {}", |
| 3035 | asm |
| 3036 | ); |
| 3037 | |
| 3038 | let _ = std::fs::remove_file(&out); |
| 3039 | let _ = std::fs::remove_file(&src); |
| 3040 | } |
| 3041 | |
| 3042 | #[test] |
| 3043 | fn use_renamed_procedure_call_uses_remote_symbol() { |
| 3044 | let dir = unique_dir("use_rename_proc"); |
| 3045 | let mod_src = write_program_in( |
| 3046 | &dir, |
| 3047 | "m.f90", |
| 3048 | "module m\ncontains\n subroutine set_shell_variable()\n end subroutine set_shell_variable\nend module m\n", |
| 3049 | ); |
| 3050 | let user_src = write_program_in( |
| 3051 | &dir, |
| 3052 | "user.f90", |
| 3053 | "module user\ncontains\n subroutine run()\n use m, only: var_set_shell_variable => set_shell_variable\n call var_set_shell_variable()\n end subroutine run\nend module user\n", |
| 3054 | ); |
| 3055 | |
| 3056 | let mod_obj = dir.join("m.o"); |
| 3057 | let compile_mod = Command::new(compiler("armfortas")) |
| 3058 | .current_dir(&dir) |
| 3059 | .args([ |
| 3060 | "-c", |
| 3061 | "-J", |
| 3062 | dir.to_str().unwrap(), |
| 3063 | mod_src.to_str().unwrap(), |
| 3064 | "-o", |
| 3065 | mod_obj.to_str().unwrap(), |
| 3066 | ]) |
| 3067 | .output() |
| 3068 | .expect("rename module compile spawn failed"); |
| 3069 | assert!( |
| 3070 | compile_mod.status.success(), |
| 3071 | "rename source module should compile: {}", |
| 3072 | String::from_utf8_lossy(&compile_mod.stderr) |
| 3073 | ); |
| 3074 | |
| 3075 | let user_obj = dir.join("user.o"); |
| 3076 | let compile_user = Command::new(compiler("armfortas")) |
| 3077 | .current_dir(&dir) |
| 3078 | .args([ |
| 3079 | "-c", |
| 3080 | "-I", |
| 3081 | dir.to_str().unwrap(), |
| 3082 | "-J", |
| 3083 | dir.to_str().unwrap(), |
| 3084 | user_src.to_str().unwrap(), |
| 3085 | "-o", |
| 3086 | user_obj.to_str().unwrap(), |
| 3087 | ]) |
| 3088 | .output() |
| 3089 | .expect("rename user compile spawn failed"); |
| 3090 | assert!( |
| 3091 | compile_user.status.success(), |
| 3092 | "USE-renamed procedure call should compile: {}", |
| 3093 | String::from_utf8_lossy(&compile_user.stderr) |
| 3094 | ); |
| 3095 | |
| 3096 | let undefined = undefined_symbols(&user_obj); |
| 3097 | assert!( |
| 3098 | undefined |
| 3099 | .iter() |
| 3100 | .any(|sym| sym == "_afs_modproc_m_set_shell_variable"), |
| 3101 | "USE rename should call the imported procedure symbol: {:?}", |
| 3102 | undefined |
| 3103 | ); |
| 3104 | assert!( |
| 3105 | !undefined.iter().any(|sym| sym == "_var_set_shell_variable"), |
| 3106 | "USE rename should not lower to the local alias as a link symbol: {:?}", |
| 3107 | undefined |
| 3108 | ); |
| 3109 | |
| 3110 | let _ = std::fs::remove_dir_all(&dir); |
| 3111 | } |
| 3112 | |
| 3113 | #[test] |
| 3114 | fn linked_binary_carries_uuid_and_launches() { |
| 3115 | let dir = unique_dir("linked_binary_uuid"); |
| 3116 | let src = write_program_in( |
| 3117 | &dir, |
| 3118 | "hello.f90", |
| 3119 | "program hello\n print *, 42\nend program hello\n", |
| 3120 | ); |
| 3121 | |
| 3122 | let exe = dir.join("hello.bin"); |
| 3123 | let compile = Command::new(compiler("armfortas")) |
| 3124 | .current_dir(&dir) |
| 3125 | .args([src.to_str().unwrap(), "-o", exe.to_str().unwrap()]) |
| 3126 | .output() |
| 3127 | .expect("hello compile spawn failed"); |
| 3128 | assert!( |
| 3129 | compile.status.success(), |
| 3130 | "linked hello should compile: {}", |
| 3131 | String::from_utf8_lossy(&compile.stderr) |
| 3132 | ); |
| 3133 | |
| 3134 | let otool = Command::new("otool") |
| 3135 | .args(["-l", exe.to_str().unwrap()]) |
| 3136 | .output() |
| 3137 | .expect("otool spawn failed"); |
| 3138 | assert!( |
| 3139 | otool.status.success(), |
| 3140 | "otool should inspect linked hello: {}", |
| 3141 | String::from_utf8_lossy(&otool.stderr) |
| 3142 | ); |
| 3143 | let load_commands = String::from_utf8_lossy(&otool.stdout); |
| 3144 | assert!( |
| 3145 | load_commands.contains("LC_UUID"), |
| 3146 | "linked hello should carry LC_UUID so dyld accepts it:\n{}", |
| 3147 | load_commands |
| 3148 | ); |
| 3149 | |
| 3150 | let run = Command::new(&exe) |
| 3151 | .current_dir(&dir) |
| 3152 | .output() |
| 3153 | .expect("hello run spawn failed"); |
| 3154 | assert!( |
| 3155 | run.status.success(), |
| 3156 | "linked hello should launch successfully:\nstdout:\n{}\nstderr:\n{}", |
| 3157 | String::from_utf8_lossy(&run.stdout), |
| 3158 | String::from_utf8_lossy(&run.stderr) |
| 3159 | ); |
| 3160 | |
| 3161 | let _ = std::fs::remove_dir_all(&dir); |
| 3162 | } |
| 3163 | |
| 3164 | #[test] |
| 3165 | fn same_named_module_helpers_link_without_colliding() { |
| 3166 | let dir = unique_dir("module_helper_link_names"); |
| 3167 | let mod_a = write_program_in( |
| 3168 | &dir, |
| 3169 | "mod_a.f90", |
| 3170 | "module mod_a\ncontains\n subroutine helper()\n print *, 11\n end subroutine helper\n\n subroutine run_a()\n call helper()\n end subroutine run_a\nend module mod_a\n", |
| 3171 | ); |
| 3172 | let mod_b = write_program_in( |
| 3173 | &dir, |
| 3174 | "mod_b.f90", |
| 3175 | "module mod_b\ncontains\n subroutine helper()\n print *, 22\n end subroutine helper\n\n subroutine run_b()\n call helper()\n end subroutine run_b\nend module mod_b\n", |
| 3176 | ); |
| 3177 | let main_src = write_program_in( |
| 3178 | &dir, |
| 3179 | "main.f90", |
| 3180 | "program p\n use mod_a, only: run_a\n use mod_b, only: run_b\n call run_a()\n call run_b()\nend program p\n", |
| 3181 | ); |
| 3182 | |
| 3183 | let mod_a_obj = dir.join("mod_a.o"); |
| 3184 | let compile_a = Command::new(compiler("armfortas")) |
| 3185 | .current_dir(&dir) |
| 3186 | .args([ |
| 3187 | "-c", |
| 3188 | "-J", |
| 3189 | dir.to_str().unwrap(), |
| 3190 | mod_a.to_str().unwrap(), |
| 3191 | "-o", |
| 3192 | mod_a_obj.to_str().unwrap(), |
| 3193 | ]) |
| 3194 | .output() |
| 3195 | .expect("mod_a compile spawn failed"); |
| 3196 | assert!( |
| 3197 | compile_a.status.success(), |
| 3198 | "mod_a should compile: {}", |
| 3199 | String::from_utf8_lossy(&compile_a.stderr) |
| 3200 | ); |
| 3201 | |
| 3202 | let mod_b_obj = dir.join("mod_b.o"); |
| 3203 | let compile_b = Command::new(compiler("armfortas")) |
| 3204 | .current_dir(&dir) |
| 3205 | .args([ |
| 3206 | "-c", |
| 3207 | "-I", |
| 3208 | dir.to_str().unwrap(), |
| 3209 | "-J", |
| 3210 | dir.to_str().unwrap(), |
| 3211 | mod_b.to_str().unwrap(), |
| 3212 | "-o", |
| 3213 | mod_b_obj.to_str().unwrap(), |
| 3214 | ]) |
| 3215 | .output() |
| 3216 | .expect("mod_b compile spawn failed"); |
| 3217 | assert!( |
| 3218 | compile_b.status.success(), |
| 3219 | "mod_b should compile: {}", |
| 3220 | String::from_utf8_lossy(&compile_b.stderr) |
| 3221 | ); |
| 3222 | |
| 3223 | let main_obj = dir.join("main.o"); |
| 3224 | let compile_main = Command::new(compiler("armfortas")) |
| 3225 | .current_dir(&dir) |
| 3226 | .args([ |
| 3227 | "-c", |
| 3228 | "-I", |
| 3229 | dir.to_str().unwrap(), |
| 3230 | "-J", |
| 3231 | dir.to_str().unwrap(), |
| 3232 | main_src.to_str().unwrap(), |
| 3233 | "-o", |
| 3234 | main_obj.to_str().unwrap(), |
| 3235 | ]) |
| 3236 | .output() |
| 3237 | .expect("main compile spawn failed"); |
| 3238 | assert!( |
| 3239 | compile_main.status.success(), |
| 3240 | "main should compile: {}", |
| 3241 | String::from_utf8_lossy(&compile_main.stderr) |
| 3242 | ); |
| 3243 | |
| 3244 | let exe = dir.join("helpers.bin"); |
| 3245 | let link = Command::new(compiler("armfortas")) |
| 3246 | .current_dir(&dir) |
| 3247 | .args([ |
| 3248 | mod_a_obj.to_str().unwrap(), |
| 3249 | mod_b_obj.to_str().unwrap(), |
| 3250 | main_obj.to_str().unwrap(), |
| 3251 | "-o", |
| 3252 | exe.to_str().unwrap(), |
| 3253 | ]) |
| 3254 | .output() |
| 3255 | .expect("helper link spawn failed"); |
| 3256 | assert!( |
| 3257 | link.status.success(), |
| 3258 | "same-named module helpers should link cleanly: {}", |
| 3259 | String::from_utf8_lossy(&link.stderr) |
| 3260 | ); |
| 3261 | |
| 3262 | let _ = std::fs::remove_dir_all(&dir); |
| 3263 | } |
| 3264 | |
| 3265 | #[test] |
| 3266 | fn contained_helpers_link_without_cross_object_internal_symbol_collisions() { |
| 3267 | let dir = unique_dir("contained_helper_link_names"); |
| 3268 | let mod_a = write_program_in( |
| 3269 | &dir, |
| 3270 | "mod_a.f90", |
| 3271 | "module mod_a\ncontains\n subroutine run_a()\n implicit none\n call helper()\n contains\n subroutine helper()\n print *, 11\n end subroutine helper\n end subroutine run_a\nend module mod_a\n", |
| 3272 | ); |
| 3273 | let mod_b = write_program_in( |
| 3274 | &dir, |
| 3275 | "mod_b.f90", |
| 3276 | "module mod_b\ncontains\n subroutine run_b()\n implicit none\n call helper()\n contains\n subroutine helper()\n print *, 22\n end subroutine helper\n end subroutine run_b\nend module mod_b\n", |
| 3277 | ); |
| 3278 | let main_src = write_program_in( |
| 3279 | &dir, |
| 3280 | "main.f90", |
| 3281 | "program p\n use mod_a, only: run_a\n use mod_b, only: run_b\n call run_a()\n call run_b()\nend program p\n", |
| 3282 | ); |
| 3283 | |
| 3284 | let mod_a_obj = dir.join("mod_a.o"); |
| 3285 | let compile_a = Command::new(compiler("armfortas")) |
| 3286 | .current_dir(&dir) |
| 3287 | .args([ |
| 3288 | "-c", |
| 3289 | "-J", |
| 3290 | dir.to_str().unwrap(), |
| 3291 | mod_a.to_str().unwrap(), |
| 3292 | "-o", |
| 3293 | mod_a_obj.to_str().unwrap(), |
| 3294 | ]) |
| 3295 | .output() |
| 3296 | .expect("mod_a compile spawn failed"); |
| 3297 | assert!( |
| 3298 | compile_a.status.success(), |
| 3299 | "mod_a should compile: {}", |
| 3300 | String::from_utf8_lossy(&compile_a.stderr) |
| 3301 | ); |
| 3302 | |
| 3303 | let mod_b_obj = dir.join("mod_b.o"); |
| 3304 | let compile_b = Command::new(compiler("armfortas")) |
| 3305 | .current_dir(&dir) |
| 3306 | .args([ |
| 3307 | "-c", |
| 3308 | "-I", |
| 3309 | dir.to_str().unwrap(), |
| 3310 | "-J", |
| 3311 | dir.to_str().unwrap(), |
| 3312 | mod_b.to_str().unwrap(), |
| 3313 | "-o", |
| 3314 | mod_b_obj.to_str().unwrap(), |
| 3315 | ]) |
| 3316 | .output() |
| 3317 | .expect("mod_b compile spawn failed"); |
| 3318 | assert!( |
| 3319 | compile_b.status.success(), |
| 3320 | "mod_b should compile: {}", |
| 3321 | String::from_utf8_lossy(&compile_b.stderr) |
| 3322 | ); |
| 3323 | |
| 3324 | let main_obj = dir.join("main.o"); |
| 3325 | let compile_main = Command::new(compiler("armfortas")) |
| 3326 | .current_dir(&dir) |
| 3327 | .args([ |
| 3328 | "-c", |
| 3329 | "-I", |
| 3330 | dir.to_str().unwrap(), |
| 3331 | "-J", |
| 3332 | dir.to_str().unwrap(), |
| 3333 | main_src.to_str().unwrap(), |
| 3334 | "-o", |
| 3335 | main_obj.to_str().unwrap(), |
| 3336 | ]) |
| 3337 | .output() |
| 3338 | .expect("main compile spawn failed"); |
| 3339 | assert!( |
| 3340 | compile_main.status.success(), |
| 3341 | "main should compile: {}", |
| 3342 | String::from_utf8_lossy(&compile_main.stderr) |
| 3343 | ); |
| 3344 | |
| 3345 | let exe = dir.join("contained_helpers.bin"); |
| 3346 | let link = Command::new(compiler("armfortas")) |
| 3347 | .current_dir(&dir) |
| 3348 | .args([ |
| 3349 | mod_a_obj.to_str().unwrap(), |
| 3350 | mod_b_obj.to_str().unwrap(), |
| 3351 | main_obj.to_str().unwrap(), |
| 3352 | "-o", |
| 3353 | exe.to_str().unwrap(), |
| 3354 | ]) |
| 3355 | .output() |
| 3356 | .expect("contained helper link spawn failed"); |
| 3357 | assert!( |
| 3358 | link.status.success(), |
| 3359 | "contained helpers in different objects should link cleanly: {}", |
| 3360 | String::from_utf8_lossy(&link.stderr) |
| 3361 | ); |
| 3362 | |
| 3363 | let _ = std::fs::remove_dir_all(&dir); |
| 3364 | } |
| 3365 | |
| 3366 | #[test] |
| 3367 | fn program_internal_char_helper_assignment_uses_internal_symbol() { |
| 3368 | let dir = unique_dir("program_internal_char_helper"); |
| 3369 | let src = write_program_in( |
| 3370 | &dir, |
| 3371 | "p.f90", |
| 3372 | "program p\n implicit none\n character(len=16) :: x\n x = helper('a')\ncontains\n function helper(v) result(out)\n character(len=*), intent(in) :: v\n character(len=16) :: out\n out = v\n end function helper\nend program p\n", |
| 3373 | ); |
| 3374 | |
| 3375 | let obj = dir.join("p.o"); |
| 3376 | let compile = Command::new(compiler("armfortas")) |
| 3377 | .current_dir(&dir) |
| 3378 | .args(["-c", src.to_str().unwrap(), "-o", obj.to_str().unwrap()]) |
| 3379 | .output() |
| 3380 | .expect("program compile spawn failed"); |
| 3381 | assert!( |
| 3382 | compile.status.success(), |
| 3383 | "program-contained character helper should compile: {}", |
| 3384 | String::from_utf8_lossy(&compile.stderr) |
| 3385 | ); |
| 3386 | |
| 3387 | let undefined = undefined_symbols(&obj); |
| 3388 | assert!( |
| 3389 | !undefined.iter().any(|sym| sym == "_helper"), |
| 3390 | "program-contained character helper should not escape as a raw external symbol: {:?}", |
| 3391 | undefined |
| 3392 | ); |
| 3393 | |
| 3394 | let _ = std::fs::remove_dir_all(&dir); |
| 3395 | } |
| 3396 | |
| 3397 | #[test] |
| 3398 | fn allocatable_result_helper_assignment_uses_resolved_symbol() { |
| 3399 | let dir = unique_dir("alloc_result_helper_symbol"); |
| 3400 | let src = write_program_in( |
| 3401 | &dir, |
| 3402 | "m.f90", |
| 3403 | "module m\ncontains\n function helper(x) result(y)\n character(len=*), intent(in) :: x\n character(len=:), allocatable :: y\n y = trim(x)\n end function helper\n\n function run(x) result(y)\n character(len=*), intent(in) :: x\n character(len=:), allocatable :: y\n y = helper(x)\n end function run\nend module m\n", |
| 3404 | ); |
| 3405 | |
| 3406 | let obj = dir.join("m.o"); |
| 3407 | let compile = Command::new(compiler("armfortas")) |
| 3408 | .current_dir(&dir) |
| 3409 | .args(["-c", src.to_str().unwrap(), "-o", obj.to_str().unwrap()]) |
| 3410 | .output() |
| 3411 | .expect("module compile spawn failed"); |
| 3412 | assert!( |
| 3413 | compile.status.success(), |
| 3414 | "allocatable-result helper source should compile: {}", |
| 3415 | String::from_utf8_lossy(&compile.stderr) |
| 3416 | ); |
| 3417 | |
| 3418 | let undefined = undefined_symbols(&obj); |
| 3419 | assert!( |
| 3420 | !undefined.iter().any(|sym| sym == "_helper"), |
| 3421 | "same-file allocatable-result helper should not lower to a raw external symbol: {:?}", |
| 3422 | undefined |
| 3423 | ); |
| 3424 | |
| 3425 | let _ = std::fs::remove_dir_all(&dir); |
| 3426 | } |
| 3427 | |
| 3428 | #[test] |
| 3429 | fn derived_pointer_module_global_survives_amod_import() { |
| 3430 | let dir = unique_dir("derived_ptr_amod"); |
| 3431 | let mod_src = write_program_in( |
| 3432 | &dir, |
| 3433 | "state_mod.f90", |
| 3434 | "module state_mod\n implicit none\n type :: node_t\n integer :: value = 0\n end type node_t\n type(node_t), target, save :: backing\n type(node_t), pointer, public, save :: current => null()\ncontains\n subroutine init_state()\n current => backing\n current%value = 1\n end subroutine init_state\nend module state_mod\n", |
| 3435 | ); |
| 3436 | let user_src = write_program_in( |
| 3437 | &dir, |
| 3438 | "user_mod.f90", |
| 3439 | "module user_mod\n implicit none\ncontains\n subroutine bump()\n use state_mod\n if (.not. associated(current)) call init_state()\n current%value = current%value + 1\n end subroutine bump\nend module user_mod\n", |
| 3440 | ); |
| 3441 | |
| 3442 | let mod_obj = dir.join("state_mod.o"); |
| 3443 | let compile_mod = Command::new(compiler("armfortas")) |
| 3444 | .current_dir(&dir) |
| 3445 | .args([ |
| 3446 | "-c", |
| 3447 | "-J", |
| 3448 | dir.to_str().unwrap(), |
| 3449 | mod_src.to_str().unwrap(), |
| 3450 | "-o", |
| 3451 | mod_obj.to_str().unwrap(), |
| 3452 | ]) |
| 3453 | .output() |
| 3454 | .expect("state module compile spawn failed"); |
| 3455 | assert!( |
| 3456 | compile_mod.status.success(), |
| 3457 | "derived-pointer module should compile: {}", |
| 3458 | String::from_utf8_lossy(&compile_mod.stderr) |
| 3459 | ); |
| 3460 | |
| 3461 | let user_obj = dir.join("user_mod.o"); |
| 3462 | let compile_user = Command::new(compiler("armfortas")) |
| 3463 | .current_dir(&dir) |
| 3464 | .args([ |
| 3465 | "-c", |
| 3466 | "-I", |
| 3467 | dir.to_str().unwrap(), |
| 3468 | "-J", |
| 3469 | dir.to_str().unwrap(), |
| 3470 | user_src.to_str().unwrap(), |
| 3471 | "-o", |
| 3472 | user_obj.to_str().unwrap(), |
| 3473 | ]) |
| 3474 | .output() |
| 3475 | .expect("state user compile spawn failed"); |
| 3476 | assert!( |
| 3477 | compile_user.status.success(), |
| 3478 | "imported derived-pointer module globals should survive .amod export/import: {}", |
| 3479 | String::from_utf8_lossy(&compile_user.stderr) |
| 3480 | ); |
| 3481 | |
| 3482 | let _ = std::fs::remove_dir_all(&dir); |
| 3483 | } |
| 3484 | |
| 3485 | #[test] |
| 3486 | fn deferred_char_pointer_component_compiles_string_pool_style_ops() { |
| 3487 | let src = write_program( |
| 3488 | "module m\n implicit none\n type :: string_ref\n integer :: str_len = 0\n character(:), pointer :: data => null()\n end type string_ref\n character(len=16), target :: pool(1)\ncontains\n subroutine bind_pool(ref, n)\n type(string_ref), intent(inout) :: ref\n integer, intent(in) :: n\n ref%str_len = n\n ref%data => pool(1)(1:n)\n if (associated(ref%data)) then\n ref%data = ' '\n ref%data(1:1) = 'x'\n end if\n end subroutine bind_pool\n\n subroutine own_alloc(ref, n)\n type(string_ref), intent(inout) :: ref\n integer, intent(in) :: n\n if (associated(ref%data)) deallocate(ref%data)\n allocate(character(len=n) :: ref%data)\n ref%data = 'abc'\n end subroutine own_alloc\nend module\n", |
| 3489 | "f90", |
| 3490 | ); |
| 3491 | let out = unique_path("deferred_char_pointer_component", "o"); |
| 3492 | let result = Command::new(compiler("armfortas")) |
| 3493 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 3494 | .env("NO_COLOR", "1") |
| 3495 | .output() |
| 3496 | .expect("spawn failed"); |
| 3497 | assert!( |
| 3498 | result.status.success(), |
| 3499 | "deferred char pointer components should compile: {}", |
| 3500 | String::from_utf8_lossy(&result.stderr) |
| 3501 | ); |
| 3502 | let _ = std::fs::remove_file(&out); |
| 3503 | let _ = std::fs::remove_file(&src); |
| 3504 | } |
| 3505 | |
| 3506 | #[test] |
| 3507 | fn logical_allocatable_slice_assignment_compiles() { |
| 3508 | let src = write_program( |
| 3509 | "program p\n implicit none\n logical, allocatable :: a(:), b(:)\n integer :: n\n n = 4\n allocate(a(n), b(n))\n a = .false.\n b = .true.\n a(1:n) = b(1:n)\n b(2:n-1) = .false.\nend program\n", |
| 3510 | "f90", |
| 3511 | ); |
| 3512 | let out = unique_path("logical_slice_assign", "o"); |
| 3513 | let result = Command::new(compiler("armfortas")) |
| 3514 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 3515 | .env("NO_COLOR", "1") |
| 3516 | .output() |
| 3517 | .expect("spawn failed"); |
| 3518 | assert!( |
| 3519 | result.status.success(), |
| 3520 | "logical allocatable slice assignment should compile: {}", |
| 3521 | String::from_utf8_lossy(&result.stderr) |
| 3522 | ); |
| 3523 | let _ = std::fs::remove_file(&out); |
| 3524 | let _ = std::fs::remove_file(&src); |
| 3525 | } |
| 3526 | |
| 3527 | #[test] |
| 3528 | fn c_interop_opaque_pointer_values_compile() { |
| 3529 | let src = write_program( |
| 3530 | "program p\n use iso_c_binding\n implicit none\n type(c_ptr) :: pbuf\n type(c_funptr) :: fptr\n pbuf = c_null_ptr\n fptr = c_null_funptr\nend program\n", |
| 3531 | "f90", |
| 3532 | ); |
| 3533 | let out = unique_path("c_interop_opaque_values", "o"); |
| 3534 | let result = Command::new(compiler("armfortas")) |
| 3535 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 3536 | .env("NO_COLOR", "1") |
| 3537 | .output() |
| 3538 | .expect("spawn failed"); |
| 3539 | assert!( |
| 3540 | result.status.success(), |
| 3541 | "C interop opaque pointer values should compile: {}", |
| 3542 | String::from_utf8_lossy(&result.stderr) |
| 3543 | ); |
| 3544 | let _ = std::fs::remove_file(&out); |
| 3545 | let _ = std::fs::remove_file(&src); |
| 3546 | } |
| 3547 | |
| 3548 | #[test] |
| 3549 | fn allocated_eqv_on_allocatables_compiles() { |
| 3550 | let src = write_program( |
| 3551 | "program p\n implicit none\n logical, allocatable :: a(:), b(:)\n logical :: same\n allocate(a(1), b(1))\n same = allocated(a) .eqv. allocated(b)\nend program\n", |
| 3552 | "f90", |
| 3553 | ); |
| 3554 | let out = unique_path("allocated_eqv", "o"); |
| 3555 | let result = Command::new(compiler("armfortas")) |
| 3556 | .args(["-c", src.to_str().unwrap(), "-o", out.to_str().unwrap()]) |
| 3557 | .env("NO_COLOR", "1") |
| 3558 | .output() |
| 3559 | .expect("spawn failed"); |
| 3560 | assert!( |
| 3561 | result.status.success(), |
| 3562 | "allocated() logical combinations should compile: {}", |
| 3563 | String::from_utf8_lossy(&result.stderr) |
| 3564 | ); |
| 3565 | let _ = std::fs::remove_file(&out); |
| 3566 | let _ = std::fs::remove_file(&src); |
| 3567 | } |
| 3568 |