use std::{ env, fs, path::{Path, PathBuf}, process::{Command, Output}, sync::atomic::{AtomicUsize, Ordering}, }; static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0); #[test] fn run_manifest_records_success_report_stdout_and_program_args() { let Some(clang) = find_clang() else { eprintln!("skipping run manifest success report: set GLAGOL_CLANG or install clang"); return; }; let source = write_fixture( "success", r#" (module main) (fn main () -> i32 (print_string "beta22-out") 0) "#, "slo", ); let manifest_path = temp_path("success-manifest", "manifest.slo"); let mut command = Command::new(compiler_path()); command .arg("run") .arg(&source) .arg("--manifest") .arg(&manifest_path) .arg("--") .arg("alpha") .arg("two words") .arg("--literal") .env("GLAGOL_CLANG", &clang); configure_clang_runtime_env(&mut command, &clang); let output = command.output().expect("run glagol success manifest"); assert_success_stdout("run manifest success", &output, "beta22-out\n"); let manifest = read_manifest(&manifest_path); assert!( manifest.contains(" (mode run)\n") && manifest.contains(" (success true)\n") && manifest.contains(" (run-report\n") && manifest.contains(" (exit-status 0)\n") && manifest.contains(" (stdout \"beta22-out\\n\")\n") && manifest.contains(" (stderr \"\")\n") && manifest.contains(" (arg \"alpha\")\n") && manifest.contains(" (arg \"two words\")\n") && manifest.contains(" (arg \"--literal\")\n"), "successful run manifest mismatch:\n{}", manifest ); } #[test] fn run_manifest_records_nonzero_report_and_preserves_program_stderr() { let Some(clang) = find_clang() else { eprintln!("skipping run manifest nonzero report: set GLAGOL_CLANG or install clang"); return; }; let source = write_fixture( "nonzero", r#" (module main) (import_c beta22_stderr () -> i32) (fn emit_stderr () -> i32 (unsafe (beta22_stderr))) (fn main () -> i32 (emit_stderr) 7) "#, "slo", ); let c_source = write_fixture( "nonzero-stderr", r#" #include int beta22_stderr(void) { fputs("beta22-err\n", stderr); return 0; } "#, "c", ); let manifest_path = temp_path("nonzero-manifest", "manifest.slo"); let mut command = Command::new(compiler_path()); command .arg("run") .arg(&source) .arg("--link-c") .arg(&c_source) .arg("--manifest") .arg(&manifest_path) .env("GLAGOL_CLANG", &clang); configure_clang_runtime_env(&mut command, &clang); let output = command.output().expect("run glagol nonzero manifest"); assert_exit_code("run manifest nonzero", &output, 7); assert_eq!(output.stdout, b"", "nonzero run stdout drifted"); assert_eq!(output.stderr, b"beta22-err\n", "nonzero run stderr drifted"); let manifest = read_manifest(&manifest_path); assert!( manifest.contains(" (mode run)\n") && manifest.contains(" (success false)\n") && manifest.contains(" (run-report\n") && manifest.contains(" (exit-status 7)\n") && manifest.contains(" (stdout \"\")\n") && manifest.contains(" (stderr \"beta22-err\\n\")\n") && manifest.contains(" (args)\n"), "nonzero run manifest mismatch:\n{}", manifest ); } #[test] fn run_manifest_source_failure_does_not_record_fake_run_report() { let source = write_fixture( "source-failure", r#" (module main) (fn main () -> i32 true) "#, "slo", ); let manifest_path = temp_path("source-failure-manifest", "manifest.slo"); let output = run_glagol([ "run".as_ref(), source.as_os_str(), "--manifest".as_ref(), manifest_path.as_os_str(), ]); assert_exit_code("run manifest source failure", &output, 1); let manifest = read_manifest(&manifest_path); assert!( manifest.contains(" (mode run)\n") && manifest.contains(" (success false)\n") && manifest.contains(" (kind diagnostics)\n") && manifest.contains("TypeMismatch") && !manifest.contains(" (run-report\n"), "source failure manifest included fake run report:\n{}", manifest ); } fn run_glagol(args: [&std::ffi::OsStr; N]) -> Output { Command::new(compiler_path()) .args(args) .output() .expect("run glagol") } fn compiler_path() -> &'static str { env!("CARGO_BIN_EXE_glagol") } fn write_fixture(name: &str, source: &str, extension: &str) -> PathBuf { let path = temp_path(name, extension); fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err)); path } fn temp_path(name: &str, extension: &str) -> PathBuf { let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed); let mut path = env::temp_dir(); path.push(format!( "glagol-run-manifest-beta22-{}-{}-{}.{}", std::process::id(), id, name, extension )); path } fn read_manifest(path: &Path) -> String { fs::read_to_string(path).unwrap_or_else(|err| panic!("read `{}`: {}", path.display(), err)) } fn assert_success_stdout(context: &str, output: &Output, expected: &str) { assert!( output.status.success(), "{} failed\nstdout:\n{}\nstderr:\n{}", context, String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); assert_eq!( String::from_utf8_lossy(&output.stdout), expected, "{} stdout mismatch", context ); assert!( output.stderr.is_empty(), "{} wrote stderr:\n{}", context, String::from_utf8_lossy(&output.stderr) ); } fn assert_exit_code(context: &str, output: &Output, expected: i32) { assert_eq!( output.status.code(), Some(expected), "{} exit code mismatch\nstdout:\n{}\nstderr:\n{}", context, String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); } fn find_clang() -> Option { if let Some(path) = env::var_os("GLAGOL_CLANG").filter(|value| !value.is_empty()) { let path = PathBuf::from(path); if path.is_file() { return Some(path); } } let hermetic_clang = PathBuf::from("/tmp/glagol-clang-root/usr/bin/clang"); if hermetic_clang.is_file() { return Some(hermetic_clang); } find_on_path("clang") } fn find_on_path(name: &str) -> Option { let path = env::var_os("PATH")?; env::split_paths(&path) .map(|dir| dir.join(name)) .find(|candidate| candidate.is_file()) } fn configure_clang_runtime_env(command: &mut Command, clang: &Path) { if !clang.starts_with("/tmp/glagol-clang-root") { return; } let root = Path::new("/tmp/glagol-clang-root"); let lib64 = root.join("usr/lib64"); let lib = root.join("usr/lib"); let existing = env::var_os("LD_LIBRARY_PATH").unwrap_or_default(); let mut paths = vec![lib64, lib]; paths.extend(env::split_paths(&existing)); let joined = env::join_paths(paths).expect("join LD_LIBRARY_PATH"); command.env("LD_LIBRARY_PATH", joined); }