use std::env;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
pub fn find_origin_binary() -> Option<PathBuf> {
if let Ok(val) = env::var("NODEDB_BIN") {
let p = PathBuf::from(&val);
if p.is_file() {
return Some(p);
}
}
let manifest = env::var("CARGO_MANIFEST_DIR").ok()?;
let workspace_root = Path::new(&manifest).parent()?.to_path_buf();
let target = env::var("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| workspace_root.join("target"));
for profile in ["debug", "release"] {
let candidate = target.join(profile).join("nodedb");
if candidate.is_file() {
return Some(candidate);
}
}
None
}
pub const ORIGIN_WS: &str = "ws://127.0.0.1:9090";
pub const ORIGIN_PGWIRE_ADDR: &str = "127.0.0.1:6432";
pub struct OriginServer {
child: Child,
pub ws_url: &'static str,
_data_dir: tempfile::TempDir,
_config_dir: Option<tempfile::TempDir>,
}
impl OriginServer {
pub fn try_spawn() -> Option<Self> {
let binary = find_origin_binary()?;
Some(Self::spawn_inner(binary, false))
}
pub fn try_spawn_with_pgwire() -> Option<Self> {
let binary = find_origin_binary()?;
Some(Self::spawn_inner(binary, true))
}
fn spawn_inner(binary: PathBuf, with_pgwire: bool) -> Self {
let data_dir = tempfile::tempdir().expect("create temp data dir for Origin");
let cfg_dir = tempfile::tempdir().expect("create temp config dir for Origin");
let cfg_path = cfg_dir.path().join("nodedb.toml");
let cfg_content = "[auth]\nmode = \"trust\"\nsuperuser_name = \"nodedb\"\nmin_password_length = 8\nmax_failed_logins = 10\nlockout_duration_secs = 300\nidle_timeout_secs = 0\nmax_connections_per_user = 0\npassword_expiry_days = 0\naudit_retention_days = 0\n";
std::fs::write(&cfg_path, cfg_content).expect("write Origin trust config file");
let mut cmd = Command::new(&binary);
cmd.arg(cfg_path.to_str().expect("config path is valid UTF-8"))
.env("NODEDB_DATA_DIR", data_dir.path())
.env_remove("RUST_LOG")
.stdout(Stdio::null())
.stderr(Stdio::null());
let config_dir = Some(cfg_dir);
let child = cmd
.spawn()
.unwrap_or_else(|e| panic!("failed to spawn Origin binary {}: {e}", binary.display()));
let ports: &[(&str, u16)] = if with_pgwire {
&[("sync WebSocket", 9090), ("pgwire", 6432)]
} else {
&[("sync WebSocket", 9090)]
};
let deadline = Instant::now() + Duration::from_secs(30);
'outer: loop {
let all_ready = ports
.iter()
.all(|(_, port)| std::net::TcpStream::connect(format!("127.0.0.1:{port}")).is_ok());
if all_ready {
break 'outer;
}
if Instant::now() > deadline {
let pending: Vec<&str> = ports
.iter()
.filter(|(_, port)| {
std::net::TcpStream::connect(format!("127.0.0.1:{port}")).is_err()
})
.map(|(name, _)| *name)
.collect();
panic!(
"Origin server did not become ready within 30 seconds.\n\
Pending ports: {pending:?}\n\
Binary: {}\n\
Data dir: {}",
binary.display(),
data_dir.path().display(),
);
}
std::thread::sleep(Duration::from_millis(100));
}
OriginServer {
child,
ws_url: ORIGIN_WS,
_data_dir: data_dir,
_config_dir: config_dir,
}
}
}
impl Drop for OriginServer {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
pub type OriginWs =
tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>;
pub async fn announce_collection(ws: &mut OriginWs, name: &str) {
use futures::SinkExt;
use nodedb_types::sync::wire::{
CollectionDescriptor, CollectionSchemaSyncMsg, SyncFrame, SyncMessageType,
};
use tokio_tungstenite::tungstenite::Message;
let msg = CollectionSchemaSyncMsg {
descriptor: CollectionDescriptor {
tenant_id: 1,
database_id: nodedb_types::id::DatabaseId::DEFAULT,
name: name.to_string(),
collection_type: nodedb_types::collection::CollectionType::document(),
bitemporal: false,
crdt: true,
fields: Vec::new(),
primary: nodedb_types::PrimaryEngine::Document,
vector_primary: None,
partition_strategy: nodedb_types::PartitionStrategy::default(),
declared_primary_key: None,
descriptor_version: 1,
},
creation_hlc: nodedb_types::hlc::Hlc::ZERO,
};
let bytes = SyncFrame::try_encode(SyncMessageType::CollectionSchema, &msg)
.expect("encode CollectionSchema frame")
.to_bytes();
ws.send(Message::Binary(bytes.into()))
.await
.expect("send CollectionSchema announce");
}
pub async fn connect_and_handshake(ws_url: &str) -> OriginWs {
use std::time::Duration;
use futures::SinkExt;
use futures::StreamExt;
use nodedb_types::sync::wire::{HandshakeAckMsg, HandshakeMsg, SyncFrame, SyncMessageType};
use nodedb_types::wire_version::WIRE_FORMAT_VERSION;
use tokio_tungstenite::tungstenite::Message;
let (mut ws, _) = tokio_tungstenite::connect_async(ws_url)
.await
.unwrap_or_else(|e| panic!("connect to Origin at {ws_url}: {e}"));
let hs = HandshakeMsg {
jwt_token: String::new(),
vector_clock: std::collections::HashMap::new(),
subscribed_shapes: Vec::new(),
client_version: "interop-test".into(),
lite_id: String::new(),
epoch: 0,
wire_version: WIRE_FORMAT_VERSION,
};
let frame_bytes = SyncFrame::try_encode(SyncMessageType::Handshake, &hs)
.expect("encode handshake frame")
.to_bytes();
ws.send(Message::Binary(frame_bytes.into()))
.await
.expect("send handshake");
let resp = tokio::time::timeout(Duration::from_secs(10), ws.next())
.await
.expect("handshake ack timeout")
.expect("stream ended before ack")
.expect("WebSocket error waiting for handshake ack");
let frame =
SyncFrame::from_bytes(resp.into_data().as_ref()).expect("decode handshake ack frame");
assert_eq!(
frame.msg_type,
SyncMessageType::HandshakeAck,
"expected HandshakeAck, got {:?}",
frame.msg_type
);
let ack: HandshakeAckMsg = frame.decode_body().expect("decode HandshakeAckMsg");
assert!(ack.success, "handshake rejected by Origin: {:?}", ack.error);
ws
}