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>
This commit is contained in:
@@ -81,6 +81,47 @@ static int __attribute__((unused)) akerr_slots_in_use(void)
|
||||
AKERR_CHECK((errctx)->status == (expected_status)); \
|
||||
} while ( 0 )
|
||||
|
||||
/*
|
||||
* Helpers for functions that report failure by returning akerr_ErrorContext *.
|
||||
* Both release the context they consume, so a test that makes thousands of
|
||||
* failing calls cannot exhaust the pool. AKERR_CHECK_RAISES keeps a copy of the
|
||||
* message for AKERR_CHECK_MESSAGE_CONTAINS, since the context is gone by then.
|
||||
*/
|
||||
static char __attribute__((unused)) akerr_last_message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH];
|
||||
|
||||
#define AKERR_CHECK_SUCCEEDS(expr) \
|
||||
do { \
|
||||
akerr_ErrorContext *__akerr_result = (expr); \
|
||||
if ( __akerr_result != NULL ) { \
|
||||
fprintf(stderr, "UNEXPECTED ERROR from %s: %d (%s): %s" \
|
||||
" at %s:%d\n", #expr, __akerr_result->status, \
|
||||
akerr_name_for_status(__akerr_result->status, NULL), \
|
||||
__akerr_result->message, __FILE__, __LINE__); \
|
||||
RELEASE_ERROR(__akerr_result); \
|
||||
return 1; \
|
||||
} \
|
||||
} while ( 0 )
|
||||
|
||||
#define AKERR_CHECK_RAISES(expr, expected_status) \
|
||||
do { \
|
||||
akerr_ErrorContext *__akerr_result = (expr); \
|
||||
AKERR_CHECK(__akerr_result != NULL); \
|
||||
snprintf(akerr_last_message, sizeof(akerr_last_message), "%s", \
|
||||
__akerr_result->message); \
|
||||
if ( __akerr_result->status != (expected_status) ) { \
|
||||
fprintf(stderr, "WRONG STATUS from %s: got %d, want %s" \
|
||||
" at %s:%d\n", #expr, __akerr_result->status, \
|
||||
#expected_status, __FILE__, __LINE__); \
|
||||
RELEASE_ERROR(__akerr_result); \
|
||||
return 1; \
|
||||
} \
|
||||
RELEASE_ERROR(__akerr_result); \
|
||||
AKERR_CHECK(__akerr_result == NULL); \
|
||||
} while ( 0 )
|
||||
|
||||
#define AKERR_CHECK_MESSAGE_CONTAINS(needle) \
|
||||
AKERR_CHECK(strstr(akerr_last_message, (needle)) != NULL)
|
||||
|
||||
#define AKERR_CHECK_CONTAINS(needle) \
|
||||
AKERR_CHECK(strstr(akerr_capture_buf, (needle)) != NULL)
|
||||
|
||||
|
||||
@@ -35,6 +35,13 @@ static const struct {
|
||||
{ AKERR_ITERATOR_BREAK, "Iterator Break" },
|
||||
{ AKERR_NOT_IMPLEMENTED, "Not Implemented" },
|
||||
{ AKERR_BADEXC, "Invalid akerr_ErrorContext" },
|
||||
{ AKERR_STATUS_RANGE_OVERLAP, "Status Range Overlap" },
|
||||
{ AKERR_STATUS_RANGE_FULL, "Status Range Table Full" },
|
||||
{ AKERR_STATUS_RANGE_INVALID, "Invalid Status Range" },
|
||||
{ AKERR_STATUS_NAME_UNRESERVED, "Unreserved Status Name" },
|
||||
{ AKERR_STATUS_NAME_FOREIGN, "Foreign Status Name" },
|
||||
{ AKERR_STATUS_NAME_FULL, "Status Name Registry Full" },
|
||||
{ AKERR_STATUS_NAME_INVALID, "Invalid Status Name" },
|
||||
};
|
||||
|
||||
int main(void)
|
||||
@@ -52,7 +59,9 @@ int main(void)
|
||||
* AKERR_LAST_ERRNO_VALUE + 7 is the one deliberate hole (a removed code);
|
||||
* anything else nameless is a code someone added without registering it.
|
||||
*/
|
||||
for ( int offset = 1; offset <= AKERR_BADEXC - AKERR_LAST_ERRNO_VALUE; offset++ ) {
|
||||
for ( int offset = 1;
|
||||
offset <= AKERR_LAST_LIBRARY_STATUS - AKERR_LAST_ERRNO_VALUE;
|
||||
offset++ ) {
|
||||
int code = AKERR_LAST_ERRNO_VALUE + offset;
|
||||
char *nm = akerr_name_for_status(code, NULL);
|
||||
if ( offset == 7 ) {
|
||||
@@ -67,7 +76,7 @@ int main(void)
|
||||
}
|
||||
|
||||
/* Every AKERR_* code must sit inside the band the library reserves. */
|
||||
AKERR_CHECK(AKERR_BADEXC < AKERR_FIRST_CONSUMER_STATUS);
|
||||
AKERR_CHECK(AKERR_LAST_LIBRARY_STATUS < AKERR_FIRST_CONSUMER_STATUS);
|
||||
|
||||
fprintf(stderr, "err_error_names ok\n");
|
||||
return 0;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -10,12 +10,14 @@
|
||||
* 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;
|
||||
* - 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.
|
||||
* Every refusal is logged, because a name that fails to register degrades the
|
||||
* status to "Unknown Error" in every later stack trace.
|
||||
* 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)
|
||||
@@ -23,68 +25,92 @@ int main(void)
|
||||
akerr_capture_install();
|
||||
akerr_init();
|
||||
|
||||
AKERR_CHECK(akerr_reserve_status_range(256, 16, "lib-a") ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
AKERR_CHECK(akerr_reserve_status_range(512, 16, "lib-b") ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
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(akerr_register_status_name("lib-a", 256, "A Parse Error") ==
|
||||
AKERR_STATUS_NAME_OK);
|
||||
AKERR_CHECK(akerr_register_status_name("lib-a", 271, "A Last Error") ==
|
||||
AKERR_STATUS_NAME_OK);
|
||||
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_capture_reset();
|
||||
AKERR_CHECK(akerr_register_status_name("lib-b", 256, "B Hijack") ==
|
||||
AKERR_STATUS_NAME_FOREIGN);
|
||||
AKERR_CHECK_CONTAINS("lib-a");
|
||||
AKERR_CHECK_CONTAINS("lib-b");
|
||||
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_capture_reset();
|
||||
AKERR_CHECK(akerr_register_status_name("lib-b", AKERR_VALUE, "B Value") ==
|
||||
AKERR_STATUS_NAME_FOREIGN);
|
||||
AKERR_CHECK_CONTAINS(AKERR_LIBRARY_OWNER);
|
||||
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_capture_reset();
|
||||
AKERR_CHECK(akerr_register_status_name("lib-a", 9999, "Unclaimed") ==
|
||||
AKERR_STATUS_NAME_UNRESERVED);
|
||||
AKERR_CHECK_CONTAINS("no reserved range");
|
||||
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(akerr_register_status_name("lib-a", 255, "Below") ==
|
||||
AKERR_STATUS_NAME_FOREIGN);
|
||||
AKERR_CHECK(akerr_register_status_name("lib-a", 272, "Above") ==
|
||||
AKERR_STATUS_NAME_UNRESERVED);
|
||||
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(akerr_register_status_name("lib-a", 256, "A Renamed") ==
|
||||
AKERR_STATUS_NAME_OK);
|
||||
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. */
|
||||
AKERR_CHECK(akerr_register_status_name(NULL, 257, "No Owner") ==
|
||||
AKERR_STATUS_NAME_INVALID);
|
||||
AKERR_CHECK(akerr_register_status_name("", 257, "Empty Owner") ==
|
||||
AKERR_STATUS_NAME_INVALID);
|
||||
AKERR_CHECK(akerr_register_status_name("lib-a", 257, NULL) ==
|
||||
AKERR_STATUS_NAME_INVALID);
|
||||
/*
|
||||
* 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);
|
||||
@@ -92,6 +118,9 @@ int main(void)
|
||||
/* 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;
|
||||
}
|
||||
|
||||
@@ -22,10 +22,8 @@ int main(void)
|
||||
akerr_capture_install();
|
||||
|
||||
/* Cold call: no akerr_init(), no PREPARE_ERROR anywhere yet. */
|
||||
AKERR_CHECK(akerr_reserve_status_range(256, 16, "early-lib") ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
AKERR_CHECK(akerr_register_status_name("early-lib", 256, "Early Error") ==
|
||||
AKERR_STATUS_NAME_OK);
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "early-lib"));
|
||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("early-lib", 256, "Early Error"));
|
||||
|
||||
/* Something else now uses the library for the first time. */
|
||||
akerr_init();
|
||||
@@ -34,22 +32,23 @@ int main(void)
|
||||
|
||||
/* The early reservation and its name must both have survived. */
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "Early Error") == 0);
|
||||
AKERR_CHECK(akerr_reserve_status_range(256, 16, "late-lib") ==
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_CHECK_CONTAINS("early-lib");
|
||||
AKERR_CHECK(akerr_reserve_status_range(260, 2, "late-lib") ==
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(256, 16, "late-lib"),
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("early-lib");
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(260, 2, "late-lib"),
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
|
||||
/* The library's own initialization still happened exactly once. */
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(AKERR_NULLPOINTER, NULL),
|
||||
"Null Pointer Error") == 0);
|
||||
AKERR_CHECK(akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
||||
AKERR_LIBRARY_OWNER) ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
||||
AKERR_LIBRARY_OWNER));
|
||||
|
||||
/* An identical repeat by the original owner is still idempotent. */
|
||||
AKERR_CHECK(akerr_reserve_status_range(256, 16, "early-lib") ==
|
||||
AKERR_STATUS_RANGE_OK);
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "early-lib"));
|
||||
|
||||
/* Raising from a cold registry must not strand a pool slot either. */
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
fprintf(stderr, "err_registry_init_order ok\n");
|
||||
return 0;
|
||||
|
||||
100
tests/err_status_exception.c
Normal file
100
tests/err_status_exception.c
Normal file
@@ -0,0 +1,100 @@
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user