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
314 lines
12 KiB
C
314 lines
12 KiB
C
#include <SDL3/SDL.h>
|
|
#include <akerror.h>
|
|
|
|
#include <akgl/character.h>
|
|
#include <akgl/actor.h>
|
|
#include <akgl/error.h>
|
|
#include <akgl/heap.h>
|
|
#include <akgl/registry.h>
|
|
#include <akgl/iterator.h>
|
|
#include <akgl/game.h>
|
|
#include <akgl/renderer.h>
|
|
|
|
#include "testutil.h"
|
|
|
|
/*
|
|
* This suite used to declare its own file-scope `window` and `renderer`, back
|
|
* when the library's globals carried those bare names too. Both definitions had
|
|
* external linkage and the same spelling, so the executable's preempted the
|
|
* shared library's: akgl_sprite_load_json read the test's `SDL_Renderer *`
|
|
* through an `akgl_RenderBackend *` and happened not to crash. That is the
|
|
* collision the akgl_ prefix on exported globals exists to prevent, and it was
|
|
* invisible until they were renamed. The suite now binds a real backend, the
|
|
* way tests/sprite.c and tests/text.c do.
|
|
*/
|
|
|
|
akerr_ErrorContext *test_akgl_character_initialize()
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akgl_Character *testchar = NULL;
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_heap_next_character(&testchar));
|
|
CATCH(errctx, akgl_character_initialize(testchar, "testchar"));
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "testchar", NULL),
|
|
AKERR_KEY,
|
|
"Character was not placed in the registry");
|
|
FAIL_NONZERO_BREAK(
|
|
errctx,
|
|
strcmp((char *)&testchar->name, "testchar"),
|
|
AKERR_VALUE,
|
|
"Character was not named properly ('testchar' vs '%s')",
|
|
(char *)&testchar->name);
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
testchar->state_sprites,
|
|
AKERR_NULLPOINTER,
|
|
"Character state sprites map was not initialized");
|
|
} CLEANUP {
|
|
IGNORE(akgl_heap_release_character(testchar));
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_character_sprite_mgmt()
|
|
{
|
|
akgl_Character *testchar = NULL;
|
|
akgl_Sprite *testsprite = NULL;
|
|
akgl_Sprite *testsprite2 = NULL;
|
|
akgl_Sprite *comparesprite = NULL;
|
|
PREPARE_ERROR(errctx);
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_heap_next_character(&testchar));
|
|
CATCH(errctx, akgl_character_initialize(testchar, "testchar"));
|
|
CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
|
|
testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
testsprite,
|
|
AKERR_KEY,
|
|
"Sprite loaded from json but not in registry");
|
|
CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
|
|
testsprite2 = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
testsprite2,
|
|
AKERR_KEY,
|
|
"Sprite 2 loaded from json but not in registry");
|
|
CATCH(errctx, testchar->sprite_add(testchar, testsprite, AKGL_ACTOR_STATE_ALIVE));
|
|
CATCH(errctx, testchar->sprite_add(testchar, testsprite2, AKGL_ACTOR_STATE_DEAD));
|
|
CATCH(errctx, testchar->sprite_get(testchar, (AKGL_ACTOR_STATE_ALIVE), &comparesprite));
|
|
FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite), AKERR_VALUE, "Wrong sprite for state AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT");
|
|
CATCH(errctx, testchar->sprite_get(testchar, AKGL_ACTOR_STATE_DEAD, &comparesprite));
|
|
FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite2), AKERR_VALUE, "Wrong sprite for state AKGL_ACTOR_STATE_DEAD");
|
|
} CLEANUP {
|
|
IGNORE(akgl_heap_release_sprite(testsprite));
|
|
IGNORE(akgl_heap_release_sprite(testsprite2));
|
|
IGNORE(akgl_heap_release_character(testchar));
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_character_iterate_state_sprites()
|
|
{
|
|
akgl_Character *testchar = NULL;
|
|
akgl_Sprite *testsprite = NULL;
|
|
akgl_Sprite *testsprite2 = NULL;
|
|
akgl_Iterator opflags = {.flags = AKGL_ITERATOR_OP_RELEASE, .layerid = 0};
|
|
|
|
PREPARE_ERROR(errctx);
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_heap_next_character(&testchar));
|
|
CATCH(errctx, akgl_character_initialize(testchar, "testchar"));
|
|
CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
|
|
testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
testsprite,
|
|
AKERR_KEY,
|
|
"Sprite loaded from json but not in registry");
|
|
CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
|
|
testsprite2 = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
testsprite2,
|
|
AKERR_KEY,
|
|
"Sprite 2 loaded from json but not in registry");
|
|
CATCH(errctx, testchar->sprite_add(testchar, testsprite, AKGL_ACTOR_STATE_ALIVE));
|
|
CATCH(errctx, testchar->sprite_add(testchar, testsprite2, AKGL_ACTOR_STATE_DEAD));
|
|
|
|
SDL_EnumerateProperties(testchar->state_sprites, &akgl_character_state_sprites_iterate, &opflags);
|
|
|
|
// This is called by akgl_heap_release_character so we should assume that our property map is being
|
|
// deleted soon after this. So we don't care if the sprites have been deleted from the sprite_states.
|
|
// We just want to know they've been released.
|
|
FAIL_NONZERO_BREAK(
|
|
errctx,
|
|
(testsprite->refcount > 1),
|
|
AKERR_VALUE,
|
|
"akgl_heap_release_sprite not called for testsprite from iterator");
|
|
FAIL_NONZERO_BREAK(
|
|
errctx,
|
|
(testsprite2->refcount > 1),
|
|
AKERR_VALUE,
|
|
"akgl_heap_release_sprite not called for testsprite from iterator");
|
|
} CLEANUP {
|
|
IGNORE(akgl_heap_release_sprite(testsprite));
|
|
IGNORE(akgl_heap_release_sprite(testsprite2));
|
|
IGNORE(akgl_heap_release_character(testchar));
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_akgl_character_load_json()
|
|
{
|
|
akgl_Character *testcharacter = NULL;
|
|
akgl_Sprite *testsprite = NULL;
|
|
akgl_Sprite *testsprite2 = NULL;
|
|
akgl_Sprite *comparesprite = NULL;
|
|
int tsrc = 0;
|
|
int tsrc2 = 0;
|
|
|
|
PREPARE_ERROR(errctx);
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_heap_next_character(&testcharacter));
|
|
CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
|
|
testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
testsprite,
|
|
AKERR_KEY,
|
|
"Sprite loaded from json but not in registry");
|
|
CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
|
|
testsprite2 = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
testsprite2,
|
|
AKERR_KEY,
|
|
"Sprite 2 loaded from json but not in registry");
|
|
|
|
CATCH(errctx, akgl_character_load_json("assets/testcharacter.json"));
|
|
testcharacter = SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "testcharacter", NULL);
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
testcharacter,
|
|
AKERR_KEY,
|
|
"Character loaded from json but not in registry");
|
|
CATCH(errctx, testcharacter->sprite_get(testcharacter, (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT), &comparesprite));
|
|
FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite), AKERR_VALUE, "Wrong sprite for state AKGL_ACTOR_STATE_ALIVE");
|
|
CATCH(errctx, testcharacter->sprite_get(testcharacter, AKGL_ACTOR_STATE_DEAD, &comparesprite));
|
|
FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite2), AKERR_VALUE, "Wrong sprite for state AKGL_ACTOR_STATE_DEAD");
|
|
|
|
FAIL_ZERO_BREAK(errctx, (testcharacter->ax != 0.200000003), AKERR_VALUE, "Wrong X acceleration for test character");
|
|
FAIL_ZERO_BREAK(errctx, (testcharacter->ay != 0.200000003), AKERR_VALUE, "Wrong Y acceleration for test character");
|
|
|
|
// Release our handles on the sprites so the character's heap_release can reduce them to 0
|
|
CATCH(errctx, akgl_heap_release_sprite(testsprite));
|
|
CATCH(errctx, akgl_heap_release_sprite(testsprite2));
|
|
tsrc = testsprite->refcount;
|
|
tsrc2 = testsprite2->refcount;
|
|
CATCH(errctx, akgl_heap_release_character(testcharacter));
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
((testsprite->refcount < tsrc) || (testsprite2->refcount < tsrc2)),
|
|
AKERR_VALUE,
|
|
"character did not reduce reference count of its child sprites when released");
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief Rebinding a state must release the sprite it displaces.
|
|
*
|
|
* akgl_character_sprite_add took a reference for every binding and wrote over
|
|
* any existing entry without releasing the one it replaced, so a character that
|
|
* rebinds a state while alive leaked a sprite slot per rebind. Releasing at
|
|
* teardown does not cover it: the map only holds the last binding by then, so
|
|
* the earlier ones are unreachable.
|
|
*
|
|
* The write itself was also unchecked, so a failure to record the binding was
|
|
* reported as success -- with the reference already taken.
|
|
*/
|
|
akerr_ErrorContext *test_character_sprite_rebind_releases_displaced(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akgl_Character *testchar = NULL;
|
|
akgl_Sprite *first = NULL;
|
|
akgl_Sprite *second = NULL;
|
|
akgl_Sprite *found = NULL;
|
|
int firstbefore = 0;
|
|
int secondbefore = 0;
|
|
int i = 0;
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_heap_next_character(&testchar));
|
|
CATCH(errctx, akgl_character_initialize(testchar, "rebindchar"));
|
|
// Real sprites out of the fixtures: akgl_sprite_initialize refuses a
|
|
// NULL spritesheet, and a hand-built sprite would not be a sprite.
|
|
CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
|
|
CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
|
|
first = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
|
|
second = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
|
|
FAIL_ZERO_BREAK(errctx, first, AKERR_KEY, "testsprite is not in the registry");
|
|
FAIL_ZERO_BREAK(errctx, second, AKERR_KEY, "testsprite2 is not in the registry");
|
|
|
|
firstbefore = first->refcount;
|
|
secondbefore = second->refcount;
|
|
|
|
CATCH(errctx, akgl_character_sprite_add(testchar, first, AKGL_ACTOR_STATE_ALIVE));
|
|
TEST_ASSERT(errctx, first->refcount == (firstbefore + 1),
|
|
"binding a sprite did not take a reference (%d, expected %d)",
|
|
first->refcount, firstbefore + 1);
|
|
|
|
// Rebind the same state to a different sprite.
|
|
CATCH(errctx, akgl_character_sprite_add(testchar, second, AKGL_ACTOR_STATE_ALIVE));
|
|
TEST_ASSERT(errctx, second->refcount == (secondbefore + 1),
|
|
"rebinding did not take a reference on the new sprite (%d, expected %d)",
|
|
second->refcount, secondbefore + 1);
|
|
TEST_ASSERT(errctx, first->refcount == firstbefore,
|
|
"rebinding did not release the displaced sprite (%d, expected %d)",
|
|
first->refcount, firstbefore);
|
|
|
|
CATCH(errctx, testchar->sprite_get(testchar, AKGL_ACTOR_STATE_ALIVE, &found));
|
|
TEST_ASSERT(errctx, found == second, "the state is not bound to the new sprite");
|
|
|
|
// Rebinding a state to the sprite already there must be a no-op on the
|
|
// count rather than a release followed by a claim of the same slot.
|
|
CATCH(errctx, akgl_character_sprite_add(testchar, second, AKGL_ACTOR_STATE_ALIVE));
|
|
TEST_ASSERT(errctx, second->refcount == (secondbefore + 2),
|
|
"rebinding a state to the sprite it already held mishandled the count (%d)",
|
|
second->refcount);
|
|
|
|
// And a long run of rebinds must not drift.
|
|
for ( i = 0; i < 200; i++ ) {
|
|
CATCH(errctx, akgl_character_sprite_add(testchar, first, AKGL_ACTOR_STATE_DEAD));
|
|
CATCH(errctx, akgl_character_sprite_add(testchar, second, AKGL_ACTOR_STATE_DEAD));
|
|
}
|
|
TEST_ASSERT(errctx, first->refcount <= (firstbefore + 1),
|
|
"200 rebinds left the first sprite at refcount %d", first->refcount);
|
|
} CLEANUP {
|
|
if ( testchar != NULL ) {
|
|
IGNORE(akgl_heap_release_character(testchar));
|
|
}
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_error_init());
|
|
akgl_renderer = &akgl_default_renderer;
|
|
|
|
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
|
|
|
|
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
|
|
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
|
|
}
|
|
|
|
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_character", 640, 480, SDL_WINDOW_HIDDEN, &akgl_window, &akgl_renderer->sdl_renderer)) {
|
|
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
|
|
}
|
|
CATCH(errctx, akgl_render_2d_bind(akgl_renderer));
|
|
|
|
CATCH(errctx, akgl_heap_init());
|
|
CATCH(errctx, akgl_registry_init());
|
|
CATCH(errctx, test_akgl_character_initialize());
|
|
CATCH(errctx, test_character_sprite_mgmt());
|
|
CATCH(errctx, test_character_iterate_state_sprites());
|
|
CATCH(errctx, test_akgl_character_load_json());
|
|
CATCH(errctx, test_character_sprite_rebind_releases_displaced());
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH_NORETURN(errctx);
|
|
return 0;
|
|
}
|