From 64da04e83bac73956cd14866d05714f73753d038 Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Thu, 30 Jul 2026 20:58:47 -0400 Subject: [PATCH] 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) --- CMakeLists.txt | 1 + README.md | 185 ++++------------------------ TODO.md | 17 ++- UPGRADING.md | 208 +++++++++++++++++++++++++++++++ include/akerror.tmpl.h | 65 ++++++---- scripts/generrno.sh | 13 +- src/error.c | 212 ++++++++++++++++++++------------ tests/err_capture.h | 41 ++++++ tests/err_error_names.c | 13 +- tests/err_maxval.c | 134 ++++++++++---------- tests/err_name_ownership.c | 105 ++++++++++------ tests/err_registry_init_order.c | 27 ++-- tests/err_status_exception.c | 100 +++++++++++++++ 13 files changed, 741 insertions(+), 380 deletions(-) create mode 100644 UPGRADING.md create mode 100644 tests/err_status_exception.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f1ee7e..b8f2326 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -129,6 +129,7 @@ set(AKERR_TESTS err_maxval err_name_ownership err_registry_init_order + err_status_exception err_refcount_double_fail err_stacktrace_bounds err_name_bounds diff --git a/README.md b/README.md index 0d2eab2..2aed77a 100644 --- a/README.md +++ b/README.md @@ -4,154 +4,13 @@ This library provides a TRY/CATCH style exception handling mechanism for C. ![build badge](https://source.starfort.tech/andrew/libakerror/actions/workflows/ci.yaml/badge.svg?branch=main) -## Upgrade notice: custom status codes (1.0.0) +## Upgrading from a pre-1.0.0 release -Version 1.0.0 replaces the consumer-sized status-name array with a private -registry, and makes status-code ownership explicit and enforced. This is a -source and ABI break. The library now carries a version and an soname -(`libakerror.so.1`), so a stale installed library can no longer be silently -paired with a newer header — but anything built against a pre-1.0.0 header must -be rebuilt. +1.0.0 replaced the consumer-sized status-name array with a private, ownership- +enforced registry. That is a source and ABI break: see +[UPGRADING.md](UPGRADING.md) for what was removed, how to migrate, the capacity +limits and how to raise them, and the thread-safety rules. -What was removed: - -* `AKERR_MAX_ERR_VALUE` — the registry is sparse and accepts any `int`, so - consumers no longer size it. Delete every compile definition and source - reference. A stale `-DAKERR_MAX_ERR_VALUE=...` is now harmless but useless. -* `__AKERR_ERROR_NAMES` — the name table is private to the library. Code that - touched this data symbol was using an undocumented interface; use - `akerr_name_for_status()`. - -To migrate: - -1. Rebuild libakerror and every dependent library against the new header. -2. Move custom status codes out of the reserved `0`–`255` band. Use fixed - integer constants beginning at `AKERR_FIRST_CONSUMER_STATUS` (256) rather - than offsets from `AKERR_LAST_ERRNO_VALUE`, so a libc that grows an errno - cannot move your codes. -3. Assign a distinct range to every library that may coexist in one process, and - coordinate those ranges at the application or dependency-stack level. -4. Reserve the complete range with `akerr_reserve_status_range()` during - initialization. Treat any result other than `AKERR_STATUS_RANGE_OK` as an - initialization failure. -5. Register names with `akerr_register_status_name()`, passing the same owner - string you reserved with. **You can no longer name a status you have not - reserved** — see "Ownership is enforced" below. - -For example: - -```c -enum { - MYLIB_ERR_BASE = AKERR_FIRST_CONSUMER_STATUS, /* 256 */ - MYLIB_ERR_PARSE = MYLIB_ERR_BASE, - MYLIB_ERR_STORAGE, - MYLIB_ERR_LIMIT -}; - -#define MYLIB_OWNER "mylib" - -int mylib_init(void) -{ - if (akerr_reserve_status_range(MYLIB_ERR_BASE, - MYLIB_ERR_LIMIT - MYLIB_ERR_BASE, - MYLIB_OWNER) != AKERR_STATUS_RANGE_OK) { - return MYLIB_INIT_FAILED; /* another component owns part of the range */ - } - - akerr_register_status_name(MYLIB_OWNER, MYLIB_ERR_PARSE, "Parse Error"); - akerr_register_status_name(MYLIB_OWNER, MYLIB_ERR_STORAGE, "Storage Error"); - return MYLIB_INIT_OK; -} -``` - -You do not need to call `akerr_init()` first. Every registry entry point calls -it for you, so a library that reserves its range before anything else in the -process has touched libakerror keeps that reservation. (Before 1.0.0 this was -silently destructive: `akerr_init()` cleared the tables, so a reservation made -too early was discarded and the next component to claim the same range was told -it was free.) - -### Ownership is enforced - -Reserving a range is no longer advisory bookkeeping. A status may only be named -from inside a reservation: - -* `akerr_register_status_name(owner, status, name)` requires that `status` fall - in a range reserved by `owner`. Naming another component's status fails with - `AKERR_STATUS_NAME_FOREIGN`, and naming a status nobody reserved fails with - `AKERR_STATUS_NAME_UNRESERVED`. -* The two-argument `akerr_name_for_status(status, name)` set path still works, - but it cannot identify its caller, so it can only require that *some* - reservation covers the status. Prefer the owned form: it is the one that - catches a component writing into a range that is not its own. - -Every refusal is reported through `akerr_log_method` and names the real owner, -because a name that fails to register degrades that code to `"Unknown Error"` in -every later stack trace. Registration failures are not fatal by themselves — -check the return value during initialization if you want them to be. - -This detects *name* collisions. It cannot detect two components compiling the -same integer into a `HANDLE` label without ever registering a name, so every -co-resident library should still reserve its range. - -### Return codes - -`akerr_reserve_status_range()`: - -| Code | Meaning | -| --- | --- | -| `AKERR_STATUS_RANGE_OK` (0) | Range reserved, or an identical range was already reserved by the same owner | -| `AKERR_STATUS_RANGE_OVERLAP` | Part of the range is owned by someone else (logged, with the owner) | -| `AKERR_STATUS_RANGE_FULL` | No reservation slots remain | -| `AKERR_STATUS_RANGE_INVALID` | `count < 1`, NULL/empty/over-long owner, or the range overflows `int` | - -`akerr_register_status_name()`: - -| Code | Meaning | -| --- | --- | -| `AKERR_STATUS_NAME_OK` (0) | Name registered | -| `AKERR_STATUS_NAME_UNRESERVED` | No reservation contains this status | -| `AKERR_STATUS_NAME_FOREIGN` | The status is in a range owned by someone else | -| `AKERR_STATUS_NAME_FULL` | The name registry is full | -| `AKERR_STATUS_NAME_INVALID` | NULL/empty/over-long owner, or a NULL name | - -Repeating an identical reservation for the same owner is a no-op. A *subset* or -*superset* of your own range is not — it is reported as an overlap. Reserve the -whole range in one call. - -### Capacity - -Both tables are fixed size, allocated in BSS, and never grow: - -| Limit | Default | Build-time override | -| --- | --- | --- | -| Status names | 3072 usable (4096 slots, 75% load) | `-DAKERR_STATUS_NAME_SLOTS=` | -| Reserved ranges | 64 | `-DAKERR_MAX_RESERVED_STATUS_RANGES=` | - -`akerr_init()` consumes one name slot per host errno value plus one per -`AKERR_*` code — around 150 on glibc, leaving roughly 2900 for all consumers in -the process to share. That figure varies with the host libc, so treat it as -approximate rather than a budget to fill. - -Both overrides are CMake cache variables and apply `PRIVATE` to the library -target. The tables live entirely inside `src/error.c`, so raising them changes -nothing a consumer can observe — unlike the `AKERR_MAX_ERR_VALUE` they replaced, -where a mismatch between the library and its consumers corrupted memory. Set -them when configuring libakerror itself: - -```sh -cmake -S . -B build -DAKERR_STATUS_NAME_SLOTS=16384 -``` - -Exhausting either table is reported through `akerr_log_method` and returned to -the caller; it is never silent. - -### Thread safety - -The registry is process-global mutable state with no locking. Reserve ranges and -register names during single-threaded initialization, before spawning threads. -Lookups (`akerr_name_for_status(status, NULL)`) are safe to call concurrently -once registration has finished. # Why? @@ -220,13 +79,26 @@ names are stored sparsely, so any `int` is a legal status and no compile definition is needed to use large values. Every library that may coexist in one process must reserve its range during -initialization and treat any nonzero result as a startup error: +initialization. `akerr_reserve_status_range()` and +`akerr_register_status_name()` report failure the way everything else in this +library does — they return `akerr_ErrorContext *`, and they are marked +`AKERR_NOIGNORE` — so a collision is an exception you can `CATCH`, `HANDLE`, or +`PASS` up out of your initialization: ```c #define MYLIB_OWNER "my-library" -if (akerr_reserve_status_range(256, 16, MYLIB_OWNER) != AKERR_STATUS_RANGE_OK) { - /* Refuse to initialize: another component owns part of the range. */ +akerr_ErrorContext AKERR_NOIGNORE *mylib_init(void) +{ + PREPARE_ERROR(errctx); + + /* Another component owning part of the range propagates to our caller. */ + PASS(errctx, akerr_reserve_status_range(256, 16, MYLIB_OWNER)); + + /* Then name each code, quoting the owner you reserved with. */ + PASS(errctx, akerr_register_status_name(MYLIB_OWNER, 256, + "Some Error Code Description")); + SUCCEED_RETURN(errctx); } ``` @@ -234,15 +106,12 @@ Reservations are process-local, fixed-capacity, and idempotent when the same owner repeats the exact same range. They preserve compile-time integer constants so `HANDLE` still works, since `case` labels require them. -Then name each code, quoting the owner you reserved with: - -```c -akerr_register_status_name(MYLIB_OWNER, 256, "Some Error Code Description"); -``` - -Naming a status outside your reservation is refused and logged. See -[the upgrade notice](#upgrade-notice-custom-status-codes-100) for the return -codes, the capacity limits and how to raise them, and thread-safety rules. +Naming a status outside your reservation raises `AKERR_STATUS_NAME_FOREIGN`, and +naming one nobody reserved raises `AKERR_STATUS_NAME_UNRESERVED`; a colliding +reservation raises `AKERR_STATUS_RANGE_OVERLAP`, with a message naming the real +owner. See [UPGRADING.md](UPGRADING.md) for the full list of statuses these two +functions raise, the capacity limits and how to raise them, and thread-safety +rules. # Installation diff --git a/TODO.md b/TODO.md index 991eb83..f6e3165 100644 --- a/TODO.md +++ b/TODO.md @@ -35,8 +35,8 @@ inside a loop" hazard: that comes from exiting via `break`, not from `switch`. ## 3. No registry introspection There is no way to ask who owns a status, or to enumerate reservations. The -"coordinate ranges at the dependency-stack level" advice in README.md therefore -has no tooling behind it. +"coordinate ranges at the dependency-stack level" advice in UPGRADING.md +therefore has no tooling behind it. A read-only accessor plus a dump through `akerr_log_method` would let a startup self-check or a CI job print the whole map for a linked stack. Cheap, additive, @@ -63,6 +63,19 @@ exists for migration. Once consumers have moved to `akerr_register_status_name()`, make the set path a no-op or remove it and leave `akerr_name_for_status()` as pure lookup. +## 7. The library's own startup failure path is untested + +`__akerr_name_library_status()` and the band reservation in `akerr_init()` log +and then terminate when the library cannot name its own codes. Nothing exercises +that: it needs a build whose `AKERR_STATUS_NAME_SLOTS` is too small to hold the +library's own entries, and that macro is `PRIVATE` to the library target, so a +test executable cannot set it. Mutation testing reports those lines as surviving +mutants for this reason. + +Closing it means a second library target built with a tiny table plus a +`WILL_FAIL` test linked against it — worth doing when the CMake gains a +sanitizer variant (item 1), since that adds the same machinery. + ## Unrelated pre-existing issues - The `AKERR_USE_STDLIB=OFF` build does not compile at all: `bool`, `PATH_MAX` diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000..b9f6a0a --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,208 @@ +# Upgrade notice: custom status codes (1.0.0) + +Version 1.0.0 replaces the consumer-sized status-name array with a private +registry, and makes status-code ownership explicit and enforced. This is a +source and ABI break. The library now carries a version and an soname +(`libakerror.so.1`), so a stale installed library can no longer be silently +paired with a newer header — but anything built against a pre-1.0.0 header must +be rebuilt. + +What was removed: + +* `AKERR_MAX_ERR_VALUE` — the registry is sparse and accepts any `int`, so + consumers no longer size it. Delete every compile definition and source + reference. A stale `-DAKERR_MAX_ERR_VALUE=...` is now harmless but useless. +* `__AKERR_ERROR_NAMES` — the name table is private to the library. Code that + touched this data symbol was using an undocumented interface; use + `akerr_name_for_status()`. +* `AKERR_STATUS_RANGE_OK` and `AKERR_STATUS_NAME_OK` — the registry functions no + longer return an `int`. Success is a `NULL` `akerr_ErrorContext *`, like every + other function in the library. See "The registry raises errors" below. + +To migrate: + +1. Rebuild libakerror and every dependent library against the new header. +2. Move custom status codes out of the reserved `0`–`255` band. Use fixed + integer constants beginning at `AKERR_FIRST_CONSUMER_STATUS` (256) rather + than offsets from `AKERR_LAST_ERRNO_VALUE`, so a libc that grows an errno + cannot move your codes. +3. Assign a distinct range to every library that may coexist in one process, and + coordinate those ranges at the application or dependency-stack level. +4. Reserve the complete range with `akerr_reserve_status_range()` during + initialization, and treat the error it returns like any other error: `CATCH` + it, `PASS` it, or let it propagate out of your init function. +5. Register names with `akerr_register_status_name()`, passing the same owner + string you reserved with. **You can no longer name a status you have not + reserved** — see "Ownership is enforced" below. + +For example: + +```c +enum { + MYLIB_ERR_BASE = AKERR_FIRST_CONSUMER_STATUS, /* 256 */ + MYLIB_ERR_PARSE = MYLIB_ERR_BASE, + MYLIB_ERR_STORAGE, + MYLIB_ERR_LIMIT +}; + +#define MYLIB_OWNER "mylib" + +akerr_ErrorContext AKERR_NOIGNORE *mylib_init(void) +{ + PREPARE_ERROR(errctx); + + /* Any collision propagates out of mylib_init() to the caller. */ + PASS(errctx, akerr_reserve_status_range(MYLIB_ERR_BASE, + MYLIB_ERR_LIMIT - MYLIB_ERR_BASE, + MYLIB_OWNER)); + PASS(errctx, akerr_register_status_name(MYLIB_OWNER, MYLIB_ERR_PARSE, + "Parse Error")); + PASS(errctx, akerr_register_status_name(MYLIB_OWNER, MYLIB_ERR_STORAGE, + "Storage Error")); + SUCCEED_RETURN(errctx); +} +``` + +If your `init` cannot return an `akerr_ErrorContext *` — a C API with an `int` +return, say — catch the error and convert it. Close with `FINISH_NORETURN` +rather than `FINISH`: nothing can propagate out of a function that does not +return an error context, and `FINISH(errctx, false)` in an `int`-returning +function compiles the (dead) propagation branch anyway, which warns. + +```c +int mylib_init(void) +{ + PREPARE_ERROR(errctx); + int rc = MYLIB_INIT_OK; + + ATTEMPT { + CATCH(errctx, akerr_reserve_status_range(MYLIB_ERR_BASE, + MYLIB_ERR_LIMIT - MYLIB_ERR_BASE, + MYLIB_OWNER)); + } CLEANUP { + } PROCESS(errctx) { + } HANDLE_DEFAULT(errctx) { + LOG_ERROR_WITH_MESSAGE(errctx, "mylib could not claim its status range"); + rc = MYLIB_INIT_FAILED; /* another component owns part of the range */ + } FINISH_NORETURN(errctx); + + return rc; +} +``` + +You do not need to call `akerr_init()` first. Every registry entry point calls +it for you, so a library that reserves its range before anything else in the +process has touched libakerror keeps that reservation. (Before 1.0.0 this was +silently destructive: `akerr_init()` cleared the tables, so a reservation made +too early was discarded and the next component to claim the same range was told +it was free.) + +## Ownership is enforced + +Reserving a range is no longer advisory bookkeeping. A status may only be named +from inside a reservation: + +* `akerr_register_status_name(owner, status, name)` requires that `status` fall + in a range reserved by `owner`. Naming another component's status fails with + `AKERR_STATUS_NAME_FOREIGN`, and naming a status nobody reserved fails with + `AKERR_STATUS_NAME_UNRESERVED`. +* The two-argument `akerr_name_for_status(status, name)` set path still works, + but it cannot identify its caller, so it can only require that *some* + reservation covers the status. Prefer the owned form: it is the one that + catches a component writing into a range that is not its own. + +Every refusal is reported, because a name that fails to register degrades that +code to `"Unknown Error"` in every later stack trace. + +This detects *name* collisions. It cannot detect two components compiling the +same integer into a `HANDLE` label without ever registering a name, so every +co-resident library should still reserve its range. + +## The registry raises errors + +`akerr_reserve_status_range()` and `akerr_register_status_name()` return +`akerr_ErrorContext *`, exactly like the rest of the library. They are marked +`AKERR_NOIGNORE`, so discarding the result is a compile-time warning, and an +error you catch but do not handle propagates out of your init function instead +of leaving you with a range you do not actually own. + +```c +ATTEMPT { + CATCH(errctx, akerr_reserve_status_range(256, 16, MYLIB_OWNER)); +} CLEANUP { +} PROCESS(errctx) { +} HANDLE(errctx, AKERR_STATUS_RANGE_OVERLAP) { + /* Another component owns part of it -- the message names which. */ +} FINISH(errctx, true); +``` + +`akerr_reserve_status_range()` raises: + +| Status | Meaning | +| --- | --- | +| — (returns `NULL`) | Range reserved, or an identical range was already reserved by the same owner | +| `AKERR_STATUS_RANGE_OVERLAP` | Part of the range is owned by someone else (the message names the owner) | +| `AKERR_STATUS_RANGE_FULL` | No reservation slots remain | +| `AKERR_STATUS_RANGE_INVALID` | `count < 1`, NULL/empty/over-long owner, or the range overflows `int` | + +`akerr_register_status_name()` raises: + +| Status | Meaning | +| --- | --- | +| — (returns `NULL`) | Name registered | +| `AKERR_STATUS_NAME_UNRESERVED` | No reservation contains this status | +| `AKERR_STATUS_NAME_FOREIGN` | The status is in a range owned by someone else | +| `AKERR_STATUS_NAME_FULL` | The name registry is full | +| `AKERR_STATUS_NAME_INVALID` | NULL/empty/over-long owner, or a NULL name | + +These are ordinary status codes inside the library's reserved band, so they can +be matched with `HANDLE`, grouped with `HANDLE_GROUP`, and they print with a +name and a stack trace when they go unhandled. + +`akerr_name_for_status(status, name)` is the one exception: it returns a name +rather than an error context, so it cannot raise. A refused registration through +that path is reported through `akerr_log_method` and reads back as +`"Unknown Error"`. + +Repeating an identical reservation for the same owner is a no-op. A *subset* or +*superset* of your own range is not — it raises `AKERR_STATUS_RANGE_OVERLAP`. +Reserve the whole range in one call. + +The library holds itself to the same rule at startup: if `akerr_init()` cannot +reserve its own `0`–`255` band or name one of its own codes, it logs the failure +and terminates the program. That can only happen in a misconfigured build (a +name table too small for the library's own entries), and the alternative is a +process whose stack traces silently read `"Unknown Error"` for built-in codes. + +## Capacity + +Both tables are fixed size, allocated in BSS, and never grow: + +| Limit | Default | Build-time override | +| --- | --- | --- | +| Status names | 3072 usable (4096 slots, 75% load) | `-DAKERR_STATUS_NAME_SLOTS=` | +| Reserved ranges | 64 | `-DAKERR_MAX_RESERVED_STATUS_RANGES=` | + +`akerr_init()` consumes one name slot per host errno value plus one per +`AKERR_*` code — around 150 on glibc, leaving roughly 2900 for all consumers in +the process to share. That figure varies with the host libc, so treat it as +approximate rather than a budget to fill. + +Both overrides are CMake cache variables and apply `PRIVATE` to the library +target. The tables live entirely inside `src/error.c`, so raising them changes +nothing a consumer can observe — unlike the `AKERR_MAX_ERR_VALUE` they replaced, +where a mismatch between the library and its consumers corrupted memory. Set +them when configuring libakerror itself: + +```sh +cmake -S . -B build -DAKERR_STATUS_NAME_SLOTS=16384 +``` + +Exhausting either table raises an error to the caller; it is never silent. + +## Thread safety + +The registry is process-global mutable state with no locking. Reserve ranges and +register names during single-threaded initialization, before spawning threads. +Lookups (`akerr_name_for_status(status, NULL)`) are safe to call concurrently +once registration has finished. diff --git a/include/akerror.tmpl.h b/include/akerror.tmpl.h index 26f4b57..0a25bb8 100644 --- a/include/akerror.tmpl.h +++ b/include/akerror.tmpl.h @@ -39,6 +39,25 @@ #define AKERR_NOT_IMPLEMENTED (AKERR_LAST_ERRNO_VALUE + 16) /** A method was called that is defined but not currently implemented */ #define AKERR_BADEXC (AKERR_LAST_ERRNO_VALUE + 17) /** The libakerr library was given an akerr_ErrorContext to parse that did not come from AKERR_ARRAY_ERROR (likely an uninitialized pointer) */ +/* + * Registry failures. These are ordinary status codes, not a private return + * enumeration: akerr_reserve_status_range() and akerr_register_status_name() + * return akerr_ErrorContext * like everything else in this library, so a refused + * reservation can be CATCH-ed, HANDLE-d, PASS-ed, or left unhandled to produce a + * stack trace and stop the program. Success returns NULL. + */ +#define AKERR_STATUS_RANGE_OVERLAP (AKERR_LAST_ERRNO_VALUE + 18) /** Some part of the range is already owned by someone else */ +#define AKERR_STATUS_RANGE_FULL (AKERR_LAST_ERRNO_VALUE + 19) /** No reservation slots remain (see AKERR_MAX_RESERVED_STATUS_RANGES) */ +#define AKERR_STATUS_RANGE_INVALID (AKERR_LAST_ERRNO_VALUE + 20) /** Bad count, bad owner string, or the range overflows int */ +#define AKERR_STATUS_NAME_UNRESERVED (AKERR_LAST_ERRNO_VALUE + 21) /** No owner has reserved a range containing this status */ +#define AKERR_STATUS_NAME_FOREIGN (AKERR_LAST_ERRNO_VALUE + 22) /** The status lies in a range reserved by a different owner */ +#define AKERR_STATUS_NAME_FULL (AKERR_LAST_ERRNO_VALUE + 23) /** The name registry is full (raise AKERR_STATUS_NAME_SLOTS) */ +#define AKERR_STATUS_NAME_INVALID (AKERR_LAST_ERRNO_VALUE + 24) /** NULL/empty/over-long owner, or a NULL name */ + +/* The last status the library defines for itself. Everything from + * AKERR_LAST_ERRNO_VALUE + 1 through here must have a registered name. */ +#define AKERR_LAST_LIBRARY_STATUS AKERR_STATUS_NAME_INVALID + /* * Status values 0 through 255 are reserved by libakerror at akerr_init() time: * the host's errno values plus the AKERR_* codes above. Consumers allocate from @@ -49,19 +68,6 @@ #define AKERR_RESERVED_STATUS_COUNT 256 #define AKERR_FIRST_CONSUMER_STATUS AKERR_RESERVED_STATUS_COUNT -/* Results of akerr_reserve_status_range(). */ -#define AKERR_STATUS_RANGE_OK 0 /** The range is now reserved by the caller */ -#define AKERR_STATUS_RANGE_OVERLAP 1 /** Some part of the range is already owned by someone else */ -#define AKERR_STATUS_RANGE_FULL 2 /** No reservation slots remain (see AKERR_MAX_RESERVED_STATUS_RANGES) */ -#define AKERR_STATUS_RANGE_INVALID 3 /** Bad count, bad owner string, or the range overflows int */ - -/* Results of akerr_register_status_name(). */ -#define AKERR_STATUS_NAME_OK 0 /** The name is registered */ -#define AKERR_STATUS_NAME_UNRESERVED 1 /** No owner has reserved a range containing this status */ -#define AKERR_STATUS_NAME_FOREIGN 2 /** The status lies in a range reserved by a different owner */ -#define AKERR_STATUS_NAME_FULL 3 /** The name registry is full (raise AKERR_STATUS_NAME_SLOTS) */ -#define AKERR_STATUS_NAME_INVALID 4 /** NULL/empty/over-long owner, or a NULL name */ - /* * The library reserves status values 0 through 255 for itself (see akerr_init), * which must contain every AKERR_* code above. AKERR_LAST_ERRNO_VALUE is @@ -69,7 +75,7 @@ * unusually large errno space these codes could escape the band and collide * with consumer codes allocated at 256. Fail the build instead. */ -typedef char akerr_assert_codes_within_reserved_band[(AKERR_BADEXC < 256) ? 1 : -1]; +typedef char akerr_assert_codes_within_reserved_band[(AKERR_LAST_LIBRARY_STATUS < 256) ? 1 : -1]; #define AKERR_MAX_ARRAY_ERROR 128 @@ -105,32 +111,43 @@ akerr_ErrorContext AKERR_NOIGNORE *akerr_next_error(); * Look up (name == NULL) or register (name != NULL) the display name for a * status. Registration succeeds only if some owner has reserved a range * containing `status`; prefer akerr_register_status_name(), which also checks - * that the range belongs to you and reports why a registration was refused. + * that the range belongs to you and raises an error saying why a registration + * was refused. This entry point cannot return an error context, so a refusal + * here is reported through akerr_log_method and reads back as "Unknown Error". * Never returns NULL -- an unregistered status reads back as "Unknown Error". */ char *akerr_name_for_status(int status, char *name); /* - * Register a display name for a status you own. Returns AKERR_STATUS_NAME_OK, - * or one of the AKERR_STATUS_NAME_* codes above; every refusal is also logged - * through akerr_log_method. `owner` must match the owner string passed to - * akerr_reserve_status_range() for the range containing `status`. + * Register a display name for a status you own. `owner` must match the owner + * string passed to akerr_reserve_status_range() for the range containing + * `status`. Returns NULL on success, or an error context whose status is one of + * the AKERR_STATUS_NAME_* codes above -- CATCH it, HANDLE it, or let it + * propagate. */ -int akerr_register_status_name(const char *owner, int status, const char *name); +akerr_ErrorContext AKERR_NOIGNORE *akerr_register_status_name(const char *owner, int status, const char *name); /* * Claim `count` status values starting at `first_status` for `owner`. Repeating - * an identical reservation for the same owner is a no-op; any other collision - * is refused and logged. Returns one of the AKERR_STATUS_RANGE_* codes; treat - * anything other than AKERR_STATUS_RANGE_OK as an initialization failure. + * an identical reservation for the same owner is a no-op; any other collision is + * refused. Returns NULL on success, or an error context whose status is one of + * the AKERR_STATUS_RANGE_* codes above. Treat any error as an initialization + * failure: either handle it or let it propagate out of your init function. */ -int akerr_reserve_status_range(int first_status, int count, const char *owner); +akerr_ErrorContext AKERR_NOIGNORE *akerr_reserve_status_range(int first_status, int count, const char *owner); void akerr_init(); void akerr_default_handler_unhandled_error(akerr_ErrorContext *ptr); void akerr_default_logger(const char *f, ...); int akerr_valid_error_address(akerr_ErrorContext *ptr); /* defined in src/errno.c which is built dynamically at build time from system errno definitions */ void akerr_init_errno(void); +/* + * Internal. Names a status in the library's own reserved band on behalf of + * akerr_init() and the generated errno table, which have no caller to raise + * into: a failure here is logged and terminates the program. Not part of the + * consumer API -- use akerr_register_status_name(), which raises instead. + */ +void __akerr_name_library_status(int status, const char *name); #define LOG_ERROR_WITH_MESSAGE(__err_context, __err_message) \ akerr_log_method("%s%s:%s:%d: %s %d (%s): %s", (char *)&__err_context->stacktracebuf, (char *)__FILE__, (char *)__func__, __LINE__, __err_message, __err_context->status, akerr_name_for_status(__err_context->status, NULL), __err_context->message); \ diff --git a/scripts/generrno.sh b/scripts/generrno.sh index 247db93..b16d8a8 100644 --- a/scripts/generrno.sh +++ b/scripts/generrno.sh @@ -8,13 +8,24 @@ mkdir -p ${outdir}/include rm -f ${outdir}/src/errno.c echo "#include " >> ${outdir}/src/errno.c echo "#include " >> ${outdir}/src/errno.c +cat >> ${outdir}/src/errno.c <<'EOF' + +/* + * These names belong to the library's own reserved band, and this runs from + * akerr_init(), which has no caller to raise into -- so it goes through + * __akerr_name_library_status(), which reports a refusal and terminates rather + * than leaving every later stack trace to print "Unknown Error" for an errno. + * Keeping the branch in src/error.c also keeps this generated file free of + * control flow no test can reach. + */ +EOF echo "void akerr_init_errno(void) {" >> ${outdir}/src/errno.c maxval=$(errno --list | cut -d ' ' -f 2 | sort -g | tail -n 1) errno --list | while read LINE; do define=$(echo "$LINE" | cut -d ' ' -f 1); value=$(echo "$LINE" | cut -d ' ' -f 2); desc=$(echo "$LINE" | cut -d ' ' -f 3-); - echo " akerr_register_status_name(AKERR_LIBRARY_OWNER, ${define}, \"${desc}\");" >> ${outdir}/src/errno.c ; + echo " __akerr_name_library_status(${define}, \"${desc}\");" >> ${outdir}/src/errno.c ; done; echo "}" >> ${outdir}/src/errno.c sed "s/#define AKERR_LAST_ERRNO_VALUE .*/#define AKERR_LAST_ERRNO_VALUE ${maxval}/" ${srcdir}/include/akerror.tmpl.h > ${outdir}/include/akerror.h diff --git a/src/error.c b/src/error.c index 1b7958a..18ed3b2 100644 --- a/src/error.c +++ b/src/error.c @@ -106,6 +106,30 @@ void akerr_default_logger(const char *fmt, ...) #endif } +/* + * The library naming its own codes. The registry entry points raise errors like + * everything else in the library, but akerr_init() has no caller to raise into, + * so this is where the buck stops: log the failure and take the unhandled-error + * path, which terminates the program. + * + * This is fatal on purpose. The library cannot name its own status codes only + * if the build is misconfigured -- a name table too small to hold even the + * library's own entries, or a reservation that did not take -- and the + * consequence of continuing is every later stack trace in the process printing + * "Unknown Error" for a code the library defines. That is a startup defect, and + * it is far cheaper to see it at init than to debug it from a degraded trace. + */ +void __akerr_name_library_status(int status, const char *name) +{ + akerr_ErrorContext *errctx = akerr_register_status_name(AKERR_LIBRARY_OWNER, + status, name); + if ( errctx != NULL ) { + LOG_ERROR_WITH_MESSAGE(errctx, "** libakerror could not name its own status **"); + akerr_handler_unhandled_error(errctx); + RELEASE_ERROR(errctx); + } +} + /* * Idempotent. `inited` is set before any work so that the registry calls below * -- and the public registry entry points, which all call akerr_init() so that @@ -138,27 +162,44 @@ void akerr_init() /* errno and AKERR_* values are the library-owned compatibility band. * This must precede every registration below: naming a status is only * permitted inside a reserved range. */ - (void)akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT, - AKERR_LIBRARY_OWNER); + akerr_ErrorContext *reserve_err = + akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT, + AKERR_LIBRARY_OWNER); + if ( reserve_err != NULL ) { + /* Fatal for the same reason as __akerr_name_library_status(): + * without this band the library owns nothing, so none of the names + * below can register either. */ + LOG_ERROR_WITH_MESSAGE(reserve_err, + "** libakerror could not reserve its own status band **"); + akerr_handler_unhandled_error(reserve_err); + RELEASE_ERROR(reserve_err); + } /* Every AKERR_* code gets a name; tests/err_error_names.c asserts the * list is exhaustive so a new code cannot be added without one. */ - akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_NULLPOINTER, "Null Pointer Error"); - akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_OUTOFBOUNDS, "Out Of Bounds Error"); - akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_API, "API Error"); - akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_ATTRIBUTE, "Attribute Error"); - akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_TYPE, "Type Error"); - akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_KEY, "Key Error"); - akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_INDEX, "Index Error"); - akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_FORMAT, "Format Error"); - akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_IO, "Input Output Error"); - akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_VALUE, "Value Error"); - akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_RELATIONSHIP, "Relationship Error"); - akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_EOF, "End Of File"); - akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_CIRCULAR_REFERENCE, "Circular Reference Error"); - akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_ITERATOR_BREAK, "Iterator Break"); - akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_NOT_IMPLEMENTED, "Not Implemented"); - akerr_register_status_name(AKERR_LIBRARY_OWNER, AKERR_BADEXC, "Invalid akerr_ErrorContext"); + __akerr_name_library_status(AKERR_NULLPOINTER, "Null Pointer Error"); + __akerr_name_library_status(AKERR_OUTOFBOUNDS, "Out Of Bounds Error"); + __akerr_name_library_status(AKERR_API, "API Error"); + __akerr_name_library_status(AKERR_ATTRIBUTE, "Attribute Error"); + __akerr_name_library_status(AKERR_TYPE, "Type Error"); + __akerr_name_library_status(AKERR_KEY, "Key Error"); + __akerr_name_library_status(AKERR_INDEX, "Index Error"); + __akerr_name_library_status(AKERR_FORMAT, "Format Error"); + __akerr_name_library_status(AKERR_IO, "Input Output Error"); + __akerr_name_library_status(AKERR_VALUE, "Value Error"); + __akerr_name_library_status(AKERR_RELATIONSHIP, "Relationship Error"); + __akerr_name_library_status(AKERR_EOF, "End Of File"); + __akerr_name_library_status(AKERR_CIRCULAR_REFERENCE, "Circular Reference Error"); + __akerr_name_library_status(AKERR_ITERATOR_BREAK, "Iterator Break"); + __akerr_name_library_status(AKERR_NOT_IMPLEMENTED, "Not Implemented"); + __akerr_name_library_status(AKERR_BADEXC, "Invalid akerr_ErrorContext"); + __akerr_name_library_status(AKERR_STATUS_RANGE_OVERLAP, "Status Range Overlap"); + __akerr_name_library_status(AKERR_STATUS_RANGE_FULL, "Status Range Table Full"); + __akerr_name_library_status(AKERR_STATUS_RANGE_INVALID, "Invalid Status Range"); + __akerr_name_library_status(AKERR_STATUS_NAME_UNRESERVED, "Unreserved Status Name"); + __akerr_name_library_status(AKERR_STATUS_NAME_FOREIGN, "Foreign Status Name"); + __akerr_name_library_status(AKERR_STATUS_NAME_FULL, "Status Name Registry Full"); + __akerr_name_library_status(AKERR_STATUS_NAME_INVALID, "Invalid Status Name"); #if (defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1) || (!defined(AKERR_USE_STDLIB)) akerr_init_errno(); #endif @@ -280,76 +321,91 @@ static akerr_StatusRange *akerr_range_for_status(int status) * Shared body of both registration entry points. A NULL owner means the caller * did not identify itself (the legacy two-argument akerr_name_for_status path): * the status must still lie inside *some* reservation, but we cannot check that - * it is the caller's. Every refusal is logged -- a name that silently fails to - * register degrades into "Unknown Error" in stack traces, which is exactly the - * kind of quiet loss this registry exists to prevent. + * it is the caller's. Every refusal raises an error -- a name that silently + * fails to register degrades into "Unknown Error" in stack traces, which is + * exactly the kind of quiet loss this registry exists to prevent -- so the + * message carries everything a caller needs to see in a stack trace. */ -static int akerr_store_status_name(const char *owner, int status, const char *name) +static akerr_ErrorContext AKERR_NOIGNORE *akerr_store_status_name(const char *owner, + int status, + const char *name) { akerr_StatusRange *range; akerr_StatusName *entry; + PREPARE_ERROR(errctx); if ( name == NULL ) { - return AKERR_STATUS_NAME_INVALID; + FAIL_RETURN(errctx, AKERR_STATUS_NAME_INVALID, + "Refusing to name status %d for %s: the name is NULL", + status, owner == NULL ? "an unnamed caller" : owner); } if ( owner != NULL && ( owner[0] == '\0' || strlen(owner) >= AKERR_MAX_STATUS_RANGE_OWNER_LENGTH ) ) { - return AKERR_STATUS_NAME_INVALID; + FAIL_RETURN(errctx, AKERR_STATUS_NAME_INVALID, + "Refusing to name status %d (\"%s\"): the owner string is " + "empty or longer than %d characters", + status, name, AKERR_MAX_STATUS_RANGE_OWNER_LENGTH - 1); } range = akerr_range_for_status(status); if ( range == NULL ) { - if ( akerr_log_method != NULL ) { - akerr_log_method("Refusing to name status %d (\"%s\") for %s: no " - "reserved range contains it. Call " - "akerr_reserve_status_range() first.\n", - status, name, owner == NULL ? "an unnamed caller" : owner); - } - return AKERR_STATUS_NAME_UNRESERVED; + FAIL_RETURN(errctx, AKERR_STATUS_NAME_UNRESERVED, + "Refusing to name status %d (\"%s\") for %s: no reserved " + "range contains it. Call akerr_reserve_status_range() first.", + status, name, owner == NULL ? "an unnamed caller" : owner); } if ( owner != NULL && strcmp(owner, range->owner) != 0 ) { - if ( akerr_log_method != NULL ) { - akerr_log_method("Refusing to name status %d (\"%s\") for %s: that " - "status is in range %d..%d owned by %s.\n", - status, name, owner, - range->first, range->last, range->owner); - } - return AKERR_STATUS_NAME_FOREIGN; + FAIL_RETURN(errctx, AKERR_STATUS_NAME_FOREIGN, + "Refusing to name status %d (\"%s\") for %s: that status is " + "in range %d..%d owned by %s.", + status, name, owner, + range->first, range->last, range->owner); } entry = akerr_status_slot(status, 1); if ( entry == NULL ) { - if ( akerr_log_method != NULL ) { - akerr_log_method("Status name registry is full (%d entries); " - "dropping name \"%s\" for status %d. Rebuild " - "libakerror with a larger AKERR_STATUS_NAME_SLOTS.\n", - AKERR_MAX_REGISTERED_STATUS_NAMES, name, status); - } - return AKERR_STATUS_NAME_FULL; + FAIL_RETURN(errctx, AKERR_STATUS_NAME_FULL, + "Status name registry is full (%d entries); dropping name " + "\"%s\" for status %d. Rebuild libakerror with a larger " + "AKERR_STATUS_NAME_SLOTS.", + AKERR_MAX_REGISTERED_STATUS_NAMES, name, status); } akerr_copy_string(entry->name, AKERR_MAX_ERROR_NAME_LENGTH, name); - return AKERR_STATUS_NAME_OK; + SUCCEED_RETURN(errctx); } /* Register a name for a status inside a range the caller reserved. */ -int akerr_register_status_name(const char *owner, int status, const char *name) +akerr_ErrorContext *akerr_register_status_name(const char *owner, int status, const char *name) { - akerr_init(); - if ( owner == NULL ) { - return AKERR_STATUS_NAME_INVALID; - } - return akerr_store_status_name(owner, status, name); + PREPARE_ERROR(errctx); + + FAIL_ZERO_RETURN(errctx, (owner != NULL), AKERR_STATUS_NAME_INVALID, + "Refusing to name status %d: the owner string is NULL. " + "Pass the same owner you reserved the range with.", + status); + PASS(errctx, akerr_store_status_name(owner, status, name)); + SUCCEED_RETURN(errctx); } -/* Return or set a name. Status magnitude is unrelated to storage size. */ +/* + * Return or set a name. Status magnitude is unrelated to storage size. + * + * The set path is the legacy two-argument form and cannot hand an error back to + * its caller (it returns a name), so a refusal is logged here and released. + * akerr_register_status_name() is the form that raises. + */ char *akerr_name_for_status(int status, char *name) { akerr_StatusName *entry; akerr_init(); - if ( name != NULL && - akerr_store_status_name(NULL, status, name) != AKERR_STATUS_NAME_OK ) { - return "Unknown Error"; + if ( name != NULL ) { + akerr_ErrorContext *errctx = akerr_store_status_name(NULL, status, name); + if ( errctx != NULL ) { + LOG_ERROR_WITH_MESSAGE(errctx, "** REFUSED STATUS NAME **"); + RELEASE_ERROR(errctx); + return "Unknown Error"; + } } entry = akerr_status_slot(status, 0); if ( entry == NULL ) { @@ -359,15 +415,21 @@ char *akerr_name_for_status(int status, char *name) } /* Reserve an inclusive status interval and reject collisions. */ -int akerr_reserve_status_range(int first_status, int count, const char *owner) +akerr_ErrorContext *akerr_reserve_status_range(int first_status, int count, const char *owner) { int last_status; + PREPARE_ERROR(errctx); - akerr_init(); if ( count <= 0 || owner == NULL || owner[0] == '\0' || strlen(owner) >= AKERR_MAX_STATUS_RANGE_OWNER_LENGTH || first_status > INT_MAX - (count - 1) ) { - return AKERR_STATUS_RANGE_INVALID; + FAIL_RETURN(errctx, AKERR_STATUS_RANGE_INVALID, + "Invalid status range reservation: %d status values from " + "%d for %s (count must be positive, the owner string " + "non-empty and shorter than %d characters, and the range " + "must not overflow int)", + count, first_status, owner == NULL ? "(null)" : owner, + AKERR_MAX_STATUS_RANGE_OWNER_LENGTH); } last_status = first_status + count - 1; @@ -377,27 +439,23 @@ int akerr_reserve_status_range(int first_status, int count, const char *owner) if ( first_status == akerr_status_ranges[i].first && last_status == akerr_status_ranges[i].last && strcmp(owner, akerr_status_ranges[i].owner) == 0 ) { - return AKERR_STATUS_RANGE_OK; + SUCCEED_RETURN(errctx); } - if ( akerr_log_method != NULL ) { - akerr_log_method("Status range %d..%d requested by %s overlaps " - "%d..%d owned by %s\n", - first_status, last_status, owner, - akerr_status_ranges[i].first, - akerr_status_ranges[i].last, - akerr_status_ranges[i].owner); - } - return AKERR_STATUS_RANGE_OVERLAP; + FAIL_RETURN(errctx, AKERR_STATUS_RANGE_OVERLAP, + "Status range %d..%d requested by %s overlaps %d..%d " + "owned by %s", + first_status, last_status, owner, + akerr_status_ranges[i].first, + akerr_status_ranges[i].last, + akerr_status_ranges[i].owner); } } if ( akerr_status_range_count == AKERR_MAX_RESERVED_STATUS_RANGES ) { - if ( akerr_log_method != NULL ) { - akerr_log_method("Status range table is full (%d ranges); " - "refusing %d..%d for %s.\n", - AKERR_MAX_RESERVED_STATUS_RANGES, - first_status, last_status, owner); - } - return AKERR_STATUS_RANGE_FULL; + FAIL_RETURN(errctx, AKERR_STATUS_RANGE_FULL, + "Status range table is full (%d ranges); refusing %d..%d " + "for %s.", + AKERR_MAX_RESERVED_STATUS_RANGES, + first_status, last_status, owner); } akerr_status_ranges[akerr_status_range_count].first = first_status; @@ -405,5 +463,5 @@ int akerr_reserve_status_range(int first_status, int count, const char *owner) akerr_copy_string(akerr_status_ranges[akerr_status_range_count].owner, AKERR_MAX_STATUS_RANGE_OWNER_LENGTH, owner); akerr_status_range_count++; - return AKERR_STATUS_RANGE_OK; + SUCCEED_RETURN(errctx); } diff --git a/tests/err_capture.h b/tests/err_capture.h index 04f40f2..2b3e951 100644 --- a/tests/err_capture.h +++ b/tests/err_capture.h @@ -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) diff --git a/tests/err_error_names.c b/tests/err_error_names.c index 8ea41c3..27e25b4 100644 --- a/tests/err_error_names.c +++ b/tests/err_error_names.c @@ -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; diff --git a/tests/err_maxval.c b/tests/err_maxval.c index 5ed8e61..b540dba 100644 --- a/tests/err_maxval.c +++ b/tests/err_maxval.c @@ -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; } diff --git a/tests/err_name_ownership.c b/tests/err_name_ownership.c index bd58458..56a1160 100644 --- a/tests/err_name_ownership.c +++ b/tests/err_name_ownership.c @@ -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; } diff --git a/tests/err_registry_init_order.c b/tests/err_registry_init_order.c index 6529c8b..7e8d034 100644 --- a/tests/err_registry_init_order.c +++ b/tests/err_registry_init_order.c @@ -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; diff --git a/tests/err_status_exception.c b/tests/err_status_exception.c new file mode 100644 index 0000000..44fd533 --- /dev/null +++ b/tests/err_status_exception.c @@ -0,0 +1,100 @@ +#include "akerror.h" +#include "err_capture.h" +#include + +/* + * 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; +}