Raise errors from the status registry instead of returning codes
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m47s
libakerror CI Build / coverage (push) Successful in 2m46s
libakerror CI Build / mutation_test (push) Successful in 14m56s

akerr_reserve_status_range() and akerr_register_status_name() returned
private int enumerations, which was the one place in the library where a
failure was not an akerr_ErrorContext *. They now return one like
everything else: NULL on success, and on refusal an error whose status is
a real code in the library's reserved band, so it can be CATCH-ed,
HANDLE-d, PASS-ed, or left to propagate into a stack trace. Both are
marked AKERR_NOIGNORE, so discarding the result warns at compile time.

AKERR_STATUS_RANGE_OK and AKERR_STATUS_NAME_OK are gone; the remaining
seven codes move into the AKERR_* offset span and get registered names.
AKERR_LAST_LIBRARY_STATUS replaces AKERR_BADEXC as the top of that span
in the reserved-band static assert and the exhaustiveness sweep.

The refusal detail that used to go straight to akerr_log_method now
travels in the error message, so a caller that handles the error decides
whether it is reported. The two-argument akerr_name_for_status() set path
is the exception: it returns a name and cannot raise, so it logs and
releases. akerr_init() likewise has no caller to raise into, so failing
to reserve its own band or name its own codes is logged and fatal --
that can only happen on a misconfigured build, and continuing would
degrade every later stack trace to "Unknown Error".

Move the 1.0.0 upgrade notice out of README.md into UPGRADING.md and
rewrite its return-code tables in terms of the statuses now raised.

Tests: ctest 29/29, coverage 97.5% line / 64.5% branch, mutation 77.5%
(was 77.6%; the new survivors are the fatal init path, which needs a
library built with an undersized name table -- TODO item 7).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 20:58:47 -04:00
parent ba2430bfa1
commit 64da04e83b
13 changed files with 741 additions and 380 deletions

View File

@@ -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

185
README.md
View File

@@ -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=<power of two>` |
| Reserved ranges | 64 | `-DAKERR_MAX_RESERVED_STATUS_RANGES=<n>` |
`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

17
TODO.md
View File

@@ -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`

208
UPGRADING.md Normal file
View File

@@ -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=<power of two>` |
| Reserved ranges | 64 | `-DAKERR_MAX_RESERVED_STATUS_RANGES=<n>` |
`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.

View File

@@ -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); \

View File

@@ -8,13 +8,24 @@ mkdir -p ${outdir}/include
rm -f ${outdir}/src/errno.c
echo "#include <akerror.h>" >> ${outdir}/src/errno.c
echo "#include <errno.h>" >> ${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

View File

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

View File

@@ -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)

View File

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

View File

@@ -11,7 +11,11 @@
* Covers: arbitrary int status values, name truncation, range reservation
* semantics (overlap, idempotency, endpoints, validation, overflow), and both
* capacity limits -- the range table and the name table -- each of which must
* report the failure rather than dropping the registration quietly.
* raise rather than dropping the registration quietly.
*
* Every refusal is an error context the caller owns, so each check below also
* asserts the context returns to the pool; a leak here would exhaust the
* 128-slot pool long before these loops finish.
*/
int main(void)
@@ -20,120 +24,119 @@ int main(void)
akerr_init();
/* Any int is a legal status, at either extreme of the range. */
AKERR_CHECK(akerr_reserve_status_range(INT_MIN, 1, "min-owner") ==
AKERR_STATUS_RANGE_OK);
AKERR_CHECK(akerr_reserve_status_range(INT_MAX, 1, "max-owner") ==
AKERR_STATUS_RANGE_OK);
AKERR_CHECK(akerr_register_status_name("max-owner", INT_MAX, "Maximum Status") ==
AKERR_STATUS_NAME_OK);
AKERR_CHECK(akerr_register_status_name("min-owner", INT_MIN, "Minimum Status") ==
AKERR_STATUS_NAME_OK);
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(INT_MIN, 1, "min-owner"));
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(INT_MAX, 1, "max-owner"));
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("max-owner", INT_MAX, "Maximum Status"));
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("min-owner", INT_MIN, "Minimum Status"));
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MAX, NULL), "Maximum Status") == 0);
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MIN, NULL), "Minimum Status") == 0);
/* A name longer than the buffer is truncated and always terminated. */
AKERR_CHECK(akerr_reserve_status_range(1000000, 1, "trunc") ==
AKERR_STATUS_RANGE_OK);
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(1000000, 1, "trunc"));
const char *long_name =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-extra";
AKERR_CHECK(akerr_register_status_name("trunc", 1000000, long_name) ==
AKERR_STATUS_NAME_OK);
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("trunc", 1000000, long_name));
char *stored = akerr_name_for_status(1000000, NULL);
AKERR_CHECK(strlen(stored) == AKERR_MAX_ERROR_NAME_LENGTH - 1);
AKERR_CHECK(stored[AKERR_MAX_ERROR_NAME_LENGTH - 1] == '\0');
/* Reservation: overlap detection, and idempotency for an exact repeat. */
AKERR_CHECK(akerr_reserve_status_range(256, 16, "component-a") ==
AKERR_STATUS_RANGE_OK);
AKERR_CHECK(akerr_reserve_status_range(256, 16, "component-a") ==
AKERR_STATUS_RANGE_OK);
AKERR_CHECK(akerr_reserve_status_range(260, 2, "component-b") ==
AKERR_STATUS_RANGE_OVERLAP);
AKERR_CHECK_CONTAINS("component-a");
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "component-a"));
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "component-a"));
AKERR_CHECK_RAISES(akerr_reserve_status_range(260, 2, "component-b"),
AKERR_STATUS_RANGE_OVERLAP);
AKERR_CHECK_MESSAGE_CONTAINS("component-a");
/* The library's own 0..255 band is reserved and cannot be encroached on. */
AKERR_CHECK(akerr_reserve_status_range(255, 1, "component-b") ==
AKERR_STATUS_RANGE_OVERLAP);
AKERR_CHECK(akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
AKERR_LIBRARY_OWNER) ==
AKERR_STATUS_RANGE_OK); /* exact repeat by the owner */
AKERR_CHECK_RAISES(akerr_reserve_status_range(255, 1, "component-b"),
AKERR_STATUS_RANGE_OVERLAP);
AKERR_CHECK_MESSAGE_CONTAINS(AKERR_LIBRARY_OWNER);
/* An exact repeat by the owner is still a no-op. */
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
AKERR_LIBRARY_OWNER));
/* Argument validation. */
AKERR_CHECK(akerr_reserve_status_range(INT_MAX, 2, "overflow") ==
AKERR_STATUS_RANGE_INVALID);
AKERR_CHECK(akerr_reserve_status_range(300, 0, "empty") ==
AKERR_STATUS_RANGE_INVALID);
AKERR_CHECK(akerr_reserve_status_range(300, -1, "negative") ==
AKERR_STATUS_RANGE_INVALID);
AKERR_CHECK(akerr_reserve_status_range(300, 1, NULL) ==
AKERR_STATUS_RANGE_INVALID);
AKERR_CHECK(akerr_reserve_status_range(300, 1, "") ==
AKERR_STATUS_RANGE_INVALID);
AKERR_CHECK_RAISES(akerr_reserve_status_range(INT_MAX, 2, "overflow"),
AKERR_STATUS_RANGE_INVALID);
AKERR_CHECK_RAISES(akerr_reserve_status_range(300, 0, "empty"),
AKERR_STATUS_RANGE_INVALID);
AKERR_CHECK_MESSAGE_CONTAINS("empty"); /* the message names the caller */
AKERR_CHECK_RAISES(akerr_reserve_status_range(300, -1, "negative"),
AKERR_STATUS_RANGE_INVALID);
AKERR_CHECK_RAISES(akerr_reserve_status_range(300, 1, NULL),
AKERR_STATUS_RANGE_INVALID);
AKERR_CHECK_RAISES(akerr_reserve_status_range(300, 1, ""),
AKERR_STATUS_RANGE_INVALID);
/* Owner strings: 63 chars fit, 64 do not. */
/* Owner strings: 63 chars fit, 64 do not, and neither does anything past. */
char owner63[AKERR_MAX_ERROR_NAME_LENGTH];
char owner64[AKERR_MAX_ERROR_NAME_LENGTH + 1];
char owner70[AKERR_MAX_ERROR_NAME_LENGTH + 7];
memset(owner63, 'a', sizeof(owner63) - 1);
owner63[sizeof(owner63) - 1] = '\0';
memset(owner64, 'b', sizeof(owner64) - 1);
owner64[sizeof(owner64) - 1] = '\0';
AKERR_CHECK(akerr_reserve_status_range(400, 1, owner63) ==
AKERR_STATUS_RANGE_OK);
AKERR_CHECK(akerr_reserve_status_range(401, 1, owner64) ==
AKERR_STATUS_RANGE_INVALID);
memset(owner70, 'c', sizeof(owner70) - 1);
owner70[sizeof(owner70) - 1] = '\0';
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(400, 1, owner63));
AKERR_CHECK_RAISES(akerr_reserve_status_range(401, 1, owner64),
AKERR_STATUS_RANGE_INVALID);
AKERR_CHECK_RAISES(akerr_reserve_status_range(402, 1, owner70),
AKERR_STATUS_RANGE_INVALID);
/* Partial overlaps at either endpoint, and a same-range different owner. */
AKERR_CHECK(akerr_reserve_status_range(500, 2, "endpoint") ==
AKERR_STATUS_RANGE_OK);
AKERR_CHECK(akerr_reserve_status_range(499, 2, "left") ==
AKERR_STATUS_RANGE_OVERLAP);
AKERR_CHECK(akerr_reserve_status_range(500, 1, "endpoint") ==
AKERR_STATUS_RANGE_OVERLAP); /* subset, not an exact repeat */
AKERR_CHECK(akerr_reserve_status_range(501, 1, "endpoint") ==
AKERR_STATUS_RANGE_OVERLAP);
AKERR_CHECK(akerr_reserve_status_range(500, 2, "other") ==
AKERR_STATUS_RANGE_OVERLAP); /* same range, wrong owner */
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(500, 2, "endpoint"));
AKERR_CHECK_RAISES(akerr_reserve_status_range(499, 2, "left"),
AKERR_STATUS_RANGE_OVERLAP);
AKERR_CHECK_RAISES(akerr_reserve_status_range(500, 1, "endpoint"),
AKERR_STATUS_RANGE_OVERLAP); /* subset, not an exact repeat */
AKERR_CHECK_RAISES(akerr_reserve_status_range(501, 1, "endpoint"),
AKERR_STATUS_RANGE_OVERLAP);
AKERR_CHECK_RAISES(akerr_reserve_status_range(500, 2, "other"),
AKERR_STATUS_RANGE_OVERLAP); /* same range, wrong owner */
/* Claim room for the name-exhaustion sweep before filling the range table. */
AKERR_CHECK(akerr_reserve_status_range(2000000, 100000, "fill") ==
AKERR_STATUS_RANGE_OK);
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(2000000, 100000, "fill"));
/*
* Range table capacity. The limit is private to src/error.c on purpose, so
* discover it by filling rather than by hardcoding it here.
*/
akerr_capture_reset();
int ranges_added = 0;
int range_rc = AKERR_STATUS_RANGE_OK;
akerr_ErrorContext *range_err = NULL;
for ( int i = 0; i < 100000; i++ ) {
range_rc = akerr_reserve_status_range(1000 + (i * 2), 1, "pad");
if ( range_rc != AKERR_STATUS_RANGE_OK ) {
range_err = akerr_reserve_status_range(1000 + (i * 2), 1, "pad");
if ( range_err != NULL ) {
break;
}
ranges_added++;
}
AKERR_CHECK(ranges_added > 0);
AKERR_CHECK(range_rc == AKERR_STATUS_RANGE_FULL);
AKERR_CHECK_CONTAINS("range table is full");
AKERR_CHECK_STATUS(range_err, AKERR_STATUS_RANGE_FULL);
AKERR_CHECK(strstr(range_err->message, "range table is full") != NULL);
RELEASE_ERROR(range_err);
AKERR_CHECK(range_err == NULL);
/*
* Name table capacity. Exhaustion must be reported, not silent: a dropped
* name degrades every future stack trace for that code to "Unknown Error".
*/
akerr_capture_reset();
int full_at = -1;
for ( int i = 0; i < 100000; i++ ) {
char name[32];
snprintf(name, sizeof(name), "Filled %d", i);
int rc = akerr_register_status_name("fill", 2000000 + i, name);
if ( rc != AKERR_STATUS_NAME_OK ) {
AKERR_CHECK(rc == AKERR_STATUS_NAME_FULL);
akerr_ErrorContext *name_err =
akerr_register_status_name("fill", 2000000 + i, name);
if ( name_err != NULL ) {
AKERR_CHECK_STATUS(name_err, AKERR_STATUS_NAME_FULL);
AKERR_CHECK(strstr(name_err->message, "registry is full") != NULL);
AKERR_CHECK(strstr(name_err->message, "AKERR_STATUS_NAME_SLOTS") != NULL);
RELEASE_ERROR(name_err);
AKERR_CHECK(name_err == NULL);
full_at = i;
break;
}
}
AKERR_CHECK_CONTAINS("registry is full");
AKERR_CHECK_CONTAINS("AKERR_STATUS_NAME_SLOTS");
/*
* The table must actually hold everything it accepted. A probe sequence
@@ -154,6 +157,9 @@ int main(void)
"Unknown Error") == 0);
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MIN, NULL), "Minimum Status") == 0);
/* Thousands of refusals later, every context went back to the pool. */
AKERR_CHECK(akerr_slots_in_use() == 0);
fprintf(stderr, "err_maxval ok (%d consumer names before full)\n", full_at);
return 0;
}

View File

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

View File

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

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