Files
libakgl/tests/util.c
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

474 lines
15 KiB
C

#include <SDL3/SDL.h>
#include <string.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/heap.h>
#include <akgl/staticstring.h>
#include <akgl/util.h>
#include "testutil.h"
/**
* @brief How many entries of AKERR_ARRAY_ERROR are currently held by somebody.
*
* The error contexts are a fixed pool exactly like the object pools: a context
* is in use while its reference count is non-zero, and a function that finishes
* without releasing one has leaked a slot out of AKERR_MAX_ARRAY_ERROR.
*/
static int live_error_contexts(void)
{
int live = 0;
int i = 0;
for ( i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
if ( AKERR_ARRAY_ERROR[i].refcount != 0 ) {
live += 1;
}
}
return live;
}
akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
{
akgl_RectanglePoints points;
// Zeroed for the same reason as the fixtures in
// test_akgl_collide_point_rectangle_nullpointers: the last case here is a
// real call, and feeding it stack garbage is noise under `memcheck`.
SDL_FRect testrect = {.x = 0, .y = 0, .w = 0, .h = 0};
PREPARE_ERROR(errctx);
memset((void *)&points, 0x00, sizeof(akgl_RectanglePoints));
ATTEMPT {
CATCH(errctx, akgl_rectangle_points(NULL, NULL));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with all NULL pointers");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
// noop
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_rectangle_points(NULL, &testrect));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL SDL_FRect pointer");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
// noop
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_rectangle_points(&points, NULL));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL akgl_RectanglePoints pointer");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
// noop
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_rectangle_points(&points, &testrect));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_rectangle_points_math(void)
{
akgl_RectanglePoints points;
SDL_FRect testrect = {.x = 0, .y = 0, .w = 32, .h = 32};
memset((void *)&points, 0x00, sizeof(akgl_RectanglePoints));
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_rectangle_points(&points, &testrect));
if ( points.topleft.x != 0 ||
points.topleft.y != 0 ||
points.topright.x != 32 ||
points.topright.y != 0 ||
points.bottomleft.x != 0 ||
points.bottomleft.y != 32 ||
points.bottomright.x != 32 ||
points.bottomright.y != 32 ) {
FAIL_BREAK(
errctx,
AKGL_ERR_BEHAVIOR,
"akgl_rectangle_points incorrectly calculated points for {x=0, y=0, w=32, h=32} to {topleft={%d, %d}, topright={%d, %d}, bottomleft={%d, %d}, bottomright={%d, %d}}",
points.topleft.x, points.topleft.y,
points.topright.x, points.topright.y,
points.bottomleft.x, points.bottomleft.y,
points.bottomright.x, points.bottomright.y
);
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_collide_point_rectangle_nullpointers(void)
{
// Zeroed rather than left as whatever the stack held. The last case in this
// function is a real call with real arguments, and reading uninitialised
// floats out of it is sixteen findings under `memcheck` for a test that is
// not about coordinates at all.
akgl_Point testpoint = { .x = 0, .y = 0 };
akgl_RectanglePoints testrectpoints;
bool testcollide = false;
PREPARE_ERROR(errctx);
memset(&testrectpoints, 0x00, sizeof(akgl_RectanglePoints));
ATTEMPT {
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, NULL));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(*, *, NULL) failed");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
// noop
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, NULL, &testcollide));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(*, NULL, *) failed");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
// noop
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_collide_point_rectangle(NULL, &testrectpoints, &testcollide));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(NULL, *, *) failed");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
// noop
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_collide_point_rectangle(NULL, NULL, NULL));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(NULL, NULL, NULL) failed");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
// noop
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_collide_point_rectangle_logic(void)
{
akgl_Point testpoint = {.x = 16, .y = 16};
SDL_FRect testrect = { .x = 0, .y = 0, .w = 32, .h = 32};
akgl_RectanglePoints testrectpoints;
bool testcollide = false;
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_rectangle_points(&testrectpoints, &testrect));
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide));
if ( testcollide == false ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
}
testpoint.x = 48;
testpoint.y = 48;
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide));
if ( testcollide == true ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Invalid collision reported");
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void)
{
SDL_FRect testrect1 = {.x = 0, .y = 0, .w = 0, .h = 0};
SDL_FRect testrect2 = {.x = 0, .y = 0, .w = 0, .h = 0};
bool testcollide = false;
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, NULL));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(*, *, NULL) failed");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
// noop
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_collide_rectangles(&testrect1, NULL, &testcollide));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(*, NULL, *) failed");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
// noop
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_collide_rectangles(NULL, &testrect2, &testcollide));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(NULL, *, *) failed");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
// noop
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_collide_rectangles(NULL, NULL, NULL));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(NULL, NULL, NULL) failed");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
// noop
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
{
SDL_FRect testrect1 = { .x = 0, .y = 0, .w = 32, .h = 32};
SDL_FRect testrect2 = { .x = 30, .y = 30, .w = 40, .h = 40};
bool testcollide = false;
PREPARE_ERROR(errctx);
ATTEMPT {
// Collision overlapping on the top left
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
}
// Collision overlapping on the top right
testrect1.x = 64;
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
}
// Collision overlapping on the bottom left
testrect1.x = 0;
testrect1.y = 32;
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
}
// Collision overlapping on the bottom right
testrect1.x = 32;
testrect1.y = 32;
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
}
// Collision overlapping the top edge
testrect1.x = 0;
testrect1.y = 0;
testrect1.w = 60;
testrect1.h = 32;
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
}
// Collision overlapping the left edge
testrect1.x = 0;
testrect1.y = 0;
testrect1.w = 35;
testrect1.h = 80;
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
}
// Collision overlapping the right edge
testrect1.x = 65;
testrect1.y = 0;
testrect1.w = 60;
testrect1.h = 80;
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
}
// Collision overlapping the bottom edge
testrect1.x = 0;
testrect1.y = 65;
testrect1.w = 80;
testrect1.h = 32;
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == false ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
}
// Not colliding
testrect1.x = 0;
testrect1.y = 0;
testrect1.w = 16;
testrect1.h = 16;
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
if ( testcollide == true ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Invalid collision reported");
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief Resolving a path through the root fallback must give its context back.
*
* akgl_path_relative tries the working directory first and falls back to
* resolving against @p root when that reports ENOENT. That fallback used to be
* taken by returning from inside the HANDLE block, which skips the
* RELEASE_ERROR that FINISH ends with -- so every call down that branch leaked
* one entry of AKERR_ARRAY_ERROR, and the 129th call aborted the whole process
* with "Unable to pull an error context from the array!". A single map load
* resolves several paths this way.
*
* The loop runs well past AKERR_MAX_ARRAY_ERROR on purpose: at the old
* behaviour this test does not fail, it terminates the suite.
*/
/**
* @brief akgl_compare_sdl_surfaces must check geometry before it memcmps.
*
* It compared `s1->pitch * s1->h` bytes out of both surfaces without looking at
* the second one's dimensions, so a smaller s2 was read past its end rather
* than reported as a mismatch. Benign in practice and immediately fatal under a
* memory checker, which is the reason to fix it rather than leave it.
*/
akerr_ErrorContext *test_akgl_compare_sdl_surfaces_checks_geometry(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *big = NULL;
SDL_Surface *small = NULL;
SDL_Surface *twin = NULL;
ATTEMPT {
big = SDL_CreateSurface(32, 32, SDL_PIXELFORMAT_RGBA8888);
twin = SDL_CreateSurface(32, 32, SDL_PIXELFORMAT_RGBA8888);
small = SDL_CreateSurface(8, 8, SDL_PIXELFORMAT_RGBA8888);
FAIL_ZERO_BREAK(errctx, big, AKGL_ERR_SDL, "%s", SDL_GetError());
FAIL_ZERO_BREAK(errctx, twin, AKGL_ERR_SDL, "%s", SDL_GetError());
FAIL_ZERO_BREAK(errctx, small, AKGL_ERR_SDL, "%s", SDL_GetError());
FAIL_ZERO_BREAK(errctx, SDL_FillSurfaceRect(big, NULL, 0), AKGL_ERR_SDL, "%s", SDL_GetError());
FAIL_ZERO_BREAK(errctx, SDL_FillSurfaceRect(twin, NULL, 0), AKGL_ERR_SDL, "%s", SDL_GetError());
FAIL_ZERO_BREAK(errctx, SDL_FillSurfaceRect(small, NULL, 0), AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_EXPECT_OK(errctx, akgl_compare_sdl_surfaces(big, twin),
"comparing two identical surfaces");
// The one that used to read 4 KiB past the end of an 8x8 surface.
TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_compare_sdl_surfaces(big, small),
"comparing a 32x32 surface against an 8x8 one");
TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_compare_sdl_surfaces(small, big),
"comparing an 8x8 surface against a 32x32 one");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_compare_sdl_surfaces(NULL, big),
"comparing a NULL first surface");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_compare_sdl_surfaces(big, NULL),
"comparing a NULL second surface");
// Same dimensions, different format: the pixels are not comparable even
// though the byte count might be.
SDL_DestroySurface(small);
small = SDL_CreateSurface(32, 32, SDL_PIXELFORMAT_RGB24);
FAIL_ZERO_BREAK(errctx, small, AKGL_ERR_SDL, "%s", SDL_GetError());
TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_compare_sdl_surfaces(big, small),
"comparing two surfaces of different pixel formats");
} CLEANUP {
if ( big != NULL ) {
SDL_DestroySurface(big);
}
if ( twin != NULL ) {
SDL_DestroySurface(twin);
}
if ( small != NULL ) {
SDL_DestroySurface(small);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_path_relative_releases_contexts(void)
{
PREPARE_ERROR(errctx);
akgl_String *dst = NULL;
int before = 0;
int after = 0;
int i = 0;
PASS(errctx, akgl_heap_init());
PASS(errctx, akgl_heap_next_string(&dst));
before = live_error_contexts();
for ( i = 0; i < (AKERR_MAX_ARRAY_ERROR * 2); i++ ) {
PASS(errctx, akgl_path_relative("assets", "testcharacter.json", dst));
}
after = live_error_contexts();
ATTEMPT {
if ( after != before ) {
FAIL_BREAK(
errctx,
AKGL_ERR_BEHAVIOR,
"akgl_path_relative leaked %d error context(s) over %d root-fallback resolutions",
(after - before),
(AKERR_MAX_ARRAY_ERROR * 2));
}
} CLEANUP {
IGNORE(akgl_heap_release_string(dst));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, test_akgl_rectangle_points_nullpointers());
CATCH(errctx, test_akgl_rectangle_points_math());
CATCH(errctx, test_akgl_collide_point_rectangle_nullpointers());
// Defined since forever and never called until 0.5.0. TODO.md, "Known
// and still open" item 9.
CATCH(errctx, test_akgl_collide_point_rectangle_logic());
CATCH(errctx, test_akgl_collide_rectangles_nullpointers());
CATCH(errctx, test_akgl_collide_rectangles_logic());
CATCH(errctx, test_akgl_compare_sdl_surfaces_checks_geometry());
CATCH(errctx, test_akgl_path_relative_releases_contexts());
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}