Closes internal-consistency items 7 through 15. Nineteen non-static functions were in the ABI with no declaration anywhere, so no consumer could call them and any consumer could collide with them. The four gamepad_handle_* functions are the ones that mattered: controller.h declared akgl_controller_handle_button_down and three siblings that did not exist, so anything compiled against the header alone failed to link. The definitions carry the declared names now, which also closes Defects -> Known and still open item 10, and their documentation moved to the header. The rest are either declared under a "part of the internal API" block -- akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to declare for itself, plus six tilemap loader helpers the untested-loader work wants to reach -- or static, which is what the four save iterators and load_objectnamemap should always have been. akgl_path_relative_from is deleted: declared nowhere, called from nowhere, never wrote its output, and leaked a pooled string on every call, so it closes Known and still open item 4 and item 40 by ceasing to exist. scripts/check_api_surface.sh keeps it closed. It reads the built library's dynamic symbol table and every public header with comments stripped, and fails on an exported akgl_* symbol that is declared nowhere. Stripping comments is the whole point -- four of these were mentioned in controller.h prose, which is how they went unnoticed. The pool-size ceilings are defined once, in heap.h, so the #ifndef override hook fires for the first time; actor.h, sprite.h and character.h were defining the same four unconditionally from headers heap.h includes above its own guard. tests/header_pool_override.c fails the compile if that regresses. Also here: (void) rather than () on the twelve no-argument entry points, AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix dropped, and the six parameter-name mismatches. akgl_get_json_with_default had its two contexts swapped rather than merely misspelled -- the incoming one was `err` and its own was `e`, which is the name reserved for an incoming one. 24/24 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
501 lines
17 KiB
C
501 lines
17 KiB
C
/**
|
|
* @file game.c
|
|
* @brief Unit tests for savegame serialization, version gating, and frame accounting.
|
|
*
|
|
* akgl_game_init() and akgl_game_update() need a window and a live frame loop,
|
|
* so they are out of scope here. Everything else in the game module is either
|
|
* pure logic or file IO and is covered below.
|
|
*/
|
|
|
|
#include <SDL3/SDL.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <akerror.h>
|
|
|
|
#include <akgl/error.h>
|
|
#include <akgl/game.h>
|
|
#include <akgl/actor.h>
|
|
#include <akgl/character.h>
|
|
#include <akgl/heap.h>
|
|
#include <akgl/registry.h>
|
|
#include <akgl/sprite.h>
|
|
#include <akgl/staticstring.h>
|
|
|
|
#include "testutil.h"
|
|
|
|
/** @brief Scratch savegame path, created and removed by the tests that use it. */
|
|
static char savepath[] = "akgl_test_savegame.bin";
|
|
/** @brief Scratch path for deliberately malformed savegames. */
|
|
static char truncatedpath[] = "akgl_test_truncated.bin";
|
|
|
|
/** @brief Populate the process-wide game record with a valid identity. */
|
|
static void set_game_identity(void)
|
|
{
|
|
memset(&akgl_game, 0x00, sizeof(akgl_Game));
|
|
strncpy((char *)&akgl_game.libversion, AKGL_VERSION, 31);
|
|
strncpy((char *)&akgl_game.version, "1.2.3", 31);
|
|
strncpy((char *)&akgl_game.name, "libakgl test game", 255);
|
|
strncpy((char *)&akgl_game.uri, "https://example.invalid/akgl-test", 255);
|
|
}
|
|
|
|
akerr_ErrorContext *test_game_load_versioncmp_matching(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
|
|
ATTEMPT {
|
|
TEST_EXPECT_OK(e, akgl_game_load_versioncmp("library", "1.2.3", "1.2.3"),
|
|
"identical versions must be compatible");
|
|
TEST_EXPECT_OK(e, akgl_game_load_versioncmp("library", "0.1.0", "0.1.0"),
|
|
"identical zero-major versions must be compatible");
|
|
TEST_EXPECT_OK(e, akgl_game_load_versioncmp("game", "10.20.30", "10.20.30"),
|
|
"identical multi-digit versions must be compatible");
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_game_load_versioncmp_mismatched(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
|
|
ATTEMPT {
|
|
// A savegame from a different build is refused on any component.
|
|
TEST_EXPECT_STATUS(e, AKERR_API, akgl_game_load_versioncmp("library", "2.2.3", "1.2.3"),
|
|
"a differing major version must be refused");
|
|
TEST_EXPECT_STATUS(e, AKERR_API, akgl_game_load_versioncmp("library", "1.3.3", "1.2.3"),
|
|
"a differing minor version must be refused");
|
|
TEST_EXPECT_STATUS(e, AKERR_API, akgl_game_load_versioncmp("library", "1.2.4", "1.2.3"),
|
|
"a differing patch version must be refused");
|
|
|
|
// Unparseable versions are a value error, distinct from a mismatch.
|
|
TEST_EXPECT_STATUS(e, AKERR_VALUE, akgl_game_load_versioncmp("library", "1.2.3", "not-a-version"),
|
|
"an unparseable current version must be refused");
|
|
TEST_EXPECT_STATUS(e, AKERR_VALUE, akgl_game_load_versioncmp("library", "not-a-version", "1.2.3"),
|
|
"an unparseable savegame version must be refused");
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_game_load_versioncmp(NULL, "1.2.3", "1.2.3"),
|
|
"versioncmp with a NULL version type");
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_game_load_versioncmp("library", NULL, "1.2.3"),
|
|
"versioncmp with a NULL new version");
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_game_load_versioncmp("library", "1.2.3", NULL),
|
|
"versioncmp with a NULL current version");
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_game_load_versioncmp_releases_semver(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
int i = 0;
|
|
bool leaked = false;
|
|
|
|
ATTEMPT {
|
|
// semver_parse allocates; the comparison must free both sides on the
|
|
// success and the failure path or a long session will drift.
|
|
for ( i = 0; i < 2000; i++ ) {
|
|
akerr_ErrorContext *result = akgl_game_load_versioncmp("library", "1.2.3", "1.2.3");
|
|
if ( result != NULL ) {
|
|
result->handled = true;
|
|
result = akerr_release_error(result);
|
|
leaked = true;
|
|
}
|
|
result = akgl_game_load_versioncmp("library", "9.9.9", "1.2.3");
|
|
if ( result != NULL ) {
|
|
result->handled = true;
|
|
result = akerr_release_error(result);
|
|
}
|
|
}
|
|
TEST_ASSERT(e, leaked == false,
|
|
"a matching version comparison started failing partway through a long run");
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_game_save_roundtrip(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
akgl_Game expected;
|
|
|
|
ATTEMPT {
|
|
CATCH(e, akgl_registry_init());
|
|
CATCH(e, akgl_heap_init());
|
|
set_game_identity();
|
|
akgl_game.fps = 60;
|
|
akgl_game.framesSinceUpdate = 7;
|
|
memcpy(&expected, &akgl_game, sizeof(akgl_Game));
|
|
|
|
TEST_EXPECT_OK(e, akgl_game_save((char *)&savepath), "saving a game");
|
|
|
|
// Scribble over the live state so a successful load has to restore it.
|
|
akgl_game.fps = 0;
|
|
akgl_game.framesSinceUpdate = 0;
|
|
|
|
TEST_EXPECT_OK(e, akgl_game_load((char *)&savepath), "loading the game back");
|
|
TEST_ASSERT(e, akgl_game.fps == 60, "fps restored as %d, expected 60", akgl_game.fps);
|
|
TEST_ASSERT(e, akgl_game.framesSinceUpdate == 7,
|
|
"framesSinceUpdate restored as %d, expected 7", akgl_game.framesSinceUpdate);
|
|
TEST_ASSERT(e, strncmp((char *)&akgl_game.name, (char *)&expected.name, 256) == 0,
|
|
"the game name was not preserved across a save and load");
|
|
TEST_ASSERT(e, strncmp((char *)&akgl_game.version, (char *)&expected.version, 32) == 0,
|
|
"the game version was not preserved across a save and load");
|
|
} CLEANUP {
|
|
unlink((char *)&savepath);
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_game_load_rejects_foreign_saves(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
|
|
ATTEMPT {
|
|
CATCH(e, akgl_registry_init());
|
|
CATCH(e, akgl_heap_init());
|
|
|
|
// A save written by a different game must not load into this one.
|
|
set_game_identity();
|
|
CATCH(e, akgl_game_save((char *)&savepath));
|
|
strncpy((char *)&akgl_game.name, "a completely different game", 255);
|
|
TEST_EXPECT_STATUS(e, AKERR_API, akgl_game_load((char *)&savepath),
|
|
"a savegame with a foreign game name must be refused");
|
|
unlink((char *)&savepath);
|
|
|
|
// Same for a differing URI.
|
|
set_game_identity();
|
|
CATCH(e, akgl_game_save((char *)&savepath));
|
|
strncpy((char *)&akgl_game.uri, "https://example.invalid/other", 255);
|
|
TEST_EXPECT_STATUS(e, AKERR_API, akgl_game_load((char *)&savepath),
|
|
"a savegame with a foreign URI must be refused");
|
|
unlink((char *)&savepath);
|
|
|
|
// A save written against a different library version must be refused.
|
|
set_game_identity();
|
|
strncpy((char *)&akgl_game.libversion, "99.98.97", 31);
|
|
CATCH(e, akgl_game_save((char *)&savepath));
|
|
set_game_identity();
|
|
TEST_EXPECT_STATUS(e, AKERR_API, akgl_game_load((char *)&savepath),
|
|
"a savegame from a different library version must be refused");
|
|
unlink((char *)&savepath);
|
|
|
|
// And one written against a different game version.
|
|
set_game_identity();
|
|
strncpy((char *)&akgl_game.version, "4.5.6", 31);
|
|
CATCH(e, akgl_game_save((char *)&savepath));
|
|
set_game_identity();
|
|
TEST_EXPECT_STATUS(e, AKERR_API, akgl_game_load((char *)&savepath),
|
|
"a savegame from a different game version must be refused");
|
|
} CLEANUP {
|
|
unlink((char *)&savepath);
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_game_save_load_nullpointers(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
|
|
ATTEMPT {
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_game_save(NULL),
|
|
"akgl_game_save(NULL)");
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_game_load(NULL),
|
|
"akgl_game_load(NULL)");
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_game_save_actors(NULL),
|
|
"akgl_game_save_actors(NULL)");
|
|
|
|
// A path under a directory that does not exist cannot be opened.
|
|
TEST_EXPECT_ANY_ERROR(e, akgl_game_save("no_such_directory/save.bin"),
|
|
"saving into a nonexistent directory");
|
|
TEST_EXPECT_ANY_ERROR(e, akgl_game_load("no_such_file_anywhere.bin"),
|
|
"loading a nonexistent savegame");
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_game_load_truncated_table(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
FILE *fp = NULL;
|
|
char partial[64];
|
|
|
|
ATTEMPT {
|
|
CATCH(e, akgl_registry_init());
|
|
CATCH(e, akgl_heap_init());
|
|
set_game_identity();
|
|
|
|
// A valid header followed by a table that ends before its sentinel. The
|
|
// name-map reader loops until it sees the sentinel, so it has to notice
|
|
// EOF instead of spinning.
|
|
memset(&partial, 0x00, sizeof(partial));
|
|
fp = fopen((char *)&truncatedpath, "wb");
|
|
FAIL_ZERO_BREAK(e, fp, AKERR_IO, "unable to create the truncated savegame fixture");
|
|
FAIL_ZERO_BREAK(e, fwrite(&akgl_game, 1, sizeof(akgl_Game), fp), AKERR_IO,
|
|
"unable to write the truncated savegame header");
|
|
FAIL_ZERO_BREAK(e, fwrite(&partial, 1, sizeof(partial), fp), AKERR_IO,
|
|
"unable to write the truncated savegame body");
|
|
fclose(fp);
|
|
fp = NULL;
|
|
|
|
TEST_EXPECT_ANY_ERROR(e, akgl_game_load((char *)&truncatedpath),
|
|
"loading a savegame whose name table is truncated");
|
|
} CLEANUP {
|
|
if ( fp != NULL ) {
|
|
fclose(fp);
|
|
}
|
|
unlink((char *)&truncatedpath);
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_game_save_writes_name_tables(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
akgl_Actor *actor = NULL;
|
|
FILE *fp = NULL;
|
|
long filesize = 0;
|
|
long minimum = 0;
|
|
|
|
ATTEMPT {
|
|
CATCH(e, akgl_registry_init());
|
|
CATCH(e, akgl_heap_init());
|
|
set_game_identity();
|
|
|
|
// One registered actor, so the actor table has a real entry ahead of its
|
|
// terminating sentinel.
|
|
CATCH(e, akgl_heap_next_actor(&actor));
|
|
CATCH(e, akgl_actor_initialize(actor, "saved_actor"));
|
|
|
|
TEST_EXPECT_OK(e, akgl_game_save((char *)&savepath), "saving a game with one actor");
|
|
|
|
fp = fopen((char *)&savepath, "rb");
|
|
FAIL_ZERO_BREAK(e, fp, AKERR_IO, "unable to reopen the savegame");
|
|
fseek(fp, 0, SEEK_END);
|
|
filesize = ftell(fp);
|
|
|
|
// The header, then four name tables each ending in a name-sized and a
|
|
// pointer-sized sentinel, plus the one real actor entry.
|
|
minimum = (long)sizeof(akgl_Game)
|
|
+ (long)(AKGL_ACTOR_MAX_NAME_LENGTH + sizeof(akgl_Actor *)) * 2
|
|
+ (long)(AKGL_SPRITE_MAX_NAME_LENGTH + sizeof(akgl_Sprite *))
|
|
+ (long)(AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH + sizeof(akgl_SpriteSheet *))
|
|
+ (long)(AKGL_CHARACTER_MAX_NAME_LENGTH + sizeof(akgl_Character *));
|
|
|
|
TEST_ASSERT(e, filesize >= minimum,
|
|
"the savegame is %ld bytes, expected at least %ld for the header and four name tables",
|
|
filesize, minimum);
|
|
} CLEANUP {
|
|
if ( fp != NULL ) {
|
|
fclose(fp);
|
|
}
|
|
unlink((char *)&savepath);
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_game_state_lock(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
|
|
ATTEMPT {
|
|
set_game_identity();
|
|
akgl_game.statelock = SDL_CreateMutex();
|
|
FAIL_ZERO_BREAK(e, akgl_game.statelock, AKGL_ERR_SDL, "unable to create the state mutex");
|
|
|
|
TEST_EXPECT_OK(e, akgl_game_state_lock(), "taking the state lock");
|
|
TEST_EXPECT_OK(e, akgl_game_state_unlock(), "releasing the state lock");
|
|
|
|
// The lock is reusable after a matched unlock.
|
|
TEST_EXPECT_OK(e, akgl_game_state_lock(), "retaking the state lock");
|
|
TEST_EXPECT_OK(e, akgl_game_state_unlock(), "releasing the state lock again");
|
|
} CLEANUP {
|
|
if ( akgl_game.statelock != NULL ) {
|
|
SDL_DestroyMutex(akgl_game.statelock);
|
|
akgl_game.statelock = NULL;
|
|
}
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
/** @brief Cleared while the helper thread should keep holding the state mutex. */
|
|
static SDL_AtomicInt lockholder_release;
|
|
|
|
/** @brief Takes the state mutex and sits on it until lockholder_release is set. */
|
|
static int SDLCALL lockholder_thread(void *userdata)
|
|
{
|
|
SDL_Mutex *statelock = (SDL_Mutex *)userdata;
|
|
|
|
SDL_LockMutex(statelock);
|
|
while ( SDL_GetAtomicInt(&lockholder_release) == 0 ) {
|
|
SDL_Delay(10);
|
|
}
|
|
SDL_UnlockMutex(statelock);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief akgl_game_state_lock must give up on a contended mutex in about a second.
|
|
*
|
|
* The uncontended path above never reaches the retry loop, which is where the
|
|
* budget lives and where the defect was: the loop counted against a constant
|
|
* named "one second in milliseconds" that held 1000000, so it retried 10,000
|
|
* times at 100 ms and blocked for roughly sixteen minutes before reporting
|
|
* failure. The upper bound below is the assertion that matters. The lower bound
|
|
* is there so a build that gave up immediately -- reporting failure without
|
|
* waiting at all -- cannot pass either.
|
|
*/
|
|
akerr_ErrorContext *test_game_state_lock_budget(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
SDL_Thread *holder = NULL;
|
|
Uint64 started = 0;
|
|
Uint64 elapsed = 0;
|
|
|
|
ATTEMPT {
|
|
set_game_identity();
|
|
akgl_game.statelock = SDL_CreateMutex();
|
|
FAIL_ZERO_BREAK(e, akgl_game.statelock, AKGL_ERR_SDL, "unable to create the state mutex");
|
|
|
|
SDL_SetAtomicInt(&lockholder_release, 0);
|
|
holder = SDL_CreateThread(lockholder_thread, "akgl_test_lockholder", (void *)akgl_game.statelock);
|
|
FAIL_ZERO_BREAK(e, holder, AKGL_ERR_SDL, "unable to start the lock-holding thread");
|
|
|
|
// Wait until the helper actually owns the mutex. Without this the
|
|
// measurement races the thread start and the lock is taken on the first
|
|
// try, which measures nothing.
|
|
while ( SDL_TryLockMutex(akgl_game.statelock) == true ) {
|
|
SDL_UnlockMutex(akgl_game.statelock);
|
|
SDL_Delay(1);
|
|
}
|
|
|
|
started = SDL_GetTicksNS();
|
|
TEST_EXPECT_STATUS(
|
|
e,
|
|
AKGL_ERR_SDL,
|
|
akgl_game_state_lock(),
|
|
"taking a state lock another thread is holding");
|
|
elapsed = SDL_GetTicksNS() - started;
|
|
|
|
TEST_ASSERT(
|
|
e,
|
|
elapsed >= (AKGL_TIME_ONESEC_NS / 2),
|
|
"state lock gave up after %" SDL_PRIu64 " ns without waiting out its budget",
|
|
elapsed);
|
|
TEST_ASSERT(
|
|
e,
|
|
elapsed < (5 * (Uint64)AKGL_TIME_ONESEC_NS),
|
|
"state lock waited %" SDL_PRIu64 " ns on a %d ms budget",
|
|
elapsed,
|
|
AKGL_GAME_STATE_LOCK_BUDGET_MS);
|
|
} CLEANUP {
|
|
SDL_SetAtomicInt(&lockholder_release, 1);
|
|
if ( holder != NULL ) {
|
|
SDL_WaitThread(holder, NULL);
|
|
}
|
|
if ( akgl_game.statelock != NULL ) {
|
|
SDL_DestroyMutex(akgl_game.statelock);
|
|
akgl_game.statelock = NULL;
|
|
}
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
/** @brief Counts calls made to the low-FPS callback. */
|
|
static int lowfps_calls = 0;
|
|
|
|
/** @brief Low-FPS callback stub that only records that it fired. */
|
|
static void stub_lowfps(void)
|
|
{
|
|
lowfps_calls += 1;
|
|
}
|
|
|
|
akerr_ErrorContext *test_game_updateFPS(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
int16_t framesbefore = 0;
|
|
|
|
ATTEMPT {
|
|
set_game_identity();
|
|
akgl_game.lowfpsfunc = &stub_lowfps;
|
|
|
|
// Below the 30 FPS floor, every update notifies the callback.
|
|
akgl_game.fps = 10;
|
|
akgl_game.lastFPSTime = SDL_GetTicksNS();
|
|
lowfps_calls = 0;
|
|
framesbefore = akgl_game.framesSinceUpdate;
|
|
akgl_game_update_fps();
|
|
TEST_ASSERT(e, lowfps_calls == 1,
|
|
"a sub-30 FPS update fired the low-FPS callback %d times, expected 1", lowfps_calls);
|
|
TEST_ASSERT(e, akgl_game.framesSinceUpdate == (framesbefore + 1),
|
|
"updateFPS did not count the frame (%d, expected %d)",
|
|
akgl_game.framesSinceUpdate, framesbefore + 1);
|
|
TEST_ASSERT(e, akgl_game.lastIterTime != 0, "updateFPS did not stamp lastIterTime");
|
|
|
|
// At or above the floor, the callback stays quiet.
|
|
akgl_game.fps = 60;
|
|
akgl_game.lastFPSTime = SDL_GetTicksNS();
|
|
lowfps_calls = 0;
|
|
akgl_game_update_fps();
|
|
TEST_ASSERT(e, lowfps_calls == 0,
|
|
"a 60 FPS update fired the low-FPS callback %d times, expected 0", lowfps_calls);
|
|
|
|
// Once a full second has elapsed, the frame counter rolls into fps.
|
|
akgl_game.fps = 60;
|
|
akgl_game.framesSinceUpdate = 45;
|
|
akgl_game.lastFPSTime = SDL_GetTicksNS() - (2 * (SDL_Time)AKGL_TIME_ONESEC_NS);
|
|
akgl_game_update_fps();
|
|
TEST_ASSERT(e, akgl_game.fps == 45,
|
|
"after a second elapsed, fps rolled over as %d, expected 45", akgl_game.fps);
|
|
TEST_ASSERT(e, akgl_game.framesSinceUpdate == 1,
|
|
"the frame counter restarted at %d, expected 1", akgl_game.framesSinceUpdate);
|
|
|
|
// The shipped default callback only logs, so it just has to not crash.
|
|
akgl_game.fps = 1;
|
|
akgl_game_lowfps();
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
|
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_error_init());
|
|
TEST_TRAP_UNHANDLED_ERRORS();
|
|
CATCH(errctx, akgl_heap_init());
|
|
CATCH(errctx, akgl_registry_init());
|
|
|
|
CATCH(errctx, test_game_load_versioncmp_matching());
|
|
CATCH(errctx, test_game_load_versioncmp_mismatched());
|
|
CATCH(errctx, test_game_load_versioncmp_releases_semver());
|
|
CATCH(errctx, test_game_save_roundtrip());
|
|
CATCH(errctx, test_game_load_rejects_foreign_saves());
|
|
CATCH(errctx, test_game_save_load_nullpointers());
|
|
CATCH(errctx, test_game_load_truncated_table());
|
|
CATCH(errctx, test_game_save_writes_name_tables());
|
|
CATCH(errctx, test_game_state_lock());
|
|
CATCH(errctx, test_game_state_lock_budget());
|
|
CATCH(errctx, test_game_updateFPS());
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH_NORETURN(errctx);
|
|
}
|