Files
libakerror/tests/err_maxval.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

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;
}