Every libakgl test suite could report success while failing. libakerror's default unhandled-error handler ended in exit(errctx->status), an exit status is one byte wide, and libakgl's band starts at 256 -- so AKGL_ERR_SDL, the most common failure a library built on SDL can have, exited 0 and CTest recorded a pass. tests/character.c aborted at its second of four tests on a bad renderer and was green for months. 0.5.0 worked around that here with TEST_TRAP_UNHANDLED_ERRORS() in tests/testutil.h, and TODO.md ended the entry saying any consumer's suites have the same problem and it was worth raising upstream. It was. 2.0.1 fixes it at the source: akerr_exit() owns the mapping and the default handler calls it, so 0 exits 0, 1 through 255 exit themselves, and anything else exits AKERR_EXIT_STATUS_UNREPRESENTABLE (125). The trap and its 21 call sites are gone. Verified by putting the original failure back rather than by reading the release notes: a FAIL_BREAK(AKGL_ERR_SDL) in tests/character.c's main exits 125 and CTest reports a failure. A standalone consumer raising the same status unhandled exits 125 where it exited 0 before. tests/actor.c installs its own handler and called exit(errctx->status) from it, which is the same defect one layer up. It calls akerr_exit() now. 2.0.0 also makes the error pool and the status registry thread safe, which libakgl needs more than it knew: audio_stream_callback raises error contexts on SDL's audio thread. With an unlocked pool that callback and the main thread could scan AKERR_ARRAY_ERROR at the same time and be handed the same slot. The comment there says so. This is a hard dependency floor, not a preference. 2.0.0 moved __akerr_last_ignored to thread-local storage and made akerr_next_error() return a context that already holds its reference, and both expand at libakgl's call sites -- and at a consumer's, because akerror.h is part of libakgl's public interface. Mixing headers and libraries across that line double-counts every reference and never returns a pool slot. The soname moved to libakerror.so.2; include/akgl/error.h now also feature- tests AKERR_EXIT_STATUS_UNREPRESENTABLE, which is the narrowest probe for 2.0.1 since libakerror publishes no version macro. 0.7.0 for that reason: libakgl's own ABI is unchanged, but the one it re-exports through its headers is not. TODO.md records the pkg-config gap this makes sharper -- akgl.pc names no dependencies at all, so nothing tells a pkg-config consumer which libakerror it needs. Clean build, 26/26 ctest, memcheck clean, warning-clean at -Wall -Werror. libakgl.so.0.7 links libakerror.so.2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B8T5FAYXE8HEJqFLCYwNNc
271 lines
10 KiB
C
271 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);
|
|
|
|
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;
|
|
}
|