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_time_lowers_to_private_runtime_helpers() { let fixture = write_fixture( "lowering", r#" (module main) (fn main () -> i32 (std.time.sleep_ms 0) (std.time.monotonic_ms)) "#, ); let output = run_glagol([fixture.as_os_str()]); assert_success("compile standard time lowering", &output); let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.contains("declare i32 @__glagol_time_monotonic_ms()") && stdout.contains("declare void @__glagol_time_sleep_ms(i32)") && stdout.contains("call void @__glagol_time_sleep_ms(i32 0)") && stdout.contains("call i32 @__glagol_time_monotonic_ms()") && !stdout.contains("@std.time."), "standard time LLVM shape drifted\nstdout:\n{}", stdout ); } #[test] fn test_runner_executes_monotonic_and_sleep_zero() { let fixture = write_fixture( "test-runner", r#" (module main) (fn sleep_zero_then_one () -> i32 (std.time.sleep_ms 0) 1) (test "monotonic is non-negative" (>= (std.time.monotonic_ms) 0)) (test "sleep zero returns" (= (sleep_zero_then_one) 1)) "#, ); let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]); assert_success("run standard time tests", &output); assert_eq!( String::from_utf8_lossy(&output.stdout), concat!( "test \"monotonic is non-negative\" ... ok\n", "test \"sleep zero returns\" ... ok\n", "2 test(s) passed\n", ), "standard time test runner stdout drifted" ); } #[test] fn test_runner_reports_negative_sleep_trap() { let fixture = write_fixture( "negative-sleep-test-runner", r#" (module main) (fn bad_sleep () -> i32 (std.time.sleep_ms -1) 0) (test "negative sleep traps" (= (bad_sleep) 0)) "#, ); let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert_eq!( output.status.code(), Some(1), "negative sleep trap exit code drifted\nstdout:\n{}\nstderr:\n{}", stdout, stderr ); assert!( stdout.is_empty(), "negative sleep trap wrote stdout:\n{}", stdout ); assert!( stderr.contains("TestRuntimeTrap") && stderr.contains("slovo runtime error: sleep_ms negative duration"), "negative sleep trap diagnostic drifted\nstderr:\n{}", stderr ); } #[test] fn standard_time_diagnostics_cover_promoted_and_unpromoted_names() { let cases = [ ( "monotonic-arity", r#" (module main) (fn main () -> i32 (std.time.monotonic_ms 1)) "#, "ArityMismatch", ), ( "sleep-arity", r#" (module main) (fn main () -> i32 (std.time.sleep_ms) 0) "#, "ArityMismatch", ), ( "sleep-type", r#" (module main) (fn main () -> i32 (std.time.sleep_ms true) 0) "#, "TypeMismatch", ), ( "unknown-time", r#" (module main) (fn main () -> i32 (std.time.now)) "#, "UnsupportedStandardLibraryCall", ), ( "promoted-shadow", r#" (module main) (fn std.time.monotonic_ms () -> i32 0) (fn main () -> i32 (std.time.monotonic_ms)) "#, "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_and_negative_sleep_trap_when_clang_is_available() { let Some(clang) = find_clang() else { eprintln!("skipping standard time runtime smoke: set GLAGOL_CLANG or install clang"); return; }; let smoke = write_fixture( "runtime-smoke", r#" (module main) (fn main () -> i32 (std.time.sleep_ms 0) (if (>= (std.time.monotonic_ms) 0) 0 1)) "#, ); let compile = run_glagol([smoke.as_os_str()]); assert_success("compile standard time runtime smoke", &compile); let run = compile_and_run_with_runtime(&clang, "standard-time-smoke", &compile.stdout); assert_success("run standard time runtime smoke", &run); let trap = write_fixture( "runtime-negative-sleep", r#" (module main) (fn main () -> i32 (std.time.sleep_ms -1) 0) "#, ); let compile = run_glagol([trap.as_os_str()]); assert_success("compile standard time negative sleep trap", &compile); let run = compile_and_run_with_runtime(&clang, "standard-time-negative-sleep", &compile.stdout); assert_eq!( run.status.code(), Some(1), "negative sleep 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.stderr), "slovo runtime error: sleep_ms negative duration\n", "negative sleep runtime stderr drifted" ); assert!( run.stdout.is_empty(), "negative sleep runtime wrote stdout:\n{}", 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-time-{}-{}-{}.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-time-{}-{}", 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 time 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); }