use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
use std::collections::HashMap;
use std::time::Duration;
pub fn database_url() -> String {
std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgres://postgres:test@localhost:15432/awa_test".to_string())
}
pub fn database_url_with_app_name(app_name: &str) -> String {
let mut url = database_url();
let sep = if url.contains('?') { '&' } else { '?' };
url.push(sep);
url.push_str("application_name=");
url.push_str(app_name);
url
}
pub async fn pool(max_connections: u32) -> PgPool {
PgPoolOptions::new()
.max_connections(max_connections)
.connect(&database_url())
.await
.expect("Failed to connect to database")
}
pub async fn pool_with_url(url: &str, max_connections: u32) -> PgPool {
PgPoolOptions::new()
.max_connections(max_connections)
.connect(url)
.await
.expect("Failed to connect to database")
}
pub async fn setup(max_connections: u32) -> PgPool {
let pool = pool(max_connections).await;
awa_model::migrations::run(&pool)
.await
.expect("Failed to run migrations");
reset_runtime_backend(&pool).await;
pool
}
pub async fn reset_runtime_backend(pool: &PgPool) {
match std::env::var("AWA_TEST_ENGINE").as_deref() {
Ok("queue_storage") => activate_queue_storage(pool).await,
_ => reset_to_canonical(pool).await,
}
}
pub async fn reset_to_canonical(pool: &PgPool) {
let mut tx = pool
.begin()
.await
.expect("Failed to start runtime backend reset transaction");
sqlx::query(
r#"
UPDATE awa.storage_transition_state
SET current_engine = 'canonical',
prepared_engine = NULL,
state = 'canonical',
transition_epoch = transition_epoch + 1,
details = '{}'::jsonb,
updated_at = now(),
finalized_at = NULL
WHERE singleton
"#,
)
.execute(&mut *tx)
.await
.expect("Failed to reset storage transition state for test setup");
sqlx::query("DELETE FROM awa.runtime_storage_backends WHERE backend = 'queue_storage'")
.execute(&mut *tx)
.await
.expect("Failed to reset active runtime backend for test setup");
sqlx::query("DELETE FROM awa.runtime_instances")
.execute(&mut *tx)
.await
.expect("Failed to reset runtime instances for test setup");
tx.commit()
.await
.expect("Failed to commit runtime backend reset transaction");
}
pub async fn activate_queue_storage(pool: &PgPool) {
let mut tx = pool
.begin()
.await
.expect("Failed to start queue-storage activation transaction");
sqlx::query(
r#"
UPDATE awa.storage_transition_state
SET state = 'active',
current_engine = 'queue_storage',
prepared_engine = NULL,
details = jsonb_build_object('schema', 'awa'),
transition_epoch = transition_epoch + 1,
updated_at = now(),
finalized_at = now()
WHERE singleton
"#,
)
.execute(&mut *tx)
.await
.expect("Failed to activate queue-storage transition state for test setup");
sqlx::query(
r#"
INSERT INTO awa.runtime_storage_backends (backend, schema_name, updated_at)
VALUES ('queue_storage', 'awa', now())
ON CONFLICT (backend) DO UPDATE
SET schema_name = EXCLUDED.schema_name, updated_at = EXCLUDED.updated_at
"#,
)
.execute(&mut *tx)
.await
.expect("Failed to register queue-storage backend for test setup");
sqlx::query("DELETE FROM awa.runtime_instances")
.execute(&mut *tx)
.await
.expect("Failed to reset runtime instances for test setup");
tx.commit()
.await
.expect("Failed to commit queue-storage activation transaction");
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TestEngine {
Canonical,
QueueStorage,
}
pub fn test_engine() -> TestEngine {
match std::env::var("AWA_TEST_ENGINE").as_deref() {
Ok("queue_storage") => TestEngine::QueueStorage,
_ => TestEngine::Canonical,
}
}
pub fn skip_unless_canonical(test: &str) -> bool {
if test_engine() != TestEngine::Canonical {
eprintln!(
"[skip] {test}: canonical-only assertions; running under AWA_TEST_ENGINE=queue_storage"
);
true
} else {
false
}
}
pub async fn clean_queue(pool: &PgPool, queue: &str) {
match awa_model::queue_storage::QueueStorage::active_schema(pool)
.await
.expect("active_schema lookup for clean_queue")
{
Some(schema) => clean_queue_substrate(pool, &schema, queue).await,
None => {
sqlx::query("DELETE FROM awa.jobs WHERE queue = $1")
.bind(queue)
.execute(pool)
.await
.expect("Failed to clean queue jobs");
}
}
sqlx::query("DELETE FROM awa.queue_meta WHERE queue = $1")
.bind(queue)
.execute(pool)
.await
.expect("Failed to clean queue meta");
sqlx::query("DELETE FROM awa.queue_state_counts WHERE queue = $1")
.bind(queue)
.execute(pool)
.await
.expect("Failed to clean queue state counts");
}
async fn clean_queue_substrate(pool: &PgPool, schema: &str, queue: &str) {
sqlx::query(&format!(
"DELETE FROM awa.job_unique_claims WHERE job_id IN (
SELECT job_id FROM {schema}.ready_entries WHERE queue = $1
UNION SELECT job_id FROM {schema}.deferred_jobs WHERE queue = $1
UNION SELECT job_id FROM {schema}.leases WHERE queue = $1
UNION SELECT job_id FROM {schema}.done_entries WHERE queue = $1
UNION SELECT job_id FROM {schema}.dlq_entries WHERE queue = $1)"
))
.bind(queue)
.execute(pool)
.await
.expect("Failed to release queue unique claims");
for plane in [
"ready_entries",
"deferred_jobs",
"leases",
"done_entries",
"dlq_entries",
] {
sqlx::query(&format!("DELETE FROM {schema}.{plane} WHERE queue = $1"))
.bind(queue)
.execute(pool)
.await
.unwrap_or_else(|err| panic!("Failed to clean {plane} for queue {queue}: {err}"));
}
}
pub async fn queue_state_counts(pool: &PgPool, queue: &str) -> HashMap<String, i64> {
let rows: Vec<(String, i64)> = sqlx::query_as(
r#"
SELECT state::text, count(*)::bigint
FROM awa.jobs
WHERE queue = $1
GROUP BY state
"#,
)
.bind(queue)
.fetch_all(pool)
.await
.expect("Failed to query state counts");
rows.into_iter().collect()
}
pub fn state_count(counts: &HashMap<String, i64>, state: &str) -> i64 {
counts.get(state).copied().unwrap_or(0)
}
pub async fn wait_for_counts(
pool: &PgPool,
queue: &str,
predicate: impl Fn(&HashMap<String, i64>) -> bool,
timeout: Duration,
) -> HashMap<String, i64> {
let start = std::time::Instant::now();
loop {
let counts = queue_state_counts(pool, queue).await;
if predicate(&counts) {
return counts;
}
assert!(
start.elapsed() < timeout,
"Timed out waiting for queue {queue} counts; last counts: {counts:?}"
);
tokio::time::sleep(Duration::from_millis(50)).await;
}
}