Every libakgl test suite could report success while failing. libakerror's default unhandled-error handler ended in exit(errctx->status), an exit status is one byte wide, and libakgl's band starts at 256 -- so AKGL_ERR_SDL, the most common failure a library built on SDL can have, exited 0 and CTest recorded a pass. tests/character.c aborted at its second of four tests on a bad renderer and was green for months. 0.5.0 worked around that here with TEST_TRAP_UNHANDLED_ERRORS() in tests/testutil.h, and TODO.md ended the entry saying any consumer's suites have the same problem and it was worth raising upstream. It was. 2.0.1 fixes it at the source: akerr_exit() owns the mapping and the default handler calls it, so 0 exits 0, 1 through 255 exit themselves, and anything else exits AKERR_EXIT_STATUS_UNREPRESENTABLE (125). The trap and its 21 call sites are gone. Verified by putting the original failure back rather than by reading the release notes: a FAIL_BREAK(AKGL_ERR_SDL) in tests/character.c's main exits 125 and CTest reports a failure. A standalone consumer raising the same status unhandled exits 125 where it exited 0 before. tests/actor.c installs its own handler and called exit(errctx->status) from it, which is the same defect one layer up. It calls akerr_exit() now. 2.0.0 also makes the error pool and the status registry thread safe, which libakgl needs more than it knew: audio_stream_callback raises error contexts on SDL's audio thread. With an unlocked pool that callback and the main thread could scan AKERR_ARRAY_ERROR at the same time and be handed the same slot. The comment there says so. This is a hard dependency floor, not a preference. 2.0.0 moved __akerr_last_ignored to thread-local storage and made akerr_next_error() return a context that already holds its reference, and both expand at libakgl's call sites -- and at a consumer's, because akerror.h is part of libakgl's public interface. Mixing headers and libraries across that line double-counts every reference and never returns a pool slot. The soname moved to libakerror.so.2; include/akgl/error.h now also feature- tests AKERR_EXIT_STATUS_UNREPRESENTABLE, which is the narrowest probe for 2.0.1 since libakerror publishes no version macro. 0.7.0 for that reason: libakgl's own ABI is unchanged, but the one it re-exports through its headers is not. TODO.md records the pkg-config gap this makes sharper -- akgl.pc names no dependencies at all, so nothing tells a pkg-config consumer which libakerror it needs. Clean build, 26/26 ctest, memcheck clean, warning-clean at -Wall -Werror. libakgl.so.0.7 links libakerror.so.2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B8T5FAYXE8HEJqFLCYwNNc
123 lines
3.8 KiB
C
123 lines
3.8 KiB
C
/**
|
|
* @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 <math.h>
|
|
#include <stdlib.h>
|
|
#include <SDL3/SDL.h>
|
|
#include <akerror.h>
|
|
#include <akgl/error.h>
|
|
#include <akgl/heap.h>
|
|
|
|
/**
|
|
* @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 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_
|