use std::future::Future;
use std::pin::Pin;
use crate::{InklogError, LogRecord};
pub trait LogDbProvider: Send + Sync {
#[allow(
clippy::type_complexity,
reason = "Pin<Box<dyn Future + Send>> is the canonical dyn-compatible async trait dispatch type"
)]
fn execute_log<'a>(
&'a self,
sql: &'a str,
) -> Pin<Box<dyn Future<Output = Result<(), InklogError>> + Send + 'a>>;
#[allow(
clippy::type_complexity,
reason = "Pin<Box<dyn Future + Send>> is the canonical dyn-compatible async trait dispatch type"
)]
fn batch_insert<'a>(
&'a self,
entries: Vec<LogRecord>,
) -> Pin<Box<dyn Future<Output = Result<(), InklogError>> + Send + 'a>>;
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
struct MockLogDbProvider {
executed_sqls: Mutex<Vec<String>>,
inserted_records: Mutex<Vec<LogRecord>>,
}
impl MockLogDbProvider {
fn new() -> Self {
Self {
executed_sqls: Mutex::new(Vec::new()),
inserted_records: Mutex::new(Vec::new()),
}
}
}
impl LogDbProvider for MockLogDbProvider {
fn execute_log<'a>(
&'a self,
sql: &'a str,
) -> Pin<Box<dyn Future<Output = Result<(), InklogError>> + Send + 'a>> {
Box::pin(async move {
self.executed_sqls
.lock()
.expect("mock lock poisoned")
.push(sql.to_string());
Ok(())
})
}
fn batch_insert<'a>(
&'a self,
entries: Vec<LogRecord>,
) -> Pin<Box<dyn Future<Output = Result<(), InklogError>> + Send + 'a>> {
Box::pin(async move {
let mut store = self.inserted_records.lock().expect("mock lock poisoned");
store.extend(entries);
Ok(())
})
}
}
#[test]
fn log_db_provider_is_object_safe() {
fn assert_dyn_compatible(_: Arc<dyn LogDbProvider + Send + Sync>) {}
let mock = Arc::new(MockLogDbProvider::new());
assert_dyn_compatible(mock);
}
#[tokio::test]
async fn execute_log_returns_pin_box_future() {
let provider = MockLogDbProvider::new();
provider
.execute_log("CREATE TABLE logs (id INTEGER PRIMARY KEY)")
.await
.expect("execute_log should succeed");
let sqls = provider.executed_sqls.lock().expect("lock");
assert_eq!(sqls.len(), 1);
assert!(sqls[0].contains("CREATE TABLE"));
}
#[tokio::test]
async fn batch_insert_stores_all_records() {
let provider = MockLogDbProvider::new();
let records = vec![
LogRecord::new(
tracing::Level::INFO,
"module_a".to_string(),
"message_a".to_string(),
),
LogRecord::new(
tracing::Level::WARN,
"module_b".to_string(),
"message_b".to_string(),
),
];
provider
.batch_insert(records)
.await
.expect("batch_insert should succeed");
let stored = provider.inserted_records.lock().expect("lock");
assert_eq!(stored.len(), 2);
assert_eq!(stored[0].message, "message_a");
assert_eq!(stored[1].message, "message_b");
}
#[tokio::test]
async fn batch_insert_empty_vec_succeeds() {
let provider = MockLogDbProvider::new();
provider
.batch_insert(Vec::new())
.await
.expect("empty batch should succeed");
let stored = provider.inserted_records.lock().expect("lock");
assert!(stored.is_empty());
}
#[tokio::test]
async fn log_db_provider_dyn_dispatch_works() {
let provider: Arc<dyn LogDbProvider + Send + Sync> = Arc::new(MockLogDbProvider::new());
provider
.execute_log("SELECT 1")
.await
.expect("execute via dyn should succeed");
provider
.batch_insert(vec![LogRecord::new(
tracing::Level::INFO,
"dyn_test".to_string(),
"via dyn".to_string(),
)])
.await
.expect("batch_insert via dyn should succeed");
}
#[tokio::test]
async fn execute_log_accumulates_multiple_calls() {
let provider = MockLogDbProvider::new();
provider.execute_log("SQL_1").await.expect("first");
provider.execute_log("SQL_2").await.expect("second");
provider.execute_log("SQL_3").await.expect("third");
let sqls = provider.executed_sqls.lock().expect("lock");
assert_eq!(sqls.len(), 3);
assert_eq!(sqls[0], "SQL_1");
assert_eq!(sqls[2], "SQL_3");
}
}