use std::io::{Read, Write};
use std::net::{SocketAddr, TcpStream};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use lex_api::handlers::State;
use tempfile::TempDir;
struct Server {
addr: SocketAddr,
_join: Option<thread::JoinHandle<()>>,
_server_holder: Arc<()>,
}
fn start_server() -> (Server, TempDir) {
let tmp = TempDir::new().unwrap();
let server = tiny_http::Server::http(("127.0.0.1", 0)).expect("bind ephemeral port");
let addr: SocketAddr = match server.server_addr() {
tiny_http::ListenAddr::IP(addr) => addr,
_ => panic!("expected IP listener"),
};
let state = Arc::new(State::open(tmp.path().to_path_buf()).unwrap());
let join = thread::spawn(move || {
lex_api::serve_on(server, state);
});
wait_until_serving(&addr);
(Server { addr, _join: Some(join), _server_holder: Arc::new(()) }, tmp)
}
fn wait_until_serving(addr: &SocketAddr) {
let deadline = std::time::Instant::now() + Duration::from_secs(10);
let probe = b"GET /v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n";
while std::time::Instant::now() < deadline {
if let Ok(mut s) = TcpStream::connect_timeout(addr, Duration::from_millis(200)) {
s.set_read_timeout(Some(Duration::from_millis(200))).ok();
if s.write_all(probe).is_ok() {
let mut buf = [0u8; 16];
if s.read(&mut buf).is_ok() && buf.starts_with(b"HTTP/1.1 200") {
return;
}
}
}
thread::sleep(Duration::from_millis(20));
}
panic!("test server never became ready within 10s");
}
fn post_bytes(addr: &SocketAddr, path: &str, body: &[u8]) -> (u16, String) {
let mut req = format!(
"POST {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
).into_bytes();
req.extend_from_slice(body);
let deadline = std::time::Instant::now() + Duration::from_secs(30);
loop {
match try_post(addr, &req) {
Ok(result) => return result,
Err(e) => {
if std::time::Instant::now() >= deadline {
panic!("POST {path} failed after retries: {e}");
}
thread::sleep(Duration::from_millis(50));
}
}
}
}
fn try_post(addr: &SocketAddr, req: &[u8]) -> Result<(u16, String), String> {
let mut s = TcpStream::connect_timeout(addr, Duration::from_secs(5)).map_err(|e| e.to_string())?;
s.set_read_timeout(Some(Duration::from_secs(15))).map_err(|e| e.to_string())?;
s.write_all(req).map_err(|e| e.to_string())?;
let mut buf = Vec::new();
s.read_to_end(&mut buf).map_err(|e| e.to_string())?;
if buf.is_empty() {
return Err("empty response".into());
}
let text = String::from_utf8_lossy(&buf);
let (head, body) = text.split_once("\r\n\r\n").unwrap_or((&text, ""));
let status = head.split_whitespace().nth(1).unwrap_or("0").parse().unwrap_or(0);
Ok((status, body.to_string()))
}
fn pkg_archive(name: &str, version: &str, src_files: &[(&str, &str)]) -> Vec<u8> {
let toml = format!("[package]\nname = \"{name}\"\nversion = \"{version}\"\n");
let mut files: Vec<(String, &str)> = vec![("lex.toml".to_string(), toml.as_str())];
for (path, contents) in src_files {
files.push((format!("src/{path}"), contents));
}
let mut enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
{
let mut ar = tar::Builder::new(&mut enc);
for (path, contents) in &files {
let mut header = tar::Header::new_gnu();
header.set_size(contents.len() as u64);
header.set_mode(0o644);
header.set_cksum();
ar.append_data(&mut header, path, contents.as_bytes()).unwrap();
}
ar.finish().unwrap();
}
enc.finish().unwrap()
}
#[test]
fn multi_file_publish_sees_earlier_files_own_update_in_same_request() {
let (srv, _tmp) = start_server();
let src_v1 = concat!(
"fn counter() -> Int\n",
" examples {\n",
" counter() => 1,\n",
" }\n",
"{ 1 }\n",
);
let archive_v1 = pkg_archive("multi", "0.1.0", &[("lib.lex", src_v1)]);
let (status, body) = post_bytes(&srv.addr, "/v1/pkg/publish", &archive_v1);
assert_eq!(status, 200, "v1 publish must succeed, got: {body}");
let src_v2 = concat!(
"fn counter() -> Int\n",
" examples {\n",
" counter() => 2,\n",
" }\n",
"{ 2 }\n",
);
let archive_v2 = pkg_archive("multi", "0.2.0", &[("a.lex", src_v2), ("b.lex", src_v2)]);
let (status, body) = post_bytes(&srv.addr, "/v1/pkg/publish", &archive_v2);
assert_eq!(status, 200, "v2 publish must succeed, got: {body}");
let parsed: serde_json::Value = serde_json::from_str(&body).expect("valid JSON response");
let ops = parsed["ops"].as_array().expect("ops array in publish response");
let modify_ops: Vec<&serde_json::Value> = ops.iter()
.filter(|op| op["kind"]["op"] == "modify_body")
.collect();
assert_eq!(
modify_ops.len(), 1,
"expected exactly one modify_body op (file a's diff sees the real \
1->2 change; file b's diff should see its own already-published \
2 and emit nothing) -- got {} modify_body ops: {:#?}",
modify_ops.len(), ops,
);
}