/* * The fixed-capacity hash map and FNV-1a -- src/collections.c, TODO.md 3.6. * * "The single most obviously-missing data structure in the library", by the * TODO's own account: akbasic needed one three times over -- variables, * functions and labels -- and wrote ~130 lines of open-addressed table to get it. * * The behaviour worth testing hardest is the tombstone. Deleting a key that * something else probed past has to leave a marker rather than an empty slot, * because an empty slot ends a probe chain and would make the later key * unreachable while it is still sitting there. */ #include "aksl_capture.h" #define CAP 16 typedef struct SeenLog { int count; char keys[CAP][AKSL_HASHMAP_MAX_KEY]; int break_at; } SeenLog; static akerr_ErrorContext AKERR_NOIGNORE *record_entry(const char *key, void *value, void *data) { SeenLog *log = NULL; PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, key, AKERR_NULLPOINTER, "key"); FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data"); (void)value; log = (SeenLog *)data; if ( log->count < CAP ) { snprintf(log->keys[log->count], AKSL_HASHMAP_MAX_KEY, "%s", key); } log->count += 1; if ( log->break_at == log->count - 1 ) { FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop"); } SUCCEED_RETURN(e); } /* ---------------------------------------------------------------------- */ /* FNV-1a */ /* ---------------------------------------------------------------------- */ /* * The reference values are the canonical 32-bit FNV-1a ones: h = 2166136261, * then h = (h XOR byte) * 16777619 for each byte. "a" and "foobar" are the * vectors from the FNV reference material. */ static int test_fnv1a_known_answers(void) { uint32_t h = 0; AKSL_CHECK_OK(aksl_strhash_fnv1a("", 0, &h)); AKSL_CHECK(h == 2166136261u); AKSL_CHECK_OK(aksl_strhash_fnv1a("a", 1, &h)); AKSL_CHECK(h == 0xe40c292cu); AKSL_CHECK_OK(aksl_strhash_fnv1a("foobar", 6, &h)); AKSL_CHECK(h == 0xbf9cf968u); /* The NUL-terminated form agrees with the length-driven one. */ AKSL_CHECK_OK(aksl_strhash_fnv1a_str("foobar", &h)); AKSL_CHECK(h == 0xbf9cf968u); AKSL_CHECK_STATUS(aksl_strhash_fnv1a(NULL, 0, &h), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_strhash_fnv1a("a", 1, NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_strhash_fnv1a_str(NULL, &h), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_strhash_fnv1a_str("a", NULL), AKERR_NULLPOINTER); return 0; } /* High bytes are unsigned here for the same reason they are in djb2. */ static int test_fnv1a_high_bit_bytes_are_unsigned(void) { char buf[2] = { (char)0xff, (char)0xfe }; uint32_t h = 0; uint32_t expected = 2166136261u; expected = (expected ^ 0xffu) * 16777619u; expected = (expected ^ 0xfeu) * 16777619u; AKSL_CHECK_OK(aksl_strhash_fnv1a(buf, sizeof(buf), &h)); AKSL_CHECK(h == expected); return 0; } /* ---------------------------------------------------------------------- */ /* Put and get */ /* ---------------------------------------------------------------------- */ static int test_put_and_get(void) { aksl_HashEntry slots[CAP]; aksl_HashMap map; int a = 1; int b = 2; void *value = NULL; int found = 99; AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP)); AKSL_CHECK(map.count == 0); AKSL_CHECK(map.capacity == CAP); AKSL_CHECK_OK(aksl_hashmap_put(&map, "alpha", &a)); AKSL_CHECK_OK(aksl_hashmap_put(&map, "beta", &b)); AKSL_CHECK(map.count == 2); AKSL_CHECK_OK(aksl_hashmap_get(&map, "alpha", &value, &found)); AKSL_CHECK(found == 1); AKSL_CHECK(value == &a); AKSL_CHECK_OK(aksl_hashmap_get(&map, "beta", &value, &found)); AKSL_CHECK(found == 1); AKSL_CHECK(value == &b); /* A key that is not there is found = 0 and success, not an error: looking * something up and not finding it is the ordinary case for a symbol table. */ value = (void *)0x1; AKSL_CHECK_OK(aksl_hashmap_get(&map, "gamma", &value, &found)); AKSL_CHECK(found == 0); AKSL_CHECK(value == (void *)0x1); /* untouched when the key is absent */ /* NULL value out-param is allowed: sometimes you only want to know if it is there. */ AKSL_CHECK_OK(aksl_hashmap_get(&map, "alpha", NULL, &found)); AKSL_CHECK(found == 1); return 0; } /* Putting an existing key replaces the value rather than adding a second entry. */ static int test_put_replaces_an_existing_key(void) { aksl_HashEntry slots[CAP]; aksl_HashMap map; int a = 1; int b = 2; void *value = NULL; int found = 0; AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP)); AKSL_CHECK_OK(aksl_hashmap_put(&map, "key", &a)); AKSL_CHECK_OK(aksl_hashmap_put(&map, "key", &b)); AKSL_CHECK(map.count == 1); AKSL_CHECK_OK(aksl_hashmap_get(&map, "key", &value, &found)); AKSL_CHECK(found == 1); AKSL_CHECK(value == &b); return 0; } /* The map copies keys into its slots, so it owns them and cannot outlive them. */ static int test_the_map_owns_its_keys(void) { aksl_HashEntry slots[CAP]; aksl_HashMap map; char key[16] = "transient"; int a = 1; int found = 0; AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP)); AKSL_CHECK_OK(aksl_hashmap_put(&map, key, &a)); /* Scribble over the caller's copy of the key. */ memset(key, 'Z', sizeof(key) - 1); AKSL_CHECK_OK(aksl_hashmap_get(&map, "transient", NULL, &found)); AKSL_CHECK(found == 1); return 0; } /* ---------------------------------------------------------------------- */ /* Bounds */ /* ---------------------------------------------------------------------- */ /* * Full is an error naming the capacity, not a silent resize. A table that * reallocates is a table whose entry pointers move underneath anything holding * one; this one has a worst case you can state. */ static int test_a_full_map_refuses_rather_than_resizing(void) { aksl_HashEntry slots[4]; aksl_HashMap map; char key[16]; int i = 0; AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4)); for ( i = 0; i < 4; i++ ) { snprintf(key, sizeof(key), "k%d", i); AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL)); } AKSL_CHECK(map.count == 4); AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_hashmap_put(&map, "overflow", NULL), AKERR_OUTOFBOUNDS, "does not resize"); AKSL_CHECK(map.count == 4); /* Everything already in there is still reachable and still correct. */ for ( i = 0; i < 4; i++ ) { int found = 0; snprintf(key, sizeof(key), "k%d", i); AKSL_CHECK_OK(aksl_hashmap_get(&map, key, NULL, &found)); AKSL_CHECK(found == 1); } return 0; } /* * A key too long for a slot is refused rather than truncated. Truncation would * silently collide two different keys that share a prefix, which is the worst * possible failure for a symbol table -- a lookup that returns the wrong thing. */ static int test_an_oversized_key_is_refused(void) { aksl_HashEntry slots[CAP]; aksl_HashMap map; char longkey[AKSL_HASHMAP_MAX_KEY + 8]; int found = 0; AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP)); memset(longkey, 'k', sizeof(longkey) - 1); longkey[sizeof(longkey) - 1] = '\0'; AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_hashmap_put(&map, longkey, NULL), AKERR_OUTOFBOUNDS, "AKSL_HASHMAP_MAX_KEY"); AKSL_CHECK_STATUS(aksl_hashmap_get(&map, longkey, NULL, &found), AKERR_OUTOFBOUNDS); AKSL_CHECK_STATUS(aksl_hashmap_remove(&map, longkey, NULL), AKERR_OUTOFBOUNDS); /* One byte short of the limit is fine: it is a limit, not an off-by-one. */ longkey[AKSL_HASHMAP_MAX_KEY - 1] = '\0'; AKSL_CHECK_OK(aksl_hashmap_put(&map, longkey, NULL)); AKSL_CHECK_OK(aksl_hashmap_get(&map, longkey, NULL, &found)); AKSL_CHECK(found == 1); return 0; } /* ---------------------------------------------------------------------- */ /* Removal and tombstones */ /* ---------------------------------------------------------------------- */ static int test_remove(void) { aksl_HashEntry slots[CAP]; aksl_HashMap map; int a = 1; int found = 99; int removed = 99; AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP)); AKSL_CHECK_OK(aksl_hashmap_put(&map, "alpha", &a)); AKSL_CHECK_OK(aksl_hashmap_remove(&map, "alpha", &removed)); AKSL_CHECK(removed == 1); AKSL_CHECK(map.count == 0); AKSL_CHECK_OK(aksl_hashmap_get(&map, "alpha", NULL, &found)); AKSL_CHECK(found == 0); /* Removing what is not there is success with removed = 0. */ AKSL_CHECK_OK(aksl_hashmap_remove(&map, "alpha", &removed)); AKSL_CHECK(removed == 0); /* The out-param is optional. */ AKSL_CHECK_OK(aksl_hashmap_remove(&map, "nothing", NULL)); return 0; } /* * The tombstone case, which is the one an open-addressed table gets wrong. * * A tiny capacity forces collisions, so several keys share a probe chain. * Deleting one from the middle of that chain must not cut the keys behind it * loose: if the slot were simply emptied, the probe for a later key would stop * there and report the key missing while it was still sitting in the table. */ static int test_removal_does_not_break_a_probe_chain(void) { aksl_HashEntry slots[4]; aksl_HashMap map; char key[16]; int found = 0; int i = 0; AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4)); for ( i = 0; i < 4; i++ ) { snprintf(key, sizeof(key), "k%d", i); AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL)); } /* Take out each key in turn and confirm every remaining one is still found. */ for ( i = 0; i < 4; i++ ) { int j = 0; snprintf(key, sizeof(key), "k%d", i); AKSL_CHECK_OK(aksl_hashmap_remove(&map, key, NULL)); for ( j = i + 1; j < 4; j++ ) { snprintf(key, sizeof(key), "k%d", j); AKSL_CHECK_OK(aksl_hashmap_get(&map, key, NULL, &found)); AKSL_CHECK(found == 1); } } AKSL_CHECK(map.count == 0); return 0; } /* A tombstone is reusable, so a table churned through repeatedly does not fill up. */ static int test_tombstones_are_reused(void) { aksl_HashEntry slots[4]; aksl_HashMap map; char key[16]; int i = 0; AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4)); /* Far more insert/remove cycles than there are slots. */ for ( i = 0; i < 64; i++ ) { snprintf(key, sizeof(key), "cycle%d", i); AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL)); AKSL_CHECK_OK(aksl_hashmap_remove(&map, key, NULL)); } AKSL_CHECK(map.count == 0); /* Still usable afterwards. */ AKSL_CHECK_OK(aksl_hashmap_put(&map, "final", NULL)); AKSL_CHECK(map.count == 1); return 0; } /* ---------------------------------------------------------------------- */ /* Iteration */ /* ---------------------------------------------------------------------- */ static int test_iterate_visits_every_live_entry(void) { aksl_HashEntry slots[CAP]; aksl_HashMap map; SeenLog log; char key[16]; int i = 0; AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP)); for ( i = 0; i < 5; i++ ) { snprintf(key, sizeof(key), "k%d", i); AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL)); } /* One removed, so there is a tombstone for the walk to skip. */ AKSL_CHECK_OK(aksl_hashmap_remove(&map, "k2", NULL)); memset((void *)&log, 0x00, sizeof(log)); log.break_at = -1; AKSL_CHECK_OK(aksl_hashmap_iterate(&map, &record_entry, &log)); AKSL_CHECK(log.count == 4); /* The break stops it, as everywhere else in this library. */ memset((void *)&log, 0x00, sizeof(log)); log.break_at = 1; AKSL_CHECK_OK(aksl_hashmap_iterate(&map, &record_entry, &log)); AKSL_CHECK(log.count == 2); return 0; } static int test_rejects_null_and_uninitialised(void) { aksl_HashEntry slots[CAP]; aksl_HashMap map; aksl_HashMap empty; SeenLog log; int found = 0; memset((void *)&empty, 0x00, sizeof(empty)); memset((void *)&log, 0x00, sizeof(log)); AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP)); AKSL_CHECK_STATUS(aksl_hashmap_init(NULL, slots, CAP), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_hashmap_init(&map, NULL, CAP), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_hashmap_init(&map, slots, 0), AKERR_VALUE); AKSL_CHECK_STATUS(aksl_hashmap_put(NULL, "k", NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_hashmap_put(&map, NULL, NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_hashmap_get(NULL, "k", NULL, &found), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_hashmap_get(&map, NULL, NULL, &found), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_hashmap_get(&map, "k", NULL, NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_hashmap_remove(NULL, "k", NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_hashmap_remove(&map, NULL, NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_hashmap_iterate(NULL, &record_entry, &log), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_hashmap_iterate(&map, NULL, &log), AKERR_NULLPOINTER); /* A map that was never initialised is caught rather than dereferenced. */ AKSL_CHECK_STATUS(aksl_hashmap_put(&empty, "k", NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_hashmap_get(&empty, "k", NULL, &found), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_hashmap_remove(&empty, "k", NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_hashmap_iterate(&empty, &record_entry, &log), AKERR_NULLPOINTER); return 0; } int main(void) { int failures = 0; akerr_init(); AKSL_RUN(failures, test_fnv1a_known_answers); AKSL_RUN(failures, test_fnv1a_high_bit_bytes_are_unsigned); AKSL_RUN(failures, test_put_and_get); AKSL_RUN(failures, test_put_replaces_an_existing_key); AKSL_RUN(failures, test_the_map_owns_its_keys); AKSL_RUN(failures, test_a_full_map_refuses_rather_than_resizing); AKSL_RUN(failures, test_an_oversized_key_is_refused); AKSL_RUN(failures, test_remove); AKSL_RUN(failures, test_removal_does_not_break_a_probe_chain); AKSL_RUN(failures, test_tombstones_are_reused); AKSL_RUN(failures, test_iterate_visits_every_live_entry); AKSL_RUN(failures, test_rejects_null_and_uninitialised); AKSL_REPORT(failures); }