207 lines
6.9 KiB
C
207 lines
6.9 KiB
C
|
|
#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;
|
||
|
|
}
|