#include #include #include #include #include #include #include #include #include #include "testutil.h" typedef akerr_ErrorContext *(*RegistryFuncPtr)(void); void *sdl_calloc_always_fails(size_t a, size_t b) { // This forces SDL to simulate an out of memory condition return NULL; } akerr_ErrorContext *test_akgl_registry_init(RegistryFuncPtr funcptr) { SDL_malloc_func malloc_func; SDL_calloc_func calloc_func; SDL_realloc_func realloc_func; SDL_free_func free_func; SDL_GetMemoryFunctions( &malloc_func, &calloc_func, &realloc_func, &free_func ); PREPARE_ERROR(errctx); ATTEMPT { SDL_SetMemoryFunctions( malloc_func, (SDL_calloc_func)&sdl_calloc_always_fails, realloc_func, free_func ); CATCH(errctx, funcptr()); } CLEANUP { SDL_SetMemoryFunctions( malloc_func, calloc_func, realloc_func, free_func ); } PROCESS(errctx) { } FINISH(errctx, true); FAIL_RETURN(errctx, AKGL_ERR_BEHAVIOR, "SDL memory allocator fails but registry reports successful property creation"); } akerr_ErrorContext *test_akgl_registry_init_creation_failures(void) { PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, test_akgl_registry_init(&akgl_registry_init_actor)); } CLEANUP { } PROCESS(errctx) { } HANDLE(errctx, AKERR_NULLPOINTER) { printf("Sucess\n"); } FINISH(errctx, true); ATTEMPT { CATCH(errctx, test_akgl_registry_init(&akgl_registry_init_sprite)); } CLEANUP { } PROCESS(errctx) { } HANDLE(errctx, AKERR_NULLPOINTER) { printf("Sucess\n"); } FINISH(errctx, true); ATTEMPT { CATCH(errctx, test_akgl_registry_init(&akgl_registry_init_spritesheet)); } CLEANUP { } PROCESS(errctx) { } HANDLE(errctx, AKERR_NULLPOINTER) { printf("Sucess\n"); } FINISH(errctx, true); ATTEMPT { CATCH(errctx, test_akgl_registry_init(&akgl_registry_init_character)); } CLEANUP { } PROCESS(errctx) { } HANDLE(errctx, AKERR_NULLPOINTER) { printf("Sucess\n"); } FINISH(errctx, true); SUCCEED_RETURN(errctx); } /** * @brief akgl_get_property must copy the value, and not a byte more. * * It used to copy a fixed AKGL_MAX_STRING_LENGTH bytes out of whatever SDL * returned, and what SDL returns is its own strdup of the value -- four bytes * for "0.0". That read up to 4 KiB past the end of somebody else's allocation on * every call, which valgrind reported twelve times over in tests/physics.c * alone. * * The destination is filled with a sentinel first, so the assertion is not * "the value arrived" -- that would pass either way -- but "the bytes past the * terminator were left alone", which is only true if the copy was bounded by * the source. */ akerr_ErrorContext *test_akgl_get_property_copies_only_the_value(void) { PREPARE_ERROR(errctx); akgl_String *value = NULL; char oversized[AKGL_MAX_STRING_LENGTH + 8]; size_t valuelen = 0; size_t i = 0; bool untouched = true; ATTEMPT { CATCH(errctx, akgl_heap_init()); CATCH(errctx, akgl_registry_init_properties()); CATCH(errctx, akgl_heap_next_string(&value)); memset(&value->data, 0x5a, AKGL_MAX_STRING_LENGTH); CATCH(errctx, akgl_set_property("registry.short", "0.0")); CATCH(errctx, akgl_get_property("registry.short", &value, "unset")); valuelen = strlen("0.0"); TEST_ASSERT(errctx, strcmp(value->data, "0.0") == 0, "akgl_get_property returned \"%s\", expected \"0.0\"", value->data); for ( i = (valuelen + 1); i < AKGL_MAX_STRING_LENGTH; i++ ) { TEST_ASSERT_FLAG(untouched, (value->data[i] == 0x5a)); } TEST_ASSERT(errctx, untouched, "akgl_get_property wrote past the end of a %zu byte value", valuelen); // The default takes the same path as a stored value. CATCH(errctx, akgl_get_property("registry.absent", &value, "fallback")); TEST_ASSERT(errctx, strcmp(value->data, "fallback") == 0, "akgl_get_property returned \"%s\" for an unset property, expected the default", value->data); // A value too long for an akgl_String is refused rather than truncated // into an unterminated buffer. memset(&oversized, 'x', sizeof(oversized)); oversized[AKGL_MAX_STRING_LENGTH + 7] = '\0'; CATCH(errctx, akgl_set_property("registry.oversized", (char *)&oversized)); TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_get_property("registry.oversized", &value, "unset"), "reading a property longer than an akgl_String"); // And the documented refusal survives: unset, with no default. TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_get_property("registry.absent", &value, NULL), "reading an unset property with no default"); } CLEANUP { if ( value != NULL ) { IGNORE(akgl_heap_release_string(value)); } } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } /** * @brief Every actor-state bit must be nameable, and every name must map to its own bit. * * akgl_registry_init_actor_state_strings walks AKGL_ACTOR_STATE_STRING_NAMES * and registers entry `i` under the value `1 << i`. Character JSON resolves * state names through that registry, so an entry that disagrees with the * `#define` in actor.h is a state no character can ever bind a sprite to. Bits * 11 and 12 were `UNDEFINED_11` and `UNDEFINED_12` here while actor.h called * them MOVING_IN and MOVING_OUT, so both were unreachable by name. * * The loop also pins the array's length: it reads exactly * AKGL_ACTOR_MAX_STATES entries, which is what the header declares and what * the definition is now sized by. It was declared one longer than defined. */ akerr_ErrorContext *test_actor_state_string_names(void) { PREPARE_ERROR(errctx); int i = 0; int j = 0; Sint64 registered = 0; ATTEMPT { CATCH(errctx, akgl_registry_init_actor_state_strings()); for ( i = 0; i < AKGL_ACTOR_MAX_STATES; i++ ) { TEST_ASSERT(errctx, AKGL_ACTOR_STATE_STRING_NAMES[i] != NULL, "actor state bit %d has no name", i); registered = SDL_GetNumberProperty( AKGL_REGISTRY_ACTOR_STATE_STRINGS, AKGL_ACTOR_STATE_STRING_NAMES[i], 0); TEST_ASSERT(errctx, registered == (Sint64)(1 << i), "state name %s resolves to %lld, expected %d", AKGL_ACTOR_STATE_STRING_NAMES[i], (long long)registered, (1 << i)); // No two entries may share a name, or the later one silently // overwrites the earlier in the registry and one bit becomes // unreachable without anything looking wrong. for ( j = 0; j < i; j++ ) { TEST_ASSERT(errctx, strcmp(AKGL_ACTOR_STATE_STRING_NAMES[i], AKGL_ACTOR_STATE_STRING_NAMES[j]) != 0, "actor state bits %d and %d share the name %s", j, i, AKGL_ACTOR_STATE_STRING_NAMES[i]); } } // The two that were unreachable, named explicitly so a regression reads // as what it is rather than as an index. TEST_ASSERT(errctx, SDL_GetNumberProperty(AKGL_REGISTRY_ACTOR_STATE_STRINGS, "AKGL_ACTOR_STATE_MOVING_IN", 0) == (Sint64)AKGL_ACTOR_STATE_MOVING_IN, "AKGL_ACTOR_STATE_MOVING_IN is not resolvable by name"); TEST_ASSERT(errctx, SDL_GetNumberProperty(AKGL_REGISTRY_ACTOR_STATE_STRINGS, "AKGL_ACTOR_STATE_MOVING_OUT", 0) == (Sint64)AKGL_ACTOR_STATE_MOVING_OUT, "AKGL_ACTOR_STATE_MOVING_OUT is not resolvable by name"); } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } /** * @brief Re-initializing a registry must replace the old set, not abandon it. * * Only akgl_registry_init_actor destroyed the set it was replacing; the other * seven leaked an SDL_PropertiesID on every call after the first. A game that * resets between levels calls these repeatedly. * * Also checks that akgl_registry_init() brings up the properties registry. * It did not until 0.5.0, so akgl_set_property was a silent no-op for any * caller that had not also gone through akgl_game_init. */ akerr_ErrorContext *test_akgl_registry_init_is_repeatable(void) { PREPARE_ERROR(errctx); akgl_String *readback = NULL; ATTEMPT { CATCH(errctx, akgl_registry_init()); TEST_ASSERT(errctx, AKGL_REGISTRY_PROPERTIES != 0, "akgl_registry_init did not create the properties registry"); // Whether the old set was *destroyed* is not observable from here -- // SDL hands out no liveness query, and it reuses ids -- so that half is // the memcheck run's job, and `cmake --build build --target memcheck` // gates on it. What is observable is that re-initializing really does // replace: a key written before the call must be gone after it. SDL_SetNumberProperty(AKGL_REGISTRY_SPRITE, "akgl_test_sentinel", 1); TEST_ASSERT(errctx, SDL_GetNumberProperty(AKGL_REGISTRY_SPRITE, "akgl_test_sentinel", 0) == 1, "could not write a sentinel into the sprite registry"); CATCH(errctx, akgl_registry_init_sprite()); TEST_ASSERT(errctx, AKGL_REGISTRY_SPRITE != 0, "re-initializing the sprite registry left it unset"); TEST_ASSERT(errctx, SDL_GetNumberProperty(AKGL_REGISTRY_SPRITE, "akgl_test_sentinel", 0) == 0, "re-initializing the sprite registry kept the old contents"); // Configuration set through the registry has to survive and read back, // which is the behaviour the missing initializer was breaking. CATCH(errctx, akgl_registry_init()); TEST_EXPECT_OK(errctx, akgl_set_property("test.property", "1234"), "setting a property after akgl_registry_init"); TEST_EXPECT_OK(errctx, akgl_get_property("test.property", &readback, NULL), "reading a property back after akgl_registry_init"); TEST_ASSERT(errctx, strcmp((char *)&readback->data, "1234") == 0, "property read back as '%s', expected '1234'", (char *)&readback->data); } CLEANUP { if ( readback != NULL ) { IGNORE(akgl_heap_release_string(readback)); } } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } int main(void) { PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, akgl_error_init()); TEST_TRAP_UNHANDLED_ERRORS(); CATCH(errctx, test_akgl_registry_init_creation_failures()); CATCH(errctx, test_akgl_get_property_copies_only_the_value()); CATCH(errctx, test_actor_state_string_names()); CATCH(errctx, test_akgl_registry_init_is_repeatable()); } CLEANUP { } PROCESS(errctx) { } FINISH_NORETURN(errctx); }