/** * @file symtab.c * @brief Tests the fixed-capacity open-addressed symbol table. */ #include #include #include #include #include "testutil.h" static akbasic_SymbolTable TABLE; int main(void) { void *value = NULL; int64_t ivalue = 0; char key[32]; char longkey[AKBASIC_SYMTAB_MAX_KEY + 2]; int marker_a = 1; int marker_b = 2; int i = 0; TEST_REQUIRE_OK(akbasic_error_register()); TEST_REQUIRE_OK(akbasic_symtab_init(&TABLE, 16)); /* Round-trip both payloads. */ TEST_REQUIRE_OK(akbasic_symtab_set(&TABLE, "A#", &marker_a, 42)); TEST_REQUIRE_OK(akbasic_symtab_get(&TABLE, "A#", &value, &ivalue)); TEST_REQUIRE(value == &marker_a, "wrong pointer payload"); TEST_REQUIRE_INT(ivalue, 42); /* Replacing an existing key must not grow the table. */ TEST_REQUIRE_OK(akbasic_symtab_set(&TABLE, "A#", &marker_b, 43)); TEST_REQUIRE_OK(akbasic_symtab_get(&TABLE, "A#", &value, &ivalue)); TEST_REQUIRE(value == &marker_b, "replacement did not take"); TEST_REQUIRE_INT(ivalue, 43); TEST_REQUIRE_INT(TABLE.count, 1); /* A miss is AKERR_KEY, not a crash and not a silent NULL. */ TEST_REQUIRE_STATUS(akbasic_symtab_get(&TABLE, "B$", &value, &ivalue), AKERR_KEY); /* * Collisions. Sixteen distinct keys in a sixteen-slot table guarantees the * probe sequence is exercised, and the last insert has exactly one slot to * find. */ TEST_REQUIRE_OK(akbasic_symtab_clear(&TABLE)); for ( i = 0; i < 16; i++ ) { snprintf(key, sizeof(key), "V%d#", i); TEST_REQUIRE_OK(akbasic_symtab_set(&TABLE, key, NULL, i)); } for ( i = 0; i < 16; i++ ) { snprintf(key, sizeof(key), "V%d#", i); TEST_REQUIRE_OK(akbasic_symtab_get(&TABLE, key, NULL, &ivalue)); TEST_REQUIRE_INT(ivalue, i); } /* Seventeenth key into a sixteen-slot table: full, and it says so. */ TEST_REQUIRE_STATUS(akbasic_symtab_set(&TABLE, "OVERFLOW#", NULL, 0), AKBASIC_ERR_BOUNDS); /* Clear keeps the capacity but drops the contents. */ TEST_REQUIRE_OK(akbasic_symtab_clear(&TABLE)); TEST_REQUIRE_INT(TABLE.count, 0); TEST_REQUIRE_INT(TABLE.capacity, 16); TEST_REQUIRE_STATUS(akbasic_symtab_get(&TABLE, "V0#", NULL, NULL), AKERR_KEY); /* * Boundary keys. Mutation testing found that nothing exercised a key at the * length limit, so every `AKBASIC_SYMTAB_MAX_KEY - 1` in the copy and its * NUL terminator could be perturbed to `+ 1` or `- 0` and survive. A key one * character under the limit must round-trip exactly; one *at* the limit must * be refused rather than silently truncated. */ TEST_REQUIRE_OK(akbasic_symtab_clear(&TABLE)); memset(longkey, 'K', AKBASIC_SYMTAB_MAX_KEY - 1); longkey[AKBASIC_SYMTAB_MAX_KEY - 1] = '\0'; TEST_REQUIRE_OK(akbasic_symtab_set(&TABLE, longkey, &marker_a, 7)); TEST_REQUIRE_OK(akbasic_symtab_get(&TABLE, longkey, &value, &ivalue)); TEST_REQUIRE(value == &marker_a, "a maximum-length key must round-trip"); TEST_REQUIRE_INT(ivalue, 7); TEST_REQUIRE_INT(strlen(TABLE.slots[0].key) + strlen(TABLE.slots[1].key) + strlen(TABLE.slots[2].key) + strlen(TABLE.slots[3].key) + strlen(TABLE.slots[4].key) + strlen(TABLE.slots[5].key) + strlen(TABLE.slots[6].key) + strlen(TABLE.slots[7].key) + strlen(TABLE.slots[8].key) + strlen(TABLE.slots[9].key) + strlen(TABLE.slots[10].key) + strlen(TABLE.slots[11].key) + strlen(TABLE.slots[12].key) + strlen(TABLE.slots[13].key) + strlen(TABLE.slots[14].key) + strlen(TABLE.slots[15].key), AKBASIC_SYMTAB_MAX_KEY - 1); memset(longkey, 'K', AKBASIC_SYMTAB_MAX_KEY); longkey[AKBASIC_SYMTAB_MAX_KEY] = '\0'; TEST_REQUIRE_STATUS(akbasic_symtab_set(&TABLE, longkey, NULL, 0), AKBASIC_ERR_BOUNDS); TEST_REQUIRE_STATUS(akbasic_symtab_get(&TABLE, longkey, NULL, NULL), AKERR_KEY); /* * A freshly initialised table is empty and its slots are clear. Mutation * found that deleting the memset or the count reset in init() survived, * because every other assertion here goes through set() first. */ TEST_REQUIRE_OK(akbasic_symtab_init(&TABLE, 16)); TEST_REQUIRE_INT(TABLE.count, 0); for ( i = 0; i < 16; i++ ) { TEST_REQUIRE(!TABLE.slots[i].used, "slot %d should be unused after init", i); TEST_REQUIRE(TABLE.slots[i].key[0] == '\0', "slot %d key should be clear", i); TEST_REQUIRE(TABLE.slots[i].value == NULL, "slot %d value should be NULL", i); TEST_REQUIRE_INT(TABLE.slots[i].ivalue, 0); } /* * Probing must start where the hash says. Mutation found that seeding the * probe index or the loop counter to 1, or dropping the hash call entirely, * survived -- a table that ignores its hash still works, just slowly. Assert * placement instead: with capacity 1 there is exactly one slot, so a second * distinct key has nowhere to go. */ TEST_REQUIRE_OK(akbasic_symtab_init(&TABLE, 1)); TEST_REQUIRE_OK(akbasic_symtab_set(&TABLE, "ONLY#", NULL, 1)); TEST_REQUIRE(TABLE.slots[0].used, "the single slot must be the one used"); TEST_REQUIRE_STATUS(akbasic_symtab_set(&TABLE, "OTHER#", NULL, 2), AKBASIC_ERR_BOUNDS); TEST_REQUIRE_OK(akbasic_symtab_get(&TABLE, "ONLY#", NULL, &ivalue)); TEST_REQUIRE_INT(ivalue, 1); /* Argument validation. */ TEST_REQUIRE_STATUS(akbasic_symtab_init(NULL, 16), AKERR_NULLPOINTER); TEST_REQUIRE_STATUS(akbasic_symtab_init(&TABLE, 0), AKBASIC_ERR_BOUNDS); TEST_REQUIRE_STATUS(akbasic_symtab_init(&TABLE, AKBASIC_SYMTAB_MAX_SLOTS + 1), AKBASIC_ERR_BOUNDS); return akbasic_test_failures; }