342 lines
11 KiB
Rust
342 lines
11 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_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<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-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<F>(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<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 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);
|
|
}
|