1use std::net::{TcpListener, TcpStream};
18use std::path::PathBuf;
19use std::process::{Child, Command};
20use std::sync::atomic::{AtomicUsize, Ordering};
21use std::time::{Duration, Instant};
22
23pub struct EphemeralSqlite {
25 path: PathBuf,
26}
27
28impl EphemeralSqlite {
29 pub fn new() -> Self {
32 static COUNTER: AtomicUsize = AtomicUsize::new(0);
33 let name = format!(
34 "gize-test-{}-{}.db",
35 std::process::id(),
36 COUNTER.fetch_add(1, Ordering::Relaxed)
37 );
38 Self {
39 path: std::env::temp_dir().join(name),
40 }
41 }
42
43 pub fn url(&self) -> String {
45 format!("sqlite://{}?mode=rwc", self.path.display())
46 }
47}
48
49impl Default for EphemeralSqlite {
50 fn default() -> Self {
51 Self::new()
52 }
53}
54
55impl Drop for EphemeralSqlite {
56 fn drop(&mut self) {
57 let _ = std::fs::remove_file(&self.path);
59 let name = self
60 .path
61 .file_name()
62 .unwrap()
63 .to_string_lossy()
64 .into_owned();
65 for suffix in ["-wal", "-shm"] {
66 let mut side = self.path.clone();
67 side.set_file_name(format!("{name}{suffix}"));
68 let _ = std::fs::remove_file(&side);
69 }
70 }
71}
72
73pub fn free_port() -> u16 {
75 TcpListener::bind("127.0.0.1:0")
76 .expect("binding a free port")
77 .local_addr()
78 .expect("reading the local address")
79 .port()
80}
81
82pub struct App {
84 child: Child,
85 port: u16,
86}
87
88impl App {
89 pub fn spawn(binary: &str, db: &EphemeralSqlite) -> std::io::Result<Self> {
94 let port = free_port();
95 let child = Command::new(binary)
96 .env("DATABASE_URL", db.url())
97 .env("PORT", port.to_string())
98 .env("GIZE_JWT_SECRET", "gize-testing-secret")
99 .spawn()?;
100
101 let mut app = App { child, port };
102 app.wait_until_ready(Duration::from_secs(30))?;
103 Ok(app)
104 }
105
106 pub fn base_url(&self) -> String {
108 format!("http://127.0.0.1:{}", self.port)
109 }
110
111 fn wait_until_ready(&mut self, timeout: Duration) -> std::io::Result<()> {
113 let deadline = Instant::now() + timeout;
114 let addr = format!("127.0.0.1:{}", self.port);
115 loop {
116 if TcpStream::connect(&addr).is_ok() {
117 return Ok(());
118 }
119 if Instant::now() >= deadline {
120 return Err(std::io::Error::new(
121 std::io::ErrorKind::TimedOut,
122 format!("app did not start listening on {addr} within {timeout:?}"),
123 ));
124 }
125 std::thread::sleep(Duration::from_millis(100));
126 }
127 }
128}
129
130impl Drop for App {
131 fn drop(&mut self) {
132 let _ = self.child.kill();
133 let _ = self.child.wait();
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn ephemeral_sqlite_urls_are_unique_and_rwc() {
143 let a = EphemeralSqlite::new();
144 let b = EphemeralSqlite::new();
145 assert_ne!(a.url(), b.url());
146 assert!(a.url().starts_with("sqlite://"));
147 assert!(a.url().ends_with("?mode=rwc"));
148 }
149
150 #[test]
151 fn free_port_is_bindable() {
152 let port = free_port();
153 assert!(
155 TcpListener::bind(("127.0.0.1", port)).is_ok(),
156 "expected the reported free port to be bindable"
157 );
158 }
159}