Files
libakgl/tests/actor.c

932 lines
36 KiB
C
Raw Normal View History

#define UNHANDLED_ERROR_TERMINATION_BEHAVIOR \
handle_unhandled_error(errctx);
#include <akerror.h>
#include <akgl/error.h>
#define UNHANDLED_ERROR_EXIT 0
#define UNHANDLED_ERROR_SET 1
#include <SDL3/SDL.h>
#include <stdlib.h>
#include <akgl/iterator.h>
#include <akgl/registry.h>
#include <akgl/actor.h>
Add physics, heap, json_helpers, game, and actor test suites Raise line coverage from 39.6 to 61.8 percent with four new suites and an extension to the actor suite, and register every suite through a single CMake list so a new test file cannot be left out of the coverage fixture. Give the test targets a build-tree RPATH and prepend the build tree to LD_LIBRARY_PATH for CTest, so a developer with a previously installed libakgl.so exercises the library that was just compiled. Fix six defects the new tests exposed: - akgl_physics_simulate read self->gravity_time before its NULL check, so a NULL backend crashed instead of reporting AKERR_NULLPOINTER. - akgl_game_save transposed CLEANUP and PROCESS, which placed the fclose inside the PROCESS switch. An ordinary save never flushed or closed its stream and produced an empty file. - akgl_game_save_actors wrote each name table terminator from the address of a single char, emitting stack contents into the save file and a sentinel the loader could not recognize. - akgl_game_load_objectnamemap used CATCH directly inside while(1), where the break leaves the loop rather than propagating, so a truncated name table loaded as a successful game. - akgl_Actor_cmhf_up_on and _down_on dereferenced actor->basechar with no NULL check, unlike their left and right counterparts. - akgl_actor_logic_movement checked actor twice instead of checking actor->basechar before dereferencing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:03:21 -04:00
#include <akgl/character.h>
#include <akgl/sprite.h>
#include <akgl/game.h>
#include <akgl/heap.h>
Add physics, heap, json_helpers, game, and actor test suites Raise line coverage from 39.6 to 61.8 percent with four new suites and an extension to the actor suite, and register every suite through a single CMake list so a new test file cannot be left out of the coverage fixture. Give the test targets a build-tree RPATH and prepend the build tree to LD_LIBRARY_PATH for CTest, so a developer with a previously installed libakgl.so exercises the library that was just compiled. Fix six defects the new tests exposed: - akgl_physics_simulate read self->gravity_time before its NULL check, so a NULL backend crashed instead of reporting AKERR_NULLPOINTER. - akgl_game_save transposed CLEANUP and PROCESS, which placed the fclose inside the PROCESS switch. An ordinary save never flushed or closed its stream and produced an empty file. - akgl_game_save_actors wrote each name table terminator from the address of a single char, emitting stack contents into the save file and a sentinel the loader could not recognize. - akgl_game_load_objectnamemap used CATCH directly inside while(1), where the break leaves the loop rather than propagating, so a truncated name table loaded as a successful game. - akgl_Actor_cmhf_up_on and _down_on dereferenced actor->basechar with no NULL check, unlike their left and right counterparts. - akgl_actor_logic_movement checked actor twice instead of checking actor->basechar before dereferencing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:03:21 -04:00
#include "testutil.h"
int UNHANDLED_ERROR_BEHAVIOR;
akerr_ErrorContext *unhandled_error_context;
void handle_unhandled_error_noexit(akerr_ErrorContext *errctx)
{
if ( errctx == NULL ) {
return;
}
if ( UNHANDLED_ERROR_BEHAVIOR == UNHANDLED_ERROR_EXIT ) {
exit(errctx->status);
}
if ( UNHANDLED_ERROR_BEHAVIOR == UNHANDLED_ERROR_SET ) {
unhandled_error_context = errctx;
errctx->refcount += 1;
return;
}
}
int akgl_actor_updated;
akerr_ErrorContext *akgl_actor_update_noop(akgl_Actor *obj)
{
PREPARE_ERROR(errctx);
akgl_actor_updated = 1;
SUCCEED_RETURN(errctx);
}
// Currently the renderer assumes there is a global variable named `renderer`
int akgl_actor_rendered;
akerr_ErrorContext *akgl_actor_render_noop(akgl_Actor *obj)
{
PREPARE_ERROR(errctx);
akgl_actor_rendered = 1;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_registry_actor_iterator_nullpointers(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorUnhandledErrorHandler defaulthandler = akerr_handler_unhandled_error;
akerr_handler_unhandled_error = handle_unhandled_error_noexit;
ATTEMPT {
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_SET;
DETECT(unhandled_error_context, akgl_registry_iterate_actor(NULL, AKGL_REGISTRY_ACTOR, ""));
} CLEANUP {
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_EXIT;
} PROCESS(unhandled_error_context) {
} HANDLE(unhandled_error_context, AKERR_NULLPOINTER) {
printf("Handled\n");
} FINISH(unhandled_error_context, true);
akerr_handler_unhandled_error = defaulthandler;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_registry_actor_iterator_missingactor(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorUnhandledErrorHandler defaulthandler = akerr_handler_unhandled_error;
akgl_Iterator iter = {
.layerid = 0,
.flags = 0
};
akerr_handler_unhandled_error = handle_unhandled_error_noexit;
ATTEMPT {
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_SET;
DETECT(
unhandled_error_context,
akgl_registry_iterate_actor(
&iter,
AKGL_REGISTRY_ACTOR,
"Actor who doesn't exist")
);
} CLEANUP {
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_EXIT;
} PROCESS(unhandled_error_context) {
} HANDLE(unhandled_error_context, AKERR_KEY) {
printf("Handled\n");
} FINISH(unhandled_error_context, true);
akerr_handler_unhandled_error = defaulthandler;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_registry_actor_iterator_updaterender(void)
{
akgl_Actor *testactor;
akgl_Iterator iter = {
.layerid = 0,
.flags = AKGL_ITERATOR_OP_UPDATE | AKGL_ITERATOR_OP_RENDER
};
akerr_ErrorUnhandledErrorHandler defaulthandler = akerr_handler_unhandled_error;
PREPARE_ERROR(errctx);
akerr_handler_unhandled_error = handle_unhandled_error_noexit;
ATTEMPT {
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_SET;
CATCH(unhandled_error_context, akgl_heap_next_actor(&testactor));
CATCH(unhandled_error_context, akgl_actor_initialize(testactor, "test"));
testactor->layer = 0;
testactor->updatefunc = &akgl_actor_update_noop;
testactor->renderfunc = &akgl_actor_render_noop;
DETECT(
unhandled_error_context,
akgl_registry_iterate_actor(
&iter,
AKGL_REGISTRY_ACTOR,
"test")
);
FAIL_ZERO_BREAK(
unhandled_error_context,
akgl_actor_updated,
AKGL_ERR_BEHAVIOR,
"actor->updatefunc not called by the iterator"
);
FAIL_ZERO_BREAK(
unhandled_error_context,
akgl_actor_rendered,
AKGL_ERR_BEHAVIOR,
"actor->renderfunc not called by the iterator"
);
} CLEANUP {
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_EXIT;
IGNORE(akgl_heap_release_actor(testactor));
} PROCESS(unhandled_error_context) {
} FINISH(unhandled_error_context, true);
akerr_handler_unhandled_error = defaulthandler;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_actor_set_character(void)
{
akgl_Actor *testactor = NULL;
akgl_Character *testchar = NULL;
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_actor_set_character(NULL, "test"));
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
printf("Handled\n");
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_heap_next_actor(&testactor));
CATCH(errctx, akgl_actor_initialize(testactor, "test"));
testactor->layer = 0;
testactor->updatefunc = &akgl_actor_update_noop;
testactor->renderfunc = &akgl_actor_render_noop;
CATCH(errctx, akgl_actor_set_character(testactor, "test"));
} CLEANUP {
IGNORE(akgl_heap_release_actor(testactor));
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
printf("Handled\n");
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_heap_next_actor(&testactor));
CATCH(errctx, akgl_heap_next_character(&testchar));
CATCH(errctx, akgl_actor_initialize(testactor, "test"));
testactor->layer = 0;
testactor->updatefunc = &akgl_actor_update_noop;
testactor->renderfunc = &akgl_actor_render_noop;
CATCH(errctx, akgl_character_initialize(testchar, "test"));
CATCH(errctx, akgl_actor_set_character(testactor, "test"));
} CLEANUP {
IGNORE(akgl_heap_release_actor(testactor));
IGNORE(akgl_heap_release_character(testchar));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_actor_manage_children(void)
{
akgl_Actor *parent = NULL;
akgl_Actor *child = NULL;
akgl_String *tmpstring = NULL;
int i = 0;
int j = 0;
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_heap_next_string(&tmpstring));
CATCH(errctx, akgl_heap_next_actor(&parent));
CATCH(errctx, akgl_actor_initialize(parent, "parent"));
for ( i = 0 ; i < AKGL_ACTOR_MAX_CHILDREN; i++ ) {
sprintf((char *)&tmpstring->data, "child %d", i);
CATCH(errctx, akgl_heap_next_actor(&child));
CATCH(errctx, akgl_actor_initialize(child, (char *)&tmpstring->data));
CATCH(errctx, parent->addchild(parent, child));
// Release our claim on the actor so the parent can own the child and kill it
CATCH(errctx, akgl_heap_release_actor(child));
}
} CLEANUP {
IGNORE(akgl_heap_release_string(tmpstring));
} PROCESS(errctx) {
} FINISH(errctx, true);
ATTEMPT {
// Doesn't matter which child we use for this test
child = parent->children[1];
CATCH(errctx, parent->addchild(parent, child));
} CLEANUP {
if ( errctx == NULL ) {
FAIL(errctx, AKGL_ERR_BEHAVIOR, "addchild does not throw AKERR_RELATIONSHIP when child already has a parent");
}
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_RELATIONSHIP) {
// Expected behavior
SDL_Log("addchild throws AKERR_RELATIONSHIP when child already has a parent");
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_heap_next_actor(&child));
CATCH(errctx, parent->addchild(parent, child));
} CLEANUP {
if ( errctx == NULL ) {
FAIL(errctx, AKGL_ERR_BEHAVIOR, "addchild does not throw AKERR_OUTOFBOUNDS when all children already set");
}
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_OUTOFBOUNDS) {
// Expected behavior
SDL_Log("addchild throws AKERR_OUTOFBOUNDS when all children already set");
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_heap_release_actor(parent));
// All actor objects on the heap should be empty now
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++) {
FAIL_NONZERO_BREAK(errctx, HEAP_ACTOR[i].refcount, AKERR_VALUE, "Actor not properly cleared");
FAIL_NONZERO_BREAK(errctx, HEAP_ACTOR[i].parent, AKERR_VALUE, "Actor not properly cleared");
for ( j = 0 ; j < AKGL_ACTOR_MAX_CHILDREN; j++) {
if ( HEAP_ACTOR[i].children[j] != NULL ) {
FAIL(errctx, AKERR_VALUE, "Actor not properly cleared");
goto _test_actor_addchild_heaprelease_cleanup;
}
}
}
for ( j = 0; j < AKGL_ACTOR_MAX_CHILDREN; j++) {
sprintf((char *)&tmpstring->data, "child %d", i);
FAIL_NONZERO_BREAK(
errctx,
SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, (char *)&tmpstring->data, NULL),
AKERR_KEY,
"Child %s was not removed from the registry",
(char *)&tmpstring->data);
}
_test_actor_addchild_heaprelease_cleanup:
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_heap_next_actor(&parent));
CATCH(errctx, akgl_actor_initialize(parent, "parent"));
CATCH(errctx, akgl_heap_next_actor(&child));
CATCH(errctx, akgl_actor_initialize(child, "child"));
// Don't release our claim on the child. The child should not be reclaimed.
CATCH(errctx, akgl_heap_release_actor(parent));
FAIL_NONZERO_BREAK(errctx, child->parent, AKERR_VALUE, "Child still references released parent");
FAIL_ZERO_BREAK(errctx, child->refcount, AKERR_VALUE, "Child prematurely released");
FAIL_NONZERO_BREAK(errctx, strcmp(child->name, "child"), AKERR_VALUE, "Child had identity erased");
FAIL_ZERO_BREAK(
errctx,
(child == SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, child->name, NULL)),
AKERR_KEY,
"Child prematurely removed from the registry");
// Now we can release the child
CATCH(errctx, akgl_heap_release_actor(child));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
Add physics, heap, json_helpers, game, and actor test suites Raise line coverage from 39.6 to 61.8 percent with four new suites and an extension to the actor suite, and register every suite through a single CMake list so a new test file cannot be left out of the coverage fixture. Give the test targets a build-tree RPATH and prepend the build tree to LD_LIBRARY_PATH for CTest, so a developer with a previously installed libakgl.so exercises the library that was just compiled. Fix six defects the new tests exposed: - akgl_physics_simulate read self->gravity_time before its NULL check, so a NULL backend crashed instead of reporting AKERR_NULLPOINTER. - akgl_game_save transposed CLEANUP and PROCESS, which placed the fclose inside the PROCESS switch. An ordinary save never flushed or closed its stream and produced an empty file. - akgl_game_save_actors wrote each name table terminator from the address of a single char, emitting stack contents into the save file and a sentinel the loader could not recognize. - akgl_game_load_objectnamemap used CATCH directly inside while(1), where the break leaves the loop rather than propagating, so a truncated name table loaded as a successful game. - akgl_Actor_cmhf_up_on and _down_on dereferenced actor->basechar with no NULL check, unlike their left and right counterparts. - akgl_actor_logic_movement checked actor twice instead of checking actor->basechar before dereferencing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:03:21 -04:00
/**
* @brief Build an actor bound to a character, without touching the renderer.
*
* The sprite and spritesheet are taken straight off the heap and populated by
* hand, because akgl_sprite_load_json() would need a live renderer to build a
* texture and none of the logic under test reads one.
*/
static akerr_ErrorContext *make_bound_actor(
akgl_Actor **actor,
akgl_Character **basechar,
akgl_Sprite **sprite,
char *name,
int state)
{
PREPARE_ERROR(e);
akgl_SpriteSheet *sheet = NULL;
char spritename[AKGL_SPRITE_MAX_NAME_LENGTH];
ATTEMPT {
CATCH(e, akgl_heap_next_character(basechar));
CATCH(e, akgl_character_initialize(*basechar, name));
CATCH(e, akgl_heap_next_spritesheet(&sheet));
sheet->refcount += 1;
snprintf((char *)&spritename, AKGL_SPRITE_MAX_NAME_LENGTH, "%s_sprite", name);
CATCH(e, akgl_heap_next_sprite(sprite));
CATCH(e, akgl_sprite_initialize(*sprite, (char *)&spritename, sheet));
(*sprite)->width = 32;
(*sprite)->height = 32;
(*sprite)->frames = 4;
(*sprite)->speed = 100;
CATCH(e, akgl_character_sprite_add(*basechar, *sprite, state));
CATCH(e, akgl_heap_next_actor(actor));
CATCH(e, akgl_actor_initialize(*actor, name));
(*actor)->basechar = *basechar;
(*actor)->state = state;
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_control_handlers_on(void)
{
PREPARE_ERROR(e);
akgl_Actor actor;
akgl_Character basechar;
SDL_Event event;
ATTEMPT {
memset(&event, 0x00, sizeof(SDL_Event));
memset(&basechar, 0x00, sizeof(akgl_Character));
basechar.ax = 7.0f;
basechar.ay = 11.0f;
// Left: clears every facing and movement bit, then sets its own pair and
// takes acceleration from the base character with the sign reversed.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
actor.state = (AKGL_ACTOR_STATE_FACE_RIGHT | AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_ALIVE);
TEST_EXPECT_OK(e, akgl_Actor_cmhf_left_on(&actor, &event), "left on");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_LEFT),
"left on did not set MOVING_LEFT (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_LEFT),
"left on did not set FACE_LEFT (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_RIGHT),
"left on left MOVING_RIGHT set (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
"left on left FACE_RIGHT set (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_ALIVE),
"left on cleared an unrelated state bit (state %d)", actor.state);
TEST_ASSERT_FEQ(e, actor.ax, -7.0f, "left on set ax to %f, expected -7", actor.ax);
// Right: same, with the sign preserved.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
TEST_EXPECT_OK(e, akgl_Actor_cmhf_right_on(&actor, &event), "right on");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_RIGHT),
"right on did not set MOVING_RIGHT (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
"right on did not set FACE_RIGHT (state %d)", actor.state);
TEST_ASSERT_FEQ(e, actor.ax, 7.0f, "right on set ax to %f, expected 7", actor.ax);
// Up reverses the Y acceleration, because Y grows downward.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
TEST_EXPECT_OK(e, akgl_Actor_cmhf_up_on(&actor, &event), "up on");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_UP),
"up on did not set MOVING_UP (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_UP),
"up on did not set FACE_UP (state %d)", actor.state);
TEST_ASSERT_FEQ(e, actor.ay, -11.0f, "up on set ay to %f, expected -11", actor.ay);
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
TEST_EXPECT_OK(e, akgl_Actor_cmhf_down_on(&actor, &event), "down on");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_DOWN),
"down on did not set MOVING_DOWN (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_DOWN),
"down on did not set FACE_DOWN (state %d)", actor.state);
TEST_ASSERT_FEQ(e, actor.ay, 11.0f, "down on set ay to %f, expected 11", actor.ay);
// Each direction is exclusive: turning right after left leaves only right.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
TEST_EXPECT_OK(e, akgl_Actor_cmhf_left_on(&actor, &event), "left on before turning");
TEST_EXPECT_OK(e, akgl_Actor_cmhf_right_on(&actor, &event), "right on after left");
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_LEFT),
"turning right left MOVING_LEFT set (state %d)", actor.state);
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_control_handlers_off(void)
{
PREPARE_ERROR(e);
akgl_Actor actor;
akgl_Character basechar;
SDL_Event event;
ATTEMPT {
memset(&event, 0x00, sizeof(SDL_Event));
memset(&basechar, 0x00, sizeof(akgl_Character));
basechar.ax = 7.0f;
basechar.ay = 11.0f;
// Releasing a direction zeroes that axis outright: acceleration, thrust,
// environmental force, and velocity all go to zero together.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
actor.ax = 5; actor.ex = 5; actor.tx = 5; actor.vx = 5;
actor.ay = 5; actor.ey = 5; actor.ty = 5; actor.vy = 5;
actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_LEFT);
TEST_EXPECT_OK(e, akgl_Actor_cmhf_left_off(&actor, &event), "left off");
TEST_ASSERT_FEQ(e, actor.ax, 0.0f, "left off left ax at %f", actor.ax);
TEST_ASSERT_FEQ(e, actor.ex, 0.0f, "left off left ex at %f", actor.ex);
TEST_ASSERT_FEQ(e, actor.tx, 0.0f, "left off left tx at %f", actor.tx);
TEST_ASSERT_FEQ(e, actor.vx, 0.0f, "left off left vx at %f", actor.vx);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_LEFT),
"left off did not clear MOVING_LEFT (state %d)", actor.state);
// Facing is deliberately sticky, so an idle actor keeps looking where it was.
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_LEFT),
"left off cleared FACE_LEFT, which should persist (state %d)", actor.state);
// The Y axis is untouched by a horizontal release.
TEST_ASSERT_FEQ(e, actor.vy, 5.0f, "left off disturbed vy (%f)", actor.vy);
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.ax = 5; actor.ex = 5; actor.tx = 5; actor.vx = 5;
actor.state = AKGL_ACTOR_STATE_MOVING_RIGHT;
TEST_EXPECT_OK(e, akgl_Actor_cmhf_right_off(&actor, &event), "right off");
TEST_ASSERT_FEQ(e, actor.vx, 0.0f, "right off left vx at %f", actor.vx);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_RIGHT),
"right off did not clear MOVING_RIGHT (state %d)", actor.state);
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.ay = 5; actor.ey = 5; actor.ty = 5; actor.vy = 5;
actor.state = AKGL_ACTOR_STATE_MOVING_UP;
TEST_EXPECT_OK(e, akgl_Actor_cmhf_up_off(&actor, &event), "up off");
TEST_ASSERT_FEQ(e, actor.vy, 0.0f, "up off left vy at %f", actor.vy);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_UP),
"up off did not clear MOVING_UP (state %d)", actor.state);
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.ay = 5; actor.ey = 5; actor.ty = 5; actor.vy = 5;
actor.state = AKGL_ACTOR_STATE_MOVING_DOWN;
TEST_EXPECT_OK(e, akgl_Actor_cmhf_down_off(&actor, &event), "down off");
TEST_ASSERT_FEQ(e, actor.vy, 0.0f, "down off left vy at %f", actor.vy);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_DOWN),
"down off did not clear MOVING_DOWN (state %d)", actor.state);
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_control_handlers_nullpointers(void)
{
PREPARE_ERROR(e);
akgl_Actor actor;
SDL_Event event;
ATTEMPT {
memset(&event, 0x00, sizeof(SDL_Event));
memset(&actor, 0x00, sizeof(akgl_Actor));
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_left_on(NULL, &event), "left on, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_left_off(NULL, &event), "left off, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_right_on(NULL, &event), "right on, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_right_off(NULL, &event), "right off, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_up_on(NULL, &event), "up on, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_up_off(NULL, &event), "up off, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_down_on(NULL, &event), "down on, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_down_off(NULL, &event), "down off, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_left_on(&actor, NULL), "left on, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_left_off(&actor, NULL), "left off, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_right_on(&actor, NULL), "right on, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_right_off(&actor, NULL), "right off, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_up_on(&actor, NULL), "up on, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_up_off(&actor, NULL), "up off, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_down_on(&actor, NULL), "down on, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_down_off(&actor, NULL), "down off, NULL event");
// A movement handler reads acceleration off the base character, so an
// actor without one has to be reported rather than dereferenced.
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_left_on(&actor, &event),
"left on, actor with no base character");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_right_on(&actor, &event),
"right on, actor with no base character");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_up_on(&actor, &event),
"up on, actor with no base character");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_Actor_cmhf_down_on(&actor, &event),
"down on, actor with no base character");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_automatic_face(void)
{
PREPARE_ERROR(e);
akgl_Actor actor;
ATTEMPT {
// With the flag off, facing is left entirely to the caller.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.movement_controls_face = false;
actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_RIGHT);
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face with the flag off");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
"automatic face changed facing while disabled (state %d)", actor.state);
// With the flag on, facing follows movement.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.movement_controls_face = true;
actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_RIGHT);
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving left");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_LEFT),
"automatic face did not turn the actor left (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
"automatic face left the stale facing set (state %d)", actor.state);
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.movement_controls_face = true;
actor.state = AKGL_ACTOR_STATE_MOVING_RIGHT;
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving right");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
"automatic face did not turn the actor right (state %d)", actor.state);
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.movement_controls_face = true;
actor.state = AKGL_ACTOR_STATE_MOVING_UP;
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving up");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_UP),
"automatic face did not turn the actor up (state %d)", actor.state);
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.movement_controls_face = true;
actor.state = AKGL_ACTOR_STATE_MOVING_DOWN;
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving down");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_DOWN),
"automatic face did not turn the actor down (state %d)", actor.state);
// Left wins over up when both are set, per the order of the checks.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.movement_controls_face = true;
actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_MOVING_UP);
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving diagonally");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_LEFT),
"diagonal movement did not resolve to the left facing (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_FACE_UP),
"diagonal movement set two facings at once (state %d)", actor.state);
// A stationary actor ends up facing nowhere.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.movement_controls_face = true;
actor.state = (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP);
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while stationary");
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_FACE_ALL),
"a stationary actor kept a facing bit (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_ALIVE),
"automatic face cleared an unrelated state bit (state %d)", actor.state);
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_automatic_face(NULL),
"automatic face with a NULL actor");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_logic_movement(void)
{
PREPARE_ERROR(e);
akgl_Actor actor;
akgl_Character basechar;
ATTEMPT {
memset(&basechar, 0x00, sizeof(akgl_Character));
basechar.ax = 3.0f; basechar.ay = 4.0f;
basechar.sx = 30.0f; basechar.sy = 40.0f; basechar.sz = 50.0f;
// Max speed is always refreshed from the base character.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while idle");
TEST_ASSERT_FEQ(e, actor.sx, 30.0f, "sx copied as %f, expected 30", actor.sx);
TEST_ASSERT_FEQ(e, actor.sy, 40.0f, "sy copied as %f, expected 40", actor.sy);
TEST_ASSERT_FEQ(e, actor.sz, 50.0f, "sz copied as %f, expected 50", actor.sz);
actor.state = AKGL_ACTOR_STATE_MOVING_LEFT;
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving left");
TEST_ASSERT_FEQ(e, actor.ax, -3.0f, "moving left set ax to %f, expected -3", actor.ax);
actor.state = AKGL_ACTOR_STATE_MOVING_RIGHT;
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving right");
TEST_ASSERT_FEQ(e, actor.ax, 3.0f, "moving right set ax to %f, expected 3", actor.ax);
actor.state = AKGL_ACTOR_STATE_MOVING_UP;
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving up");
TEST_ASSERT_FEQ(e, actor.ay, -4.0f, "moving up set ay to %f, expected -4", actor.ay);
actor.state = AKGL_ACTOR_STATE_MOVING_DOWN;
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving down");
TEST_ASSERT_FEQ(e, actor.ay, 4.0f, "moving down set ay to %f, expected 4", actor.ay);
// Both axes at once.
actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_MOVING_DOWN);
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving diagonally");
TEST_ASSERT_FEQ(e, actor.ax, -3.0f, "diagonal movement set ax to %f, expected -3", actor.ax);
TEST_ASSERT_FEQ(e, actor.ay, 4.0f, "diagonal movement set ay to %f, expected 4", actor.ay);
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_logic_movement(NULL, 1.0f),
"movement logic with a NULL actor");
// Every value the movement logic writes is read off the base character,
// so an actor without one has to be reported rather than dereferenced.
memset(&actor, 0x00, sizeof(akgl_Actor));
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_logic_movement(&actor, 1.0f),
"movement logic with no base character");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_logic_changeframe(void)
{
PREPARE_ERROR(e);
akgl_Actor actor;
akgl_Sprite sprite;
ATTEMPT {
memset(&sprite, 0x00, sizeof(akgl_Sprite));
sprite.frames = 4;
// Mid-animation, the frame simply advances.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.curSpriteFrameId = 1;
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "advancing mid-animation");
TEST_ASSERT(e, actor.curSpriteFrameId == 2,
"frame advanced to %d, expected 2", actor.curSpriteFrameId);
// At the last frame without looping, it wraps to the start.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.curSpriteFrameId = 3;
sprite.loop = false;
sprite.loopReverse = false;
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "wrapping a non-looping sprite");
TEST_ASSERT(e, actor.curSpriteFrameId == 0,
"the last frame wrapped to %d, expected 0", actor.curSpriteFrameId);
// A forward-looping sprite behaves the same way at the end.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.curSpriteFrameId = 3;
sprite.loop = true;
sprite.loopReverse = false;
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "wrapping a forward-looping sprite");
TEST_ASSERT(e, actor.curSpriteFrameId == 0,
"the forward loop wrapped to %d, expected 0", actor.curSpriteFrameId);
// A ping-pong sprite turns around at the end instead of wrapping.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.curSpriteFrameId = 3;
sprite.loop = true;
sprite.loopReverse = true;
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "reversing at the end");
TEST_ASSERT(e, actor.curSpriteReversing == true,
"the sprite did not enter its reverse phase at the last frame");
TEST_ASSERT(e, actor.curSpriteFrameId == 2,
"reversing stepped to frame %d, expected 2", actor.curSpriteFrameId);
// While reversing, the frame counts back down.
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "stepping back while reversing");
TEST_ASSERT(e, actor.curSpriteFrameId == 1,
"reversing stepped to frame %d, expected 1", actor.curSpriteFrameId);
// At frame zero it turns around again and resumes going forward.
actor.curSpriteFrameId = 0;
actor.curSpriteReversing = true;
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "turning around at frame zero");
TEST_ASSERT(e, actor.curSpriteReversing == false,
"the sprite stayed in its reverse phase at frame zero");
TEST_ASSERT(e, actor.curSpriteFrameId == 1,
"turning around stepped to frame %d, expected 1", actor.curSpriteFrameId);
// A single-frame sprite has nowhere to advance to.
memset(&actor, 0x00, sizeof(akgl_Actor));
sprite.frames = 1;
sprite.loop = false;
sprite.loopReverse = false;
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "advancing a single-frame sprite");
TEST_ASSERT(e, actor.curSpriteFrameId == 0,
"a single-frame sprite moved to frame %d, expected 0", actor.curSpriteFrameId);
sprite.frames = 4;
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_logic_changeframe(NULL, &sprite, 0),
"changeframe with a NULL actor");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
/** @brief Records calls made to the changeframe stub. */
static int changeframe_calls = 0;
/** @brief Changeframe stub that records its invocation without advancing. */
static akerr_ErrorContext *stub_changeframe(akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtimems)
{
PREPARE_ERROR(e);
changeframe_calls += 1;
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_update(void)
{
PREPARE_ERROR(e);
akgl_Actor *actor = NULL;
akgl_Character *basechar = NULL;
akgl_Sprite *sprite = NULL;
SDL_Time now = 0;
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());
CATCH(e, make_bound_actor(&actor, &basechar, &sprite, "updatable", AKGL_ACTOR_STATE_ALIVE));
actor->changeframefunc = &stub_changeframe;
// Long enough since the last frame change, so the sprite advances.
SDL_GetCurrentTime(&now);
sprite->speed = 1;
actor->curSpriteFrameTimer = 0;
changeframe_calls = 0;
TEST_EXPECT_OK(e, akgl_actor_update(actor), "updating an actor whose frame is due");
TEST_ASSERT(e, changeframe_calls == 1,
"a due frame change fired %d times, expected 1", changeframe_calls);
TEST_ASSERT(e, actor->curSpriteFrameTimer != 0,
"the frame timer was not restamped after a frame change");
// Too soon since the last change, so nothing happens.
SDL_GetCurrentTime(&now);
sprite->speed = 1000000000;
actor->curSpriteFrameTimer = now;
changeframe_calls = 0;
TEST_EXPECT_OK(e, akgl_actor_update(actor), "updating an actor whose frame is not due");
TEST_ASSERT(e, changeframe_calls == 0,
"an early frame change fired %d times, expected 0", changeframe_calls);
// An actor in a state with no sprite bound is skipped, not failed: the
// missing binding is reported as AKERR_KEY and swallowed by update.
actor->state = AKGL_ACTOR_STATE_DEAD;
changeframe_calls = 0;
TEST_EXPECT_OK(e, akgl_actor_update(actor),
"updating an actor whose state has no sprite");
TEST_ASSERT(e, changeframe_calls == 0,
"an actor with no sprite for its state still changed frames");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_update(NULL),
"updating a NULL actor");
// An actor with no base character cannot resolve a sprite at all.
actor->basechar = NULL;
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_update(actor),
"updating an actor with no base character");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_character_sprite_binding(void)
{
PREPARE_ERROR(e);
akgl_Actor *actor = NULL;
akgl_Character *basechar = NULL;
akgl_Sprite *sprite = NULL;
akgl_Sprite *found = NULL;
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());
CATCH(e, make_bound_actor(&actor, &basechar, &sprite, "bound", AKGL_ACTOR_STATE_ALIVE));
TEST_EXPECT_OK(e, akgl_character_sprite_get(basechar, AKGL_ACTOR_STATE_ALIVE, &found),
"reading back a bound sprite");
TEST_ASSERT(e, found == sprite, "the bound sprite came back as a different object");
// A composite state is a distinct key from either of its components.
TEST_EXPECT_STATUS(e, AKERR_KEY,
akgl_character_sprite_get(basechar,
(AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP),
&found),
"reading a composite state that was never bound");
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_character_sprite_get(basechar, 0, &found),
"reading state zero, which was never bound");
// Binding a composite state makes it resolvable.
TEST_EXPECT_OK(e, akgl_character_sprite_add(basechar, sprite,
(AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP)),
"binding a sprite to a composite state");
TEST_EXPECT_OK(e, akgl_character_sprite_get(basechar,
(AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP),
&found),
"reading back the composite binding");
TEST_ASSERT(e, found == sprite, "the composite binding resolved to a different sprite");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_character_sprite_get(NULL, 0, &found),
"sprite_get with a NULL character");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
akgl_character_sprite_get(basechar, AKGL_ACTOR_STATE_ALIVE, NULL),
"sprite_get with a NULL destination");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_character_sprite_add(NULL, sprite, 0),
"sprite_add with a NULL character");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_character_sprite_add(basechar, NULL, 0),
"sprite_add with a NULL sprite");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_sprite_sheet_coords(void)
{
PREPARE_ERROR(e);
akgl_Sprite sprite;
SDL_FRect coords;
ATTEMPT {
memset(&sprite, 0x00, sizeof(akgl_Sprite));
sprite.width = 32;
sprite.height = 32;
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_sprite_sheet_coords_for_frame(NULL, &coords, 0),
"sheet coords with a NULL sprite");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_sprite_sheet_coords_for_frame(&sprite, NULL, 0),
"sheet coords with a NULL rectangle");
// The sprite has no sheet attached, so there is no texture to measure against.
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_sprite_sheet_coords_for_frame(&sprite, &coords, 0),
"sheet coords with a NULL spritesheet");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
int main(void)
{
akgl_actor_updated = 0;
akgl_actor_rendered = 0;
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_EXIT;
PREPARE_ERROR(errctx);
Add physics, heap, json_helpers, game, and actor test suites Raise line coverage from 39.6 to 61.8 percent with four new suites and an extension to the actor suite, and register every suite through a single CMake list so a new test file cannot be left out of the coverage fixture. Give the test targets a build-tree RPATH and prepend the build tree to LD_LIBRARY_PATH for CTest, so a developer with a previously installed libakgl.so exercises the library that was just compiled. Fix six defects the new tests exposed: - akgl_physics_simulate read self->gravity_time before its NULL check, so a NULL backend crashed instead of reporting AKERR_NULLPOINTER. - akgl_game_save transposed CLEANUP and PROCESS, which placed the fclose inside the PROCESS switch. An ordinary save never flushed or closed its stream and produced an empty file. - akgl_game_save_actors wrote each name table terminator from the address of a single char, emitting stack contents into the save file and a sentinel the loader could not recognize. - akgl_game_load_objectnamemap used CATCH directly inside while(1), where the break leaves the loop rather than propagating, so a truncated name table loaded as a successful game. - akgl_Actor_cmhf_up_on and _down_on dereferenced actor->basechar with no NULL check, unlike their left and right counterparts. - akgl_actor_logic_movement checked actor twice instead of checking actor->basechar before dereferencing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:03:21 -04:00
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
ATTEMPT {
CATCH(errctx, akgl_registry_init_actor());
CATCH(errctx, akgl_registry_init_sprite());
CATCH(errctx, akgl_registry_init_spritesheet());
CATCH(errctx, akgl_registry_init_character());
CATCH(errctx, test_registry_actor_iterator_nullpointers());
CATCH(errctx, test_registry_actor_iterator_missingactor());
CATCH(errctx, test_registry_actor_iterator_updaterender());
CATCH(errctx, test_akgl_actor_set_character());
CATCH(errctx, test_actor_manage_children());
Add physics, heap, json_helpers, game, and actor test suites Raise line coverage from 39.6 to 61.8 percent with four new suites and an extension to the actor suite, and register every suite through a single CMake list so a new test file cannot be left out of the coverage fixture. Give the test targets a build-tree RPATH and prepend the build tree to LD_LIBRARY_PATH for CTest, so a developer with a previously installed libakgl.so exercises the library that was just compiled. Fix six defects the new tests exposed: - akgl_physics_simulate read self->gravity_time before its NULL check, so a NULL backend crashed instead of reporting AKERR_NULLPOINTER. - akgl_game_save transposed CLEANUP and PROCESS, which placed the fclose inside the PROCESS switch. An ordinary save never flushed or closed its stream and produced an empty file. - akgl_game_save_actors wrote each name table terminator from the address of a single char, emitting stack contents into the save file and a sentinel the loader could not recognize. - akgl_game_load_objectnamemap used CATCH directly inside while(1), where the break leaves the loop rather than propagating, so a truncated name table loaded as a successful game. - akgl_Actor_cmhf_up_on and _down_on dereferenced actor->basechar with no NULL check, unlike their left and right counterparts. - akgl_actor_logic_movement checked actor twice instead of checking actor->basechar before dereferencing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:03:21 -04:00
CATCH(errctx, test_actor_control_handlers_on());
CATCH(errctx, test_actor_control_handlers_off());
CATCH(errctx, test_actor_control_handlers_nullpointers());
CATCH(errctx, test_actor_automatic_face());
CATCH(errctx, test_actor_logic_movement());
CATCH(errctx, test_actor_logic_changeframe());
CATCH(errctx, test_actor_update());
CATCH(errctx, test_actor_character_sprite_binding());
CATCH(errctx, test_actor_sprite_sheet_coords());
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
return 0;
}