diff --git a/.gitignore b/.gitignore index d7328db..7164ba8 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,11 @@ target/ #.idea/ .DS_Store +# Untracked internal references (papers, private docs, etc.). +# Contents must never be referenced directly in code or comments. +# Extract and restate any needed information explicitly in the codebase. +internal/ + Cargo.lock .idea/ diff --git a/CLAUDE.md b/CLAUDE.md index 5533984..9e3088f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,15 @@ This is a Rust implementation of Skip Graphs - a distributed data structure for ## References -- `skip-graphs-paper.pdf` (Aspnes & Shah) — canonical algorithms: search (Algorithm 1, p.6), insert/join (Algorithm 2, p.8), delete (Algorithm 3, p.8). +Source papers and private documents live in `internal/` (gitignored, never committed). + +- `internal/skip-graphs-paper.pdf` — Aspnes & Shah, "Skip Graphs". Canonical algorithms: search (Algorithm 1, p.6), insert/join (Algorithm 2, p.8), delete (Algorithm 3, p.8). + +### Rules for internal references + +1. **Never cite internal documents in code.** No path references, no "see PDF p.X", no file names from `internal/` in source files, comments, or doc-strings. +2. **Restate, don't link.** If an algorithm, invariant, or design decision from an internal document is needed in the codebase, copy the relevant content explicitly — as a comment, a doc-string, or a design doc under `docs/` — and use that in-codebase copy as the reference going forward. +3. **Agents must follow the same rules.** Any agent working in this repository must not produce code or comments that reference `internal/` paths or document titles. Extract and restate; never leak. ## Architecture diff --git a/skip-graphs-paper.pdf b/skip-graphs-paper.pdf deleted file mode 100644 index 36d0ddd..0000000 Binary files a/skip-graphs-paper.pdf and /dev/null differ diff --git a/src/core/context/context_test.rs b/src/core/context/context_test.rs index b91a55c..7e8d079 100644 --- a/src/core/context/context_test.rs +++ b/src/core/context/context_test.rs @@ -14,10 +14,9 @@ mod tests { // Wait until cancellation is processed let ctx_clone = ctx.clone(); - wait_until( - move || ctx_clone.is_cancelled(), - Duration::from_millis(100) - ).await.expect("context should be cancelled within 100ms"); + wait_until(move || ctx_clone.is_cancelled(), Duration::from_millis(100)) + .await + .expect("context should be cancelled within 100ms"); } /// this test ensures that cancelling a parent context cancels its children @@ -33,8 +32,10 @@ mod tests { let child_clone = child.clone(); wait_until( move || child_clone.is_cancelled(), - Duration::from_millis(100) - ).await.expect("child context should be cancelled within 100ms"); + Duration::from_millis(100), + ) + .await + .expect("child context should be cancelled within 100ms"); } /// this test ensures that running an operation completes successfully if its context is not canceled @@ -42,9 +43,7 @@ mod tests { async fn test_successful_operation() { let ctx = IrrevocableContext::new(&span_fixture(), "test_context"); - let result = ctx.run(async { - Ok::(42) - }).await; + let result = ctx.run(async { Ok::(42) }).await; assert!(result.is_ok()); assert_eq!(result.unwrap(), 42); @@ -61,25 +60,27 @@ mod tests { // Wait for cancellation to be processed let ctx_clone = ctx.clone(); - wait_until( - move || ctx_clone.is_cancelled(), - Duration::from_millis(100) - ).await.expect("context should be cancelled within 100ms"); - - let result = ctx.run(async { - // This should not execute due to cancellation - sleep(Duration::from_millis(10)).await; - Ok::(42) - - }).await; + wait_until(move || ctx_clone.is_cancelled(), Duration::from_millis(100)) + .await + .expect("context should be cancelled within 100ms"); + + let result = ctx + .run(async { + // This should not execute due to cancellation + sleep(Duration::from_millis(10)).await; + Ok::(42) + }) + .await; // The operation should return an error since the context was canceled before it could complete assert!(result.is_err()); // The error should indicate cancellation - assert!(result.unwrap_err().to_string().contains("context cancelled")); + assert!(result + .unwrap_err() + .to_string() + .contains("context cancelled")); } - /// this test ensures that nested child contexts are canceled when the root context is canceled #[tokio::test] async fn test_nested_children() { @@ -99,9 +100,15 @@ mod tests { let child2_clone = child2.clone(); let grandchild_clone = grandchild.clone(); wait_until( - move || child1_clone.is_cancelled() && child2_clone.is_cancelled() && grandchild_clone.is_cancelled(), - Duration::from_millis(100) - ).await.expect("all child contexts should be cancelled within 100ms"); + move || { + child1_clone.is_cancelled() + && child2_clone.is_cancelled() + && grandchild_clone.is_cancelled() + }, + Duration::from_millis(100), + ) + .await + .expect("all child contexts should be cancelled within 100ms"); } /// Test that we can create the error propagation hierarchy @@ -137,9 +144,9 @@ mod tests { if let Err(panic_payload) = result { if let Some(panic_msg) = panic_payload.downcast_ref::() { assert_eq!(panic_msg, "irrecoverable error: test irrecoverable error"); - } else{ + } else { panic!("unexpected panic payload type"); } } } -} \ No newline at end of file +} diff --git a/src/core/context/mod.rs b/src/core/context/mod.rs index 4166dab..a0a5106 100644 --- a/src/core/context/mod.rs +++ b/src/core/context/mod.rs @@ -17,7 +17,7 @@ use tokio_util::sync::CancellationToken; use tracing::Span; /// A cancelable context that supports parent-child hierarchies and irrecoverable error propagation. -/// +/// /// When an irrecoverable error is thrown, it propagates up to the root context and terminates the program. /// Children automatically get cancelled when their parent is cancelled. pub struct IrrevocableContext { @@ -34,7 +34,7 @@ impl IrrevocableContext { /// Create a new root context pub fn new(parent_span: &Span, tag: &str) -> Self { let span = tracing::span!(parent: parent_span, tracing::Level::TRACE, "irrevocable_context", tag = tag); - + Self { inner: Arc::new(ContextInner { token: CancellationToken::new(), @@ -82,7 +82,7 @@ impl IrrevocableContext { F: std::future::Future>, { let _enter = self.inner.span.enter(); - + tokio::select! { result = future => result, _ = self.cancelled() => { @@ -96,13 +96,13 @@ impl IrrevocableContext { /// there is no return from this function. pub fn throw_irrecoverable(&self, err: anyhow::Error) -> ! { let _enter = self.inner.span.enter(); - + // Propagate to parent if it exists if let Some(parent) = &self.inner.parent { tracing::error!("propagating irrecoverable error to parent context"); parent.throw_irrecoverable(err); } - + // Root context - panic with the error panic!("irrecoverable error: {}", err); } @@ -145,4 +145,3 @@ impl Clone for IrrevocableContext { } } } - diff --git a/src/core/lookup/array_lookup_table.rs b/src/core/lookup/array_lookup_table.rs index d4c8026..cac79bc 100644 --- a/src/core/lookup/array_lookup_table.rs +++ b/src/core/lookup/array_lookup_table.rs @@ -87,7 +87,12 @@ impl LookupTable for ArrayLookupTable { } // Log the update operation - tracing::trace!("lookup table entry updated: level {}, direction {}, identifier {}", level, direction, identity.id()); + tracing::trace!( + "lookup table entry updated: level {}, direction {}, identifier {}", + level, + direction, + identity.id() + ); Ok(()) } diff --git a/src/core/lookup/array_lookup_table_test.rs b/src/core/lookup/array_lookup_table_test.rs index 3642695..f7e68c2 100644 --- a/src/core/lookup/array_lookup_table_test.rs +++ b/src/core/lookup/array_lookup_table_test.rs @@ -1,10 +1,10 @@ #[cfg(test)] mod tests { - use crate::core::testutil::fixtures::*; - use std::collections::HashMap; - use crate::core::{model, ArrayLookupTable, LookupTable, LOOKUP_TABLE_LEVELS}; use crate::core::model::direction::Direction; use crate::core::model::identity::Identity; + use crate::core::testutil::fixtures::*; + use crate::core::{model, ArrayLookupTable, LookupTable, LOOKUP_TABLE_LEVELS}; + use std::collections::HashMap; #[test] /// A new lookup table should be empty. @@ -127,8 +127,7 @@ mod tests { let identities = random_identities(2 * levels); for i in 0..levels { - lt.update_entry(identities[i], i, Direction::Left) - .unwrap(); + lt.update_entry(identities[i], i, Direction::Left).unwrap(); lt.update_entry(identities[i + levels], i, Direction::Right) .unwrap(); } @@ -242,8 +241,8 @@ mod tests { /// Note: failure or flaky behavior of this test may indicate a bug in the implementation. #[test] fn test_randomized_concurrent_operations_with_validation() { - use rand::Rng; use parking_lot::Mutex; + use rand::Rng; use std::sync::{Arc, Barrier}; use std::thread; @@ -440,4 +439,4 @@ mod tests { assert_eq!(lt2.get_entry(2, Direction::Left).unwrap(), Some(id3)); assert_eq!(lt3.get_entry(2, Direction::Left).unwrap(), Some(id3)); } -} \ No newline at end of file +} diff --git a/src/core/lookup/mod.rs b/src/core/lookup/mod.rs index 41a806d..93bea7f 100644 --- a/src/core/lookup/mod.rs +++ b/src/core/lookup/mod.rs @@ -1,7 +1,6 @@ use crate::core::model::direction::Direction; use crate::core::model::identity::Identity; - pub mod array_lookup_table; mod array_lookup_table_test; @@ -40,7 +39,7 @@ pub trait LookupTable: Send + Sync { fn right_neighbors(&self) -> anyhow::Result>; /// Creates a shallow copy of this lookup table. - /// + /// /// Implementations should ensure that cloned instances share the same underlying data /// (e.g., using Arc for shared ownership). Changes made through one instance should be /// visible in all cloned instances. This is the standard cloning behavior for all diff --git a/src/core/model/address.rs b/src/core/model/address.rs index 1ca23ac..4b40d42 100644 --- a/src/core/model/address.rs +++ b/src/core/model/address.rs @@ -1,5 +1,5 @@ -use std::fmt::Debug; use fixedstr::{str128, str8}; +use std::fmt::Debug; /// Represents a networking address; composed of host + port #[derive(Copy, Clone, PartialEq)] diff --git a/src/core/model/direction.rs b/src/core/model/direction.rs index d0b88f0..05bbf06 100644 --- a/src/core/model/direction.rs +++ b/src/core/model/direction.rs @@ -19,5 +19,5 @@ impl Display for Direction { impl Debug for Direction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self) - } + } } diff --git a/src/core/model/identity.rs b/src/core/model/identity.rs index a2fe210..9622430 100644 --- a/src/core/model/identity.rs +++ b/src/core/model/identity.rs @@ -10,22 +10,22 @@ pub struct Identity { impl Identity { /// Create a new Identity - pub fn new(id: &Identifier, mem_vec: &MembershipVector, address: Address) -> Identity { + pub fn new(id: Identifier, mem_vec: MembershipVector, address: Address) -> Identity { Identity { - id: *id, - mem_vec: *mem_vec, + id, + mem_vec, address, } } /// Get the identifier of the node - pub fn id(&self) -> &Identifier { - &self.id + pub fn id(&self) -> Identifier { + self.id } /// Get the membership vector of the node - pub fn mem_vec(&self) -> &MembershipVector { - &self.mem_vec + pub fn mem_vec(&self) -> MembershipVector { + self.mem_vec } /// Get the address of the node @@ -45,9 +45,9 @@ mod tests { let id = random_identifier(); let mem_vec = random_membership_vector(); let address = Address::new("localhost", "1234"); - let identity = Identity::new(&id, &mem_vec, address); - assert_eq!(identity.id(), &id); - assert_eq!(identity.mem_vec(), &mem_vec); + let identity = Identity::new(id, mem_vec, address); + assert_eq!(identity.id(), id); + assert_eq!(identity.mem_vec(), mem_vec); assert_eq!(identity.address(), address); } } diff --git a/src/core/model/memvec.rs b/src/core/model/memvec.rs index 3b00e2b..fbe9d5f 100644 --- a/src/core/model/memvec.rs +++ b/src/core/model/memvec.rs @@ -69,7 +69,7 @@ impl MembershipVector { /// # Returns /// /// * `Vec` - A vector containing the bytes of the MembershipVector. - /// + /// /// Consider using `as_bytes()` if you don't need ownership. pub fn to_bytes(&self) -> Vec { self.0.to_vec() @@ -84,7 +84,7 @@ impl MembershipVector { /// # Returns /// /// * `u64` - The number of common prefix bits. - pub fn common_prefix_bit(&self, other: &MembershipVector) -> usize { + pub fn common_prefix_bit(&self, other: MembershipVector) -> usize { let mut common_bits = 0; for (byte_a, byte_b) in self.0.iter().zip(other.0.iter()) { let xor = byte_a ^ byte_b; @@ -239,16 +239,16 @@ mod test { // every membership vector should have complete common bits prefix with itself. assert_eq!( - mv_0.common_prefix_bit(&mv_0), + mv_0.common_prefix_bit(mv_0), model::IDENTIFIER_SIZE_BYTES * 8 ); assert_eq!( - mv_1.common_prefix_bit(&mv_1), + mv_1.common_prefix_bit(mv_1), model::IDENTIFIER_SIZE_BYTES * 8 ); // all zero and all one should not have any common bits prefix - assert_eq!(mv_0.common_prefix_bit(&mv_1), 0); + assert_eq!(mv_0.common_prefix_bit(mv_1), 0); // first byte is 01111111 and the rest is all 1 let first_bit_zero = [ @@ -259,15 +259,15 @@ mod test { let mv_01 = MembershipVector::from_bytes(&first_bit_zero).unwrap(); // should have complete common prefix with itself assert_eq!( - mv_01.common_prefix_bit(&mv_01), + mv_01.common_prefix_bit(mv_01), model::IDENTIFIER_SIZE_BYTES * 8 ); // should have zero common prefix with all 1s. - assert_eq!(mv_1.common_prefix_bit(&mv_01), 0); - assert_eq!(mv_01.common_prefix_bit(&mv_1), 0); + assert_eq!(mv_1.common_prefix_bit(mv_01), 0); + assert_eq!(mv_01.common_prefix_bit(mv_1), 0); // should have 1 common bit prefix with all 0s (only the first bit) - assert_eq!(mv_0.common_prefix_bit(&mv_01), 1); - assert_eq!(mv_01.common_prefix_bit(&mv_0), 1); + assert_eq!(mv_0.common_prefix_bit(mv_01), 1); + assert_eq!(mv_01.common_prefix_bit(mv_0), 1); // 00111111 11111111 ... // first two bits are zero and the rest is all 1 @@ -276,15 +276,15 @@ mod test { let mv_001 = MembershipVector::from_bytes(&first_two_bits_zero).unwrap(); // should have complete common prefix with itself assert_eq!( - mv_001.common_prefix_bit(&mv_001), + mv_001.common_prefix_bit(mv_001), model::IDENTIFIER_SIZE_BYTES * 8 ); // should have two bits common prefix with all 0s. - assert_eq!(mv_001.common_prefix_bit(&mv_0), 2); - assert_eq!(mv_0.common_prefix_bit(&mv_001), 2); + assert_eq!(mv_001.common_prefix_bit(mv_0), 2); + assert_eq!(mv_0.common_prefix_bit(mv_001), 2); // should have zero bits common prefix with all 1s. - assert_eq!(mv_001.common_prefix_bit(&mv_1), 0); - assert_eq!(mv_1.common_prefix_bit(&mv_001), 0); + assert_eq!(mv_001.common_prefix_bit(mv_1), 0); + assert_eq!(mv_1.common_prefix_bit(mv_001), 0); // 11111111 00000000 11111111 11111111 ... // first byte all ones; second byte all zeros; third and rest all ones @@ -297,15 +297,15 @@ mod test { let mv_second_byte_all_zero = MembershipVector::from_bytes(&second_byte_all_zero).unwrap(); // should have complete common prefix with itself. assert_eq!( - mv_second_byte_all_zero.common_prefix_bit(&mv_second_byte_all_zero), + mv_second_byte_all_zero.common_prefix_bit(mv_second_byte_all_zero), model::IDENTIFIER_SIZE_BYTES * 8 ); // should have zero bits common prefix with all zeros - assert_eq!(mv_second_byte_all_zero.common_prefix_bit(&mv_0), 0); - assert_eq!(mv_0.common_prefix_bit(&mv_second_byte_all_zero), 0); + assert_eq!(mv_second_byte_all_zero.common_prefix_bit(mv_0), 0); + assert_eq!(mv_0.common_prefix_bit(mv_second_byte_all_zero), 0); // should have 8 bits (one complete byte) common prefix with all ones. - assert_eq!(mv_second_byte_all_zero.common_prefix_bit(&mv_1), 8); - assert_eq!(mv_1.common_prefix_bit(&mv_second_byte_all_zero), 8); + assert_eq!(mv_second_byte_all_zero.common_prefix_bit(mv_1), 8); + assert_eq!(mv_1.common_prefix_bit(mv_second_byte_all_zero), 8); // 11111111 10000000 11111111 11111111 ... let second_byte_128 = [ @@ -317,22 +317,22 @@ mod test { let mv_second_byte_128 = MembershipVector::from_bytes(&second_byte_128).unwrap(); // should have complete common prefix with itself assert_eq!( - mv_second_byte_128.common_prefix_bit(&mv_second_byte_128), + mv_second_byte_128.common_prefix_bit(mv_second_byte_128), model::IDENTIFIER_SIZE_BYTES * 8 ); // should have zero bits common prefix with all zeros; - assert_eq!(mv_second_byte_128.common_prefix_bit(&mv_0), 0); - assert_eq!(mv_0.common_prefix_bit(&mv_second_byte_128), 0); + assert_eq!(mv_second_byte_128.common_prefix_bit(mv_0), 0); + assert_eq!(mv_0.common_prefix_bit(mv_second_byte_128), 0); // should have 9 bits common prefix with all ones (first byte and first bit of the second byte): - assert_eq!(mv_second_byte_128.common_prefix_bit(&mv_1), 8 + 1); - assert_eq!(mv_1.common_prefix_bit(&mv_second_byte_128), 8 + 1); + assert_eq!(mv_second_byte_128.common_prefix_bit(mv_1), 8 + 1); + assert_eq!(mv_1.common_prefix_bit(mv_second_byte_128), 8 + 1); // should have 8 bits common prefix with the second_byte_all_zero assert_eq!( - mv_second_byte_128.common_prefix_bit(&mv_second_byte_all_zero), + mv_second_byte_128.common_prefix_bit(mv_second_byte_all_zero), 8 ); assert_eq!( - mv_second_byte_all_zero.common_prefix_bit(&mv_second_byte_128), + mv_second_byte_all_zero.common_prefix_bit(mv_second_byte_128), 8 ); @@ -359,8 +359,8 @@ mod test { &[left_bytes.clone(), vec![255u8; 1], right_bytes.clone()].concat(), ) .unwrap(); - assert_eq!(mv_239.common_prefix_bit(&mv_255), random_index * 8 + 3); - assert_eq!(mv_255.common_prefix_bit(&mv_239), random_index * 8 + 3); + assert_eq!(mv_239.common_prefix_bit(mv_255), random_index * 8 + 3); + assert_eq!(mv_255.common_prefix_bit(mv_239), random_index * 8 + 3); // Case 2: at random index; they differ at a random bit let byte_1 = random::bytes(1)[0]; @@ -387,7 +387,7 @@ mod test { // is at the random_bit_index, so the first 8 - random_bit_index bits are common prefix. // However, random-bit-index starts from 0, so we indeed have (8 - (random_bit_index + 1)) bits common prefix. assert_eq!( - mv_1.common_prefix_bit(&mv_2), + mv_1.common_prefix_bit(mv_2), random_index * 8 + (7 - random_bit_index) ); } diff --git a/src/core/model/search.rs b/src/core/model/search.rs index 5ef4f14..5b97a24 100644 --- a/src/core/model/search.rs +++ b/src/core/model/search.rs @@ -3,13 +3,13 @@ use crate::core::model::direction::Direction; use crate::core::Identifier; #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] -pub struct RequestId { +pub struct Nonce { id: u128, } -impl RequestId { +impl Nonce { pub fn random() -> Self { - RequestId { + Nonce { id: rand::random::(), } } @@ -17,103 +17,26 @@ impl RequestId { #[derive(Debug, Copy, Clone)] pub struct IdSearchReq { - req_id: RequestId, - target: Identifier, - origin: Identifier, - level: LookupTableLevel, - direction: Direction, -} - -impl IdSearchReq { - pub fn new( - req_id: RequestId, - origin: Identifier, - target: Identifier, - level: LookupTableLevel, - direction: Direction, - ) -> Self { - IdSearchReq { - req_id, - target, - origin, - level, - direction, - } - } - - pub fn target(&self) -> &Identifier { - &self.target - } - - pub fn level(&self) -> LookupTableLevel { - self.level - } - - pub fn direction(&self) -> Direction { - self.direction - } - - pub fn origin(&self) -> &Identifier { - &self.origin - } - - pub fn req_id(&self) -> RequestId { - self.req_id - } + /// The unique identifier of the search request across all nodes (randomly generated). + pub nonce: Nonce, + /// The identifier that is being searched for. + pub target: Identifier, + /// The identifier of the node that initiated the search. + pub origin: Identifier, + /// The level of the lookup table where the search is being performed. + pub level: LookupTableLevel, + /// The direction of the search. + pub direction: Direction, } #[derive(Debug, Copy, Clone)] pub struct IdSearchRes { /// The unique identifier of the search request across all nodes (randomly generated). - request_id: RequestId, + pub nonce: Nonce, /// The identifier that is being searched for. - target: Identifier, + pub target: Identifier, /// The level of the lookup table where the search was terminated at the current node. - termination_level: LookupTableLevel, + pub termination_level: LookupTableLevel, /// The identifier that was found during the search process at the current node. - result: Identifier, -} - -impl IdSearchRes { - /// Constructs a new `IdentifierSearchResult` instance. - /// - /// # Parameters - /// - /// - `request_id`: A `RequestId` that uniquely identifies the search request across all nodes (randomly generated). - /// - `target`: An `Identifier` representing the target element for the search operation in the lookup table of the current node. - /// - `termination_level`: A `LookupTableLevel` that specifies the lookup level where the search was terminated at the current node. - /// - `result`: An `Identifier` holding the result of the search operation at the current node. - /// - /// # Returns - /// - /// Returns a new `IdentifierSearchResult` instance. - /// and `result` parameters. - pub fn new(request_id: RequestId, target: Identifier, level: LookupTableLevel, result: Identifier) -> Self { - IdSearchRes { - request_id, - target, - termination_level: level, - result, - } - } - - /// Returns a reference to the `target` field of the struct. - pub fn target(&self) -> &Identifier { - &self.target - } - - /// Returns the level of the lookup table where the search was terminated at the current node. - pub fn termination_level(&self) -> LookupTableLevel { - self.termination_level - } - - /// Returns the result of the search operation at the current node. - pub fn result(&self) -> &Identifier { - &self.result - } - - /// Returns the request id of the search, the unique id that identifies the search request across all nodes. - pub fn request_id(&self) -> RequestId { - self.request_id - } + pub result: Identifier, } diff --git a/src/core/testutil/fixtures.rs b/src/core/testutil/fixtures.rs index 0bd3b91..2ef26e1 100644 --- a/src/core/testutil/fixtures.rs +++ b/src/core/testutil/fixtures.rs @@ -83,8 +83,8 @@ pub fn random_address() -> Address { pub fn random_identity() -> Identity { Identity::new( - &random_identifier(), - &random_membership_vector(), + random_identifier(), + random_membership_vector(), random_address(), ) } @@ -99,8 +99,7 @@ pub fn random_lookup_table(n: usize) -> ArrayLookupTable { let ids = random_identities(2 * n); for i in 0..n { lt.update_entry(ids[i], i, Direction::Left).unwrap(); - lt.update_entry(ids[i + n], i, Direction::Right) - .unwrap(); + lt.update_entry(ids[i + n], i, Direction::Right).unwrap(); } lt } @@ -115,8 +114,8 @@ pub fn random_lookup_table_with_extremes(n: usize) -> ArrayLookupTable { let max_id = Identifier::from_bytes(&[0xFFu8; model::IDENTIFIER_SIZE_BYTES]).unwrap(); let max_mv = MembershipVector::from_bytes(&[0xFFu8; model::IDENTIFIER_SIZE_BYTES]).unwrap(); - let zero_identity = Identity::new(&zero_id, &zero_mv, random_address()); - let max_identity = Identity::new(&max_id, &max_mv, random_address()); + let zero_identity = Identity::new(zero_id, zero_mv, random_address()); + let max_identity = Identity::new(max_id, max_mv, random_address()); lt.update_entry(zero_identity, 0, Direction::Left).unwrap(); lt.update_entry(max_identity, 0, Direction::Right).unwrap(); @@ -242,23 +241,18 @@ mod test { } /// Polls `condition` on a blocking task (yielding between checks) until it is true or `timeout` elapses. -pub async fn wait_until( - mut condition: F, - timeout: Duration, -) -> Result<(), String> +pub async fn wait_until(mut condition: F, timeout: Duration) -> Result<(), String> where F: FnMut() -> bool + Send + 'static, { let (tx, rx) = tokio::sync::oneshot::channel::>(); - let condition_task = tokio::task::spawn_blocking(move || { - loop { - if condition() { - let _ = tx.send(Ok(())); - return; - } - std::thread::yield_now(); + let condition_task = tokio::task::spawn_blocking(move || loop { + if condition() { + let _ = tx.send(Ok(())); + return; } + std::thread::yield_now(); }); let result = match tokio::time::timeout(timeout, rx).await { diff --git a/src/lib.rs b/src/lib.rs index cb137c0..af57158 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,3 @@ pub mod core; -mod node; mod network; +mod node; diff --git a/src/network/mock/network.rs b/src/network/mock/network.rs index 2326277..93a9530 100644 --- a/src/network/mock/network.rs +++ b/src/network/mock/network.rs @@ -1,9 +1,9 @@ -use std::sync::Arc; +use crate::core::Identifier; use crate::network::mock::hub::NetworkHub; use crate::network::{Event, MessageProcessor, Network}; use anyhow::{anyhow, Context}; use parking_lot::RwLock; -use crate::core::Identifier; +use std::sync::Arc; /// MockNetwork is a mock implementation of the Network trait for testing purposes. /// It does not perform any real network operations but simulates event routing and processing through a `NetworkHub`. @@ -38,12 +38,12 @@ impl MockNetwork { /// * `Result<(), anyhow::Error>`: Returns Ok if the event was processed successfully, or an error if processing failed. pub fn incoming_event(&self, origin_id: Identifier, event: Event) -> anyhow::Result<()> { let core_guard = self.core.read(); - + let processor = match core_guard.processor.as_ref() { Some(p) => p, None => return Err(anyhow!("no event processor registered")), }; - + processor .process_incoming_event(origin_id, event) .context("failed to process incoming event") @@ -62,8 +62,9 @@ impl Network for MockNetwork { /// Sends an event through the mock network by routing it through the NetworkHub. fn send_event(&self, target_id: Identifier, event: Event) -> anyhow::Result<()> { let core_guard = self.core.read(); - - core_guard.hub + + core_guard + .hub .route_event(core_guard.id, target_id, event) .map_err(|e| anyhow!("failed to route event: {}", e)) } @@ -71,12 +72,9 @@ impl Network for MockNetwork { /// Registers an event processor to handle incoming events. /// Only one processor can be registered at a time. /// If a processor is already registered, an error is returned. - fn register_processor( - &self, - processor: MessageProcessor, - ) -> anyhow::Result<()> { + fn register_processor(&self, processor: MessageProcessor) -> anyhow::Result<()> { let mut core_guard = self.core.write(); - + match core_guard.processor.as_ref() { Some(_) => Err(anyhow!("an event processor is already registered")), None => { diff --git a/src/network/mock/network_test.rs b/src/network/mock/network_test.rs index bfe56d5..d524815 100644 --- a/src/network/mock/network_test.rs +++ b/src/network/mock/network_test.rs @@ -1,11 +1,11 @@ use crate::core::testutil::fixtures::random_identifier; +use crate::core::Identifier; use crate::network::mock::hub::NetworkHub; use crate::network::Event::TestMessage; -use crate::network::{MessageProcessor, EventProcessorCore, Network, Event}; +use crate::network::{Event, EventProcessorCore, MessageProcessor, Network}; use std::collections::HashSet; use std::sync::{Arc, Barrier, RwLock}; use std::thread; -use crate::core::Identifier; struct MockEventProcessor { inner: Arc>, @@ -45,7 +45,9 @@ impl EventProcessorCore for MockEventProcessor { self.inner.write().unwrap().seen.insert(content); Ok(()) } - _ => Err(anyhow::anyhow!("MockEventProcessor only handles TestMessage payloads")), + _ => Err(anyhow::anyhow!( + "MockEventProcessor only handles TestMessage payloads" + )), } } } @@ -60,15 +62,12 @@ fn test_mock_event_processor() { let processor = MessageProcessor::new(Box::new(core_processor.clone())); let event = TestMessage("Hello, World!".to_string()); - assert!(!core_processor.has_seen("Hello, World!")); - - assert!(mock_network - .register_processor(processor) - .is_ok()); + + assert!(mock_network.register_processor(processor).is_ok()); let origin_id = random_identifier(); assert!(hub.route_event(origin_id, target_id, event).is_ok()); - + assert!(core_processor.has_seen("Hello, World!")); } @@ -91,9 +90,9 @@ fn test_hub_route_event() { let event = TestMessage("Test message".to_string()); assert!(!core_proc_1.has_seen("Test message")); - + assert!(mock_net_2.send_event(id_1, event).is_ok()); - + assert!(core_proc_1.has_seen("Test message")); } @@ -102,29 +101,29 @@ fn test_hub_route_event() { fn test_network_hub_shallow_clone() { let hub = NetworkHub::new(); let hub_clone = hub.clone(); - + let target_id = random_identifier(); - + // Create a mock network through the original hub let mock_network = NetworkHub::new_mock_network(hub.clone(), target_id).unwrap(); - + // Create an event to route through the cloned hub let event = TestMessage("Shallow clone test".to_string()); - + // Register a processor on the mock network let core_processor = MockEventProcessor::new(); let processor = MessageProcessor::new(Box::new(core_processor.clone())); mock_network .register_processor(processor) .expect("failed to register event processor"); - + // Verify the event hasn't been seen yet assert!(!core_processor.has_seen("Shallow clone test")); - + // Route event through the CLONED hub - this should work because it shares the same underlying data let origin_id = random_identifier(); assert!(hub_clone.route_event(origin_id, target_id, event).is_ok()); - + // Verify the event was processed - proving the clone shares the same networks map assert!(core_processor.has_seen("Shallow clone test")); } @@ -146,8 +145,7 @@ fn test_concurrent_event_sending() { let mock_net_2 = NetworkHub::new_mock_network(hub, id_2).unwrap(); // Create 10 different event contents - let event_contents: Vec = - (0..10).map(|i| format!("Concurrent message {i}")).collect(); + let event_contents: Vec = (0..10).map(|i| format!("Concurrent message {i}")).collect(); // Set up a barrier to synchronize all threads let barrier = Arc::new(Barrier::new(10)); @@ -194,26 +192,26 @@ fn test_mock_network_processor_sharing_between_clones() { let hub = NetworkHub::new(); let identifier = random_identifier(); let mock_network = NetworkHub::new_mock_network(hub.clone(), identifier).unwrap(); - + // Clone the network before registering a processor let mock_network_clone = mock_network.clone(); - + // Register a processor on the original network let core_processor = MockEventProcessor::new(); let processor = MessageProcessor::new(Box::new(core_processor.clone())); - + assert!(mock_network.register_processor(processor).is_ok()); - + // Create an event to test with let event = TestMessage("Shared processor test".to_string()); - + // Verify the event hasn't been seen yet assert!(!core_processor.has_seen("Shared processor test")); - + // Send event through the CLONED network - should work because processor is shared let origin_id = random_identifier(); assert!(mock_network_clone.incoming_event(origin_id, event).is_ok()); - + // Verify the event was processed through the shared processor assert!(core_processor.has_seen("Shared processor test")); } @@ -224,26 +222,26 @@ fn test_mock_network_processor_sharing_clone_to_original() { let hub = NetworkHub::new(); let identifier = random_identifier(); let mock_network = NetworkHub::new_mock_network(hub.clone(), identifier).unwrap(); - + // Clone the network let mock_network_clone = mock_network.clone(); - + // Register a processor on the CLONED network let core_processor = MockEventProcessor::new(); let processor = MessageProcessor::new(Box::new(core_processor.clone())); - + assert!(mock_network_clone.register_processor(processor).is_ok()); - + // Create an event to test with let event = TestMessage("Clone to original test".to_string()); - + // Verify the event hasn't been seen yet assert!(!core_processor.has_seen("Clone to original test")); - + // Send event through the ORIGINAL network - should work because processor is shared let origin_id = random_identifier(); assert!(mock_network.incoming_event(origin_id, event).is_ok()); - + // Verify the event was processed through the shared processor assert!(core_processor.has_seen("Clone to original test")); } @@ -256,26 +254,24 @@ fn test_event_processor_clone_functionality() { let processor1 = MessageProcessor::new(Box::new(core_processor.clone())); let processor2 = processor1.clone(); - // Create test events let event1 = TestMessage("Processor clone test 1".to_string()); - - let event2 = TestMessage("Processor clone test 2".to_string()); - + let event2 = TestMessage("Processor clone test 2".to_string()); + // Verify events haven't been seen yet assert!(!core_processor.has_seen("Processor clone test 1")); assert!(!core_processor.has_seen("Processor clone test 2")); - + // Process first event with first processor - let origin_id = random_identifier(); + let origin_id = random_identifier(); assert!(processor1.process_incoming_event(origin_id, event1).is_ok()); assert!(core_processor.has_seen("Processor clone test 1")); - + // Process second event with cloned processor assert!(processor2.process_incoming_event(origin_id, event2).is_ok()); assert!(core_processor.has_seen("Processor clone test 2")); - + // Both events should be visible from the shared state assert!(core_processor.has_seen("Processor clone test 1")); assert!(core_processor.has_seen("Processor clone test 2")); diff --git a/src/network/mod.rs b/src/network/mod.rs index 7fbda45..a81281b 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -1,7 +1,7 @@ pub mod mock; mod processor; -use crate::core::{Identifier, IdSearchReq, IdSearchRes}; +use crate::core::{IdSearchReq, IdSearchRes, Identifier}; #[allow(unused)] pub use processor::MessageProcessor; @@ -11,7 +11,7 @@ pub use processor::MessageProcessor; pub enum Event { TestMessage(String), // A payload for testing purposes, it is a simple string event, and is not used in production. SearchByIdRequest(IdSearchReq), // A payload representing an identifier search request. - SearchByIdResponse(IdSearchRes) // A payload representing an identifier search response. + SearchByIdResponse(IdSearchRes), // A payload representing an identifier search response. } /// Core event processing logic that implementations must provide. diff --git a/src/network/processor.rs b/src/network/processor.rs index 4f3052a..8a4bdf8 100644 --- a/src/network/processor.rs +++ b/src/network/processor.rs @@ -1,7 +1,7 @@ -use crate::network::{EventProcessorCore, Event}; +use crate::core::Identifier; +use crate::network::{Event, EventProcessorCore}; use parking_lot::RwLock; use std::sync::Arc; -use crate::core::Identifier; /// A thread-safe wrapper that enforces internal thread-safety for event processors. /// This type guarantees that all event processing is properly synchronized. @@ -19,7 +19,11 @@ impl MessageProcessor { } /// Process an incoming event with guaranteed thread-safety. - pub fn process_incoming_event(&self, origin_id: Identifier, event: Event) -> anyhow::Result<()> { + pub fn process_incoming_event( + &self, + origin_id: Identifier, + event: Event, + ) -> anyhow::Result<()> { let core = self.core.read(); core.process_incoming_event(origin_id, event) } @@ -50,7 +54,11 @@ mod tests { } impl EventProcessorCore for MockMessageProcessorCore { - fn process_incoming_event(&self, _origin_id: Identifier, _event: Event) -> anyhow::Result<()> { + fn process_incoming_event( + &self, + _origin_id: Identifier, + _event: Event, + ) -> anyhow::Result<()> { self.counter.fetch_add(1, Ordering::SeqCst); Ok(()) } @@ -70,12 +78,14 @@ mod tests { assert_eq!(counter_ref.load(Ordering::SeqCst), 0); let origin_id = random_identifier(); - processor.process_incoming_event(origin_id, test_event).unwrap(); + processor + .process_incoming_event(origin_id, test_event) + .unwrap(); assert_eq!(counter_ref.load(Ordering::SeqCst), 1); let origin_id2 = random_identifier(); let test_event2 = Event::TestMessage("test2".to_string()); - + processor_clone .process_incoming_event(origin_id2, test_event2) .unwrap(); diff --git a/src/node/base_node.rs b/src/node/base_node.rs index b00a69d..c57f824 100644 --- a/src/node/base_node.rs +++ b/src/node/base_node.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use crate::core::model::search::Nonce; use crate::core::{IdSearchReq, IdSearchRes, Identifier, IrrevocableContext, MembershipVector}; use crate::network::Event::{SearchByIdRequest, SearchByIdResponse}; #[cfg(test)] // TODO: Remove once BaseNode is used in production code. @@ -6,12 +6,12 @@ use crate::network::MessageProcessor; use crate::network::{Event, EventProcessorCore, Network}; use crate::node::core::Core; use anyhow::anyhow; +use std::collections::HashMap; use std::fmt; use std::fmt::Formatter; use std::sync::mpsc::sync_channel; use std::sync::{mpsc::SyncSender, Arc, Mutex}; use tracing::Span; -use crate::core::model::search::RequestId; // TODO: Remove #[allow(dead_code)] once BaseNode is used in production code. #[allow(dead_code)] @@ -28,7 +28,7 @@ pub(crate) struct BaseNode { span: Span, ctx: IrrevocableContext, // map from request id to the sender end of the channel for the response - request_id_map: Arc>>>, + request_id_map: Arc>>>, } impl BaseNode { @@ -69,58 +69,74 @@ impl BaseNode { /// Returns the node's identifier (delegated to core). #[allow(dead_code)] - pub(crate) fn id(&self) -> &Identifier { + pub(crate) fn id(&self) -> Identifier { self.core.id() } /// Returns the node's membership vector (delegated to core). #[allow(dead_code)] - pub(crate) fn mem_vec(&self) -> &MembershipVector { + pub(crate) fn mem_vec(&self) -> MembershipVector { self.core.mem_vec() } #[allow(dead_code)] - pub(crate) fn search_by_id(&self, req: &IdSearchReq) -> anyhow::Result { - let span = - tracing::trace_span!("search_by_id", target = ?req.target(), level = ?req.level()); + pub(crate) fn search_by_id(&self, req: IdSearchReq) -> anyhow::Result { + let span = tracing::trace_span!("search_by_id", target = ?req.target, level = ?req.level); let _enter = span.enter(); - tracing::trace!("searching for target {:?}", req.target()); + tracing::trace!("searching for target {:?}", req.target); let local_res = self .core .search_by_id(req) .map_err(|e| anyhow!("failed to perform search by id {}", e))?; - if local_res.result() == self.core.id() { + if local_res.result == self.core.id() { tracing::trace!("found self in search by id, terminating the search result"); return Ok(local_res); } let (tx, rx) = sync_channel::(1); { - let mut request_id_map = self.request_id_map.lock().expect("mutex was poisoned by a previous panic"); - request_id_map.insert(req.req_id(), tx); + let mut request_id_map = self + .request_id_map + .lock() + .expect("mutex was poisoned by a previous panic"); + request_id_map.insert(req.nonce, tx); } - let relay_request = SearchByIdRequest(IdSearchReq::new( - req.req_id(), - *self.core.id(), - *req.target(), - local_res.termination_level(), - req.direction(), - )); - if let Err(e) = self.net.send_event(*local_res.result(), relay_request) { - self.request_id_map.lock().expect("mutex was poisoned by a previous panic").remove(&req.req_id()); + let relay_request = SearchByIdRequest(IdSearchReq { + nonce: req.nonce, + target: req.target, + origin: self.core.id(), + level: local_res.termination_level, + direction: req.direction, + }); + + if let Err(e) = self.net.send_event(local_res.result, relay_request) { + self.request_id_map + .lock() + .expect("mutex was poisoned by a previous panic") + .remove(&req.nonce); return Err(anyhow!("failed to perform search by id {}", e)); } tracing::info!("relayed search by id request to the next node, pending response"); - let net_result = rx - .recv() - .map_err(|_| anyhow!("failed to receive response for search by id"))?; - tracing::info!( - "received network response for search by id {:?}: {:?}", - *req.target(), - net_result.result() - ); - Ok(net_result) + match rx.recv() { + Ok(net_result) => { + tracing::info!( + "received network response for search by id {:?}: {:?}", + req.target, + net_result.result + ); + Ok(net_result) + } + Err(_) => { + self.request_id_map + .lock() + .expect("mutex was poisoned by a previous panic") + .remove(&req.nonce); + Err(anyhow!( + "failed to receive network response for search by id" + )) + } + } } } @@ -133,28 +149,28 @@ impl EventProcessorCore for BaseNode { let span = tracing::trace_span!( "search_by_id_request", origin = ?origin_id, - target = ?req.target(), - direction = ?req.direction(), - level = ?req.level() + target = ?req.target, + direction = ?req.direction, + level = ?req.level ); let _enter = span.enter(); tracing::trace!("received request"); let res = self .core - .search_by_id(&req) + .search_by_id(req) .map_err(|e| anyhow!("failed to perform search by id {}", e))?; let span = tracing::trace_span!( "terminating", - result = ?res.result(), - termination_level = ?res.termination_level() + result = ?res.result, + termination_level = ?res.termination_level ); let _enter = span.enter(); - if res.result() == self.core.id() { + if res.result == self.core.id() { self.net - .send_event(*req.origin(), SearchByIdResponse(res)) + .send_event(req.origin, SearchByIdResponse(res)) .map_err(|e| { anyhow!("failed to send response event for search by id: {}", e) })?; @@ -162,15 +178,13 @@ impl EventProcessorCore for BaseNode { return Ok(()); } - let relay_request = SearchByIdRequest(IdSearchReq::new( - req.req_id(), - *req.origin(), - *req.target(), - res.termination_level(), - req.direction(), - )); + let relay_request = SearchByIdRequest(IdSearchReq { + level: res.termination_level, + ..req + }); + self.net - .send_event(*res.result(), relay_request) + .send_event(res.result, relay_request) .map_err(|e| { anyhow!( "failed to send relay response event for search by id: {}", @@ -184,16 +198,17 @@ impl EventProcessorCore for BaseNode { let span = tracing::trace_span!( "search_by_id_response", origin = ?origin_id, - target = ?res.target(), - result = ?res.result(), - termination_level = ?res.termination_level() + target = ?res.target, + result = ?res.result, + termination_level = ?res.termination_level ); let _enter = span.enter(); - let waiter = self.request_id_map + let waiter = self + .request_id_map .lock() .expect("mutex was poisoned by a previous panic") - .remove(&res.request_id()); + .remove(&res.nonce); if let Some(tx) = waiter { if let Err(e) = tx.send(res) { tracing::warn!("failed to send the response to the receiver end: {:?}", e) @@ -221,8 +236,8 @@ impl PartialEq for BaseNode { impl fmt::Debug for BaseNode { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.debug_struct("BaseNode") - .field("id", self.core.id()) - .field("mem_vec", self.core.mem_vec()) + .field("id", &self.core.id()) + .field("mem_vec", &self.core.mem_vec()) .finish() } } @@ -275,7 +290,7 @@ mod tests { )); let node = BaseNode::new(span.clone(), core, Box::new(mock_net)).unwrap(); - assert_eq!(node.id(), &id); - assert_eq!(node.mem_vec(), &mem_vec); + assert_eq!(node.id(), id); + assert_eq!(node.mem_vec(), mem_vec); } } diff --git a/src/node/core.rs b/src/node/core.rs index b3d3975..60cc652 100644 --- a/src/node/core.rs +++ b/src/node/core.rs @@ -15,21 +15,21 @@ use tracing::Span; /// `BaseNode`, which owns a `Box` and routes events to/from it. pub trait Core: Send + Sync { /// Returns the identifier of the node this core belongs to. - fn id(&self) -> &Identifier; + fn id(&self) -> Identifier; /// Returns the membership vector of the node this core belongs to. - fn mem_vec(&self) -> &MembershipVector; + fn mem_vec(&self) -> MembershipVector; /// Performs a local search for the given identifier in the lookup table /// in the direction and up to the level specified by the request. The /// result is the closest neighbor satisfying the directional constraint, /// or — if no such neighbor exists at any level — the caller's own /// identifier at level 0 (the Aspnes & Shah fallback). - fn search_by_id(&self, req: &IdSearchReq) -> anyhow::Result; + fn search_by_id(&self, req: IdSearchReq) -> anyhow::Result; /// Performs a local search for the given membership vector. #[allow(dead_code)] - fn search_by_mem_vec(&self, req: &IdSearchReq) -> anyhow::Result; + fn search_by_mem_vec(&self, req: IdSearchReq) -> anyhow::Result; /// Shallow-clones this core. Cloned instances share the same underlying /// state (lookup table, etc.) via Arc. @@ -87,28 +87,28 @@ impl Clone for BaseCore { } impl Core for BaseCore { - fn id(&self) -> &Identifier { - &self.id + fn id(&self) -> Identifier { + self.id } - fn mem_vec(&self) -> &MembershipVector { - &self.mem_vec + fn mem_vec(&self) -> MembershipVector { + self.mem_vec } - fn search_by_id(&self, req: &IdSearchReq) -> anyhow::Result { + fn search_by_id(&self, req: IdSearchReq) -> anyhow::Result { let span = tracing::trace_span!( parent: &self.span, "search_by_id_req", - target = ?req.target(), - dir = ?req.direction(), - level = ?req.level() + target = ?req.target, + dir = ?req.direction, + level = ?req.level ); let _enter = span.enter(); // Collect neighbors from levels <= req.level in req.direction - let candidates: Result, _> = (0..=req.level()) - .filter_map(|lvl| match self.lt.get_entry(lvl, req.direction()) { - Ok(Some(identity)) => Some(Ok((*identity.id(), lvl))), + let candidates: Result, _> = (0..=req.level) + .filter_map(|lvl| match self.lt.get_entry(lvl, req.direction) { + Ok(Some(identity)) => Some(Ok((identity.id(), lvl))), Ok(None) => None, Err(e) => Some(Err(anyhow!( "error while searching by id in level {}: {}", @@ -123,30 +123,35 @@ impl Core for BaseCore { tracing::trace!( "found {} candidates across levels 0-{}", candidates.len(), - req.level() + req.level ); // Filter candidates based on the direction - let result = match req.direction() { + let result = match req.direction { Direction::Left => { // smallest identifier that is >= target candidates .into_iter() - .filter(|(id, _)| id >= req.target()) + .filter(|(id, _)| id >= &req.target) .min_by_key(|(id, _)| *id) } Direction::Right => { // greatest identifier that is <= target candidates .into_iter() - .filter(|(id, _)| id <= req.target()) + .filter(|(id, _)| id <= &req.target) .max_by_key(|(id, _)| *id) } }; match result { Some((id, level)) => { - let search_result = IdSearchRes::new(req.req_id(), *req.target(), level, id); + let search_result = IdSearchRes { + nonce: req.nonce, + target: req.target, + termination_level: level, + result: id, + }; tracing::trace!("search successful: found match {:?} at level {}", id, level); Ok(search_result) } @@ -157,12 +162,17 @@ impl Core for BaseCore { "search fallback: no valid candidates found, returning own identifier {:?}", self.id ); - Ok(IdSearchRes::new(req.req_id(), *req.target(), 0, self.id)) + Ok(IdSearchRes { + nonce: req.nonce, + target: req.target, + termination_level: 0, + result: self.id, + }) } } } - fn search_by_mem_vec(&self, _req: &IdSearchReq) -> anyhow::Result { + fn search_by_mem_vec(&self, _req: IdSearchReq) -> anyhow::Result { todo!() } diff --git a/src/node/core_test.rs b/src/node/core_test.rs index 317bb98..fa98371 100644 --- a/src/node/core_test.rs +++ b/src/node/core_test.rs @@ -1,5 +1,6 @@ use crate::core::model::direction::Direction; use crate::core::model::identity::Identity; +use crate::core::model::search::Nonce; use crate::core::testutil::fixtures::{ join_all_with_timeout, random_address, random_identifier, random_identifier_greater_than, random_identifier_less_than, random_lookup_table_with_extremes, random_membership_vector, @@ -12,7 +13,6 @@ use crate::node::core::{BaseCore, Core}; use anyhow::anyhow; use rand::Rng; use std::sync::Arc; -use crate::core::model::search::RequestId; fn make_core(id: Identifier, lt: Box) -> BaseCore { BaseCore::new(span_fixture(), id, random_membership_vector(), lt) @@ -33,10 +33,16 @@ fn test_search_by_id_singleton_fallback() { ]; for (target, direction) in cases { - let req = IdSearchReq::new(RequestId::random(), origin_id, target, 3, direction); - let res = core.search_by_id(&req).expect("search failed"); - assert_eq!(res.termination_level(), 0); - assert_eq!(*res.result(), origin_id); + let req = IdSearchReq { + nonce: Nonce::random(), + origin: origin_id, + target, + level: 3, + direction, + }; + let res = core.search_by_id(req).expect("search failed"); + assert_eq!(res.termination_level, 0); + assert_eq!(res.result, origin_id); } } @@ -49,30 +55,32 @@ fn test_search_by_id_found_left_direction() { let safe_neighbor = random_identifier_greater_than(&target); lt.update_entry( - Identity::new( - &safe_neighbor, - &random_membership_vector(), - random_address(), - ), + Identity::new(safe_neighbor, random_membership_vector(), random_address()), 0, Direction::Left, ) .expect("failed to update entry in lookup table"); let core = make_core(random_identifier(), Box::new(lt.clone())); - let req = IdSearchReq::new(RequestId::random(), *core.id(), target, lvl, Direction::Left); - let actual = core.search_by_id(&req).unwrap(); + let req = IdSearchReq { + nonce: Nonce::random(), + origin: core.id(), + target, + level: lvl, + direction: Direction::Left, + }; + let actual = core.search_by_id(req).unwrap(); let (expected_lvl, expected_identity) = lt .left_neighbors() .unwrap() .into_iter() - .filter(|(l, id)| *l <= req.level() && id.id() >= req.target()) - .min_by_key(|(_, id)| *id.id()) + .filter(|(l, id)| *l <= req.level && id.id() >= req.target) + .min_by_key(|(_, id)| id.id()) .unwrap(); - assert_eq!(expected_lvl, actual.termination_level()); - assert_eq!(*expected_identity.id(), *actual.result()); + assert_eq!(expected_lvl, actual.termination_level); + assert_eq!(expected_identity.id(), actual.result); } } @@ -85,30 +93,32 @@ fn test_search_by_id_found_right_direction() { let safe_neighbor = random_identifier_less_than(&target); lt.update_entry( - Identity::new( - &safe_neighbor, - &random_membership_vector(), - random_address(), - ), + Identity::new(safe_neighbor, random_membership_vector(), random_address()), 0, Direction::Right, ) .expect("failed to update entry in lookup table"); let core = make_core(random_identifier(), Box::new(lt.clone())); - let req = IdSearchReq::new(RequestId::random(), *core.id(), target, lvl, Direction::Right); - let actual = core.search_by_id(&req).unwrap(); + let req = IdSearchReq { + nonce: Nonce::random(), + origin: core.id(), + target, + level: lvl, + direction: Direction::Right, + }; + let actual = core.search_by_id(req).unwrap(); let (expected_lvl, expected_identity) = lt .right_neighbors() .unwrap() .into_iter() - .filter(|(lvl, id)| *lvl <= req.level() && id.id() <= req.target()) - .max_by_key(|(_, id)| *id.id()) + .filter(|(lvl, id)| *lvl <= req.level && id.id() <= req.target) + .max_by_key(|(_, id)| id.id()) .unwrap(); - assert_eq!(expected_lvl, actual.termination_level()); - assert_eq!(*expected_identity.id(), *actual.result()); + assert_eq!(expected_lvl, actual.termination_level); + assert_eq!(expected_identity.id(), actual.result); } } @@ -123,8 +133,8 @@ fn test_search_by_id_not_found_left_direction() { for fill_lvl in 0..LOOKUP_TABLE_LEVELS { lt.update_entry( Identity::new( - &random_identifier_less_than(&target), - &random_membership_vector(), + random_identifier_less_than(&target), + random_membership_vector(), random_address(), ), fill_lvl, @@ -134,11 +144,17 @@ fn test_search_by_id_not_found_left_direction() { } let core = make_core(random_identifier(), Box::new(lt.clone())); - let req = IdSearchReq::new(RequestId::random(), *core.id(), target, lvl, Direction::Left); - let actual = core.search_by_id(&req).unwrap(); - - assert_eq!(actual.termination_level(), 0); - assert_eq!(*actual.result(), *core.id()); + let req = IdSearchReq { + nonce: Nonce::random(), + origin: core.id(), + target, + level: lvl, + direction: Direction::Left, + }; + let actual = core.search_by_id(req).unwrap(); + + assert_eq!(actual.termination_level, 0); + assert_eq!(actual.result, core.id()); } } @@ -153,8 +169,8 @@ fn test_search_by_id_not_found_right_direction() { for fill_lvl in 0..LOOKUP_TABLE_LEVELS { lt.update_entry( Identity::new( - &random_identifier_greater_than(&target), - &random_membership_vector(), + random_identifier_greater_than(&target), + random_membership_vector(), random_address(), ), fill_lvl, @@ -164,11 +180,17 @@ fn test_search_by_id_not_found_right_direction() { } let core = make_core(random_identifier(), Box::new(lt.clone())); - let req = IdSearchReq::new(RequestId::random(), *core.id(), target, lvl, Direction::Right); - let actual = core.search_by_id(&req).unwrap(); - - assert_eq!(actual.termination_level(), 0); - assert_eq!(*actual.result(), *core.id()); + let req = IdSearchReq { + nonce: Nonce::random(), + origin: core.id(), + target, + level: lvl, + direction: Direction::Right, + }; + let actual = core.search_by_id(req).unwrap(); + + assert_eq!(actual.termination_level, 0); + assert_eq!(actual.result, core.id()); } } @@ -183,11 +205,17 @@ fn test_search_by_id_exact_result() { for direction in [Direction::Left, Direction::Right] { let target_identity = lt.get_entry(lvl, direction).unwrap().unwrap(); let target = target_identity.id(); - let req = IdSearchReq::new(RequestId::random(), *core.id(), *target, lvl, direction); - let actual = core.search_by_id(&req).unwrap(); - - assert_eq!(actual.termination_level(), lvl); - assert_eq!(*actual.result(), *target); + let req = IdSearchReq { + nonce: Nonce::random(), + origin: core.id(), + target, + level: lvl, + direction, + }; + let actual = core.search_by_id(req).unwrap(); + + assert_eq!(actual.termination_level, lvl); + assert_eq!(actual.result, target); } } } @@ -200,7 +228,7 @@ fn test_search_by_id_concurrent_found_left_direction() { let target = random_identifier(); let core: Box = Box::new(make_core(random_identifier(), Box::new(lt.clone()))); - assert_ne!(&target, core.id()); + assert_ne!(target, core.id()); let num_threads = 20; let barrier = Arc::new(std::sync::Barrier::new(num_threads + 1)); @@ -212,24 +240,30 @@ fn test_search_by_id_concurrent_found_left_direction() { let handle = std::thread::spawn(move || { handle_barrier.wait(); let lvl = rand::rng().random_range(0..LOOKUP_TABLE_LEVELS); - let req = IdSearchReq::new(RequestId::random(), *core_ref.id(), target, lvl, Direction::Left); - let actual = core_ref.search_by_id(&req).unwrap(); + let req = IdSearchReq { + nonce: Nonce::random(), + origin: core_ref.id(), + target, + level: lvl, + direction: Direction::Left, + }; + let actual = core_ref.search_by_id(req).unwrap(); let expected = lt_clone .left_neighbors() .unwrap() .into_iter() - .filter(|(l, id)| *l <= req.level() && id.id() >= req.target()) - .min_by_key(|(_, id)| *id.id()); + .filter(|(l, id)| *l <= req.level && id.id() >= req.target) + .min_by_key(|(_, id)| id.id()); match expected { Some((expected_lvl, expected_identity)) => { - assert_eq!(expected_lvl, actual.termination_level()); - assert_eq!(*expected_identity.id(), *actual.result()); + assert_eq!(expected_lvl, actual.termination_level); + assert_eq!(expected_identity.id(), actual.result); } None => { - assert_eq!(actual.termination_level(), 0); - assert_eq!(*actual.result(), *core_ref.id()); + assert_eq!(actual.termination_level, 0); + assert_eq!(actual.result, core_ref.id()); } } }); @@ -249,7 +283,7 @@ fn test_search_by_id_concurrent_right_direction() { let target = random_identifier(); let core: Box = Box::new(make_core(random_identifier(), Box::new(lt.clone()))); - assert_ne!(&target, core.id()); + assert_ne!(target, core.id()); let num_threads = 20; let barrier = Arc::new(std::sync::Barrier::new(num_threads + 1)); @@ -261,24 +295,30 @@ fn test_search_by_id_concurrent_right_direction() { let handle = std::thread::spawn(move || { handle_barrier.wait(); let lvl = rand::rng().random_range(0..LOOKUP_TABLE_LEVELS); - let req = IdSearchReq::new(RequestId::random(), *core_ref.id(), target, lvl, Direction::Right); - let actual = core_ref.search_by_id(&req).unwrap(); + let req = IdSearchReq { + nonce: Nonce::random(), + origin: core_ref.id(), + target, + level: lvl, + direction: Direction::Right, + }; + let actual = core_ref.search_by_id(req).unwrap(); let expected = lt_clone .right_neighbors() .unwrap() .into_iter() - .filter(|(l, id)| *l <= req.level() && id.id() <= req.target()) - .max_by_key(|(_, id)| *id.id()); + .filter(|(l, id)| *l <= req.level && id.id() <= req.target) + .max_by_key(|(_, id)| id.id()); match expected { Some((expected_lvl, expected_identity)) => { - assert_eq!(expected_lvl, actual.termination_level()); - assert_eq!(*expected_identity.id(), *actual.result()); + assert_eq!(expected_lvl, actual.termination_level); + assert_eq!(expected_identity.id(), actual.result); } None => { - assert_eq!(actual.termination_level(), 0); - assert_eq!(*actual.result(), *core_ref.id()); + assert_eq!(actual.termination_level, 0); + assert_eq!(actual.result, core_ref.id()); } } }); @@ -337,8 +377,14 @@ fn test_search_by_id_error_propagation() { } let core = make_core(random_identifier(), Box::new(MockErrorLookupTable)); - let req = IdSearchReq::new(RequestId::random(), *core.id(), random_identifier(), 3, Direction::Left); - let result = core.search_by_id(&req); + let req = IdSearchReq { + nonce: Nonce::random(), + origin: core.id(), + target: random_identifier(), + level: 3, + direction: Direction::Left, + }; + let result = core.search_by_id(req); assert!( result.is_err(), diff --git a/src/node/search_by_id_test.rs b/src/node/search_by_id_test.rs index ca3ca9e..753a659 100644 --- a/src/node/search_by_id_test.rs +++ b/src/node/search_by_id_test.rs @@ -1,6 +1,7 @@ use super::base_node::BaseNode; use crate::core::model::direction::Direction; use crate::core::model::identity::Identity; +use crate::core::model::search::Nonce; use crate::core::testutil::fixtures::{ random_address, random_identifier, random_identifier_greater_than, random_lookup_table_with_extremes, random_membership_vector, span_fixture, @@ -10,7 +11,6 @@ use crate::network::{Event, EventProcessorCore, NetworkMock}; use crate::node::core::BaseCore; use std::sync::Arc; use unimock::*; -use crate::core::model::search::RequestId; /// Verifies the node, acting as an `EventProcessor`, relays an /// `IdSearchRequest` event to the expected neighbor via the network. @@ -23,26 +23,28 @@ fn test_search_by_id_networking_integration_relay() { // not self — forcing the relay branch. let safe_neighbor = random_identifier_greater_than(&target); lt.update_entry( - Identity::new( - &safe_neighbor, - &random_membership_vector(), - random_address(), - ), + Identity::new(safe_neighbor, random_membership_vector(), random_address()), 0, Direction::Left, ) .expect("failed to update entry in lookup table"); let node_id = random_identifier(); - let search_request = IdSearchReq::new(RequestId::random(), node_id, target, 0, Direction::Left); + let search_request = IdSearchReq { + nonce: Nonce::random(), + origin: node_id, + target, + level: 0, + direction: Direction::Left, + }; let request_event = Event::SearchByIdRequest(search_request); let (expected_lvl, expected_identity) = lt .left_neighbors() .unwrap() .into_iter() - .filter(|(l, id)| *l <= search_request.level() && id.id() >= search_request.target()) - .min_by_key(|(_, id)| *id.id()) + .filter(|(l, id)| *l <= search_request.level && id.id() >= search_request.target) + .min_by_key(|(_, id)| id.id()) .unwrap(); let mock_net = Unimock::new(( @@ -54,8 +56,8 @@ fn test_search_by_id_networking_integration_relay() { .answers_arc(Arc::new( move |_, id: Identifier, event: Event| match event { Event::SearchByIdRequest(req) => { - assert_eq!(req.level(), expected_lvl); - assert_eq!(id, *expected_identity.id()); + assert_eq!(req.level, expected_lvl); + assert_eq!(id, expected_identity.id()); Ok(()) } _ => panic!("expected IdSearchRequest payload, got: {:?}", event), @@ -91,7 +93,13 @@ fn test_search_by_id_networking_integration_target_is_this_node() { let origin_id = random_identifier(); let node_id = random_identifier(); - let search_request = IdSearchReq::new(RequestId::random(), origin_id, node_id, 0, Direction::Left); + let search_request = IdSearchReq { + nonce: Nonce::random(), + origin: origin_id, + target: node_id, + level: 0, + direction: Direction::Left, + }; let request_event = Event::SearchByIdRequest(search_request); let mock_net = Unimock::new(( @@ -108,8 +116,7 @@ fn test_search_by_id_networking_integration_target_is_this_node() { "expected result to be to the originator's identifier" ); assert_eq!( - *res.result(), - node_id, + res.result, node_id, "expected result to be the node's identifier" ); Ok(()) diff --git a/src/node/skip_graph_integration_test.rs b/src/node/skip_graph_integration_test.rs index 2b15a0e..0aadc23 100644 --- a/src/node/skip_graph_integration_test.rs +++ b/src/node/skip_graph_integration_test.rs @@ -1,9 +1,15 @@ use super::base_node::BaseNode; use crate::core::model::direction::Direction; use crate::core::model::identity::Identity; -use crate::core::testutil::fixtures::{join_all_with_timeout, join_with_timeout, random_membership_vector, random_sorted_identifiers, span_fixture}; -use crate::core::{Address, ArrayLookupTable, IdSearchReq, Identifier, LookupTable, MembershipVector, LOOKUP_TABLE_LEVELS}; -use crate::core::model::search::RequestId; +use crate::core::model::search::Nonce; +use crate::core::testutil::fixtures::{ + join_all_with_timeout, join_with_timeout, random_membership_vector, random_sorted_identifiers, + span_fixture, +}; +use crate::core::{ + Address, ArrayLookupTable, IdSearchReq, Identifier, LookupTable, MembershipVector, + LOOKUP_TABLE_LEVELS, +}; use crate::network::mock::hub::NetworkHub; use crate::network::Network; use crate::node::core::BaseCore; @@ -96,7 +102,7 @@ impl LocalSkipGraph { } } - let mvs = nodes.iter().map(|n| *n.mem_vec()).collect(); + let mvs = nodes.iter().map(|n| n.mem_vec()).collect(); Ok(LocalSkipGraph { nodes, lts, @@ -118,13 +124,13 @@ fn test_lookup_tables_validity() { // find the max j < i: common_prefix_bit(m_i, m_j) ≥ level let expected_left: Option = (0..i) .rev() - .find(|&j| sg.mvs[i].common_prefix_bit(&sg.mvs[j]) >= level); + .find(|&j| sg.mvs[i].common_prefix_bit(sg.mvs[j]) >= level); - let expected_left_neighbor_id = expected_left.map(|j| *sg.nodes[j].id()); + let expected_left_neighbor_id = expected_left.map(|j| sg.nodes[j].id()); let actual_left_neighbor_id = lt .get_entry(level, Direction::Left) .expect("get_entry should never error") - .map(|identity| *identity.id()); + .map(|identity| identity.id()); assert_eq!( actual_left_neighbor_id, expected_left_neighbor_id, "left lookup table entry is not valid" @@ -132,12 +138,12 @@ fn test_lookup_tables_validity() { // find the min j > i: common_prefix_bit(m_i, m_j) >= level let expected_right: Option = - (i + 1..sg.nodes.len()).find(|&j| sg.mvs[i].common_prefix_bit(&sg.mvs[j]) >= level); - let expected_right_neighbor_id = expected_right.map(|j| *sg.nodes[j].id()); + (i + 1..sg.nodes.len()).find(|&j| sg.mvs[i].common_prefix_bit(sg.mvs[j]) >= level); + let expected_right_neighbor_id = expected_right.map(|j| sg.nodes[j].id()); let actual_right_neighbor_id = lt .get_entry(level, Direction::Right) .expect("get_entry should never error") - .map(|identity| *identity.id()); + .map(|identity| identity.id()); assert_eq!( actual_right_neighbor_id, expected_right_neighbor_id, "right lookup table entry is not valid" @@ -167,9 +173,17 @@ fn test_skip_graph_search_by_id() { let target_id = sg.identifiers[7]; let handle = std::thread::spawn(move || { - let id_search_req = IdSearchReq::new(RequestId::random(), *origin_node.id(), target_id, LOOKUP_TABLE_LEVELS - 1, Direction::Right); - let result = origin_node.search_by_id(&id_search_req).expect("failed to search by id"); - assert_eq!(*result.result(), target_id); + let id_search_req = IdSearchReq { + nonce: Nonce::random(), + target: target_id, + origin: origin_node.id(), + level: LOOKUP_TABLE_LEVELS - 1, + direction: Direction::Right, + }; + let result = origin_node + .search_by_id(id_search_req) + .expect("failed to search by id"); + assert_eq!(result.result, target_id); }); join_with_timeout(handle, std::time::Duration::from_secs(10)) @@ -186,14 +200,24 @@ fn test_skip_graph_search_by_id_concurrent() { let origin_node = origin_node.clone(); let target_id = sg.identifiers[i]; let handle = std::thread::spawn(move || { - let id_search_req = IdSearchReq::new(RequestId::random(), *origin_node.id(), target_id, LOOKUP_TABLE_LEVELS - 1, Direction::Right); - let result = origin_node.search_by_id(&id_search_req).expect("failed to search by id"); - assert_eq!(*result.result(), target_id); + let id_search_req = IdSearchReq { + nonce: Nonce::random(), + target: target_id, + origin: origin_node.id(), + level: LOOKUP_TABLE_LEVELS - 1, + direction: Direction::Right, + }; + let result = origin_node + .search_by_id(id_search_req) + .expect("failed to search by id"); + assert_eq!(result.result, target_id); }); handles.push(handle); } - join_all_with_timeout(handles.into_boxed_slice(), std::time::Duration::from_secs(10)) - .expect("search_by_id did not complete within timeout (likely deadlocked)"); + join_all_with_timeout( + handles.into_boxed_slice(), + std::time::Duration::from_secs(10), + ) + .expect("search_by_id did not complete within timeout (likely deadlocked)"); } - diff --git a/tests/test_debug_hex_format.rs b/tests/test_debug_hex_format.rs index c0db429..2be3a35 100644 --- a/tests/test_debug_hex_format.rs +++ b/tests/test_debug_hex_format.rs @@ -1,48 +1,64 @@ -use skipgraph::core::{Identifier, MembershipVector, Address}; use skipgraph::core::model::identity::Identity; +use skipgraph::core::{Address, Identifier, MembershipVector}; #[test] fn test_identifier_debug_shows_hex_not_raw_bytes() { - - let id_bytes = [69, 11, 103, 102, 141, 75, 166, 128, 3, 116, 40, 7, 102, 211, 4, 44, 26, 234, 34, 150, 21, 99, 236, 216, 142, 116, 70, 33, 143, 133, 174, 83]; + let id_bytes = [ + 69, 11, 103, 102, 141, 75, 166, 128, 3, 116, 40, 7, 102, 211, 4, 44, 26, 234, 34, 150, 21, + 99, 236, 216, 142, 116, 70, 33, 143, 133, 174, 83, + ]; let identifier = Identifier::from_bytes(&id_bytes).unwrap(); - + let debug_output = format!("{:?}", identifier); let expected_hex = hex::encode(id_bytes); - + // Should show hex format, not raw byte array assert_eq!(debug_output, expected_hex); - assert_eq!(debug_output, "450b67668d4ba6800374280766d3042c1aea22961563ecd88e7446218f85ae53"); - + assert_eq!( + debug_output, + "450b67668d4ba6800374280766d3042c1aea22961563ecd88e7446218f85ae53" + ); } #[test] fn test_membership_vector_debug_shows_hex_not_raw_bytes() { - let mem_vec_bytes = [81, 77, 192, 82, 129, 173, 87, 224, 233, 181, 134, 100, 170, 151, 59, 27, 241, 80, 96, 46, 54, 235, 57, 31, 97, 14, 136, 195, 63, 101, 157, 240]; + let mem_vec_bytes = [ + 81, 77, 192, 82, 129, 173, 87, 224, 233, 181, 134, 100, 170, 151, 59, 27, 241, 80, 96, 46, + 54, 235, 57, 31, 97, 14, 136, 195, 63, 101, 157, 240, + ]; let mem_vec = MembershipVector::from_bytes(&mem_vec_bytes).unwrap(); - + let debug_output = format!("{:?}", mem_vec); let expected_hex = hex::encode(mem_vec_bytes); - + // Should show hex format, not raw byte array assert_eq!(debug_output, expected_hex); - assert_eq!(debug_output, "514dc05281ad57e0e9b58664aa973b1bf150602e36eb391f610e88c33f659df0"); + assert_eq!( + debug_output, + "514dc05281ad57e0e9b58664aa973b1bf150602e36eb391f610e88c33f659df0" + ); } #[test] fn test_identity_debug_shows_hex_format_for_components() { - let id_bytes = [69, 11, 103, 102, 141, 75, 166, 128, 3, 116, 40, 7, 102, 211, 4, 44, 26, 234, 34, 150, 21, 99, 236, 216, 142, 116, 70, 33, 143, 133, 174, 83]; - let mem_vec_bytes = [81, 77, 192, 82, 129, 173, 87, 224, 233, 181, 134, 100, 170, 151, 59, 27, 241, 80, 96, 46, 54, 235, 57, 31, 97, 14, 136, 195, 63, 101, 157, 240]; - + let id_bytes = [ + 69, 11, 103, 102, 141, 75, 166, 128, 3, 116, 40, 7, 102, 211, 4, 44, 26, 234, 34, 150, 21, + 99, 236, 216, 142, 116, 70, 33, 143, 133, 174, 83, + ]; + let mem_vec_bytes = [ + 81, 77, 192, 82, 129, 173, 87, 224, 233, 181, 134, 100, 170, 151, 59, 27, 241, 80, 96, 46, + 54, 235, 57, 31, 97, 14, 136, 195, 63, 101, 157, 240, + ]; + let identifier = Identifier::from_bytes(&id_bytes).unwrap(); let mem_vec = MembershipVector::from_bytes(&mem_vec_bytes).unwrap(); let address = Address::new("localhost", "8080"); - let identity = Identity::new(&identifier, &mem_vec, address); - + let identity = Identity::new(identifier, mem_vec, address); + let debug_output = format!("{:?}", identity); let expected_id_hex = hex::encode(id_bytes); let expected_mem_vec_hex = hex::encode(mem_vec_bytes); - + // Should contain hex representations of both identifier and membership vector assert_eq!( debug_output, @@ -52,26 +68,30 @@ fn test_identity_debug_shows_hex_format_for_components() { ) ); - assert_eq!(debug_output, - "Identity { \ + assert_eq!( + debug_output, + "Identity { \ id: 450b67668d4ba6800374280766d3042c1aea22961563ecd88e7446218f85ae53, \ mem_vec: 514dc05281ad57e0e9b58664aa973b1bf150602e36eb391f610e88c33f659df0, \ address: localhost:8080 \ - }"); - + }" + ); } #[test] fn test_debug_format_consistency_with_display() { // Test that Debug format matches Display format for individual components - let id_bytes = [255, 0, 128, 64, 32, 16, 8, 4, 2, 1, 0, 255, 128, 64, 32, 16, 8, 4, 2, 1, 255, 0, 128, 64, 32, 16, 8, 4, 2, 1, 0, 255]; + let id_bytes = [ + 255, 0, 128, 64, 32, 16, 8, 4, 2, 1, 0, 255, 128, 64, 32, 16, 8, 4, 2, 1, 255, 0, 128, 64, + 32, 16, 8, 4, 2, 1, 0, 255, + ]; let identifier = Identifier::from_bytes(&id_bytes).unwrap(); let mem_vec = MembershipVector::from_bytes(&id_bytes).unwrap(); - + // Debug format should match Display format assert_eq!(format!("{:?}", identifier), format!("{}", identifier)); assert_eq!(format!("{:?}", mem_vec), format!("{}", mem_vec)); - + // Both should be hex encoded let expected_hex = hex::encode(id_bytes); assert_eq!(format!("{:?}", identifier), expected_hex); @@ -86,17 +106,26 @@ fn test_various_byte_patterns_show_as_hex() { let all_zeros = [0u8; 32]; let id_zeros = Identifier::from_bytes(&all_zeros).unwrap(); let debug_zeros = format!("{:?}", id_zeros); - assert_eq!(debug_zeros, "0000000000000000000000000000000000000000000000000000000000000000"); - + assert_eq!( + debug_zeros, + "0000000000000000000000000000000000000000000000000000000000000000" + ); + // Test edge cases: all 255s let all_255s = [255u8; 32]; let id_255s = Identifier::from_bytes(&all_255s).unwrap(); let debug_255s = format!("{:?}", id_255s); - assert_eq!(debug_255s, "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); - + assert_eq!( + debug_255s, + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + ); + // Test mixed pattern - let mixed = [0, 255, 128, 64, 32, 16, 8, 4, 2, 1, 170, 85, 240, 15, 204, 51, 0, 255, 128, 64, 32, 16, 8, 4, 2, 1, 170, 85, 240, 15, 204, 51]; + let mixed = [ + 0, 255, 128, 64, 32, 16, 8, 4, 2, 1, 170, 85, 240, 15, 204, 51, 0, 255, 128, 64, 32, 16, 8, + 4, 2, 1, 170, 85, 240, 15, 204, 51, + ]; let id_mixed = Identifier::from_bytes(&mixed).unwrap(); let debug_mixed = format!("{:?}", id_mixed); assert_eq!(debug_mixed, hex::encode(mixed)); -} \ No newline at end of file +}