From 27039807de47fbc2af589f67c5a3f78f0087e228 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Wed, 17 Sep 2025 01:10:34 +0800 Subject: [PATCH] Add tests --- ds/map/btree/test.ha | 15 +++++++++++++++ ds/map/fnv/test.ha | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++ ds/map/hashmap/test.ha | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++++ ds/map/rbtree/test.ha | 10 ++++++++++ ds/map/siphash/test.ha | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++ ds/map/slice_basic/test.ha | 10 ++++++++++ ds/map/slice_sorted/test.ha | 10 ++++++++++ ds/map/swiss_siphash/test.ha | 25 +++++++++++++++++++++++++ ds/map/test.ha | 181 +++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/ds/map/btree/test.ha b/ds/map/btree/test.ha new file mode 100644 index 0000000000000000000000000000000000000000..1538ead639a6f44a267a11af3732848d1a5a1a85 --- /dev/null +++ b/ds/map/btree/test.ha @@ -0,0 +1,15 @@ +use ds::map; +use errors; + +@test fn test() void = { + const ts: [3]size = [2z, 3z, 8z]; + for (let i = 0z; i < len(ts); i += 1) { + let m: *map = match (new(ts[i])) { + case let p: *map => yield p; + case errors::invalid => abort("btree: invalid t"); + case nomem => abort("btree: nomem"); + }; + defer finish(m); + map::stress_test(m, 200000); + }; +}; diff --git a/ds/map/fnv/test.ha b/ds/map/fnv/test.ha new file mode 100644 index 0000000000000000000000000000000000000000..304e6f50e851807af9513bd10cc6eb5bb8a85c28 --- /dev/null +++ b/ds/map/fnv/test.ha @@ -0,0 +1,64 @@ +use crypto::random; +use ds::map; +use ds::map::slice_basic; +use ds::map::slice_sorted; +use ds::map::btree; +use ds::map::rbtree; +use ds::map::swiss_siphash; +use errors; + +fn mk_slice_basic() (*map::map | nomem) = { + match (slice_basic::new()) { + case let p: *slice_basic::map => return (p: *map::map); + case nomem => return nomem; + }; +}; + +fn mk_slice_sorted() (*map::map | nomem) = { + match (slice_sorted::new()) { + case let p: *slice_sorted::map => return (p: *map::map); + case nomem => return nomem; + }; +}; + +fn mk_btree2() (*map::map | nomem) = { + match (btree::new(2)) { + case let p: *btree::map => return (p: *map::map); + case errors::invalid => abort("btree(2) invalid"); + case nomem => return nomem; + }; +}; + +fn mk_rbtree() (*map::map | nomem) = { + match (rbtree::new()) { + case let p: *rbtree::map => return (p: *map::map); + case nomem => return nomem; + }; +}; + +fn mk_swiss() (*map::map | nomem) = { + let key: [16]u8 = [0...]; + random::buffer(&key); + match (swiss_siphash::new(1, key)) { + case let p: *swiss_siphash::map => return (p: *map::map); + case errors::invalid => abort("swiss: invalid"); + case nomem => return nomem; + }; +}; + +@test fn test() void = { + const buckets: [2]size = [128z, 256z]; + const makers: [5]*fn() (*map::map | nomem) = [&mk_slice_basic, &mk_slice_sorted, &mk_btree2, &mk_rbtree, &mk_swiss]; + + for (let bi = 0z; bi < len(buckets); bi += 1) { + for (let mi = 0z; mi < len(makers); mi += 1) { + let m: *map = match (new(makers[mi], buckets[bi])) { + case let p: *map => yield p; + case errors::invalid => abort("fnv: invalid"); + case nomem => abort("fnv: nomem"); + }; + defer finish(m); + map::stress_test(m, 20000); + }; + }; +}; diff --git a/ds/map/hashmap/test.ha b/ds/map/hashmap/test.ha new file mode 100644 index 0000000000000000000000000000000000000000..e58216e3da7995b6567473d61bf689ee0f9dc7a9 --- /dev/null +++ b/ds/map/hashmap/test.ha @@ -0,0 +1,90 @@ +use crypto::random; +use ds::map; +use ds::map::slice_basic; +use ds::map::slice_sorted; +use ds::map::btree; +use ds::map::rbtree; +use ds::map::swiss_siphash; +use errors; +use hash; +use hash::fnv; +use hash::siphash; + +fn test_fnv64(_params: nullable *opaque, key: []u8) size = { + let h = fnv::fnv64a(); + hash::write(&h, key); + return fnv::sum64(&h): size; +}; + +fn test_siphash64(params: nullable *opaque, key: []u8) size = { + let k = match (params) { + case null => abort("siphash test hash: missing key"); + case let p: *opaque => yield (p: *[16]u8); + }; + let h = siphash::siphash(2, 4, k); + defer hash::close(&h); + hash::write(&h, key); + return siphash::sum(&h): size; +}; + +fn mk_slice_basic() (*map::map | nomem) = { + match (slice_basic::new()) { + case let p: *slice_basic::map => return (p: *map::map); + case nomem => return nomem; + }; +}; +fn mk_slice_sorted() (*map::map | nomem) = { + match (slice_sorted::new()) { + case let p: *slice_sorted::map => return (p: *map::map); + case nomem => return nomem; + }; +}; +fn mk_btree2() (*map::map | nomem) = { + match (btree::new(2)) { + case let p: *btree::map => return (p: *map::map); + case errors::invalid => abort("btree(2) invalid"); + case nomem => return nomem; + }; +}; +fn mk_rbtree() (*map::map | nomem) = { + match (rbtree::new()) { + case let p: *rbtree::map => return (p: *map::map); + case nomem => return nomem; + }; +}; +fn mk_swiss() (*map::map | nomem) = { + let key: [16]u8 = [0...]; + random::buffer(&key); + match (swiss_siphash::new(1, key)) { + case let p: *swiss_siphash::map => return (p: *map::map); + case errors::invalid => abort("swiss: invalid"); + case nomem => return nomem; + }; +}; + +@test fn test() void = { + const buckets: [2]size = [128z, 256z]; + const makers: [5]*fn() (*map::map | nomem) = [&mk_slice_basic, &mk_slice_sorted, &mk_btree2, &mk_rbtree, &mk_swiss]; + + let skey1: [16]u8 = [0...]; + let skey2: [16]u8 = [0...]; + random::buffer(&skey1); + random::buffer(&skey2); + const hf: [2]*fn(hash_params: nullable *opaque, key: []u8) size = + [&test_fnv64, &test_siphash64]; + const hp: [2]nullable *opaque = [null, (&skey1: *opaque)]; + + for (let bi = 0z; bi < len(buckets); bi += 1) { + for (let hi = 0z; hi < len(hf); hi += 1) { + for (let mi = 0z; mi < len(makers); mi += 1) { + let m: *map = match (new(makers[mi], buckets[bi], hf[hi], hp[hi])) { + case let p: *map => yield p; + case errors::invalid => abort("hashmap: invalid"); + case nomem => abort("hashmap: nomem"); + }; + defer finish(m); + map::stress_test(m, 20000); + }; + }; + }; +}; diff --git a/ds/map/rbtree/test.ha b/ds/map/rbtree/test.ha new file mode 100644 index 0000000000000000000000000000000000000000..e5a2168cf6d7925c7a2a8dfaf2a694957b00809f --- /dev/null +++ b/ds/map/rbtree/test.ha @@ -0,0 +1,10 @@ +use ds::map; + +@test fn test() void = { + let m: *map = match (new()) { + case let p: *map => yield p; + case nomem => abort("rbtree: nomem"); + }; + defer finish(m); + map::stress_test(m, 200000); +}; diff --git a/ds/map/siphash/test.ha b/ds/map/siphash/test.ha new file mode 100644 index 0000000000000000000000000000000000000000..189844106b7869d53cd985277b5f8fd4e494f711 --- /dev/null +++ b/ds/map/siphash/test.ha @@ -0,0 +1,72 @@ +use crypto::random; +use ds::map; +use ds::map::slice_basic; +use ds::map::slice_sorted; +use ds::map::btree; +use ds::map::rbtree; +use ds::map::swiss_siphash; +use errors; + +fn mk_slice_basic() (*map::map | nomem) = { + match (slice_basic::new()) { + case let p: *slice_basic::map => return (p: *map::map); + case nomem => return nomem; + }; +}; + +fn mk_slice_sorted() (*map::map | nomem) = { + match (slice_sorted::new()) { + case let p: *slice_sorted::map => return (p: *map::map); + case nomem => return nomem; + }; +}; + +fn mk_btree2() (*map::map | nomem) = { + match (btree::new(2)) { + case let p: *btree::map => return (p: *map::map); + case errors::invalid => abort("btree(2) invalid"); + case nomem => return nomem; + }; +}; + +fn mk_rbtree() (*map::map | nomem) = { + match (rbtree::new()) { + case let p: *rbtree::map => return (p: *map::map); + case nomem => return nomem; + }; +}; + +fn mk_swiss() (*map::map | nomem) = { + let key: [16]u8 = [0...]; + random::buffer(&key); + match (swiss_siphash::new(1, key)) { + case let p: *swiss_siphash::map => return (p: *map::map); + case errors::invalid => abort("swiss: invalid"); + case nomem => return nomem; + }; +}; + +@test fn test() void = { + const buckets: [2]size = [128z, 256z]; + const makers: [5]*fn() (*map::map | nomem) = [&mk_slice_basic, &mk_slice_sorted, &mk_btree2, &mk_rbtree, &mk_swiss]; + + let key1: [16]u8 = [0...]; + let key2: [16]u8 = [0...]; + random::buffer(&key1); + random::buffer(&key2); + const keys: [2]*[16]u8 = [&key1, &key2]; + + for (let bi = 0z; bi < len(buckets); bi += 1) { + for (let ki = 0z; ki < len(keys); ki += 1) { + for (let mi = 0z; mi < len(makers); mi += 1) { + let m: *map = match (new(makers[mi], buckets[bi], *keys[ki])) { + case let p: *map => yield p; + case errors::invalid => abort("siphash: invalid"); + case nomem => abort("siphash: nomem"); + }; + defer finish(m); + map::stress_test(m, 20000); + }; + }; + }; +}; diff --git a/ds/map/slice_basic/test.ha b/ds/map/slice_basic/test.ha new file mode 100644 index 0000000000000000000000000000000000000000..fdad8e79c680678abe6836a5c5837d6d46451004 --- /dev/null +++ b/ds/map/slice_basic/test.ha @@ -0,0 +1,10 @@ +use ds::map; + +@test fn test() void = { + let m: *map = match (new()) { + case let p: *map => yield p; + case nomem => abort("slice_basic: nomem"); + }; + defer finish(m); + map::stress_test(m, 2000); +}; diff --git a/ds/map/slice_sorted/test.ha b/ds/map/slice_sorted/test.ha new file mode 100644 index 0000000000000000000000000000000000000000..a83ea53b445b367c814aec38d5145b3b7b5daed0 --- /dev/null +++ b/ds/map/slice_sorted/test.ha @@ -0,0 +1,10 @@ +use ds::map; + +@test fn test() void = { + let m: *map = match (new()) { + case let p: *map => yield p; + case nomem => abort("slice_sorted: nomem"); + }; + defer finish(m); + map::stress_test(m, 1000); +}; diff --git a/ds/map/swiss_siphash/test.ha b/ds/map/swiss_siphash/test.ha new file mode 100644 index 0000000000000000000000000000000000000000..61f1ef0d0dd6a735a4acac7b2871b462f15c48de --- /dev/null +++ b/ds/map/swiss_siphash/test.ha @@ -0,0 +1,25 @@ +use crypto::random; +use ds::map; +use errors; + +@test fn test() void = { + let key1: [16]u8 = [0...]; + let key2: [16]u8 = [0...]; + random::buffer(&key1); + random::buffer(&key2); + + const groups: [2]size = [1z, 32z]; + const keys: [2]*[16]u8 = [&key1, &key2]; + + for (let gi = 0z; gi < len(groups); gi += 1) { + for (let ki = 0z; ki < len(keys); ki += 1) { + let m: *map = match (new(groups[gi], *keys[ki])) { + case let p: *map => yield p; + case errors::invalid => abort("swiss_siphash: invalid groups"); + case nomem => abort("swiss_siphash: nomem"); + }; + defer finish(m); + map::stress_test(m, 200000); + }; + }; +}; diff --git a/ds/map/test.ha b/ds/map/test.ha new file mode 100644 index 0000000000000000000000000000000000000000..5789d1c04581ae4b021eef3aba1f250078bc06d4 --- /dev/null +++ b/ds/map/test.ha @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: MPL-2.0 + +def KEY_LEN: size = 16z; + +fn put_le64(dst: []u8, x: u64) void = { + for (let i = 0z; i < 8z; i += 1) { + dst[i] = ((x >> (8u64 * (i: u64))) & 0xFFu64): u8; + }; +}; + +type oracle = []nullable *opaque; + +fn must_set(m: *map, key: []u8, v: *opaque) void = { + match (set(m, key, v)) { + case void => void; + case nomem => abort("set: out of memory"); + }; +}; +fn must_get(m: *map, key: []u8) (*opaque | void) = get(m, key); +fn must_del(m: *map, key: []u8) (*opaque | void) = del(m, key); + +export fn stress_test(m: *map, key_space: size) void = { + let empty: [KEY_LEN]u8 = [0...]; + let keybufs: [][KEY_LEN]u8 = alloc([empty...], key_space)!; + let keys: [][]u8 = alloc([[0...]...], key_space)!; + + for (let i = 0z; i < key_space; i += 1) { + for (let j = 8z; j < KEY_LEN; j += 1) keybufs[i][j] = 0xABu8; + put_le64((keybufs[i][..8]), (i: u64)); + keys[i] = keybufs[i][..]; + }; + + let vals: []int = alloc([0...], key_space)!; + for (let i = 0z; i < key_space; i += 1) vals[i] = (i: int); + + let exp: oracle = alloc([null...], key_space)!; + + // Sequential inserts with immediate verification + for (let i = 0z; i < key_space; i += 1) { + let vp: *opaque = (&vals[i]: *opaque); + must_set(m, keys[i], vp); + exp[i] = vp; + + match (must_get(m, keys[i])) { + case let got: *opaque => assert(got == vp, "phase1: get != set"); + case void => abort("phase1: get void after set"); + }; + }; + + // Forward read sweep (all should be present) + for (let i = 0z; i < key_space; i += 1) { + match (must_get(m, keys[i])) { + case let got: *opaque => + let want = match (exp[i]) { + case null => abort("phase2: expect null but found value"); + case let p: *opaque => yield p; + }; + assert(got == want, "phase2: value mismatch"); + case void => + abort("phase2: get void but expect value"); + }; + }; + + // Strided overwrites with immediate verification + for (let step = 0z; step < key_space; step += 1) { + let i = (step * 7z) % key_space; + let new_ix = (i + 12345z) % key_space; + let vp: *opaque = (&vals[new_ix]: *opaque); + must_set(m, keys[i], vp); + exp[i] = vp; + + match (must_get(m, keys[i])) { + case let got: *opaque => assert(got == vp, "phase3: replace mismatch"); + case void => abort("phase3: get void after replace"); + }; + }; + + // Sparse deletes, every 3rd key + for (let i = 0z; i < key_space; i += 1) { + if (i % 3z != 0z) continue; + + let want = exp[i]; + match (must_del(m, keys[i])) { + case let ret: *opaque => + match (want) { + case null => abort("phase4: del returned value but expect null"); + case let p: *opaque => assert(ret == p, "phase4: del value mismatch"); + }; + exp[i] = null; + + match (must_del(m, keys[i])) { + case void => void; + case *opaque => abort("phase4: second delete returned value"); + }; + case void => + match (want) { + case null => void; + case *opaque => abort("phase4: del void but expect value"); + }; + }; + }; + + // Insert again every 6th key + for (let i = 0z; i < key_space; i += 1) { + if (i % 6z != 0z) continue; + let vp: *opaque = (&vals[(i * 5z + 1z) % key_space]: *opaque); + must_set(m, keys[i], vp); + exp[i] = vp; + }; + + // Even indices read, odd indices write + for (let i = 0z; i < key_space; i += 1) { + if ((i & 1z) == 0z) { + // Read check + match (must_get(m, keys[i])) { + case let got: *opaque => + match (exp[i]) { + case null => abort("phase6: even get found value; expect null"); + case let p: *opaque => assert(got == p, "phase6: even mismatch"); + }; + case void => + match (exp[i]) { + case null => void; + case *opaque => abort("phase6: even get void; expect value"); + }; + }; + } else { + // Write (overwrite or create) + let vp: *opaque = (&vals[(i * 3z + 7z) % key_space]: *opaque); + must_set(m, keys[i], vp); + exp[i] = vp; + }; + }; + + // Reading in reverse order + for (let r = key_space; r > 0z; r -= 1) { + let i = r - 1z; + match (must_get(m, keys[i])) { + case let got: *opaque => + match (exp[i]) { + case null => abort("phase7: get found value; expect null"); + case let p: *opaque => assert(got == p, "phase7: reverse mismatch"); + }; + case void => + match (exp[i]) { + case null => void; + case *opaque => abort("phase7: reverse get void; expect value"); + }; + }; + }; + + // Clear in reverse order + for (let r = key_space; r > 0z; r -= 1) { + let i = r - 1z; + match (must_del(m, keys[i])) { + case let ret: *opaque => + match (exp[i]) { + case null => abort("phase8: del returned value; expect null"); + case let p: *opaque => assert(ret == p, "phase8: final del mismatch"); + }; + exp[i] = null; + case void => + match (exp[i]) { + case null => void; + case *opaque => abort("phase8: del void; expect value"); + }; + }; + }; + + // Read sweep ensure empty + for (let i = 0z; i < key_space; i += 1) { + match (must_get(m, keys[i])) { + case void => void; + case *opaque => abort("final: get returned value after full clear"); + }; + match (must_del(m, keys[i])) { + case void => void; + case *opaque => abort("final: del returned value after full clear"); + }; + }; +}; -- 2.48.1