#include #include #include #include #include #include #include #include #include #include #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()); TEST_TRAP_UNHANDLED_ERRORS(); 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; }