Files
libakerror/tests/err_status_exception.c
Andrew Kesterson 64da04e83b
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m47s
libakerror CI Build / coverage (push) Successful in 2m46s
libakerror CI Build / mutation_test (push) Successful in 14m56s
Raise errors from the status registry instead of returning codes
akerr_reserve_status_range() and akerr_register_status_name() returned
private int enumerations, which was the one place in the library where a
failure was not an akerr_ErrorContext *. They now return one like
everything else: NULL on success, and on refusal an error whose status is
a real code in the library's reserved band, so it can be CATCH-ed,
HANDLE-d, PASS-ed, or left to propagate into a stack trace. Both are
marked AKERR_NOIGNORE, so discarding the result warns at compile time.

AKERR_STATUS_RANGE_OK and AKERR_STATUS_NAME_OK are gone; the remaining
seven codes move into the AKERR_* offset span and get registered names.
AKERR_LAST_LIBRARY_STATUS replaces AKERR_BADEXC as the top of that span
in the reserved-band static assert and the exhaustiveness sweep.

The refusal detail that used to go straight to akerr_log_method now
travels in the error message, so a caller that handles the error decides
whether it is reported. The two-argument akerr_name_for_status() set path
is the exception: it returns a name and cannot raise, so it logs and
releases. akerr_init() likewise has no caller to raise into, so failing
to reserve its own band or name its own codes is logged and fatal --
that can only happen on a misconfigured build, and continuing would
degrade every later stack trace to "Unknown Error".

Move the 1.0.0 upgrade notice out of README.md into UPGRADING.md and
rewrite its return-code tables in terms of the statuses now raised.

Tests: ctest 29/29, coverage 97.5% line / 64.5% branch, mutation 77.5%
(was 77.6%; the new survivors are the fatal init path, which needs a
library built with an undersized name table -- TODO item 7).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 20:58:47 -04:00

101 lines
3.3 KiB
C

#include "akerror.h"
#include "err_capture.h"
#include <string.h>
/*
* The registry entry points report failure the same way every other function in
* this library does: they return akerr_ErrorContext *. A refused reservation is
* therefore an ordinary error, and everything that works on an ordinary error
* must work on it -- CATCH, HANDLE, PASS, propagation to the caller, and the
* stack trace an unhandled one prints.
*
* This is what distinguishes the current design from the integer return codes
* it replaced: a consumer that ignores the result gets a compiler warning
* (AKERR_NOIGNORE), and a consumer that catches it but does not handle it gets
* the error propagated out of its init function rather than a silently
* unreserved range.
*/
#define TEST_OWNER "exception-test"
/* A consumer init function in the shape the documentation recommends. */
static akerr_ErrorContext AKERR_NOIGNORE *component_init(int base, const char *owner)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akerr_reserve_status_range(base, 4, owner));
CATCH(errctx, akerr_register_status_name(owner, base, "Component Error"));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
int main(void)
{
akerr_capture_install();
akerr_init();
PREPARE_ERROR(errctx);
/* A successful reservation raises nothing at all. */
AKERR_CHECK_SUCCEEDS(component_init(256, TEST_OWNER));
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "Component Error") == 0);
AKERR_CHECK(akerr_slots_in_use() == 0);
/*
* A collision propagates out of the component's init and is caught here.
* HANDLE proves the status is a real, distinct exception a consumer can
* dispatch on -- not an opaque nonzero int.
*/
int handled_overlap = 0;
ATTEMPT {
CATCH(errctx, component_init(258, "other-component"));
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_STATUS_RANGE_OVERLAP) {
handled_overlap = 1;
} HANDLE_DEFAULT(errctx) {
handled_overlap = -1;
} FINISH_NORETURN(errctx);
AKERR_CHECK(handled_overlap == 1);
AKERR_CHECK(akerr_slots_in_use() == 0);
/* The name-side refusals dispatch the same way. */
int handled_foreign = 0;
ATTEMPT {
CATCH(errctx, akerr_register_status_name("interloper", 256, "Hijack"));
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_STATUS_NAME_FOREIGN) {
handled_foreign = 1;
} FINISH_NORETURN(errctx);
AKERR_CHECK(handled_foreign == 1);
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "Component Error") == 0);
/*
* An unhandled refusal carries a stack trace naming the real owner, so the
* report a consumer gets is the library's normal one.
*/
akerr_capture_reset();
ATTEMPT {
CATCH(errctx, akerr_reserve_status_range(AKERR_VALUE, 1, "encroacher"));
} CLEANUP {
} PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) {
LOG_ERROR_WITH_MESSAGE(errctx, "Reservation refused");
} FINISH_NORETURN(errctx);
AKERR_CHECK_CONTAINS("Reservation refused");
AKERR_CHECK_CONTAINS("Status Range Overlap");
AKERR_CHECK_CONTAINS(AKERR_LIBRARY_OWNER);
AKERR_CHECK_CONTAINS("encroacher");
AKERR_CHECK_CONTAINS("error.c");
/* Nothing above stranded a pool slot. */
AKERR_CHECK(akerr_slots_in_use() == 0);
fprintf(stderr, "err_status_exception ok\n");
return 0;
}