use std::env;
use std::process::Command;
use std::sync::OnceLock;
use std::time::Duration;
use sqlx::postgres::PgPoolOptions;
pub(crate) static POSTGRES_CONTAINER: OnceLock<()> = OnceLock::new();
pub(crate) fn ensure_postgres_running() {
let host = env::var("POSTGRES_HOST").unwrap_or_else(|_| "localhost".to_string());
let port = env::var("POSTGRES_PORT").unwrap_or_else(|_| "5433".to_string());
let connection_string = format!("postgres://postgres:postgres@{}:{}/postgres", host, port);
if can_connect_to_postgres(&connection_string) {
eprintln!("Postgres already available at {}", connection_string);
return;
}
eprintln!("Postgres not accessible, attempting to start via docker-compose...");
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let project_root = std::path::Path::new(manifest_dir)
.parent()
.expect("should have parent directory");
let result = Command::new("docker")
.args(["compose", "up", "-d", "--wait"])
.current_dir(project_root)
.output();
match result {
Ok(output) if output.status.success() => {
eprintln!("Started postgres via docker-compose");
}
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.contains("already in use") && !stderr.contains("already exists") {
eprintln!("Warning: docker-compose failed: {}", stderr);
}
}
Err(e) => {
eprintln!("Warning: docker command not available: {}", e);
}
}
let max_retries = 30;
let retry_delay = Duration::from_millis(500);
for attempt in 0..max_retries {
if can_connect_to_postgres(&connection_string) {
eprintln!("Postgres is now accessible");
return;
}
if attempt < max_retries - 1 {
std::thread::sleep(retry_delay);
}
}
panic!(
"Postgres is not accessible at {} after {} retries. \
Ensure either: (1) GitHub Actions service container is configured, \
or (2) Docker is installed and docker-compose.yml exists",
connection_string, max_retries
);
}
fn can_connect_to_postgres(connection_string: &str) -> bool {
let connection_string = connection_string.to_string();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().ok()?;
rt.block_on(async {
PgPoolOptions::new()
.max_connections(1)
.acquire_timeout(Duration::from_secs(2))
.connect(&connection_string)
.await
.ok()
})
})
.join()
.ok()
.flatten()
.is_some()
}
pub(crate) fn connection_string() -> String {
let host = env::var("POSTGRES_HOST").unwrap_or_else(|_| "localhost".to_string());
let port = env::var("POSTGRES_PORT").unwrap_or_else(|_| "5433".to_string());
format!("postgres://postgres:postgres@{}:{}/postgres", host, port)
}
pub(crate) async fn create_test_store() -> eventcore_postgres::PostgresEventStore {
let _ = POSTGRES_CONTAINER.get_or_init(|| {
ensure_postgres_running();
});
let connection_string = connection_string();
let max_retries = 30;
let retry_delay = Duration::from_millis(500);
let mut store = None;
for attempt in 0..max_retries {
match eventcore_postgres::PostgresEventStore::new(connection_string.clone()).await {
Ok(s) => {
store = Some(s);
break;
}
Err(e) => {
if attempt < max_retries - 1 {
eprintln!(
"Postgres not ready (attempt {}/{}): {}",
attempt + 1,
max_retries,
e
);
tokio::time::sleep(retry_delay).await;
continue;
}
panic!(
"Failed to connect to postgres after {} retries: {}",
max_retries, e
);
}
}
}
let store = store.expect("store should be set after successful connection");
store.migrate().await;
store
}