Raise errors from the status registry instead of returning codes
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

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>
This commit is contained in:
2026-07-30 20:58:47 -04:00
parent ba2430bfa1
commit 64da04e83b
13 changed files with 741 additions and 380 deletions

View File

@@ -11,7 +11,11 @@
* 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
* report the failure rather than dropping the registration quietly.
* 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)
@@ -20,120 +24,119 @@ int main(void)
akerr_init();
/* Any int is a legal status, at either extreme of the range. */
AKERR_CHECK(akerr_reserve_status_range(INT_MIN, 1, "min-owner") ==
AKERR_STATUS_RANGE_OK);
AKERR_CHECK(akerr_reserve_status_range(INT_MAX, 1, "max-owner") ==
AKERR_STATUS_RANGE_OK);
AKERR_CHECK(akerr_register_status_name("max-owner", INT_MAX, "Maximum Status") ==
AKERR_STATUS_NAME_OK);
AKERR_CHECK(akerr_register_status_name("min-owner", INT_MIN, "Minimum Status") ==
AKERR_STATUS_NAME_OK);
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(akerr_reserve_status_range(1000000, 1, "trunc") ==
AKERR_STATUS_RANGE_OK);
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(1000000, 1, "trunc"));
const char *long_name =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-extra";
AKERR_CHECK(akerr_register_status_name("trunc", 1000000, long_name) ==
AKERR_STATUS_NAME_OK);
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(akerr_reserve_status_range(256, 16, "component-a") ==
AKERR_STATUS_RANGE_OK);
AKERR_CHECK(akerr_reserve_status_range(256, 16, "component-a") ==
AKERR_STATUS_RANGE_OK);
AKERR_CHECK(akerr_reserve_status_range(260, 2, "component-b") ==
AKERR_STATUS_RANGE_OVERLAP);
AKERR_CHECK_CONTAINS("component-a");
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(akerr_reserve_status_range(255, 1, "component-b") ==
AKERR_STATUS_RANGE_OVERLAP);
AKERR_CHECK(akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
AKERR_LIBRARY_OWNER) ==
AKERR_STATUS_RANGE_OK); /* exact repeat by the owner */
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(akerr_reserve_status_range(INT_MAX, 2, "overflow") ==
AKERR_STATUS_RANGE_INVALID);
AKERR_CHECK(akerr_reserve_status_range(300, 0, "empty") ==
AKERR_STATUS_RANGE_INVALID);
AKERR_CHECK(akerr_reserve_status_range(300, -1, "negative") ==
AKERR_STATUS_RANGE_INVALID);
AKERR_CHECK(akerr_reserve_status_range(300, 1, NULL) ==
AKERR_STATUS_RANGE_INVALID);
AKERR_CHECK(akerr_reserve_status_range(300, 1, "") ==
AKERR_STATUS_RANGE_INVALID);
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. */
/* 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';
AKERR_CHECK(akerr_reserve_status_range(400, 1, owner63) ==
AKERR_STATUS_RANGE_OK);
AKERR_CHECK(akerr_reserve_status_range(401, 1, owner64) ==
AKERR_STATUS_RANGE_INVALID);
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(akerr_reserve_status_range(500, 2, "endpoint") ==
AKERR_STATUS_RANGE_OK);
AKERR_CHECK(akerr_reserve_status_range(499, 2, "left") ==
AKERR_STATUS_RANGE_OVERLAP);
AKERR_CHECK(akerr_reserve_status_range(500, 1, "endpoint") ==
AKERR_STATUS_RANGE_OVERLAP); /* subset, not an exact repeat */
AKERR_CHECK(akerr_reserve_status_range(501, 1, "endpoint") ==
AKERR_STATUS_RANGE_OVERLAP);
AKERR_CHECK(akerr_reserve_status_range(500, 2, "other") ==
AKERR_STATUS_RANGE_OVERLAP); /* same range, wrong 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(akerr_reserve_status_range(2000000, 100000, "fill") ==
AKERR_STATUS_RANGE_OK);
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.
*/
akerr_capture_reset();
int ranges_added = 0;
int range_rc = AKERR_STATUS_RANGE_OK;
akerr_ErrorContext *range_err = NULL;
for ( int i = 0; i < 100000; i++ ) {
range_rc = akerr_reserve_status_range(1000 + (i * 2), 1, "pad");
if ( range_rc != AKERR_STATUS_RANGE_OK ) {
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(range_rc == AKERR_STATUS_RANGE_FULL);
AKERR_CHECK_CONTAINS("range table is full");
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".
*/
akerr_capture_reset();
int full_at = -1;
for ( int i = 0; i < 100000; i++ ) {
char name[32];
snprintf(name, sizeof(name), "Filled %d", i);
int rc = akerr_register_status_name("fill", 2000000 + i, name);
if ( rc != AKERR_STATUS_NAME_OK ) {
AKERR_CHECK(rc == AKERR_STATUS_NAME_FULL);
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;
}
}
AKERR_CHECK_CONTAINS("registry is full");
AKERR_CHECK_CONTAINS("AKERR_STATUS_NAME_SLOTS");
/*
* The table must actually hold everything it accepted. A probe sequence
@@ -154,6 +157,9 @@ int main(void)
"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;
}