Files
libakgl/tests/testutil.h
Andrew Kesterson e4aa6a5084
Some checks failed
libakgl CI Build / cmake_build (push) Failing after 20s
libakgl CI Build / performance (push) Failing after 19s
libakgl CI Build / memory_check (push) Failing after 15s
libakgl CI Build / mutation_test (push) Failing after 17s
Take libakerror 2.0.1 and drop the workaround it makes obsolete
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
2026-08-01 13:05:43 -04:00

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_