Stop an unhandled error from exiting zero
An unhandled error could kill the process and still report success. The default handler ended in exit(errctx->status), and an exit status is one byte wide: the kernel keeps the low 8 bits of the argument and discards the rest. Consumer statuses start at AKERR_FIRST_CONSUMER_STATUS (256), so the first status any consumer can reserve exited 0 and a shell saw a clean run. Status 300 exited 44, an unrelated error's code. There is no wider exit() to reach for. _exit(), _Exit(), quick_exit() and the raw exit_group syscall all truncate identically, and even waitid(), whose si_status is a full int, reports the truncated value -- the truncation happened before the parent looked. akerr_exit() now owns that mapping and the default handler calls it: 0 exits 0, 1 through 255 exit the status, and anything else exits AKERR_EXIT_STATUS_UNREPRESENTABLE (125) rather than a low byte that is either a lie or a claim of success. Only values that were already being delivered wrong behave differently. Call it instead of exit() anywhere you leave the process on a status; it is declared AKERR_NORETURN. akerr_exit(0) exits 0, because 0 is this library's success status. That is not a hole in the rule: PROCESS opens with case 0, which marks a zero status handled, so a successful context never reaches FINISH_NORETURN's call to the handler at all. tests/err_exit_status.c drives one table through akerr_exit() and through the default handler in forked children and requires identical exit codes, so the handler cannot grow a mapping of its own. With the clamp removed it fails with "akerr_exit(256) exited 0, want 125". The full-width status was already reaching the log and still does, which the same test asserts against the captured stack trace. 2.0.1. No ABI break: the soname stays libakerror.so.2 and nothing that already existed changed shape. akerr_exit() is a new exported symbol, so a consumer that starts calling it needs 2.0.1 at link time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
10
AGENTS.md
10
AGENTS.md
@@ -88,6 +88,16 @@ locked body does the work, and a thin wrapper takes the lock, calls it, and
|
||||
releases it on the single return path. Do not call consumer code
|
||||
(`akerr_log_method`, `akerr_handler_unhandled_error`) while holding it.
|
||||
|
||||
**Exiting.** Never call `exit()` with a status value. Call `akerr_exit()`, which
|
||||
owns the one mapping from an akerr status to an exit code: an exit status is a
|
||||
byte, and every consumer status starts at 256, so passing a status through
|
||||
`exit()` silently truncates it — status 256 exits 0 and reports success. The
|
||||
only exits that bypass it are the two that have no status to map, both in
|
||||
`src/error.c`: `ENSURE_ERROR_READY`'s pool-exhaustion abort and the NULL-context
|
||||
case in `akerr_default_handler_unhandled_error()`. This rule applies to test
|
||||
programs too, except where the test's whole point is to observe the raw
|
||||
truncation.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
Add tests as `tests/err_<behavior>.c`. Register each new test in the
|
||||
|
||||
@@ -6,7 +6,10 @@ cmake_minimum_required(VERSION 3.10)
|
||||
# now takes for it. Consumer code compiled against a 1.x header would
|
||||
# double-count every reference. Hence the major bump and the SOVERSION, so a
|
||||
# stale installed libakerror.so cannot be silently paired with new headers.
|
||||
project(akerror VERSION 2.0.0 LANGUAGES C)
|
||||
# 2.0.1 fixes the unhandled-error exit code, which reported success for any
|
||||
# status whose low byte was zero. It adds akerr_exit() but breaks nothing: the
|
||||
# soname is unchanged and no existing entry point changed shape.
|
||||
project(akerror VERSION 2.0.1 LANGUAGES C)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
include(CMakePackageConfigHelpers)
|
||||
@@ -217,6 +220,7 @@ set(AKERR_TESTS
|
||||
err_name_bounds
|
||||
err_format_string
|
||||
err_unhandled_null
|
||||
err_exit_status
|
||||
err_release_null
|
||||
err_release_refcount
|
||||
)
|
||||
|
||||
91
README.md
91
README.md
@@ -6,12 +6,17 @@ This library provides a TRY/CATCH style exception handling mechanism for C.
|
||||
|
||||
## Upgrading
|
||||
|
||||
2.0.1 fixes an unhandled error killing the process and still reporting success:
|
||||
the exit code was the status truncated to a byte, and every consumer status
|
||||
starts at 256. Use `akerr_exit()` instead of `exit()` — see
|
||||
[Exit status](#exit-status). No ABI break.
|
||||
|
||||
2.0.0 makes the library thread safe. That is an ABI break — `__akerr_last_ignored`
|
||||
became thread-local storage and the pool now takes its own reference — so
|
||||
everything built against a 1.x header must be rebuilt. 1.0.0 replaced the
|
||||
consumer-sized status-name array with a private, ownership-enforced registry.
|
||||
See [UPGRADING.md](UPGRADING.md) for both, what was removed, how to migrate, the
|
||||
capacity limits and how to raise them, and the thread-safety rules.
|
||||
See [UPGRADING.md](UPGRADING.md) for all three, what was removed, how to migrate,
|
||||
the capacity limits and how to raise them, and the thread-safety rules.
|
||||
|
||||
|
||||
# Why?
|
||||
@@ -78,7 +83,8 @@ You can define additional error types as integer constants. Values 0 through 255
|
||||
are reserved by libakerror (the host's errno values plus the `AKERR_*` codes);
|
||||
consumers allocate codes starting at `AKERR_FIRST_CONSUMER_STATUS` (256). Status
|
||||
names are stored sparsely, so any `int` is a legal status and no compile
|
||||
definition is needed to use large values.
|
||||
definition is needed to use large values. Note that no consumer status can be a
|
||||
process exit code — see [Exit status](#exit-status).
|
||||
|
||||
Every library that may coexist in one process must reserve its range during
|
||||
initialization. `akerr_reserve_status_range()` and
|
||||
@@ -151,7 +157,7 @@ What it does not cover, and cannot:
|
||||
Registering a *new* status concurrently is fine.
|
||||
* **Which unhandled error terminates the process.** An error that reaches
|
||||
`FINISH_NORETURN` unhandled prints its stack trace and calls
|
||||
`akerr_handler_unhandled_error`, which by default calls `exit()`. Each
|
||||
`akerr_handler_unhandled_error`, which by default calls `akerr_exit()`. Each
|
||||
thread's trace is whole — the buffer belongs to its context, and each line is
|
||||
one call to `akerr_log_method` — but if two threads get there at the same
|
||||
instant, both traces print and the exit status is whichever one won.
|
||||
@@ -567,3 +573,80 @@ From bottom to top, we have:
|
||||
* Above that, the `FINISH()` macro in `func2()` which detected an unhandled error and passed it out of the function
|
||||
* Above that, a reference to the line where the `FAIL()` macro set the error code and provided the message which is printed here
|
||||
|
||||
## Exit status
|
||||
|
||||
**Never call `exit()` with an akerr status. Call `akerr_exit()`.**
|
||||
|
||||
```c
|
||||
void akerr_exit(int status);
|
||||
```
|
||||
|
||||
This applies everywhere you are leaving the process on account of a status, not
|
||||
just in an unhandled-error handler: a CLI's top-level `HANDLE` block, an
|
||||
initialization routine that cannot continue, a `main()` that ends by reporting
|
||||
the status it finished with. One function owns the mapping so that a given
|
||||
status produces the same exit code no matter which of your exits it left by.
|
||||
|
||||
The mapping exists because a process exit status is one byte wide. `exit()`
|
||||
accepts an `int` and the kernel keeps the low 8 bits of it — `_exit()`,
|
||||
`_Exit()`, `quick_exit()` and the raw `exit_group` syscall all behave
|
||||
identically, and even `waitid()`, whose `si_status` is a full `int`, sees the
|
||||
truncated value because the truncation happened before the parent looked. There
|
||||
is no wider `exit()` to reach for.
|
||||
|
||||
That leaves 0 through 255 as the only statuses an exit code can carry, and
|
||||
consumer statuses start at `AKERR_FIRST_CONSUMER_STATUS` (256) — so *no* consumer
|
||||
status can be an exit code:
|
||||
|
||||
```
|
||||
status exit code
|
||||
0 0 (success)
|
||||
1 .. 255 the status
|
||||
negative, or > 255 AKERR_EXIT_STATUS_UNREPRESENTABLE (125)
|
||||
```
|
||||
|
||||
Passing the low byte instead would have made status 256 exit 0 and report
|
||||
success to the shell, and status 300 exit 44 — an unrelated error's code. 125 is
|
||||
the conventional "the tool itself failed" status; 126, 127 and 128+*n* already
|
||||
belong to the shell.
|
||||
|
||||
Status 0 exits 0, because 0 is this library's success status. That is not a hole
|
||||
in the rule that an unhandled error never exits 0: `PROCESS` opens with `case
|
||||
0`, which marks a zero status handled, so a successful context cannot reach
|
||||
`FINISH_NORETURN`'s call to the handler at all.
|
||||
|
||||
Above 255, the exit code tells you the process died of an error, not which one.
|
||||
125 is inside the library's reserved band and so is also some host's `errno`, and
|
||||
every status above 255 collapses onto it. **The stack trace is what identifies
|
||||
the error** — it is printed before the handler runs, carrying the status at full
|
||||
width along with its registered name.
|
||||
|
||||
### Replacing the handler
|
||||
|
||||
After the trace is printed, `FINISH_NORETURN` calls
|
||||
`akerr_handler_unhandled_error`. The default implementation,
|
||||
`akerr_default_handler_unhandled_error()`, hands `errctx->status` to
|
||||
`akerr_exit()` (a NULL context exits 1). Replace it if you need something else
|
||||
to happen first — a core dump, a crash reporter, a flush — and finish by calling
|
||||
`akerr_exit()` so the exit code still means what it means everywhere else:
|
||||
|
||||
```c
|
||||
static void mylib_handler(akerr_ErrorContext *e)
|
||||
{
|
||||
mylib_flush_telemetry();
|
||||
if ( e == NULL ) {
|
||||
akerr_exit(AKERR_API);
|
||||
}
|
||||
akerr_exit(e->status);
|
||||
}
|
||||
|
||||
akerr_handler_unhandled_error = &mylib_handler;
|
||||
```
|
||||
|
||||
`akerr_exit()` is declared `AKERR_NORETURN`, so the compiler knows a handler
|
||||
ending in one of those calls is complete rather than falling off the end.
|
||||
|
||||
Set the handler once, before you start any threads. `tests/err_custom_handler.c`
|
||||
installs one that does not exit at all, which is how the test suite asserts on
|
||||
unhandled errors without dying.
|
||||
|
||||
|
||||
42
UPGRADING.md
42
UPGRADING.md
@@ -1,3 +1,45 @@
|
||||
# Bug fix: unhandled-error exit status (2.0.1)
|
||||
|
||||
An unhandled error could kill the process and still report success.
|
||||
|
||||
`akerr_default_handler_unhandled_error()` ended in `exit(errctx->status)`, and a
|
||||
process exit status is one byte wide — the kernel keeps the low 8 bits of the
|
||||
argument and discards the rest. Consumer statuses start at
|
||||
`AKERR_FIRST_CONSUMER_STATUS` (256), so **the first status any consumer can
|
||||
reserve exited 0**, and a shell or supervisor watching `$?` saw a clean run.
|
||||
Status 300 exited 44, which is some unrelated error's code. No status a consumer
|
||||
owns could ever come out of `$?` intact, and there is no wider `exit()` to reach
|
||||
for: `_exit()`, `_Exit()`, `quick_exit()` and the raw `exit_group` syscall all
|
||||
truncate identically, and even `waitid()`, whose `si_status` is a full `int`,
|
||||
reports the truncated value.
|
||||
|
||||
The mapping now lives in one place, `akerr_exit()`, which the default handler
|
||||
calls:
|
||||
|
||||
```
|
||||
status exit code
|
||||
0 0 (success)
|
||||
1 .. 255 the status
|
||||
negative, or > 255 AKERR_EXIT_STATUS_UNREPRESENTABLE (125)
|
||||
```
|
||||
|
||||
Statuses 0 through 255 are unchanged, which covers every `errno` and every
|
||||
`AKERR_*` code. Only the values that were already being delivered wrong behave
|
||||
differently, and they now exit 125 instead of a truncated byte.
|
||||
|
||||
**What you should change.** Call `akerr_exit()` instead of `exit()` anywhere you
|
||||
leave the process on an akerr status — your own unhandled-error handler, a
|
||||
top-level `HANDLE` block, an init routine that cannot continue — so one mapping
|
||||
covers every exit. It is declared `AKERR_NORETURN`. If you were reading a
|
||||
consumer status out of `$?`, you were never getting it: read the stack trace,
|
||||
which carries the status at full width along with its registered name, or
|
||||
install a handler that maps your own statuses into a byte. See "Exit status" in
|
||||
[README.md](README.md).
|
||||
|
||||
No ABI break. The soname stays `libakerror.so.2` and nothing you already call
|
||||
changed shape. `akerr_exit()` is a new exported symbol, so a consumer that
|
||||
starts calling it needs 2.0.1 or later at link time.
|
||||
|
||||
# Upgrade notice: thread safety (2.0.0)
|
||||
|
||||
2.0.0 makes the library thread safe. Every entry point may be called from any
|
||||
|
||||
@@ -113,6 +113,26 @@
|
||||
*/
|
||||
typedef char akerr_assert_codes_within_reserved_band[(AKERR_LAST_LIBRARY_STATUS < 256) ? 1 : -1];
|
||||
|
||||
/*
|
||||
* A process exit status is one byte wide. exit() takes an int, but the kernel
|
||||
* keeps only the low 8 bits of it and throws the rest away, so 0 through 255
|
||||
* are the only statuses that can also be exit codes -- and every consumer status
|
||||
* begins at AKERR_FIRST_CONSUMER_STATUS (256). No wider variant exists to reach
|
||||
* for: _exit(), _Exit(), quick_exit() and the raw exit_group syscall all
|
||||
* truncate identically, and even waitid()'s int-wide si_status reports the
|
||||
* truncated value, because the truncation happened before the parent looked.
|
||||
*
|
||||
* akerr_exit() therefore substitutes AKERR_EXIT_STATUS_UNREPRESENTABLE for any
|
||||
* status it cannot deliver intact, rather than passing the low byte -- status
|
||||
* 256 would exit 0 and report success. 125 is the conventional "the tool itself
|
||||
* failed" code (126, 127 and 128+n belong to the shell). It is inside the
|
||||
* library's reserved band, so it is also some host's errno: the exit code says
|
||||
* only that the process died of an error, and the stack trace carries the real
|
||||
* status.
|
||||
*/
|
||||
#define AKERR_EXIT_STATUS_MAX 255
|
||||
#define AKERR_EXIT_STATUS_UNREPRESENTABLE 125
|
||||
|
||||
#define AKERR_MAX_ARRAY_ERROR 128
|
||||
|
||||
|
||||
@@ -132,6 +152,9 @@ typedef struct
|
||||
} akerr_ErrorContext;
|
||||
|
||||
#define AKERR_NOIGNORE __attribute__((warn_unused_result))
|
||||
/* akerr_exit() does not come back, and the compiler should know it: a handler
|
||||
* whose last statement is a call to it is complete, not falling off the end. */
|
||||
#define AKERR_NORETURN __attribute__((noreturn))
|
||||
|
||||
typedef void (*akerr_ErrorUnhandledErrorHandler)(akerr_ErrorContext *errctx);
|
||||
typedef void (*akerr_ErrorLogFunction)(const char *f, ...);
|
||||
@@ -190,6 +213,25 @@ akerr_ErrorContext AKERR_NOIGNORE *akerr_register_status_name(const char *owner,
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_reserve_status_range(int first_status, int count, const char *owner);
|
||||
void akerr_init();
|
||||
/*
|
||||
* Terminate the process, reporting `status`. Use this instead of exit()
|
||||
* anywhere you are leaving on account of an akerr status -- an unhandled-error
|
||||
* handler of your own, a CLI's top-level HANDLE block, an init routine that
|
||||
* cannot continue -- so that every exit out of the library's status space maps
|
||||
* the same way.
|
||||
*
|
||||
* Exits with `status` when 0 <= status <= AKERR_EXIT_STATUS_MAX, and with
|
||||
* AKERR_EXIT_STATUS_UNREPRESENTABLE otherwise (see above). Status 0 exits 0:
|
||||
* zero is this library's success status, and passing it here says the program
|
||||
* finished, not that it failed with a code that got lost.
|
||||
*/
|
||||
void AKERR_NORETURN akerr_exit(int status);
|
||||
/*
|
||||
* The default akerr_handler_unhandled_error: logs nothing further -- the stack
|
||||
* trace has already been printed by the time it runs -- and hands `ptr->status`
|
||||
* to akerr_exit(), or exits 1 when `ptr` is NULL. Replace it if you need a
|
||||
* different mapping, and call akerr_exit() from your replacement.
|
||||
*/
|
||||
void akerr_default_handler_unhandled_error(akerr_ErrorContext *ptr);
|
||||
void akerr_default_logger(const char *f, ...);
|
||||
int akerr_valid_error_address(akerr_ErrorContext *ptr);
|
||||
|
||||
37
src/error.c
37
src/error.c
@@ -305,12 +305,47 @@ void akerr_init()
|
||||
akerr_once(&akerr_state_once, &akerr_init_state);
|
||||
}
|
||||
|
||||
/*
|
||||
* Every way out of the library's status space goes through here, so that a
|
||||
* status becomes an exit code exactly one way no matter who is leaving.
|
||||
*
|
||||
* Only 0 through AKERR_EXIT_STATUS_MAX survive the trip -- see the note on
|
||||
* AKERR_EXIT_STATUS_UNREPRESENTABLE in the header for why there is no wider
|
||||
* exit() to reach for. Everything else exits with that sentinel instead of its
|
||||
* low byte, because the low byte is either a lie (status 300 exiting 44, which
|
||||
* is some other error's code) or a disaster (status 256, the first status a
|
||||
* consumer can own, exiting 0 and telling the shell the program succeeded).
|
||||
*
|
||||
* Status 0 exits 0, because 0 is this library's success status and an exit code
|
||||
* of 0 is what success is called out here. What keeps an *unhandled* error from
|
||||
* exiting 0 is not this function: PROCESS opens with `case 0`, which marks a
|
||||
* zero status handled, so a successful context can never reach
|
||||
* FINISH_NORETURN's call to the handler in the first place.
|
||||
*
|
||||
* Nothing is logged here. Callers arrive from a position that has already
|
||||
* reported -- FINISH_NORETURN logs the stack trace, carrying the status at full
|
||||
* width, before it calls the handler -- and a second line naming a number the
|
||||
* trace already gave would only invite the reader to trust the exit code.
|
||||
*/
|
||||
void akerr_exit(int status)
|
||||
{
|
||||
if ( status < 0 || status > AKERR_EXIT_STATUS_MAX ) {
|
||||
exit(AKERR_EXIT_STATUS_UNREPRESENTABLE);
|
||||
}
|
||||
exit(status);
|
||||
}
|
||||
|
||||
/*
|
||||
* The last stop for an unhandled error. A handler invoked with no error at all
|
||||
* has no status to report and nothing akerr_exit() could map, so it exits 1
|
||||
* directly: a plain generic failure.
|
||||
*/
|
||||
void akerr_default_handler_unhandled_error(akerr_ErrorContext *errctx)
|
||||
{
|
||||
if ( errctx == NULL ) {
|
||||
exit(1);
|
||||
}
|
||||
exit(errctx->status);
|
||||
akerr_exit(errctx->status);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
206
tests/err_exit_status.c
Normal file
206
tests/err_exit_status.c
Normal file
@@ -0,0 +1,206 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
/*
|
||||
* An unhandled error must never leave the process looking like a success.
|
||||
*
|
||||
* The default handler used to exit(errctx->status) unconditionally, and an exit
|
||||
* status is one byte wide: status 256 -- AKERR_FIRST_CONSUMER_STATUS, the very
|
||||
* first code any consumer can reserve -- exited 0 and told the shell the
|
||||
* program succeeded. Status 300 exited 44, which is some unrelated error's code.
|
||||
*
|
||||
* akerr_exit() now owns that mapping, and the default handler is one of its
|
||||
* callers. The same table therefore drives both: a status must produce the same
|
||||
* exit code whether a consumer calls akerr_exit() from their own handler or
|
||||
* lets the library's handler run.
|
||||
*
|
||||
* akerr_exit(0) exits 0 -- zero is the library's success status. The thing that
|
||||
* keeps an unhandled error off that path is PROCESS's `case 0`, which marks a
|
||||
* zero status handled before FINISH_NORETURN can reach the handler, and the
|
||||
* last three cases assert that, plus that the status an exit code could not
|
||||
* carry is still recoverable from the stack trace.
|
||||
*
|
||||
* Neither exit path returns, so those cases run in forked children.
|
||||
*/
|
||||
|
||||
#define TEST_OWNER "err_exit_status"
|
||||
#define TEST_STATUS AKERR_FIRST_CONSUMER_STATUS
|
||||
#define TEST_STATUS_NAME "Consumer Status Two Fifty Six"
|
||||
|
||||
static const struct {
|
||||
int status;
|
||||
int expect;
|
||||
} exit_cases[] = {
|
||||
/* status expected exit */
|
||||
{ 0, 0 }, /* Zero is the library's success status, and exit code 0 is what that is called out here */
|
||||
{ 1, 1 }, /* Lowest status an exit code can carry */
|
||||
{ AKERR_VALUE, AKERR_VALUE }, /* An ordinary library status, delivered intact */
|
||||
{ AKERR_EXIT_STATUS_MAX, AKERR_EXIT_STATUS_MAX }, /* Highest status an exit code can carry */
|
||||
{ AKERR_FIRST_CONSUMER_STATUS, AKERR_EXIT_STATUS_UNREPRESENTABLE }, /* 256: low byte 0, the case that used to exit success */
|
||||
{ 300, AKERR_EXIT_STATUS_UNREPRESENTABLE }, /* Low byte 44 would alias an unrelated status */
|
||||
{ 65536, AKERR_EXIT_STATUS_UNREPRESENTABLE }, /* Low byte 0 again, further out */
|
||||
{ -1, AKERR_EXIT_STATUS_UNREPRESENTABLE }, /* Negative: low byte 255 */
|
||||
};
|
||||
|
||||
/*
|
||||
* Run body in a child and return its exit status, or -1 if it did not exit
|
||||
* normally. The 99 sentinel catches a body that returns instead of terminating.
|
||||
*/
|
||||
static int child_exit_status(void (*body)(void))
|
||||
{
|
||||
pid_t pid = fork();
|
||||
if ( pid == 0 ) {
|
||||
body();
|
||||
_exit(99);
|
||||
}
|
||||
int status = 0;
|
||||
if ( pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status) ) {
|
||||
return -1;
|
||||
}
|
||||
return WEXITSTATUS(status);
|
||||
}
|
||||
|
||||
/* The status the next child leaves with. Set before each fork. */
|
||||
static int pending_status;
|
||||
|
||||
/* The way a consumer's own handler is expected to leave. */
|
||||
static void run_akerr_exit(void)
|
||||
{
|
||||
akerr_exit(pending_status);
|
||||
}
|
||||
|
||||
/* The way the library leaves when nothing handled the error. */
|
||||
static void run_default_handler(void)
|
||||
{
|
||||
akerr_ErrorContext *slot = akerr_next_error();
|
||||
if ( slot == NULL ) {
|
||||
_exit(98);
|
||||
}
|
||||
slot->status = pending_status;
|
||||
akerr_default_handler_unhandled_error(slot);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *raise_consumer_error(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_RETURN(errctx, TEST_STATUS, "consumer status, deliberately unhandled");
|
||||
}
|
||||
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *raise_nothing(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* A full propagation to the top of the stack, with nothing handling it. */
|
||||
static void unhandled_consumer_error(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, raise_consumer_error());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
/* no HANDLE for TEST_STATUS -> stays unhandled */
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
|
||||
/*
|
||||
* Reached through FINISH_NORETURN rather than by calling the handler directly,
|
||||
* so the child exercises the path a consumer actually takes. The handler is set
|
||||
* explicitly because an earlier case in this process may have replaced it.
|
||||
*/
|
||||
static void unhandled_consumer_error_fatal(void)
|
||||
{
|
||||
akerr_handler_unhandled_error = &akerr_default_handler_unhandled_error;
|
||||
unhandled_consumer_error();
|
||||
}
|
||||
|
||||
static int trace_fired = -2;
|
||||
|
||||
static void nonfatal_handler(akerr_ErrorContext *e)
|
||||
{
|
||||
trace_fired = (e != NULL) ? e->status : -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* The same shape as unhandled_consumer_error(), except nothing fails. PROCESS
|
||||
* opens with `case 0`, so a zero status is handled and the handler must not
|
||||
* run -- which is what keeps akerr_exit(0) exiting 0 from being a hole in the
|
||||
* "an unhandled error never exits 0" rule.
|
||||
*/
|
||||
static void successful_operation(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, raise_nothing());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akerr_capture_install();
|
||||
akerr_init();
|
||||
|
||||
/* The three outcomes have to stay distinguishable from each other. */
|
||||
AKERR_CHECK(AKERR_EXIT_STATUS_UNREPRESENTABLE != 0);
|
||||
AKERR_CHECK(AKERR_EXIT_STATUS_UNREPRESENTABLE != 1);
|
||||
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(TEST_STATUS, 1, TEST_OWNER));
|
||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name(TEST_OWNER, TEST_STATUS,
|
||||
TEST_STATUS_NAME));
|
||||
|
||||
for ( size_t i = 0; i < sizeof(exit_cases) / sizeof(exit_cases[0]); i++ ) {
|
||||
pending_status = exit_cases[i].status;
|
||||
|
||||
int direct = child_exit_status(&run_akerr_exit);
|
||||
if ( direct != exit_cases[i].expect ) {
|
||||
fprintf(stderr, "akerr_exit(%d) exited %d, want %d\n",
|
||||
exit_cases[i].status, direct, exit_cases[i].expect);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* The handler must not carry a mapping of its own. */
|
||||
int handled = child_exit_status(&run_default_handler);
|
||||
if ( handled != direct ) {
|
||||
fprintf(stderr, "default handler on status %d exited %d,"
|
||||
" but akerr_exit(%d) exited %d\n",
|
||||
exit_cases[i].status, handled, exit_cases[i].status, direct);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* The NULL-context exit is asserted by tests/err_unhandled_null.c. */
|
||||
|
||||
/* End to end: an unhandled consumer error kills the process non-zero. */
|
||||
AKERR_CHECK(child_exit_status(&unhandled_consumer_error_fatal)
|
||||
== AKERR_EXIT_STATUS_UNREPRESENTABLE);
|
||||
|
||||
/*
|
||||
* And the status the exit code could not carry is in the trace. Run with a
|
||||
* handler that returns so the assertions happen in this process, where the
|
||||
* captured log lives.
|
||||
*/
|
||||
akerr_handler_unhandled_error = &nonfatal_handler;
|
||||
akerr_capture_reset();
|
||||
unhandled_consumer_error();
|
||||
AKERR_CHECK(trace_fired == TEST_STATUS);
|
||||
AKERR_CHECK_CONTAINS("Unhandled Error");
|
||||
AKERR_CHECK_CONTAINS("256");
|
||||
AKERR_CHECK_CONTAINS(TEST_STATUS_NAME);
|
||||
|
||||
/* A zero status is handled by PROCESS and never reaches the handler. */
|
||||
trace_fired = -2;
|
||||
akerr_capture_reset();
|
||||
successful_operation();
|
||||
AKERR_CHECK(trace_fired == -2);
|
||||
AKERR_CHECK_NOT_CONTAINS("Unhandled Error");
|
||||
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
fprintf(stderr, "err_exit_status ok\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -6,7 +6,10 @@
|
||||
/*
|
||||
* The default unhandled-error handler is the library's last stop: it exits the
|
||||
* process. Both of its exits were untested -- exit(1) for a NULL context (a
|
||||
* handler invoked with no error at all) and exit(errctx->status) for a real one.
|
||||
* handler invoked with no error at all) and, for a real one, the status handed
|
||||
* to akerr_exit(). tests/err_exit_status.c covers what akerr_exit() does with a
|
||||
* status; this covers that the handler reaches it, and the NULL case, which
|
||||
* never gets that far.
|
||||
*
|
||||
* The handler never returns, so each case runs in a forked child and the test
|
||||
* asserts the exact exit status. That is stricter than a WILL_FAIL test, which
|
||||
|
||||
Reference in New Issue
Block a user