use std::io::Read as _;
use std::process::{Child, Command};
static SERVER_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn temp_path(name: &str, ext: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!(
"rete_serve_cli_{}_{}.{}",
std::process::id(),
name,
ext
))
}
fn free_port() -> u16 {
std::net::TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port()
}
#[allow(clippy::zombie_processes)]
fn spawn_server(file: &std::path::Path, port: u16, extra: &[&str]) -> Child {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_rete"));
cmd.arg("serve")
.arg(file)
.arg("--bind")
.arg(format!("127.0.0.1:{port}"))
.args(extra)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
let mut child = cmd.spawn().expect("spawn rete serve");
for _ in 0..100 {
if ureq::get(&format!("http://127.0.0.1:{port}/"))
.call()
.is_ok()
{
return child;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
let _ = child.kill();
let _ = child.wait();
panic!("server did not come up on port {port}");
}
fn stop_server(mut child: Child) {
#[cfg(unix)]
{
let status = Command::new("kill")
.args(["-INT", &child.id().to_string()])
.status()
.expect("send SIGINT to rete serve");
assert!(status.success());
}
#[cfg(not(unix))]
child.kill().expect("stop rete serve");
let status = child.wait().expect("wait for rete serve");
assert!(status.success(), "rete serve did not shut down cleanly");
}
fn select_rows(port: u16, query: &str) -> Vec<rete_core::Binding> {
let body = ureq::post(&format!("http://127.0.0.1:{port}/sparql"))
.send_form(&[("query", query)])
.expect("query ok")
.into_string()
.unwrap();
rete_core::parse_sparql_json_results(&body).expect("valid results JSON")
}
fn post_update(port: u16, update: &str) -> Result<u16, u16> {
match ureq::post(&format!("http://127.0.0.1:{port}/sparql")).send_form(&[("update", update)]) {
Ok(r) => Ok(r.status()),
Err(ureq::Error::Status(code, _)) => Err(code),
Err(e) => panic!("transport: {e}"),
}
}
fn build_fixture(name: &str) -> std::path::PathBuf {
let nt = temp_path(name, "nt");
let file = temp_path(name, "rete");
std::fs::write(
&nt,
"<http://ex/a> <http://ex/name> \"Alice\" .\n\
<http://ex/b> <http://ex/name> \"Bob\" .\n",
)
.unwrap();
let out = Command::new(env!("CARGO_BIN_EXE_rete"))
.args([
"build",
nt.to_str().unwrap(),
"-o",
file.to_str().unwrap(),
"--no-pyramid",
])
.output()
.unwrap();
assert!(
out.status.success(),
"build: {}",
String::from_utf8_lossy(&out.stderr)
);
let _ = std::fs::remove_file(format!("{}.changes", file.display()));
file
}
#[test]
fn serve_query_update_snapshot_and_journal_replay() {
let _server_test_guard = SERVER_TEST_LOCK.lock().unwrap();
let file = build_fixture("replay");
let port = free_port();
let server = spawn_server(&file, port, &[]);
let all = "SELECT ?s ?n WHERE { ?s <http://ex/name> ?n } ORDER BY ?n";
assert_eq!(select_rows(port, all).len(), 2);
assert_eq!(
post_update(
port,
"INSERT DATA { <http://ex/c> <http://ex/name> \"Carol\" }"
),
Ok(204)
);
let rows = select_rows(port, all);
assert_eq!(rows.len(), 3);
assert!(rows.iter().any(|b| b["n"] == "\"Carol\""));
assert_eq!(
post_update(
port,
"DELETE { ?s <http://ex/name> \"Bob\" } INSERT { ?s <http://ex/name> \"Robert\" } \
WHERE { ?s <http://ex/name> \"Bob\" }"
),
Ok(204)
);
let rows = select_rows(port, all);
assert_eq!(rows.len(), 3);
assert!(rows.iter().any(|b| b["n"] == "\"Robert\""));
assert!(!rows.iter().any(|b| b["n"] == "\"Bob\""));
assert_eq!(
post_update(
port,
"DELETE DATA { <http://ex/a> <http://ex/name> \"Alice\" }"
),
Ok(204)
);
assert_eq!(select_rows(port, all).len(), 2);
let mut snapshot = Vec::new();
ureq::get(&format!("http://127.0.0.1:{port}/snapshot.rete"))
.call()
.unwrap()
.into_reader()
.read_to_end(&mut snapshot)
.unwrap();
let snap = rete_core::Rete::open(&snapshot).expect("snapshot opens");
let (_, rows) = rete_core::eval_sparql(&snap, all).unwrap();
assert_eq!(rows.len(), 2, "snapshot carries the updated state");
let base = rete_core::Rete::open(&std::fs::read(&file).unwrap()).unwrap();
let (_, rows) = rete_core::eval_sparql(&base, all).unwrap();
assert_eq!(rows.len(), 2, "base file untouched (still Alice+Bob)");
assert!(
rows.iter().any(|b| b["n"] == "\"Alice\""),
"base still has Alice — updates went to the journal only"
);
stop_server(server);
let port2 = free_port();
let server2 = spawn_server(&file, port2, &[]);
let rows = select_rows(port2, all);
assert_eq!(rows.len(), 2);
assert!(rows.iter().any(|b| b["n"] == "\"Robert\""));
assert!(!rows.iter().any(|b| b["n"] == "\"Alice\""));
stop_server(server2);
let _ = std::fs::remove_file(format!("{}.changes", file.display()));
}
#[test]
fn serve_update_token_guards_writes_not_reads() {
let _server_test_guard = SERVER_TEST_LOCK.lock().unwrap();
let file = build_fixture("guarded");
let journal = temp_path("guarded", "changes");
let _ = std::fs::remove_file(&journal);
let port = free_port();
let server = spawn_server(
&file,
port,
&["--token", "s3cret", "--journal", journal.to_str().unwrap()],
);
let all = "SELECT ?s WHERE { ?s <http://ex/name> ?n }";
assert_eq!(select_rows(port, all).len(), 2);
assert_eq!(
post_update(port, "INSERT DATA { <http://ex/x> <http://ex/name> \"X\" }"),
Err(401)
);
assert_eq!(select_rows(port, all).len(), 2);
let status = ureq::post(&format!("http://127.0.0.1:{port}/sparql"))
.set("Authorization", "Bearer s3cret")
.send_form(&[(
"update",
"INSERT DATA { <http://ex/x> <http://ex/name> \"X\" }",
)])
.unwrap()
.status();
assert_eq!(status, 204);
assert_eq!(select_rows(port, all).len(), 3);
stop_server(server);
let _ = std::fs::remove_file(&journal);
}