test: add integration tests to catalog

This commit is contained in:
2025-10-16 11:35:09 +02:00
parent b34532832e
commit e4ddeaf400

View File

@@ -1172,36 +1172,235 @@ mod tests {
#[test]
fn integration_test_init_insert_persist_load_get() {
// Scenario: 'init' -> 'insert' -> 'persist' -> create a new catalog instance -> 'load_by_id' -> 'get' -> verify data integrity.
todo!();
let db_path = "target/test_dbs/integration_test_init_insert_persist_load_get.db";
let path = std::path::Path::new(db_path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
if path.exists() {
std::fs::remove_file(path).unwrap();
}
// 1. 'init' -> 'insert' -> 'persist'
let mut catalog1 = Catalog::new(db_path);
catalog1.init().unwrap();
let item_to_insert = Item {
id: "item-1".to_string(),
name: "Integration Test Item".to_string(),
price: 999,
in_stock: true,
};
catalog1.upsert(item_to_insert.clone());
catalog1.persist().unwrap();
// 2. create a new catalog instance -> 'load_by_id' -> 'get'
let mut catalog2 = Catalog::new(db_path);
catalog2.load_by_id("item-1").unwrap();
let loaded_item: Option<Item> = catalog2.get("item-1");
// 3. verify data integrity
assert_eq!(loaded_item, Some(item_to_insert));
// Clean up
std::fs::remove_file(path).unwrap();
}
#[test]
fn integration_test_init_insert_many_persist_load_list() {
// Scenario: 'init' -> 'insert_many' -> 'persist' -> new catalog -> 'load_by_class' -> 'list_by_class' -> verify all items are loaded.
todo!();
let db_path = "target/test_dbs/integration_test_init_insert_many_persist_load_list.db";
let path = std::path::Path::new(db_path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
if path.exists() {
std::fs::remove_file(path).unwrap();
}
// 1. 'init' -> 'insert_many' -> 'persist'
let mut catalog1 = Catalog::new(db_path);
catalog1.init().unwrap();
let items_to_insert = vec![
Item {
id: "item-1".to_string(),
name: "Item One".to_string(),
price: 100,
in_stock: true,
},
Item {
id: "item-2".to_string(),
name: "Item Two".to_string(),
price: 200,
in_stock: false,
},
];
catalog1.insert_many(items_to_insert.clone());
catalog1.persist().unwrap();
// 2. new catalog -> 'load_by_class' -> 'list_by_class'
let mut catalog2 = Catalog::new(db_path);
catalog2.load_by_class::<Item>().unwrap();
let mut loaded_items: Vec<Item> = catalog2.list_by_class::<Item>().collect();
// Sort by ID to ensure consistent order for comparison
let mut expected_items = items_to_insert;
loaded_items.sort_by(|a, b| a.id.cmp(&b.id));
expected_items.sort_by(|a, b| a.id.cmp(&b.id));
// 3. verify all items are loaded
assert_eq!(loaded_items.len(), 2);
assert_eq!(loaded_items, expected_items);
// Clean up
std::fs::remove_file(path).unwrap();
}
#[test]
fn integration_test_insert_persist_load_delete_persist_load() {
// Scenario: 'insert' -> 'persist' -> 'load_by_id' -> 'delete' -> 'persist' -> 'load_by_id' should now return nothing for the deleted ID.
todo!();
let db_path = "target/test_dbs/integration_test_insert_persist_load_delete_persist_load.db";
let path = std::path::Path::new(db_path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
if path.exists() {
std::fs::remove_file(path).unwrap();
}
// 1. 'insert' -> 'persist'
let mut catalog1 = Catalog::new(db_path);
catalog1.init().unwrap();
let item_to_delete = Item {
id: "item-to-delete".to_string(),
name: "Test Item".to_string(),
price: 123,
in_stock: true,
};
catalog1.upsert(item_to_delete.clone());
catalog1.persist().unwrap();
// 2. 'load_by_id' to confirm it's there
let mut catalog2 = Catalog::new(db_path);
catalog2.load_by_id("item-to-delete").unwrap();
assert!(catalog2.get::<Item>("item-to-delete").is_some());
// 3. 'delete' -> 'persist'
catalog2.delete("item-to-delete");
catalog2.persist().unwrap();
// 4. 'load_by_id' should now return nothing
let mut catalog3 = Catalog::new(db_path);
catalog3.load_by_id("item-to-delete").unwrap();
let loaded_item: Option<Item> = catalog3.get("item-to-delete");
assert!(loaded_item.is_none());
// Clean up
std::fs::remove_file(path).unwrap();
}
#[test]
fn integration_test_insert_get_by_class_and_attribute() {
// Scenario: 'insert' -> 'get_by_class_and_attribute' should return the correct item.
todo!();
// This test focuses on in-memory functionality.
let mut catalog = Catalog::new("dummy.db");
let item1 = Item {
id: "item-1".to_string(),
name: "First Item".to_string(),
price: 100,
in_stock: true,
};
let item2 = Item {
id: "item-2".to_string(),
name: "Second Item".to_string(),
price: 200,
in_stock: false,
};
catalog.upsert(item1.clone());
catalog.upsert(item2.clone());
// Retrieve by a unique attribute
let retrieved_item: Option<Item> =
catalog.get_by_class_and_attribute("name", "Second Item");
// Verify the correct item was retrieved
assert_eq!(retrieved_item, Some(item2));
}
#[test]
fn integration_test_insert_many_list_by_class_and_attribute() {
// Scenario: 'insert_many' -> 'list_by_class_and_attribute' should return all matching items.
todo!();
// This test focuses on in-memory functionality.
let mut catalog = Catalog::new("dummy.db");
let item1 = Item {
id: "item-1".to_string(),
name: "Item One".to_string(),
price: 100,
in_stock: true,
};
let item2 = Item {
id: "item-2".to_string(),
name: "Item Two".to_string(),
price: 200,
in_stock: false,
};
let item3 = Item {
id: "item-3".to_string(),
name: "Item Three".to_string(),
price: 150,
in_stock: true,
};
catalog.insert_many(vec![item1.clone(), item2.clone(), item3.clone()]);
// List all items that are in stock
let mut results: Vec<Item> = catalog
.list_by_class_and_attribute("in_stock", true)
.collect();
// Sort for deterministic comparison
results.sort_by(|a, b| a.id.cmp(&b.id));
let mut expected = vec![item1, item3];
expected.sort_by(|a, b| a.id.cmp(&b.id));
// Verify the correct items were retrieved
assert_eq!(results.len(), 2);
assert_eq!(results, expected);
}
#[test]
#[ignore] // This test can be flaky as it depends on thread scheduling.
fn integration_test_concurrency() {
// Scenario: Concurrency - what happens if two 'Catalog' instances point to the same file? (e.g., one reads while the other writes).
todo!();
// Scenario: Concurrency - what happens if two 'Catalog' instances point to the same file?
// This test demonstrates that without application-level locking, a "last-write-wins"
// race condition can occur, leading to lost updates.
let db_path = "target/test_dbs/integration_test_concurrency.db";
let path = std::path::Path::new(db_path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
if path.exists() {
std::fs::remove_file(path).unwrap();
}
// 1. Initial setup
let mut catalog_setup = Catalog::new(db_path);
catalog_setup.init().unwrap();
let initial_item = Item {
id: "item-1".to_string(),
name: "Original".to_string(),
price: 100,
in_stock: true,
};
catalog_setup.upsert(initial_item);
catalog_setup.persist().unwrap();
let db_path_arc = std::sync::Arc::new(String::from(db_path));
// 2. Thread 1: Loads, updates name, and persists.
let db_path_arc1 = std::sync::Arc::clone(&db_path_arc);
let handle1 = std::thread::spawn(move || {
let mut catalog1 = Catalog::new(&db_path_arc1);
catalog1.load_by_id("item-1").unwrap();
let mut item = catalog1.get::<Item>("item-1").unwrap();
item.name = "Updated by Thread 1".to_string();
catalog1.upsert(item);
catalog1.persist().unwrap();
});
// 3. Thread 2: Loads, updates price, and persists.
let db_path_arc2 = std::sync::Arc::clone(&db_path_arc);
let handle2 = std::thread::spawn(move || {
let mut catalog2 = Catalog::new(&db_path_arc2);
catalog2.load_by_id("item-1").unwrap();
let mut item = catalog2.get::<Item>("item-1").unwrap();
item.price = 200;
catalog2.upsert(item);
catalog2.persist().unwrap();
});
handle1.join().unwrap();
handle2.join().unwrap();
// 4. Verification: Load the data and check the final state.
let mut catalog_verify = Catalog::new(db_path);
catalog_verify.load_by_id("item-1").unwrap();
let final_item: Item = catalog_verify.get("item-1").unwrap();
// The final state depends on which thread persisted last. One update will have been lost.
let thread1_won =
final_item.name == "Updated by Thread 1" && final_item.price == 100;
let thread2_won =
final_item.name == "Original" && final_item.price == 200;
assert!(
thread1_won || thread2_won,
"Final state must be the result of one of the threads winning the race."
);
// Clean up
std::fs::remove_file(path).unwrap();
}
}