From 4daa411f3f40d8a5d111d8f81a78e33fa2c0db3e Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Mon, 27 Jul 2026 16:18:17 -0400 Subject: [PATCH] Expand test coverage for error-handling macros and error pool Add 11 CTest programs and a shared test helper covering gaps left by the original four tests (which only exercised FAIL/CATCH/HANDLE and CLEANUP): - err_capture.h: capturing akerr_log_method + NDEBUG-proof AKERR_CHECK so tests can assert on message/status/stacktrace content, not just exit codes - err_success: clean nested return does not break/handle/leak - err_pool_refcount: 100k raise->catch->handle cycles leak 0 pool slots - err_handle_default / err_handle_group / err_handle_dispatch: handler routing - err_pass / err_ignore / err_swallow: PASS, IGNORE, FINISH(e,false) - err_break_variants: FAIL_*_BREAK and FAIL_*_RETURN - err_errno: system errno name lookup + "Unknown Error" boundary - err_custom_handler: override the unhandled-error hook, assert non-fatally Register tests via a foreach loop in CMakeLists.txt. Ignore build/. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 2 + CMakeLists.txt | 52 ++++++++++++++------- tests/err_break_variants.c | 92 +++++++++++++++++++++++++++++++++++++ tests/err_capture.h | 84 +++++++++++++++++++++++++++++++++ tests/err_custom_handler.c | 45 ++++++++++++++++++ tests/err_errno.c | 47 +++++++++++++++++++ tests/err_handle_default.c | 38 +++++++++++++++ tests/err_handle_dispatch.c | 42 +++++++++++++++++ tests/err_handle_group.c | 56 ++++++++++++++++++++++ tests/err_ignore.c | 34 ++++++++++++++ tests/err_pass.c | 46 +++++++++++++++++++ tests/err_pool_refcount.c | 55 ++++++++++++++++++++++ tests/err_success.c | 44 ++++++++++++++++++ tests/err_swallow.c | 45 ++++++++++++++++++ 14 files changed, 665 insertions(+), 17 deletions(-) create mode 100644 .gitignore create mode 100644 tests/err_break_variants.c create mode 100644 tests/err_capture.h create mode 100644 tests/err_custom_handler.c create mode 100644 tests/err_errno.c create mode 100644 tests/err_handle_default.c create mode 100644 tests/err_handle_dispatch.c create mode 100644 tests/err_handle_group.c create mode 100644 tests/err_ignore.c create mode 100644 tests/err_pass.c create mode 100644 tests/err_pool_refcount.c create mode 100644 tests/err_success.c create mode 100644 tests/err_swallow.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..94aab09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +include/akerror.h +build/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 37a6564..015debf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,25 +44,43 @@ target_compile_definitions(akerror PUBLIC AKERR_USE_STDLIB=${AKERR_USE_STDLIB} ) -add_executable(test_err_catch tests/err_catch.c) -add_executable(test_err_cleanup tests/err_cleanup.c) -add_executable(test_err_trace tests/err_trace.c) -add_executable(test_err_improper_closure tests/err_improper_closure.c) -add_test(NAME err_catch COMMAND test_err_catch) -add_test(NAME err_cleanup COMMAND test_err_cleanup) -add_test(NAME err_trace COMMAND test_err_trace) -add_test(NAME err_improper_closure COMMAND test_err_improper_closure) - -set_tests_properties( - err_trace - err_improper_closure - PROPERTIES WILL_FAIL TRUE +# Each test is one source file in tests/ built into test_ and registered +# as CTest . Tests expected to abort (unhandled error / contract +# violation) go in AKERR_WILL_FAIL_TESTS; all others must exit 0. +set(AKERR_TESTS + err_catch + err_cleanup + err_trace + err_improper_closure + err_success + err_pool_refcount + err_handle_default + err_handle_group + err_handle_dispatch + err_pass + err_ignore + err_swallow + err_errno + err_break_variants + err_custom_handler ) -target_link_libraries(test_err_catch PRIVATE akerror) -target_link_libraries(test_err_cleanup PRIVATE akerror) -target_link_libraries(test_err_trace PRIVATE akerror) -target_link_libraries(test_err_improper_closure PRIVATE akerror) +set(AKERR_WILL_FAIL_TESTS + err_trace + err_improper_closure +) + +foreach(_test IN LISTS AKERR_TESTS) + add_executable(test_${_test} tests/${_test}.c) + target_include_directories(test_${_test} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests) + target_link_libraries(test_${_test} PRIVATE akerror) + add_test(NAME ${_test} COMMAND test_${_test}) +endforeach() + +set_tests_properties( + ${AKERR_WILL_FAIL_TESTS} + PROPERTIES WILL_FAIL TRUE +) set(main_lib_dest "lib/my_library-${MY_LIBRARY_VERSION}") install(TARGETS akerror diff --git a/tests/err_break_variants.c b/tests/err_break_variants.c new file mode 100644 index 0000000..4cc8ff2 --- /dev/null +++ b/tests/err_break_variants.c @@ -0,0 +1,92 @@ +#include "akerror.h" +#include "err_capture.h" + +/* + * Cover the conditional break/return failure macros: + * FAIL_ZERO_BREAK / FAIL_NONZERO_BREAK / FAIL_BREAK (inside ATTEMPT) + * FAIL_ZERO_RETURN / FAIL_NONZERO_RETURN / FAIL_RETURN (direct return) + * Each is checked in both its failing and its non-failing (pass-through) state. + */ + +akerr_ErrorContext *zero_break(int x) +{ + PREPARE_ERROR(e); + ATTEMPT { + FAIL_ZERO_BREAK(e, x, AKERR_VALUE, "x was zero"); + } CLEANUP { + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext *nonzero_break(int x) +{ + PREPARE_ERROR(e); + ATTEMPT { + FAIL_NONZERO_BREAK(e, x, AKERR_INDEX, "x was nonzero"); + } CLEANUP { + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext *always_break(void) +{ + PREPARE_ERROR(e); + ATTEMPT { + FAIL_BREAK(e, AKERR_IO, "always"); + } CLEANUP { + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext *zero_return(int x) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, x, AKERR_KEY, "x was zero"); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext *nonzero_return(int x) +{ + PREPARE_ERROR(e); + FAIL_NONZERO_RETURN(e, x, AKERR_TYPE, "x was nonzero"); + SUCCEED_RETURN(e); +} + +int main(void) +{ + akerr_capture_install(); + akerr_init(); + + akerr_ErrorContext *r; + + r = zero_break(0); + AKERR_CHECK(r != NULL && r->status == AKERR_VALUE); + r = akerr_release_error(r); + AKERR_CHECK(zero_break(7) == NULL); + + r = nonzero_break(7); + AKERR_CHECK(r != NULL && r->status == AKERR_INDEX); + r = akerr_release_error(r); + AKERR_CHECK(nonzero_break(0) == NULL); + + r = always_break(); + AKERR_CHECK(r != NULL && r->status == AKERR_IO); + r = akerr_release_error(r); + + r = zero_return(0); + AKERR_CHECK(r != NULL && r->status == AKERR_KEY); + r = akerr_release_error(r); + AKERR_CHECK(zero_return(7) == NULL); + + r = nonzero_return(7); + AKERR_CHECK(r != NULL && r->status == AKERR_TYPE); + r = akerr_release_error(r); + AKERR_CHECK(nonzero_return(0) == NULL); + + AKERR_CHECK(akerr_slots_in_use() == 0); + fprintf(stderr, "err_break_variants ok\n"); + return 0; +} diff --git a/tests/err_capture.h b/tests/err_capture.h new file mode 100644 index 0000000..ec4e2e5 --- /dev/null +++ b/tests/err_capture.h @@ -0,0 +1,84 @@ +#ifndef AKERR_TEST_CAPTURE_H +#define AKERR_TEST_CAPTURE_H + +/* + * Shared test helpers for libakerror. + * + * Installs a capturing implementation of akerr_log_method so tests can assert + * on the *content* of log/stacktrace output (messages, status codes, error + * names) instead of relying solely on process exit codes. + * + * Also provides AKERR_CHECK(), a NDEBUG-proof assertion that fails the test by + * returning non-zero from main() (unlike assert(), which is compiled out in + * release builds and would silently turn a test into a no-op). + */ + +#include "akerror.h" +#include +#include +#include + +#define AKERR_CAPTURE_BUFSZ 65536 +static char akerr_capture_buf[AKERR_CAPTURE_BUFSZ]; +static size_t akerr_capture_len = 0; + +static void __attribute__((unused)) akerr_capture_logger(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(akerr_capture_buf + akerr_capture_len, + AKERR_CAPTURE_BUFSZ - akerr_capture_len, fmt, ap); + va_end(ap); + if ( n > 0 ) { + akerr_capture_len += (size_t)n; + if ( akerr_capture_len >= AKERR_CAPTURE_BUFSZ ) { + akerr_capture_len = AKERR_CAPTURE_BUFSZ - 1; + } + } +} + +static void __attribute__((unused)) akerr_capture_reset(void) +{ + akerr_capture_len = 0; + akerr_capture_buf[0] = '\0'; +} + +/* + * Install the capturing logger. akerr_init() only assigns a default logger when + * akerr_log_method is NULL, and it is idempotent, so calling this either before + * or after the first PREPARE_ERROR keeps our logger in place. + */ +static void __attribute__((unused)) akerr_capture_install(void) +{ + akerr_capture_reset(); + akerr_log_method = &akerr_capture_logger; +} + +/* Count array slots currently checked out of the pool (refcount != 0). */ +static int __attribute__((unused)) akerr_slots_in_use(void) +{ + int n = 0; + for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) { + if ( AKERR_ARRAY_ERROR[i].refcount != 0 ) { + n++; + } + } + return n; +} + +#define AKERR_CHECK(cond) \ + do { \ + if ( !(cond) ) { \ + fprintf(stderr, "CHECK FAILED: %s at %s:%d\n", \ + #cond, __FILE__, __LINE__); \ + return 1; \ + } \ + } while ( 0 ) + +#define AKERR_CHECK_CONTAINS(needle) \ + AKERR_CHECK(strstr(akerr_capture_buf, (needle)) != NULL) + +#define AKERR_CHECK_NOT_CONTAINS(needle) \ + AKERR_CHECK(strstr(akerr_capture_buf, (needle)) == NULL) + +#endif // AKERR_TEST_CAPTURE_H diff --git a/tests/err_custom_handler.c b/tests/err_custom_handler.c new file mode 100644 index 0000000..ed3d873 --- /dev/null +++ b/tests/err_custom_handler.c @@ -0,0 +1,45 @@ +#include "akerror.h" +#include "err_capture.h" + +/* + * The unhandled-error hook (akerr_handler_unhandled_error) is overridable. + * Install a non-fatal handler so an unhandled error can be asserted directly + * (status, invocation) instead of relying on process death / WILL_FAIL. + */ + +static int custom_fired = 0; +static int custom_status = 0; + +static void my_handler(akerr_ErrorContext *e) +{ + custom_fired = 1; + custom_status = (e != NULL) ? e->status : -1; + /* deliberately does NOT exit() */ +} + +akerr_ErrorContext *boom(void) +{ + PREPARE_ERROR(e); + FAIL_RETURN(e, AKERR_TYPE, "unhandled on purpose"); +} + +int main(void) +{ + akerr_capture_install(); + akerr_init(); /* sets the default handler... */ + akerr_handler_unhandled_error = &my_handler; /* ...which we then override */ + + PREPARE_ERROR(e); + ATTEMPT { + CATCH(e, boom()); + } CLEANUP { + } PROCESS(e) { + /* no HANDLE for AKERR_TYPE -> stays unhandled */ + } FINISH_NORETURN(e); + + AKERR_CHECK(custom_fired == 1); + AKERR_CHECK(custom_status == AKERR_TYPE); + AKERR_CHECK_CONTAINS("Unhandled Error"); + fprintf(stderr, "err_custom_handler ok\n"); + return 0; +} diff --git a/tests/err_errno.c b/tests/err_errno.c new file mode 100644 index 0000000..df1316a --- /dev/null +++ b/tests/err_errno.c @@ -0,0 +1,47 @@ +#include "akerror.h" +#include "err_capture.h" +#include + +/* + * The library imports system errno codes and their descriptions at build time + * (scripts/generrno.sh -> akerr_init_errno). Verify: + * - a system errno (EACCES) has a registered, non-empty name; + * - an out-of-range status returns the "Unknown Error" sentinel; + * - a system errno can be raised, propagated and handled like any AKERR_* code. + */ + +static int handled = 0; + +akerr_ErrorContext *boom(void) +{ + PREPARE_ERROR(e); + FAIL_RETURN(e, EACCES, "permission denied"); +} + +int main(void) +{ + akerr_capture_install(); + akerr_init(); + + char *nm = akerr_name_for_status(EACCES, NULL); + AKERR_CHECK(nm != NULL); + AKERR_CHECK(nm[0] != '\0'); + AKERR_CHECK(strcmp(nm, "Unknown Error") != 0); + + AKERR_CHECK(strcmp(akerr_name_for_status(AKERR_MAX_ERR_VALUE + 5000, NULL), + "Unknown Error") == 0); + + PREPARE_ERROR(e); + ATTEMPT { + CATCH(e, boom()); + } CLEANUP { + } PROCESS(e) { + } HANDLE(e, EACCES) { + handled = 1; + } FINISH_NORETURN(e); + + AKERR_CHECK(handled == 1); + AKERR_CHECK(akerr_slots_in_use() == 0); + fprintf(stderr, "err_errno ok (EACCES name: \"%s\")\n", nm); + return 0; +} diff --git a/tests/err_handle_default.c b/tests/err_handle_default.c new file mode 100644 index 0000000..95e88ac --- /dev/null +++ b/tests/err_handle_default.c @@ -0,0 +1,38 @@ +#include "akerror.h" +#include "err_capture.h" + +/* + * An error whose status has no matching HANDLE block must fall through to + * HANDLE_DEFAULT, and the non-matching HANDLE body must not run. + */ + +static int specific_fired = 0; +static int default_fired = 0; + +akerr_ErrorContext *boom(void) +{ + PREPARE_ERROR(e); + FAIL_RETURN(e, AKERR_TYPE, "wrong type"); +} + +int main(void) +{ + akerr_capture_install(); + + PREPARE_ERROR(e); + ATTEMPT { + CATCH(e, boom()); + } CLEANUP { + } PROCESS(e) { + } HANDLE(e, AKERR_KEY) { + specific_fired = 1; /* must NOT run: error is AKERR_TYPE */ + } HANDLE_DEFAULT(e) { + default_fired = 1; + } FINISH_NORETURN(e); + + AKERR_CHECK(specific_fired == 0); + AKERR_CHECK(default_fired == 1); + AKERR_CHECK(akerr_slots_in_use() == 0); + fprintf(stderr, "err_handle_default ok\n"); + return 0; +} diff --git a/tests/err_handle_dispatch.c b/tests/err_handle_dispatch.c new file mode 100644 index 0000000..6bbace6 --- /dev/null +++ b/tests/err_handle_dispatch.c @@ -0,0 +1,42 @@ +#include "akerror.h" +#include "err_capture.h" + +/* + * With several distinct HANDLE blocks, only the one matching the raised status + * may run. Raise the middle code and confirm exact dispatch. + */ + +static int a_fired = 0; +static int b_fired = 0; +static int c_fired = 0; + +akerr_ErrorContext *boom(void) +{ + PREPARE_ERROR(e); + FAIL_RETURN(e, AKERR_TYPE, "the middle one"); +} + +int main(void) +{ + akerr_capture_install(); + + PREPARE_ERROR(e); + ATTEMPT { + CATCH(e, boom()); + } CLEANUP { + } PROCESS(e) { + } HANDLE(e, AKERR_KEY) { + a_fired = 1; + } HANDLE(e, AKERR_TYPE) { + b_fired = 1; + } HANDLE(e, AKERR_IO) { + c_fired = 1; + } FINISH_NORETURN(e); + + AKERR_CHECK(a_fired == 0); + AKERR_CHECK(b_fired == 1); + AKERR_CHECK(c_fired == 0); + AKERR_CHECK(akerr_slots_in_use() == 0); + fprintf(stderr, "err_handle_dispatch ok\n"); + return 0; +} diff --git a/tests/err_handle_group.c b/tests/err_handle_group.c new file mode 100644 index 0000000..232d33d --- /dev/null +++ b/tests/err_handle_group.c @@ -0,0 +1,56 @@ +#include "akerror.h" +#include "err_capture.h" + +/* + * HANDLE_GROUP lets several status codes share one handler body via + * case-fallthrough. The first member of the group uses HANDLE (which emits the + * leading break that terminates the previous case); each additional member + * uses HANDLE_GROUP; the shared body follows the last member. Verify that two + * different status codes both reach the shared body. + */ + +static int group_fired = 0; +static int other_fired = 0; + +akerr_ErrorContext *boom(int status) +{ + PREPARE_ERROR(e); + FAIL_RETURN(e, status, "boom %d", status); +} + +akerr_ErrorContext *run(int status) +{ + PREPARE_ERROR(e); + ATTEMPT { + CATCH(e, boom(status)); + } CLEANUP { + } PROCESS(e) { + } HANDLE(e, AKERR_KEY) + HANDLE_GROUP(e, AKERR_INDEX) { + group_fired++; + } HANDLE(e, AKERR_IO) { + other_fired++; + } FINISH(e, false); + return e; +} + +int main(void) +{ + akerr_capture_install(); + + run(AKERR_KEY); + AKERR_CHECK(group_fired == 1); + AKERR_CHECK(other_fired == 0); + + run(AKERR_INDEX); + AKERR_CHECK(group_fired == 2); + AKERR_CHECK(other_fired == 0); + + run(AKERR_IO); + AKERR_CHECK(group_fired == 2); + AKERR_CHECK(other_fired == 1); + + AKERR_CHECK(akerr_slots_in_use() == 0); + fprintf(stderr, "err_handle_group ok\n"); + return 0; +} diff --git a/tests/err_ignore.c b/tests/err_ignore.c new file mode 100644 index 0000000..f52ca55 --- /dev/null +++ b/tests/err_ignore.c @@ -0,0 +1,34 @@ +#include "akerror.h" +#include "err_capture.h" + +/* + * IGNORE deliberately swallows an error: it records the context in + * __akerr_last_ignored, logs it with an "IGNORED ERROR" marker, and lets + * execution continue. + */ + +akerr_ErrorContext *boom(void) +{ + PREPARE_ERROR(e); + FAIL_RETURN(e, AKERR_VALUE, "this error is ignored on purpose"); +} + +int main(void) +{ + akerr_capture_install(); + + int reached_after_ignore = 0; + PREPARE_ERROR(e); + (void)e; + + IGNORE(boom()); + reached_after_ignore = 1; + + AKERR_CHECK(__akerr_last_ignored != NULL); + AKERR_CHECK(__akerr_last_ignored->status == AKERR_VALUE); + AKERR_CHECK(reached_after_ignore == 1); + AKERR_CHECK_CONTAINS("IGNORED ERROR"); + AKERR_CHECK_CONTAINS("this error is ignored on purpose"); + fprintf(stderr, "err_ignore ok\n"); + return 0; +} diff --git a/tests/err_pass.c b/tests/err_pass.c new file mode 100644 index 0000000..2bde126 --- /dev/null +++ b/tests/err_pass.c @@ -0,0 +1,46 @@ +#include "akerror.h" +#include "err_capture.h" + +/* + * PASS bubbles an error up to the caller without a local ATTEMPT/PROCESS block: + * if the wrapped call fails, PASS returns the context from the current function. + * Verify the error reaches main and that the code after PASS is skipped on + * failure. + */ + +static int reached_after_pass = 0; + +akerr_ErrorContext *inner(void) +{ + PREPARE_ERROR(e); + FAIL_RETURN(e, AKERR_IO, "inner failed"); +} + +akerr_ErrorContext *outer(void) +{ + PREPARE_ERROR(e); + PASS(e, inner()); + reached_after_pass = 1; /* must NOT run: PASS returned already */ + SUCCEED_RETURN(e); +} + +int main(void) +{ + akerr_capture_install(); + + int handled = 0; + PREPARE_ERROR(e); + ATTEMPT { + CATCH(e, outer()); + } CLEANUP { + } PROCESS(e) { + } HANDLE(e, AKERR_IO) { + handled = 1; + } FINISH_NORETURN(e); + + AKERR_CHECK(handled == 1); + AKERR_CHECK(reached_after_pass == 0); + AKERR_CHECK(akerr_slots_in_use() == 0); + fprintf(stderr, "err_pass ok\n"); + return 0; +} diff --git a/tests/err_pool_refcount.c b/tests/err_pool_refcount.c new file mode 100644 index 0000000..c0be693 --- /dev/null +++ b/tests/err_pool_refcount.c @@ -0,0 +1,55 @@ +#include "akerror.h" +#include "err_capture.h" + +/* + * Pool-hygiene regression test. The error pool is a fixed 128-slot static + * array; a single leaked reference per error would silently exhaust it. Run a + * large number of full raise -> catch -> handle cycles and confirm every slot + * is returned to the pool (refcount 0) and the pool can still hand out + * contexts afterwards. + */ + +#define ITERATIONS 100000 + +akerr_ErrorContext *boom(void) +{ + PREPARE_ERROR(e); + ATTEMPT { + FAIL(e, AKERR_VALUE, "boom %d", 1); + } CLEANUP { + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +/* One full raise -> catch -> handle cycle. Returns NULL (context released). */ +akerr_ErrorContext *one_cycle(void) +{ + PREPARE_ERROR(e); + ATTEMPT { + CATCH(e, boom()); + } CLEANUP { + } PROCESS(e) { + } HANDLE(e, AKERR_VALUE) { + } FINISH(e, false); + return e; +} + +int main(void) +{ + akerr_capture_install(); + akerr_init(); + AKERR_CHECK(akerr_slots_in_use() == 0); + + for ( int iter = 0; iter < ITERATIONS; iter++ ) { + (void)one_cycle(); + } + + AKERR_CHECK(akerr_slots_in_use() == 0); + + akerr_ErrorContext *probe = akerr_next_error(); + AKERR_CHECK(probe != NULL); + + fprintf(stderr, "err_pool_refcount ok (%d cycles, 0 leaked)\n", ITERATIONS); + return 0; +} diff --git a/tests/err_success.c b/tests/err_success.c new file mode 100644 index 0000000..45c2f4d --- /dev/null +++ b/tests/err_success.c @@ -0,0 +1,44 @@ +#include "akerror.h" +#include "err_capture.h" + +/* + * The success path: a nested call that returns cleanly (NULL) must not break + * out of the caller's ATTEMPT block, must not enter any handler, and must not + * consume a slot from the error pool. + */ + +static int default_fired = 0; + +akerr_ErrorContext *ok_func(void) +{ + PREPARE_ERROR(e); + ATTEMPT { + } CLEANUP { + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +int main(void) +{ + akerr_capture_install(); + akerr_init(); + + int reached_after_catch = 0; + PREPARE_ERROR(e); + ATTEMPT { + CATCH(e, ok_func()); + reached_after_catch = 1; /* must run: no break on success */ + } CLEANUP { + } PROCESS(e) { + } HANDLE_DEFAULT(e) { + default_fired = 1; /* must NOT run */ + } FINISH_NORETURN(e); + + AKERR_CHECK(reached_after_catch == 1); + AKERR_CHECK(default_fired == 0); + AKERR_CHECK(e == NULL); + AKERR_CHECK(akerr_slots_in_use() == 0); + fprintf(stderr, "err_success ok\n"); + return 0; +} diff --git a/tests/err_swallow.c b/tests/err_swallow.c new file mode 100644 index 0000000..509ea77 --- /dev/null +++ b/tests/err_swallow.c @@ -0,0 +1,45 @@ +#include "akerror.h" +#include "err_capture.h" + +/* + * FINISH(ctx, false) closes an ATTEMPT block without propagating: an unhandled + * error is dropped (not returned, not passed to the unhandled handler) and the + * context is released back to the pool. Because we use FINISH (not + * FINISH_NORETURN) the process must continue normally and exit 0. + */ + +static int wrong_handler_fired = 0; + +akerr_ErrorContext *boom(void) +{ + PREPARE_ERROR(e); + FAIL_RETURN(e, AKERR_VALUE, "unhandled but swallowed"); +} + +/* FINISH(e, false) belongs in a context-returning function. On the swallow + * path e is released to NULL, so this returns NULL to its caller. */ +akerr_ErrorContext *swallow_it(void) +{ + PREPARE_ERROR(e); + ATTEMPT { + CATCH(e, boom()); + } CLEANUP { + } PROCESS(e) { + } HANDLE(e, AKERR_KEY) { + wrong_handler_fired = 1; /* does not match AKERR_VALUE */ + } FINISH(e, false); + return e; /* NULL: released even though unhandled */ +} + +int main(void) +{ + akerr_capture_install(); + + akerr_ErrorContext *res = swallow_it(); + + AKERR_CHECK(wrong_handler_fired == 0); + AKERR_CHECK(res == NULL); /* released even though unhandled */ + AKERR_CHECK(akerr_slots_in_use() == 0); + fprintf(stderr, "err_swallow ok\n"); + return 0; +}