#[cfg(test)]
mod multi_sink_fallback_test {
use inklog::config::{ConsoleSinkConfig, DatabaseDriver, DatabaseSinkConfig, FileSinkConfig};
use inklog::metrics::{FallbackAction, FallbackConfig, FallbackState};
use inklog::{InklogConfig, LoggerManager};
use std::fs;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tempfile::TempDir;
use inklog::tokio::sync::Mutex;
use inklog::tokio::time::Duration;
#[test]
fn test_fallback_state_creation() {
let state = FallbackState::new();
assert!(!state.is_fallback_active());
assert_eq!(state.get_current_action(), FallbackAction::None);
assert_eq!(state.get_retry_count(), 0);
}
#[test]
fn test_fallback_state_transitions() {
let state = FallbackState::new();
assert!(!state.is_fallback_active());
state.enter_fallback(FallbackAction::Database);
assert!(state.is_fallback_active());
assert_eq!(state.get_current_action(), FallbackAction::Database);
state.exit_fallback();
assert!(!state.is_fallback_active());
assert_eq!(state.get_current_action(), FallbackAction::None);
}
#[test]
fn test_fallback_state_retry_counting() {
let state = FallbackState::new();
assert_eq!(state.get_retry_count(), 0);
state.enter_fallback(FallbackAction::Console);
assert_eq!(state.get_retry_count(), 1);
state.enter_fallback(FallbackAction::Console);
assert_eq!(state.get_retry_count(), 2);
state.exit_fallback();
assert_eq!(state.get_retry_count(), 0);
}
#[test]
fn test_fallback_config_defaults() {
let config = FallbackConfig::default();
assert!(config.auto_fallback);
assert_eq!(config.initial_delay_ms, 1000);
assert_eq!(config.max_delay_ms, 60000);
assert_eq!(config.max_retries, 10);
}
#[tokio::test]
async fn test_file_sink_failure_triggers_fallback() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let db_url = format!("sqlite://{}", db_path.display());
let invalid_path = PathBuf::from("/nonexistent/path/that/does/not/exist/log.log");
let file_config = FileSinkConfig {
enabled: true,
path: invalid_path,
max_size: "1MB".into(),
..Default::default()
};
let db_config = DatabaseSinkConfig {
enabled: true,
driver: DatabaseDriver::SQLite,
url: db_url.clone(),
pool_size: 2,
batch_size: 10,
flush_interval_ms: 100,
table_name: "logs".to_string(),
..Default::default()
};
let config = InklogConfig {
global: inklog::config::GlobalConfig {
auto_fallback: true,
fallback_initial_delay_ms: 100,
fallback_max_retries: 3,
..Default::default()
},
file_sink: Some(file_config),
database_sink: Some(db_config),
console_sink: Some(ConsoleSinkConfig {
enabled: true,
..Default::default()
}),
..Default::default()
};
let logger = LoggerManager::with_config(config).await.unwrap();
log::info!("Testing fallback from file to database");
log::warn!("This should be logged via fallback mechanism");
tokio::time::sleep(Duration::from_millis(500)).await;
log::info!("Fallback test completed");
logger.shutdown().await.ok();
}
#[tokio::test]
async fn test_database_sink_failure_triggers_fallback() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("fallback_test.log");
let invalid_db_url = "postgres://invalid:invalid@nonexistent:5432/nonexistent";
let file_config = FileSinkConfig {
enabled: true,
path: file_path,
max_size: "1MB".into(),
..Default::default()
};
let db_config = DatabaseSinkConfig {
enabled: true,
driver: DatabaseDriver::PostgreSQL,
url: invalid_db_url.to_string(),
pool_size: 1,
batch_size: 10,
flush_interval_ms: 100,
table_name: "logs".to_string(),
..Default::default()
};
let config = InklogConfig {
global: inklog::config::GlobalConfig {
auto_fallback: true,
fallback_initial_delay_ms: 100,
fallback_max_retries: 3,
..Default::default()
},
file_sink: Some(file_config),
database_sink: Some(db_config),
console_sink: Some(ConsoleSinkConfig {
enabled: true,
..Default::default()
}),
..Default::default()
};
let logger = LoggerManager::with_config(config).await.unwrap();
log::info!("Testing fallback from database to file");
log::error!("Database connection failed, falling back to file");
tokio::time::sleep(Duration::from_millis(500)).await;
if file_path.exists() {
let content = fs::read_to_string(&file_path).unwrap();
assert!(content.contains("fallback") || content.contains("database"));
}
log::info!("Database fallback test completed");
logger.shutdown().await.ok();
}
#[tokio::test]
async fn test_all_sinks_unavailable() {
let temp_dir = TempDir::new().unwrap();
let invalid_file = PathBuf::from("/invalid/path/fallback.log");
let invalid_db = "mysql://invalid:invalid@localhost:3306/invalid";
let config = InklogConfig {
global: inklog::config::GlobalConfig {
auto_fallback: true,
fallback_initial_delay_ms: 50,
fallback_max_retries: 2,
..Default::default()
},
file_sink: Some(FileSinkConfig {
enabled: true,
path: invalid_file,
..Default::default()
}),
database_sink: Some(DatabaseSinkConfig {
enabled: true,
driver: DatabaseDriver::MySQL,
url: invalid_db.to_string(),
pool_size: 1,
table_name: "logs".to_string(),
..Default::default()
}),
console_sink: Some(ConsoleSinkConfig {
enabled: true,
..Default::default()
}),
..Default::default()
};
let logger = LoggerManager::with_config(config).await.unwrap();
log::info!("All sinks unavailable test - message should appear in console");
log::warn!("Fallback to console successful");
tokio::time::sleep(Duration::from_millis(200)).await;
log::info!("Console fallback test completed");
logger.shutdown().await.ok();
}
#[tokio::test]
async fn test_fallback_recovery() {
use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
let state = Arc::new(FallbackState::new());
assert!(!state.is_fallback_active());
state.enter_fallback(FallbackAction::Database);
assert!(state.is_fallback_active());
state.exit_fallback();
assert!(!state.is_fallback_active());
}
#[tokio::test]
async fn test_concurrent_fallback_scenarios() {
use std::sync::Arc;
use inklog::tokio::sync::Barrier;
use std::sync::atomic::{AtomicUsize, Ordering};
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("concurrent.db");
let db_url = format!("sqlite://{}", db_path.display());
let file_config = FileSinkConfig {
enabled: true,
path: PathBuf::from("/invalid/concurrent.log"),
..Default::default()
};
let db_config = DatabaseSinkConfig {
enabled: true,
driver: DatabaseDriver::SQLite,
url: db_url.clone(),
pool_size: 5,
batch_size: 100,
flush_interval_ms: 50,
table_name: "logs".to_string(),
..Default::default()
};
let config = InklogConfig {
global: inklog::config::GlobalConfig {
auto_fallback: true,
..Default::default()
},
file_sink: Some(file_config),
database_sink: Some(db_config),
console_sink: Some(ConsoleSinkConfig::default()),
..Default::default()
};
let logger = LoggerManager::with_config(config).await.unwrap();
let barrier = Arc::new(Barrier::new(10));
let counter = Arc::new(AtomicUsize::new(0));
let handles: Vec<_> = (0..10)
.map(|_| {
let barrier = barrier.clone();
let counter = counter.clone();
tokio::spawn(async move {
barrier.wait().await;
for _ in 0..10 {
log::info!("Concurrent fallback test message");
counter.fetch_add(1, Ordering::SeqCst);
}
})
})
.collect();
for handle in handles {
handle.await.unwrap();
}
assert_eq!(counter.load(Ordering::SeqCst), 100);
log::info!("Concurrent fallback test completed");
logger.shutdown().await.ok();
}
#[test]
fn test_fallback_config_options() {
let config = FallbackConfig {
auto_fallback: true,
initial_delay_ms: 500,
max_delay_ms: 30000,
max_retries: 5,
};
assert!(config.auto_fallback);
assert_eq!(config.initial_delay_ms, 500);
assert_eq!(config.max_delay_ms, 30000);
assert_eq!(config.max_retries, 5);
}
#[test]
fn test_fallback_action_variants() {
let actions = [
FallbackAction::None,
FallbackAction::Console,
FallbackAction::File,
FallbackAction::Database,
];
for action in actions {
let state = FallbackState::new();
state.enter_fallback(action);
assert_eq!(state.get_current_action(), action);
}
}
}