366 lines
13 KiB
Rust
366 lines
13 KiB
Rust
use std::collections::HashMap;
|
|
use std::collections::HashSet;
|
|
use std::sync::Arc;
|
|
|
|
|
|
use crate::{Graph, Reference, random_object};
|
|
|
|
fn get_deterministic_candidate(graph: &Graph) -> Arc<Reference> {
|
|
// Pick a deterministic leaf node to update (first lexicographically)
|
|
let mut refs: Vec<Arc<Reference>> = graph.refs.values()
|
|
.filter(|r| r.name != graph.root.name && r.is_leaf())
|
|
.map(|r| r.clone())
|
|
.collect();
|
|
|
|
// Sort by name to ensure deterministic ordering
|
|
refs.sort_by(|a, b| a.name.cmp(&b.name));
|
|
|
|
refs[0].clone()
|
|
}
|
|
|
|
/// Tests that all dependencies are kept updated when new nodes are added
|
|
#[test]
|
|
fn test_dependencies_updated_when_nodes_added() {
|
|
// Create a simple graph
|
|
let mut graph = create_test_graph();
|
|
|
|
// Get the initial content address of the root
|
|
let initial_root_id = graph.root.id.clone();
|
|
|
|
let candidate = get_deterministic_candidate(&graph);
|
|
|
|
// Update the leaf node with deterministic content
|
|
let new_content = b"deterministic_test_content".to_vec();
|
|
graph.update_object_reference(&candidate.name, new_content).unwrap();
|
|
|
|
// Verify that the leaf node's ID has changed
|
|
let updated_leaf = graph.get_reference(&candidate.name).unwrap();
|
|
assert_ne!(updated_leaf.id, candidate.id,
|
|
"Leaf node ID should change when content is updated");
|
|
|
|
// Verify that the root's ID has changed
|
|
assert_ne!(graph.root.id, initial_root_id,
|
|
"Root ID should change when a dependent node is updated");
|
|
|
|
}
|
|
|
|
/// Tests that the root of the graph is not itself a dependency of any other node
|
|
#[test]
|
|
fn test_root_not_a_dependency() {
|
|
let graph = create_test_graph();
|
|
let root_name = graph.root.name.clone();
|
|
|
|
// Check all references to ensure none have the root as a dependent
|
|
for (_, reference) in graph.refs.as_ref() {
|
|
for dep in &reference.dependents {
|
|
assert_ne!(dep.name, root_name,
|
|
"Root should not be a dependency of any other node");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Tests that all nodes are dependents or transitive dependents of the root
|
|
#[test]
|
|
fn test_all_nodes_connected_to_root() {
|
|
let graph = create_test_graph();
|
|
|
|
// Collect all nodes reachable from the root
|
|
let mut reachable = HashSet::new();
|
|
|
|
fn collect_reachable(node: &Arc<Reference>, reachable: &mut HashSet<String>) {
|
|
reachable.insert(node.name.clone());
|
|
|
|
for dep in &node.dependents {
|
|
if !reachable.contains(&dep.name) {
|
|
collect_reachable(dep, reachable);
|
|
}
|
|
}
|
|
}
|
|
|
|
collect_reachable(&graph.root, &mut reachable);
|
|
|
|
// Check that all nodes in the graph are reachable from the root
|
|
for (name, _) in graph.refs.as_ref() {
|
|
assert!(reachable.contains(name),
|
|
"All nodes should be reachable from the root: {}", name);
|
|
}
|
|
}
|
|
|
|
/// Helper function to create a test graph with a known structure
|
|
fn create_test_graph() -> Graph {
|
|
let root_name = String::from("/root");
|
|
let mut objects = HashMap::new();
|
|
let mut refs = HashMap::new();
|
|
|
|
// Create the root reference
|
|
let mut root_ref = Reference::new(
|
|
Some(String::from("root_content")),
|
|
root_name.clone(),
|
|
);
|
|
|
|
// Create 3 item references
|
|
for i in 1..=3 {
|
|
let item_name = format!("/item/{}", i);
|
|
let mut item_ref = Reference::new(
|
|
Some(format!("item_content_{}", i)),
|
|
item_name.clone(),
|
|
);
|
|
|
|
// Create 3 subitems for each item
|
|
for j in 1..=3 {
|
|
let (address, content) = random_object();
|
|
let subitem_name = format!("/item/{}/subitem/{}", i, j);
|
|
|
|
// Create a leaf reference
|
|
let leaf_ref = Reference::new(
|
|
Some(address.clone()),
|
|
subitem_name,
|
|
).to_arc();
|
|
|
|
// Add the leaf reference as a dependent to the item reference
|
|
item_ref = item_ref.add_dep(leaf_ref.clone());
|
|
|
|
// Store the content in the objects map
|
|
objects.insert(address.clone(), content);
|
|
|
|
// Store the leaf reference in the refs map
|
|
refs.insert(leaf_ref.name.clone(), leaf_ref);
|
|
}
|
|
|
|
// Convert the item reference to Arc and add it to the root reference
|
|
let arc_item_ref = item_ref.to_arc();
|
|
root_ref = root_ref.add_dep(arc_item_ref.clone());
|
|
|
|
// Store the item reference in the refs map
|
|
refs.insert(arc_item_ref.name.clone(), arc_item_ref);
|
|
}
|
|
|
|
// Convert the root reference to Arc
|
|
let arc_root_ref = root_ref.to_arc();
|
|
|
|
// Store the root reference in the refs map
|
|
refs.insert(arc_root_ref.name.clone(), arc_root_ref.clone());
|
|
|
|
Graph {
|
|
root: arc_root_ref,
|
|
refs: Arc::new(refs),
|
|
objects: Arc::new(objects),
|
|
}
|
|
}
|
|
|
|
/// Tests that the graph correctly handles content-addressable properties
|
|
#[test]
|
|
fn test_content_addressable_properties() {
|
|
let mut graph = create_test_graph();
|
|
|
|
// Update a leaf node with the same content
|
|
let leaf_path = "/item/1/subitem/1".to_string();
|
|
let initial_leaf = graph.get_reference(&leaf_path).unwrap();
|
|
if let Some(content_address) = initial_leaf.content_address.clone() {
|
|
// Get the content for this address
|
|
let content = graph.get_object(&content_address).unwrap().clone();
|
|
|
|
// Update with the same content
|
|
graph.update_object_reference(&leaf_path, content).unwrap();
|
|
}
|
|
|
|
// Verify that nothing changed since the content is the same
|
|
let updated_leaf = graph.get_reference(&leaf_path).unwrap();
|
|
assert_eq!(updated_leaf.content_address, initial_leaf.content_address,
|
|
"Content address should not change when content remains the same");
|
|
}
|
|
|
|
/// Tests that the graph correctly handles ID calculation
|
|
#[test]
|
|
fn test_id_calculation() {
|
|
let mut graph = create_test_graph();
|
|
|
|
// Update a leaf node
|
|
let leaf_path = "/item/1/subitem/1".to_string();
|
|
let initial_leaf = graph.get_reference(&leaf_path).unwrap();
|
|
|
|
graph.update_object_reference(&leaf_path, "new content".as_bytes().to_vec()).unwrap();
|
|
|
|
// Verify that the ID changed
|
|
let updated_leaf = graph.get_reference(&leaf_path).unwrap();
|
|
assert_ne!(updated_leaf.id, initial_leaf.id,
|
|
"Reference ID should change when content changes");
|
|
|
|
// Verify that parent ID changed
|
|
let parent_path = "/item/1".to_string();
|
|
let parent = graph.get_reference(&parent_path).unwrap();
|
|
|
|
// Create a reference with the same properties to calculate expected ID
|
|
let mut test_ref = Reference::new(
|
|
parent.content_address.clone(),
|
|
parent.name.clone(),
|
|
);
|
|
|
|
// Add the same dependents
|
|
for dep in &parent.dependents {
|
|
test_ref = test_ref.add_dep(dep.clone());
|
|
}
|
|
|
|
// Verify the ID calculation is consistent
|
|
assert_eq!(parent.id, test_ref.id,
|
|
"ID calculation should be consistent for the same reference properties");
|
|
}
|
|
|
|
/// Tests that the root ID always changes when any reference is updated
|
|
#[test]
|
|
fn test_root_id_changes_for_any_reference_update() {
|
|
let mut graph = create_test_graph();
|
|
|
|
// Get all non-root references sorted by name for deterministic iteration
|
|
let mut all_refs: Vec<(String, String)> = graph.refs.as_ref()
|
|
.iter()
|
|
.filter(|(name, _)| **name != graph.root.name)
|
|
.map(|(name, ref_arc)| (name.clone(), ref_arc.id.clone()))
|
|
.collect();
|
|
|
|
all_refs.sort_by(|a, b| a.0.cmp(&b.0));
|
|
|
|
// Test each reference update
|
|
for (ref_name, original_ref_id) in all_refs {
|
|
// Record the current root ID
|
|
let initial_root_id = graph.root.id.clone();
|
|
|
|
// Update the reference with new content
|
|
let new_content = format!("updated_content_for_{}", ref_name).into_bytes();
|
|
graph.update_object_reference(&ref_name, new_content).unwrap();
|
|
|
|
// Verify the reference itself changed
|
|
let updated_ref = graph.get_reference(&ref_name).unwrap();
|
|
assert_ne!(updated_ref.id, original_ref_id,
|
|
"Reference {} should have changed ID after update", ref_name);
|
|
|
|
// Verify the root ID changed
|
|
assert_ne!(graph.root.id, initial_root_id,
|
|
"Root ID should change when reference {} is updated", ref_name);
|
|
}
|
|
}
|
|
|
|
/// Tests that Reference IDs are stable regardless of dependency add order
|
|
#[test]
|
|
fn test_reference_ids_stable_regardless_of_dependency_order() {
|
|
// Create test dependencies
|
|
let dep_a = Reference::new(
|
|
Some(String::from("content_a")),
|
|
String::from("/dep_a"),
|
|
).to_arc();
|
|
|
|
let dep_b = Reference::new(
|
|
Some(String::from("content_b")),
|
|
String::from("/dep_b"),
|
|
).to_arc();
|
|
|
|
let dep_c = Reference::new(
|
|
Some(String::from("content_c")),
|
|
String::from("/dep_c"),
|
|
).to_arc();
|
|
|
|
let dep_d = Reference::new(
|
|
Some(String::from("content_d")),
|
|
String::from("/dep_d"),
|
|
).to_arc();
|
|
|
|
// Create base reference
|
|
let base_ref = Reference::new(
|
|
Some(String::from("base_content")),
|
|
String::from("/base"),
|
|
);
|
|
|
|
// Test multiple different orders of adding the same dependencies
|
|
let orders = vec![
|
|
vec![dep_a.clone(), dep_b.clone(), dep_c.clone(), dep_d.clone()], // alphabetical
|
|
vec![dep_d.clone(), dep_c.clone(), dep_b.clone(), dep_a.clone()], // reverse alphabetical
|
|
vec![dep_b.clone(), dep_d.clone(), dep_a.clone(), dep_c.clone()], // random order 1
|
|
vec![dep_c.clone(), dep_a.clone(), dep_d.clone(), dep_b.clone()], // random order 2
|
|
vec![dep_d.clone(), dep_a.clone(), dep_b.clone(), dep_c.clone()], // random order 3
|
|
];
|
|
|
|
let mut all_ids = Vec::new();
|
|
|
|
for (i, order) in orders.iter().enumerate() {
|
|
let mut test_ref = base_ref.clone();
|
|
|
|
// Add dependencies in this specific order
|
|
for dep in order {
|
|
test_ref = test_ref.add_dep(dep.clone());
|
|
}
|
|
|
|
all_ids.push(test_ref.id.clone());
|
|
|
|
// Verify that dependencies are always sorted lexicographically regardless of add order
|
|
for j in 0..test_ref.dependents.len() - 1 {
|
|
let current = &test_ref.dependents[j];
|
|
let next = &test_ref.dependents[j + 1];
|
|
|
|
assert!(current.name < next.name,
|
|
"Dependencies should be lexicographically ordered. Order {}: Found '{}' before '{}'",
|
|
i, current.name, next.name);
|
|
}
|
|
}
|
|
|
|
// Verify all IDs are identical
|
|
let first_id = &all_ids[0];
|
|
for (i, id) in all_ids.iter().enumerate() {
|
|
assert_eq!(id, first_id,
|
|
"Reference ID should be stable regardless of dependency add order. Order {} produced different ID", i);
|
|
}
|
|
}
|
|
|
|
/// Tests that dependencies of a Reference are always lexicographically ordered by name
|
|
#[test]
|
|
fn test_dependencies_lexicographically_ordered() {
|
|
let graph = create_test_graph();
|
|
|
|
// Check all references to ensure their dependents are lexicographically ordered
|
|
for (_, reference) in graph.refs.as_ref() {
|
|
if reference.dependents.len() > 1 {
|
|
// Check that dependents are ordered by name
|
|
for i in 0..reference.dependents.len() - 1 {
|
|
let current = &reference.dependents[i];
|
|
let next = &reference.dependents[i + 1];
|
|
|
|
assert!(current.name < next.name,
|
|
"Dependencies should be lexicographically ordered by name. Found '{}' before '{}'",
|
|
current.name, next.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also verify that when we add dependencies, they maintain the correct order
|
|
let mut test_ref = Reference::new(
|
|
Some(String::from("test_content")),
|
|
String::from("/test_ordering"),
|
|
);
|
|
|
|
// Add dependencies in non-lexicographical order
|
|
let dep_c = Reference::new(
|
|
Some(String::from("c_content")),
|
|
String::from("/c"),
|
|
).to_arc();
|
|
|
|
let dep_a = Reference::new(
|
|
Some(String::from("a_content")),
|
|
String::from("/a"),
|
|
).to_arc();
|
|
|
|
let dep_b = Reference::new(
|
|
Some(String::from("b_content")),
|
|
String::from("/b"),
|
|
).to_arc();
|
|
|
|
// Add in non-lexicographical order
|
|
test_ref = test_ref.add_dep(dep_c.clone());
|
|
test_ref = test_ref.add_dep(dep_a.clone());
|
|
test_ref = test_ref.add_dep(dep_b.clone());
|
|
|
|
// Verify they are stored in lexicographical order
|
|
assert_eq!(test_ref.dependents[0].name, "/a", "First dependent should be '/a'");
|
|
assert_eq!(test_ref.dependents[1].name, "/b", "Second dependent should be '/b'");
|
|
assert_eq!(test_ref.dependents[2].name, "/c", "Third dependent should be '/c'");
|
|
}
|
|
|