Files
libakstdlib/tests/test_strhash.c
Tachikoma 55d986c631
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 5m53s
libakstdlib CI Build / sanitizers (push) Successful in 2m55s
libakstdlib CI Build / coverage (push) Successful in 2m48s
libakstdlib CI Build / mutation_test (push) Successful in 12m39s
Drop the TODO.md section numbers from 26 files
Eighty-five comments cited section numbers -- 1.1, 2.2.6, 3.6 -- from a
numbering the file had already abandoned before the move to the tracker. They
label completed work, so the pointer was the only wrong part.

The citation is removed and the sentence kept, which is what issue #27
recommended: these are labels, not references, and a label carrying a
version-dependent pointer goes stale again at the next reorganisation. Where a
pointer earns its place it names what actually holds the content now --
UPGRADING.md for the confirmed defects, libakerror #15 for the target
namespacing, issue #7 for the mutation survivors.

README.md and akstdlib.h sent readers to TODO.md for 'what is still open';
they name the tracker.

Verified: cmake --build build && ctest --test-dir build, 19/19.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 22:01:27 -04:00

149 lines
4.5 KiB
C

/*
* aksl_strhash_djb2.
*
* The expected values are the canonical djb2 ones: h = 5381, then
* h = h * 33 + byte for each of len bytes, truncated to 32 bits. They were
* computed independently of this implementation.
*
* Most vectors here are 7-bit ASCII, where signed and unsigned char agree and
* the test therefore says nothing either way about byte signedness. The high-bit
* vector is the one that pins the sign-extension defect down.
*/
#include "aksl_capture.h"
static int test_empty_string_is_the_djb2_seed(void)
{
uint32_t h = 0;
AKSL_CHECK_OK(aksl_strhash_djb2("", 0, &h));
AKSL_CHECK(h == 5381);
return 0;
}
/* len drives the loop, so a zero length ignores the contents entirely. */
static int test_zero_length_ignores_the_buffer(void)
{
char buf[] = "ignored";
uint32_t h = 0;
AKSL_CHECK_OK(aksl_strhash_djb2(buf, 0, &h));
AKSL_CHECK(h == 5381);
return 0;
}
static int test_known_answer_vectors(void)
{
char hello[] = "hello";
char libname[] = "libakstdlib";
uint32_t h = 0;
AKSL_CHECK_OK(aksl_strhash_djb2(hello, 5, &h));
AKSL_CHECK(h == 261238937u);
AKSL_CHECK_OK(aksl_strhash_djb2(libname, 11, &h));
AKSL_CHECK(h == 884285482u);
return 0;
}
/*
* Pinned. The cursor is an unsigned char * now, so bytes at or
* above 0x80 contribute their unsigned value. Iterating a plain char * on x86 or
* ARM Linux made them negative, giving 5859874 here instead of 5868578 -- a hash
* that disagreed with canonical djb2 and, worse, disagreed with itself across
* platforms depending on whether char happened to be signed.
*
* Benign for the 7-bit identifiers akbasic hashes; quietly wrong for the first
* caller to key a table on a filename or a UTF-8 string literal.
*/
static int test_high_bit_bytes_are_unsigned(void)
{
char buf[2] = { (char)0xff, (char)0xfe };
uint32_t h = 0;
AKSL_CHECK_OK(aksl_strhash_djb2(buf, sizeof(buf), &h));
AKSL_CHECK(h == 5868578u);
/* The sign-extended answer, spelled out so a regression names itself. */
AKSL_CHECK(h != 5859874u);
return 0;
}
/* The NUL-terminated convenience form agrees with the length one. */
static int test_str_form_matches_the_length_form(void)
{
const char *s = "libakstdlib";
uint32_t from_str = 0;
uint32_t from_len = 0;
AKSL_CHECK_OK(aksl_strhash_djb2_str(s, &from_str));
AKSL_CHECK_OK(aksl_strhash_djb2(s, strlen(s), &from_len));
AKSL_CHECK(from_str == from_len);
AKSL_CHECK(from_str == 884285482u);
/* The empty string is the seed, not an error. */
AKSL_CHECK_OK(aksl_strhash_djb2_str("", &from_str));
AKSL_CHECK(from_str == 5381);
AKSL_CHECK_STATUS(aksl_strhash_djb2_str(NULL, &from_str), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strhash_djb2_str("x", NULL), AKERR_NULLPOINTER);
return 0;
}
/* The function is length-driven, not NUL-driven: an embedded NUL is hashed. */
static int test_embedded_nul_is_hashed(void)
{
char buf[3] = { 'a', '\0', 'b' };
uint32_t whole = 0;
uint32_t prefix = 0;
AKSL_CHECK_OK(aksl_strhash_djb2(buf, sizeof(buf), &whole));
AKSL_CHECK(whole == 193482728u);
/* Stopping at the NUL would give the one-byte hash instead. */
AKSL_CHECK_OK(aksl_strhash_djb2(buf, 1, &prefix));
AKSL_CHECK(prefix != whole);
return 0;
}
static int test_hash_is_stable_across_calls(void)
{
char buf[] = "repeatable";
uint32_t first = 0;
uint32_t second = 0;
AKSL_CHECK_OK(aksl_strhash_djb2(buf, sizeof(buf) - 1, &first));
AKSL_CHECK_OK(aksl_strhash_djb2(buf, sizeof(buf) - 1, &second));
AKSL_CHECK(first == second);
return 0;
}
static int test_rejects_null_arguments(void)
{
char buf[] = "x";
uint32_t h = 0;
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strhash_djb2(NULL, 1, &h),
AKERR_NULLPOINTER, "str");
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strhash_djb2(buf, 1, NULL),
AKERR_NULLPOINTER, "hashval");
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_empty_string_is_the_djb2_seed);
AKSL_RUN(failures, test_zero_length_ignores_the_buffer);
AKSL_RUN(failures, test_known_answer_vectors);
AKSL_RUN(failures, test_high_bit_bytes_are_unsigned);
AKSL_RUN(failures, test_str_form_matches_the_length_form);
AKSL_RUN(failures, test_embedded_nul_is_hashed);
AKSL_RUN(failures, test_hash_is_stable_across_calls);
AKSL_RUN(failures, test_rejects_null_arguments);
AKSL_REPORT(failures);
}