1use sqlx::postgres::PgPoolOptions;
4use sqlx::PgPool;
5use std::collections::HashMap;
6use std::time::Duration;
7
8pub fn database_url() -> String {
10 std::env::var("DATABASE_URL")
11 .unwrap_or_else(|_| "postgres://postgres:test@localhost:15432/awa_test".to_string())
12}
13
14pub fn database_url_with_app_name(app_name: &str) -> String {
16 let mut url = database_url();
17 let sep = if url.contains('?') { '&' } else { '?' };
18 url.push(sep);
19 url.push_str("application_name=");
20 url.push_str(app_name);
21 url
22}
23
24pub async fn pool(max_connections: u32) -> PgPool {
26 PgPoolOptions::new()
27 .max_connections(max_connections)
28 .connect(&database_url())
29 .await
30 .expect("Failed to connect to database")
31}
32
33pub async fn pool_with_url(url: &str, max_connections: u32) -> PgPool {
35 PgPoolOptions::new()
36 .max_connections(max_connections)
37 .connect(url)
38 .await
39 .expect("Failed to connect to database")
40}
41
42pub async fn setup(max_connections: u32) -> PgPool {
44 let pool = pool(max_connections).await;
45 awa_model::migrations::run(&pool)
46 .await
47 .expect("Failed to run migrations");
48 reset_runtime_backend(&pool).await;
49 pool
50}
51
52pub async fn reset_runtime_backend(pool: &PgPool) {
56 let mut tx = pool
57 .begin()
58 .await
59 .expect("Failed to start runtime backend reset transaction");
60
61 sqlx::query(
62 r#"
63 UPDATE awa.storage_transition_state
64 SET current_engine = 'canonical',
65 prepared_engine = NULL,
66 state = 'canonical',
67 transition_epoch = transition_epoch + 1,
68 details = '{}'::jsonb,
69 updated_at = now(),
70 finalized_at = NULL
71 WHERE singleton
72 "#,
73 )
74 .execute(&mut *tx)
75 .await
76 .expect("Failed to reset storage transition state for test setup");
77 sqlx::query("DELETE FROM awa.runtime_storage_backends WHERE backend = 'queue_storage'")
78 .execute(&mut *tx)
79 .await
80 .expect("Failed to reset active runtime backend for test setup");
81 sqlx::query("DELETE FROM awa.runtime_instances")
82 .execute(&mut *tx)
83 .await
84 .expect("Failed to reset runtime instances for test setup");
85 tx.commit()
86 .await
87 .expect("Failed to commit runtime backend reset transaction");
88}
89
90pub async fn clean_queue(pool: &PgPool, queue: &str) {
97 sqlx::query("DELETE FROM awa.jobs WHERE queue = $1")
98 .bind(queue)
99 .execute(pool)
100 .await
101 .expect("Failed to clean queue jobs");
102 sqlx::query("DELETE FROM awa.queue_meta WHERE queue = $1")
103 .bind(queue)
104 .execute(pool)
105 .await
106 .expect("Failed to clean queue meta");
107 sqlx::query("DELETE FROM awa.queue_state_counts WHERE queue = $1")
108 .bind(queue)
109 .execute(pool)
110 .await
111 .expect("Failed to clean queue state counts");
112}
113
114pub async fn queue_state_counts(pool: &PgPool, queue: &str) -> HashMap<String, i64> {
116 let rows: Vec<(String, i64)> = sqlx::query_as(
117 r#"
118 SELECT state::text, count(*)::bigint
119 FROM awa.jobs
120 WHERE queue = $1
121 GROUP BY state
122 "#,
123 )
124 .bind(queue)
125 .fetch_all(pool)
126 .await
127 .expect("Failed to query state counts");
128
129 rows.into_iter().collect()
130}
131
132pub fn state_count(counts: &HashMap<String, i64>, state: &str) -> i64 {
134 counts.get(state).copied().unwrap_or(0)
135}
136
137pub async fn wait_for_counts(
139 pool: &PgPool,
140 queue: &str,
141 predicate: impl Fn(&HashMap<String, i64>) -> bool,
142 timeout: Duration,
143) -> HashMap<String, i64> {
144 let start = std::time::Instant::now();
145 loop {
146 let counts = queue_state_counts(pool, queue).await;
147 if predicate(&counts) {
148 return counts;
149 }
150 assert!(
151 start.elapsed() < timeout,
152 "Timed out waiting for queue {queue} counts; last counts: {counts:?}"
153 );
154 tokio::time::sleep(Duration::from_millis(50)).await;
155 }
156}