Expand error release and handler test coverage
This commit is contained in:
@@ -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
|
||||
|
||||
43
tests/err_release_null.c
Normal file
43
tests/err_release_null.c
Normal file
@@ -0,0 +1,43 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
71
tests/err_release_refcount.c
Normal file
71
tests/err_release_refcount.c
Normal file
@@ -0,0 +1,71 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
61
tests/err_unhandled_null.c
Normal file
61
tests/err_unhandled_null.c
Normal file
@@ -0,0 +1,61 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user