CI has never gone green in the retained run history, and every failure is the same 20-second death during checkout: six submodules -- SDL, SDL_image, SDL_mixer, SDL_ttf, jansson, semver -- were registered with git@github.com: SSH URLs, and the runner has no GitHub key. The https submodules (libccd, tg, clay, and the two starfort ones) clone fine in the same run. All six now use https, which is what every stanza added since already did; these are public upstream repositories nothing here pushes to, so SSH bought nothing. Second problem, waiting right behind the first: the character, sprite and tilemap suites were the only three of seventeen that never set the dummy video/audio/software-render hints, so on a headless runner they die in SDL_Init with "No available video device" before testing anything. They now set the same three hints the other fourteen suites set. (How this survived: every local run happens on a machine with a display, and CI never got far enough to run a test.) Pre-flighted locally against every job the workflow defines, headless with no driver variables in the environment, exactly as the runner would: Debug with -Werror and coverage builds clean and all 37 tests pass including the coverage fixtures; RelWithDebInfo with -Werror builds clean and the perf suites pass their budgets at full scale; doxygen Doxyfile exits 0 under FAIL_ON_WARNINGS; and the new ui suite runs under valgrind with zero errors and no leaks. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
275 lines
10 KiB
C
275 lines
10 KiB
C
#include <SDL3/SDL.h>
|
|
#include <SDL3_image/SDL_image.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <akerror.h>
|
|
|
|
#include <akgl/registry.h>
|
|
#include <akgl/sprite.h>
|
|
#include <akgl/heap.h>
|
|
#include <akgl/util.h>
|
|
#include <akgl/error.h>
|
|
#include <akgl/game.h>
|
|
#include <akgl/renderer.h>
|
|
|
|
#include "testutil.h"
|
|
|
|
|
|
akerr_ErrorContext *test_akgl_spritesheet_initialize(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akgl_SpriteSheet *sheet = NULL;
|
|
SDL_Texture *image = NULL;
|
|
akgl_String *tmpstr = NULL;
|
|
// Does the image file get loaded?
|
|
// Is the image file loaded correctly? (Surface comparison)
|
|
// Is the spritesheet in the registry?
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_heap_next_spritesheet(&sheet));
|
|
CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
|
|
|
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
|
|
CATCH(errctx, akgl_spritesheet_initialize(sheet, 48, 48, "assets/spritesheet.png"));
|
|
FAIL_ZERO_BREAK(errctx, sheet->texture, AKERR_VALUE, "akgl_spritesheet_initialize failed to load the sprite texture");
|
|
FAIL_NONZERO_BREAK(
|
|
errctx,
|
|
((sheet->texture->w != 576) || (sheet->texture->h != 384)),
|
|
AKERR_VALUE,
|
|
"Loaded texture was not the correct size");
|
|
|
|
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
|
|
image = IMG_LoadTexture(akgl_renderer->sdl_renderer, (char *)&tmpstr->data);
|
|
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
|
|
|
|
CATCH(
|
|
errctx,
|
|
akgl_render_and_compare(
|
|
sheet->texture,
|
|
image,
|
|
0, 0, 576, 384,
|
|
"test_spritesheet_loaded_image.png")
|
|
);
|
|
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
SDL_GetPointerProperty(AKGL_REGISTRY_SPRITESHEET, "assets/spritesheet.png", NULL),
|
|
AKERR_KEY,
|
|
"Spritesheet was not placed in the registry");
|
|
|
|
} CLEANUP {
|
|
IGNORE(akgl_heap_release_string(tmpstr));
|
|
IGNORE(akgl_heap_release_spritesheet(sheet));
|
|
if ( image != NULL )
|
|
SDL_DestroyTexture(image);
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_akgl_sprite_initialize(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akgl_SpriteSheet *testsheet = NULL;
|
|
akgl_Sprite *testsprite = NULL;
|
|
akgl_String *tmpstr = NULL;
|
|
|
|
// Does the sprite get loaded?
|
|
// Do all frames of the sprite get loaded?
|
|
// Are all the frames of the sprite what we expect? (Surface comparison)
|
|
// Is the sprite added to the registry?
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_heap_next_spritesheet(&testsheet));
|
|
CATCH(errctx, akgl_heap_next_sprite(&testsprite));
|
|
CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
|
|
|
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
|
|
CATCH(errctx, akgl_spritesheet_initialize(testsheet, 48, 48, "assets/spritesheet.png"));
|
|
FAIL_ZERO_BREAK(errctx, testsheet, AKERR_VALUE, "akgl_spritesheet_initialize failed");
|
|
|
|
CATCH(errctx, akgl_sprite_initialize(testsprite, "test", testsheet));
|
|
FAIL_NONZERO_BREAK(errctx, (testsprite->sheet != testsheet), AKERR_VALUE, "Initialized sprite uses wrong sheet");
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "test", NULL),
|
|
AKERR_KEY,
|
|
"Sprite was not placed in the registry");
|
|
|
|
} CLEANUP {
|
|
IGNORE(akgl_heap_release_sprite(testsprite));
|
|
IGNORE(akgl_heap_release_string(tmpstr));
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_akgl_sprite_load_json(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akgl_Sprite *testsprite = NULL;
|
|
akgl_Sprite *testsprite2 = NULL;
|
|
akgl_String *tmpstr = NULL;
|
|
SDL_Texture *image = NULL;
|
|
|
|
// Does the sprite get loaded?
|
|
// Do all frames of the sprite get loaded?
|
|
// Are all the frames of the sprite what we expect? (Surface comparison)
|
|
// Is the sprite added to the registry?
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
|
|
|
CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
|
|
testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
testsprite,
|
|
AKERR_KEY,
|
|
"akgl_sprite_load_json succeeds but sprite is not placed in the registry");
|
|
|
|
FAIL_ZERO_BREAK(errctx, (testsprite->width == 48), AKERR_VALUE, "width incorrect (48 : %d)", testsprite->width);
|
|
FAIL_ZERO_BREAK(errctx, (testsprite->height == 48), AKERR_VALUE, "height incorrect (48 : %d)", testsprite->height);
|
|
FAIL_ZERO_BREAK(errctx, (testsprite->speed == 100000000), AKERR_VALUE, "speed incorrect (100 : %d)", testsprite->speed);
|
|
FAIL_ZERO_BREAK(errctx, (testsprite->loop == true), AKERR_VALUE, "loop incorrect (1 : %d)", testsprite->loop);
|
|
FAIL_ZERO_BREAK(errctx, (testsprite->loopReverse == true), AKERR_VALUE, "loopReverse incorrect (1 : %d)", testsprite->loopReverse);
|
|
FAIL_ZERO_BREAK(errctx, (testsprite->frames == 3), AKERR_VALUE, "frame count incorrect (3 : %d)", testsprite->frames);
|
|
FAIL_ZERO_BREAK(errctx, (testsprite->frameids[0] == 12), AKERR_VALUE, "frameids[0] incorrect (12 : %d)", testsprite->frameids[0]);
|
|
FAIL_ZERO_BREAK(errctx, (testsprite->frameids[1] == 13), AKERR_VALUE, "frameids[1] incorrect (13 : %d)", testsprite->frameids[1]);
|
|
FAIL_ZERO_BREAK(errctx, (testsprite->frameids[2] == 14), AKERR_VALUE, "frameids[2] incorrect (14 : %d)", testsprite->frameids[2]);
|
|
FAIL_NONZERO_BREAK(errctx, strcmp(testsprite->name, "testsprite"), AKERR_VALUE, "name incorrect (testsprite : %s)", (char *)testsprite->name);
|
|
|
|
// Is it using the right spritesheet?
|
|
|
|
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
|
|
image = IMG_LoadTexture(akgl_renderer->sdl_renderer, (char *)&tmpstr->data);
|
|
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
|
|
|
|
CATCH(
|
|
errctx,
|
|
akgl_render_and_compare(
|
|
testsprite->sheet->texture,
|
|
image,
|
|
0, 0, 576, 384,
|
|
"test_sprite_loaded_from_json_sheet.png"
|
|
)
|
|
);
|
|
|
|
// If we load a second sprite using the same sheet name, do they use the same sheet in memory?
|
|
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testsprite2.json");
|
|
CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
|
|
testsprite2 = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
testsprite,
|
|
AKERR_KEY,
|
|
"akgl_sprite_load_json succeeds but second sprite is not placed in the registry");
|
|
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
(testsprite->sheet == testsprite2->sheet),
|
|
AKERR_VALUE,
|
|
"Previously loaded spritesheets are not reused");
|
|
|
|
} CLEANUP {
|
|
if ( testsprite != NULL ) {
|
|
IGNORE(akgl_heap_release_sprite(testsprite));
|
|
}
|
|
if ( testsprite2 != NULL ) {
|
|
IGNORE(akgl_heap_release_sprite(testsprite2));
|
|
}
|
|
IGNORE(akgl_heap_release_string(tmpstr));
|
|
if ( image != NULL )
|
|
SDL_DestroyTexture(image);
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief A sprite definition must not be able to write past `frameids`.
|
|
*
|
|
* `frameids` is AKGL_SPRITE_MAX_FRAMES bytes and the loader took its count
|
|
* straight from the document, so a definition with seventeen frames wrote past
|
|
* the array, past the rest of akgl_Sprite, and into the next pool slot. It also
|
|
* wrote through a `uint32_t *` cast of a `uint8_t *`, four bytes at a time,
|
|
* which is why the overrun reached four bytes past the array rather than one.
|
|
*
|
|
* The neighbouring slot is claimed and stamped first, so the test fails on the
|
|
* corruption rather than on whatever the corruption happens to do later.
|
|
*/
|
|
akerr_ErrorContext *test_akgl_sprite_load_json_bounds_frames(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akgl_Sprite *loaded = NULL;
|
|
int i = 0;
|
|
|
|
ATTEMPT {
|
|
// Exactly the maximum is legal and must still load.
|
|
TEST_EXPECT_OK(errctx, akgl_sprite_load_json("assets/testsprite_maxframes.json"),
|
|
"loading a sprite with exactly AKGL_SPRITE_MAX_FRAMES frames");
|
|
loaded = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite_maxframes", NULL);
|
|
FAIL_ZERO_BREAK(errctx, loaded, AKERR_KEY, "the max-frames sprite is not in the registry");
|
|
TEST_ASSERT(errctx, loaded->frames == AKGL_SPRITE_MAX_FRAMES,
|
|
"max-frames sprite loaded %d frames, expected %d",
|
|
loaded->frames, AKGL_SPRITE_MAX_FRAMES);
|
|
for ( i = 0; i < AKGL_SPRITE_MAX_FRAMES; i++ ) {
|
|
TEST_ASSERT(errctx, loaded->frameids[i] == (uint8_t)i,
|
|
"max-frames sprite frame %d is %d, expected %d",
|
|
i, loaded->frameids[i], i);
|
|
}
|
|
|
|
// One more than the maximum is refused, and nothing is registered.
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
|
akgl_sprite_load_json("assets/testsprite_toomanyframes.json"),
|
|
"loading a sprite with more frames than the array holds");
|
|
TEST_ASSERT(errctx,
|
|
SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite_toomanyframes", NULL) == NULL,
|
|
"a sprite with too many frames was registered anyway");
|
|
|
|
// A frame number a uint8_t cannot hold is refused rather than truncated
|
|
// to something that indexes a different tile.
|
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
|
akgl_sprite_load_json("assets/testsprite_widecount.json"),
|
|
"loading a sprite whose frame number does not fit a uint8_t");
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
|
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
|
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_error_init());
|
|
akgl_renderer = &akgl_default_renderer;
|
|
|
|
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
|
|
|
|
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
|
|
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
|
|
}
|
|
|
|
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 640, 480, 0, &akgl_window, &akgl_renderer->sdl_renderer)) {
|
|
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
|
|
}
|
|
akgl_renderer->draw_texture = &akgl_render_2d_draw_texture;
|
|
|
|
CATCH(errctx, akgl_heap_init());
|
|
CATCH(errctx, akgl_registry_init_sprite());
|
|
CATCH(errctx, akgl_registry_init_spritesheet());
|
|
|
|
CATCH(errctx, test_akgl_spritesheet_initialize());
|
|
CATCH(errctx, test_akgl_sprite_initialize());
|
|
CATCH(errctx, test_akgl_sprite_load_json());
|
|
CATCH(errctx, test_akgl_sprite_load_json_bounds_frames());
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH_NORETURN(errctx);
|
|
return 0;
|
|
}
|