clippy: adhere to pedantic clippy, uniform test error handling

This commit is contained in:
Per Stark
2026-05-26 11:43:45 +02:00
parent 6a5d631287
commit 000852c94c
68 changed files with 2468 additions and 2547 deletions
+142 -108
View File
@@ -13,13 +13,13 @@ use object_store::{path::Path as ObjPath, ObjectStore};
use crate::utils::config::{AppConfig, StorageKind};
pub type DynStore = Arc<dyn ObjectStore>;
pub type DynStorage = Arc<dyn ObjectStore>;
/// Storage manager with persistent state and proper lifecycle management.
#[derive(Clone)]
pub struct StorageManager {
// Store from objectstore wrapped as dyn
store: DynStore,
store: DynStorage,
// Simple enum to track which kind
backend_kind: StorageKind,
// Where on disk
@@ -46,7 +46,7 @@ impl StorageManager {
///
/// This method is useful for testing scenarios where you want to inject
/// a specific storage backend.
pub fn with_backend(store: DynStore, backend_kind: StorageKind) -> Self {
pub fn with_backend(store: DynStorage, backend_kind: StorageKind) -> Self {
Self {
store,
backend_kind,
@@ -216,7 +216,7 @@ impl StorageManager {
/// storage backends with proper error handling and validation.
async fn create_storage_backend(
cfg: &AppConfig,
) -> object_store::Result<(DynStore, Option<PathBuf>)> {
) -> object_store::Result<(DynStorage, Option<PathBuf>)> {
match cfg.storage {
StorageKind::Local => {
let base = resolve_base_dir(cfg);
@@ -261,9 +261,7 @@ async fn create_storage_backend(
builder = builder.with_endpoint(endpoint);
}
if let Some(region) = &cfg.s3_region {
builder = builder.with_region(region);
}
builder = builder.with_region(&cfg.s3_region);
let store = builder.build()?;
Ok((Arc::new(store), None))
@@ -342,7 +340,7 @@ pub mod testing {
surrealdb_password: "test".into(),
surrealdb_namespace: "test".into(),
surrealdb_database: "test".into(),
data_dir: base.into(),
data_dir: base,
http_port: 0,
openai_base_url: "..".into(),
storage: StorageKind::Local,
@@ -382,7 +380,7 @@ pub mod testing {
#[derive(Clone)]
pub struct TestStorageManager {
storage: StorageManager,
_temp_dir: Option<(String, std::path::PathBuf)>, // For local storage cleanup
temp_dir: Option<(String, std::path::PathBuf)>, // For local storage cleanup
}
impl TestStorageManager {
@@ -396,7 +394,7 @@ pub mod testing {
Ok(Self {
storage,
_temp_dir: None,
temp_dir: None,
})
}
@@ -413,7 +411,7 @@ pub mod testing {
Ok(Self {
storage,
_temp_dir: resolved,
temp_dir: resolved,
})
}
@@ -437,7 +435,7 @@ pub mod testing {
Ok(Self {
storage,
_temp_dir: None,
temp_dir: None,
})
}
@@ -454,7 +452,7 @@ pub mod testing {
Ok(Self {
storage,
_temp_dir: temp_dir,
temp_dir,
})
}
@@ -508,7 +506,7 @@ pub mod testing {
impl Drop for TestStorageManager {
fn drop(&mut self) {
// Clean up temporary directories for local storage
if let Some((_, path)) = &self._temp_dir {
if let Some((_, path)) = &self.temp_dir {
if path.exists() {
let _ = std::fs::remove_dir_all(path);
}
@@ -584,6 +582,7 @@ pub fn split_object_path(path: &str) -> AnyResult<(String, String)> {
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Context;
use crate::utils::config::{PdfIngestMode::LlmFirst, StorageKind};
use bytes::Bytes;
use uuid::Uuid;
@@ -623,11 +622,11 @@ mod tests {
}
#[tokio::test]
async fn test_storage_manager_memory_basic_operations() {
async fn test_storage_manager_memory_basic_operations() -> anyhow::Result<()> {
let cfg = test_config_memory();
let storage = StorageManager::new(&cfg)
.await
.expect("create storage manager");
.with_context(|| "create storage manager".to_string())?;
assert!(storage.local_base_path().is_none());
let location = "test/data/file.txt";
@@ -637,31 +636,33 @@ mod tests {
storage
.put(location, Bytes::from(data.to_vec()))
.await
.expect("put");
let retrieved = storage.get(location).await.expect("get");
.with_context(|| "put".to_string())?;
let retrieved = storage.get(location).await.with_context(|| "get".to_string())?;
assert_eq!(retrieved.as_ref(), data);
// Test exists
assert!(storage.exists(location).await.expect("exists check"));
assert!(storage.exists(location).await.with_context(|| "exists check".to_string())?);
// Test delete
storage.delete_prefix("test/data/").await.expect("delete");
storage.delete_prefix("test/data/").await.with_context(|| "delete".to_string())?;
assert!(!storage
.exists(location)
.await
.expect("exists check after delete"));
.with_context(|| "exists check after delete".to_string())?);
Ok(())
}
#[tokio::test]
async fn test_storage_manager_local_basic_operations() {
async fn test_storage_manager_local_basic_operations() -> anyhow::Result<()> {
let base = format!("/tmp/minne_storage_test_{}", Uuid::new_v4());
let cfg = test_config(&base);
let storage = StorageManager::new(&cfg)
.await
.expect("create storage manager");
.with_context(|| "create storage manager".to_string())?;
let resolved_base = storage
.local_base_path()
.expect("resolved base dir")
.with_context(|| "resolved base dir".to_string())?
.to_path_buf();
assert_eq!(resolved_base, PathBuf::from(&base));
@@ -672,42 +673,44 @@ mod tests {
storage
.put(location, Bytes::from(data.to_vec()))
.await
.expect("put");
let retrieved = storage.get(location).await.expect("get");
.with_context(|| "put".to_string())?;
let retrieved = storage.get(location).await.with_context(|| "get".to_string())?;
assert_eq!(retrieved.as_ref(), data);
let object_dir = resolved_base.join("test/data");
tokio::fs::metadata(&object_dir)
.await
.expect("object directory exists after write");
.with_context(|| "object directory exists after write".to_string())?;
// Test exists
assert!(storage.exists(location).await.expect("exists check"));
assert!(storage.exists(location).await.with_context(|| "exists check".to_string())?);
// Test delete
storage.delete_prefix("test/data/").await.expect("delete");
storage.delete_prefix("test/data/").await.with_context(|| "delete".to_string())?;
assert!(!storage
.exists(location)
.await
.expect("exists check after delete"));
.with_context(|| "exists check after delete".to_string())?);
assert!(
tokio::fs::metadata(&object_dir).await.is_err(),
"object directory should be removed"
);
tokio::fs::metadata(&resolved_base)
.await
.expect("base directory remains intact");
.with_context(|| "base directory remains intact".to_string())?;
// Clean up
let _ = tokio::fs::remove_dir_all(&base).await;
Ok(())
}
#[tokio::test]
async fn test_storage_manager_memory_persistence() {
async fn test_storage_manager_memory_persistence() -> anyhow::Result<()> {
let cfg = test_config_memory();
let storage = StorageManager::new(&cfg)
.await
.expect("create storage manager");
.with_context(|| "create storage manager".to_string())?;
let location = "persistence/test.txt";
let data1 = b"first data";
@@ -717,32 +720,34 @@ mod tests {
storage
.put(location, Bytes::from(data1.to_vec()))
.await
.expect("put first");
.with_context(|| "put first".to_string())?;
// Retrieve and verify first data
let retrieved1 = storage.get(location).await.expect("get first");
let retrieved1 = storage.get(location).await.with_context(|| "get first".to_string())?;
assert_eq!(retrieved1.as_ref(), data1);
// Overwrite with second data
storage
.put(location, Bytes::from(data2.to_vec()))
.await
.expect("put second");
.with_context(|| "put second".to_string())?;
// Retrieve and verify second data
let retrieved2 = storage.get(location).await.expect("get second");
let retrieved2 = storage.get(location).await.with_context(|| "get second".to_string())?;
assert_eq!(retrieved2.as_ref(), data2);
// Data persists across multiple operations using the same StorageManager
assert_ne!(retrieved1.as_ref(), retrieved2.as_ref());
Ok(())
}
#[tokio::test]
async fn test_storage_manager_list_operations() {
async fn test_storage_manager_list_operations() -> anyhow::Result<()> {
let cfg = test_config_memory();
let storage = StorageManager::new(&cfg)
.await
.expect("create storage manager");
.with_context(|| "create storage manager".to_string())?;
// Create multiple files
let files = vec![
@@ -755,15 +760,15 @@ mod tests {
storage
.put(location, Bytes::from(data.to_vec()))
.await
.expect("put");
.with_context(|| "put".to_string())?;
}
// Test listing without prefix
let all_files = storage.list(None).await.expect("list all");
let all_files = storage.list(None).await.with_context(|| "list all".to_string())?;
assert_eq!(all_files.len(), 3);
// Test listing with prefix
let dir1_files = storage.list(Some("dir1/")).await.expect("list dir1");
let dir1_files = storage.list(Some("dir1/")).await.with_context(|| "list dir1".to_string())?;
assert_eq!(dir1_files.len(), 2);
assert!(dir1_files
.iter()
@@ -776,16 +781,18 @@ mod tests {
let empty_files = storage
.list(Some("nonexistent/"))
.await
.expect("list nonexistent");
.with_context(|| "list nonexistent".to_string())?;
assert_eq!(empty_files.len(), 0);
Ok(())
}
#[tokio::test]
async fn test_storage_manager_stream_operations() {
async fn test_storage_manager_stream_operations() -> anyhow::Result<()> {
let cfg = test_config_memory();
let storage = StorageManager::new(&cfg)
.await
.expect("create storage manager");
.with_context(|| "create storage manager".to_string())?;
let location = "stream/test.bin";
let content = vec![42u8; 1024 * 64]; // 64KB of data
@@ -794,22 +801,24 @@ mod tests {
storage
.put(location, Bytes::from(content.clone()))
.await
.expect("put large data");
.with_context(|| "put large data".to_string())?;
// Get as stream
let mut stream = storage.get_stream(location).await.expect("get stream");
let mut stream = storage.get_stream(location).await.with_context(|| "get stream".to_string())?;
let mut collected = Vec::new();
while let Some(chunk) = stream.next().await {
let chunk = chunk.expect("stream chunk");
let chunk = chunk.with_context(|| "stream chunk".to_string())?;
collected.extend_from_slice(&chunk);
}
assert_eq!(collected, content);
Ok(())
}
#[tokio::test]
async fn test_storage_manager_with_custom_backend() {
async fn test_storage_manager_with_custom_backend() -> anyhow::Result<()> {
use object_store::memory::InMemory;
// Create custom memory backend
@@ -823,20 +832,22 @@ mod tests {
storage
.put(location, Bytes::from(data.to_vec()))
.await
.expect("put");
let retrieved = storage.get(location).await.expect("get");
.with_context(|| "put".to_string())?;
let retrieved = storage.get(location).await.with_context(|| "get".to_string())?;
assert_eq!(retrieved.as_ref(), data);
assert!(storage.exists(location).await.expect("exists"));
assert!(storage.exists(location).await.with_context(|| "exists".to_string())?);
assert_eq!(*storage.backend_kind(), StorageKind::Memory);
Ok(())
}
#[tokio::test]
async fn test_storage_manager_error_handling() {
async fn test_storage_manager_error_handling() -> anyhow::Result<()> {
let cfg = test_config_memory();
let storage = StorageManager::new(&cfg)
.await
.expect("create storage manager");
.with_context(|| "create storage manager".to_string())?;
// Test getting non-existent file
let result = storage.get("nonexistent.txt").await;
@@ -846,124 +857,136 @@ mod tests {
let exists = storage
.exists("nonexistent.txt")
.await
.expect("exists check");
.with_context(|| "exists check".to_string())?;
assert!(!exists);
// Test listing with invalid location (should not panic)
let _result = storage.get("").await;
// This may or may not error depending on the backend implementation
// The important thing is that it doesn't panic
Ok(())
}
// TestStorageManager tests
#[tokio::test]
async fn test_test_storage_manager_memory() {
async fn test_test_storage_manager_memory() -> anyhow::Result<()> {
let test_storage = testing::TestStorageManager::new_memory()
.await
.expect("create test storage");
.with_context(|| "create test storage".to_string())?;
let location = "test/storage/file.txt";
let data = b"test data with TestStorageManager";
// Test put and get
test_storage.put(location, data).await.expect("put");
let retrieved = test_storage.get(location).await.expect("get");
test_storage.put(location, data).await.with_context(|| "put".to_string())?;
let retrieved = test_storage.get(location).await.with_context(|| "get".to_string())?;
assert_eq!(retrieved.as_ref(), data);
// Test existence check
assert!(test_storage.exists(location).await.expect("exists"));
assert!(test_storage.exists(location).await.with_context(|| "exists".to_string())?);
// Test list
let files = test_storage
.list(Some("test/storage/"))
.await
.expect("list");
.with_context(|| "list".to_string())?;
assert_eq!(files.len(), 1);
// Test delete
test_storage
.delete_prefix("test/storage/")
.await
.expect("delete");
.with_context(|| "delete".to_string())?;
assert!(!test_storage
.exists(location)
.await
.expect("exists after delete"));
.with_context(|| "exists after delete".to_string())?);
Ok(())
}
#[tokio::test]
async fn test_test_storage_manager_local() {
async fn test_test_storage_manager_local() -> anyhow::Result<()> {
let test_storage = testing::TestStorageManager::new_local()
.await
.expect("create test storage");
.with_context(|| "create test storage".to_string())?;
let location = "test/local/file.txt";
let data = b"test data with local TestStorageManager";
// Test put and get
test_storage.put(location, data).await.expect("put");
let retrieved = test_storage.get(location).await.expect("get");
test_storage.put(location, data).await
.with_context(|| "put".to_string())?;
let retrieved = test_storage.get(location).await
.with_context(|| "get".to_string())?;
assert_eq!(retrieved.as_ref(), data);
// Test existence check
assert!(test_storage.exists(location).await.expect("exists"));
assert!(test_storage.exists(location).await
.with_context(|| "exists".to_string())?);
// The storage should be automatically cleaned up when test_storage is dropped
Ok(())
}
#[tokio::test]
async fn test_test_storage_manager_isolation() {
async fn test_test_storage_manager_isolation() -> anyhow::Result<()> {
let storage1 = testing::TestStorageManager::new_memory()
.await
.expect("create test storage 1");
.with_context(|| "create test storage 1".to_string())?;
let storage2 = testing::TestStorageManager::new_memory()
.await
.expect("create test storage 2");
.with_context(|| "create test storage 2".to_string())?;
let location = "isolation/test.txt";
let data1 = b"storage 1 data";
let data2 = b"storage 2 data";
// Put different data in each storage
storage1.put(location, data1).await.expect("put storage 1");
storage2.put(location, data2).await.expect("put storage 2");
storage1.put(location, data1).await
.with_context(|| "put storage 1".to_string())?;
storage2.put(location, data2).await
.with_context(|| "put storage 2".to_string())?;
// Verify isolation
let retrieved1 = storage1.get(location).await.expect("get storage 1");
let retrieved2 = storage2.get(location).await.expect("get storage 2");
let retrieved1 = storage1.get(location).await
.with_context(|| "get storage 1".to_string())?;
let retrieved2 = storage2.get(location).await
.with_context(|| "get storage 2".to_string())?;
assert_eq!(retrieved1.as_ref(), data1);
assert_eq!(retrieved2.as_ref(), data2);
assert_ne!(retrieved1.as_ref(), retrieved2.as_ref());
Ok(())
}
#[tokio::test]
async fn test_test_storage_manager_config() {
async fn test_test_storage_manager_config() -> anyhow::Result<()> {
let cfg = testing::test_config_memory();
let test_storage = testing::TestStorageManager::with_config(&cfg)
.await
.expect("create test storage with config");
.with_context(|| "create test storage with config".to_string())?;
let location = "config/test.txt";
let data = b"test data with custom config";
test_storage.put(location, data).await.expect("put");
let retrieved = test_storage.get(location).await.expect("get");
test_storage.put(location, data).await
.with_context(|| "put".to_string())?;
let retrieved = test_storage.get(location).await
.with_context(|| "get".to_string())?;
assert_eq!(retrieved.as_ref(), data);
// Verify it's using memory backend
assert_eq!(*test_storage.storage().backend_kind(), StorageKind::Memory);
Ok(())
}
// S3 Tests - Require a reachable MinIO endpoint and test bucket.
// `TestStorageManager::new_s3()` probes connectivity and these tests auto-skip when unavailable.
#[tokio::test]
async fn test_storage_manager_s3_basic_operations() {
async fn test_storage_manager_s3_basic_operations() -> anyhow::Result<()> {
// Skip if S3 connection fails (e.g. no MinIO)
let Ok(storage) = testing::TestStorageManager::new_s3().await else {
eprintln!("Skipping S3 test (setup failed)");
return;
return Ok(());
};
let prefix = format!("test-basic-{}", Uuid::new_v4());
@@ -973,31 +996,33 @@ mod tests {
// Test put
if let Err(e) = storage.put(&location, data).await {
eprintln!("Skipping S3 test (put failed - bucket missing?): {e}");
return;
return Ok(());
}
// Test get
let retrieved = storage.get(&location).await.expect("get");
let retrieved = storage.get(&location).await.with_context(|| "get".to_string())?;
assert_eq!(retrieved.as_ref(), data);
// Test exists
assert!(storage.exists(&location).await.expect("exists"));
assert!(storage.exists(&location).await.with_context(|| "exists".to_string())?);
// Test delete
storage
.delete_prefix(&format!("{prefix}/"))
.await
.expect("delete");
.with_context(|| "delete".to_string())?;
assert!(!storage
.exists(&location)
.await
.expect("exists after delete"));
.with_context(|| "exists after delete".to_string())?);
Ok(())
}
#[tokio::test]
async fn test_storage_manager_s3_list_operations() {
async fn test_storage_manager_s3_list_operations() -> anyhow::Result<()> {
let Ok(storage) = testing::TestStorageManager::new_s3().await else {
return;
return Ok(());
};
let prefix = format!("test-list-{}", Uuid::new_v4());
@@ -1009,23 +1034,25 @@ mod tests {
for (loc, data) in &files {
if storage.put(loc, *data).await.is_err() {
return; // Abort if put fails
return Ok(()); // Abort if put fails
}
}
// List with prefix
let list_prefix = format!("{prefix}/");
let items = storage.list(Some(&list_prefix)).await.expect("list");
let items = storage.list(Some(&list_prefix)).await.with_context(|| "list".to_string())?;
assert_eq!(items.len(), 3);
// Cleanup
storage.delete_prefix(&list_prefix).await.expect("cleanup");
storage.delete_prefix(&list_prefix).await.with_context(|| "cleanup".to_string())?;
Ok(())
}
#[tokio::test]
async fn test_storage_manager_s3_stream_operations() {
async fn test_storage_manager_s3_stream_operations() -> anyhow::Result<()> {
let Ok(storage) = testing::TestStorageManager::new_s3().await else {
return;
return Ok(());
};
let prefix = format!("test-stream-{}", Uuid::new_v4());
@@ -1033,38 +1060,45 @@ mod tests {
let content = vec![42u8; 1024 * 10]; // 10KB
if storage.put(&location, &content).await.is_err() {
return;
return Ok(());
}
let mut stream = storage.get_stream(&location).await.expect("get stream");
let mut stream = storage.get_stream(&location).await.with_context(|| "get stream".to_string())?;
let mut collected = Vec::new();
while let Some(chunk) = stream.next().await {
collected.extend_from_slice(&chunk.expect("chunk"));
collected.extend_from_slice(&chunk.with_context(|| "chunk".to_string())?);
}
assert_eq!(collected, content);
storage
.delete_prefix(&format!("{prefix}/"))
.await
.expect("cleanup");
.with_context(|| "cleanup".to_string())?;
Ok(())
}
#[tokio::test]
async fn test_storage_manager_s3_backend_kind() {
async fn test_storage_manager_s3_backend_kind() -> anyhow::Result<()> {
let Ok(storage) = testing::TestStorageManager::new_s3().await else {
return;
return Ok(());
};
assert_eq!(*storage.storage().backend_kind(), StorageKind::S3);
Ok(())
}
#[tokio::test]
async fn test_storage_manager_s3_error_handling() {
async fn test_storage_manager_s3_error_handling() -> anyhow::Result<()> {
let Ok(storage) = testing::TestStorageManager::new_s3().await else {
return;
return Ok(());
};
let location = format!("nonexistent-{}/file.txt", Uuid::new_v4());
assert!(storage.get(&location).await.is_err());
assert!(!storage.exists(&location).await.expect("exists check"));
// exists may fail if S3 is unavailable; treat error as false
assert!(!storage.exists(&location).await.unwrap_or(false));
Ok(())
}
}