497 lines
16 KiB
Rust
497 lines
16 KiB
Rust
use std::{
|
|
env,
|
|
ffi::OsStr,
|
|
fs,
|
|
io::{ErrorKind, Read, Write},
|
|
net::{Shutdown, TcpListener, TcpStream},
|
|
path::{Path, PathBuf},
|
|
process::{Child, Command, Output, Stdio},
|
|
sync::atomic::{AtomicUsize, Ordering},
|
|
thread,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0);
|
|
|
|
#[test]
|
|
fn standard_net_lowers_to_private_runtime_helpers() {
|
|
let fixture = write_fixture(
|
|
"lowering",
|
|
r#"
|
|
(module main)
|
|
|
|
(fn main () -> i32
|
|
(match (std.net.tcp_connect_loopback_result 1)
|
|
((ok client)
|
|
(std.net.tcp_write_text_result client "ping")
|
|
(std.net.tcp_read_all_result client)
|
|
(std.net.tcp_close_result client)
|
|
0)
|
|
((err code)
|
|
(match (std.net.tcp_listen_loopback_result 0)
|
|
((ok listener)
|
|
(std.net.tcp_bound_port_result listener)
|
|
(std.net.tcp_accept_result listener)
|
|
(std.net.tcp_close_result listener)
|
|
0)
|
|
((err listen_code)
|
|
listen_code)))))
|
|
"#,
|
|
);
|
|
let output = run_glagol([fixture.as_os_str()]);
|
|
assert_success("compile standard net lowering", &output);
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
|
|
assert!(
|
|
stdout.contains("declare i64 @__glagol_net_tcp_connect_loopback_result(i32)")
|
|
&& stdout.contains("declare i64 @__glagol_net_tcp_listen_loopback_result(i32)")
|
|
&& stdout.contains("declare i64 @__glagol_net_tcp_bound_port_result(i32)")
|
|
&& stdout.contains("declare i64 @__glagol_net_tcp_accept_result(i32)")
|
|
&& stdout.contains("declare ptr @__glagol_net_tcp_read_all_result(i32)")
|
|
&& stdout.contains("declare i32 @__glagol_net_tcp_write_text_result(i32, ptr)")
|
|
&& stdout.contains("declare i32 @__glagol_net_tcp_close_result(i32)")
|
|
&& stdout.contains("call i64 @__glagol_net_tcp_connect_loopback_result(i32 1)")
|
|
&& stdout.contains("call i32 @__glagol_net_tcp_write_text_result(")
|
|
&& stdout.contains("call ptr @__glagol_net_tcp_read_all_result(")
|
|
&& stdout.contains("call i64 @__glagol_net_tcp_listen_loopback_result(i32 0)")
|
|
&& stdout.contains("call i64 @__glagol_net_tcp_bound_port_result(")
|
|
&& stdout.contains("call i64 @__glagol_net_tcp_accept_result(")
|
|
&& stdout.contains("call i32 @__glagol_net_tcp_close_result(")
|
|
&& !stdout.contains("@std.net."),
|
|
"standard net LLVM shape drifted\nstdout:\n{}",
|
|
stdout
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_runner_reports_deterministic_net_result_errors() {
|
|
let fixture = write_fixture(
|
|
"test-runner",
|
|
r#"
|
|
(module main)
|
|
|
|
(test "connect invalid port returns err"
|
|
(= (unwrap_err (std.net.tcp_connect_loopback_result 0)) 1))
|
|
|
|
(test "listen invalid port returns err"
|
|
(= (unwrap_err (std.net.tcp_listen_loopback_result -1)) 1))
|
|
|
|
(test "bound port invalid handle returns err"
|
|
(= (unwrap_err (std.net.tcp_bound_port_result -1)) 1))
|
|
|
|
(test "accept invalid listener returns err"
|
|
(= (unwrap_err (std.net.tcp_accept_result -1)) 1))
|
|
|
|
(test "read invalid handle returns err"
|
|
(= (unwrap_err (std.net.tcp_read_all_result -1)) 1))
|
|
|
|
(test "write invalid handle returns err"
|
|
(= (unwrap_err (std.net.tcp_write_text_result -1 "ping")) 1))
|
|
|
|
(test "close invalid handle returns err"
|
|
(= (unwrap_err (std.net.tcp_close_result -1)) 1))
|
|
"#,
|
|
);
|
|
let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
|
|
assert_success("run standard net tests", &output);
|
|
assert_eq!(
|
|
String::from_utf8_lossy(&output.stdout),
|
|
concat!(
|
|
"test \"connect invalid port returns err\" ... ok\n",
|
|
"test \"listen invalid port returns err\" ... ok\n",
|
|
"test \"bound port invalid handle returns err\" ... ok\n",
|
|
"test \"accept invalid listener returns err\" ... ok\n",
|
|
"test \"read invalid handle returns err\" ... ok\n",
|
|
"test \"write invalid handle returns err\" ... ok\n",
|
|
"test \"close invalid handle returns err\" ... ok\n",
|
|
"7 test(s) passed\n",
|
|
),
|
|
"standard net test runner stdout drifted"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn standard_net_diagnostics_cover_promoted_and_unpromoted_names() {
|
|
let cases = [
|
|
(
|
|
"connect-arity",
|
|
r#"
|
|
(module main)
|
|
|
|
(fn main () -> i32
|
|
(std.net.tcp_connect_loopback_result))
|
|
"#,
|
|
"ArityMismatch",
|
|
),
|
|
(
|
|
"write-type",
|
|
r#"
|
|
(module main)
|
|
|
|
(fn main () -> i32
|
|
(std.net.tcp_write_text_result "handle" "ping")
|
|
0)
|
|
"#,
|
|
"TypeMismatch",
|
|
),
|
|
(
|
|
"unknown-net",
|
|
r#"
|
|
(module main)
|
|
|
|
(fn main () -> i32
|
|
(std.net.udp_bind_loopback_result 0))
|
|
"#,
|
|
"UnsupportedStandardLibraryCall",
|
|
),
|
|
(
|
|
"promoted-shadow",
|
|
r#"
|
|
(module main)
|
|
|
|
(fn std.net.tcp_close_result ((handle i32)) -> (result i32 i32)
|
|
(ok i32 i32 0))
|
|
|
|
(fn main () -> i32
|
|
(unwrap_ok (std.net.tcp_close_result 1)))
|
|
"#,
|
|
"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_loopback_client_and_server_smoke_when_clang_is_available() {
|
|
let Some(clang) = find_clang() else {
|
|
eprintln!("skipping standard net runtime smoke: set GLAGOL_CLANG or install clang");
|
|
return;
|
|
};
|
|
|
|
let Some(port) = try_reserve_loopback_port() else {
|
|
eprintln!("skipping standard net server smoke: loopback bind is not permitted");
|
|
return;
|
|
};
|
|
let server_source = format!(
|
|
r#"
|
|
(module main)
|
|
|
|
(fn main () -> i32
|
|
(match (std.net.tcp_listen_loopback_result {port})
|
|
((ok listener)
|
|
(match (std.net.tcp_accept_result listener)
|
|
((ok stream)
|
|
(match (std.net.tcp_read_all_result stream)
|
|
((ok text)
|
|
(std.net.tcp_write_text_result stream "pong")
|
|
(std.net.tcp_close_result stream)
|
|
(std.net.tcp_close_result listener)
|
|
(if (= text "ping") 0 2))
|
|
((err read_code)
|
|
(std.net.tcp_close_result stream)
|
|
(std.net.tcp_close_result listener)
|
|
3)))
|
|
((err accept_code)
|
|
(std.net.tcp_close_result listener)
|
|
4)))
|
|
((err listen_code)
|
|
5)))
|
|
"#
|
|
);
|
|
let server_fixture = write_fixture("runtime-server", &server_source);
|
|
let compile = run_glagol([server_fixture.as_os_str()]);
|
|
assert_success("compile standard net server smoke", &compile);
|
|
let server_exe = compile_with_runtime(&clang, "standard-net-server", &compile.stdout);
|
|
|
|
let mut child = Command::new(&server_exe)
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.unwrap_or_else(|err| panic!("spawn `{}`: {}", server_exe.display(), err));
|
|
|
|
let mut stream = connect_with_retry(port, &mut child);
|
|
stream
|
|
.write_all(b"ping")
|
|
.unwrap_or_else(|err| panic!("write ping to loopback server: {}", err));
|
|
stream
|
|
.shutdown(Shutdown::Write)
|
|
.expect("shutdown loopback client write half");
|
|
|
|
let mut response = String::new();
|
|
stream
|
|
.read_to_string(&mut response)
|
|
.unwrap_or_else(|err| panic!("read loopback server response: {}", err));
|
|
assert_eq!(response, "pong", "loopback response drifted");
|
|
|
|
let output = wait_child_with_timeout(child, Duration::from_secs(5));
|
|
assert_success("run standard net server smoke", &output);
|
|
assert!(
|
|
output.stdout.is_empty(),
|
|
"standard net server smoke wrote stdout:\n{}",
|
|
String::from_utf8_lossy(&output.stdout)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hosted_loopback_client_smoke_when_clang_is_available() {
|
|
let Some(clang) = find_clang() else {
|
|
eprintln!("skipping standard net client smoke: set GLAGOL_CLANG or install clang");
|
|
return;
|
|
};
|
|
|
|
let listener = match TcpListener::bind(("127.0.0.1", 0)) {
|
|
Ok(listener) => listener,
|
|
Err(err) if err.kind() == ErrorKind::PermissionDenied => {
|
|
eprintln!("skipping standard net client smoke: loopback bind is not permitted");
|
|
return;
|
|
}
|
|
Err(err) => panic!("bind loopback smoke listener: {}", err),
|
|
};
|
|
let port = listener
|
|
.local_addr()
|
|
.expect("loopback smoke listener addr")
|
|
.port();
|
|
let server = thread::spawn(move || {
|
|
let (mut stream, _) = listener.accept().expect("accept loopback client");
|
|
let mut request = [0u8; 4];
|
|
stream
|
|
.read_exact(&mut request)
|
|
.expect("read loopback client request");
|
|
assert_eq!(&request, b"ping");
|
|
stream
|
|
.write_all(b"pong")
|
|
.expect("write loopback client response");
|
|
});
|
|
|
|
let client_source = format!(
|
|
r#"
|
|
(module main)
|
|
|
|
(fn main () -> i32
|
|
(match (std.net.tcp_connect_loopback_result {port})
|
|
((ok client)
|
|
(std.net.tcp_write_text_result client "ping")
|
|
(match (std.net.tcp_read_all_result client)
|
|
((ok text)
|
|
(std.net.tcp_close_result client)
|
|
(if (= text "pong") 0 2))
|
|
((err read_code)
|
|
(std.net.tcp_close_result client)
|
|
3)))
|
|
((err connect_code)
|
|
4)))
|
|
"#
|
|
);
|
|
let client_fixture = write_fixture("runtime-client", &client_source);
|
|
let compile = run_glagol([client_fixture.as_os_str()]);
|
|
assert_success("compile standard net client smoke", &compile);
|
|
let run = compile_and_run_with_runtime(&clang, "standard-net-client", &compile.stdout);
|
|
assert_success("run standard net client smoke", &run);
|
|
assert!(
|
|
run.stdout.is_empty(),
|
|
"standard net client smoke wrote stdout:\n{}",
|
|
String::from_utf8_lossy(&run.stdout)
|
|
);
|
|
server.join().expect("loopback smoke server thread");
|
|
}
|
|
|
|
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-net-{}-{}-{}.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 try_reserve_loopback_port() -> Option<u16> {
|
|
let listener = match TcpListener::bind(("127.0.0.1", 0)) {
|
|
Ok(listener) => listener,
|
|
Err(err) if err.kind() == ErrorKind::PermissionDenied => return None,
|
|
Err(err) => panic!("reserve loopback port: {}", err),
|
|
};
|
|
Some(
|
|
listener
|
|
.local_addr()
|
|
.expect("reserved loopback addr")
|
|
.port(),
|
|
)
|
|
}
|
|
|
|
fn connect_with_retry(port: u16, child: &mut Child) -> TcpStream {
|
|
let deadline = Instant::now() + Duration::from_secs(5);
|
|
loop {
|
|
match TcpStream::connect(("127.0.0.1", port)) {
|
|
Ok(stream) => return stream,
|
|
Err(connect_err) => {
|
|
if let Some(status) = child.try_wait().expect("poll loopback server child") {
|
|
let mut stdout = String::new();
|
|
let mut stderr = String::new();
|
|
if let Some(mut out) = child.stdout.take() {
|
|
let _ = out.read_to_string(&mut stdout);
|
|
}
|
|
if let Some(mut err) = child.stderr.take() {
|
|
let _ = err.read_to_string(&mut stderr);
|
|
}
|
|
panic!(
|
|
"loopback server exited before connect: {}\nstdout:\n{}\nstderr:\n{}",
|
|
status, stdout, stderr
|
|
);
|
|
}
|
|
if Instant::now() >= deadline {
|
|
panic!("timed out connecting to loopback server: {}", connect_err);
|
|
}
|
|
thread::sleep(Duration::from_millis(20));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn wait_child_with_timeout(mut child: Child, timeout: Duration) -> Output {
|
|
let deadline = Instant::now() + timeout;
|
|
while Instant::now() < deadline {
|
|
if child.try_wait().expect("poll child").is_some() {
|
|
return child.wait_with_output().expect("collect child output");
|
|
}
|
|
thread::sleep(Duration::from_millis(20));
|
|
}
|
|
|
|
let _ = child.kill();
|
|
let output = child
|
|
.wait_with_output()
|
|
.expect("collect killed child output");
|
|
panic!(
|
|
"child timed out\nstdout:\n{}\nstderr:\n{}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
}
|
|
|
|
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 exe_path = compile_with_runtime(clang, name, ir);
|
|
Command::new(&exe_path)
|
|
.output()
|
|
.unwrap_or_else(|err| panic!("run `{}`: {}", exe_path.display(), err))
|
|
}
|
|
|
|
fn compile_with_runtime(clang: &Path, name: &str, ir: &[u8]) -> PathBuf {
|
|
let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
|
|
let temp_dir = env::temp_dir().join(format!(
|
|
"glagol-standard-net-{}-{}",
|
|
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 net runtime smoke", &clang_output);
|
|
exe_path
|
|
}
|
|
|
|
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);
|
|
}
|