Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test([kh]amt): improve test coverage #2124

Merged
merged 3 commits into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 238 additions & 3 deletions ipld/hamt/tests/hamt_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,76 @@ impl Drop for CidChecker {
fn test_basics(factory: HamtFactory) {
let store = MemoryBlockstore::default();
let mut hamt = factory.new(&store);
hamt.set(1, "world".to_string()).unwrap();

hamt.set(1, "world".to_string()).unwrap();
assert_eq!(hamt.get(&1).unwrap(), Some(&"world".to_string()));
assert!(hamt.contains_key(&1).unwrap());

hamt.set(1, "world2".to_string()).unwrap();
assert_eq!(hamt.get(&1).unwrap(), Some(&"world2".to_string()));
assert!(hamt.contains_key(&1).unwrap());

assert_eq!(hamt.get(&2).unwrap(), None);
assert!(!hamt.contains_key(&2).unwrap());
}

fn test_n_keys(factory: HamtFactory) {
const KEY_LEN: usize = 32;
let store = MemoryBlockstore::default();
// Test increasing numbers of sequential keys.
fn key(j: u64) -> BytesKey {
let mut k = [0; KEY_LEN];
let encoded = j.to_be_bytes();
k[(KEY_LEN - encoded.len())..].copy_from_slice(&encoded[..]);
BytesKey(k.to_vec())
}

for do_flush in [true, false] {
for i in 0..=300 {
let mut hamt: Hamt<_, _, BytesKey> = factory.new(&store);
let k_too_big = key(i + 1);
for j in 0..i {
// Maybe try flushing/reloading (clearing the cache and/or dirty bits).
if do_flush {
if j == i / 3 {
// Flush but don't reload.
hamt.flush().unwrap();
} else if j == (2 * i) / 3 {
// Flush and reload.
let new_root = hamt.flush().unwrap();
hamt.set(key(0), "yolo".to_string()).unwrap();
hamt.set_root(&new_root).unwrap();
}
}

let k = key(j);
hamt.set(k, format!("{j}")).unwrap();
}

// Fail to get an item out of range.
assert_eq!(hamt.get(&k_too_big).unwrap(), None);

// Make sure we get what we expect after reloading.
let root = hamt.flush().unwrap();
let new_hamt = factory.load(&root, &store).unwrap();
assert_eq!(hamt, new_hamt);

// And the items are the same.
let old_items = hamt.iter().collect::<Result<Vec<_>, _>>().unwrap();
let new_items = new_hamt.iter().collect::<Result<Vec<_>, _>>().unwrap();
assert_eq!(old_items, new_items);

// And we still fail to get an item out of range.
assert_eq!(new_hamt.get(&k_too_big).unwrap(), None);

// Assert we can independently look up every key when load a fresh hamt.
for j in 0..i {
let hamt: Hamt<_, _, BytesKey> = factory.load(&root, &store).unwrap();
let k = key(j);
assert_eq!(hamt.get(&k).unwrap(), Some(&format!("{j}")));
}
}
}
}

fn test_load(factory: HamtFactory) {
Expand Down Expand Up @@ -180,21 +245,26 @@ fn test_set_root(factory: HamtFactory) {
let mut hamt: Hamt<_, _, usize> = factory.new(&store);
hamt.set(1, "world".to_string()).unwrap();

// Record a hamt root with one entry.
assert_eq!(hamt.get(&1).unwrap(), Some(&"world".to_string()));
let c1 = hamt.flush().unwrap();

// Record a second hamt root with 2 entries.
hamt.set(2, "world2".to_string()).unwrap();
assert_eq!(hamt.get(&2).unwrap(), Some(&"world2".to_string()));

let c2 = hamt.flush().unwrap();

// Re-load the original hamt with one entry.
let mut new_hamt: Hamt<_, String, usize> = factory.load(&c1, &store).unwrap();
assert_eq!(new_hamt.get(&1).unwrap(), Some(&"world".to_string()));
assert_eq!(new_hamt.get(&2).unwrap(), None);

// Try to update it to the new hamt by setting its root manually.
new_hamt.set_root(&c2).unwrap();
assert_eq!(new_hamt.get(&2).unwrap(), Some(&"world2".to_string()));

// Flush the new hamt and make sure it matches the root we just set.
let c3 = new_hamt.flush().unwrap();
assert_eq!(c2, c3);
}
Expand Down Expand Up @@ -275,6 +345,7 @@ fn delete(factory: HamtFactory, stats: Option<BSStats>, mut cids: CidChecker) {
hamt.set(tstring("bar"), tstring("cat dog")).unwrap();
hamt.set(tstring("baz"), tstring("cat")).unwrap();
assert!(hamt.get(&tstring("foo")).unwrap().is_some());
assert!(hamt.contains_key(&tstring("foo")).unwrap());

let c = hamt.flush().unwrap();
cids.check_next(c);
Expand All @@ -283,6 +354,10 @@ fn delete(factory: HamtFactory, stats: Option<BSStats>, mut cids: CidChecker) {
assert!(h2.get(&b"foo".to_vec()).unwrap().is_some());
assert!(h2.delete(&b"foo".to_vec()).unwrap().is_some());
assert_eq!(h2.get(&b"foo".to_vec()).unwrap(), None);
assert!(!h2.contains_key(&b"foo".to_vec()).unwrap());

// Delete a non-existent key
assert!(h2.delete(&b"nonexistent".to_vec()).unwrap().is_none());

let c2 = h2.flush().unwrap();
cids.check_next(c2);
Expand Down Expand Up @@ -362,6 +437,7 @@ fn set_delete_many(
// Ensure first size_factor keys still exist
for i in 0..size_factor {
assert_eq!(hamt.get(&tstring(i)).unwrap(), Some(&tstring(i)));
assert!(hamt.contains_key(&tstring(i)).unwrap());
}

let cid_d = hamt.flush().unwrap();
Expand All @@ -370,6 +446,7 @@ fn set_delete_many(
// Assert that we can empty it.
for i in 0..size_factor {
assert!(hamt.delete(&tstring(i)).unwrap().is_some());
assert!(!hamt.contains_key(&tstring(i)).unwrap());
}

assert_eq!(hamt.iter().count(), 0);
Expand Down Expand Up @@ -435,6 +512,58 @@ fn for_each(
.unwrap();
assert_eq!(count, size_factor);

{
let c = hamt.flush().unwrap();
cids.check_next(c);
}

// Iterate with a few modified nodes.
if size_factor > 10 {
hamt.set(tstring(10), tstring("modified-10")).unwrap();
}
if size_factor > 80 {
hamt.set(tstring(80), tstring("modified-80")).unwrap();
hamt.set(tstring(81), tstring("modified-81")).unwrap();
}
if size_factor > 30 {
assert!(hamt.delete(&tstring(30)).unwrap().is_some());
}

// Delete a non-existent value
assert!(hamt.delete(&tstring(size_factor + 100)).unwrap().is_none());

// Iterate and verify modifications
let mut count = 0;
hamt.for_each(|k, v| {
if size_factor > 30 {
// Should not see deleted key
assert_ne!(k, &tstring(30));
}

if size_factor > 10 && k == &tstring(10) {
assert_eq!(v, &tstring("modified-10"));
} else if size_factor > 80 && k == &tstring(80) {
assert_eq!(v, &tstring("modified-80"));
} else if size_factor > 80 && k == &tstring(81) {
assert_eq!(v, &tstring("modified-81"));
} else if k != &tstring(30) {
// Normal key-value equality except for modified keys
assert_eq!(k, v);
}

count += 1;
Ok(())
})
.unwrap();

// Verify count matches expectation: original size - deleted + new entries
let expected_count = if size_factor > 30 {
size_factor - 1
} else {
size_factor
};
assert_eq!(count, expected_count);

let c = hamt.flush().unwrap();
cids.check_next(c);

Expand Down Expand Up @@ -614,11 +743,86 @@ fn for_each_ranged(
let c = hamt.flush().unwrap();
cids.check_next(c);

// Test modifications and deletions in ranged iteration
if size_factor > 10 {
hamt.set(tstring(10), size_factor + 10).unwrap();
}
if size_factor > 30 {
assert!(hamt.delete(&tstring(30)).unwrap().is_some());
}

// Verify modified content during ranged iteration
let mut kvs_after_mod = Vec::new();
hamt.for_each_ranged::<BytesKey, _>(None, None, |k, v| {
if size_factor > 10 && k == &tstring(10) {
assert_eq!(*v, size_factor + 10);
}
if size_factor > 30 {
assert_ne!(k, &tstring(30));
}
kvs_after_mod.push((k.clone(), *v));
Ok(())
})
.unwrap();

// Expected count after modifications
let expected_count = if size_factor > 30 {
size_factor - 1
} else {
size_factor
};
assert_eq!(kvs_after_mod.len(), expected_count);

let c = hamt.flush().unwrap();
cids.check_next(c);

if let Some(stats) = stats {
assert_eq!(*store.stats.borrow(), stats);
}
}

fn clear(factory: HamtFactory, mut cids: CidChecker) {
let store = MemoryBlockstore::default();
let mut hamt = factory.new(&store);

// Verify the HAMT is initially empty
assert!(hamt.is_empty());

// Call clear on an already empty HAMT
hamt.clear();

// Verify it is still empty
assert!(hamt.is_empty());

// Insert some entries into the HAMT
hamt.set(1, "a".to_string()).unwrap();
hamt.set(2, "b".to_string()).unwrap();

// Verify the entries exist
assert_eq!(hamt.get(&1).unwrap(), Some(&"a".to_string()));
assert_eq!(hamt.get(&2).unwrap(), Some(&"b".to_string()));

// Verify the HAMT is not empty
assert!(!hamt.is_empty());

// Clear the HAMT
hamt.clear();

// Verify the HAMT is empty
assert!(hamt.is_empty());

// Verify previous entries are gone
assert_eq!(hamt.get(&1).unwrap(), None);
assert_eq!(hamt.get(&2).unwrap(), None);

// Ensure subsequent operations still work
hamt.set(3, "c".to_string()).unwrap();
assert_eq!(hamt.get(&3).unwrap(), Some(&"c".to_string()));

let c = hamt.flush().unwrap();
cids.check_next(c);
}

#[cfg(feature = "identity")]
fn add_and_remove_keys(
bit_width: u32,
Expand Down Expand Up @@ -649,6 +853,7 @@ fn add_and_remove_keys(

for (k, v) in all {
assert_eq!(Some(&v), h1.get(&k).unwrap());
assert!(h1.contains_key(&k).unwrap());
}

// Set and delete extra keys
Expand Down Expand Up @@ -975,6 +1180,11 @@ mod test_default {
super::test_basics(HamtFactory::default())
}

#[test]
fn test_n_keys() {
super::test_n_keys(HamtFactory::default())
}

#[test]
fn test_load() {
super::test_load(HamtFactory::default())
Expand Down Expand Up @@ -1055,21 +1265,23 @@ mod test_default {
#[test]
fn for_each() {
#[rustfmt::skip]
let stats = BSStats {r: 30, w: 30, br: 3209, bw: 3209};
let stats = BSStats {r: 30, w: 33, br: 3209, bw: 4697};
let cids = CidChecker::new(vec![
"bafy2bzaceczhz54xmmz3xqnbmvxfbaty3qprr6dq7xh5vzwqbirlsnbd36z7a",
"bafy2bzaceczhz54xmmz3xqnbmvxfbaty3qprr6dq7xh5vzwqbirlsnbd36z7a",
"bafy2bzacebln5j7tdfavh2qqhio6mgaoq6mm2jbmcex2ngcmi3uqlx5k3mov4",
]);
super::for_each(200, HamtFactory::default(), Some(stats), cids);
}

#[test]
fn for_each_ranged() {
#[rustfmt::skip]
let stats = BSStats {r: 30, w: 30, br: 2895, bw: 2895};
let stats = BSStats {r: 30, w: 33, br: 2895, bw: 4321};
let cids = CidChecker::new(vec![
"bafy2bzacedy4ypl2vedhdqep3llnwko6vrtfiys5flciz2f3c55pl4whlhlqm",
"bafy2bzacedy4ypl2vedhdqep3llnwko6vrtfiys5flciz2f3c55pl4whlhlqm",
"bafy2bzacecrxqeuuk34jsrol3azfpgy35ldw4bpgsa6pdxvzj262jzpvoekye",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new root at the end of the test (there are now three checks in the test). We have the CID checks there to avoid unintentional changes.

]);
super::for_each_ranged(200, HamtFactory::default(), Some(stats), cids);
}
Expand All @@ -1085,6 +1297,14 @@ mod test_default {
super::clean_child_ordering(HamtFactory::default(), Some(stats), cids);
}

#[test]
fn test_clear() {
let cids = CidChecker::new(vec![
"bafy2bzaceagvm62vt2eiholjosl6hiii2t4zaxjax65bcqicnb4y4qjhexz4y",
]);
super::clear(HamtFactory::default(), cids);
}

#[test]
fn test_hamtv0() {
let config = Config {
Expand Down Expand Up @@ -1139,11 +1359,21 @@ macro_rules! test_hamt_mod {
super::test_basics($factory)
}

#[test]
fn test_n_keys() {
super::test_n_keys($factory)
}

#[test]
fn test_load() {
super::test_load($factory)
}

#[test]
fn test_set_root() {
super::test_set_root($factory)
}

#[test]
fn test_set_if_absent() {
super::test_set_if_absent($factory, None, CidChecker::empty())
Expand Down Expand Up @@ -1190,6 +1420,11 @@ macro_rules! test_hamt_mod {
}
}

#[test]
fn clear() {
super::clear($factory, CidChecker::empty());
}

#[test]
fn clean_child_ordering() {
super::clean_child_ordering($factory, None, CidChecker::empty())
Expand Down
Loading
Loading