Move 17 library crates into crates/, CLI binary into bin/fxt, consolidate 10 test crates into testing/, split config crate from deployment config files. Root directory reduced from 38+ to ~17 directories. All Cargo.toml paths and build.rs proto refs updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
169 lines
4.6 KiB
Rust
169 lines
4.6 KiB
Rust
//! Database test fixtures
|
|
//!
|
|
//! Provides shared database setup/teardown utilities used across 39+ test files.
|
|
|
|
use anyhow::Result;
|
|
use once_cell::sync::OnceCell;
|
|
use sqlx::{postgres::PgPoolOptions, PgPool};
|
|
use std::sync::Arc;
|
|
use tokio::sync::Mutex;
|
|
|
|
static DB_POOL: OnceCell<Arc<Mutex<PgPool>>> = OnceCell::new();
|
|
|
|
/// Test database wrapper with automatic setup and cleanup
|
|
pub struct TestDb {
|
|
pool: PgPool,
|
|
schema_name: String,
|
|
}
|
|
|
|
impl TestDb {
|
|
/// Create a new test database instance
|
|
///
|
|
/// Uses the DATABASE_URL environment variable or falls back to default.
|
|
/// Creates an isolated schema for test isolation.
|
|
pub async fn new() -> Self {
|
|
let pool = get_test_pool().await;
|
|
let schema_name = format!("test_{}", uuid::Uuid::new_v4().to_string().replace('-', "_"));
|
|
|
|
// Create isolated schema
|
|
sqlx::query(&format!("CREATE SCHEMA {}", schema_name))
|
|
.execute(&pool)
|
|
.await
|
|
.expect("Failed to create test schema");
|
|
|
|
Self {
|
|
pool,
|
|
schema_name,
|
|
}
|
|
}
|
|
|
|
/// Create a new test database with migrations applied
|
|
pub async fn with_migrations() -> Self {
|
|
let db = Self::new().await;
|
|
|
|
// Set search path to test schema
|
|
sqlx::query(&format!("SET search_path TO {}", db.schema_name))
|
|
.execute(&db.pool)
|
|
.await
|
|
.expect("Failed to set search path");
|
|
|
|
// Run migrations
|
|
sqlx::migrate!("../../migrations")
|
|
.run(&db.pool)
|
|
.await
|
|
.expect("Failed to run migrations");
|
|
|
|
db
|
|
}
|
|
|
|
/// Get a reference to the connection pool
|
|
pub fn pool(&self) -> &PgPool {
|
|
&self.pool
|
|
}
|
|
|
|
/// Execute a query in the test schema
|
|
pub async fn execute(&self, sql: &str) -> Result<sqlx::postgres::PgQueryResult> {
|
|
sqlx::query(&format!("SET search_path TO {}; {}", self.schema_name, sql))
|
|
.execute(&self.pool)
|
|
.await
|
|
.map_err(Into::into)
|
|
}
|
|
|
|
/// Begin a transaction for test isolation
|
|
pub async fn begin(&self) -> Result<sqlx::Transaction<'_, sqlx::Postgres>> {
|
|
self.pool.begin().await.map_err(Into::into)
|
|
}
|
|
}
|
|
|
|
impl Drop for TestDb {
|
|
fn drop(&mut self) {
|
|
// Clean up test schema
|
|
let pool = self.pool.clone();
|
|
let schema_name = self.schema_name.clone();
|
|
|
|
tokio::spawn(async move {
|
|
let _ = sqlx::query(&format!("DROP SCHEMA IF EXISTS {} CASCADE", schema_name))
|
|
.execute(&pool)
|
|
.await;
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Get or create the shared test database pool
|
|
///
|
|
/// Pattern found in 39 test files:
|
|
/// - services/ml_training_service/tests/checkpoint_manager_tests.rs
|
|
/// - services/ml_training_service/tests/job_tracker_test.rs
|
|
/// - database/src/test_helpers.rs
|
|
/// - And 36+ more
|
|
async fn get_test_pool() -> PgPool {
|
|
let pool = DB_POOL.get_or_init(|| {
|
|
let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| {
|
|
"postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string()
|
|
});
|
|
|
|
Arc::new(Mutex::new(
|
|
tokio::task::block_in_place(|| {
|
|
tokio::runtime::Handle::current().block_on(async {
|
|
PgPoolOptions::new()
|
|
.max_connections(5)
|
|
.connect(&database_url)
|
|
.await
|
|
.expect("Failed to create test pool")
|
|
})
|
|
})
|
|
))
|
|
});
|
|
|
|
pool.lock().await.clone()
|
|
}
|
|
|
|
/// Setup test data in the database
|
|
///
|
|
/// Pattern found in data_loader_integration.rs
|
|
pub async fn setup_test_data(pool: &PgPool, symbol_prefix: &str) -> Result<()> {
|
|
// Clean existing test data
|
|
sqlx::query(&format!(
|
|
"DELETE FROM market_events WHERE symbol LIKE '{}%'",
|
|
symbol_prefix
|
|
))
|
|
.execute(pool)
|
|
.await?;
|
|
|
|
sqlx::query(&format!(
|
|
"DELETE FROM trade_executions WHERE symbol LIKE '{}%'",
|
|
symbol_prefix
|
|
))
|
|
.execute(pool)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_db_creation() {
|
|
let db = TestDb::new().await;
|
|
assert!(db.pool().is_closed() == false);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_db_with_migrations() {
|
|
let db = TestDb::with_migrations().await;
|
|
let result = db.execute("SELECT 1").await;
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_schema_isolation() {
|
|
let db1 = TestDb::new().await;
|
|
let db2 = TestDb::new().await;
|
|
|
|
// Different schemas should be isolated
|
|
assert_ne!(db1.schema_name, db2.schema_name);
|
|
}
|
|
}
|