slovo/compiler/tests/standard_time.rs
2026-05-22 08:38:43 +02:00

357 lines
9.3 KiB
Rust

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<I, S>(args: I) -> Output
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
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<PathBuf> {
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<PathBuf> {
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);
}