From 539293cc1c74a0c3d8048d08defccab96376fd4e Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Thu, 30 Jul 2026 02:00:54 -0400 Subject: [PATCH] Expand error release and handler test coverage --- CMakeLists.txt | 3 ++ tests/err_release_null.c | 43 ++++++++++++++++++++++ tests/err_release_refcount.c | 71 ++++++++++++++++++++++++++++++++++++ tests/err_unhandled_null.c | 61 +++++++++++++++++++++++++++++++ 4 files changed, 178 insertions(+) create mode 100644 tests/err_release_null.c create mode 100644 tests/err_release_refcount.c create mode 100644 tests/err_unhandled_null.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 5df4012..953765f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -108,6 +108,9 @@ set(AKERR_TESTS err_stacktrace_bounds err_name_bounds err_format_string + err_unhandled_null + err_release_null + err_release_refcount ) set(AKERR_WILL_FAIL_TESTS diff --git a/tests/err_release_null.c b/tests/err_release_null.c new file mode 100644 index 0000000..ac6889d --- /dev/null +++ b/tests/err_release_null.c @@ -0,0 +1,43 @@ +#include "akerror.h" +#include "err_capture.h" +#include + +/* + * akerr_release_error(NULL) is an API contract violation the library reports + * rather than crashes on: it raises AKERR_NULLPOINTER against the internal + * last-ditch context and hands that back, so the caller gets a describable + * error instead of a dereferenced NULL. Nothing exercised that path. + * + * The last-ditch context deliberately lives outside AKERR_ARRAY_ERROR so + * reporting this failure cannot consume a pool slot -- which is exactly what + * akerr_valid_error_address() reports on, and what this test checks. + */ + +int main(void) +{ + akerr_capture_install(); + /* akerr_init() sets up the last-ditch context's stacktrace cursor. */ + akerr_init(); + AKERR_CHECK(akerr_slots_in_use() == 0); + + akerr_ErrorContext *ret = akerr_release_error(NULL); + + AKERR_CHECK(ret != NULL); + AKERR_CHECK(ret->status == AKERR_NULLPOINTER); + AKERR_CHECK(strstr(ret->message, "NULL context pointer") != NULL); + + /* Reported through FAIL, so the stack trace names the error too. */ + AKERR_CHECK(strstr(ret->stacktracebuf, "Null Pointer Error") != NULL); + + /* Not a pool slot, and no slot was checked out to report the failure. */ + AKERR_CHECK(akerr_valid_error_address(ret) == 0); + AKERR_CHECK(akerr_slots_in_use() == 0); + + /* The last-ditch context is a singleton: the same one comes back. */ + akerr_ErrorContext *again = akerr_release_error(NULL); + AKERR_CHECK(again == ret); + AKERR_CHECK(akerr_slots_in_use() == 0); + + fprintf(stderr, "err_release_null ok\n"); + return 0; +} diff --git a/tests/err_release_refcount.c b/tests/err_release_refcount.c new file mode 100644 index 0000000..03a8cd4 --- /dev/null +++ b/tests/err_release_refcount.c @@ -0,0 +1,71 @@ +#include "akerror.h" +#include "err_capture.h" +#include + +/* + * akerr_release_error() drops one reference and only recycles the slot when the + * last one goes away. Two of its refcount edges had no test: + * + * - refcount > 1: the release must decrement and return the context intact, + * not wipe it out from under the reference that is still held. + * - refcount == 0: releasing a context nobody holds must not underflow the + * count; it wipes and returns NULL like any other fully-released slot. + * + * The macro API never produces a refcount above 1 (ENSURE_ERROR_READY only + * increments when it acquires a fresh slot), so this test sets the count + * directly to model a caller that took an extra reference. + */ + +int main(void) +{ + akerr_capture_install(); + akerr_init(); + AKERR_CHECK(akerr_slots_in_use() == 0); + + /* Check out a slot and give it two holders. */ + akerr_ErrorContext *held = akerr_next_error(); + AKERR_CHECK(held != NULL); + int slotid = held->arrayid; + held->refcount = 2; + held->status = AKERR_VALUE; + snprintf((char *)held->message, AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH, + "still referenced"); + + /* First release: one holder left, so the context survives untouched. */ + akerr_ErrorContext *ret = akerr_release_error(held); + AKERR_CHECK(ret == held); + AKERR_CHECK(held->refcount == 1); + AKERR_CHECK(held->status == AKERR_VALUE); + AKERR_CHECK(strcmp(held->message, "still referenced") == 0); + AKERR_CHECK(akerr_slots_in_use() == 1); + + /* Second release: last holder gone, so the slot is wiped and recycled. */ + ret = akerr_release_error(held); + AKERR_CHECK(ret == NULL); + AKERR_CHECK(held->refcount == 0); + AKERR_CHECK(held->status == 0); + AKERR_CHECK(held->message[0] == '\0'); + /* The wipe must preserve the slot's identity and stacktrace cursor. */ + AKERR_CHECK(held->arrayid == slotid); + AKERR_CHECK(held->stacktracebufptr == (char *)&held->stacktracebuf); + AKERR_CHECK(akerr_slots_in_use() == 0); + + /* + * Releasing an unheld slot: refcount is already 0, so there is nothing to + * decrement and the count must not go negative. + */ + akerr_ErrorContext *unheld = akerr_next_error(); + AKERR_CHECK(unheld != NULL); + AKERR_CHECK(unheld->refcount == 0); + ret = akerr_release_error(unheld); + AKERR_CHECK(ret == NULL); + AKERR_CHECK(unheld->refcount == 0); + AKERR_CHECK(akerr_slots_in_use() == 0); + + /* The pool is still healthy afterwards. */ + akerr_ErrorContext *probe = akerr_next_error(); + AKERR_CHECK(probe != NULL); + + fprintf(stderr, "err_release_refcount ok\n"); + return 0; +} diff --git a/tests/err_unhandled_null.c b/tests/err_unhandled_null.c new file mode 100644 index 0000000..f5464ed --- /dev/null +++ b/tests/err_unhandled_null.c @@ -0,0 +1,61 @@ +#include "akerror.h" +#include "err_capture.h" +#include +#include + +/* + * The default unhandled-error handler is the library's last stop: it exits the + * process. Both of its exits were untested -- exit(1) for a NULL context (a + * handler invoked with no error at all) and exit(errctx->status) for a real one. + * + * The handler never returns, so each case runs in a forked child and the test + * asserts the exact exit status. That is stricter than a WILL_FAIL test, which + * would pass on any non-zero exit, including one caused by an unrelated bug. + */ + +/* + * Run the default handler on ctx in a child process and return the child's exit + * status, or -1 if it did not exit normally. The _exit() sentinel catches a + * handler that returns instead of terminating. + */ +static int handler_exit_status(akerr_ErrorContext *ctx) +{ + pid_t pid = fork(); + if ( pid == 0 ) { + akerr_default_handler_unhandled_error(ctx); + _exit(99); + } + int status = 0; + if ( pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status) ) { + return -1; + } + return WEXITSTATUS(status); +} + +int main(void) +{ + akerr_capture_install(); + akerr_init(); + + /* No context to report: the handler has nothing to exit with but failure. */ + AKERR_CHECK(handler_exit_status(NULL) == 1); + + /* + * With a context, the status becomes the exit code. waitpid only reports + * its low 8 bits, which is where AKERR_VALUE (144) lands, and it is neither + * 0 nor the 1 used for the NULL case, so the two exits stay distinguishable. + */ + akerr_ErrorContext *slot = akerr_next_error(); + AKERR_CHECK(slot != NULL); + slot->refcount = 1; + slot->status = AKERR_VALUE; + AKERR_CHECK((AKERR_VALUE & 0xff) != 0 && (AKERR_VALUE & 0xff) != 1); + AKERR_CHECK(handler_exit_status(slot) == (AKERR_VALUE & 0xff)); + + slot = akerr_release_error(slot); + AKERR_CHECK(slot == NULL); + AKERR_CHECK(akerr_slots_in_use() == 0); + + fprintf(stderr, "err_unhandled_null ok\n"); + return 0; +}