Version at 0.2.0: complete the wishlist, document it, gate the docs

Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that
file to hold outstanding items only.

The API break gets a minor bump, because pre-1.0 the soname carries
MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five
signatures changed and the ato* contract with them; UPGRADING.md is new
and lists every one, with the before/after for the cases the compiler
cannot warn about.

Section 3.1 is finished: reallocarray with the multiplication checked,
aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf.
Four functions on that list are deliberately absent rather than missing
-- sprintf, strtok, setbuf and perror -- and TODO.md now says which and
why, so nobody adds them thinking they were forgotten.

Section 1.9, the cross-cutting tests:

  tests/test_pool.c    drives every failure path AKERR_MAX_ARRAY_ERROR
                       + 10 times and checks the pool after each round,
                       because a wrapper that leaks a slot fails a
                       hundred calls later in unrelated code. It also
                       asserts that each error names the function and
                       file it was raised from, which is what catches a
                       FAIL that migrates into a helper during a
                       refactor: status right, message right, origin
                       quietly lying.
  tests/negative/      two sources that must FAIL to compile, built with
                       -Werror and registered WILL_FAIL. AKERR_NOIGNORE
                       and the format attributes are enforced by the
                       compiler and by nothing else; drop either and
                       every ordinary test still passes.

Thread safety is answered rather than tested: the library is not
thread-safe and cannot be made so from here, because libakerror's error
pool is an unlocked process-global array. README.md says so plainly and
TODO.md carries it as the item blocking any future pthread wrappers.

Doxygen is configured and gated. All 147 public functions have @brief,
a @param each, @throws per status and @return; EXTRACT_ALL is off and
WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an
undocumented entity. It ran to 0 warnings. The Doxyfile carries no
version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from
project(), so that stays the one place a version is written.

CI now builds against the submodule it pins instead of also installing
libakerror@main and never linking it, adds -Werror, and gains a
sanitizer job. The pre-push hook matches, and runs the docs check too.

Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The
eight uncovered lines are each uncovered on purpose and TODO.md says
which and why.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 08:00:16 -04:00
parent 98a0a8562d
commit 125eeb2109
22 changed files with 4017 additions and 973 deletions

410
tests/test_pool.c Normal file
View File

@@ -0,0 +1,410 @@
/*
* Cross-cutting properties every wrapper has to have -- TODO.md section 1.9.
*
* Two of them, and neither is about what any individual function computes:
*
* Error-pool accounting. libakerror hands out error contexts from a fixed
* process-global array of AKERR_MAX_ARRAY_ERROR slots. A wrapper that raises
* an error and forgets to release it does not fail visibly -- it fails
* AKERR_MAX_ARRAY_ERROR calls later, in whatever unrelated code happens to ask
* for a slot next, by which time the leak is nowhere near the symptom. So
* every failure path here is driven AKERR_MAX_ARRAY_ERROR + 10 times, which is
* past exhaustion several times over, with the pool checked after each round.
*
* Stack-trace content. An error that reports the wrong origin is worse than
* useless when the only debugging you get is a log file after the fact, which
* is the stated reason this library exists at all. Each assertion here names
* the function the error must say it came from and the source file it must
* name, so a FAIL that migrates into a helper is caught.
*
* AKSL_RUN already checks the pool after every test in every file. This one
* checks it inside the loop as well, because a leak of one slot per call needs
* more than one call to show up.
*/
#include "aksl_capture.h"
#include <errno.h>
#include <limits.h>
/* Rounds enough to exhaust the pool several times over. */
#define ROUNDS (AKERR_MAX_ARRAY_ERROR + 10)
/*
* Assert the last error came from `func` in a file whose name ends in `file`.
* The recorded fname is whatever __FILE__ expanded to at the FAIL site, which is
* an absolute path under CMake, so this matches on the tail rather than the
* whole thing.
*/
static int came_from(const char *func, const char *file)
{
size_t flen = strlen(file);
size_t rlen = strlen(aksl_last_file);
if ( strcmp(aksl_last_function, func) != 0 ) {
fprintf(stderr, " CHECK FAILED: error says it came from \"%s\", expected \"%s\"\n",
aksl_last_function, func);
return 1;
}
if ( rlen < flen || strcmp(aksl_last_file + (rlen - flen), file) != 0 ) {
fprintf(stderr, " CHECK FAILED: error names file \"%s\", expected one ending \"%s\"\n",
aksl_last_file, file);
return 1;
}
if ( aksl_last_line <= 0 ) {
fprintf(stderr, " CHECK FAILED: error records line %d\n", aksl_last_line);
return 1;
}
return 0;
}
/* ---------------------------------------------------------------------- */
/* Pool accounting */
/* ---------------------------------------------------------------------- */
static int test_memory_wrappers_do_not_leak_slots(void)
{
void *ptr = NULL;
int i = 0;
for ( i = 0; i < ROUNDS; i++ ) {
AKSL_CHECK_STATUS(aksl_malloc(32, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_malloc(0, &ptr), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_calloc(0, 1, &ptr), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_realloc(NULL, 8), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_free(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_freep(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memset(NULL, 0, 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memcpy(NULL, "a", 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memmove(NULL, "a", 1), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memcmp(NULL, "a", 1, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_memchr(NULL, 'a', 1, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(aksl_slots_in_use() == 0);
}
return 0;
}
static int test_stream_wrappers_do_not_leak_slots(void)
{
FILE *fp = NULL;
size_t moved = 0;
int i = 0;
for ( i = 0; i < ROUNDS; i++ ) {
AKSL_CHECK_STATUS(aksl_fopen(NULL, "r", &fp), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fopen("/nonexistent/aksl/x", "r", &fp), ENOENT);
AKSL_CHECK_STATUS(aksl_fread(NULL, 1, 1, stdin, &moved), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fwrite(NULL, 1, 1, stdout, &moved), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fclose(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fseek(NULL, 0, SEEK_SET), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_ftell(NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fgetc(NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fputs(NULL, stdout), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_remove("/nonexistent/aksl/x"), ENOENT);
AKSL_CHECK(aksl_slots_in_use() == 0);
}
return 0;
}
static int test_format_wrappers_do_not_leak_slots(void)
{
char buf[16];
int count = 0;
int i = 0;
for ( i = 0; i < ROUNDS; i++ ) {
AKSL_CHECK_STATUS(aksl_printf(NULL, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_fprintf(NULL, stdout, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_snprintf(NULL, buf, sizeof(buf), "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_snprintf(&count, buf, 4, "%s", "far too long"),
AKERR_OUTOFBOUNDS);
AKSL_CHECK(aksl_slots_in_use() == 0);
}
return 0;
}
static int test_conversion_wrappers_do_not_leak_slots(void)
{
int iout = 0;
long lout = 0;
unsigned long ulout = 0;
double dout = 0.0;
int i = 0;
for ( i = 0; i < ROUNDS; i++ ) {
AKSL_CHECK_STATUS(aksl_atoi(NULL, &iout), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_atoi("junk", &iout), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_atoi("99999999999999999999", &iout), ERANGE);
AKSL_CHECK_STATUS(aksl_strtol("junk", NULL, 10, &lout), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strtoul("-1", NULL, 10, &ulout), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strtod("junk", NULL, &dout), AKERR_VALUE);
AKSL_CHECK(aksl_slots_in_use() == 0);
}
return 0;
}
static int test_string_wrappers_do_not_leak_slots(void)
{
char buf[8];
char *at = NULL;
size_t n = 0;
int r = 0;
int i = 0;
for ( i = 0; i < ROUNDS; i++ ) {
AKSL_CHECK_STATUS(aksl_strlen(NULL, &n), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcpy(buf, sizeof(buf), "far too long for eight"),
AKERR_OUTOFBOUNDS);
AKSL_CHECK_STATUS(aksl_strcat(buf, 0, "x"), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_strdup(NULL, &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strcmp(NULL, "b", &r), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strchr(NULL, 'a', &at), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strerror(0, buf, 0), AKERR_VALUE);
AKSL_CHECK(aksl_slots_in_use() == 0);
}
return 0;
}
static int test_collection_wrappers_do_not_leak_slots(void)
{
aksl_ListNode node;
aksl_ListNode *head = NULL;
aksl_List list;
aksl_TreeNode tree;
aksl_HashMap map;
aksl_HashEntry slots[4];
aksl_StrBuf strbuf;
size_t n = 0;
int i = 0;
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
AKSL_CHECK_OK(aksl_list_init(&list));
AKSL_CHECK_OK(aksl_tree_node_init(&tree, NULL));
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
memset((void *)&strbuf, 0x00, sizeof(strbuf));
for ( i = 0; i < ROUNDS; i++ ) {
AKSL_CHECK_STATUS(aksl_list_append(NULL, &node), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_pop(&head, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_prepend(NULL, &node), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_length(&node, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_list_remove(&list, &node), AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree, NULL, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, NULL),
AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_tree_count(&tree, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_hashmap_get(&map, "absent", NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strbuf_append(&strbuf, "x"), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_strhash_djb2(NULL, 0, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(aksl_slots_in_use() == 0);
}
(void)n;
return 0;
}
/*
* A traversal that fails part-way through has the most opportunities to leak: an
* error is raised inside a callback, propagated up through several frames, and
* handled or not at the top. Drive that repeatedly too.
*/
static akerr_ErrorContext AKERR_NOIGNORE *always_fails(aksl_TreeNode *node, void *data)
{
PREPARE_ERROR(e);
(void)node;
(void)data;
FAIL_RETURN(e, AKERR_VALUE, "callback always fails");
}
static akerr_ErrorContext AKERR_NOIGNORE *always_breaks(aksl_TreeNode *node, void *data)
{
PREPARE_ERROR(e);
(void)node;
(void)data;
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "callback always breaks");
}
static int test_traversal_failures_do_not_leak_slots(void)
{
aksl_TreeNode tree[3];
int i = 0;
for ( i = 0; i < 3; i++ ) {
AKSL_CHECK_OK(aksl_tree_node_init(&tree[i], NULL));
}
tree[0].left = &tree[1];
tree[0].right = &tree[2];
for ( i = 0; i < ROUNDS; i++ ) {
/* Propagated out of the recursion. */
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], &always_fails, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, NULL),
AKERR_VALUE);
/* Swallowed at the top -- the released-not-returned path. */
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &always_breaks, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, NULL));
/* And through the breadth-first walk, which also drains a queue. */
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], &always_fails, NULL, NULL,
AKSL_TREE_SEARCH_BFS, NULL),
AKERR_VALUE);
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &always_breaks, NULL, NULL,
AKSL_TREE_SEARCH_BFS, NULL));
AKSL_CHECK(aksl_slots_in_use() == 0);
}
return 0;
}
/* ---------------------------------------------------------------------- */
/* Stack-trace content */
/* ---------------------------------------------------------------------- */
/*
* Each of these asserts that the error names the function that raised it and
* the file that function lives in. This is what catches a FAIL that gets moved
* into a shared helper during a refactor: the status stays right, the message
* stays right, and the origin quietly starts pointing at the wrong place.
*/
static int test_errors_name_their_origin_in_stdlib(void)
{
void *ptr = NULL;
int count = 0;
char resolved[PATH_MAX];
uint32_t h = 0;
aksl_ListNode node;
AKSL_CHECK_STATUS(aksl_malloc(32, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_malloc", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_free(NULL), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_free", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_memcpy(NULL, "a", 1), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_memcpy", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_fopen(NULL, "r", (FILE **)&ptr), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_fopen", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_fclose(NULL), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_fclose", "src/stdlib.c") == 0);
/*
* aksl_printf forwards to aksl_vprintf, so the error legitimately comes
* from the latter -- the variadic form is a three-line wrapper with no FAIL
* of its own. Asserting the real origin rather than the one a reader might
* expect is the point of recording it.
*/
AKSL_CHECK_STATUS(aksl_printf(NULL, "x"), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_vprintf", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_snprintf(&count, NULL, 8, "x"), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_vsnprintf", "src/stdlib.c") == 0);
/* Likewise, the ato* forms are calls into the strto* ones. */
AKSL_CHECK_STATUS(aksl_atol("junk", (long *)&ptr), AKERR_VALUE);
AKSL_CHECK(came_from("aksl_strtol", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_realpath(NULL, resolved, sizeof(resolved)), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_realpath", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_strhash_djb2(NULL, 0, &h), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_strhash_djb2", "src/stdlib.c") == 0);
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
AKSL_CHECK_STATUS(aksl_list_append(NULL, &node), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_list_append", "src/stdlib.c") == 0);
AKSL_CHECK_STATUS(aksl_tree_iterate(NULL, NULL, NULL, NULL,
AKSL_TREE_SEARCH_DFS_PREORDER, NULL),
AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_tree_iterate", "src/stdlib.c") == 0);
return 0;
}
static int test_errors_name_their_origin_in_string_and_stream(void)
{
char buf[8];
size_t n = 0;
long pos = 0;
AKSL_CHECK_STATUS(aksl_strlen(NULL, &n), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_strlen", "src/string.c") == 0);
AKSL_CHECK_STATUS(aksl_strcpy(buf, sizeof(buf), "far too long for eight"),
AKERR_OUTOFBOUNDS);
AKSL_CHECK(came_from("aksl_strcpy", "src/string.c") == 0);
AKSL_CHECK_STATUS(aksl_strdup(NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_strdup", "src/string.c") == 0);
AKSL_CHECK_STATUS(aksl_strerror(0, buf, 0), AKERR_VALUE);
AKSL_CHECK(came_from("aksl_strerror", "src/string.c") == 0);
AKSL_CHECK_STATUS(aksl_fseek(NULL, 0, SEEK_SET), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_fseek", "src/stream.c") == 0);
AKSL_CHECK_STATUS(aksl_ftell(NULL, &pos), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_ftell", "src/stream.c") == 0);
AKSL_CHECK_STATUS(aksl_getline(NULL, NULL, NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_getline", "src/stream.c") == 0);
AKSL_CHECK_STATUS(aksl_mkdtemp("no-placeholder"), AKERR_VALUE);
AKSL_CHECK(came_from("aksl_mkdtemp", "src/stream.c") == 0);
return 0;
}
static int test_errors_name_their_origin_in_collections(void)
{
aksl_ListNode node;
aksl_List list;
aksl_HashMap map;
aksl_HashEntry slots[4];
aksl_StrBuf strbuf;
char longkey[AKSL_HASHMAP_MAX_KEY + 4];
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
AKSL_CHECK_OK(aksl_list_init(&list));
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
memset((void *)&strbuf, 0x00, sizeof(strbuf));
memset(longkey, 'k', sizeof(longkey) - 1);
longkey[sizeof(longkey) - 1] = '\0';
AKSL_CHECK_STATUS(aksl_list_prepend(NULL, &node), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_list_prepend", "src/collections.c") == 0);
AKSL_CHECK_STATUS(aksl_list_remove(&list, &node), AKERR_VALUE);
AKSL_CHECK(came_from("aksl_list_remove", "src/collections.c") == 0);
AKSL_CHECK_STATUS(aksl_tree_insert(NULL, NULL, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_tree_insert", "src/collections.c") == 0);
AKSL_CHECK_STATUS(aksl_hashmap_put(&map, longkey, NULL), AKERR_OUTOFBOUNDS);
AKSL_CHECK(came_from("aksl_hashmap_put", "src/collections.c") == 0);
AKSL_CHECK_STATUS(aksl_strhash_fnv1a(NULL, 0, NULL), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_strhash_fnv1a", "src/collections.c") == 0);
AKSL_CHECK_STATUS(aksl_strbuf_reset(&strbuf), AKERR_NULLPOINTER);
AKSL_CHECK(came_from("aksl_strbuf_reset", "src/collections.c") == 0);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_memory_wrappers_do_not_leak_slots);
AKSL_RUN(failures, test_stream_wrappers_do_not_leak_slots);
AKSL_RUN(failures, test_format_wrappers_do_not_leak_slots);
AKSL_RUN(failures, test_conversion_wrappers_do_not_leak_slots);
AKSL_RUN(failures, test_string_wrappers_do_not_leak_slots);
AKSL_RUN(failures, test_collection_wrappers_do_not_leak_slots);
AKSL_RUN(failures, test_traversal_failures_do_not_leak_slots);
AKSL_RUN(failures, test_errors_name_their_origin_in_stdlib);
AKSL_RUN(failures, test_errors_name_their_origin_in_string_and_stream);
AKSL_RUN(failures, test_errors_name_their_origin_in_collections);
AKSL_REPORT(failures);
}