use std::net::TcpListener;
use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;
fn free_port() -> u16 {
TcpListener::bind(("127.0.0.1", 0))
.expect("bind an ephemeral port")
.local_addr()
.expect("local_addr of the ephemeral listener")
.port()
}
fn pg_bin(tool: &str) -> PathBuf {
if let Ok(dir) = std::env::var("KEEL_PG_BIN") {
return PathBuf::from(dir).join(tool);
}
let homebrew = PathBuf::from("/opt/homebrew/opt/postgresql@15/bin").join(tool);
if homebrew.exists() {
return homebrew;
}
PathBuf::from(tool)
}
pub struct ScratchPg {
dir: TempDir,
port: u16,
}
impl ScratchPg {
#[must_use]
pub fn start() -> Option<Self> {
let initdb = pg_bin("initdb");
let pg_ctl = pg_bin("pg_ctl");
if Command::new(&initdb).arg("--version").output().is_err() {
return None;
}
let dir = TempDir::new().expect("tempdir for scratch postgres");
let data_dir = dir.path().join("data");
let init = Command::new(&initdb)
.arg("-D")
.arg(&data_dir)
.args(["-U", "keeltest", "-A", "trust", "--no-sync"])
.output()
.expect("run initdb");
assert!(
init.status.success(),
"initdb failed: {}",
String::from_utf8_lossy(&init.stderr)
);
let mut last_err = String::new();
for _ in 0..3 {
let port = free_port();
let start = Command::new(&pg_ctl)
.arg("-D")
.arg(&data_dir)
.arg("-o")
.arg(format!("-p {port} -h 127.0.0.1 -k {}", data_dir.display()))
.arg("-w")
.arg("-l")
.arg(dir.path().join("log"))
.arg("start")
.output()
.expect("run pg_ctl start");
if start.status.success() {
return Some(Self { dir, port });
}
last_err = String::from_utf8_lossy(&start.stderr).into_owned();
}
panic!("pg_ctl start failed after retries: {last_err}");
}
#[must_use]
pub fn url(&self) -> String {
format!("postgres://keeltest@127.0.0.1:{}/postgres", self.port)
}
}
impl Drop for ScratchPg {
fn drop(&mut self) {
let pg_ctl = pg_bin("pg_ctl");
let _ = Command::new(&pg_ctl)
.arg("-D")
.arg(self.dir.path().join("data"))
.arg("-m")
.arg("fast")
.arg("stop")
.output();
}
}