use std::net::{TcpListener, TcpStream};
use std::path::PathBuf;
use std::process::{Child, Command};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};
pub struct EphemeralSqlite {
path: PathBuf,
}
impl EphemeralSqlite {
pub fn new() -> Self {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let name = format!(
"gize-test-{}-{}.db",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed)
);
Self {
path: std::env::temp_dir().join(name),
}
}
pub fn url(&self) -> String {
format!("sqlite://{}?mode=rwc", self.path.display())
}
}
impl Default for EphemeralSqlite {
fn default() -> Self {
Self::new()
}
}
impl Drop for EphemeralSqlite {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
let name = self
.path
.file_name()
.unwrap()
.to_string_lossy()
.into_owned();
for suffix in ["-wal", "-shm"] {
let mut side = self.path.clone();
side.set_file_name(format!("{name}{suffix}"));
let _ = std::fs::remove_file(&side);
}
}
}
pub fn free_port() -> u16 {
TcpListener::bind("127.0.0.1:0")
.expect("binding a free port")
.local_addr()
.expect("reading the local address")
.port()
}
pub struct App {
child: Child,
port: u16,
}
impl App {
pub fn spawn(binary: &str, db: &EphemeralSqlite) -> std::io::Result<Self> {
let port = free_port();
let child = Command::new(binary)
.env("DATABASE_URL", db.url())
.env("PORT", port.to_string())
.env("GIZE_JWT_SECRET", "gize-testing-secret")
.spawn()?;
let mut app = App { child, port };
app.wait_until_ready(Duration::from_secs(30))?;
Ok(app)
}
pub fn base_url(&self) -> String {
format!("http://127.0.0.1:{}", self.port)
}
fn wait_until_ready(&mut self, timeout: Duration) -> std::io::Result<()> {
let deadline = Instant::now() + timeout;
let addr = format!("127.0.0.1:{}", self.port);
loop {
if TcpStream::connect(&addr).is_ok() {
return Ok(());
}
if Instant::now() >= deadline {
return Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!("app did not start listening on {addr} within {timeout:?}"),
));
}
std::thread::sleep(Duration::from_millis(100));
}
}
}
impl Drop for App {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ephemeral_sqlite_urls_are_unique_and_rwc() {
let a = EphemeralSqlite::new();
let b = EphemeralSqlite::new();
assert_ne!(a.url(), b.url());
assert!(a.url().starts_with("sqlite://"));
assert!(a.url().ends_with("?mode=rwc"));
}
#[test]
fn free_port_is_bindable() {
let port = free_port();
assert!(
TcpListener::bind(("127.0.0.1", port)).is_ok(),
"expected the reported free port to be bindable"
);
}
}