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) {
58 match std::env::var("AWA_TEST_ENGINE").as_deref() {
59 Ok("queue_storage") => activate_queue_storage(pool).await,
60 _ => reset_to_canonical(pool).await,
61 }
62}
63
64pub async fn reset_to_canonical(pool: &PgPool) {
66 let mut tx = pool
67 .begin()
68 .await
69 .expect("Failed to start runtime backend reset transaction");
70
71 sqlx::query(
72 r#"
73 UPDATE awa.storage_transition_state
74 SET current_engine = 'canonical',
75 prepared_engine = NULL,
76 state = 'canonical',
77 transition_epoch = transition_epoch + 1,
78 details = '{}'::jsonb,
79 updated_at = now(),
80 finalized_at = NULL
81 WHERE singleton
82 "#,
83 )
84 .execute(&mut *tx)
85 .await
86 .expect("Failed to reset storage transition state for test setup");
87 sqlx::query("DELETE FROM awa.runtime_storage_backends WHERE backend = 'queue_storage'")
88 .execute(&mut *tx)
89 .await
90 .expect("Failed to reset active runtime backend for test setup");
91 sqlx::query("DELETE FROM awa.runtime_instances")
92 .execute(&mut *tx)
93 .await
94 .expect("Failed to reset runtime instances for test setup");
95 tx.commit()
96 .await
97 .expect("Failed to commit runtime backend reset transaction");
98}
99
100pub async fn activate_queue_storage(pool: &PgPool) {
103 let mut tx = pool
104 .begin()
105 .await
106 .expect("Failed to start queue-storage activation transaction");
107
108 sqlx::query(
109 r#"
110 UPDATE awa.storage_transition_state
111 SET state = 'active',
112 current_engine = 'queue_storage',
113 prepared_engine = NULL,
114 details = jsonb_build_object('schema', 'awa'),
115 transition_epoch = transition_epoch + 1,
116 updated_at = now(),
117 finalized_at = now()
118 WHERE singleton
119 "#,
120 )
121 .execute(&mut *tx)
122 .await
123 .expect("Failed to activate queue-storage transition state for test setup");
124 sqlx::query(
125 r#"
126 INSERT INTO awa.runtime_storage_backends (backend, schema_name, updated_at)
127 VALUES ('queue_storage', 'awa', now())
128 ON CONFLICT (backend) DO UPDATE
129 SET schema_name = EXCLUDED.schema_name, updated_at = EXCLUDED.updated_at
130 "#,
131 )
132 .execute(&mut *tx)
133 .await
134 .expect("Failed to register queue-storage backend for test setup");
135 sqlx::query("DELETE FROM awa.runtime_instances")
136 .execute(&mut *tx)
137 .await
138 .expect("Failed to reset runtime instances for test setup");
139 tx.commit()
140 .await
141 .expect("Failed to commit queue-storage activation transaction");
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum TestEngine {
147 Canonical,
148 QueueStorage,
149}
150
151pub fn test_engine() -> TestEngine {
153 match std::env::var("AWA_TEST_ENGINE").as_deref() {
154 Ok("queue_storage") => TestEngine::QueueStorage,
155 _ => TestEngine::Canonical,
156 }
157}
158
159pub fn skip_unless_canonical(test: &str) -> bool {
168 if test_engine() != TestEngine::Canonical {
169 eprintln!(
170 "[skip] {test}: canonical-only assertions; running under AWA_TEST_ENGINE=queue_storage"
171 );
172 true
173 } else {
174 false
175 }
176}
177
178pub async fn clean_queue(pool: &PgPool, queue: &str) {
185 match awa_model::queue_storage::QueueStorage::active_schema(pool)
186 .await
187 .expect("active_schema lookup for clean_queue")
188 {
189 Some(schema) => clean_queue_substrate(pool, &schema, queue).await,
190 None => {
191 sqlx::query("DELETE FROM awa.jobs WHERE queue = $1")
192 .bind(queue)
193 .execute(pool)
194 .await
195 .expect("Failed to clean queue jobs");
196 }
197 }
198 sqlx::query("DELETE FROM awa.queue_meta WHERE queue = $1")
199 .bind(queue)
200 .execute(pool)
201 .await
202 .expect("Failed to clean queue meta");
203 sqlx::query("DELETE FROM awa.queue_state_counts WHERE queue = $1")
204 .bind(queue)
205 .execute(pool)
206 .await
207 .expect("Failed to clean queue state counts");
208}
209
210async fn clean_queue_substrate(pool: &PgPool, schema: &str, queue: &str) {
216 sqlx::query(&format!(
218 "DELETE FROM awa.job_unique_claims WHERE job_id IN (
219 SELECT job_id FROM {schema}.ready_entries WHERE queue = $1
220 UNION SELECT job_id FROM {schema}.deferred_jobs WHERE queue = $1
221 UNION SELECT job_id FROM {schema}.leases WHERE queue = $1
222 UNION SELECT job_id FROM {schema}.done_entries WHERE queue = $1
223 UNION SELECT job_id FROM {schema}.dlq_entries WHERE queue = $1)"
224 ))
225 .bind(queue)
226 .execute(pool)
227 .await
228 .expect("Failed to release queue unique claims");
229
230 for plane in [
231 "ready_entries",
232 "deferred_jobs",
233 "leases",
234 "done_entries",
235 "dlq_entries",
236 ] {
237 sqlx::query(&format!("DELETE FROM {schema}.{plane} WHERE queue = $1"))
238 .bind(queue)
239 .execute(pool)
240 .await
241 .unwrap_or_else(|err| panic!("Failed to clean {plane} for queue {queue}: {err}"));
242 }
243}
244
245pub async fn queue_state_counts(pool: &PgPool, queue: &str) -> HashMap<String, i64> {
247 let rows: Vec<(String, i64)> = sqlx::query_as(
248 r#"
249 SELECT state::text, count(*)::bigint
250 FROM awa.jobs
251 WHERE queue = $1
252 GROUP BY state
253 "#,
254 )
255 .bind(queue)
256 .fetch_all(pool)
257 .await
258 .expect("Failed to query state counts");
259
260 rows.into_iter().collect()
261}
262
263pub fn state_count(counts: &HashMap<String, i64>, state: &str) -> i64 {
265 counts.get(state).copied().unwrap_or(0)
266}
267
268pub async fn wait_for_counts(
270 pool: &PgPool,
271 queue: &str,
272 predicate: impl Fn(&HashMap<String, i64>) -> bool,
273 timeout: Duration,
274) -> HashMap<String, i64> {
275 let start = std::time::Instant::now();
276 loop {
277 let counts = queue_state_counts(pool, queue).await;
278 if predicate(&counts) {
279 return counts;
280 }
281 assert!(
282 start.elapsed() < timeout,
283 "Timed out waiting for queue {queue} counts; last counts: {counts:?}"
284 );
285 tokio::time::sleep(Duration::from_millis(50)).await;
286 }
287}