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>
166 lines
7.4 KiB
C
166 lines
7.4 KiB
C
#include "akerror.h"
|
|
#include "err_capture.h"
|
|
#include <limits.h>
|
|
#include <string.h>
|
|
|
|
/*
|
|
* Status magnitude is no longer coupled to a public array bound: any int is a
|
|
* legal status, and storage is a private sparse registry. What bounds the
|
|
* registry now is its *capacity*, not the value of the largest code.
|
|
*
|
|
* Covers: arbitrary int status values, name truncation, range reservation
|
|
* semantics (overlap, idempotency, endpoints, validation, overflow), and both
|
|
* capacity limits -- the range table and the name table -- each of which must
|
|
* raise rather than dropping the registration quietly.
|
|
*
|
|
* Every refusal is an error context the caller owns, so each check below also
|
|
* asserts the context returns to the pool; a leak here would exhaust the
|
|
* 128-slot pool long before these loops finish.
|
|
*/
|
|
|
|
int main(void)
|
|
{
|
|
akerr_capture_install();
|
|
akerr_init();
|
|
|
|
/* Any int is a legal status, at either extreme of the range. */
|
|
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(INT_MIN, 1, "min-owner"));
|
|
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(INT_MAX, 1, "max-owner"));
|
|
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("max-owner", INT_MAX, "Maximum Status"));
|
|
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("min-owner", INT_MIN, "Minimum Status"));
|
|
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MAX, NULL), "Maximum Status") == 0);
|
|
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MIN, NULL), "Minimum Status") == 0);
|
|
|
|
/* A name longer than the buffer is truncated and always terminated. */
|
|
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(1000000, 1, "trunc"));
|
|
const char *long_name =
|
|
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-extra";
|
|
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("trunc", 1000000, long_name));
|
|
char *stored = akerr_name_for_status(1000000, NULL);
|
|
AKERR_CHECK(strlen(stored) == AKERR_MAX_ERROR_NAME_LENGTH - 1);
|
|
AKERR_CHECK(stored[AKERR_MAX_ERROR_NAME_LENGTH - 1] == '\0');
|
|
|
|
/* Reservation: overlap detection, and idempotency for an exact repeat. */
|
|
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "component-a"));
|
|
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "component-a"));
|
|
AKERR_CHECK_RAISES(akerr_reserve_status_range(260, 2, "component-b"),
|
|
AKERR_STATUS_RANGE_OVERLAP);
|
|
AKERR_CHECK_MESSAGE_CONTAINS("component-a");
|
|
|
|
/* The library's own 0..255 band is reserved and cannot be encroached on. */
|
|
AKERR_CHECK_RAISES(akerr_reserve_status_range(255, 1, "component-b"),
|
|
AKERR_STATUS_RANGE_OVERLAP);
|
|
AKERR_CHECK_MESSAGE_CONTAINS(AKERR_LIBRARY_OWNER);
|
|
/* An exact repeat by the owner is still a no-op. */
|
|
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
|
AKERR_LIBRARY_OWNER));
|
|
|
|
/* Argument validation. */
|
|
AKERR_CHECK_RAISES(akerr_reserve_status_range(INT_MAX, 2, "overflow"),
|
|
AKERR_STATUS_RANGE_INVALID);
|
|
AKERR_CHECK_RAISES(akerr_reserve_status_range(300, 0, "empty"),
|
|
AKERR_STATUS_RANGE_INVALID);
|
|
AKERR_CHECK_MESSAGE_CONTAINS("empty"); /* the message names the caller */
|
|
AKERR_CHECK_RAISES(akerr_reserve_status_range(300, -1, "negative"),
|
|
AKERR_STATUS_RANGE_INVALID);
|
|
AKERR_CHECK_RAISES(akerr_reserve_status_range(300, 1, NULL),
|
|
AKERR_STATUS_RANGE_INVALID);
|
|
AKERR_CHECK_RAISES(akerr_reserve_status_range(300, 1, ""),
|
|
AKERR_STATUS_RANGE_INVALID);
|
|
|
|
/* Owner strings: 63 chars fit, 64 do not, and neither does anything past. */
|
|
char owner63[AKERR_MAX_ERROR_NAME_LENGTH];
|
|
char owner64[AKERR_MAX_ERROR_NAME_LENGTH + 1];
|
|
char owner70[AKERR_MAX_ERROR_NAME_LENGTH + 7];
|
|
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(400, 1, owner63));
|
|
AKERR_CHECK_RAISES(akerr_reserve_status_range(401, 1, owner64),
|
|
AKERR_STATUS_RANGE_INVALID);
|
|
AKERR_CHECK_RAISES(akerr_reserve_status_range(402, 1, owner70),
|
|
AKERR_STATUS_RANGE_INVALID);
|
|
|
|
/* Partial overlaps at either endpoint, and a same-range different owner. */
|
|
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(500, 2, "endpoint"));
|
|
AKERR_CHECK_RAISES(akerr_reserve_status_range(499, 2, "left"),
|
|
AKERR_STATUS_RANGE_OVERLAP);
|
|
AKERR_CHECK_RAISES(akerr_reserve_status_range(500, 1, "endpoint"),
|
|
AKERR_STATUS_RANGE_OVERLAP); /* subset, not an exact repeat */
|
|
AKERR_CHECK_RAISES(akerr_reserve_status_range(501, 1, "endpoint"),
|
|
AKERR_STATUS_RANGE_OVERLAP);
|
|
AKERR_CHECK_RAISES(akerr_reserve_status_range(500, 2, "other"),
|
|
AKERR_STATUS_RANGE_OVERLAP); /* same range, wrong owner */
|
|
|
|
/* Claim room for the name-exhaustion sweep before filling the range table. */
|
|
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(2000000, 100000, "fill"));
|
|
|
|
/*
|
|
* Range table capacity. The limit is private to src/error.c on purpose, so
|
|
* discover it by filling rather than by hardcoding it here.
|
|
*/
|
|
int ranges_added = 0;
|
|
akerr_ErrorContext *range_err = NULL;
|
|
for ( int i = 0; i < 100000; i++ ) {
|
|
range_err = akerr_reserve_status_range(1000 + (i * 2), 1, "pad");
|
|
if ( range_err != NULL ) {
|
|
break;
|
|
}
|
|
ranges_added++;
|
|
}
|
|
AKERR_CHECK(ranges_added > 0);
|
|
AKERR_CHECK_STATUS(range_err, AKERR_STATUS_RANGE_FULL);
|
|
AKERR_CHECK(strstr(range_err->message, "range table is full") != NULL);
|
|
RELEASE_ERROR(range_err);
|
|
AKERR_CHECK(range_err == NULL);
|
|
|
|
/*
|
|
* Name table capacity. Exhaustion must be reported, not silent: a dropped
|
|
* name degrades every future stack trace for that code to "Unknown Error".
|
|
*/
|
|
int full_at = -1;
|
|
for ( int i = 0; i < 100000; i++ ) {
|
|
char name[32];
|
|
snprintf(name, sizeof(name), "Filled %d", i);
|
|
akerr_ErrorContext *name_err =
|
|
akerr_register_status_name("fill", 2000000 + i, name);
|
|
if ( name_err != NULL ) {
|
|
AKERR_CHECK_STATUS(name_err, AKERR_STATUS_NAME_FULL);
|
|
AKERR_CHECK(strstr(name_err->message, "registry is full") != NULL);
|
|
AKERR_CHECK(strstr(name_err->message, "AKERR_STATUS_NAME_SLOTS") != NULL);
|
|
RELEASE_ERROR(name_err);
|
|
AKERR_CHECK(name_err == NULL);
|
|
full_at = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* The table must actually hold everything it accepted. A probe sequence
|
|
* that revisits slots instead of walking the table -- e.g. masking with
|
|
* SLOTS rather than SLOTS-1 -- both collapses the usable capacity and
|
|
* loses earlier entries, and each check below catches it independently.
|
|
* The floor assumes at least the default table size (4096 slots).
|
|
*/
|
|
AKERR_CHECK(full_at > 256);
|
|
for ( int i = 0; i < full_at; i++ ) {
|
|
char expected[32];
|
|
snprintf(expected, sizeof(expected), "Filled %d", i);
|
|
AKERR_CHECK(strcmp(akerr_name_for_status(2000000 + i, NULL), expected) == 0);
|
|
}
|
|
|
|
/* A dropped name reads back as the sentinel, and earlier ones survive. */
|
|
AKERR_CHECK(strcmp(akerr_name_for_status(2000000 + full_at, NULL),
|
|
"Unknown Error") == 0);
|
|
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MIN, NULL), "Minimum Status") == 0);
|
|
|
|
/* Thousands of refusals later, every context went back to the pool. */
|
|
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
|
|
fprintf(stderr, "err_maxval ok (%d consumer names before full)\n", full_at);
|
|
return 0;
|
|
}
|