2025-08-03 10:07:35 -04:00
|
|
|
#define UNHANDLED_ERROR_TERMINATION_BEHAVIOR \
|
|
|
|
|
handle_unhandled_error(errctx);
|
|
|
|
|
|
2026-01-05 08:58:06 -05:00
|
|
|
#include <akerror.h>
|
2026-07-29 18:01:05 -04:00
|
|
|
#include <akgl/error.h>
|
2025-08-03 10:07:35 -04:00
|
|
|
|
|
|
|
|
#define UNHANDLED_ERROR_EXIT 0
|
|
|
|
|
#define UNHANDLED_ERROR_SET 1
|
|
|
|
|
|
|
|
|
|
#include <SDL3/SDL.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
|
2026-05-07 22:20:10 -04:00
|
|
|
#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>
|
2026-05-07 22:20:10 -04:00
|
|
|
#include <akgl/heap.h>
|
2025-08-03 10:41:09 -04:00
|
|
|
|
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"
|
|
|
|
|
|
2025-08-03 10:07:35 -04:00
|
|
|
int UNHANDLED_ERROR_BEHAVIOR;
|
2026-05-03 23:57:55 -04:00
|
|
|
akerr_ErrorContext *unhandled_error_context;
|
2025-08-03 10:07:35 -04:00
|
|
|
|
2026-05-03 23:57:55 -04:00
|
|
|
void handle_unhandled_error_noexit(akerr_ErrorContext *errctx)
|
2025-08-03 10:07:35 -04:00
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 23:18:42 -04:00
|
|
|
int akgl_actor_updated;
|
|
|
|
|
akerr_ErrorContext *akgl_actor_update_noop(akgl_Actor *obj)
|
2025-08-03 10:07:35 -04:00
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
2026-05-06 23:18:42 -04:00
|
|
|
akgl_actor_updated = 1;
|
2025-08-03 10:07:35 -04:00
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Currently the renderer assumes there is a global variable named `renderer`
|
2026-05-06 23:18:42 -04:00
|
|
|
int akgl_actor_rendered;
|
2026-07-29 18:01:05 -04:00
|
|
|
akerr_ErrorContext *akgl_actor_render_noop(akgl_Actor *obj)
|
2025-08-03 10:07:35 -04:00
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
2026-05-06 23:18:42 -04:00
|
|
|
akgl_actor_rendered = 1;
|
2025-08-03 10:07:35 -04:00
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 23:57:55 -04:00
|
|
|
akerr_ErrorContext *test_registry_actor_iterator_nullpointers(void)
|
2025-08-03 10:07:35 -04:00
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
2026-05-03 23:57:55 -04:00
|
|
|
akerr_ErrorUnhandledErrorHandler defaulthandler = akerr_handler_unhandled_error;
|
2025-08-03 10:07:35 -04:00
|
|
|
|
2026-05-03 23:57:55 -04:00
|
|
|
akerr_handler_unhandled_error = handle_unhandled_error_noexit;
|
2025-08-03 10:07:35 -04:00
|
|
|
ATTEMPT {
|
|
|
|
|
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_SET;
|
2026-05-06 23:18:42 -04:00
|
|
|
DETECT(unhandled_error_context, akgl_registry_iterate_actor(NULL, AKGL_REGISTRY_ACTOR, ""));
|
2025-08-03 10:07:35 -04:00
|
|
|
} CLEANUP {
|
|
|
|
|
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_EXIT;
|
|
|
|
|
} PROCESS(unhandled_error_context) {
|
2026-05-03 23:57:55 -04:00
|
|
|
} HANDLE(unhandled_error_context, AKERR_NULLPOINTER) {
|
2025-08-03 10:07:35 -04:00
|
|
|
printf("Handled\n");
|
|
|
|
|
} FINISH(unhandled_error_context, true);
|
2026-05-03 23:57:55 -04:00
|
|
|
akerr_handler_unhandled_error = defaulthandler;
|
2025-08-03 10:07:35 -04:00
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 23:57:55 -04:00
|
|
|
akerr_ErrorContext *test_registry_actor_iterator_missingactor(void)
|
2025-08-03 10:07:35 -04:00
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
2026-05-03 23:57:55 -04:00
|
|
|
akerr_ErrorUnhandledErrorHandler defaulthandler = akerr_handler_unhandled_error;
|
2025-08-03 10:07:35 -04:00
|
|
|
|
2026-05-06 23:18:42 -04:00
|
|
|
akgl_Iterator iter = {
|
2025-08-03 10:07:35 -04:00
|
|
|
.layerid = 0,
|
|
|
|
|
.flags = 0
|
|
|
|
|
};
|
2026-05-03 23:57:55 -04:00
|
|
|
akerr_handler_unhandled_error = handle_unhandled_error_noexit;
|
2025-08-03 10:07:35 -04:00
|
|
|
ATTEMPT {
|
|
|
|
|
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_SET;
|
|
|
|
|
DETECT(
|
|
|
|
|
unhandled_error_context,
|
2026-05-06 23:18:42 -04:00
|
|
|
akgl_registry_iterate_actor(
|
2025-08-03 10:07:35 -04:00
|
|
|
&iter,
|
2026-05-06 23:18:42 -04:00
|
|
|
AKGL_REGISTRY_ACTOR,
|
2025-08-03 10:07:35 -04:00
|
|
|
"Actor who doesn't exist")
|
|
|
|
|
);
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_EXIT;
|
|
|
|
|
} PROCESS(unhandled_error_context) {
|
2026-05-03 23:57:55 -04:00
|
|
|
} HANDLE(unhandled_error_context, AKERR_KEY) {
|
2025-08-03 10:07:35 -04:00
|
|
|
printf("Handled\n");
|
|
|
|
|
} FINISH(unhandled_error_context, true);
|
2026-05-03 23:57:55 -04:00
|
|
|
akerr_handler_unhandled_error = defaulthandler;
|
2025-08-03 10:07:35 -04:00
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 23:57:55 -04:00
|
|
|
akerr_ErrorContext *test_registry_actor_iterator_updaterender(void)
|
2025-08-03 10:07:35 -04:00
|
|
|
{
|
2026-05-06 23:18:42 -04:00
|
|
|
akgl_Actor *testactor;
|
|
|
|
|
akgl_Iterator iter = {
|
2025-08-03 10:07:35 -04:00
|
|
|
.layerid = 0,
|
2026-05-06 23:18:42 -04:00
|
|
|
.flags = AKGL_ITERATOR_OP_UPDATE | AKGL_ITERATOR_OP_RENDER
|
2025-08-03 10:07:35 -04:00
|
|
|
};
|
2026-05-03 23:57:55 -04:00
|
|
|
akerr_ErrorUnhandledErrorHandler defaulthandler = akerr_handler_unhandled_error;
|
2025-08-03 10:07:35 -04:00
|
|
|
|
|
|
|
|
PREPARE_ERROR(errctx);
|
2026-05-03 23:57:55 -04:00
|
|
|
akerr_handler_unhandled_error = handle_unhandled_error_noexit;
|
2025-08-03 10:07:35 -04:00
|
|
|
ATTEMPT {
|
|
|
|
|
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_SET;
|
2026-05-06 23:18:42 -04:00
|
|
|
CATCH(unhandled_error_context, akgl_heap_next_actor(&testactor));
|
|
|
|
|
CATCH(unhandled_error_context, akgl_actor_initialize(testactor, "test"));
|
2025-08-03 10:07:35 -04:00
|
|
|
|
|
|
|
|
testactor->layer = 0;
|
2026-05-06 23:18:42 -04:00
|
|
|
testactor->updatefunc = &akgl_actor_update_noop;
|
|
|
|
|
testactor->renderfunc = &akgl_actor_render_noop;
|
2025-08-03 10:07:35 -04:00
|
|
|
|
|
|
|
|
DETECT(
|
|
|
|
|
unhandled_error_context,
|
2026-05-06 23:18:42 -04:00
|
|
|
akgl_registry_iterate_actor(
|
2025-08-03 10:07:35 -04:00
|
|
|
&iter,
|
2026-05-06 23:18:42 -04:00
|
|
|
AKGL_REGISTRY_ACTOR,
|
2025-08-03 10:07:35 -04:00
|
|
|
"test")
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_BREAK(
|
|
|
|
|
unhandled_error_context,
|
2026-05-06 23:18:42 -04:00
|
|
|
akgl_actor_updated,
|
2026-07-29 18:01:05 -04:00
|
|
|
AKGL_ERR_BEHAVIOR,
|
2025-08-03 10:07:35 -04:00
|
|
|
"actor->updatefunc not called by the iterator"
|
|
|
|
|
);
|
|
|
|
|
FAIL_ZERO_BREAK(
|
|
|
|
|
unhandled_error_context,
|
2026-05-06 23:18:42 -04:00
|
|
|
akgl_actor_rendered,
|
2026-07-29 18:01:05 -04:00
|
|
|
AKGL_ERR_BEHAVIOR,
|
2025-08-03 10:07:35 -04:00
|
|
|
"actor->renderfunc not called by the iterator"
|
|
|
|
|
);
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_EXIT;
|
2026-05-06 23:18:42 -04:00
|
|
|
IGNORE(akgl_heap_release_actor(testactor));
|
2025-08-03 10:07:35 -04:00
|
|
|
} PROCESS(unhandled_error_context) {
|
|
|
|
|
} FINISH(unhandled_error_context, true);
|
|
|
|
|
|
2026-05-03 23:57:55 -04:00
|
|
|
akerr_handler_unhandled_error = defaulthandler;
|
2025-08-03 10:07:35 -04:00
|
|
|
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 23:18:42 -04:00
|
|
|
akerr_ErrorContext *test_akgl_actor_set_character(void)
|
2025-08-03 10:07:35 -04:00
|
|
|
{
|
2026-05-06 23:18:42 -04:00
|
|
|
akgl_Actor *testactor = NULL;
|
|
|
|
|
akgl_Character *testchar = NULL;
|
2025-08-03 10:07:35 -04:00
|
|
|
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
ATTEMPT {
|
2026-05-06 23:18:42 -04:00
|
|
|
CATCH(errctx, akgl_actor_set_character(NULL, "test"));
|
2025-08-03 10:07:35 -04:00
|
|
|
} CLEANUP {
|
|
|
|
|
} PROCESS(errctx) {
|
2026-05-03 23:57:55 -04:00
|
|
|
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
2025-08-03 10:07:35 -04:00
|
|
|
printf("Handled\n");
|
|
|
|
|
} FINISH(errctx, true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ATTEMPT {
|
2026-05-06 23:18:42 -04:00
|
|
|
CATCH(errctx, akgl_heap_next_actor(&testactor));
|
2025-08-03 10:07:35 -04:00
|
|
|
|
2026-05-06 23:18:42 -04:00
|
|
|
CATCH(errctx, akgl_actor_initialize(testactor, "test"));
|
2025-08-03 10:07:35 -04:00
|
|
|
testactor->layer = 0;
|
2026-05-06 23:18:42 -04:00
|
|
|
testactor->updatefunc = &akgl_actor_update_noop;
|
|
|
|
|
testactor->renderfunc = &akgl_actor_render_noop;
|
2025-08-03 10:07:35 -04:00
|
|
|
|
2026-05-06 23:18:42 -04:00
|
|
|
CATCH(errctx, akgl_actor_set_character(testactor, "test"));
|
2025-08-03 10:07:35 -04:00
|
|
|
} CLEANUP {
|
2026-05-06 23:18:42 -04:00
|
|
|
IGNORE(akgl_heap_release_actor(testactor));
|
2025-08-03 10:07:35 -04:00
|
|
|
} PROCESS(errctx) {
|
2026-05-03 23:57:55 -04:00
|
|
|
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
2025-08-03 10:07:35 -04:00
|
|
|
printf("Handled\n");
|
|
|
|
|
} FINISH(errctx, true);
|
|
|
|
|
|
|
|
|
|
ATTEMPT {
|
2026-05-06 23:18:42 -04:00
|
|
|
CATCH(errctx, akgl_heap_next_actor(&testactor));
|
|
|
|
|
CATCH(errctx, akgl_heap_next_character(&testchar));
|
2025-08-03 10:07:35 -04:00
|
|
|
|
2026-05-06 23:18:42 -04:00
|
|
|
CATCH(errctx, akgl_actor_initialize(testactor, "test"));
|
2025-08-03 10:07:35 -04:00
|
|
|
testactor->layer = 0;
|
2026-05-06 23:18:42 -04:00
|
|
|
testactor->updatefunc = &akgl_actor_update_noop;
|
|
|
|
|
testactor->renderfunc = &akgl_actor_render_noop;
|
2025-08-03 10:07:35 -04:00
|
|
|
|
2026-05-06 23:18:42 -04:00
|
|
|
CATCH(errctx, akgl_character_initialize(testchar, "test"));
|
2025-08-03 10:07:35 -04:00
|
|
|
|
2026-05-06 23:18:42 -04:00
|
|
|
CATCH(errctx, akgl_actor_set_character(testactor, "test"));
|
2025-08-03 10:07:35 -04:00
|
|
|
} CLEANUP {
|
2026-05-06 23:18:42 -04:00
|
|
|
IGNORE(akgl_heap_release_actor(testactor));
|
|
|
|
|
IGNORE(akgl_heap_release_character(testchar));
|
2025-08-03 10:07:35 -04:00
|
|
|
} PROCESS(errctx) {
|
|
|
|
|
} FINISH(errctx, true);
|
|
|
|
|
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 23:57:55 -04:00
|
|
|
akerr_ErrorContext *test_actor_manage_children(void)
|
2025-08-03 10:07:35 -04:00
|
|
|
{
|
2026-05-06 23:18:42 -04:00
|
|
|
akgl_Actor *parent = NULL;
|
|
|
|
|
akgl_Actor *child = NULL;
|
|
|
|
|
akgl_String *tmpstring = NULL;
|
2025-08-03 10:07:35 -04:00
|
|
|
int i = 0;
|
|
|
|
|
int j = 0;
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
ATTEMPT {
|
2026-05-06 23:18:42 -04:00
|
|
|
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++ ) {
|
2025-08-03 10:07:35 -04:00
|
|
|
sprintf((char *)&tmpstring->data, "child %d", i);
|
2026-05-06 23:18:42 -04:00
|
|
|
CATCH(errctx, akgl_heap_next_actor(&child));
|
|
|
|
|
CATCH(errctx, akgl_actor_initialize(child, (char *)&tmpstring->data));
|
2025-08-03 10:07:35 -04:00
|
|
|
CATCH(errctx, parent->addchild(parent, child));
|
|
|
|
|
// Release our claim on the actor so the parent can own the child and kill it
|
2026-05-06 23:18:42 -04:00
|
|
|
CATCH(errctx, akgl_heap_release_actor(child));
|
2025-08-03 10:07:35 -04:00
|
|
|
}
|
|
|
|
|
} CLEANUP {
|
2026-05-06 23:18:42 -04:00
|
|
|
IGNORE(akgl_heap_release_string(tmpstring));
|
2025-08-03 10:07:35 -04:00
|
|
|
} 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 ) {
|
2026-07-29 18:01:05 -04:00
|
|
|
FAIL(errctx, AKGL_ERR_BEHAVIOR, "addchild does not throw AKERR_RELATIONSHIP when child already has a parent");
|
2025-08-03 10:07:35 -04:00
|
|
|
}
|
|
|
|
|
} PROCESS(errctx) {
|
2026-05-03 23:57:55 -04:00
|
|
|
} HANDLE(errctx, AKERR_RELATIONSHIP) {
|
2025-08-03 10:07:35 -04:00
|
|
|
// Expected behavior
|
2026-05-03 23:57:55 -04:00
|
|
|
SDL_Log("addchild throws AKERR_RELATIONSHIP when child already has a parent");
|
2025-08-03 10:07:35 -04:00
|
|
|
} FINISH(errctx, true);
|
|
|
|
|
|
|
|
|
|
ATTEMPT {
|
2026-05-06 23:18:42 -04:00
|
|
|
CATCH(errctx, akgl_heap_next_actor(&child));
|
2025-08-03 10:07:35 -04:00
|
|
|
CATCH(errctx, parent->addchild(parent, child));
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
if ( errctx == NULL ) {
|
2026-07-29 18:01:05 -04:00
|
|
|
FAIL(errctx, AKGL_ERR_BEHAVIOR, "addchild does not throw AKERR_OUTOFBOUNDS when all children already set");
|
2025-08-03 10:07:35 -04:00
|
|
|
}
|
|
|
|
|
} PROCESS(errctx) {
|
2026-05-03 23:57:55 -04:00
|
|
|
} HANDLE(errctx, AKERR_OUTOFBOUNDS) {
|
2025-08-03 10:07:35 -04:00
|
|
|
// Expected behavior
|
2026-05-03 23:57:55 -04:00
|
|
|
SDL_Log("addchild throws AKERR_OUTOFBOUNDS when all children already set");
|
2025-08-03 10:07:35 -04:00
|
|
|
} FINISH(errctx, true);
|
|
|
|
|
|
|
|
|
|
ATTEMPT {
|
2026-05-06 23:18:42 -04:00
|
|
|
CATCH(errctx, akgl_heap_release_actor(parent));
|
2025-08-03 10:07:35 -04:00
|
|
|
// All actor objects on the heap should be empty now
|
2026-05-06 23:18:42 -04:00
|
|
|
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++) {
|
2026-05-03 23:57:55 -04:00
|
|
|
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");
|
2026-05-06 23:18:42 -04:00
|
|
|
for ( j = 0 ; j < AKGL_ACTOR_MAX_CHILDREN; j++) {
|
2025-08-03 10:07:35 -04:00
|
|
|
if ( HEAP_ACTOR[i].children[j] != NULL ) {
|
2026-05-03 23:57:55 -04:00
|
|
|
FAIL(errctx, AKERR_VALUE, "Actor not properly cleared");
|
2025-08-03 10:07:35 -04:00
|
|
|
goto _test_actor_addchild_heaprelease_cleanup;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-06 23:18:42 -04:00
|
|
|
for ( j = 0; j < AKGL_ACTOR_MAX_CHILDREN; j++) {
|
2025-08-03 10:07:35 -04:00
|
|
|
sprintf((char *)&tmpstring->data, "child %d", i);
|
|
|
|
|
FAIL_NONZERO_BREAK(
|
|
|
|
|
errctx,
|
2026-05-06 23:18:42 -04:00
|
|
|
SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, (char *)&tmpstring->data, NULL),
|
2026-05-03 23:57:55 -04:00
|
|
|
AKERR_KEY,
|
2025-08-03 10:07:35 -04:00
|
|
|
"Child %s was not removed from the registry",
|
|
|
|
|
(char *)&tmpstring->data);
|
|
|
|
|
}
|
|
|
|
|
_test_actor_addchild_heaprelease_cleanup:
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
} PROCESS(errctx) {
|
|
|
|
|
} FINISH(errctx, true);
|
|
|
|
|
|
|
|
|
|
ATTEMPT {
|
2026-05-06 23:18:42 -04:00
|
|
|
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"));
|
2025-08-03 10:07:35 -04:00
|
|
|
// Don't release our claim on the child. The child should not be reclaimed.
|
2026-05-06 23:18:42 -04:00
|
|
|
CATCH(errctx, akgl_heap_release_actor(parent));
|
2026-05-03 23:57:55 -04:00
|
|
|
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");
|
2025-08-03 10:07:35 -04:00
|
|
|
FAIL_ZERO_BREAK(
|
|
|
|
|
errctx,
|
2026-05-06 23:18:42 -04:00
|
|
|
(child == SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, child->name, NULL)),
|
2026-05-03 23:57:55 -04:00
|
|
|
AKERR_KEY,
|
2025-08-03 10:07:35 -04:00
|
|
|
"Child prematurely removed from the registry");
|
|
|
|
|
// Now we can release the child
|
2026-05-06 23:18:42 -04:00
|
|
|
CATCH(errctx, akgl_heap_release_actor(child));
|
2025-08-03 10:07:35 -04:00
|
|
|
} 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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-03 10:07:35 -04:00
|
|
|
int main(void)
|
|
|
|
|
{
|
2026-05-06 23:18:42 -04:00
|
|
|
akgl_actor_updated = 0;
|
|
|
|
|
akgl_actor_rendered = 0;
|
2025-08-03 10:07:35 -04:00
|
|
|
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");
|
|
|
|
|
|
2025-08-03 10:07:35 -04:00
|
|
|
ATTEMPT {
|
2026-05-06 23:18:42 -04:00
|
|
|
CATCH(errctx, akgl_registry_init_actor());
|
|
|
|
|
CATCH(errctx, akgl_registry_init_sprite());
|
|
|
|
|
CATCH(errctx, akgl_registry_init_spritesheet());
|
|
|
|
|
CATCH(errctx, akgl_registry_init_character());
|
2025-08-03 10:07:35 -04:00
|
|
|
|
|
|
|
|
CATCH(errctx, test_registry_actor_iterator_nullpointers());
|
|
|
|
|
CATCH(errctx, test_registry_actor_iterator_missingactor());
|
|
|
|
|
CATCH(errctx, test_registry_actor_iterator_updaterender());
|
2026-05-06 23:18:42 -04:00
|
|
|
CATCH(errctx, test_akgl_actor_set_character());
|
2025-08-03 10:07:35 -04:00
|
|
|
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());
|
2025-08-03 10:07:35 -04:00
|
|
|
} CLEANUP {
|
|
|
|
|
} PROCESS(errctx) {
|
|
|
|
|
} FINISH_NORETURN(errctx);
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|