#include "akerror.h" #include "err_capture.h" #include #include /* * 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; }