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
380 lines
12 KiB
C
380 lines
12 KiB
C
/**
|
|
* @file heap.c
|
|
* @brief Unit tests for the fixed-size object pools and their refcounting.
|
|
*
|
|
* The pools are process-wide arrays, so every test that fills one calls
|
|
* akgl_heap_init() first and leaves the heap empty behind it.
|
|
*/
|
|
|
|
#include <SDL3/SDL.h>
|
|
#include <string.h>
|
|
#include <akerror.h>
|
|
|
|
#include <akgl/error.h>
|
|
#include <akgl/heap.h>
|
|
#include <akgl/actor.h>
|
|
#include <akgl/character.h>
|
|
#include <akgl/sprite.h>
|
|
#include <akgl/registry.h>
|
|
#include <akgl/staticstring.h>
|
|
|
|
#include "testutil.h"
|
|
|
|
/**
|
|
* @brief Reset every pool and the registries that reference their objects.
|
|
*
|
|
* The registries hold raw pointers into the pools, so clearing a pool without
|
|
* clearing the registry would leave dangling entries for the next test.
|
|
*/
|
|
static akerr_ErrorContext *reset_all_heaps(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
ATTEMPT {
|
|
CATCH(e, akgl_registry_init_actor());
|
|
CATCH(e, akgl_registry_init_sprite());
|
|
CATCH(e, akgl_registry_init_spritesheet());
|
|
CATCH(e, akgl_registry_init_character());
|
|
CATCH(e, akgl_heap_init());
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_heap_init_clears_every_pool(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
bool clean = true;
|
|
int i = 0;
|
|
|
|
ATTEMPT {
|
|
// Dirty every pool, then require that init scrubs all of them.
|
|
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
|
akgl_heap_actors[i].refcount = 5;
|
|
}
|
|
for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++ ) {
|
|
akgl_heap_sprites[i].refcount = 5;
|
|
}
|
|
for ( i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++ ) {
|
|
akgl_heap_spritesheets[i].refcount = 5;
|
|
}
|
|
for ( i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++ ) {
|
|
akgl_heap_characters[i].refcount = 5;
|
|
}
|
|
for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
|
|
akgl_heap_strings[i].refcount = 5;
|
|
}
|
|
|
|
CATCH(e, akgl_heap_init());
|
|
|
|
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
|
TEST_ASSERT_FLAG(clean, akgl_heap_actors[i].refcount == 0);
|
|
}
|
|
TEST_ASSERT(e, clean, "akgl_heap_init left a nonzero refcount in the actor pool");
|
|
|
|
for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++ ) {
|
|
TEST_ASSERT_FLAG(clean, akgl_heap_sprites[i].refcount == 0);
|
|
}
|
|
TEST_ASSERT(e, clean, "akgl_heap_init left a nonzero refcount in the sprite pool");
|
|
|
|
for ( i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++ ) {
|
|
TEST_ASSERT_FLAG(clean, akgl_heap_spritesheets[i].refcount == 0);
|
|
}
|
|
TEST_ASSERT(e, clean, "akgl_heap_init left a nonzero refcount in the spritesheet pool");
|
|
|
|
for ( i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++ ) {
|
|
TEST_ASSERT_FLAG(clean, akgl_heap_characters[i].refcount == 0);
|
|
}
|
|
TEST_ASSERT(e, clean, "akgl_heap_init left a nonzero refcount in the character pool");
|
|
|
|
for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
|
|
TEST_ASSERT_FLAG(clean, akgl_heap_strings[i].refcount == 0);
|
|
}
|
|
TEST_ASSERT(e, clean, "akgl_heap_init left a nonzero refcount in the string pool");
|
|
|
|
// akgl_heap_init_actor clears only the actor pool.
|
|
akgl_heap_actors[0].refcount = 9;
|
|
akgl_heap_sprites[0].refcount = 9;
|
|
CATCH(e, akgl_heap_init_actor());
|
|
TEST_ASSERT(e, akgl_heap_actors[0].refcount == 0,
|
|
"akgl_heap_init_actor did not clear the actor pool");
|
|
TEST_ASSERT(e, akgl_heap_sprites[0].refcount == 9,
|
|
"akgl_heap_init_actor cleared the sprite pool as well");
|
|
akgl_heap_sprites[0].refcount = 0;
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_heap_next_string_refcounting(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
akgl_String *first = NULL;
|
|
akgl_String *second = NULL;
|
|
|
|
ATTEMPT {
|
|
CATCH(e, reset_all_heaps());
|
|
|
|
// akgl_heap_next_string is the only acquire function that claims the slot
|
|
// it hands out; the others leave refcount at zero for the caller to set.
|
|
CATCH(e, akgl_heap_next_string(&first));
|
|
TEST_ASSERT(e, first->refcount == 1,
|
|
"akgl_heap_next_string returned a slot with refcount %d, expected 1",
|
|
first->refcount);
|
|
|
|
CATCH(e, akgl_heap_next_string(&second));
|
|
TEST_ASSERT(e, second != first,
|
|
"akgl_heap_next_string handed out the same slot twice");
|
|
|
|
CATCH(e, akgl_heap_release_string(first));
|
|
CATCH(e, akgl_heap_release_string(second));
|
|
TEST_ASSERT(e, first->refcount == 0,
|
|
"releasing a string left refcount at %d", first->refcount);
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_heap_exhaustion(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
akgl_Actor *actor = NULL;
|
|
akgl_Sprite *sprite = NULL;
|
|
akgl_SpriteSheet *sheet = NULL;
|
|
akgl_Character *basechar = NULL;
|
|
akgl_String *str = NULL;
|
|
int i = 0;
|
|
|
|
ATTEMPT {
|
|
// Actors: the acquire function does not claim the slot, so the test does.
|
|
CATCH(e, reset_all_heaps());
|
|
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
|
akgl_heap_actors[i].refcount = 1;
|
|
}
|
|
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP, akgl_heap_next_actor(&actor),
|
|
"akgl_heap_next_actor with every slot claimed");
|
|
|
|
CATCH(e, reset_all_heaps());
|
|
for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++ ) {
|
|
akgl_heap_sprites[i].refcount = 1;
|
|
}
|
|
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP, akgl_heap_next_sprite(&sprite),
|
|
"akgl_heap_next_sprite with every slot claimed");
|
|
|
|
CATCH(e, reset_all_heaps());
|
|
for ( i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++ ) {
|
|
akgl_heap_spritesheets[i].refcount = 1;
|
|
}
|
|
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP, akgl_heap_next_spritesheet(&sheet),
|
|
"akgl_heap_next_spritesheet with every slot claimed");
|
|
|
|
CATCH(e, reset_all_heaps());
|
|
for ( i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++ ) {
|
|
akgl_heap_characters[i].refcount = 1;
|
|
}
|
|
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP, akgl_heap_next_character(&basechar),
|
|
"akgl_heap_next_character with every slot claimed");
|
|
|
|
// Strings claim their own slots, so draining the pool needs no help.
|
|
CATCH(e, reset_all_heaps());
|
|
for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
|
|
CATCH(e, akgl_heap_next_string(&str));
|
|
}
|
|
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP, akgl_heap_next_string(&str),
|
|
"akgl_heap_next_string with the pool drained");
|
|
|
|
// A single release makes exactly one slot available again.
|
|
CATCH(e, akgl_heap_release_string(&akgl_heap_strings[0]));
|
|
TEST_EXPECT_OK(e, akgl_heap_next_string(&str),
|
|
"akgl_heap_next_string after freeing one slot");
|
|
TEST_ASSERT(e, str == &akgl_heap_strings[0],
|
|
"the reclaimed string was not the slot that was released");
|
|
|
|
CATCH(e, reset_all_heaps());
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_heap_release_refcounting(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
akgl_Sprite *sprite = NULL;
|
|
|
|
ATTEMPT {
|
|
CATCH(e, reset_all_heaps());
|
|
CATCH(e, akgl_heap_next_sprite(&sprite));
|
|
|
|
// A shared object survives until the last reference goes away.
|
|
sprite->refcount = 2;
|
|
strncpy((char *)&sprite->name, "shared", AKGL_SPRITE_MAX_NAME_LENGTH - 1);
|
|
CATCH(e, akgl_heap_release_sprite(sprite));
|
|
TEST_ASSERT(e, sprite->refcount == 1,
|
|
"releasing a twice-referenced sprite left refcount %d, expected 1",
|
|
sprite->refcount);
|
|
TEST_ASSERT(e, sprite->name[0] == 's',
|
|
"releasing a still-referenced sprite cleared its data");
|
|
|
|
CATCH(e, akgl_heap_release_sprite(sprite));
|
|
TEST_ASSERT(e, sprite->refcount == 0,
|
|
"the final release left refcount %d, expected 0", sprite->refcount);
|
|
TEST_ASSERT(e, sprite->name[0] == 0x00,
|
|
"the final release did not clear the sprite");
|
|
|
|
// Releasing an already-free object is clamped, not wrapped below zero.
|
|
CATCH(e, akgl_heap_release_sprite(sprite));
|
|
TEST_ASSERT(e, sprite->refcount == 0,
|
|
"over-releasing drove refcount to %d, expected a clamp at 0",
|
|
sprite->refcount);
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_heap_release_actor_children(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
akgl_Actor *parent = NULL;
|
|
akgl_Actor *child = NULL;
|
|
char namebuf[AKGL_ACTOR_MAX_NAME_LENGTH];
|
|
bool released = true;
|
|
int i = 0;
|
|
|
|
ATTEMPT {
|
|
CATCH(e, reset_all_heaps());
|
|
CATCH(e, akgl_heap_next_actor(&parent));
|
|
CATCH(e, akgl_actor_initialize(parent, "parent"));
|
|
|
|
// Fill every child slot, then release the parent once.
|
|
for ( i = 0; i < AKGL_ACTOR_MAX_CHILDREN; i++ ) {
|
|
snprintf((char *)&namebuf, AKGL_ACTOR_MAX_NAME_LENGTH, "child%d", i);
|
|
CATCH(e, akgl_heap_next_actor(&child));
|
|
CATCH(e, akgl_actor_initialize(child, (char *)&namebuf));
|
|
CATCH(e, akgl_actor_add_child(parent, child));
|
|
}
|
|
|
|
CATCH(e, akgl_heap_release_actor(parent));
|
|
TEST_ASSERT(e, parent->refcount == 0,
|
|
"releasing the parent left refcount %d", parent->refcount);
|
|
|
|
// Each child was initialized to 1 and incremented to 2 by add_child, so
|
|
// the recursive release should have brought every one of them back to 1.
|
|
for ( i = 1; i <= AKGL_ACTOR_MAX_CHILDREN; i++ ) {
|
|
TEST_ASSERT_FLAG(released, akgl_heap_actors[i].refcount == 1);
|
|
}
|
|
TEST_ASSERT(e, released,
|
|
"releasing a parent did not decrement every child exactly once");
|
|
|
|
CATCH(e, reset_all_heaps());
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_heap_release_clears_registry(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
akgl_Actor *actor = NULL;
|
|
akgl_Character *basechar = NULL;
|
|
|
|
ATTEMPT {
|
|
CATCH(e, reset_all_heaps());
|
|
|
|
CATCH(e, akgl_heap_next_actor(&actor));
|
|
CATCH(e, akgl_actor_initialize(actor, "registered"));
|
|
TEST_ASSERT(e, SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "registered", NULL) != NULL,
|
|
"akgl_actor_initialize did not register the actor");
|
|
CATCH(e, akgl_heap_release_actor(actor));
|
|
TEST_ASSERT(e, SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "registered", NULL) == NULL,
|
|
"releasing an actor left a dangling registry entry");
|
|
|
|
CATCH(e, akgl_heap_next_character(&basechar));
|
|
CATCH(e, akgl_character_initialize(basechar, "regchar"));
|
|
TEST_ASSERT(e, SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "regchar", NULL) != NULL,
|
|
"akgl_character_initialize did not register the character");
|
|
CATCH(e, akgl_heap_release_character(basechar));
|
|
TEST_ASSERT(e, SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "regchar", NULL) == NULL,
|
|
"releasing a character left a dangling registry entry");
|
|
|
|
CATCH(e, reset_all_heaps());
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_heap_release_nullpointers(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
|
|
ATTEMPT {
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_heap_release_actor(NULL),
|
|
"akgl_heap_release_actor(NULL)");
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_heap_release_sprite(NULL),
|
|
"akgl_heap_release_sprite(NULL)");
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_heap_release_spritesheet(NULL),
|
|
"akgl_heap_release_spritesheet(NULL)");
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_heap_release_character(NULL),
|
|
"akgl_heap_release_character(NULL)");
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_heap_release_string(NULL),
|
|
"akgl_heap_release_string(NULL)");
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_heap_release_spritesheet_texture(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
akgl_SpriteSheet *sheet = NULL;
|
|
|
|
ATTEMPT {
|
|
CATCH(e, reset_all_heaps());
|
|
CATCH(e, akgl_heap_next_spritesheet(&sheet));
|
|
|
|
// A spritesheet with no texture must still release cleanly; the texture
|
|
// branch is exercised by the sprite suite, which has a live renderer.
|
|
sheet->refcount = 1;
|
|
sheet->texture = NULL;
|
|
CATCH(e, akgl_heap_release_spritesheet(sheet));
|
|
TEST_ASSERT(e, sheet->refcount == 0,
|
|
"releasing a textureless spritesheet left refcount %d", sheet->refcount);
|
|
TEST_ASSERT(e, sheet->texture == NULL,
|
|
"releasing a spritesheet left a non-NULL texture pointer");
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
|
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_error_init());
|
|
CATCH(errctx, akgl_heap_init());
|
|
CATCH(errctx, akgl_registry_init());
|
|
|
|
CATCH(errctx, test_heap_init_clears_every_pool());
|
|
CATCH(errctx, test_heap_next_string_refcounting());
|
|
CATCH(errctx, test_heap_exhaustion());
|
|
CATCH(errctx, test_heap_release_refcounting());
|
|
CATCH(errctx, test_heap_release_actor_children());
|
|
CATCH(errctx, test_heap_release_clears_registry());
|
|
CATCH(errctx, test_heap_release_nullpointers());
|
|
CATCH(errctx, test_heap_release_spritesheet_texture());
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH_NORETURN(errctx);
|
|
}
|