diff --git a/cubestore/src/lib.rs b/cubestore/src/lib.rs index 7f26b88..9096887 100644 --- a/cubestore/src/lib.rs +++ b/cubestore/src/lib.rs @@ -151,7 +151,6 @@ pub struct CubeStore { /// encoding is used to avoid a serde dependency at Package 1.) mod record_codec { use cubecoords::{CubeHeader, Czyx}; - use std::collections::HashMap; // Compact, dependency-free encoding of the header. // Fields are written in a fixed tag-length-value stream so unknown @@ -356,12 +355,6 @@ mod record_codec { let s = String::from_utf8(rest[..n].to_vec()).ok()?; Some((s, &rest[n..])) } - - /// Keep `HashMap` referenced so the dependency is explicit in this module - /// even though the codec itself is generic over bytes. (Prevents an - /// unused-import warning if the backend type changes.) - #[allow(dead_code)] - pub(crate) fn _assert_backend_assoc(_: &HashMap>) {} } impl CubeStore { @@ -867,12 +860,106 @@ mod tests { // The path flag bit is set so the record is also flag-discoverable. let by_path_flag = store.scan_by_flag(cubecoords::HeaderFlags::HAS_PATH); assert_eq!(by_path_flag.len(), 4); + } - // query_by_path_prefix returns decoded payloads. - let full = store.query_by_path_prefix("/etc"); - assert_eq!(full.len(), 3); - assert!(full - .iter() - .all(|(_, h, _)| h.path.as_deref().unwrap().starts_with("/etc"))); + // ---- HashBackend trait-level tests (PDF spec: HashMap>) ---- + + #[test] + fn hash_backend_put_get_delete_roundtrip() { + let mut b = HashBackend::new(); + let k = Czyx::new(10, 20, 30, 40); + b.put(k, b"payload".to_vec()); + assert_eq!(b.get(&k), Some(b"payload".to_vec())); + b.delete(&k); + assert_eq!(b.get(&k), None); + } + + #[test] + fn hash_backend_overwrite_replaces_value() { + let mut b = HashBackend::new(); + let k = Czyx::new(0, 1, 2, 3); + b.put(k, b"old".to_vec()); + b.put(k, b"new".to_vec()); + assert_eq!(b.get(&k), Some(b"new".to_vec())); + } + + #[test] + fn hash_backend_keys_sorted_order() { + let mut b = HashBackend::new(); + for c in 1u8..=3 { + b.put(Czyx::new(50, c, 10, 1), vec![c as u8]); + } + let keys = b.keys(); + assert_eq!(keys.len(), 3); + // HashMap iteration is unordered; keys() must sort for stable output. + let mut sorted = keys.clone(); + sorted.sort(); + assert_eq!(keys, sorted); + for k in &keys { + assert_eq!(k.c, 50); + } + } + + #[test] + fn hash_backend_scan_prefix_filters_correctly() { + let mut b = HashBackend::new(); + b.put(Czyx::new(1, 2, 3, 4), b"a".to_vec()); + b.put(Czyx::new(1, 9, 9, 9), b"b".to_vec()); + b.put(Czyx::new(2, 0, 0, 0), b"c".to_vec()); + b.put(Czyx::new(1, 2, 9, 9), b"d".to_vec()); + + // C=1 only. + let c1 = b.scan_prefix(1, None, None); + assert_eq!(c1.len(), 3); + for k in &c1 { + assert_eq!(k.c, 1); + } + + // C=1, Z=2 only. + let c1z2 = b.scan_prefix(1, Some(2), None); + assert_eq!(c1z2.len(), 2); + for k in &c1z2 { + assert_eq!(k.c, 1); + assert_eq!(k.z, 2); + } + + // C=2 only. + let c2 = b.scan_prefix(2, None, None); + assert_eq!(c2, vec![Czyx::new(2, 0, 0, 0)]); + + // Non-existent C. + assert!(b.scan_prefix(99, None, None).is_empty()); + } + + #[test] + fn hash_backend_empty_keys_and_scan() { + let b = HashBackend::new(); + assert!(b.keys().is_empty()); + assert!(b.scan_prefix(0, None, None).is_empty()); + } + + #[test] + fn hash_backend_null_coord_is_valid_key() { + let mut b = HashBackend::new(); + let null = Czyx::new(0, 0, 0, 0); + b.put(null, b"null-cube".to_vec()); + assert_eq!(b.get(&null), Some(b"null-cube".to_vec())); + b.delete(&null); + assert_eq!(b.get(&null), None); + + // Null cube (0,0,0,0) is distinct from a nearby coord. + let near = Czyx::new(0, 0, 0, 1); + b.put(near, b"near".to_vec()); + assert_eq!(b.get(&near), Some(b"near".to_vec())); + assert_eq!(b.get(&null), None); + } + + #[test] + fn hash_backend_multiple_backends_are_independent() { + let b1 = HashBackend::new(); + let mut b2 = HashBackend::new(); + b2.put(Czyx::new(1, 1, 1, 1), b"x".to_vec()); + assert!(b1.get(&Czyx::new(1, 1, 1, 1)).is_none()); + assert_eq!(b2.get(&Czyx::new(1, 1, 1, 1)), Some(b"x".to_vec())); } }