chore: extract tests from catalog.rs into their own module implementation
This commit is contained in:
File diff suppressed because it is too large
Load Diff
40
01.workspace/heave/src/tst/catalog_delete.rs
Normal file
40
01.workspace/heave/src/tst/catalog_delete.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::*;
|
||||||
|
#[test]
|
||||||
|
fn delete_should_mark_entity_as_to_delete() {
|
||||||
|
// Should mark an existing entity's state as 'ToDelete'.
|
||||||
|
let mut catalog = Catalog::new("dummy.db");
|
||||||
|
let item = Item {
|
||||||
|
id: "item-123".to_string(),
|
||||||
|
name: "Test Item".to_string(),
|
||||||
|
price: 100,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let item_id = item.id.clone();
|
||||||
|
let _ = catalog.upsert(item);
|
||||||
|
catalog.delete(&item_id);
|
||||||
|
let entity = catalog.items.get(&item_id).unwrap();
|
||||||
|
assert_eq!(entity.state, EntityState::ToDelete);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn delete_should_have_no_effect_for_nonexistent_id() {
|
||||||
|
// Should have no effect if the entity ID does not exist.
|
||||||
|
let mut catalog = Catalog::new("dummy.db");
|
||||||
|
let item = Item {
|
||||||
|
id: "item-123".to_string(),
|
||||||
|
name: "Test Item".to_string(),
|
||||||
|
price: 100,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog.upsert(item);
|
||||||
|
let original_items = catalog.items.clone();
|
||||||
|
// Attempt to delete a non-existent entity, which should not panic or change anything.
|
||||||
|
catalog.delete("nonexistent-id");
|
||||||
|
assert_eq!(catalog.items, original_items);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
01.workspace/heave/src/tst/catalog_get.rs
Normal file
37
01.workspace/heave/src/tst/catalog_get.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::*;
|
||||||
|
#[test]
|
||||||
|
fn get_should_retrieve_and_convert_entity_by_id() {
|
||||||
|
// Should retrieve an entity by its ID and correctly convert it to the target type 'T'.
|
||||||
|
let mut catalog = Catalog::new("dummy.db");
|
||||||
|
let item = Item {
|
||||||
|
id: "item-123".to_string(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "Test Item".to_string(),
|
||||||
|
price: 100,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog.upsert(item.clone());
|
||||||
|
let retrieved_item: Option<Item> = catalog.get::<Item>("item-123").unwrap();
|
||||||
|
assert_eq!(retrieved_item, Some(item));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn get_should_return_none_for_nonexistent_id() {
|
||||||
|
// Should return 'None' if the ID does not exist.
|
||||||
|
let mut catalog = Catalog::new("dummy.db");
|
||||||
|
let item = Item {
|
||||||
|
id: "item-123".to_string(),
|
||||||
|
name: "Test Item".to_string(),
|
||||||
|
price: 100,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog.upsert(item.clone());
|
||||||
|
let retrieved_item: Option<Item> = catalog.get("nonexistent-id").unwrap();
|
||||||
|
assert!(retrieved_item.is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
52
01.workspace/heave/src/tst/catalog_init.rs
Normal file
52
01.workspace/heave/src/tst/catalog_init.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::*;
|
||||||
|
#[test]
|
||||||
|
fn init_should_create_db_file_if_not_exists() {
|
||||||
|
// Should create the SQLite database file if it doesn't exist.
|
||||||
|
let db_path = "target/test_dbs/init_should_create_db_file_if_not_exists.db";
|
||||||
|
let path = std::path::Path::new(db_path);
|
||||||
|
// Ensure the directory exists
|
||||||
|
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||||
|
// Ensure the file does not exist before the test
|
||||||
|
if path.exists() {
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
let catalog = Catalog::new(db_path);
|
||||||
|
let result = catalog.init();
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert!(path.exists());
|
||||||
|
// Clean up the created file
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn init_should_not_fail_if_db_file_exists() {
|
||||||
|
// Should not fail if the database file already exists.
|
||||||
|
let db_path = "target/test_dbs/init_should_not_fail_if_db_file_exists.db";
|
||||||
|
let path = std::path::Path::new(db_path);
|
||||||
|
// Ensure the directory exists
|
||||||
|
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||||
|
// Create the DB file first
|
||||||
|
let catalog = Catalog::new(db_path);
|
||||||
|
catalog.init().unwrap();
|
||||||
|
// Calling init() again should not fail
|
||||||
|
let result = catalog.init();
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert!(path.exists());
|
||||||
|
// Clean up
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn init_should_return_error_for_invalid_path() {
|
||||||
|
// Should return an error for an invalid path or permissions issue.
|
||||||
|
// Using a directory as a path should fail.
|
||||||
|
let invalid_path = "target/test_dbs/an_invalid_path_dir";
|
||||||
|
std::fs::create_dir_all(invalid_path).unwrap();
|
||||||
|
let catalog = Catalog::new(invalid_path);
|
||||||
|
let result = catalog.init();
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err(), FailedTo::InitDatabase);
|
||||||
|
// Clean up
|
||||||
|
std::fs::remove_dir_all(invalid_path).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
39
01.workspace/heave/src/tst/catalog_insert_many.rs
Normal file
39
01.workspace/heave/src/tst/catalog_insert_many.rs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::*;
|
||||||
|
#[test]
|
||||||
|
fn insert_many_should_add_all_entities() {
|
||||||
|
// 'insert_many()': Should add all provided entities to the 'items' map.
|
||||||
|
let mut catalog = Catalog::new("dummy.db");
|
||||||
|
let items = vec![
|
||||||
|
Item {
|
||||||
|
id: "item-1".to_string(),
|
||||||
|
name: "Item 1".to_string(),
|
||||||
|
price: 10,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
},
|
||||||
|
Item {
|
||||||
|
id: "item-2".to_string(),
|
||||||
|
name: "Item 2".to_string(),
|
||||||
|
price: 20,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: false,
|
||||||
|
..Item::default()
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let _ = catalog.insert_many(items);
|
||||||
|
assert_eq!(catalog.items.len(), 2);
|
||||||
|
let entity1 = catalog.items.get("item-1").unwrap();
|
||||||
|
assert_eq!(entity1.state, EntityState::New);
|
||||||
|
assert_eq!(entity1.value_of("name"), Some(&Value::from("Item 1")));
|
||||||
|
assert_eq!(entity1.value_of("price"), Some(&Value::from(10u64)));
|
||||||
|
assert_eq!(entity1.value_of("sell_trend"), Some(&Value::from(0i64)));
|
||||||
|
let entity2 = catalog.items.get("item-2").unwrap();
|
||||||
|
assert_eq!(entity2.state, EntityState::New);
|
||||||
|
assert_eq!(entity2.value_of("name"), Some(&Value::from("Item 2")));
|
||||||
|
assert_eq!(entity2.value_of("price"), Some(&Value::from(20u64)));
|
||||||
|
assert_eq!(entity2.value_of("sell_trend"), Some(&Value::from(0i64)));
|
||||||
|
}
|
||||||
|
}
|
||||||
189
01.workspace/heave/src/tst/catalog_integration.rs
Normal file
189
01.workspace/heave/src/tst/catalog_integration.rs
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::*;
|
||||||
|
#[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.
|
||||||
|
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(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "Integration Test Item".to_string(),
|
||||||
|
price: 999,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = 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").unwrap();
|
||||||
|
// 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.
|
||||||
|
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(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "Item One".to_string(),
|
||||||
|
price: 100,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
},
|
||||||
|
Item {
|
||||||
|
id: "item-2".to_string(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "Item Two".to_string(),
|
||||||
|
price: 200,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: false,
|
||||||
|
..Item::default()
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let _ = 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>()
|
||||||
|
.map(|item| item.unwrap())
|
||||||
|
.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.
|
||||||
|
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,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = 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").unwrap().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").unwrap();
|
||||||
|
assert!(loaded_item.is_none());
|
||||||
|
// Clean up
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
#[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?
|
||||||
|
// 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,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = 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().unwrap();
|
||||||
|
item.name = "Updated by Thread 1".to_string();
|
||||||
|
let _ = 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().unwrap();
|
||||||
|
item.price = 200;
|
||||||
|
let _ = 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().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
|
||||||
|
&& final_item.sell_trend == 0;
|
||||||
|
let thread2_won =
|
||||||
|
final_item.name == "Original" && final_item.price == 200 && final_item.sell_trend == 0;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
46
01.workspace/heave/src/tst/catalog_list_by_class.rs
Normal file
46
01.workspace/heave/src/tst/catalog_list_by_class.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::*;
|
||||||
|
#[test]
|
||||||
|
fn list_by_class_should_return_all_entities_of_class() {
|
||||||
|
// Should return an iterator with all entities of a specific class.
|
||||||
|
let mut catalog = Catalog::new("dummy.db");
|
||||||
|
let item1 = Item {
|
||||||
|
id: "item-1".to_string(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "Item One".to_string(),
|
||||||
|
price: 100,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let item2 = Item {
|
||||||
|
id: "item-2".to_string(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "Item Two".to_string(),
|
||||||
|
price: 200,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: false,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog.upsert(item1.clone());
|
||||||
|
let _ = catalog.upsert(item2.clone());
|
||||||
|
let results: Vec<Item> = catalog
|
||||||
|
.list_by_class::<Item>()
|
||||||
|
.map(|item| item.unwrap())
|
||||||
|
.collect();
|
||||||
|
assert_eq!(results.len(), 2);
|
||||||
|
assert!(results.contains(&item1));
|
||||||
|
assert!(results.contains(&item2));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn list_by_class_should_return_empty_iterator_if_no_match() {
|
||||||
|
// Should return an empty iterator if no entities of that class exist.
|
||||||
|
let catalog = Catalog::new("dummy.db");
|
||||||
|
let results: Vec<Item> = catalog
|
||||||
|
.list_by_class::<Item>()
|
||||||
|
.map(|item| item.unwrap())
|
||||||
|
.collect();
|
||||||
|
assert!(results.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::*;
|
||||||
|
#[test]
|
||||||
|
fn list_by_class_and_subclass_should_return_matching_entities() {
|
||||||
|
let mut catalog = Catalog::new("dummy.db");
|
||||||
|
let item1 = Item {
|
||||||
|
id: "item-1".to_string(),
|
||||||
|
subclass: Some("electronics".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let item2 = Item {
|
||||||
|
id: "item-2".to_string(),
|
||||||
|
subclass: Some("books".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let item3 = Item {
|
||||||
|
id: "item-3".to_string(),
|
||||||
|
subclass: Some("electronics".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let _ = catalog.upsert(item1.clone());
|
||||||
|
let _ = catalog.upsert(item2.clone());
|
||||||
|
let _ = catalog.upsert(item3.clone());
|
||||||
|
let results: Vec<Item> = catalog
|
||||||
|
.list_by_class_and_subclass("electronics")
|
||||||
|
.map(|item| item.unwrap())
|
||||||
|
.collect();
|
||||||
|
assert_eq!(results.len(), 2);
|
||||||
|
assert!(results.contains(&item1));
|
||||||
|
assert!(results.contains(&item3));
|
||||||
|
assert!(!results.contains(&item2));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn list_by_class_and_subclass_should_return_empty_if_no_match() {
|
||||||
|
let mut catalog = Catalog::new("dummy.db");
|
||||||
|
let item1 = Item {
|
||||||
|
id: "item-1".to_string(),
|
||||||
|
subclass: Some("electronics".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let _ = catalog.upsert(item1.clone());
|
||||||
|
let results: Vec<Item> = catalog
|
||||||
|
.list_by_class_and_subclass("books")
|
||||||
|
.map(|item| item.unwrap())
|
||||||
|
.collect();
|
||||||
|
assert!(results.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
165
01.workspace/heave/src/tst/catalog_load_by_class.rs
Normal file
165
01.workspace/heave/src/tst/catalog_load_by_class.rs
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::*;
|
||||||
|
#[test]
|
||||||
|
fn load_by_class_should_load_all_entities_of_class() {
|
||||||
|
// Should load all entities of a given class from the database into the 'items' map.
|
||||||
|
let db_path = "target/test_dbs/load_by_class_should_load_all_entities_of_class.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. Setup DB with a few items of the same class
|
||||||
|
let mut catalog1 = Catalog::new(db_path);
|
||||||
|
catalog1.init().unwrap();
|
||||||
|
let item1 = Item {
|
||||||
|
id: "item-1".to_string(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "Item One".to_string(),
|
||||||
|
price: 100,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let item2 = Item {
|
||||||
|
id: "item-2".to_string(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "Item Two".to_string(),
|
||||||
|
price: 200,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: false,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog1.upsert(item1.clone());
|
||||||
|
let _ = catalog1.upsert(item2.clone());
|
||||||
|
catalog1.persist().unwrap();
|
||||||
|
// 2. Create a new catalog and load the items by class
|
||||||
|
let mut catalog2 = Catalog::new(db_path);
|
||||||
|
let result = catalog2.load_by_class::<Item>();
|
||||||
|
assert!(result.is_ok());
|
||||||
|
// 3. Verify that all items of that class were loaded
|
||||||
|
assert_eq!(catalog2.items.len(), 2);
|
||||||
|
let loaded_item1: Item = catalog2.get("item-1").unwrap().unwrap();
|
||||||
|
let loaded_item2: Item = catalog2.get("item-2").unwrap().unwrap();
|
||||||
|
assert_eq!(loaded_item1, item1);
|
||||||
|
assert_eq!(loaded_item2, item2);
|
||||||
|
assert_eq!(
|
||||||
|
catalog2.items.get("item-1").unwrap().state,
|
||||||
|
EntityState::Loaded
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
catalog2.items.get("item-2").unwrap().state,
|
||||||
|
EntityState::Loaded
|
||||||
|
);
|
||||||
|
// Clean up
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn load_by_class_should_overwrite_in_memory_entities() {
|
||||||
|
// Should overwrite any existing in-memory entities with the same IDs.
|
||||||
|
let db_path = "target/test_dbs/load_by_class_should_overwrite_in_memory_entities.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. Persist an item to the database.
|
||||||
|
let mut catalog1 = Catalog::new(db_path);
|
||||||
|
catalog1.init().unwrap();
|
||||||
|
let item_in_db = Item {
|
||||||
|
id: "item-1".to_string(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "DB Version".to_string(),
|
||||||
|
price: 100,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog1.upsert(item_in_db.clone());
|
||||||
|
catalog1.persist().unwrap();
|
||||||
|
// 2. Create a new catalog with a different in-memory version of the same item.
|
||||||
|
let mut catalog2 = Catalog::new(db_path);
|
||||||
|
let item_in_memory = Item {
|
||||||
|
id: "item-1".to_string(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "Memory Version".to_string(),
|
||||||
|
price: 200,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: false,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog2.upsert(item_in_memory);
|
||||||
|
assert_eq!(catalog2.items.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
catalog2.get::<Item>("item-1").unwrap().unwrap().name,
|
||||||
|
"Memory Version"
|
||||||
|
);
|
||||||
|
// 3. Load from the database, which should overwrite the in-memory version.
|
||||||
|
let result = catalog2.load_by_class::<Item>();
|
||||||
|
assert!(result.is_ok());
|
||||||
|
// 4. Verify that the in-memory entity has been replaced with the one from the DB.
|
||||||
|
assert_eq!(catalog2.items.len(), 1);
|
||||||
|
let loaded_item: Item = catalog2.get("item-1").unwrap().unwrap();
|
||||||
|
assert_eq!(loaded_item, item_in_db);
|
||||||
|
assert_eq!(
|
||||||
|
catalog2.items.get("item-1").unwrap().state,
|
||||||
|
EntityState::Loaded
|
||||||
|
);
|
||||||
|
// Clean up
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn load_by_class_should_do_nothing_if_none_found() {
|
||||||
|
// Should do nothing if no entities of that class are found in the database.
|
||||||
|
let db_path = "target/test_dbs/load_by_class_should_do_nothing_if_none_found.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. Create an empty, initialized database.
|
||||||
|
let mut catalog = Catalog::new(db_path);
|
||||||
|
catalog.init().unwrap();
|
||||||
|
// 2. Attempt to load from the empty DB.
|
||||||
|
let result = catalog.load_by_class::<Item>();
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert!(catalog.items.is_empty());
|
||||||
|
// 3. Add an item to memory and try loading again from the empty DB.
|
||||||
|
let item_in_memory = Item {
|
||||||
|
id: "item-1".to_string(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "In-memory only".to_string(),
|
||||||
|
price: 100,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog.upsert(item_in_memory.clone());
|
||||||
|
assert_eq!(catalog.items.len(), 1);
|
||||||
|
let result2 = catalog.load_by_class::<Item>();
|
||||||
|
assert!(result2.is_ok());
|
||||||
|
// 4. Verify the in-memory item is untouched because nothing was loaded from DB.
|
||||||
|
assert_eq!(catalog.items.len(), 1);
|
||||||
|
let retrieved_item: Item = catalog.get("item-1").unwrap().unwrap();
|
||||||
|
assert_eq!(retrieved_item, item_in_memory);
|
||||||
|
// Clean up
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn load_by_class_should_return_error_on_db_failure() {
|
||||||
|
// Should return an error if the database operation fails.
|
||||||
|
// Using a directory as a path should cause a failure.
|
||||||
|
let invalid_path = "target/test_dbs/a_directory_for_load_class_fail";
|
||||||
|
std::fs::create_dir_all(invalid_path).unwrap();
|
||||||
|
let mut catalog = Catalog::new(invalid_path);
|
||||||
|
// Attempt to load from the invalid path.
|
||||||
|
let result = catalog.load_by_class::<Item>();
|
||||||
|
assert!(result.is_err());
|
||||||
|
// Based on `load_by_id`, the error should be `LoadFromDB`.
|
||||||
|
// This assumes `From<SqliteFailedTo>` is implemented to produce `FailedTo::LoadFromDB`.
|
||||||
|
assert_eq!(result.unwrap_err(), FailedTo::LoadFromDB);
|
||||||
|
// Clean up
|
||||||
|
std::fs::remove_dir_all(invalid_path).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
1507
01.workspace/heave/src/tst/catalog_load_by_filter.rs
Normal file
1507
01.workspace/heave/src/tst/catalog_load_by_filter.rs
Normal file
File diff suppressed because it is too large
Load Diff
155
01.workspace/heave/src/tst/catalog_load_by_id.rs
Normal file
155
01.workspace/heave/src/tst/catalog_load_by_id.rs
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::*;
|
||||||
|
#[test]
|
||||||
|
fn load_by_id_should_load_entity_from_db() {
|
||||||
|
// Should load a single entity from the database into the 'items' map.
|
||||||
|
let db_path = "target/test_dbs/load_by_id_should_load_entity_from_db.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. Create a catalog, insert an item, and persist it to the DB.
|
||||||
|
let mut catalog1 = Catalog::new(db_path);
|
||||||
|
catalog1.init().unwrap();
|
||||||
|
let item_to_persist = Item {
|
||||||
|
id: "item-1".to_string(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "Test Item".to_string(),
|
||||||
|
price: 123,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog1.upsert(item_to_persist.clone());
|
||||||
|
catalog1.persist().unwrap();
|
||||||
|
// 2. Create a new, empty catalog instance for the same DB.
|
||||||
|
let mut catalog2 = Catalog::new(db_path);
|
||||||
|
assert!(catalog2.items.is_empty());
|
||||||
|
// 3. Load the item by its ID.
|
||||||
|
let result = catalog2.load_by_id("item-1");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
// 4. Verify the item is now in the in-memory 'items' map.
|
||||||
|
assert_eq!(catalog2.items.len(), 1);
|
||||||
|
let loaded_entity = catalog2.items.get("item-1").unwrap();
|
||||||
|
// 5. Verify the loaded entity's data and state.
|
||||||
|
assert_eq!(loaded_entity.id, "item-1");
|
||||||
|
assert_eq!(loaded_entity.class, "item");
|
||||||
|
assert_eq!(
|
||||||
|
loaded_entity.value_of("name"),
|
||||||
|
Some(&Value::from("Test Item"))
|
||||||
|
);
|
||||||
|
assert_eq!(loaded_entity.value_of("price"), Some(&Value::from(123u64)));
|
||||||
|
assert_eq!(
|
||||||
|
loaded_entity.value_of("sell_trend"),
|
||||||
|
Some(&Value::from(0i64))
|
||||||
|
);
|
||||||
|
assert_eq!(loaded_entity.value_of("in_stock"), Some(&Value::from(true)));
|
||||||
|
assert_eq!(loaded_entity.state, EntityState::Loaded); // Should be Synced after loading.
|
||||||
|
// 6. Also verify by using the public 'get' method.
|
||||||
|
let retrieved_item: Option<Item> = catalog2.get("item-1").unwrap();
|
||||||
|
assert_eq!(retrieved_item, Some(item_to_persist));
|
||||||
|
// Clean up
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn load_by_id_should_overwrite_in_memory_entity() {
|
||||||
|
// Should overwrite an existing in-memory entity with the same ID.
|
||||||
|
let db_path = "target/test_dbs/load_by_id_should_overwrite_in_memory_entity.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. Persist an item to the database.
|
||||||
|
let mut catalog1 = Catalog::new(db_path);
|
||||||
|
catalog1.init().unwrap();
|
||||||
|
let item_in_db = Item {
|
||||||
|
id: "item-1".to_string(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "Item from DB".to_string(),
|
||||||
|
price: 100,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog1.upsert(item_in_db.clone());
|
||||||
|
catalog1.persist().unwrap();
|
||||||
|
// 2. Create a new catalog and add a *different* in-memory version of the same item.
|
||||||
|
let mut catalog2 = Catalog::new(db_path);
|
||||||
|
let item_in_memory = Item {
|
||||||
|
id: "item-1".to_string(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "In-memory version".to_string(),
|
||||||
|
price: 200,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: false,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog2.upsert(item_in_memory);
|
||||||
|
let entity_before_load = catalog2.items.get("item-1").unwrap();
|
||||||
|
assert_eq!(entity_before_load.state, EntityState::New);
|
||||||
|
assert_eq!(
|
||||||
|
entity_before_load.value_of("name"),
|
||||||
|
Some(&Value::from("In-memory version"))
|
||||||
|
);
|
||||||
|
// 3. Load the item from the database, which should overwrite the in-memory version.
|
||||||
|
let result = catalog2.load_by_id("item-1");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
// 4. Verify that the in-memory entity has been replaced with the one from the DB.
|
||||||
|
let entity_after_load = catalog2.items.get("item-1").unwrap();
|
||||||
|
assert_eq!(entity_after_load.state, EntityState::Loaded);
|
||||||
|
assert_eq!(
|
||||||
|
entity_after_load.value_of("name"),
|
||||||
|
Some(&Value::from("Item from DB"))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
entity_after_load.value_of("price"),
|
||||||
|
Some(&Value::from(100u64))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
entity_after_load.value_of("sell_trend"),
|
||||||
|
Some(&Value::from(0i64))
|
||||||
|
);
|
||||||
|
// 5. Verify using the public 'get' method.
|
||||||
|
let retrieved_item: Item = catalog2.get("item-1").unwrap().unwrap();
|
||||||
|
assert_eq!(retrieved_item, item_in_db);
|
||||||
|
// Clean up
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn load_by_id_should_do_nothing_if_not_found() {
|
||||||
|
// Should do nothing if the entity is not found in the database.
|
||||||
|
let db_path = "target/test_dbs/load_by_id_should_do_nothing_if_not_found.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. Create an empty, initialized database.
|
||||||
|
let mut catalog = Catalog::new(db_path);
|
||||||
|
catalog.init().unwrap();
|
||||||
|
// 2. Attempt to load an ID that does not exist.
|
||||||
|
let result = catalog.load_by_id("nonexistent-id");
|
||||||
|
// 3. Verify that the operation succeeded and the catalog remains empty.
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert!(catalog.items.is_empty());
|
||||||
|
// Clean up
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn load_by_id_should_return_error_on_db_failure() {
|
||||||
|
// Should return an error if the database operation fails.
|
||||||
|
// Using a directory as a path should cause a failure.
|
||||||
|
let invalid_path = "target/test_dbs/a_directory_for_load_fail";
|
||||||
|
std::fs::create_dir_all(invalid_path).unwrap();
|
||||||
|
let mut catalog = Catalog::new(invalid_path);
|
||||||
|
// Attempt to load from the invalid path.
|
||||||
|
let result = catalog.load_by_id("any-id");
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err(), FailedTo::LoadFromDB);
|
||||||
|
// Clean up
|
||||||
|
std::fs::remove_dir_all(invalid_path).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
12
01.workspace/heave/src/tst/catalog_new.rs
Normal file
12
01.workspace/heave/src/tst/catalog_new.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::*;
|
||||||
|
#[test]
|
||||||
|
fn new_should_create_catalog_with_path_and_empty_items() {
|
||||||
|
// Should create a new Catalog with the given path and an empty 'items' map.
|
||||||
|
let path = "test.db";
|
||||||
|
let catalog = Catalog::new(path);
|
||||||
|
assert_eq!(catalog.path, path);
|
||||||
|
assert!(catalog.items.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
364
01.workspace/heave/src/tst/catalog_persist.rs
Normal file
364
01.workspace/heave/src/tst/catalog_persist.rs
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::*;
|
||||||
|
#[test]
|
||||||
|
fn persist_should_insert_new_entities() {
|
||||||
|
// Should insert entities with 'EntityState::New' into the database.
|
||||||
|
let db_path = "target/test_dbs/persist_should_insert_new_entities.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. Create catalog, insert an item, and persist
|
||||||
|
let mut catalog1 = Catalog::new(db_path);
|
||||||
|
catalog1.init().unwrap();
|
||||||
|
let item1 = Item {
|
||||||
|
id: "item-1".to_string(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "Test Item".to_string(),
|
||||||
|
price: 123,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog1.upsert(item1.clone());
|
||||||
|
assert!(catalog1.persist().is_ok());
|
||||||
|
// 2. Create a new catalog and load the item to verify it was persisted
|
||||||
|
let mut catalog2 = Catalog::new(db_path);
|
||||||
|
assert!(catalog2.load_by_id("item-1").is_ok());
|
||||||
|
// 3. Get the item and assert it's the same as the one we inserted
|
||||||
|
let loaded_item: Option<Item> = catalog2.get("item-1").unwrap();
|
||||||
|
assert_eq!(loaded_item, Some(item1));
|
||||||
|
// Clean up
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn persist_should_delete_to_delete_entities() {
|
||||||
|
// Should delete entities with 'EntityState::ToDelete' from the database.
|
||||||
|
let db_path = "target/test_dbs/persist_should_delete_to_delete_entities.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. Create catalog, insert an item, and persist it.
|
||||||
|
let mut catalog1 = Catalog::new(db_path);
|
||||||
|
catalog1.init().unwrap();
|
||||||
|
let item1 = Item {
|
||||||
|
id: "item-to-delete".to_string(),
|
||||||
|
name: "Test Item".to_string(),
|
||||||
|
price: 123,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog1.upsert(item1.clone());
|
||||||
|
assert!(catalog1.persist().is_ok());
|
||||||
|
// 2. Mark the item for deletion and persist again.
|
||||||
|
catalog1.delete(&item1.id);
|
||||||
|
assert!(catalog1.persist().is_ok());
|
||||||
|
// 3. Create a new catalog and try to load the deleted item.
|
||||||
|
let mut catalog2 = Catalog::new(db_path);
|
||||||
|
assert!(catalog2.load_by_id(&item1.id).is_ok());
|
||||||
|
// 4. Assert that the item was not found.
|
||||||
|
let loaded_item: Option<Item> = catalog2.get(&item1.id).unwrap();
|
||||||
|
assert!(loaded_item.is_none());
|
||||||
|
// Clean up
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn persist_should_update_updated_entities() {
|
||||||
|
// Should update entities with 'EntityState::Updated' in the database.
|
||||||
|
let db_path = "target/test_dbs/persist_should_update_updated_entities.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 an entity and persist it.
|
||||||
|
let mut catalog1 = Catalog::new(db_path);
|
||||||
|
catalog1.init().unwrap();
|
||||||
|
let original_item = Item {
|
||||||
|
id: "item-1".to_string(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "Original Name".to_string(),
|
||||||
|
price: 100,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog1.upsert(original_item.clone());
|
||||||
|
catalog1.persist().unwrap();
|
||||||
|
// 2. Load it into a new catalog to simulate a separate session.
|
||||||
|
let mut catalog2 = Catalog::new(db_path);
|
||||||
|
catalog2.load_by_id("item-1").unwrap();
|
||||||
|
// 3. Upsert updated data for the same item. This should mark it as 'Updated'.
|
||||||
|
let updated_item = Item {
|
||||||
|
id: "item-1".to_string(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "Updated Name".to_string(),
|
||||||
|
price: 200,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: false,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog2.upsert(updated_item.clone());
|
||||||
|
assert_eq!(
|
||||||
|
catalog2.items.get("item-1").unwrap().state,
|
||||||
|
EntityState::Updated
|
||||||
|
);
|
||||||
|
// 4. Persist the changes.
|
||||||
|
catalog2.persist().unwrap();
|
||||||
|
// 5. Load the data into a third catalog to verify the update was written to the DB.
|
||||||
|
let mut catalog3 = Catalog::new(db_path);
|
||||||
|
catalog3.load_by_id("item-1").unwrap();
|
||||||
|
let loaded_item: Item = catalog3.get("item-1").unwrap().unwrap();
|
||||||
|
// 6. Assert that the loaded item has the updated values.
|
||||||
|
assert_eq!(loaded_item, updated_item);
|
||||||
|
assert_ne!(loaded_item, original_item);
|
||||||
|
// Clean up
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn persist_should_handle_mixed_entity_states() {
|
||||||
|
// Should handle a mix of new, updated, and deleted entities in one operation.
|
||||||
|
let db_path = "target/test_dbs/persist_should_handle_mixed_entity_states.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. Setup: Pre-populate the database with some items.
|
||||||
|
let mut catalog_setup = Catalog::new(db_path);
|
||||||
|
catalog_setup.init().unwrap();
|
||||||
|
let item_to_update_original = Item {
|
||||||
|
id: "update-me".to_string(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "Original".to_string(),
|
||||||
|
price: 10,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let item_to_delete = Item {
|
||||||
|
id: "delete-me".to_string(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "Delete Me".to_string(),
|
||||||
|
price: 20,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let item_to_keep = Item {
|
||||||
|
id: "keep-me".to_string(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "Keep Me".to_string(),
|
||||||
|
price: 30,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog_setup.upsert(item_to_update_original.clone());
|
||||||
|
let _ = catalog_setup.upsert(item_to_delete.clone());
|
||||||
|
let _ = catalog_setup.upsert(item_to_keep.clone());
|
||||||
|
catalog_setup.persist().unwrap();
|
||||||
|
// 2. Manipulation: Load the data and perform mixed operations.
|
||||||
|
let mut catalog_ops = Catalog::new(db_path);
|
||||||
|
catalog_ops.load_by_class::<Item>().unwrap(); // Load all items
|
||||||
|
// A new item to be inserted.
|
||||||
|
let item_to_add = Item {
|
||||||
|
id: "add-me".to_string(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "Add Me".to_string(),
|
||||||
|
price: 40,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: false,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog_ops.upsert(item_to_add.clone()); // State: New
|
||||||
|
// An updated version of an existing item.
|
||||||
|
let item_to_update_new = Item {
|
||||||
|
id: "update-me".to_string(),
|
||||||
|
subclass: Some("subitem".to_string()),
|
||||||
|
name: "Updated".to_string(),
|
||||||
|
price: 11,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: false,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog_ops.upsert(item_to_update_new.clone()); // State: Updated
|
||||||
|
// An item to be deleted.
|
||||||
|
catalog_ops.delete("delete-me"); // State: ToDelete
|
||||||
|
// item_to_keep is left untouched (State: Synced after load)
|
||||||
|
// 3. Execution: Persist all the changes in one go.
|
||||||
|
catalog_ops.persist().unwrap();
|
||||||
|
// 4. Verification: Load into a new catalog and check the final state of the DB.
|
||||||
|
let mut catalog_verify = Catalog::new(db_path);
|
||||||
|
catalog_verify.load_by_class::<Item>().unwrap();
|
||||||
|
// Check total count
|
||||||
|
assert_eq!(catalog_verify.items.len(), 3);
|
||||||
|
// Verify added item
|
||||||
|
let added_item: Item = catalog_verify.get("add-me").unwrap().unwrap();
|
||||||
|
assert_eq!(added_item, item_to_add);
|
||||||
|
// Verify updated item
|
||||||
|
let updated_item: Item = catalog_verify.get("update-me").unwrap().unwrap();
|
||||||
|
assert_eq!(updated_item, item_to_update_new);
|
||||||
|
// Verify deleted item
|
||||||
|
let deleted_item: Option<Item> = catalog_verify.get("delete-me").unwrap();
|
||||||
|
assert!(deleted_item.is_none());
|
||||||
|
// Verify untouched item
|
||||||
|
let kept_item: Item = catalog_verify.get("keep-me").unwrap().unwrap();
|
||||||
|
assert_eq!(kept_item, item_to_keep);
|
||||||
|
// Clean up
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn persist_should_return_error_on_db_failure() {
|
||||||
|
// Should return an error if the database connection fails or a query fails.
|
||||||
|
// Using a directory as a path should cause a failure.
|
||||||
|
let invalid_path = "target/test_dbs/a_directory_for_persist_fail";
|
||||||
|
std::fs::create_dir_all(invalid_path).unwrap();
|
||||||
|
let mut catalog = Catalog::new(invalid_path);
|
||||||
|
let item = Item {
|
||||||
|
id: "item-1".to_string(),
|
||||||
|
name: "Test".to_string(),
|
||||||
|
price: 10,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog.upsert(item);
|
||||||
|
let result = catalog.persist();
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err(), FailedTo::PersistCatalog);
|
||||||
|
// Clean up
|
||||||
|
std::fs::remove_dir_all(invalid_path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn persist_should_update_in_memory_state() {
|
||||||
|
// After persisting, the in-memory state of entities should be considered.
|
||||||
|
// (e.g., should deleted items be removed from the 'items' map, all other items should be marked as Loaded).
|
||||||
|
let db_path = "target/test_dbs/persist_should_update_in_memory_state.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. Setup: Create a catalog and pre-populate it with some data.
|
||||||
|
let mut catalog = Catalog::new(db_path);
|
||||||
|
catalog.init().unwrap();
|
||||||
|
let item_to_update = Item {
|
||||||
|
id: "update-me".to_string(),
|
||||||
|
name: "Original".to_string(),
|
||||||
|
price: 0,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: false,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let item_to_delete = Item {
|
||||||
|
id: "delete-me".to_string(),
|
||||||
|
name: "Delete Me".to_string(),
|
||||||
|
price: 0,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: false,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let item_untouched = Item {
|
||||||
|
id: "keep-me".to_string(),
|
||||||
|
name: "Keep Me".to_string(),
|
||||||
|
price: 0,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: false,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog.upsert(item_to_update.clone());
|
||||||
|
let _ = catalog.upsert(item_to_delete.clone());
|
||||||
|
let _ = catalog.upsert(item_untouched.clone());
|
||||||
|
catalog.persist().unwrap();
|
||||||
|
// At this point, all items are in the DB and in-memory state is `Loaded`.
|
||||||
|
assert_eq!(catalog.items.len(), 3);
|
||||||
|
assert_eq!(
|
||||||
|
catalog.items.get("update-me").unwrap().state,
|
||||||
|
EntityState::Loaded
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
catalog.items.get("delete-me").unwrap().state,
|
||||||
|
EntityState::Loaded
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
catalog.items.get("keep-me").unwrap().state,
|
||||||
|
EntityState::Loaded
|
||||||
|
);
|
||||||
|
// 2. Manipulate the catalog to have entities in various states.
|
||||||
|
// A new item to be inserted.
|
||||||
|
let item_new = Item {
|
||||||
|
id: "add-me".to_string(),
|
||||||
|
name: "Add Me".to_string(),
|
||||||
|
price: 0,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: false,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog.upsert(item_new.clone()); // State: New
|
||||||
|
// An updated version of an existing item.
|
||||||
|
let item_updated = Item {
|
||||||
|
id: "update-me".to_string(),
|
||||||
|
name: "Updated".to_string(),
|
||||||
|
price: 0,
|
||||||
|
sell_trend: 10,
|
||||||
|
in_stock: false,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog.upsert(item_updated.clone()); // State: Updated
|
||||||
|
// An item to be deleted.
|
||||||
|
catalog.delete("delete-me"); // State: ToDelete
|
||||||
|
// 'item_untouched' remains with state `Loaded`.
|
||||||
|
// Check states before final persist
|
||||||
|
assert_eq!(catalog.items.get("add-me").unwrap().state, EntityState::New);
|
||||||
|
assert_eq!(
|
||||||
|
catalog.items.get("update-me").unwrap().state,
|
||||||
|
EntityState::Updated
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
catalog.items.get("delete-me").unwrap().state,
|
||||||
|
EntityState::ToDelete
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
catalog.items.get("keep-me").unwrap().state,
|
||||||
|
EntityState::Loaded
|
||||||
|
);
|
||||||
|
assert_eq!(catalog.items.len(), 4);
|
||||||
|
// 3. Persist all changes.
|
||||||
|
catalog.persist().unwrap();
|
||||||
|
// 4. Verify the in-memory state after persisting.
|
||||||
|
// The item marked for deletion should be gone.
|
||||||
|
assert!(!catalog.items.contains_key("delete-me"));
|
||||||
|
assert_eq!(catalog.items.len(), 3);
|
||||||
|
// All remaining items should have their state as `Loaded`.
|
||||||
|
let new_item_entity = catalog.items.get("add-me").unwrap();
|
||||||
|
assert_eq!(new_item_entity.state, EntityState::Loaded);
|
||||||
|
assert_eq!(
|
||||||
|
new_item_entity.value_of("name"),
|
||||||
|
Some(&Value::from("Add Me"))
|
||||||
|
);
|
||||||
|
let updated_item_entity = catalog.items.get("update-me").unwrap();
|
||||||
|
assert_eq!(updated_item_entity.state, EntityState::Loaded);
|
||||||
|
assert_eq!(
|
||||||
|
updated_item_entity.value_of("name"),
|
||||||
|
Some(&Value::from("Updated"))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
updated_item_entity.value_of("sell_trend"),
|
||||||
|
Some(&Value::from(10i64))
|
||||||
|
);
|
||||||
|
let untouched_item_entity = catalog.items.get("keep-me").unwrap();
|
||||||
|
assert_eq!(untouched_item_entity.state, EntityState::Loaded);
|
||||||
|
assert_eq!(
|
||||||
|
untouched_item_entity.value_of("name"),
|
||||||
|
Some(&Value::from("Keep Me"))
|
||||||
|
);
|
||||||
|
// Clean up
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
58
01.workspace/heave/src/tst/catalog_upsert.rs
Normal file
58
01.workspace/heave/src/tst/catalog_upsert.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::*;
|
||||||
|
#[test]
|
||||||
|
fn upsert_should_add_single_entity_as_new() {
|
||||||
|
// 'upsert()': Should add a single entity to the 'items' map with 'EntityState::New'.
|
||||||
|
let mut catalog = Catalog::new("dummy.db");
|
||||||
|
let item = Item {
|
||||||
|
id: "item-123".to_string(),
|
||||||
|
name: "Test Item".to_string(),
|
||||||
|
price: 100,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let item_id = item.id.clone();
|
||||||
|
let _ = catalog.upsert(item);
|
||||||
|
let entity = catalog.items.get(&item_id).unwrap();
|
||||||
|
assert_eq!(entity.id, item_id);
|
||||||
|
assert_eq!(entity.state, EntityState::New);
|
||||||
|
assert_eq!(entity.class, "item");
|
||||||
|
assert_eq!(entity.value_of("name"), Some(&Value::from("Test Item")));
|
||||||
|
assert_eq!(entity.value_of("price"), Some(&Value::from(100u64)));
|
||||||
|
assert_eq!(entity.value_of("sell_trend"), Some(&Value::from(0i64)));
|
||||||
|
assert_eq!(entity.value_of("in_stock"), Some(&Value::from(true)));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn upsert_should_overwrite_existing_entity() {
|
||||||
|
// 'upsert()': Should overwrite an existing entity with the same ID.
|
||||||
|
let mut catalog = Catalog::new("dummy.db");
|
||||||
|
let item1 = Item {
|
||||||
|
id: "item-123".to_string(),
|
||||||
|
name: "First Item".to_string(),
|
||||||
|
price: 100,
|
||||||
|
sell_trend: 0,
|
||||||
|
in_stock: true,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let item_id = item1.id.clone();
|
||||||
|
let _ = catalog.upsert(item1);
|
||||||
|
let item2 = Item {
|
||||||
|
id: "item-123".to_string(),
|
||||||
|
name: "Second Item".to_string(),
|
||||||
|
price: 200,
|
||||||
|
sell_trend: 10,
|
||||||
|
in_stock: false,
|
||||||
|
..Item::default()
|
||||||
|
};
|
||||||
|
let _ = catalog.upsert(item2);
|
||||||
|
assert_eq!(catalog.items.len(), 1);
|
||||||
|
let entity = catalog.items.get(&item_id).unwrap();
|
||||||
|
assert_eq!(entity.value_of("name"), Some(&Value::from("Second Item")));
|
||||||
|
assert_eq!(entity.value_of("price"), Some(&Value::from(200u64)));
|
||||||
|
assert_eq!(entity.value_of("sell_trend"), Some(&Value::from(10i64)));
|
||||||
|
assert_eq!(entity.value_of("in_stock"), Some(&Value::from(false)));
|
||||||
|
assert_eq!(entity.state, EntityState::Updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,13 @@
|
|||||||
|
pub mod catalog_delete;
|
||||||
|
pub mod catalog_get;
|
||||||
|
pub mod catalog_init;
|
||||||
|
pub mod catalog_insert_many;
|
||||||
|
pub mod catalog_integration;
|
||||||
|
pub mod catalog_list_by_class;
|
||||||
|
pub mod catalog_list_by_class_and_subclass;
|
||||||
|
pub mod catalog_load_by_class;
|
||||||
|
pub mod catalog_load_by_filter;
|
||||||
|
pub mod catalog_load_by_id;
|
||||||
|
pub mod catalog_new;
|
||||||
|
pub mod catalog_persist;
|
||||||
|
pub mod catalog_upsert;
|
||||||
|
|||||||
Reference in New Issue
Block a user