use std::{ env, ffi::OsStr, fs, path::{Path, PathBuf}, process::{Command, Output}, sync::atomic::{AtomicUsize, Ordering}, }; static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0); #[test] fn standard_io_host_env_fixture_lowers_to_runtime_helpers() { let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/standard-io-host-env.slo"); let compile = run_glagol([fixture.as_os_str()]); let stdout = String::from_utf8_lossy(&compile.stdout); let stderr = String::from_utf8_lossy(&compile.stderr); assert!( compile.status.success(), "compiler rejected exp-3 fixture\nstdout:\n{}\nstderr:\n{}", stdout, stderr ); assert!( stdout.contains("declare void @__glagol_io_eprint(ptr)") && stdout.contains("declare void @__glagol_process_init(i32, ptr)") && stdout.contains("declare i32 @__glagol_process_argc()") && stdout.contains("declare ptr @__glagol_process_arg(i32)") && stdout.contains("declare ptr @__glagol_env_get(ptr)") && stdout.contains("declare ptr @__glagol_fs_read_text(ptr)") && stdout.contains("declare i32 @__glagol_fs_write_text(ptr, ptr)") && stdout.contains("define i32 @main(i32 %__glagol_argc, ptr %__glagol_argv)") && stdout.contains( "call void @__glagol_process_init(i32 %__glagol_argc, ptr %__glagol_argv)" ) && stdout.contains("call void @__glagol_io_eprint(ptr @") && stdout.contains("call i32 @__glagol_process_argc()") && stdout.contains("call ptr @__glagol_process_arg(i32 0)") && stdout.contains("call ptr @__glagol_env_get(ptr @") && stdout.contains("call ptr @__glagol_fs_read_text(ptr @") && stdout.contains("call i32 @__glagol_fs_write_text(ptr @") && !stdout.contains("@std."), "LLVM output did not contain expected exp-3 runtime shape\nstdout:\n{}", stdout ); assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr); } #[test] fn standard_io_host_env_test_runner_interprets_host_calls() { let root = temp_root("test-runner"); let input = root.join("input.txt"); let output = root.join("output.txt"); fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err)); fs::write(&input, "hello").unwrap_or_else(|err| panic!("write `{}`: {}", input.display(), err)); let source = format!( r#" (module main) (test "env missing is empty" (= (std.env.get "GLAGOL_EXP_3_MISSING") "")) (test "fs roundtrip" (= (std.fs.write_text "{}" (std.fs.read_text "{}")) 0)) "#, slovo_path(&output), slovo_path(&input) ); let fixture = write_fixture("host-test-runner", &source); let run = run_glagol([OsStr::new("test"), fixture.as_os_str()]); assert_success_stdout( run, concat!( "test \"env missing is empty\" ... ok\n", "test \"fs roundtrip\" ... ok\n", "2 test(s) passed\n", ), "exp-3 test runner output", ); assert_eq!( fs::read_to_string(&output).expect("read roundtrip output"), "hello" ); } #[test] fn standard_io_host_env_runtime_smoke_when_clang_is_available() { let Some(clang) = find_clang() else { eprintln!("skipping exp-3 runtime smoke: set GLAGOL_CLANG or install clang"); return; }; let root = temp_root("runtime"); let input = root.join("input.txt"); let output = root.join("output.txt"); fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err)); fs::write(&input, "runtime text") .unwrap_or_else(|err| panic!("write `{}`: {}", input.display(), err)); let source = format!( r#" (module main) (fn main () -> i32 (std.io.eprint "err-line\n") (std.io.print_i32 (std.process.argc)) (std.io.print_string (std.process.arg 1)) (std.io.print_string (std.env.get "GLAGOL_EXP_3_PRESENT")) (let text string (std.fs.read_text "{}")) (std.io.print_string text) (std.fs.write_text "{}" (std.string.concat text "-written"))) "#, slovo_path(&input), slovo_path(&output) ); let fixture = write_fixture("host-runtime", &source); let compile = run_glagol([fixture.as_os_str()]); assert_success("compile exp-3 runtime smoke", &compile); let run = compile_and_run_with_runtime(&clang, "host-runtime", &compile.stdout, |command| { command .arg("argument-value") .env("GLAGOL_EXP_3_PRESENT", "env-value"); }); assert_eq!( run.status.code(), Some(0), "exp-3 runtime exit code drifted\nstdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&run.stdout), String::from_utf8_lossy(&run.stderr) ); assert_eq!( String::from_utf8_lossy(&run.stdout), "2\nargument-value\nenv-value\nruntime text\n", "exp-3 runtime stdout drifted" ); assert_eq!( String::from_utf8_lossy(&run.stderr), "err-line\n", "exp-3 runtime stderr drifted" ); assert_eq!( fs::read_to_string(&output).expect("read exp-3 write output"), "runtime text-written" ); } #[test] fn standard_io_host_env_runtime_traps_when_clang_is_available() { let Some(clang) = find_clang() else { eprintln!("skipping exp-3 trap smoke: set GLAGOL_CLANG or install clang"); return; }; let cases = [ ( "process-arg-oob", r#" (module main) (fn main () -> i32 (std.string.len (std.process.arg 99))) "#, "slovo runtime error: process argument index out of bounds\n", ), ( "file-read-failed", r#" (module main) (fn main () -> i32 (std.string.len (std.fs.read_text "/tmp/glagol-exp-3-missing-file-for-trap.txt"))) "#, "slovo runtime error: file read failed\n", ), ]; for (name, source, expected_stderr) in cases { let fixture = write_fixture(name, source); let compile = run_glagol([fixture.as_os_str()]); assert_success("compile exp-3 trap smoke", &compile); let run = compile_and_run_with_runtime(&clang, name, &compile.stdout, |_| {}); assert_eq!( run.status.code(), Some(1), "exp-3 trap `{}` exit code drifted\nstdout:\n{}\nstderr:\n{}", name, String::from_utf8_lossy(&run.stdout), String::from_utf8_lossy(&run.stderr) ); assert_eq!( String::from_utf8_lossy(&run.stderr), expected_stderr, "exp-3 trap `{}` stderr drifted", name ); assert!( run.stdout.is_empty(), "exp-3 trap `{}` wrote stdout:\n{}", name, String::from_utf8_lossy(&run.stdout) ); } } fn run_glagol(args: I) -> Output where I: IntoIterator, S: AsRef, { Command::new(env!("CARGO_BIN_EXE_glagol")) .args(args) .current_dir(Path::new(env!("CARGO_MANIFEST_DIR"))) .output() .expect("run glagol") } fn write_fixture(name: &str, source: &str) -> PathBuf { let mut path = env::temp_dir(); path.push(format!( "glagol-standard-io-host-env-{}-{}-{}.slo", name, std::process::id(), NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed) )); fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err)); path } fn temp_root(name: &str) -> PathBuf { env::temp_dir().join(format!( "glagol-standard-io-host-env-{}-{}-{}", name, std::process::id(), NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed) )) } fn slovo_path(path: &Path) -> String { path.to_string_lossy() .replace('\\', "\\\\") .replace('"', "\\\"") } fn assert_success(context: &str, output: &Output) { let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( output.status.success(), "{} failed\nstdout:\n{}\nstderr:\n{}", context, stdout, stderr ); } fn assert_success_stdout(output: Output, expected: &str, context: &str) { let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( output.status.success(), "{} failed\nstdout:\n{}\nstderr:\n{}", context, stdout, stderr ); assert_eq!(stdout, expected, "{} stdout drifted", context); assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr); } fn compile_and_run_with_runtime(clang: &Path, name: &str, ir: &[u8], configure: F) -> Output where F: FnOnce(&mut Command), { let manifest = Path::new(env!("CARGO_MANIFEST_DIR")); let temp_dir = temp_root("clang"); fs::create_dir_all(&temp_dir) .unwrap_or_else(|err| panic!("create `{}`: {}", temp_dir.display(), err)); let ir_path = temp_dir.join(format!("{}.ll", name)); let exe_path = temp_dir.join(name); fs::write(&ir_path, ir).unwrap_or_else(|err| panic!("write `{}`: {}", ir_path.display(), err)); let runtime = manifest.join("../runtime/runtime.c"); let mut clang_command = Command::new(clang); clang_command .arg(&runtime) .arg(&ir_path) .arg("-o") .arg(&exe_path) .current_dir(manifest); configure_clang_runtime_env(&mut clang_command, clang); let clang_output = clang_command .output() .unwrap_or_else(|err| panic!("run `{}`: {}", clang.display(), err)); assert_success("clang exp-3 runtime smoke", &clang_output); let mut run = Command::new(&exe_path); configure(&mut run); run.output() .unwrap_or_else(|err| panic!("run `{}`: {}", exe_path.display(), err)) } fn find_clang() -> Option { if let Some(path) = env::var_os("GLAGOL_CLANG").filter(|value| !value.is_empty()) { return Some(PathBuf::from(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(program: &str) -> Option { let path = env::var_os("PATH")?; env::split_paths(&path) .map(|dir| dir.join(program)) .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); }