Report the failures that used to be crashes
Closes Defects items 30 and 31 and Known-and-still-open items 1, 2, 5, 9 and 11. Both string accessors in json_helpers.c ended their ATTEMPT block with FINISH(errctx, false), which swallows the failure, and then strncpy'd through the pointer akgl_heap_next_string never set. So the one condition the pool exists to report -- it is full, which in practice means something is not releasing -- arrived as a segfault somewhere else entirely. It is FINISH(errctx, true) now, and tests/json_helpers.c claims every slot and asserts AKGL_ERR_HEAP comes back out of both. That test segfaults against the old code, which is also how the tilemap leak test in the previous commit confirmed this one. akgl_tilemap_release tested layers[i].texture and destroyed tilesets[i].texture, so every tileset texture was freed twice on one release and no image layer's texture was freed at all. Pointers are cleared as they go, so a second release is safe instead of a use-after-free. akgl_game_update_fps called game.lowfpsfunc() unguarded, on a path taken on frame one because fps is 0 for the first second. Only akgl_game_init installs it, and renderer.h documents the other path deliberately -- a host that owns its window binds a backend instead. It installs the default when it finds NULL. akgl_controller_pushmap and akgl_controller_default checked only the upper bound, so a negative id indexed before akgl_controlmaps. The two test-harness helpers were quietly worthless. akgl_render_and_compare drew t1 on both passes, so it always reported a match and every image assertion built on it asserted nothing; and akgl_compare_sdl_surfaces memcmp'd s1->pitch * s1->h bytes out of both surfaces without checking that the second was the same size, so a smaller one was read past its end. Both fixed, both tested. tests/util.c also now calls the collide-point test it has defined and never run. 25/25 pass, memcheck clean, reindent --check and check_error_protocol clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,76 @@ static akerr_ErrorContext *load_fixture(void)
|
||||
* could safely get wrong depended on which typed accessor they happened to
|
||||
* call -- and the ones that crashed were the ones used most.
|
||||
*/
|
||||
/**
|
||||
* @brief An exhausted string pool must report AKGL_ERR_HEAP, not crash.
|
||||
*
|
||||
* Both string accessors ended their ATTEMPT block with `FINISH(errctx, false)`,
|
||||
* which swallows the failure instead of passing it up, and then ran
|
||||
* `strncpy(&(*dest)->data, ...)` through the pointer akgl_heap_next_string
|
||||
* never set. So the one condition the pool is designed to report -- it is full,
|
||||
* usually because something is not releasing -- arrived as a segfault inside
|
||||
* strncpy.
|
||||
*
|
||||
* This is what TODO.md Performance item 29 looked like from the outside: not
|
||||
* "the tilemap loader leaks five strings a load", but a crash somewhere else
|
||||
* entirely, fifty levels later.
|
||||
*/
|
||||
akerr_ErrorContext *test_json_string_accessor_reports_pool_exhaustion(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_String *claimed[AKGL_MAX_HEAP_STRING];
|
||||
akgl_String *dest = NULL;
|
||||
json_t *strings = NULL;
|
||||
int held = 0;
|
||||
int i = 0;
|
||||
|
||||
memset(&claimed, 0x00, sizeof(claimed));
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, akgl_get_json_array_value(fixture, "strings", &strings));
|
||||
|
||||
// Take every slot the pool has. Whatever else is holding one already
|
||||
// simply means this stops sooner.
|
||||
while ( held < AKGL_MAX_HEAP_STRING ) {
|
||||
akerr_ErrorContext *claim = akgl_heap_next_string(&claimed[held]);
|
||||
if ( claim != NULL ) {
|
||||
claim->handled = true;
|
||||
claim = akerr_release_error(claim);
|
||||
claimed[held] = NULL;
|
||||
break;
|
||||
}
|
||||
held += 1;
|
||||
}
|
||||
TEST_ASSERT(e, held > 0, "could not claim any pool strings");
|
||||
TEST_ASSERT(e, test_string_pool_used() == AKGL_MAX_HEAP_STRING,
|
||||
"the string pool is not full: %d of %d claimed",
|
||||
test_string_pool_used(), AKGL_MAX_HEAP_STRING);
|
||||
|
||||
// dest is NULL, so both accessors have to claim -- and cannot.
|
||||
dest = NULL;
|
||||
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP,
|
||||
akgl_get_json_string_value(fixture, "name", &dest),
|
||||
"reading a string with the pool exhausted");
|
||||
TEST_ASSERT(e, dest == NULL,
|
||||
"a refused string accessor wrote to its destination");
|
||||
|
||||
dest = NULL;
|
||||
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP,
|
||||
akgl_get_json_array_index_string(strings, 0, &dest),
|
||||
"reading an array string with the pool exhausted");
|
||||
TEST_ASSERT(e, dest == NULL,
|
||||
"a refused array string accessor wrote to its destination");
|
||||
} CLEANUP {
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
|
||||
if ( claimed[i] != NULL ) {
|
||||
IGNORE(akgl_heap_release_string(claimed[i]));
|
||||
}
|
||||
}
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_json_accessor_null_arguments(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
@@ -422,6 +492,7 @@ int main(void)
|
||||
CATCH(errctx, test_json_array_index_accessors());
|
||||
CATCH(errctx, test_json_type_and_key_errors());
|
||||
CATCH(errctx, test_json_with_default());
|
||||
CATCH(errctx, test_json_string_accessor_reports_pool_exhaustion());
|
||||
} CLEANUP {
|
||||
if ( fixture != NULL ) {
|
||||
json_decref(fixture);
|
||||
|
||||
Reference in New Issue
Block a user