/** * @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 #include #include #include #include #include #include #include #include #include #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++ ) { HEAP_ACTOR[i].refcount = 5; } for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++ ) { HEAP_SPRITE[i].refcount = 5; } for ( i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++ ) { HEAP_SPRITESHEET[i].refcount = 5; } for ( i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++ ) { HEAP_CHARACTER[i].refcount = 5; } for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) { HEAP_STRING[i].refcount = 5; } CATCH(e, akgl_heap_init()); for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) { TEST_ASSERT_FLAG(clean, HEAP_ACTOR[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, HEAP_SPRITE[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, HEAP_SPRITESHEET[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, HEAP_CHARACTER[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, HEAP_STRING[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. HEAP_ACTOR[0].refcount = 9; HEAP_SPRITE[0].refcount = 9; CATCH(e, akgl_heap_init_actor()); TEST_ASSERT(e, HEAP_ACTOR[0].refcount == 0, "akgl_heap_init_actor did not clear the actor pool"); TEST_ASSERT(e, HEAP_SPRITE[0].refcount == 9, "akgl_heap_init_actor cleared the sprite pool as well"); HEAP_SPRITE[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++ ) { HEAP_ACTOR[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++ ) { HEAP_SPRITE[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++ ) { HEAP_SPRITESHEET[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++ ) { HEAP_CHARACTER[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(&HEAP_STRING[0])); TEST_EXPECT_OK(e, akgl_heap_next_string(&str), "akgl_heap_next_string after freeing one slot"); TEST_ASSERT(e, str == &HEAP_STRING[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, HEAP_ACTOR[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_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); }