chore: extract tests from sqlite_* files into their own module
This commit is contained in:
@@ -30,57 +30,3 @@ pub fn run(path: &path::Path) -> result::Result<(), FailedTo> {
|
|||||||
.map_err(|_| sqlite::FailedTo::ExecuteBatch)?;
|
.map_err(|_| sqlite::FailedTo::ExecuteBatch)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use std::fs;
|
|
||||||
use std::path::Path;
|
|
||||||
#[test]
|
|
||||||
fn init_db_should_create_tables_and_indexes_on_new_db() {
|
|
||||||
// This test verifies that a new database is correctly initialized with the necessary tables ('entity', 'attribute') and indexes.
|
|
||||||
let db_path = Path::new("test_new.db");
|
|
||||||
let _ = fs::remove_file(db_path); // Ensure the file doesn't exist
|
|
||||||
assert!(run(db_path).is_ok());
|
|
||||||
let conn = Connection::open(db_path).unwrap();
|
|
||||||
let mut stmt = conn
|
|
||||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('entity', 'attribute')")
|
|
||||||
.unwrap();
|
|
||||||
let tables: Vec<String> = stmt
|
|
||||||
.query_map([], |row| row.get(0))
|
|
||||||
.unwrap()
|
|
||||||
.map(|r| r.unwrap())
|
|
||||||
.collect();
|
|
||||||
assert_eq!(tables.len(), 2);
|
|
||||||
assert!(tables.contains(&"entity".to_string()));
|
|
||||||
assert!(tables.contains(&"attribute".to_string()));
|
|
||||||
let mut stmt = conn
|
|
||||||
.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name IN ('entity_class', 'attribute_id')")
|
|
||||||
.unwrap();
|
|
||||||
let indexes: Vec<String> = stmt
|
|
||||||
.query_map([], |row| row.get(0))
|
|
||||||
.unwrap()
|
|
||||||
.map(|r| r.unwrap())
|
|
||||||
.collect();
|
|
||||||
assert_eq!(indexes.len(), 2);
|
|
||||||
assert!(indexes.contains(&"entity_class".to_string()));
|
|
||||||
assert!(indexes.contains(&"attribute_id".to_string()));
|
|
||||||
let _ = fs::remove_file(db_path);
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn init_db_should_be_idempotent_and_not_fail_on_existing_db() {
|
|
||||||
// This test ensures that initializing an already existing and initialized database does not cause errors.
|
|
||||||
let db_path = Path::new("test_idempotent.db");
|
|
||||||
let _ = fs::remove_file(db_path);
|
|
||||||
assert!(run(db_path).is_ok());
|
|
||||||
assert!(run(db_path).is_ok());
|
|
||||||
let _ = fs::remove_file(db_path);
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn init_db_should_fail_gracefully_on_invalid_path() {
|
|
||||||
// This test checks that the function returns an error when provided with an invalid or inaccessible file path.
|
|
||||||
// An invalid path, like a directory, should cause an error
|
|
||||||
let invalid_path = Path::new("/");
|
|
||||||
assert!(run(invalid_path).is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -19,98 +19,3 @@ pub fn run(transaction: &Transaction, entity: &mut Entity) -> Result<(), FailedT
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::{Entity, Value, fun};
|
|
||||||
use std::{collections::HashMap, fs, path::Path};
|
|
||||||
fn setup_db(db_path: &Path) -> Connection {
|
|
||||||
let _ = fs::remove_file(db_path);
|
|
||||||
fun::sqlite_init_db::run(db_path).unwrap();
|
|
||||||
Connection::open(db_path).unwrap()
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn load_attributes_should_populate_entity_from_db() {
|
|
||||||
// Verifies that attributes for a given entity are correctly loaded from the database and added to the entity's attributes map.
|
|
||||||
let db_path = Path::new("test_load_attributes.db");
|
|
||||||
let mut conn = setup_db(db_path);
|
|
||||||
let mut entity = Entity {
|
|
||||||
id: "entity1".to_string(),
|
|
||||||
class: "class1".to_string(),
|
|
||||||
subclass: None,
|
|
||||||
attributes: HashMap::new(),
|
|
||||||
state: Default::default(),
|
|
||||||
ref_date: None,
|
|
||||||
};
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO entity (id, class) VALUES (?1, ?2)",
|
|
||||||
params![&entity.id, &entity.class],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO attribute (id, entity_id, value_text) VALUES ('attr1', 'entity1', 'hello')",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO attribute (id, entity_id, value_int) VALUES ('attr2', 'entity1', 42)",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let tx = conn.transaction().unwrap();
|
|
||||||
assert!(run(&tx, &mut entity).is_ok());
|
|
||||||
tx.commit().unwrap();
|
|
||||||
assert_eq!(entity.attributes.len(), 2);
|
|
||||||
let attr1 = entity.attributes.get("attr1").unwrap();
|
|
||||||
assert_eq!(attr1.id, "attr1");
|
|
||||||
assert_eq!(attr1.value, Value::Text("hello".to_string()));
|
|
||||||
let attr2 = entity.attributes.get("attr2").unwrap();
|
|
||||||
assert_eq!(attr2.id, "attr2");
|
|
||||||
assert_eq!(attr2.value, Value::SignedInt(42));
|
|
||||||
fs::remove_file(db_path).unwrap();
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn load_attributes_should_handle_entities_with_no_attributes() {
|
|
||||||
// Ensures that the function completes without error and without adding attributes for an entity that has none in the database.
|
|
||||||
let db_path = Path::new("test_no_attributes.db");
|
|
||||||
let mut conn = setup_db(db_path);
|
|
||||||
let mut entity = Entity {
|
|
||||||
id: "entity2".to_string(),
|
|
||||||
class: "class1".to_string(),
|
|
||||||
subclass: None,
|
|
||||||
attributes: HashMap::new(),
|
|
||||||
state: Default::default(),
|
|
||||||
ref_date: None,
|
|
||||||
};
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO entity (id, class) VALUES (?1, ?2)",
|
|
||||||
params![&entity.id, &entity.class],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let tx = conn.transaction().unwrap();
|
|
||||||
assert!(run(&tx, &mut entity).is_ok());
|
|
||||||
tx.commit().unwrap();
|
|
||||||
assert!(entity.attributes.is_empty());
|
|
||||||
fs::remove_file(db_path).unwrap();
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn load_attributes_should_return_error_on_query_failure() {
|
|
||||||
// Checks that an error is returned if the database query to select attributes fails.
|
|
||||||
let db_path = Path::new("test_query_failure.db");
|
|
||||||
let mut conn = setup_db(db_path);
|
|
||||||
conn.execute("DROP TABLE attribute", []).unwrap();
|
|
||||||
let mut entity = Entity {
|
|
||||||
id: "entity3".to_string(),
|
|
||||||
class: "class1".to_string(),
|
|
||||||
subclass: None,
|
|
||||||
attributes: HashMap::new(),
|
|
||||||
state: Default::default(),
|
|
||||||
ref_date: None,
|
|
||||||
};
|
|
||||||
let tx = conn.transaction().unwrap();
|
|
||||||
let result = run(&tx, &mut entity);
|
|
||||||
assert!(result.is_err());
|
|
||||||
fs::remove_file(db_path).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -27,81 +27,3 @@ pub fn run(path: &path::Path, entity_class: &str) -> Result<Vec<Entity>, FailedT
|
|||||||
}
|
}
|
||||||
Ok(entities)
|
Ok(entities)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::{fun, str::value::Value};
|
|
||||||
use std::{fs, path::Path};
|
|
||||||
fn setup_db(db_path: &Path) {
|
|
||||||
let _ = fs::remove_file(db_path);
|
|
||||||
fun::sqlite_init_db::run(db_path).unwrap();
|
|
||||||
let conn = Connection::open(db_path).unwrap();
|
|
||||||
// Class 'c1'
|
|
||||||
conn.execute("INSERT INTO entity (id, class) VALUES ('e1_c1', 'c1')", [])
|
|
||||||
.unwrap();
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO attribute (id, entity_id, value_text) VALUES ('a1', 'e1_c1', 'v1')",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO attribute (id, entity_id, value_int) VALUES ('a2', 'e1_c1', 1)",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
conn.execute("INSERT INTO entity (id, class) VALUES ('e2_c1', 'c1')", [])
|
|
||||||
.unwrap();
|
|
||||||
// Class 'c2'
|
|
||||||
conn.execute("INSERT INTO entity (id, class) VALUES ('e1_c2', 'c2')", [])
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn load_by_class_should_fetch_all_entities_for_a_given_class() {
|
|
||||||
// Verifies that all entities belonging to a specific class are retrieved from the database.
|
|
||||||
let db_path = Path::new("test_fetch_by_class.db");
|
|
||||||
setup_db(db_path);
|
|
||||||
let result = run(db_path, "c1");
|
|
||||||
assert!(result.is_ok());
|
|
||||||
let entities = result.unwrap();
|
|
||||||
assert_eq!(entities.len(), 2);
|
|
||||||
let ids: Vec<_> = entities.iter().map(|e| e.id.clone()).collect();
|
|
||||||
assert!(ids.contains(&"e1_c1".to_string()));
|
|
||||||
assert!(ids.contains(&"e2_c1".to_string()));
|
|
||||||
fs::remove_file(db_path).unwrap();
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn load_by_class_should_return_empty_vec_for_non_existent_class() {
|
|
||||||
// Ensures that an empty vector is returned when querying for a class that has no entities in the database.
|
|
||||||
let db_path = Path::new("test_non_existent_class.db");
|
|
||||||
setup_db(db_path);
|
|
||||||
let result = run(db_path, "non_existent");
|
|
||||||
assert!(result.is_ok());
|
|
||||||
let entities = result.unwrap();
|
|
||||||
assert!(entities.is_empty());
|
|
||||||
fs::remove_file(db_path).unwrap();
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn load_by_class_should_fully_load_entities_with_attributes() {
|
|
||||||
// Checks that the retrieved entities are complete, including all their associated attributes.
|
|
||||||
let db_path = Path::new("test_fully_load.db");
|
|
||||||
setup_db(db_path);
|
|
||||||
let entities = run(db_path, "c1").unwrap();
|
|
||||||
let entity1 = entities.iter().find(|e| e.id == "e1_c1").unwrap();
|
|
||||||
assert_eq!(entity1.attributes.len(), 2);
|
|
||||||
assert!(entity1.attributes.contains_key("a1"));
|
|
||||||
assert!(entity1.attributes.contains_key("a2"));
|
|
||||||
let attr1 = entity1.attributes.get("a1").unwrap();
|
|
||||||
assert_eq!(attr1.value, Value::Text("v1".to_string()));
|
|
||||||
let attr2 = entity1.attributes.get("a2").unwrap();
|
|
||||||
assert_eq!(attr2.value, Value::SignedInt(1));
|
|
||||||
fs::remove_file(db_path).unwrap();
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn load_by_class_should_fail_gracefully_on_db_connection_error() {
|
|
||||||
// Tests that the function returns an appropriate error if the database connection cannot be established.
|
|
||||||
let invalid_path = Path::new("/"); // A directory is not a valid database file
|
|
||||||
let result = run(invalid_path, "any_class");
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -22,85 +22,3 @@ pub fn run(path: &path::Path, entity_id: &str) -> Result<Option<Entity>, FailedT
|
|||||||
}
|
}
|
||||||
Ok(entity)
|
Ok(entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::{fun, str::value::Value};
|
|
||||||
use std::{fs, path::Path};
|
|
||||||
fn setup_db(db_path: &Path) {
|
|
||||||
let _ = fs::remove_file(db_path);
|
|
||||||
fun::sqlite_init_db::run(db_path).unwrap();
|
|
||||||
let conn = Connection::open(db_path).unwrap();
|
|
||||||
conn.execute("INSERT INTO entity (id, class) VALUES ('e1', 'c1')", [])
|
|
||||||
.unwrap();
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO attribute (id, entity_id, value_text) VALUES ('a1', 'e1', 'v1')",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO attribute (id, entity_id, value_int) VALUES ('a2', 'e1', 100)",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
conn.execute("INSERT INTO entity (id, class) VALUES ('e2', 'c2')", [])
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn load_by_id_should_fetch_correct_entity() {
|
|
||||||
// Verifies that the correct entity is retrieved from the database when a valid ID is provided.
|
|
||||||
let db_path = Path::new("test_fetch_by_id.db");
|
|
||||||
setup_db(db_path);
|
|
||||||
let result = run(db_path, "e1");
|
|
||||||
assert!(result.is_ok());
|
|
||||||
let entity_opt = result.unwrap();
|
|
||||||
assert!(entity_opt.is_some());
|
|
||||||
let entity = entity_opt.unwrap();
|
|
||||||
assert_eq!(entity.id, "e1");
|
|
||||||
assert_eq!(entity.class, "c1");
|
|
||||||
fs::remove_file(db_path).unwrap();
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn load_by_id_should_return_none_for_non_existent_id() {
|
|
||||||
// Ensures that `Ok(None)` is returned when querying for an ID that does not exist in the database.
|
|
||||||
let db_path = Path::new("test_non_existent_id.db");
|
|
||||||
setup_db(db_path);
|
|
||||||
let result = run(db_path, "non_existent");
|
|
||||||
assert!(result.is_ok());
|
|
||||||
let entity_opt = result.unwrap();
|
|
||||||
assert!(entity_opt.is_none());
|
|
||||||
fs::remove_file(db_path).unwrap();
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn load_by_id_should_load_entity_with_all_attributes() {
|
|
||||||
// Checks that the retrieved entity includes all of its associated attributes.
|
|
||||||
let db_path = Path::new("test_load_with_attributes.db");
|
|
||||||
setup_db(db_path);
|
|
||||||
let entity = run(db_path, "e1").unwrap().unwrap();
|
|
||||||
assert_eq!(entity.attributes.len(), 2);
|
|
||||||
let attr1 = entity.attributes.get("a1").unwrap();
|
|
||||||
assert_eq!(attr1.value, Value::Text("v1".to_string()));
|
|
||||||
let attr2 = entity.attributes.get("a2").unwrap();
|
|
||||||
assert_eq!(attr2.value, Value::SignedInt(100));
|
|
||||||
fs::remove_file(db_path).unwrap();
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn load_by_id_should_fail_gracefully_on_db_error() {
|
|
||||||
// Tests that an error is returned if the database query fails for any reason.
|
|
||||||
// Test case 1: Invalid path
|
|
||||||
let invalid_path = Path::new("/");
|
|
||||||
let result = run(invalid_path, "any_id");
|
|
||||||
assert!(result.is_err());
|
|
||||||
// Test case 2: Query error
|
|
||||||
let db_path = Path::new("test_db_error.db");
|
|
||||||
let _ = fs::remove_file(db_path);
|
|
||||||
fun::sqlite_init_db::run(db_path).unwrap();
|
|
||||||
let conn = Connection::open(db_path).unwrap();
|
|
||||||
conn.execute("DROP TABLE entity", []).unwrap();
|
|
||||||
drop(conn); // Close connection before `run` tries to open it.
|
|
||||||
let result = run(db_path, "any_id");
|
|
||||||
assert!(result.is_err());
|
|
||||||
fs::remove_file(db_path).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,78 +20,3 @@ pub fn run(row: &rusqlite::Row) -> rusqlite::Result<Attribute> {
|
|||||||
};
|
};
|
||||||
Ok(Attribute { id, value })
|
Ok(Attribute { id, value })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use rusqlite::{Connection, Result};
|
|
||||||
fn get_row_from_query(conn: &Connection, query: &str) -> Result<Attribute, rusqlite::Error> {
|
|
||||||
let mut stmt = conn.prepare(query).unwrap();
|
|
||||||
stmt.query_row([], run)
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn map_row_to_attribute_with_signed_int_value() {
|
|
||||||
// Verifies that a database row with a signed integer value is correctly mapped to an Attribute with a Value::SignedInt.
|
|
||||||
let conn = Connection::open_in_memory().unwrap();
|
|
||||||
let attr =
|
|
||||||
get_row_from_query(&conn, "SELECT 'a1', 'e1', -42, NULL, NULL, NULL, NULL").unwrap();
|
|
||||||
assert_eq!(attr.id, "a1");
|
|
||||||
assert_eq!(attr.value, Value::SignedInt(-42));
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn map_row_to_attribute_with_unsigned_int_value() {
|
|
||||||
// Verifies that a database row with an unsigned integer value is correctly mapped to an Attribute with a Value::UnsignedInt.
|
|
||||||
let conn = Connection::open_in_memory().unwrap();
|
|
||||||
let attr =
|
|
||||||
get_row_from_query(&conn, "SELECT 'a2', 'e1', NULL, 42, NULL, NULL, NULL").unwrap();
|
|
||||||
assert_eq!(attr.id, "a2");
|
|
||||||
assert_eq!(attr.value, Value::UnsignedInt(42));
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn map_row_to_attribute_with_real_value() {
|
|
||||||
// Verifies that a database row with a real (float) value is correctly mapped to an Attribute with a Value::Real.
|
|
||||||
let conn = Connection::open_in_memory().unwrap();
|
|
||||||
let attr = get_row_from_query(&conn, "SELECT 'a3', 'e1', NULL, NULL, 12.2345, NULL, NULL")
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(attr.id, "a3");
|
|
||||||
assert_eq!(attr.value, Value::Real(12.2345));
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn map_row_to_attribute_with_text_value() {
|
|
||||||
// Verifies that a database row with a text value is correctly mapped to an Attribute with a Value::Text.
|
|
||||||
let conn = Connection::open_in_memory().unwrap();
|
|
||||||
let attr = get_row_from_query(&conn, "SELECT 'a4', 'e1', NULL, NULL, NULL, 'hello', NULL")
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(attr.id, "a4");
|
|
||||||
assert_eq!(attr.value, Value::Text("hello".to_string()));
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn map_row_to_attribute_with_bool_value() {
|
|
||||||
// Verifies that a database row with a boolean value is correctly mapped to an Attribute with a Value::Bool.
|
|
||||||
let conn = Connection::open_in_memory().unwrap();
|
|
||||||
let attr =
|
|
||||||
get_row_from_query(&conn, "SELECT 'a5', 'e1', NULL, NULL, NULL, NULL, 1").unwrap();
|
|
||||||
assert_eq!(attr.id, "a5");
|
|
||||||
assert_eq!(attr.value, Value::Bool(true));
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn map_row_to_attribute_should_panic_on_multiple_values() {
|
|
||||||
// Ensures that the function panics if a row contains data in more than one 'value' column, which indicates data corruption.
|
|
||||||
let conn = Connection::open_in_memory().unwrap();
|
|
||||||
// This should panic because both signed_int and text have values.
|
|
||||||
get_row_from_query(&conn, "SELECT 'a6', 'e1', -42, NULL, NULL, 'hello', NULL").unwrap();
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn map_row_to_attribute_should_return_error_on_type_mismatch() {
|
|
||||||
// Checks that a rusqlite::Error is returned if a column's type does not match the expected schema (e.g., text in an integer column).
|
|
||||||
let conn = Connection::open_in_memory().unwrap();
|
|
||||||
// Trying to get a text value as an integer should fail.
|
|
||||||
let result = get_row_from_query(
|
|
||||||
&conn,
|
|
||||||
// The value 'not a number' is in the signed_int column position.
|
|
||||||
"SELECT 'a7', 'e1', 'not a number', NULL, NULL, NULL, NULL",
|
|
||||||
);
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,53 +15,3 @@ pub fn run(row: &rusqlite::Row) -> rusqlite::Result<Entity> {
|
|||||||
};
|
};
|
||||||
Ok(entity)
|
Ok(entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use rusqlite::{Connection, Result};
|
|
||||||
fn get_entity_from_query(conn: &Connection, query: &str) -> Result<Entity, rusqlite::Error> {
|
|
||||||
let mut stmt = conn.prepare(query).unwrap();
|
|
||||||
stmt.query_row([], run)
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn map_row_to_entity_should_correctly_map_valid_row() {
|
|
||||||
// Verifies that a valid database row is correctly mapped to an Entity struct with all fields populated.
|
|
||||||
let conn = Connection::open_in_memory().unwrap();
|
|
||||||
let entity = get_entity_from_query(&conn, "SELECT 'e1', 'c1', 's1', 12345").unwrap();
|
|
||||||
assert_eq!(entity.id, "e1");
|
|
||||||
assert_eq!(entity.class, "c1");
|
|
||||||
assert_eq!(entity.subclass, Some("s1".to_string()));
|
|
||||||
assert_eq!(entity.ref_date, Some(12345));
|
|
||||||
assert_eq!(entity.state, EntityState::Loaded);
|
|
||||||
assert!(entity.attributes.is_empty());
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn map_row_to_entity_should_handle_null_ref_date() {
|
|
||||||
// Ensures that a row with a NULL 'ref_date' is successfully mapped to an Entity with 'ref_date' as None.
|
|
||||||
let conn = Connection::open_in_memory().unwrap();
|
|
||||||
let entity = get_entity_from_query(&conn, "SELECT 'e2', 'c2', 's2', NULL").unwrap();
|
|
||||||
assert_eq!(entity.id, "e2");
|
|
||||||
assert_eq!(entity.class, "c2");
|
|
||||||
assert_eq!(entity.subclass, Some("s2".to_string()));
|
|
||||||
assert_eq!(entity.ref_date, None);
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn map_row_to_entity_should_handle_null_subclass() {
|
|
||||||
// Ensures that a row with a NULL 'subclass' is successfully mapped to an Entity with 'subclass' as None.
|
|
||||||
let conn = Connection::open_in_memory().unwrap();
|
|
||||||
let entity = get_entity_from_query(&conn, "SELECT 'e2', 'c2', NULL, NULL").unwrap();
|
|
||||||
assert_eq!(entity.id, "e2");
|
|
||||||
assert_eq!(entity.class, "c2");
|
|
||||||
assert_eq!(entity.subclass, None);
|
|
||||||
assert_eq!(entity.ref_date, None);
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn map_row_to_entity_should_return_error_on_type_mismatch() {
|
|
||||||
// Checks that a rusqlite::Error is returned if a column's type does not match the expected schema.
|
|
||||||
let conn = Connection::open_in_memory().unwrap();
|
|
||||||
// Trying to get an integer as a string for the class should fail.
|
|
||||||
let result = get_entity_from_query(&conn, "SELECT 'e3', 123, NULL");
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -85,163 +85,3 @@ pub fn run(path: &path::Path, catalog: &Catalog) -> result::Result<(), FailedTo>
|
|||||||
.map_err(|_| sqlite::FailedTo::CommitTransaction)?;
|
.map_err(|_| sqlite::FailedTo::CommitTransaction)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use std::{collections::HashMap, fs, path::Path};
|
|
||||||
fn setup_db(db_path: &Path) -> Connection {
|
|
||||||
let _ = fs::remove_file(db_path);
|
|
||||||
fun::sqlite_init_db::run(db_path).unwrap();
|
|
||||||
Connection::open(db_path).unwrap()
|
|
||||||
}
|
|
||||||
fn count_rows(conn: &Connection, table: &str, where_clause: &str) -> i64 {
|
|
||||||
let mut stmt = conn
|
|
||||||
.prepare(&format!(
|
|
||||||
"SELECT COUNT(*) FROM {} WHERE {}",
|
|
||||||
table, where_clause
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
stmt.query_row([], |row| row.get(0)).unwrap()
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn persist_should_insert_new_entities_and_attributes() {
|
|
||||||
// Verifies that entities marked as 'New' in the catalog are inserted into the database, along with all their attributes.
|
|
||||||
let db_path = Path::new("test_insert.db");
|
|
||||||
let conn = setup_db(db_path);
|
|
||||||
let mut catalog = Catalog::new(db_path.to_str().unwrap());
|
|
||||||
let mut entity = Entity {
|
|
||||||
id: "e1".to_string(),
|
|
||||||
class: "c1".to_string(),
|
|
||||||
subclass: None,
|
|
||||||
attributes: HashMap::new(),
|
|
||||||
state: EntityState::New,
|
|
||||||
ref_date: None,
|
|
||||||
};
|
|
||||||
entity.attributes.insert(
|
|
||||||
"a1".to_string(),
|
|
||||||
Attribute {
|
|
||||||
id: "a1".to_string(),
|
|
||||||
value: Value::Text("v1".to_string()),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
catalog.items.insert("e1".to_string(), entity);
|
|
||||||
assert!(run(db_path, &catalog).is_ok());
|
|
||||||
assert_eq!(count_rows(&conn, "entity", "id = 'e1'"), 1);
|
|
||||||
assert_eq!(count_rows(&conn, "attribute", "entity_id = 'e1'"), 1);
|
|
||||||
fs::remove_file(db_path).unwrap();
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn persist_should_delete_entities_marked_for_deletion() {
|
|
||||||
// Ensures that entities marked as 'ToDelete' are correctly removed from the database.
|
|
||||||
let db_path = Path::new("test_delete.db");
|
|
||||||
let conn = setup_db(db_path);
|
|
||||||
conn.execute("INSERT INTO entity (id, class) VALUES ('e1', 'c1')", [])
|
|
||||||
.unwrap();
|
|
||||||
let mut catalog = Catalog::new(db_path.to_str().unwrap());
|
|
||||||
let entity = Entity {
|
|
||||||
id: "e1".to_string(),
|
|
||||||
class: "c1".to_string(),
|
|
||||||
subclass: None,
|
|
||||||
attributes: HashMap::new(),
|
|
||||||
state: EntityState::ToDelete,
|
|
||||||
ref_date: None,
|
|
||||||
};
|
|
||||||
catalog.items.insert("e1".to_string(), entity);
|
|
||||||
assert!(run(db_path, &catalog).is_ok());
|
|
||||||
assert_eq!(count_rows(&conn, "entity", "id = 'e1'"), 0);
|
|
||||||
fs::remove_file(db_path).unwrap();
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn persist_should_handle_a_mix_of_new_and_deleted_entities() {
|
|
||||||
// Tests the function's ability to handle a batch operation involving both new entities to be inserted and existing ones to be deleted.
|
|
||||||
let db_path = Path::new("test_mix.db");
|
|
||||||
let conn = setup_db(db_path);
|
|
||||||
conn.execute("INSERT INTO entity (id, class) VALUES ('e1', 'c1')", [])
|
|
||||||
.unwrap();
|
|
||||||
let mut catalog = Catalog::new(db_path.to_str().unwrap());
|
|
||||||
let to_delete = Entity {
|
|
||||||
id: "e1".to_string(),
|
|
||||||
class: "c1".to_string(),
|
|
||||||
subclass: None,
|
|
||||||
attributes: HashMap::new(),
|
|
||||||
state: EntityState::ToDelete,
|
|
||||||
ref_date: None,
|
|
||||||
};
|
|
||||||
let to_add = Entity {
|
|
||||||
id: "e2".to_string(),
|
|
||||||
class: "c2".to_string(),
|
|
||||||
subclass: None,
|
|
||||||
attributes: HashMap::new(),
|
|
||||||
state: EntityState::New,
|
|
||||||
ref_date: None,
|
|
||||||
};
|
|
||||||
catalog.items.insert("e1".to_string(), to_delete);
|
|
||||||
catalog.items.insert("e2".to_string(), to_add);
|
|
||||||
assert!(run(db_path, &catalog).is_ok());
|
|
||||||
assert_eq!(count_rows(&conn, "entity", "id = 'e1'"), 0);
|
|
||||||
assert_eq!(count_rows(&conn, "entity", "id = 'e2'"), 1);
|
|
||||||
fs::remove_file(db_path).unwrap();
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn persist_should_not_affect_unmodified_entities() {
|
|
||||||
// Verifies that entities in the catalog that are not marked as 'New' or 'ToDelete' remain untouched in the database.
|
|
||||||
let db_path = Path::new("test_unmodified.db");
|
|
||||||
let conn = setup_db(db_path);
|
|
||||||
conn.execute("INSERT INTO entity (id, class) VALUES ('e1', 'c1')", [])
|
|
||||||
.unwrap();
|
|
||||||
let mut catalog = Catalog::new(db_path.to_str().unwrap());
|
|
||||||
let unmodified = Entity {
|
|
||||||
id: "e1".to_string(),
|
|
||||||
class: "c1".to_string(),
|
|
||||||
subclass: None,
|
|
||||||
attributes: HashMap::new(),
|
|
||||||
state: EntityState::Loaded,
|
|
||||||
ref_date: None,
|
|
||||||
};
|
|
||||||
catalog.items.insert("e1".to_string(), unmodified);
|
|
||||||
assert!(run(db_path, &catalog).is_ok());
|
|
||||||
assert_eq!(count_rows(&conn, "entity", "id = 'e1'"), 1);
|
|
||||||
fs::remove_file(db_path).unwrap();
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn persist_should_rollback_transaction_on_failure() {
|
|
||||||
// Ensures that if any part of the persistence process fails, the entire transaction is rolled back, leaving the database state unchanged.
|
|
||||||
let db_path = Path::new("test_rollback.db");
|
|
||||||
let conn = setup_db(db_path);
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO entity (id, class) VALUES ('e_existing', 'c1')",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let mut catalog = Catalog::new(db_path.to_str().unwrap());
|
|
||||||
let mut new_entity = Entity {
|
|
||||||
id: "e_new".to_string(),
|
|
||||||
class: "c1".to_string(),
|
|
||||||
subclass: None,
|
|
||||||
attributes: HashMap::new(),
|
|
||||||
state: EntityState::New,
|
|
||||||
ref_date: None,
|
|
||||||
};
|
|
||||||
new_entity.attributes.insert(
|
|
||||||
"a1".to_string(),
|
|
||||||
Attribute {
|
|
||||||
id: "a1".to_string(),
|
|
||||||
value: Value::Text("v1".to_string()),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
catalog.items.insert("e_new".to_string(), new_entity);
|
|
||||||
// Corrupt the DB to cause a failure during the transaction
|
|
||||||
conn.execute("DROP TABLE attribute", []).unwrap();
|
|
||||||
drop(conn);
|
|
||||||
let result = run(db_path, &catalog);
|
|
||||||
assert!(result.is_err());
|
|
||||||
// Re-open connection to check state
|
|
||||||
let conn = Connection::open(db_path).unwrap();
|
|
||||||
// The new entity should not have been inserted due to rollback
|
|
||||||
assert_eq!(count_rows(&conn, "entity", "id = 'e_new'"), 0);
|
|
||||||
// The existing entity should still be there
|
|
||||||
assert_eq!(count_rows(&conn, "entity", "id = 'e_existing'"), 1);
|
|
||||||
fs::remove_file(db_path).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,3 +11,10 @@ pub mod catalog_load_by_id;
|
|||||||
pub mod catalog_new;
|
pub mod catalog_new;
|
||||||
pub mod catalog_persist;
|
pub mod catalog_persist;
|
||||||
pub mod catalog_upsert;
|
pub mod catalog_upsert;
|
||||||
|
pub mod sqlite_init_db;
|
||||||
|
pub mod sqlite_load_attributes;
|
||||||
|
pub mod sqlite_load_by_class;
|
||||||
|
pub mod sqlite_load_by_id;
|
||||||
|
pub mod sqlite_map_row_to_attribute;
|
||||||
|
pub mod sqlite_map_row_to_entity;
|
||||||
|
pub mod sqlite_persist_catalog;
|
||||||
|
|||||||
54
01.workspace/heave/src/tst/sqlite_init_db.rs
Normal file
54
01.workspace/heave/src/tst/sqlite_init_db.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::*;
|
||||||
|
use rusqlite::*;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
#[test]
|
||||||
|
fn init_db_should_create_tables_and_indexes_on_new_db() {
|
||||||
|
// This test verifies that a new database is correctly initialized with the necessary tables ('entity', 'attribute') and indexes.
|
||||||
|
let db_path = Path::new("test_new.db");
|
||||||
|
let _ = fs::remove_file(db_path); // Ensure the file doesn't exist
|
||||||
|
assert!(sqlite::init::db(db_path).is_ok());
|
||||||
|
let conn = Connection::open(db_path).unwrap();
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('entity', 'attribute')")
|
||||||
|
.unwrap();
|
||||||
|
let tables: Vec<String> = stmt
|
||||||
|
.query_map([], |row| row.get(0))
|
||||||
|
.unwrap()
|
||||||
|
.map(|r| r.unwrap())
|
||||||
|
.collect();
|
||||||
|
assert_eq!(tables.len(), 2);
|
||||||
|
assert!(tables.contains(&"entity".to_string()));
|
||||||
|
assert!(tables.contains(&"attribute".to_string()));
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name IN ('entity_class', 'attribute_id')")
|
||||||
|
.unwrap();
|
||||||
|
let indexes: Vec<String> = stmt
|
||||||
|
.query_map([], |row| row.get(0))
|
||||||
|
.unwrap()
|
||||||
|
.map(|r| r.unwrap())
|
||||||
|
.collect();
|
||||||
|
assert_eq!(indexes.len(), 2);
|
||||||
|
assert!(indexes.contains(&"entity_class".to_string()));
|
||||||
|
assert!(indexes.contains(&"attribute_id".to_string()));
|
||||||
|
let _ = fs::remove_file(db_path);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn init_db_should_be_idempotent_and_not_fail_on_existing_db() {
|
||||||
|
// This test ensures that initializing an already existing and initialized database does not cause errors.
|
||||||
|
let db_path = Path::new("test_idempotent.db");
|
||||||
|
let _ = fs::remove_file(db_path);
|
||||||
|
assert!(sqlite::init::db(db_path).is_ok());
|
||||||
|
assert!(sqlite::init::db(db_path).is_ok());
|
||||||
|
let _ = fs::remove_file(db_path);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn init_db_should_fail_gracefully_on_invalid_path() {
|
||||||
|
// This test checks that the function returns an error when provided with an invalid or inaccessible file path.
|
||||||
|
// An invalid path, like a directory, should cause an error
|
||||||
|
let invalid_path = Path::new("/");
|
||||||
|
assert!(sqlite::init::db(invalid_path).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
94
01.workspace/heave/src/tst/sqlite_load_attributes.rs
Normal file
94
01.workspace/heave/src/tst/sqlite_load_attributes.rs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::*;
|
||||||
|
use rusqlite::*;
|
||||||
|
use std::{collections::HashMap, fs, path::Path};
|
||||||
|
fn setup_db(db_path: &Path) -> Connection {
|
||||||
|
let _ = fs::remove_file(db_path);
|
||||||
|
sqlite::init::db(db_path).unwrap();
|
||||||
|
Connection::open(db_path).unwrap()
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn load_attributes_should_populate_entity_from_db() {
|
||||||
|
// Verifies that attributes for a given entity are correctly loaded from the database and added to the entity's attributes map.
|
||||||
|
let db_path = Path::new("test_load_attributes.db");
|
||||||
|
let mut conn = setup_db(db_path);
|
||||||
|
let mut entity = Entity {
|
||||||
|
id: "entity1".to_string(),
|
||||||
|
class: "class1".to_string(),
|
||||||
|
subclass: None,
|
||||||
|
attributes: HashMap::new(),
|
||||||
|
state: Default::default(),
|
||||||
|
ref_date: None,
|
||||||
|
};
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO entity (id, class) VALUES (?1, ?2)",
|
||||||
|
params![&entity.id, &entity.class],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO attribute (id, entity_id, value_text) VALUES ('attr1', 'entity1', 'hello')",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO attribute (id, entity_id, value_int) VALUES ('attr2', 'entity1', 42)",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let tx = conn.transaction().unwrap();
|
||||||
|
assert!(sqlite::load::attributes(&tx, &mut entity).is_ok());
|
||||||
|
tx.commit().unwrap();
|
||||||
|
assert_eq!(entity.attributes.len(), 2);
|
||||||
|
let attr1 = entity.attributes.get("attr1").unwrap();
|
||||||
|
assert_eq!(attr1.id, "attr1");
|
||||||
|
assert_eq!(attr1.value, Value::Text("hello".to_string()));
|
||||||
|
let attr2 = entity.attributes.get("attr2").unwrap();
|
||||||
|
assert_eq!(attr2.id, "attr2");
|
||||||
|
assert_eq!(attr2.value, Value::SignedInt(42));
|
||||||
|
fs::remove_file(db_path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn load_attributes_should_handle_entities_with_no_attributes() {
|
||||||
|
// Ensures that the function completes without error and without adding attributes for an entity that has none in the database.
|
||||||
|
let db_path = Path::new("test_no_attributes.db");
|
||||||
|
let mut conn = setup_db(db_path);
|
||||||
|
let mut entity = Entity {
|
||||||
|
id: "entity2".to_string(),
|
||||||
|
class: "class1".to_string(),
|
||||||
|
subclass: None,
|
||||||
|
attributes: HashMap::new(),
|
||||||
|
state: Default::default(),
|
||||||
|
ref_date: None,
|
||||||
|
};
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO entity (id, class) VALUES (?1, ?2)",
|
||||||
|
params![&entity.id, &entity.class],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let tx = conn.transaction().unwrap();
|
||||||
|
assert!(sqlite::load::attributes(&tx, &mut entity).is_ok());
|
||||||
|
tx.commit().unwrap();
|
||||||
|
assert!(entity.attributes.is_empty());
|
||||||
|
fs::remove_file(db_path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn load_attributes_should_return_error_on_query_failure() {
|
||||||
|
// Checks that an error is returned if the database query to select attributes fails.
|
||||||
|
let db_path = Path::new("test_query_failure.db");
|
||||||
|
let mut conn = setup_db(db_path);
|
||||||
|
conn.execute("DROP TABLE attribute", []).unwrap();
|
||||||
|
let mut entity = Entity {
|
||||||
|
id: "entity3".to_string(),
|
||||||
|
class: "class1".to_string(),
|
||||||
|
subclass: None,
|
||||||
|
attributes: HashMap::new(),
|
||||||
|
state: Default::default(),
|
||||||
|
ref_date: None,
|
||||||
|
};
|
||||||
|
let tx = conn.transaction().unwrap();
|
||||||
|
let result = sqlite::load::attributes(&tx, &mut entity);
|
||||||
|
assert!(result.is_err());
|
||||||
|
fs::remove_file(db_path).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
77
01.workspace/heave/src/tst/sqlite_load_by_class.rs
Normal file
77
01.workspace/heave/src/tst/sqlite_load_by_class.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::*;
|
||||||
|
use rusqlite::*;
|
||||||
|
use std::{fs, path::Path};
|
||||||
|
fn setup_db(db_path: &Path) {
|
||||||
|
let _ = fs::remove_file(db_path);
|
||||||
|
sqlite::init::db(db_path).unwrap();
|
||||||
|
let conn = Connection::open(db_path).unwrap();
|
||||||
|
// Class 'c1'
|
||||||
|
conn.execute("INSERT INTO entity (id, class) VALUES ('e1_c1', 'c1')", [])
|
||||||
|
.unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO attribute (id, entity_id, value_text) VALUES ('a1', 'e1_c1', 'v1')",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO attribute (id, entity_id, value_int) VALUES ('a2', 'e1_c1', 1)",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
conn.execute("INSERT INTO entity (id, class) VALUES ('e2_c1', 'c1')", [])
|
||||||
|
.unwrap();
|
||||||
|
// Class 'c2'
|
||||||
|
conn.execute("INSERT INTO entity (id, class) VALUES ('e1_c2', 'c2')", [])
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn load_by_class_should_fetch_all_entities_for_a_given_class() {
|
||||||
|
// Verifies that all entities belonging to a specific class are retrieved from the database.
|
||||||
|
let db_path = Path::new("test_fetch_by_class.db");
|
||||||
|
setup_db(db_path);
|
||||||
|
let result = sqlite::load::by_class(db_path, "c1");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let entities = result.unwrap();
|
||||||
|
assert_eq!(entities.len(), 2);
|
||||||
|
let ids: Vec<_> = entities.iter().map(|e| e.id.clone()).collect();
|
||||||
|
assert!(ids.contains(&"e1_c1".to_string()));
|
||||||
|
assert!(ids.contains(&"e2_c1".to_string()));
|
||||||
|
fs::remove_file(db_path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn load_by_class_should_return_empty_vec_for_non_existent_class() {
|
||||||
|
// Ensures that an empty vector is returned when querying for a class that has no entities in the database.
|
||||||
|
let db_path = Path::new("test_non_existent_class.db");
|
||||||
|
setup_db(db_path);
|
||||||
|
let result = sqlite::load::by_class(db_path, "non_existent");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let entities = result.unwrap();
|
||||||
|
assert!(entities.is_empty());
|
||||||
|
fs::remove_file(db_path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn load_by_class_should_fully_load_entities_with_attributes() {
|
||||||
|
// Checks that the retrieved entities are complete, including all their associated attributes.
|
||||||
|
let db_path = Path::new("test_fully_load.db");
|
||||||
|
setup_db(db_path);
|
||||||
|
let entities = sqlite::load::by_class(db_path, "c1").unwrap();
|
||||||
|
let entity1 = entities.iter().find(|e| e.id == "e1_c1").unwrap();
|
||||||
|
assert_eq!(entity1.attributes.len(), 2);
|
||||||
|
assert!(entity1.attributes.contains_key("a1"));
|
||||||
|
assert!(entity1.attributes.contains_key("a2"));
|
||||||
|
let attr1 = entity1.attributes.get("a1").unwrap();
|
||||||
|
assert_eq!(attr1.value, Value::Text("v1".to_string()));
|
||||||
|
let attr2 = entity1.attributes.get("a2").unwrap();
|
||||||
|
assert_eq!(attr2.value, Value::SignedInt(1));
|
||||||
|
fs::remove_file(db_path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn load_by_class_should_fail_gracefully_on_db_connection_error() {
|
||||||
|
// Tests that the function returns an appropriate error if the database connection cannot be established.
|
||||||
|
let invalid_path = Path::new("/"); // A directory is not a valid database file
|
||||||
|
let result = sqlite::load::by_class(invalid_path, "any_class");
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
81
01.workspace/heave/src/tst/sqlite_load_by_id.rs
Normal file
81
01.workspace/heave/src/tst/sqlite_load_by_id.rs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::*;
|
||||||
|
use rusqlite::*;
|
||||||
|
use std::{fs, path::Path};
|
||||||
|
fn setup_db(db_path: &Path) {
|
||||||
|
let _ = fs::remove_file(db_path);
|
||||||
|
sqlite::init::db(db_path).unwrap();
|
||||||
|
let conn = Connection::open(db_path).unwrap();
|
||||||
|
conn.execute("INSERT INTO entity (id, class) VALUES ('e1', 'c1')", [])
|
||||||
|
.unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO attribute (id, entity_id, value_text) VALUES ('a1', 'e1', 'v1')",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO attribute (id, entity_id, value_int) VALUES ('a2', 'e1', 100)",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
conn.execute("INSERT INTO entity (id, class) VALUES ('e2', 'c2')", [])
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn load_by_id_should_fetch_correct_entity() {
|
||||||
|
// Verifies that the correct entity is retrieved from the database when a valid ID is provided.
|
||||||
|
let db_path = Path::new("test_fetch_by_id.db");
|
||||||
|
setup_db(db_path);
|
||||||
|
let result = sqlite::load::by_id(db_path, "e1");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let entity_opt = result.unwrap();
|
||||||
|
assert!(entity_opt.is_some());
|
||||||
|
let entity = entity_opt.unwrap();
|
||||||
|
assert_eq!(entity.id, "e1");
|
||||||
|
assert_eq!(entity.class, "c1");
|
||||||
|
fs::remove_file(db_path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn load_by_id_should_return_none_for_non_existent_id() {
|
||||||
|
// Ensures that `Ok(None)` is returned when querying for an ID that does not exist in the database.
|
||||||
|
let db_path = Path::new("test_non_existent_id.db");
|
||||||
|
setup_db(db_path);
|
||||||
|
let result = sqlite::load::by_id(db_path, "non_existent");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let entity_opt = result.unwrap();
|
||||||
|
assert!(entity_opt.is_none());
|
||||||
|
fs::remove_file(db_path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn load_by_id_should_load_entity_with_all_attributes() {
|
||||||
|
// Checks that the retrieved entity includes all of its associated attributes.
|
||||||
|
let db_path = Path::new("test_load_with_attributes.db");
|
||||||
|
setup_db(db_path);
|
||||||
|
let entity = sqlite::load::by_id(db_path, "e1").unwrap().unwrap();
|
||||||
|
assert_eq!(entity.attributes.len(), 2);
|
||||||
|
let attr1 = entity.attributes.get("a1").unwrap();
|
||||||
|
assert_eq!(attr1.value, Value::Text("v1".to_string()));
|
||||||
|
let attr2 = entity.attributes.get("a2").unwrap();
|
||||||
|
assert_eq!(attr2.value, Value::SignedInt(100));
|
||||||
|
fs::remove_file(db_path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn load_by_id_should_fail_gracefully_on_db_error() {
|
||||||
|
// Tests that an error is returned if the database query fails for any reason.
|
||||||
|
// Test case 1: Invalid path
|
||||||
|
let invalid_path = Path::new("/");
|
||||||
|
let result = sqlite::load::by_id(invalid_path, "any_id");
|
||||||
|
assert!(result.is_err());
|
||||||
|
// Test case 2: Query error
|
||||||
|
let db_path = Path::new("test_db_error.db");
|
||||||
|
let _ = fs::remove_file(db_path);
|
||||||
|
sqlite::init::db(db_path).unwrap();
|
||||||
|
let conn = Connection::open(db_path).unwrap();
|
||||||
|
conn.execute("DROP TABLE entity", []).unwrap();
|
||||||
|
drop(conn); // Close connection before `run` tries to open it.
|
||||||
|
let result = sqlite::load::by_id(db_path, "any_id");
|
||||||
|
assert!(result.is_err());
|
||||||
|
fs::remove_file(db_path).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
74
01.workspace/heave/src/tst/sqlite_map_row_to_attribute.rs
Normal file
74
01.workspace/heave/src/tst/sqlite_map_row_to_attribute.rs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::*;
|
||||||
|
use rusqlite::*;
|
||||||
|
fn get_row_from_query(conn: &Connection, query: &str) -> Result<Attribute, rusqlite::Error> {
|
||||||
|
let mut stmt = conn.prepare(query).unwrap();
|
||||||
|
stmt.query_row([], sqlite::map::row_to_attribute)
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn map_row_to_attribute_with_signed_int_value() {
|
||||||
|
// Verifies that a database row with a signed integer value is correctly mapped to an Attribute with a Value::SignedInt.
|
||||||
|
let conn = Connection::open_in_memory().unwrap();
|
||||||
|
let attr =
|
||||||
|
get_row_from_query(&conn, "SELECT 'a1', 'e1', -42, NULL, NULL, NULL, NULL").unwrap();
|
||||||
|
assert_eq!(attr.id, "a1");
|
||||||
|
assert_eq!(attr.value, Value::SignedInt(-42));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn map_row_to_attribute_with_unsigned_int_value() {
|
||||||
|
// Verifies that a database row with an unsigned integer value is correctly mapped to an Attribute with a Value::UnsignedInt.
|
||||||
|
let conn = Connection::open_in_memory().unwrap();
|
||||||
|
let attr =
|
||||||
|
get_row_from_query(&conn, "SELECT 'a2', 'e1', NULL, 42, NULL, NULL, NULL").unwrap();
|
||||||
|
assert_eq!(attr.id, "a2");
|
||||||
|
assert_eq!(attr.value, Value::UnsignedInt(42));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn map_row_to_attribute_with_real_value() {
|
||||||
|
// Verifies that a database row with a real (float) value is correctly mapped to an Attribute with a Value::Real.
|
||||||
|
let conn = Connection::open_in_memory().unwrap();
|
||||||
|
let attr = get_row_from_query(&conn, "SELECT 'a3', 'e1', NULL, NULL, 12.2345, NULL, NULL")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(attr.id, "a3");
|
||||||
|
assert_eq!(attr.value, Value::Real(12.2345));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn map_row_to_attribute_with_text_value() {
|
||||||
|
// Verifies that a database row with a text value is correctly mapped to an Attribute with a Value::Text.
|
||||||
|
let conn = Connection::open_in_memory().unwrap();
|
||||||
|
let attr = get_row_from_query(&conn, "SELECT 'a4', 'e1', NULL, NULL, NULL, 'hello', NULL")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(attr.id, "a4");
|
||||||
|
assert_eq!(attr.value, Value::Text("hello".to_string()));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn map_row_to_attribute_with_bool_value() {
|
||||||
|
// Verifies that a database row with a boolean value is correctly mapped to an Attribute with a Value::Bool.
|
||||||
|
let conn = Connection::open_in_memory().unwrap();
|
||||||
|
let attr =
|
||||||
|
get_row_from_query(&conn, "SELECT 'a5', 'e1', NULL, NULL, NULL, NULL, 1").unwrap();
|
||||||
|
assert_eq!(attr.id, "a5");
|
||||||
|
assert_eq!(attr.value, Value::Bool(true));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn map_row_to_attribute_should_panic_on_multiple_values() {
|
||||||
|
// Ensures that the function panics if a row contains data in more than one 'value' column, which indicates data corruption.
|
||||||
|
let conn = Connection::open_in_memory().unwrap();
|
||||||
|
// This should panic because both signed_int and text have values.
|
||||||
|
get_row_from_query(&conn, "SELECT 'a6', 'e1', -42, NULL, NULL, 'hello', NULL").unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn map_row_to_attribute_should_return_error_on_type_mismatch() {
|
||||||
|
// Checks that a rusqlite::Error is returned if a column's type does not match the expected schema (e.g., text in an integer column).
|
||||||
|
let conn = Connection::open_in_memory().unwrap();
|
||||||
|
// Trying to get a text value as an integer should fail.
|
||||||
|
let result = get_row_from_query(
|
||||||
|
&conn,
|
||||||
|
// The value 'not a number' is in the signed_int column position.
|
||||||
|
"SELECT 'a7', 'e1', 'not a number', NULL, NULL, NULL, NULL",
|
||||||
|
);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
49
01.workspace/heave/src/tst/sqlite_map_row_to_entity.rs
Normal file
49
01.workspace/heave/src/tst/sqlite_map_row_to_entity.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::*;
|
||||||
|
use rusqlite::*;
|
||||||
|
fn get_entity_from_query(conn: &Connection, query: &str) -> Result<Entity, rusqlite::Error> {
|
||||||
|
let mut stmt = conn.prepare(query).unwrap();
|
||||||
|
stmt.query_row([], sqlite::map::row_to_entity)
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn map_row_to_entity_should_correctly_map_valid_row() {
|
||||||
|
// Verifies that a valid database row is correctly mapped to an Entity struct with all fields populated.
|
||||||
|
let conn = Connection::open_in_memory().unwrap();
|
||||||
|
let entity = get_entity_from_query(&conn, "SELECT 'e1', 'c1', 's1', 12345").unwrap();
|
||||||
|
assert_eq!(entity.id, "e1");
|
||||||
|
assert_eq!(entity.class, "c1");
|
||||||
|
assert_eq!(entity.subclass, Some("s1".to_string()));
|
||||||
|
assert_eq!(entity.ref_date, Some(12345));
|
||||||
|
assert_eq!(entity.state, EntityState::Loaded);
|
||||||
|
assert!(entity.attributes.is_empty());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn map_row_to_entity_should_handle_null_ref_date() {
|
||||||
|
// Ensures that a row with a NULL 'ref_date' is successfully mapped to an Entity with 'ref_date' as None.
|
||||||
|
let conn = Connection::open_in_memory().unwrap();
|
||||||
|
let entity = get_entity_from_query(&conn, "SELECT 'e2', 'c2', 's2', NULL").unwrap();
|
||||||
|
assert_eq!(entity.id, "e2");
|
||||||
|
assert_eq!(entity.class, "c2");
|
||||||
|
assert_eq!(entity.subclass, Some("s2".to_string()));
|
||||||
|
assert_eq!(entity.ref_date, None);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn map_row_to_entity_should_handle_null_subclass() {
|
||||||
|
// Ensures that a row with a NULL 'subclass' is successfully mapped to an Entity with 'subclass' as None.
|
||||||
|
let conn = Connection::open_in_memory().unwrap();
|
||||||
|
let entity = get_entity_from_query(&conn, "SELECT 'e2', 'c2', NULL, NULL").unwrap();
|
||||||
|
assert_eq!(entity.id, "e2");
|
||||||
|
assert_eq!(entity.class, "c2");
|
||||||
|
assert_eq!(entity.subclass, None);
|
||||||
|
assert_eq!(entity.ref_date, None);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn map_row_to_entity_should_return_error_on_type_mismatch() {
|
||||||
|
// Checks that a rusqlite::Error is returned if a column's type does not match the expected schema.
|
||||||
|
let conn = Connection::open_in_memory().unwrap();
|
||||||
|
// Trying to get an integer as a string for the class should fail.
|
||||||
|
let result = get_entity_from_query(&conn, "SELECT 'e3', 123, NULL");
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
160
01.workspace/heave/src/tst/sqlite_persist_catalog.rs
Normal file
160
01.workspace/heave/src/tst/sqlite_persist_catalog.rs
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::*;
|
||||||
|
use rusqlite::*;
|
||||||
|
use std::{collections::HashMap, fs, path::Path};
|
||||||
|
fn setup_db(db_path: &Path) -> Connection {
|
||||||
|
let _ = fs::remove_file(db_path);
|
||||||
|
fun::sqlite_init_db::run(db_path).unwrap();
|
||||||
|
Connection::open(db_path).unwrap()
|
||||||
|
}
|
||||||
|
fn count_rows(conn: &Connection, table: &str, where_clause: &str) -> i64 {
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare(&format!(
|
||||||
|
"SELECT COUNT(*) FROM {} WHERE {}",
|
||||||
|
table, where_clause
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
stmt.query_row([], |row| row.get(0)).unwrap()
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn persist_should_insert_new_entities_and_attributes() {
|
||||||
|
// Verifies that entities marked as 'New' in the catalog are inserted into the database, along with all their attributes.
|
||||||
|
let db_path = Path::new("test_insert.db");
|
||||||
|
let conn = setup_db(db_path);
|
||||||
|
let mut catalog = Catalog::new(db_path.to_str().unwrap());
|
||||||
|
let mut entity = Entity {
|
||||||
|
id: "e1".to_string(),
|
||||||
|
class: "c1".to_string(),
|
||||||
|
subclass: None,
|
||||||
|
attributes: HashMap::new(),
|
||||||
|
state: EntityState::New,
|
||||||
|
ref_date: None,
|
||||||
|
};
|
||||||
|
entity.attributes.insert(
|
||||||
|
"a1".to_string(),
|
||||||
|
Attribute {
|
||||||
|
id: "a1".to_string(),
|
||||||
|
value: Value::Text("v1".to_string()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
catalog.items.insert("e1".to_string(), entity);
|
||||||
|
assert!(sqlite::persist::catalog(db_path, &catalog).is_ok());
|
||||||
|
assert_eq!(count_rows(&conn, "entity", "id = 'e1'"), 1);
|
||||||
|
assert_eq!(count_rows(&conn, "attribute", "entity_id = 'e1'"), 1);
|
||||||
|
fs::remove_file(db_path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn persist_should_delete_entities_marked_for_deletion() {
|
||||||
|
// Ensures that entities marked as 'ToDelete' are correctly removed from the database.
|
||||||
|
let db_path = Path::new("test_delete.db");
|
||||||
|
let conn = setup_db(db_path);
|
||||||
|
conn.execute("INSERT INTO entity (id, class) VALUES ('e1', 'c1')", [])
|
||||||
|
.unwrap();
|
||||||
|
let mut catalog = Catalog::new(db_path.to_str().unwrap());
|
||||||
|
let entity = Entity {
|
||||||
|
id: "e1".to_string(),
|
||||||
|
class: "c1".to_string(),
|
||||||
|
subclass: None,
|
||||||
|
attributes: HashMap::new(),
|
||||||
|
state: EntityState::ToDelete,
|
||||||
|
ref_date: None,
|
||||||
|
};
|
||||||
|
catalog.items.insert("e1".to_string(), entity);
|
||||||
|
assert!(sqlite::persist::catalog(db_path, &catalog).is_ok());
|
||||||
|
assert_eq!(count_rows(&conn, "entity", "id = 'e1'"), 0);
|
||||||
|
fs::remove_file(db_path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn persist_should_handle_a_mix_of_new_and_deleted_entities() {
|
||||||
|
// Tests the function's ability to handle a batch operation involving both new entities to be inserted and existing ones to be deleted.
|
||||||
|
let db_path = Path::new("test_mix.db");
|
||||||
|
let conn = setup_db(db_path);
|
||||||
|
conn.execute("INSERT INTO entity (id, class) VALUES ('e1', 'c1')", [])
|
||||||
|
.unwrap();
|
||||||
|
let mut catalog = Catalog::new(db_path.to_str().unwrap());
|
||||||
|
let to_delete = Entity {
|
||||||
|
id: "e1".to_string(),
|
||||||
|
class: "c1".to_string(),
|
||||||
|
subclass: None,
|
||||||
|
attributes: HashMap::new(),
|
||||||
|
state: EntityState::ToDelete,
|
||||||
|
ref_date: None,
|
||||||
|
};
|
||||||
|
let to_add = Entity {
|
||||||
|
id: "e2".to_string(),
|
||||||
|
class: "c2".to_string(),
|
||||||
|
subclass: None,
|
||||||
|
attributes: HashMap::new(),
|
||||||
|
state: EntityState::New,
|
||||||
|
ref_date: None,
|
||||||
|
};
|
||||||
|
catalog.items.insert("e1".to_string(), to_delete);
|
||||||
|
catalog.items.insert("e2".to_string(), to_add);
|
||||||
|
assert!(sqlite::persist::catalog(db_path, &catalog).is_ok());
|
||||||
|
assert_eq!(count_rows(&conn, "entity", "id = 'e1'"), 0);
|
||||||
|
assert_eq!(count_rows(&conn, "entity", "id = 'e2'"), 1);
|
||||||
|
fs::remove_file(db_path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn persist_should_not_affect_unmodified_entities() {
|
||||||
|
// Verifies that entities in the catalog that are not marked as 'New' or 'ToDelete' remain untouched in the database.
|
||||||
|
let db_path = Path::new("test_unmodified.db");
|
||||||
|
let conn = setup_db(db_path);
|
||||||
|
conn.execute("INSERT INTO entity (id, class) VALUES ('e1', 'c1')", [])
|
||||||
|
.unwrap();
|
||||||
|
let mut catalog = Catalog::new(db_path.to_str().unwrap());
|
||||||
|
let unmodified = Entity {
|
||||||
|
id: "e1".to_string(),
|
||||||
|
class: "c1".to_string(),
|
||||||
|
subclass: None,
|
||||||
|
attributes: HashMap::new(),
|
||||||
|
state: EntityState::Loaded,
|
||||||
|
ref_date: None,
|
||||||
|
};
|
||||||
|
catalog.items.insert("e1".to_string(), unmodified);
|
||||||
|
assert!(sqlite::persist::catalog(db_path, &catalog).is_ok());
|
||||||
|
assert_eq!(count_rows(&conn, "entity", "id = 'e1'"), 1);
|
||||||
|
fs::remove_file(db_path).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn persist_should_rollback_transaction_on_failure() {
|
||||||
|
// Ensures that if any part of the persistence process fails, the entire transaction is rolled back, leaving the database state unchanged.
|
||||||
|
let db_path = Path::new("test_rollback.db");
|
||||||
|
let conn = setup_db(db_path);
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO entity (id, class) VALUES ('e_existing', 'c1')",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let mut catalog = Catalog::new(db_path.to_str().unwrap());
|
||||||
|
let mut new_entity = Entity {
|
||||||
|
id: "e_new".to_string(),
|
||||||
|
class: "c1".to_string(),
|
||||||
|
subclass: None,
|
||||||
|
attributes: HashMap::new(),
|
||||||
|
state: EntityState::New,
|
||||||
|
ref_date: None,
|
||||||
|
};
|
||||||
|
new_entity.attributes.insert(
|
||||||
|
"a1".to_string(),
|
||||||
|
Attribute {
|
||||||
|
id: "a1".to_string(),
|
||||||
|
value: Value::Text("v1".to_string()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
catalog.items.insert("e_new".to_string(), new_entity);
|
||||||
|
// Corrupt the DB to cause a failure during the transaction
|
||||||
|
conn.execute("DROP TABLE attribute", []).unwrap();
|
||||||
|
drop(conn);
|
||||||
|
let result = sqlite::persist::catalog(db_path, &catalog);
|
||||||
|
assert!(result.is_err());
|
||||||
|
// Re-open connection to check state
|
||||||
|
let conn = Connection::open(db_path).unwrap();
|
||||||
|
// The new entity should not have been inserted due to rollback
|
||||||
|
assert_eq!(count_rows(&conn, "entity", "id = 'e_new'"), 0);
|
||||||
|
// The existing entity should still be there
|
||||||
|
assert_eq!(count_rows(&conn, "entity", "id = 'e_existing'"), 1);
|
||||||
|
fs::remove_file(db_path).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user