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_random_lowers_to_private_runtime_helper() { let fixture = write_fixture( "lowering", r#" (module main) (fn main () -> i32 (std.random.i32)) "#, ); let output = run_glagol([fixture.as_os_str()]); assert_success("compile standard random lowering", &output); let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.contains("declare i32 @__glagol_random_i32()") && stdout.contains("call i32 @__glagol_random_i32()") && !stdout.contains("@std.random"), "standard random LLVM shape drifted\nstdout:\n{}", stdout ); } #[test] fn test_runner_executes_random_i32_as_non_negative() { let fixture = write_fixture( "test-runner", r#" (module main) (test "random i32 is non-negative" (if (< (std.random.i32) 0) false true)) "#, ); let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]); assert_success("run standard random tests", &output); assert_eq!( String::from_utf8_lossy(&output.stdout), concat!( "test \"random i32 is non-negative\" ... ok\n", "1 test(s) passed\n", ), "standard random test runner stdout drifted" ); } #[test] fn standard_random_diagnostics_cover_promoted_and_deferred_names() { let cases = [ ( "random-arity", r#" (module main) (fn main () -> i32 (std.random.i32 1)) "#, "ArityMismatch", ), ( "random-bool-context", r#" (module main) (fn main () -> i32 (if (std.random.i32) 1 0)) "#, "IfConditionNotBool", ), ( "unknown-random", r#" (module main) (fn main () -> i32 (std.random.range 0 10)) "#, "UnsupportedStandardLibraryCall", ), ( "promoted-shadow", r#" (module main) (fn std.random.i32 () -> i32 0) (fn main () -> i32 (std.random.i32)) "#, "DuplicateFunction", ), ]; for (name, source, diagnostic) in cases { let fixture = write_fixture(name, source); let output = run_glagol([fixture.as_os_str()]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( !output.status.success(), "compiler unexpectedly accepted `{}`\nstdout:\n{}\nstderr:\n{}", name, stdout, stderr ); assert!( stdout.is_empty(), "rejected compile wrote stdout:\n{}", stdout ); assert!( stderr.contains(diagnostic), "diagnostic `{}` was not reported for `{}`\nstderr:\n{}", diagnostic, name, stderr ); } } #[test] fn hosted_runtime_smoke_when_clang_is_available() { let Some(clang) = find_clang() else { eprintln!("skipping standard random runtime smoke: set GLAGOL_CLANG or install clang"); return; }; let smoke = write_fixture( "runtime-smoke", r#" (module main) (fn main () -> i32 (if (< (std.random.i32) 0) 1 0)) "#, ); let compile = run_glagol([smoke.as_os_str()]); assert_success("compile standard random runtime smoke", &compile); let run = compile_and_run_with_runtime(&clang, "standard-random-smoke", &compile.stdout); assert_success("run standard random runtime smoke", &run); } 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-random-{}-{}-{}.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 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 compile_and_run_with_runtime(clang: &Path, name: &str, ir: &[u8]) -> Output { let manifest = Path::new(env!("CARGO_MANIFEST_DIR")); let temp_dir = env::temp_dir().join(format!( "glagol-standard-random-{}-{}", std::process::id(), NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed) )); 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 standard random runtime smoke", &clang_output); Command::new(&exe_path) .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 mut paths = vec![lib64, lib]; if let Some(existing) = env::var_os("LD_LIBRARY_PATH") { paths.extend(env::split_paths(&existing)); } let joined = env::join_paths(paths).expect("join LD_LIBRARY_PATH"); command.env("LD_LIBRARY_PATH", joined); }