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>
This commit is contained in:
598
tests/actor.c
598
tests/actor.c
@@ -13,8 +13,13 @@
|
||||
#include <akgl/iterator.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/character.h>
|
||||
#include <akgl/sprite.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/heap.h>
|
||||
|
||||
#include "testutil.h"
|
||||
|
||||
int UNHANDLED_ERROR_BEHAVIOR;
|
||||
akerr_ErrorContext *unhandled_error_context;
|
||||
|
||||
@@ -308,12 +313,595 @@ _test_actor_addchild_heaprelease_cleanup:
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
|
||||
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());
|
||||
@@ -325,6 +913,16 @@ int main(void)
|
||||
CATCH(errctx, test_registry_actor_iterator_updaterender());
|
||||
CATCH(errctx, test_akgl_actor_set_character());
|
||||
CATCH(errctx, test_actor_manage_children());
|
||||
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user