use super::{TestConfig, TestResult};
use crate::config::Config;
use cqlite_core::{Config as CoreConfig, Database};
use std::path::PathBuf;
use std::sync::Arc;
use tempfile::TempDir;
use tokio::sync::Mutex;
pub struct TestContainer {
config: TestConfig,
temp_dir: Arc<TempDir>,
database: Arc<Mutex<Option<TestDatabase>>>,
cleanup_handlers: Vec<Box<dyn Fn() -> TestResult + Send + Sync>>,
}
impl std::fmt::Debug for TestContainer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TestContainer")
.field("config", &self.config)
.field("temp_dir", &self.temp_dir)
.field("database", &self.database)
.field(
"cleanup_handlers",
&format!("{} cleanup handlers", self.cleanup_handlers.len()),
)
.finish()
}
}
#[derive(Debug)]
pub struct TestDatabase {
pub database: Database,
pub db_path: PathBuf,
pub schema_path: Option<PathBuf>,
pub data_fixtures: Vec<PathBuf>,
}
#[derive(Debug, Clone)]
pub struct TestEnvironment {
pub temp_dir: PathBuf,
pub db_path: PathBuf,
pub config_path: PathBuf,
pub fixtures_dir: PathBuf,
pub output_dir: PathBuf,
}
impl TestContainer {
pub fn new() -> TestResult<Self> {
Self::with_config(TestConfig::default())
}
pub fn with_config(config: TestConfig) -> TestResult<Self> {
let temp_dir = if let Some(ref dir) = config.temp_dir {
TempDir::new_in(dir)?
} else {
TempDir::new()?
};
let container = Self {
config,
temp_dir: Arc::new(temp_dir),
database: Arc::new(Mutex::new(None)),
cleanup_handlers: Vec::new(),
};
container.setup_environment()?;
Ok(container)
}
pub fn environment(&self) -> TestEnvironment {
let temp_path = self.temp_dir.path();
TestEnvironment {
temp_dir: temp_path.to_path_buf(),
db_path: temp_path.join("test.db"),
config_path: temp_path.join("config.toml"),
fixtures_dir: temp_path.join("fixtures"),
output_dir: temp_path.join("output"),
}
}
pub async fn init_database(&self) -> TestResult<Arc<Mutex<TestDatabase>>> {
let env = self.environment();
let core_config = CoreConfig::default();
let database = Database::open(&env.db_path, core_config)
.await
.map_err(|e| format!("Failed to initialize test database: {}", e))?;
let test_db = TestDatabase {
database,
db_path: env.db_path,
schema_path: None,
data_fixtures: Vec::new(),
};
let test_db_arc = Arc::new(Mutex::new(test_db.clone()));
*self.database.lock().await = Some(test_db);
Ok(test_db_arc)
}
pub async fn database(&self) -> TestResult<Arc<Mutex<TestDatabase>>> {
let db_guard = self.database.lock().await;
if db_guard.is_none() {
drop(db_guard);
return self.init_database().await;
}
let test_db = db_guard.as_ref().unwrap().clone();
Ok(Arc::new(Mutex::new(test_db)))
}
pub fn create_cli_config(&self) -> TestResult<Config> {
let env = self.environment();
let mut config = Config::default();
config.default_database = Some(env.db_path);
config.save_to_file(&env.config_path)?;
Ok(config)
}
pub fn add_cleanup_handler<F>(&mut self, handler: F)
where
F: Fn() -> TestResult + Send + Sync + 'static,
{
self.cleanup_handlers.push(Box::new(handler));
}
fn setup_environment(&self) -> TestResult<()> {
let env = self.environment();
std::fs::create_dir_all(&env.fixtures_dir)?;
std::fs::create_dir_all(&env.output_dir)?;
let config = Config::default();
config.save_to_file(&env.config_path)?;
Ok(())
}
pub fn cleanup(&self) -> TestResult<()> {
if !self.config.cleanup {
return Ok(());
}
for handler in &self.cleanup_handlers {
if let Err(e) = handler() {
eprintln!("Cleanup handler failed: {}", e);
}
}
Ok(())
}
}
impl Drop for TestContainer {
fn drop(&mut self) {
if let Err(e) = self.cleanup() {
eprintln!("Failed to cleanup test container: {}", e);
}
}
}
impl Clone for TestContainer {
fn clone(&self) -> Self {
Self {
config: self.config.clone(),
temp_dir: self.temp_dir.clone(),
database: self.database.clone(),
cleanup_handlers: Vec::new(), }
}
}
impl Clone for TestDatabase {
fn clone(&self) -> Self {
Self {
database: self.database.clone(),
db_path: self.db_path.clone(),
schema_path: self.schema_path.clone(),
data_fixtures: self.data_fixtures.clone(),
}
}
}
impl TestDatabase {
pub fn with_schema<P: Into<PathBuf>>(mut self, schema_path: P) -> Self {
self.schema_path = Some(schema_path.into());
self
}
pub fn with_fixture<P: Into<PathBuf>>(mut self, fixture_path: P) -> Self {
self.data_fixtures.push(fixture_path.into());
self
}
pub async fn execute_query(&self, query: &str) -> TestResult<Vec<String>> {
match self.database.execute(query).await {
Ok(result) => {
if result.rows.is_empty() && result.rows_affected > 0 {
Ok(vec![format!("{} rows affected", result.rows_affected)])
} else if !result.rows.is_empty() {
let mut results = Vec::new();
for row in &result.rows {
let row_str = format!("{:?}", row.values);
results.push(row_str);
}
Ok(results)
} else {
Ok(vec!["Empty result set".to_string()])
}
}
Err(e) => {
println!("Query execution failed: {}", e);
Err(format!("Query execution failed: {}", e).into())
}
}
}
pub async fn load_schema(&self) -> TestResult<()> {
if let Some(ref schema_path) = self.schema_path {
let schema_content = std::fs::read_to_string(schema_path)?;
println!("Loading schema: {}", schema_content);
}
Ok(())
}
pub async fn load_fixtures(&self) -> TestResult<()> {
for fixture_path in &self.data_fixtures {
let fixture_content = std::fs::read_to_string(fixture_path)?;
println!("Loading fixture: {}", fixture_content);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_container_creation() {
let container = TestContainer::new().unwrap();
let env = container.environment();
assert!(env.temp_dir.exists());
assert!(env.fixtures_dir.exists());
assert!(env.output_dir.exists());
assert!(env.config_path.exists());
}
#[tokio::test]
async fn test_database_initialization() {
let container = TestContainer::new().unwrap();
let _db = container.init_database().await.unwrap();
let env = container.environment();
assert!(env.db_path.exists());
}
#[tokio::test]
async fn test_cli_config_creation() {
let container = TestContainer::new().unwrap();
let config = container.create_cli_config().unwrap();
let env = container.environment();
assert_eq!(config.default_database, Some(env.db_path));
}
#[test]
fn test_test_config_builder() {
let config = TestConfig::new()
.with_timeout(std::time::Duration::from_secs(60))
.with_parallel()
.with_verbose()
.no_cleanup();
assert_eq!(config.timeout, std::time::Duration::from_secs(60));
assert!(config.parallel);
assert!(config.verbose);
assert!(!config.cleanup);
}
}