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>
127 lines
5.9 KiB
C
127 lines
5.9 KiB
C
#include "akerror.h"
|
|
#include "err_capture.h"
|
|
#include <string.h>
|
|
|
|
/*
|
|
* Reserving a range used to be pure bookkeeping: akerr_name_for_status() would
|
|
* name any status for any caller, so two components could still register names
|
|
* for the same code -- and HANDLE the same code -- with nothing detecting it.
|
|
* Reservation only caught components that both opted in AND declared ranges
|
|
* that happened to overlap.
|
|
*
|
|
* Naming a status is now permitted only inside a reservation:
|
|
* - akerr_register_status_name() requires the range to belong to the caller,
|
|
* and raises AKERR_STATUS_NAME_* when it does not;
|
|
* - the legacy two-argument akerr_name_for_status() set path cannot identify
|
|
* its caller, so it can only require that *some* reservation covers the
|
|
* status -- still enough to stop a code nobody claimed. It returns a name
|
|
* rather than an error context, so its refusals are logged instead.
|
|
* Either way the refusal is visible, because a name that fails to register
|
|
* degrades the status to "Unknown Error" in every later stack trace.
|
|
*/
|
|
|
|
int main(void)
|
|
{
|
|
akerr_capture_install();
|
|
akerr_init();
|
|
|
|
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "lib-a"));
|
|
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(512, 16, "lib-b"));
|
|
|
|
/* The owner of a range may name statuses inside it. */
|
|
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("lib-a", 256, "A Parse Error"));
|
|
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("lib-a", 271, "A Last Error"));
|
|
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "A Parse Error") == 0);
|
|
|
|
/* Naming another owner's status is refused and names the real owner. */
|
|
AKERR_CHECK_RAISES(akerr_register_status_name("lib-b", 256, "B Hijack"),
|
|
AKERR_STATUS_NAME_FOREIGN);
|
|
AKERR_CHECK_MESSAGE_CONTAINS("lib-a");
|
|
AKERR_CHECK_MESSAGE_CONTAINS("lib-b");
|
|
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "A Parse Error") == 0);
|
|
|
|
/* Including the library's own reserved band. */
|
|
AKERR_CHECK_RAISES(akerr_register_status_name("lib-b", AKERR_VALUE, "B Value"),
|
|
AKERR_STATUS_NAME_FOREIGN);
|
|
AKERR_CHECK_MESSAGE_CONTAINS(AKERR_LIBRARY_OWNER);
|
|
AKERR_CHECK(strcmp(akerr_name_for_status(AKERR_VALUE, NULL), "Value Error") == 0);
|
|
|
|
/* A status nobody reserved cannot be named through either entry point. */
|
|
AKERR_CHECK_RAISES(akerr_register_status_name("lib-a", 9999, "Unclaimed"),
|
|
AKERR_STATUS_NAME_UNRESERVED);
|
|
AKERR_CHECK_MESSAGE_CONTAINS("no reserved range");
|
|
AKERR_CHECK(strcmp(akerr_name_for_status(9999, NULL), "Unknown Error") == 0);
|
|
|
|
/* The legacy path has no caller to raise into, so it logs the refusal.
|
|
* It also cannot name the caller, and must say so rather than printing a
|
|
* stray owner. */
|
|
akerr_capture_reset();
|
|
AKERR_CHECK(strcmp(akerr_name_for_status(9999, "Unclaimed Legacy"),
|
|
"Unknown Error") == 0);
|
|
AKERR_CHECK_CONTAINS("no reserved range");
|
|
AKERR_CHECK_CONTAINS("an unnamed caller");
|
|
AKERR_CHECK_CONTAINS("REFUSED STATUS NAME");
|
|
AKERR_CHECK(strcmp(akerr_name_for_status(9999, NULL), "Unknown Error") == 0);
|
|
/* ... and hands the context it raised back to the pool. */
|
|
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
|
|
/* Boundaries: just outside lib-a's range is not lib-a's to name. */
|
|
AKERR_CHECK_RAISES(akerr_register_status_name("lib-a", 255, "Below"),
|
|
AKERR_STATUS_NAME_FOREIGN);
|
|
AKERR_CHECK_RAISES(akerr_register_status_name("lib-a", 272, "Above"),
|
|
AKERR_STATUS_NAME_UNRESERVED);
|
|
|
|
/* The legacy set path still works inside any reservation. */
|
|
AKERR_CHECK(strcmp(akerr_name_for_status(513, "B Legacy"), "B Legacy") == 0);
|
|
AKERR_CHECK(strcmp(akerr_name_for_status(513, NULL), "B Legacy") == 0);
|
|
|
|
/* Re-registering your own status overwrites the name. */
|
|
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("lib-a", 256, "A Renamed"));
|
|
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "A Renamed") == 0);
|
|
|
|
/*
|
|
* Argument validation. Each message must identify the caller it refused,
|
|
* since that message is the whole report a consumer gets.
|
|
*/
|
|
AKERR_CHECK_RAISES(akerr_register_status_name(NULL, 257, "No Owner"),
|
|
AKERR_STATUS_NAME_INVALID);
|
|
AKERR_CHECK_MESSAGE_CONTAINS("257");
|
|
AKERR_CHECK_RAISES(akerr_register_status_name("", 257, "Empty Owner"),
|
|
AKERR_STATUS_NAME_INVALID);
|
|
AKERR_CHECK_MESSAGE_CONTAINS("Empty Owner");
|
|
AKERR_CHECK_RAISES(akerr_register_status_name("lib-a", 257, NULL),
|
|
AKERR_STATUS_NAME_INVALID);
|
|
AKERR_CHECK_MESSAGE_CONTAINS("lib-a");
|
|
|
|
/* Over-long owner strings: 63 characters fit, 64 and beyond do not. */
|
|
char owner63[64];
|
|
char owner64[65];
|
|
char owner70[71];
|
|
memset(owner63, 'a', sizeof(owner63) - 1);
|
|
owner63[sizeof(owner63) - 1] = '\0';
|
|
memset(owner64, 'b', sizeof(owner64) - 1);
|
|
owner64[sizeof(owner64) - 1] = '\0';
|
|
memset(owner70, 'c', sizeof(owner70) - 1);
|
|
owner70[sizeof(owner70) - 1] = '\0';
|
|
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(600, 1, owner63));
|
|
AKERR_CHECK_SUCCEEDS(akerr_register_status_name(owner63, 600, "Long Owner"));
|
|
AKERR_CHECK_RAISES(akerr_register_status_name(owner64, 600, "Too Long"),
|
|
AKERR_STATUS_NAME_INVALID);
|
|
AKERR_CHECK_MESSAGE_CONTAINS("63");
|
|
AKERR_CHECK_RAISES(akerr_register_status_name(owner70, 600, "Far Too Long"),
|
|
AKERR_STATUS_NAME_INVALID);
|
|
AKERR_CHECK(strcmp(akerr_name_for_status(600, NULL), "Long Owner") == 0);
|
|
|
|
/* A refused registration must not consume a slot or leave a partial entry. */
|
|
AKERR_CHECK(strcmp(akerr_name_for_status(257, NULL), "Unknown Error") == 0);
|
|
|
|
/* Lookup is unaffected by ownership -- anyone may read any name. */
|
|
AKERR_CHECK(strcmp(akerr_name_for_status(271, NULL), "A Last Error") == 0);
|
|
|
|
/* Every refusal above released its context. */
|
|
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
|
|
fprintf(stderr, "err_name_ownership ok\n");
|
|
return 0;
|
|
}
|