/** * @file testutil.h * @brief Shared assertion helpers and headless bootstrap for the libakgl test suites. * * The akerror ATTEMPT/CATCH/PROCESS/FINISH macros are verbose when what a test * wants to say is "this call must fail with exactly this status". These helpers * wrap that pattern. * * All of the TEST_* assertion macros expand to a `break` on failure, so they * must be used directly inside an ATTEMPT block. Inside a `for` or `while` * nested in an ATTEMPT they would break the inner loop instead; use * TEST_ASSERT_FLAG in that case and check the flag after the loop. */ #ifndef _AKGL_TESTUTIL_H_ #define _AKGL_TESTUTIL_H_ #include #include #include #include #include #include /** * @brief How many pool string slots are currently claimed. * * The string pool is 256 entries and every loader borrows scratch strings from * it, so "does this function give back what it took" is a question a test can * answer directly by counting either side of a call. A leak that would take * fifty map loads to show up in production shows up here on the first one. */ static inline int test_string_pool_used(void) { int used = 0; int i = 0; for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) { if ( akgl_heap_strings[i].refcount != 0 ) { used += 1; } } return used; } /** * @brief Exit with a status the shell can actually see. * * libakerror's default unhandled-error handler ends in `exit(errctx->status)`. * `exit` keeps only the low byte of what it is given, and libakgl's own * statuses start at `AKERR_FIRST_CONSUMER_STATUS`, which is 256 -- so * #AKGL_ERR_SDL exits 0, and CTest records a pass. Every suite in this * directory that failed for the most common reason in this library therefore * reported success. `tests/character.c` did exactly that: it aborted at its * second of four tests on a bad renderer and was green for months. * * This collapses any status that a byte cannot carry onto 1. It does not log -- * `FINISH_NORETURN` has already logged the context and its stack by the time it * calls the handler. */ static inline void test_handler_unhandled_error(akerr_ErrorContext *errctx) { int status = ( errctx == NULL ) ? 1 : (errctx->status & 0xFF); if ( status == 0 ) { status = 1; } exit(status); } /** * @brief Install test_handler_unhandled_error(). Call once, after akgl_error_init(). * * akgl_error_init() reaches akerr_init(), which installs libakerror's default * handler, so this has to come after it rather than before. */ #define TEST_TRAP_UNHANDLED_ERRORS() \ (akerr_handler_unhandled_error = &test_handler_unhandled_error) /** @brief Fail the enclosing ATTEMPT block unless @p cond holds. */ #define TEST_ASSERT(e, cond, ...) \ if ( ! (cond) ) { \ FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, __VA_ARGS__); \ } /** * @brief Run @p stmt and require that it reports exactly @p expected. * * Releases whatever context @p stmt returns, so a test can assert many failure * paths in a row without draining AKERR_ARRAY_ERROR. Pass 0 for @p expected to * require success. */ #define TEST_EXPECT_STATUS(e, expected, stmt, desc) \ { \ akerr_ErrorContext *__tec = (stmt); \ int __tst = ( __tec == NULL ) ? 0 : __tec->status; \ if ( __tec != NULL ) { \ __tec->handled = true; \ __tec = akerr_release_error(__tec); \ } \ if ( __tst != (expected) ) { \ FAIL_BREAK( \ e, \ AKGL_ERR_BEHAVIOR, \ "%s: expected status %d (%s), got %d (%s)", \ desc, \ (int)(expected), \ akerr_name_for_status((int)(expected), NULL), \ __tst, \ akerr_name_for_status(__tst, NULL)); \ } \ } /** @brief Require that @p stmt succeeds, reporting @p desc if it does not. */ #define TEST_EXPECT_OK(e, stmt, desc) \ TEST_EXPECT_STATUS(e, 0, stmt, desc) /** * @brief Require that @p stmt fails, without pinning which status it reports. * * For paths that are delegated to a dependency, where the exact status is that * dependency's business and asserting it would make the test brittle. */ #define TEST_EXPECT_ANY_ERROR(e, stmt, desc) \ { \ akerr_ErrorContext *__tec = (stmt); \ int __tst = ( __tec == NULL ) ? 0 : __tec->status; \ if ( __tec != NULL ) { \ __tec->handled = true; \ __tec = akerr_release_error(__tec); \ } \ if ( __tst == 0 ) { \ FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, "%s: expected a failure, got success", desc); \ } \ } /** @brief Require that two floats agree to within AKGL_TEST_EPSILON. */ #define AKGL_TEST_EPSILON 0.0001f #define TEST_ASSERT_FEQ(e, actual, expected, ...) \ if ( fabsf((float)(actual) - (float)(expected)) > AKGL_TEST_EPSILON ) { \ FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, __VA_ARGS__); \ } /** * @brief Record a failure into a flag instead of breaking. * * For assertions inside a loop nested in an ATTEMPT block, where `break` would * only leave the loop. */ #define TEST_ASSERT_FLAG(flag, cond) \ if ( ! (cond) ) { \ (flag) = false; \ } #endif // _AKGL_TESTUTIL_H_