898 lines
28 KiB
Rust
898 lines
28 KiB
Rust
use std::{
|
|
env,
|
|
ffi::OsStr,
|
|
fs,
|
|
io::Write,
|
|
path::{Path, PathBuf},
|
|
process::{Command, Output, Stdio},
|
|
sync::atomic::{AtomicUsize, Ordering},
|
|
};
|
|
|
|
static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0);
|
|
|
|
#[test]
|
|
fn exp10_fixture_formats_and_lowers_when_implementation_lands() {
|
|
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/host-io-result.slo");
|
|
|
|
let formatted = run_glagol([OsStr::new("--format"), fixture.as_os_str()]);
|
|
assert_success_stdout(
|
|
formatted,
|
|
&fs::read_to_string(&fixture).expect("read exp-10 formatter fixture"),
|
|
"exp-10 formatter fixture",
|
|
);
|
|
|
|
let surface = run_glagol([
|
|
OsStr::new("--inspect-lowering=surface"),
|
|
fixture.as_os_str(),
|
|
]);
|
|
assert_success("exp-10 surface lowering", &surface);
|
|
let surface_stdout = String::from_utf8_lossy(&surface.stdout);
|
|
assert!(
|
|
surface_stdout.contains("fn ok_text(value: string) -> (result string i32)")
|
|
&& surface_stdout.contains("ok string i32")
|
|
&& surface_stdout.contains("call std.process.arg_result")
|
|
&& surface_stdout.contains("call std.env.get_result")
|
|
&& surface_stdout.contains("call std.fs.read_text_result")
|
|
&& surface_stdout.contains("call std.fs.write_text_result")
|
|
&& surface_stdout.contains("match")
|
|
&& surface_stdout.contains("unwrap_ok")
|
|
&& surface_stdout.contains("unwrap_err"),
|
|
"surface lowering lost exp-10 result host shape\nstdout:\n{}",
|
|
surface_stdout
|
|
);
|
|
|
|
let checked = run_glagol([
|
|
OsStr::new("--inspect-lowering=checked"),
|
|
fixture.as_os_str(),
|
|
]);
|
|
assert_success("exp-10 checked lowering", &checked);
|
|
let checked_stdout = String::from_utf8_lossy(&checked.stdout);
|
|
assert!(
|
|
checked_stdout.contains("ok : (result string i32)")
|
|
&& checked_stdout.contains("err : (result string i32)")
|
|
&& checked_stdout.contains("call std.process.arg_result : (result string i32)")
|
|
&& checked_stdout.contains("call std.env.get_result : (result string i32)")
|
|
&& checked_stdout.contains("call std.fs.read_text_result : (result string i32)")
|
|
&& checked_stdout.contains("call std.fs.write_text_result : (result i32 i32)"),
|
|
"checked lowering lost exp-10 typed result host shape\nstdout:\n{}",
|
|
checked_stdout
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn exp10_fixture_emits_private_result_host_runtime_shape() {
|
|
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/host-io-result.slo");
|
|
let compile = run_glagol([fixture.as_os_str()]);
|
|
assert_success("compile exp-10 host result fixture", &compile);
|
|
let stdout = String::from_utf8_lossy(&compile.stdout);
|
|
|
|
assert!(
|
|
stdout.contains("__glagol_process_arg_result")
|
|
&& stdout.contains("__glagol_env_get_result")
|
|
&& stdout.contains("__glagol_fs_read_text_result")
|
|
&& stdout.contains("__glagol_fs_write_text_result")
|
|
&& stdout.contains("ptr")
|
|
&& stdout.contains("i32")
|
|
&& !stdout.contains("@std.process.arg_result")
|
|
&& !stdout.contains("@std.env.get_result")
|
|
&& !stdout.contains("@std.fs.read_text_result")
|
|
&& !stdout.contains("@std.fs.write_text_result"),
|
|
"LLVM output did not contain expected exp-10 private runtime shape\nstdout:\n{}",
|
|
stdout
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_runner_executes_deterministic_exp10_ok_and_err_paths() {
|
|
let root = temp_root("test-runner");
|
|
fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err));
|
|
let existing = root.join("existing.txt");
|
|
let roundtrip = root.join("roundtrip.txt");
|
|
let missing = root.join("missing.txt");
|
|
let unwritable = root.join("missing-dir").join("out.txt");
|
|
fs::write(&existing, "fixture text")
|
|
.unwrap_or_else(|err| panic!("write `{}`: {}", existing.display(), err));
|
|
|
|
let missing_env = format!(
|
|
"GLAGOL_EXP10_MISSING_{}_{}",
|
|
std::process::id(),
|
|
NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed)
|
|
);
|
|
let source = format!(
|
|
r#"
|
|
(module main)
|
|
|
|
(test "env present result ok"
|
|
(= (unwrap_ok (std.env.get_result "GLAGOL_EXP10_PRESENT")) "env-value"))
|
|
|
|
(test "env missing result err one"
|
|
(= (unwrap_err (std.env.get_result "{}")) 1))
|
|
|
|
(test "arg zero result ok"
|
|
(is_ok (std.process.arg_result 0)))
|
|
|
|
(test "arg negative result err one"
|
|
(= (unwrap_err (std.process.arg_result -1)) 1))
|
|
|
|
(test "arg out of range result err one"
|
|
(= (unwrap_err (std.process.arg_result 99999)) 1))
|
|
|
|
(test "read text result ok"
|
|
(= (unwrap_ok (std.fs.read_text_result "{}")) "fixture text"))
|
|
|
|
(test "read text result err one"
|
|
(= (unwrap_err (std.fs.read_text_result "{}")) 1))
|
|
|
|
(test "write text result ok zero"
|
|
(= (unwrap_ok (std.fs.write_text_result "{}" "roundtrip")) 0))
|
|
|
|
(test "write text result err one"
|
|
(= (unwrap_err (std.fs.write_text_result "{}" "nope")) 1))
|
|
|
|
(test "read written text result ok"
|
|
(= (unwrap_ok (std.fs.read_text_result "{}")) "roundtrip"))
|
|
|
|
(test "string payload can be matched"
|
|
(= (match (std.fs.read_text_result "{}")
|
|
((ok payload)
|
|
(std.string.len payload))
|
|
((err code)
|
|
code))
|
|
12))
|
|
|
|
(test "stdin result deterministic ok"
|
|
(is_ok (std.io.read_stdin_result)))
|
|
|
|
(test "stdin result deterministic empty payload"
|
|
(= (std.string.len (unwrap_ok (std.io.read_stdin_result))) 0))
|
|
"#,
|
|
missing_env,
|
|
slovo_path(&existing),
|
|
slovo_path(&missing),
|
|
slovo_path(&roundtrip),
|
|
slovo_path(&unwritable),
|
|
slovo_path(&roundtrip),
|
|
slovo_path(&existing)
|
|
);
|
|
let fixture = write_fixture("test-runner", &source);
|
|
let run = run_glagol_configured([OsStr::new("test"), fixture.as_os_str()], |command| {
|
|
command
|
|
.env("GLAGOL_EXP10_PRESENT", "env-value")
|
|
.env_remove(&missing_env);
|
|
});
|
|
|
|
assert_success_stdout(
|
|
run,
|
|
concat!(
|
|
"test \"env present result ok\" ... ok\n",
|
|
"test \"env missing result err one\" ... ok\n",
|
|
"test \"arg zero result ok\" ... ok\n",
|
|
"test \"arg negative result err one\" ... ok\n",
|
|
"test \"arg out of range result err one\" ... ok\n",
|
|
"test \"read text result ok\" ... ok\n",
|
|
"test \"read text result err one\" ... ok\n",
|
|
"test \"write text result ok zero\" ... ok\n",
|
|
"test \"write text result err one\" ... ok\n",
|
|
"test \"read written text result ok\" ... ok\n",
|
|
"test \"string payload can be matched\" ... ok\n",
|
|
"test \"stdin result deterministic ok\" ... ok\n",
|
|
"test \"stdin result deterministic empty payload\" ... ok\n",
|
|
"13 test(s) passed\n",
|
|
),
|
|
"exp-10 test-runner host result output",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn exp3_host_calls_keep_trap_and_status_behavior() {
|
|
let root = temp_root("exp3-regression");
|
|
fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err));
|
|
let unwritable = root.join("missing-dir").join("out.txt");
|
|
let missing_env = format!(
|
|
"GLAGOL_EXP10_EXP3_MISSING_{}_{}",
|
|
std::process::id(),
|
|
NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed)
|
|
);
|
|
let source = format!(
|
|
r#"
|
|
(module main)
|
|
|
|
(test "exp3 missing env is still empty"
|
|
(= (std.env.get "{}") ""))
|
|
|
|
(test "exp3 write failure is still status one"
|
|
(= (std.fs.write_text "{}" "nope") 1))
|
|
"#,
|
|
missing_env,
|
|
slovo_path(&unwritable)
|
|
);
|
|
let fixture = write_fixture("exp3-regression", &source);
|
|
let run = run_glagol_configured([OsStr::new("test"), fixture.as_os_str()], |command| {
|
|
command.env_remove(&missing_env);
|
|
});
|
|
|
|
assert_success_stdout(
|
|
run,
|
|
concat!(
|
|
"test \"exp3 missing env is still empty\" ... ok\n",
|
|
"test \"exp3 write failure is still status one\" ... ok\n",
|
|
"2 test(s) passed\n",
|
|
),
|
|
"exp-3 host regression output",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn exp10_diagnostics_cover_promoted_and_deferred_boundaries() {
|
|
let cases = [
|
|
(
|
|
"arg-result-arity",
|
|
r#"
|
|
(module main)
|
|
|
|
(fn main () -> i32
|
|
(std.process.arg_result)
|
|
0)
|
|
"#,
|
|
"ArityMismatch",
|
|
),
|
|
(
|
|
"env-result-type",
|
|
r#"
|
|
(module main)
|
|
|
|
(fn main () -> i32
|
|
(std.env.get_result 1)
|
|
0)
|
|
"#,
|
|
"TypeMismatch",
|
|
),
|
|
(
|
|
"write-result-type",
|
|
r#"
|
|
(module main)
|
|
|
|
(fn main () -> i32
|
|
(std.fs.write_text_result "path" 1)
|
|
0)
|
|
"#,
|
|
"TypeMismatch",
|
|
),
|
|
(
|
|
"fs-open-handle-type",
|
|
r#"
|
|
(module main)
|
|
|
|
(fn main () -> i32
|
|
(std.fs.open_text_read_result 1)
|
|
0)
|
|
"#,
|
|
"TypeMismatch",
|
|
),
|
|
(
|
|
"fs-close-handle-type",
|
|
r#"
|
|
(module main)
|
|
|
|
(fn main () -> i32
|
|
(std.fs.close_result "handle")
|
|
0)
|
|
"#,
|
|
"TypeMismatch",
|
|
),
|
|
(
|
|
"unsupported-result-payload-family",
|
|
r#"
|
|
(module main)
|
|
|
|
(fn main () -> i32
|
|
(ok string bool "value"))
|
|
"#,
|
|
"UnsupportedResultPayloadType",
|
|
),
|
|
(
|
|
"result-equality",
|
|
r#"
|
|
(module main)
|
|
|
|
(fn main () -> i32
|
|
(if (= (ok string i32 "a") (err string i32 1)) 1 0))
|
|
"#,
|
|
"UnsupportedOptionResultEquality",
|
|
),
|
|
(
|
|
"result-printing",
|
|
r#"
|
|
(module main)
|
|
|
|
(fn main () -> i32
|
|
(std.io.print_i32 (ok string i32 "a"))
|
|
0)
|
|
"#,
|
|
"UnsupportedOptionResultPrint",
|
|
),
|
|
(
|
|
"result-mapping",
|
|
r#"
|
|
(module main)
|
|
|
|
(fn main () -> i32
|
|
(std.result.map (ok string i32 "a"))
|
|
0)
|
|
"#,
|
|
"UnsupportedStandardLibraryCall",
|
|
),
|
|
(
|
|
"promoted-name-shadow",
|
|
r#"
|
|
(module main)
|
|
|
|
(fn std.process.arg_result ((index i32)) -> (result string i32)
|
|
(err string i32 1))
|
|
|
|
(fn main () -> i32
|
|
0)
|
|
"#,
|
|
"DuplicateFunction",
|
|
),
|
|
(
|
|
"helper-shadow",
|
|
r#"
|
|
(module main)
|
|
|
|
(fn __glagol_process_arg_result ((index i32)) -> (result string i32)
|
|
(err string i32 1))
|
|
|
|
(fn main () -> i32
|
|
0)
|
|
"#,
|
|
"DuplicateFunction",
|
|
),
|
|
];
|
|
|
|
for (name, source, expected_code) 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 exp-10 diagnostic case `{}` wrote stdout:\n{}",
|
|
name,
|
|
stdout
|
|
);
|
|
assert!(
|
|
stderr.contains(expected_code),
|
|
"diagnostic `{}` was not reported for `{}`\nstderr:\n{}",
|
|
expected_code,
|
|
name,
|
|
stderr
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn beta2_file_resource_handles_execute_in_test_runner() {
|
|
let root = temp_root("resource-test-runner");
|
|
fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err));
|
|
let existing = root.join("resource.txt");
|
|
let missing = root.join("missing-resource.txt");
|
|
fs::write(&existing, "resource text")
|
|
.unwrap_or_else(|err| panic!("write `{}`: {}", existing.display(), err));
|
|
|
|
let source = format!(
|
|
r#"
|
|
(module main)
|
|
|
|
(fn read_len_and_close ((path string)) -> i32
|
|
(match (std.fs.open_text_read_result path)
|
|
((ok handle)
|
|
(let text string (unwrap_ok (std.fs.read_open_text_result handle)))
|
|
(let close_status i32 (unwrap_ok (std.fs.close_result handle)))
|
|
(+ (std.string.len text) close_status))
|
|
((err code)
|
|
code)))
|
|
|
|
(fn read_after_close_err ((path string)) -> bool
|
|
(match (std.fs.open_text_read_result path)
|
|
((ok handle)
|
|
(std.fs.close_result handle)
|
|
(= (unwrap_err (std.fs.read_open_text_result handle)) 1))
|
|
((err code)
|
|
false)))
|
|
|
|
(test "resource handle open read close"
|
|
(= (read_len_and_close "{}") 13))
|
|
|
|
(test "resource handle missing open is err one"
|
|
(= (unwrap_err (std.fs.open_text_read_result "{}")) 1))
|
|
|
|
(test "resource handle closed read is err one"
|
|
(read_after_close_err "{}"))
|
|
|
|
(test "resource handle invalid close is err one"
|
|
(= (unwrap_err (std.fs.close_result -1)) 1))
|
|
"#,
|
|
slovo_path(&existing),
|
|
slovo_path(&missing),
|
|
slovo_path(&existing)
|
|
);
|
|
let fixture = write_fixture("resource-test-runner", &source);
|
|
let run = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
|
|
|
|
assert_success_stdout(
|
|
run,
|
|
concat!(
|
|
"test \"resource handle open read close\" ... ok\n",
|
|
"test \"resource handle missing open is err one\" ... ok\n",
|
|
"test \"resource handle closed read is err one\" ... ok\n",
|
|
"test \"resource handle invalid close is err one\" ... ok\n",
|
|
"4 test(s) passed\n",
|
|
),
|
|
"beta.2 resource handle test-runner output",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn beta2_file_resource_handles_emit_expected_private_runtime_shape() {
|
|
let source = r#"
|
|
(module main)
|
|
|
|
(fn main () -> i32
|
|
(match (std.fs.open_text_read_result "missing.txt")
|
|
((ok handle)
|
|
(std.string.len (unwrap_ok (std.fs.read_open_text_result handle))))
|
|
((err code)
|
|
(unwrap_err (std.fs.close_result -1)))))
|
|
"#;
|
|
let fixture = write_fixture("resource-lowering", source);
|
|
let compile = run_glagol([fixture.as_os_str()]);
|
|
assert_success("compile beta.2 resource handle lowering", &compile);
|
|
let stdout = String::from_utf8_lossy(&compile.stdout);
|
|
|
|
assert!(
|
|
stdout.contains("__glagol_fs_open_text_read_result")
|
|
&& stdout.contains("__glagol_fs_read_open_text_result")
|
|
&& stdout.contains("__glagol_fs_close_result")
|
|
&& stdout.contains("declare i64 @__glagol_fs_open_text_read_result(ptr)")
|
|
&& stdout.contains("declare ptr @__glagol_fs_read_open_text_result(i32)")
|
|
&& stdout.contains("declare i32 @__glagol_fs_close_result(i32)")
|
|
&& !stdout.contains("@std.fs.open_text_read_result")
|
|
&& !stdout.contains("@std.fs.read_open_text_result")
|
|
&& !stdout.contains("@std.fs.close_result"),
|
|
"LLVM output did not contain expected beta.2 resource runtime shape\nstdout:\n{}",
|
|
stdout
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hosted_runtime_executes_beta2_file_resource_handles_when_clang_is_available() {
|
|
let Some(clang) = find_clang() else {
|
|
eprintln!("skipping beta.2 resource runtime smoke: set GLAGOL_CLANG or install clang");
|
|
return;
|
|
};
|
|
|
|
let root = temp_root("resource-native");
|
|
fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err));
|
|
let existing = root.join("native-resource.txt");
|
|
fs::write(&existing, "native handle")
|
|
.unwrap_or_else(|err| panic!("write `{}`: {}", existing.display(), err));
|
|
|
|
let source = format!(
|
|
r#"
|
|
(module main)
|
|
|
|
(fn main () -> i32
|
|
(match (std.fs.open_text_read_result "{}")
|
|
((ok handle)
|
|
(let text string (unwrap_ok (std.fs.read_open_text_result handle)))
|
|
(let close_status i32 (unwrap_ok (std.fs.close_result handle)))
|
|
(+ (std.string.len text) close_status))
|
|
((err code)
|
|
code)))
|
|
"#,
|
|
slovo_path(&existing)
|
|
);
|
|
let fixture = write_fixture("resource-native", &source);
|
|
let compile = run_glagol([fixture.as_os_str()]);
|
|
assert_success("compile beta.2 resource native smoke", &compile);
|
|
|
|
let run =
|
|
compile_and_run_with_runtime(&clang, "beta2-resource-native", &compile.stdout, |_| {});
|
|
assert_eq!(
|
|
run.status.code(),
|
|
Some(13),
|
|
"beta.2 resource native smoke exit code drifted\nstdout:\n{}\nstderr:\n{}",
|
|
String::from_utf8_lossy(&run.stdout),
|
|
String::from_utf8_lossy(&run.stderr)
|
|
);
|
|
assert!(
|
|
run.stdout.is_empty(),
|
|
"beta.2 resource native smoke wrote stdout:\n{}",
|
|
String::from_utf8_lossy(&run.stdout)
|
|
);
|
|
assert!(
|
|
run.stderr.is_empty(),
|
|
"beta.2 resource native smoke wrote stderr:\n{}",
|
|
String::from_utf8_lossy(&run.stderr)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hosted_runtime_executes_exp10_results_when_clang_is_available() {
|
|
let Some(clang) = find_clang() else {
|
|
eprintln!("skipping exp-10 runtime smoke: set GLAGOL_CLANG or install clang");
|
|
return;
|
|
};
|
|
|
|
let root = temp_root("native");
|
|
fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err));
|
|
let output = root.join("native.txt");
|
|
let missing = root.join("missing.txt");
|
|
let source = format!(
|
|
r#"
|
|
(module main)
|
|
|
|
(fn write_status () -> i32
|
|
(match (std.fs.write_text_result "{}" "native text")
|
|
((ok code)
|
|
code)
|
|
((err code)
|
|
code)))
|
|
|
|
(fn main () -> i32
|
|
(std.io.print_string (unwrap_ok (std.env.get_result "GLAGOL_EXP10_NATIVE_PRESENT")))
|
|
(std.io.print_string (unwrap_ok (std.process.arg_result 1)))
|
|
(std.io.print_i32 (write_status))
|
|
(std.io.print_string (unwrap_ok (std.fs.read_text_result "{}")))
|
|
(unwrap_err (std.fs.read_text_result "{}")))
|
|
"#,
|
|
slovo_path(&output),
|
|
slovo_path(&output),
|
|
slovo_path(&missing)
|
|
);
|
|
let fixture = write_fixture("native", &source);
|
|
let compile = run_glagol([fixture.as_os_str()]);
|
|
assert_success("compile exp-10 native smoke", &compile);
|
|
|
|
let run = compile_and_run_with_runtime(&clang, "exp10-native", &compile.stdout, |command| {
|
|
command
|
|
.arg("argv-native")
|
|
.env("GLAGOL_EXP10_NATIVE_PRESENT", "env-native");
|
|
});
|
|
|
|
assert_eq!(
|
|
run.status.code(),
|
|
Some(1),
|
|
"exp-10 native smoke 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),
|
|
"env-native\nargv-native\n0\nnative text\n",
|
|
"exp-10 native smoke stdout drifted"
|
|
);
|
|
assert!(
|
|
run.stderr.is_empty(),
|
|
"exp-10 native smoke wrote stderr:\n{}",
|
|
String::from_utf8_lossy(&run.stderr)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hosted_runtime_executes_exp12_stdin_result_when_clang_is_available() {
|
|
let Some(clang) = find_clang() else {
|
|
eprintln!("skipping exp-12 stdin runtime smoke: set GLAGOL_CLANG or install clang");
|
|
return;
|
|
};
|
|
|
|
let source = r#"
|
|
(module main)
|
|
|
|
(fn stdin_len_or_code () -> i32
|
|
(match (std.io.read_stdin_result)
|
|
((ok text)
|
|
(std.string.len text))
|
|
((err code)
|
|
code)))
|
|
|
|
(fn main () -> i32
|
|
(stdin_len_or_code))
|
|
"#;
|
|
let fixture = write_fixture("stdin-native", source);
|
|
let compile = run_glagol([fixture.as_os_str()]);
|
|
assert_success("compile exp-12 stdin native smoke", &compile);
|
|
let ir = String::from_utf8_lossy(&compile.stdout);
|
|
assert!(
|
|
ir.contains("declare ptr @__glagol_io_read_stdin_result()")
|
|
&& ir.contains("call ptr @__glagol_io_read_stdin_result()")
|
|
&& !ir.contains("@std.io.read_stdin_result"),
|
|
"LLVM output did not contain expected exp-12 stdin runtime shape\nstdout:\n{}",
|
|
ir
|
|
);
|
|
|
|
let run = compile_and_run_with_runtime_input(
|
|
&clang,
|
|
"exp12-stdin-native",
|
|
&compile.stdout,
|
|
b"native stdin",
|
|
|_| {},
|
|
);
|
|
assert_eq!(
|
|
run.status.code(),
|
|
Some(12),
|
|
"exp-12 stdin native smoke exit code drifted\nstdout:\n{}\nstderr:\n{}",
|
|
String::from_utf8_lossy(&run.stdout),
|
|
String::from_utf8_lossy(&run.stderr)
|
|
);
|
|
assert!(
|
|
run.stdout.is_empty(),
|
|
"exp-12 stdin native smoke wrote stdout:\n{}",
|
|
String::from_utf8_lossy(&run.stdout)
|
|
);
|
|
assert!(
|
|
run.stderr.is_empty(),
|
|
"exp-12 stdin native smoke wrote stderr:\n{}",
|
|
String::from_utf8_lossy(&run.stderr)
|
|
);
|
|
|
|
let eof =
|
|
compile_and_run_with_runtime_input(&clang, "exp12-stdin-eof", &compile.stdout, b"", |_| {});
|
|
assert_eq!(
|
|
eof.status.code(),
|
|
Some(0),
|
|
"exp-12 stdin EOF should be ok empty string\nstdout:\n{}\nstderr:\n{}",
|
|
String::from_utf8_lossy(&eof.stdout),
|
|
String::from_utf8_lossy(&eof.stderr)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hosted_runtime_keeps_exp3_traps_when_clang_is_available() {
|
|
let Some(clang) = find_clang() else {
|
|
eprintln!("skipping exp-3 runtime regression: set GLAGOL_CLANG or install clang");
|
|
return;
|
|
};
|
|
|
|
let source = r#"
|
|
(module main)
|
|
|
|
(fn main () -> i32
|
|
(std.string.len (std.process.arg 99)))
|
|
"#;
|
|
let fixture = write_fixture("exp3-native-trap", source);
|
|
let compile = run_glagol([fixture.as_os_str()]);
|
|
assert_success("compile exp-3 native trap regression", &compile);
|
|
|
|
let run = compile_and_run_with_runtime(&clang, "exp3-native-trap", &compile.stdout, |_| {});
|
|
assert_eq!(
|
|
run.status.code(),
|
|
Some(1),
|
|
"exp-3 native trap 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: process argument index out of bounds\n",
|
|
"exp-3 native trap stderr drifted"
|
|
);
|
|
}
|
|
|
|
fn run_glagol<I, S>(args: I) -> Output
|
|
where
|
|
I: IntoIterator<Item = S>,
|
|
S: AsRef<OsStr>,
|
|
{
|
|
run_glagol_configured(args, |_| {})
|
|
}
|
|
|
|
fn run_glagol_configured<I, S, F>(args: I, configure: F) -> Output
|
|
where
|
|
I: IntoIterator<Item = S>,
|
|
S: AsRef<OsStr>,
|
|
F: FnOnce(&mut Command),
|
|
{
|
|
let mut command = Command::new(env!("CARGO_BIN_EXE_glagol"));
|
|
command
|
|
.args(args)
|
|
.current_dir(Path::new(env!("CARGO_MANIFEST_DIR")));
|
|
configure(&mut command);
|
|
command.output().expect("run glagol")
|
|
}
|
|
|
|
fn write_fixture(name: &str, source: &str) -> PathBuf {
|
|
let mut path = env::temp_dir();
|
|
path.push(format!(
|
|
"glagol-exp10-host-result-{}-{}-{}.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-exp10-host-result-{}-{}-{}",
|
|
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
|
|
);
|
|
assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, 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_status_success("clang exp-10 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 compile_and_run_with_runtime_input<F>(
|
|
clang: &Path,
|
|
name: &str,
|
|
ir: &[u8],
|
|
input: &[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_status_success("clang exp-12 runtime smoke", &clang_output);
|
|
|
|
let mut run = Command::new(&exe_path);
|
|
run.stdin(Stdio::piped());
|
|
configure(&mut run);
|
|
let mut child = run
|
|
.spawn()
|
|
.unwrap_or_else(|err| panic!("run `{}`: {}", exe_path.display(), err));
|
|
let mut stdin = child
|
|
.stdin
|
|
.take()
|
|
.unwrap_or_else(|| panic!("open stdin for `{}`", exe_path.display()));
|
|
stdin
|
|
.write_all(input)
|
|
.unwrap_or_else(|err| panic!("write stdin to `{}`: {}", exe_path.display(), err));
|
|
drop(stdin);
|
|
child
|
|
.wait_with_output()
|
|
.unwrap_or_else(|err| panic!("wait for `{}`: {}", 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 assert_status_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 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);
|
|
}
|