From 0c369864d0886bda2c291cf4b097cd3955164836 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 21 May 2025 19:23:13 -0400 Subject: [PATCH] wip: the beginnings of some unit tests --- .gitignore | 1 + DESIGN.md | 2 +- exp1/src/serve.rs | 2 +- exp2/src/serve.rs | 2 +- offline-web-model/src/lib.rs | 21 ++-- offline-web-model/src/test.rs | 202 ++++++++++++++++++++++++++++++++++ 6 files changed, 218 insertions(+), 12 deletions(-) create mode 100644 offline-web-model/src/test.rs diff --git a/.gitignore b/.gitignore index d99f800..318b2e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ result/ result target/ +*.avanterules diff --git a/DESIGN.md b/DESIGN.md index 2d77747..7724aa6 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -21,7 +21,7 @@ there is one, and a list of any dependent resources. { "objectId": , "content_address": , - "path": "/path/name0", + "name": "/path/name0", "dependents": [ { "path": "path/name1", diff --git a/exp1/src/serve.rs b/exp1/src/serve.rs index 6015fa1..6483ddb 100644 --- a/exp1/src/serve.rs +++ b/exp1/src/serve.rs @@ -17,7 +17,7 @@ async fn ref_path(refs: Arc>>, Path(path): Path>, Path(addr): Path) -> String { +async fn object_path(objects: Arc>>, Path(addr): Path) -> Vec { dbg!(&addr); match objects.get(&addr) { Some(o) => o.clone(), diff --git a/exp2/src/serve.rs b/exp2/src/serve.rs index e7ba257..1c06b90 100644 --- a/exp2/src/serve.rs +++ b/exp2/src/serve.rs @@ -21,7 +21,7 @@ async fn get_client_js() -> impl IntoResponse { #[derive(Debug, Serialize, Deserialize)] enum ServerMsg { Reference(Reference), - Object(String), + Object(Vec), } #[derive(Debug, Serialize, Deserialize)] diff --git a/offline-web-model/src/lib.rs b/offline-web-model/src/lib.rs index 2a30c40..f094cff 100644 --- a/offline-web-model/src/lib.rs +++ b/offline-web-model/src/lib.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; /// Each reference contains: /// - An id that uniquely identifies the referenced object /// - A content address hash that represents the content -/// - A path that provides a human-readable location for the reference +/// - A name that provides a human-readable name for the reference /// - A list of dependent references that this reference depends on /// /// References form a directed acyclic graph where each node can have multiple dependents. @@ -102,7 +102,7 @@ impl Reference { pub struct Graph { pub root: Arc, pub refs: Arc>>, - pub objects: Arc>, + pub objects: Arc>>, } impl Graph { @@ -112,23 +112,23 @@ impl Graph { } /// Gets an object by its content address - pub fn get_object(&self, content_address: &str) -> Option<&String> { + pub fn get_object(&self, content_address: &str) -> Option<&Vec> { self.objects.get(content_address) } } -pub fn random_object() -> (String, String) { +pub fn random_object() -> (String, Vec) { let mut rng = rand::rng(); let random_size = rng.random_range(50..=4096); - let random_string: String = (0..random_size) - .map(|_| rng.sample(rand::distr::Alphanumeric) as char) + let random_bytes: Vec = (0..random_size) + .map(|_| rng.random::()) .collect(); let mut hasher = Blake2b512::new(); - hasher.update(&random_string); + hasher.update(&random_bytes); let hash = format!("{:x}", hasher.finalize()); - (hash, random_string) + (hash, random_bytes) } impl Graph { @@ -137,7 +137,7 @@ impl Graph { /// /// The reference ID is calculated from the content address, name, and any dependents, /// ensuring that it's truly content-addressable. - pub fn update_reference(&mut self, name: &String, new_content: String) -> Result<(), String> { + pub fn update_reference(&mut self, name: &String, new_content: Vec) -> Result<(), String> { // Create a mutable copy of our maps let mut refs = HashMap::new(); for (k, v) in self.refs.as_ref() { @@ -296,3 +296,6 @@ impl Graph { } } } + +#[cfg(test)] +mod test; diff --git a/offline-web-model/src/test.rs b/offline-web-model/src/test.rs new file mode 100644 index 0000000..1c25290 --- /dev/null +++ b/offline-web-model/src/test.rs @@ -0,0 +1,202 @@ +use std::collections::HashMap; +use std::collections::HashSet; +use std::sync::Arc; + +use rand::random_range; + +use crate::{Graph, Reference, random_object}; + +fn get_random_candidate(graph: &Graph) -> Arc { + // Pick a random leaf node to update + let refs: Vec> =graph.refs.values().filter(|r| r.name != graph.root.name).map(|r| r.clone()).collect(); + let random_index = random_range(0..refs.len()); + refs[random_index].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_random_candidate(&graph); + + // Update the leaf node + graph.update_reference(&candidate.name, random_object().1).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, reachable: &mut HashSet) { + 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( + 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( + 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( + 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(); + let 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_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_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"); +} +