1use std::path::{Path, PathBuf};
12use std::process::Command;
13
14use anyhow::{Context, Result, bail};
15use sqlx::migrate::Migrator;
16
17static MIGRATOR: Migrator = sqlx::migrate!("../migrations");
21
22const PORT_RANGE: std::ops::Range<u16> = 54320..54340;
25
26const TIMEOUTS: &[&str] = &[
35 "-c",
36 "statement_timeout=120s",
37 "-c",
38 "idle_in_transaction_session_timeout=300s",
39];
40
41const DURABILITY_OFF: &[&str] = &[
46 "-c",
47 "fsync=off",
48 "-c",
49 "synchronous_commit=off",
50 "-c",
51 "full_page_writes=off",
52 "-c",
53 "autovacuum=off",
54];
55
56pub struct Postgres {
58 data_dir: PathBuf,
59 port: u16,
60 _root: tempfile::TempDir,
62}
63
64impl Postgres {
65 pub fn database_url(&self) -> String {
66 format!("postgres://postgres@127.0.0.1:{}/nomisync", self.port)
67 }
68}
69
70impl Drop for Postgres {
71 fn drop(&mut self) {
72 let _ = Command::new("pg_ctl")
75 .args([
76 "-D",
77 &self.data_dir.to_string_lossy(),
78 "-m",
79 "immediate",
80 "stop",
81 ])
82 .status();
83 }
84}
85
86fn run(program: &str, args: &[&str]) -> Result<()> {
87 let status = Command::new(program)
88 .args(args)
89 .status()
90 .with_context(|| format!("failed to run {program}"))?;
91 if !status.success() {
92 bail!("{program} exited with {status}");
93 }
94 Ok(())
95}
96
97pub async fn start() -> Result<Postgres> {
99 let root =
102 tempfile::TempDir::with_prefix("nomisync-ci-pg").context("could not make a temp dir")?;
103 let data_dir = root.path().join("pgdata");
104 let socket_dir = data_dir.join("sockets");
105
106 run(
107 "initdb",
108 &[
109 "-D",
110 &data_dir.to_string_lossy(),
111 "-U",
112 "postgres",
113 "--auth=trust",
114 ],
115 )
116 .context("initdb failed")?;
117 std::fs::create_dir_all(&socket_dir).context("could not make the socket dir")?;
118
119 let log = root.path().join("postgres.log");
120 let port = start_on_free_port(&data_dir, &socket_dir, &log)?;
121 let postgres = Postgres {
122 data_dir,
123 port,
124 _root: root,
125 };
126
127 println!(">> postgres: listening on 127.0.0.1:{port}");
128 create_and_migrate(&postgres).await?;
129 Ok(postgres)
130}
131
132fn start_on_free_port(data_dir: &Path, socket_dir: &Path, log: &Path) -> Result<u16> {
133 for port in PORT_RANGE {
134 let options = format!(
138 "-p {port} -c listen_addresses=127.0.0.1 -c unix_socket_directories={} {}",
139 socket_dir.display(),
140 [DURABILITY_OFF, TIMEOUTS].concat().join(" ")
141 );
142 let status = Command::new("pg_ctl")
143 .args([
144 "-D",
145 &data_dir.to_string_lossy(),
146 "-l",
147 &log.to_string_lossy(),
148 "-w",
149 "-o",
150 &options,
151 "start",
152 ])
153 .status()
154 .context("failed to run pg_ctl")?;
155 if status.success() {
156 return Ok(port);
157 }
158 }
159 bail!("no free port in {PORT_RANGE:?} accepted a postgres server")
160}
161
162async fn create_and_migrate(postgres: &Postgres) -> Result<()> {
163 use sqlx::{Connection, Executor, PgConnection};
164
165 let admin = format!("postgres://postgres@127.0.0.1:{}/postgres", postgres.port);
166 let mut conn = PgConnection::connect(&admin)
167 .await
168 .context("could not connect to the new cluster")?;
169 conn.execute("CREATE DATABASE nomisync")
170 .await
171 .context("could not create the nomisync database")?;
172 conn.close().await.ok();
173
174 let mut conn = PgConnection::connect(&postgres.database_url())
175 .await
176 .context("could not connect to the nomisync database")?;
177 MIGRATOR.run(&mut conn).await.context("migrations failed")?;
178 conn.close().await.ok();
179
180 println!(
181 ">> postgres: migrated ({} migrations)",
182 MIGRATOR.iter().count()
183 );
184 Ok(())
185}