use std::io::{Read, Write};
use std::net::TcpStream;
use std::process::{Child, Command};
use std::time::{Duration, Instant};
fn ilo() -> Command {
Command::new(env!("CARGO_BIN_EXE_ilo"))
}
fn free_port() -> u16 {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind ephemeral port");
listener.local_addr().expect("local_addr").port()
}
fn spawn_httpd(handler: &std::path::Path, port: u16) -> Child {
let child = ilo()
.args([
"httpd",
"--port",
&port.to_string(),
handler.to_str().unwrap(),
])
.stderr(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.expect("spawn ilo httpd");
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
if TcpStream::connect(("127.0.0.1", port)).is_ok() {
return child;
}
std::thread::sleep(Duration::from_millis(50));
}
child
}
fn http_get(port: u16, path: &str) -> String {
let mut stream = TcpStream::connect(("127.0.0.1", port)).expect("connect to ilo httpd");
let req = format!("GET {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n");
stream.write_all(req.as_bytes()).expect("write request");
stream.flush().ok();
let mut buf = String::new();
stream.read_to_string(&mut buf).expect("read response");
buf
}
#[test]
fn handler_uses_sibling_module() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(
dir.path().join("store.ilo"),
"greet name:t>t\n +\"hello, \" name\n",
)
.expect("write module");
std::fs::write(
dir.path().join("handler.ilo"),
"use \"store.ilo\"\ntype rsp{status:n;body:t}\nhandler req:_>rsp\n rsp status:200 body:(greet \"world\")\n",
)
.expect("write handler");
let port = free_port();
let mut child = spawn_httpd(&dir.path().join("handler.ilo"), port);
let resp = http_get(port, "/");
child.kill().ok();
let stderr = {
let mut s = String::new();
if let Some(mut e) = child.stderr.take() {
let _ = e.read_to_string(&mut s);
}
s
};
child.wait().ok();
assert!(
resp.contains("hello, world"),
"expected imported function output in response, got: {resp:?}\nserver stderr: {stderr}"
);
}
#[test]
fn missing_import_surfaces_diagnostic() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(
dir.path().join("handler.ilo"),
"use \"does-not-exist.ilo\"\ntype rsp{status:n;body:t}\nhandler req:_>rsp\n rsp status:200 body:\"x\"\n",
)
.expect("write handler");
let out = ilo()
.args([
"httpd",
"--port",
"0",
dir.path().join("handler.ilo").to_str().unwrap(),
])
.output()
.expect("run ilo httpd");
assert!(
!out.status.success(),
"expected non-zero exit for missing import, stdout={:?} stderr={:?}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("does-not-exist.ilo") || stderr.contains("P017"),
"expected an import diagnostic mentioning the missing module, got: {stderr}"
);
}
#[test]
fn single_file_handler_still_works() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(
dir.path().join("handler.ilo"),
"type rsp{status:n;body:t}\nhandler req:_>rsp\n rsp status:200 body:\"standalone\"\n",
)
.expect("write handler");
let port = free_port();
let mut child = spawn_httpd(&dir.path().join("handler.ilo"), port);
let resp = http_get(port, "/");
child.kill().ok();
child.wait().ok();
assert!(
resp.contains("standalone"),
"expected single-file handler response, got: {resp:?}"
);
}