Add physics, heap, json_helpers, game, and actor test suites
Raise line coverage from 39.6 to 61.8 percent with four new suites and an
extension to the actor suite, and register every suite through a single
CMake list so a new test file cannot be left out of the coverage fixture.
Give the test targets a build-tree RPATH and prepend the build tree to
LD_LIBRARY_PATH for CTest, so a developer with a previously installed
libakgl.so exercises the library that was just compiled.
Fix six defects the new tests exposed:
- akgl_physics_simulate read self->gravity_time before its NULL check, so a
NULL backend crashed instead of reporting AKERR_NULLPOINTER.
- akgl_game_save transposed CLEANUP and PROCESS, which placed the fclose
inside the PROCESS switch. An ordinary save never flushed or closed its
stream and produced an empty file.
- akgl_game_save_actors wrote each name table terminator from the address of
a single char, emitting stack contents into the save file and a sentinel
the loader could not recognize.
- akgl_game_load_objectnamemap used CATCH directly inside while(1), where the
break leaves the loop rather than propagating, so a truncated name table
loaded as a successful game.
- akgl_Actor_cmhf_up_on and _down_on dereferenced actor->basechar with no
NULL check, unlike their left and right counterparts.
- akgl_actor_logic_movement checked actor twice instead of checking
actor->basechar before dereferencing it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:03:21 -04:00
|
|
|
/**
|
|
|
|
|
* @file json_helpers.c
|
|
|
|
|
* @brief Unit tests for the typed JSON accessors.
|
|
|
|
|
*
|
|
|
|
|
* These exercise the accessors directly rather than through sprite, character,
|
|
|
|
|
* or tilemap loading, so the error paths are reachable without building a
|
|
|
|
|
* malformed asset for every case.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include <SDL3/SDL.h>
|
|
|
|
|
#include <jansson.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
#include <akerror.h>
|
|
|
|
|
|
|
|
|
|
#include <akgl/error.h>
|
|
|
|
|
#include <akgl/json_helpers.h>
|
|
|
|
|
#include <akgl/heap.h>
|
|
|
|
|
#include <akgl/registry.h>
|
|
|
|
|
#include <akgl/staticstring.h>
|
|
|
|
|
|
|
|
|
|
#include "testutil.h"
|
|
|
|
|
|
|
|
|
|
/** @brief Fixture document shared by every test in this file. */
|
|
|
|
|
static json_t *fixture = NULL;
|
|
|
|
|
|
|
|
|
|
/** @brief Load tests/assets/snippets/test_json_helpers.json into @ref fixture. */
|
|
|
|
|
static akerr_ErrorContext *load_fixture(void)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(e);
|
|
|
|
|
json_error_t jsonerr;
|
|
|
|
|
akgl_String *pathstr = NULL;
|
|
|
|
|
|
|
|
|
|
ATTEMPT {
|
|
|
|
|
CATCH(e, akgl_heap_next_string(&pathstr));
|
|
|
|
|
snprintf(
|
|
|
|
|
(char *)&pathstr->data,
|
|
|
|
|
AKGL_MAX_STRING_LENGTH,
|
|
|
|
|
"%s%s",
|
|
|
|
|
SDL_GetBasePath(),
|
|
|
|
|
"assets/snippets/test_json_helpers.json");
|
|
|
|
|
fixture = json_load_file((char *)&pathstr->data, 0, &jsonerr);
|
|
|
|
|
FAIL_ZERO_BREAK(e, fixture, AKERR_IO,
|
|
|
|
|
"Unable to load the JSON fixture: %s", (char *)jsonerr.text);
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
IGNORE(akgl_heap_release_string(pathstr));
|
|
|
|
|
} PROCESS(e) {
|
|
|
|
|
} FINISH(e, true);
|
|
|
|
|
SUCCEED_RETURN(e);
|
|
|
|
|
}
|
|
|
|
|
|
Stop returning past CLEANUP, and validate the arguments every sibling validates
Closes internal-consistency items 16 and 17.
Ten *_RETURN macros sat inside ATTEMPT blocks, which return past CLEANUP and
skip every release in it. The one that mattered was the success path of
akgl_get_json_tilemap_property: it leaked two of the string pool's 256 entries
on every lookup that *found* what it was asked for, and a map load does that
several times per layer. tests/tilemap.c now runs each of its three paths --
found, absent, wrong type -- twice the pool size and asserts the pool is where
it started. Against the old code that test does not merely fail, it segfaults,
which is Defects item 30 seen from the outside: pool exhaustion arriving as a
NULL strncpy rather than as AKGL_ERR_HEAP.
Two of the conversions needed more than swapping the macro. In
akgl_get_json_tilemap_property a plain break would have fallen through to the
"property not found" FAIL_RETURN after FINISH, reporting a miss for something
found, so the success path sets a flag. In akgl_collide_rectangles the eight
early exits are followed by `*collide = false;`, which would have overwritten
the hit that broke out; each corner test writes the flag itself, so that line
is gone rather than moved. The same function also released its scratch string
once per loop iteration while continuing to use it, so the slot was free while
still live -- one claim now covers the whole scan.
akgl_controller_default is the other behavioural one: its SUCCEED_RETURN was
the last statement in the ATTEMPT block, so the path that falls out of FINISH
reached the closing brace of a non-void function.
scripts/check_error_protocol.py keeps both rules enforced -- a *_RETURN inside
ATTEMPT, and a return out of a HANDLE block -- as the error_protocol test.
Neither produces a compiler diagnostic and neither fails a test run until the
pool it drains is empty, which is why both have already shipped once.
For item 17: the eight typed JSON accessors that validated their container and
then wrote through dest unconditionally now check key and dest as the two
string accessors always did; the null physics backend checks its actors like
the arcade one; and akgl_render_2d_frame_start, _frame_end and _shutdown check
self, which the first two read straight through. tests/renderer.c calls all
three with NULL, which segfaulted before.
25/25 pass, memcheck clean, reindent --check clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:56:10 -04:00
|
|
|
/**
|
|
|
|
|
* @brief Every accessor must refuse a NULL key or destination, not dereference it.
|
|
|
|
|
*
|
|
|
|
|
* Only the two string accessors did. The other eight validated the container
|
|
|
|
|
* and then wrote through @p dest unconditionally, so which arguments a caller
|
|
|
|
|
* could safely get wrong depended on which typed accessor they happened to
|
|
|
|
|
* call -- and the ones that crashed were the ones used most.
|
|
|
|
|
*/
|
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>
2026-08-01 00:32:11 -04:00
|
|
|
/**
|
|
|
|
|
* @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);
|
|
|
|
|
}
|
|
|
|
|
|
Stop returning past CLEANUP, and validate the arguments every sibling validates
Closes internal-consistency items 16 and 17.
Ten *_RETURN macros sat inside ATTEMPT blocks, which return past CLEANUP and
skip every release in it. The one that mattered was the success path of
akgl_get_json_tilemap_property: it leaked two of the string pool's 256 entries
on every lookup that *found* what it was asked for, and a map load does that
several times per layer. tests/tilemap.c now runs each of its three paths --
found, absent, wrong type -- twice the pool size and asserts the pool is where
it started. Against the old code that test does not merely fail, it segfaults,
which is Defects item 30 seen from the outside: pool exhaustion arriving as a
NULL strncpy rather than as AKGL_ERR_HEAP.
Two of the conversions needed more than swapping the macro. In
akgl_get_json_tilemap_property a plain break would have fallen through to the
"property not found" FAIL_RETURN after FINISH, reporting a miss for something
found, so the success path sets a flag. In akgl_collide_rectangles the eight
early exits are followed by `*collide = false;`, which would have overwritten
the hit that broke out; each corner test writes the flag itself, so that line
is gone rather than moved. The same function also released its scratch string
once per loop iteration while continuing to use it, so the slot was free while
still live -- one claim now covers the whole scan.
akgl_controller_default is the other behavioural one: its SUCCEED_RETURN was
the last statement in the ATTEMPT block, so the path that falls out of FINISH
reached the closing brace of a non-void function.
scripts/check_error_protocol.py keeps both rules enforced -- a *_RETURN inside
ATTEMPT, and a return out of a HANDLE block -- as the error_protocol test.
Neither produces a compiler diagnostic and neither fails a test run until the
pool it drains is empty, which is why both have already shipped once.
For item 17: the eight typed JSON accessors that validated their container and
then wrote through dest unconditionally now check key and dest as the two
string accessors always did; the null physics backend checks its actors like
the arcade one; and akgl_render_2d_frame_start, _frame_end and _shutdown check
self, which the first two read straight through. tests/renderer.c calls all
three with NULL, which segfaulted before.
25/25 pass, memcheck clean, reindent --check clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:56:10 -04:00
|
|
|
akerr_ErrorContext *test_json_accessor_null_arguments(void)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(e);
|
|
|
|
|
int intval = 0;
|
|
|
|
|
float floatval = 0;
|
|
|
|
|
double dblval = 0;
|
|
|
|
|
bool boolval = false;
|
|
|
|
|
json_t *jsonval = NULL;
|
|
|
|
|
akgl_String *strval = NULL;
|
|
|
|
|
|
|
|
|
|
ATTEMPT {
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_object_value(fixture, NULL, &jsonval),
|
|
|
|
|
"object accessor with a NULL key");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_object_value(fixture, "nested", NULL),
|
|
|
|
|
"object accessor with a NULL destination");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_boolean_value(fixture, NULL, &boolval),
|
|
|
|
|
"boolean accessor with a NULL key");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_boolean_value(fixture, "enabled", NULL),
|
|
|
|
|
"boolean accessor with a NULL destination");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_integer_value(fixture, NULL, &intval),
|
|
|
|
|
"integer accessor with a NULL key");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_integer_value(fixture, "count", NULL),
|
|
|
|
|
"integer accessor with a NULL destination");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_number_value(fixture, NULL, &floatval),
|
|
|
|
|
"number accessor with a NULL key");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_number_value(fixture, "ratio", NULL),
|
|
|
|
|
"number accessor with a NULL destination");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_double_value(fixture, NULL, &dblval),
|
|
|
|
|
"double accessor with a NULL key");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_double_value(fixture, "ratio", NULL),
|
|
|
|
|
"double accessor with a NULL destination");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_value(fixture, NULL, &jsonval),
|
|
|
|
|
"array accessor with a NULL key");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_value(fixture, "integers", NULL),
|
|
|
|
|
"array accessor with a NULL destination");
|
|
|
|
|
|
|
|
|
|
// The index accessors take no key, but they take a destination.
|
|
|
|
|
CATCH(e, akgl_get_json_array_value(fixture, "integers", &jsonval));
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_index_integer(jsonval, 0, NULL),
|
|
|
|
|
"array index integer accessor with a NULL destination");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_index_object(jsonval, 0, NULL),
|
|
|
|
|
"array index object accessor with a NULL destination");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_index_string(jsonval, 0, NULL),
|
|
|
|
|
"array index string accessor with a NULL destination");
|
|
|
|
|
|
|
|
|
|
// Nothing above should have claimed a pool string on the way to
|
|
|
|
|
// refusing, which is the other half of "refuse rather than proceed".
|
|
|
|
|
TEST_ASSERT(e, strval == NULL, "a refused accessor wrote to its destination anyway");
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
} PROCESS(e) {
|
|
|
|
|
} FINISH(e, true);
|
|
|
|
|
SUCCEED_RETURN(e);
|
|
|
|
|
}
|
|
|
|
|
|
Add physics, heap, json_helpers, game, and actor test suites
Raise line coverage from 39.6 to 61.8 percent with four new suites and an
extension to the actor suite, and register every suite through a single
CMake list so a new test file cannot be left out of the coverage fixture.
Give the test targets a build-tree RPATH and prepend the build tree to
LD_LIBRARY_PATH for CTest, so a developer with a previously installed
libakgl.so exercises the library that was just compiled.
Fix six defects the new tests exposed:
- akgl_physics_simulate read self->gravity_time before its NULL check, so a
NULL backend crashed instead of reporting AKERR_NULLPOINTER.
- akgl_game_save transposed CLEANUP and PROCESS, which placed the fclose
inside the PROCESS switch. An ordinary save never flushed or closed its
stream and produced an empty file.
- akgl_game_save_actors wrote each name table terminator from the address of
a single char, emitting stack contents into the save file and a sentinel
the loader could not recognize.
- akgl_game_load_objectnamemap used CATCH directly inside while(1), where the
break leaves the loop rather than propagating, so a truncated name table
loaded as a successful game.
- akgl_Actor_cmhf_up_on and _down_on dereferenced actor->basechar with no
NULL check, unlike their left and right counterparts.
- akgl_actor_logic_movement checked actor twice instead of checking
actor->basechar before dereferencing it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:03:21 -04:00
|
|
|
akerr_ErrorContext *test_json_scalar_accessors(void)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(e);
|
|
|
|
|
int intval = 0;
|
|
|
|
|
float floatval = 0;
|
|
|
|
|
bool boolval = false;
|
|
|
|
|
json_t *objval = NULL;
|
|
|
|
|
json_t *arrayval = NULL;
|
|
|
|
|
|
|
|
|
|
ATTEMPT {
|
|
|
|
|
TEST_EXPECT_OK(e, akgl_get_json_integer_value(fixture, "count", &intval), "read count");
|
|
|
|
|
TEST_ASSERT(e, intval == 42, "count read as %d, expected 42", intval);
|
|
|
|
|
|
|
|
|
|
TEST_EXPECT_OK(e, akgl_get_json_integer_value(fixture, "negative", &intval), "read negative");
|
|
|
|
|
TEST_ASSERT(e, intval == -17, "negative read as %d, expected -17", intval);
|
|
|
|
|
|
|
|
|
|
TEST_EXPECT_OK(e, akgl_get_json_number_value(fixture, "ratio", &floatval), "read ratio");
|
|
|
|
|
TEST_ASSERT_FEQ(e, floatval, 2.5f, "ratio read as %f, expected 2.5", floatval);
|
|
|
|
|
|
|
|
|
|
// A JSON integer is a number, so the float accessor accepts it too.
|
|
|
|
|
TEST_EXPECT_OK(e, akgl_get_json_number_value(fixture, "count", &floatval), "read count as a number");
|
|
|
|
|
TEST_ASSERT_FEQ(e, floatval, 42.0f, "count read as number %f, expected 42", floatval);
|
|
|
|
|
|
|
|
|
|
TEST_EXPECT_OK(e, akgl_get_json_boolean_value(fixture, "enabled", &boolval), "read enabled");
|
|
|
|
|
TEST_ASSERT(e, boolval == true, "enabled read as false");
|
|
|
|
|
TEST_EXPECT_OK(e, akgl_get_json_boolean_value(fixture, "disabled", &boolval), "read disabled");
|
|
|
|
|
TEST_ASSERT(e, boolval == false, "disabled read as true");
|
|
|
|
|
|
|
|
|
|
TEST_EXPECT_OK(e, akgl_get_json_object_value(fixture, "nested", &objval), "read nested");
|
|
|
|
|
TEST_ASSERT(e, objval != NULL, "nested object came back NULL");
|
|
|
|
|
TEST_EXPECT_OK(e, akgl_get_json_integer_value(objval, "innercount", &intval), "read nested innercount");
|
|
|
|
|
TEST_ASSERT(e, intval == 7, "nested innercount read as %d, expected 7", intval);
|
|
|
|
|
|
|
|
|
|
TEST_EXPECT_OK(e, akgl_get_json_array_value(fixture, "integers", &arrayval), "read integers array");
|
|
|
|
|
TEST_ASSERT(e, json_array_size(arrayval) == 3,
|
|
|
|
|
"integers array had %d entries, expected 3", (int)json_array_size(arrayval));
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
} PROCESS(e) {
|
|
|
|
|
} FINISH(e, true);
|
|
|
|
|
SUCCEED_RETURN(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *test_json_double_value(void)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(e);
|
|
|
|
|
double dblval = 0;
|
|
|
|
|
|
|
|
|
|
ATTEMPT {
|
|
|
|
|
TEST_EXPECT_OK(e, akgl_get_json_double_value(fixture, "ratio", &dblval), "read ratio as a double");
|
|
|
|
|
TEST_ASSERT(e, dblval > 2.4999 && dblval < 2.5001,
|
|
|
|
|
"ratio read as double %f, expected 2.5", dblval);
|
|
|
|
|
|
|
|
|
|
// Integers satisfy json_is_number, so the double accessor takes them.
|
|
|
|
|
TEST_EXPECT_OK(e, akgl_get_json_double_value(fixture, "count", &dblval), "read count as a double");
|
|
|
|
|
TEST_ASSERT(e, dblval > 41.999 && dblval < 42.001,
|
|
|
|
|
"count read as double %f, expected 42", dblval);
|
|
|
|
|
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_get_json_double_value(fixture, "absent", &dblval),
|
|
|
|
|
"double accessor on a missing key");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_double_value(fixture, "name", &dblval),
|
|
|
|
|
"double accessor on a string value");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_double_value(NULL, "ratio", &dblval),
|
|
|
|
|
"double accessor on a NULL object");
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
} PROCESS(e) {
|
|
|
|
|
} FINISH(e, true);
|
|
|
|
|
SUCCEED_RETURN(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *test_json_string_accessor(void)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(e);
|
|
|
|
|
akgl_String *allocated = NULL;
|
|
|
|
|
akgl_String *reused = NULL;
|
|
|
|
|
|
|
|
|
|
ATTEMPT {
|
|
|
|
|
// A NULL destination makes the accessor claim a heap string.
|
|
|
|
|
TEST_EXPECT_OK(e, akgl_get_json_string_value(fixture, "name", &allocated),
|
|
|
|
|
"read name into a NULL destination");
|
|
|
|
|
TEST_ASSERT(e, allocated != NULL, "the accessor did not allocate a destination string");
|
|
|
|
|
TEST_ASSERT(e, strcmp((char *)&allocated->data, "json helper fixture") == 0,
|
|
|
|
|
"name read as \"%s\"", (char *)&allocated->data);
|
|
|
|
|
|
|
|
|
|
// A caller-supplied destination is written in place, not replaced.
|
|
|
|
|
CATCH(e, akgl_heap_next_string(&reused));
|
|
|
|
|
CATCH(e, akgl_string_initialize(reused, "placeholder"));
|
|
|
|
|
{
|
|
|
|
|
akgl_String *before = reused;
|
|
|
|
|
TEST_EXPECT_OK(e, akgl_get_json_string_value(fixture, "name", &reused),
|
|
|
|
|
"read name into a preallocated destination");
|
|
|
|
|
TEST_ASSERT(e, reused == before,
|
|
|
|
|
"the accessor replaced a caller-supplied destination pointer");
|
|
|
|
|
}
|
|
|
|
|
TEST_ASSERT(e, strcmp((char *)&reused->data, "json helper fixture") == 0,
|
|
|
|
|
"in-place read produced \"%s\"", (char *)&reused->data);
|
|
|
|
|
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_get_json_string_value(fixture, "absent", &reused),
|
|
|
|
|
"string accessor on a missing key");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_string_value(fixture, "count", &reused),
|
|
|
|
|
"string accessor on an integer value");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_string_value(NULL, "name", &reused),
|
|
|
|
|
"string accessor on a NULL object");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_string_value(fixture, NULL, &reused),
|
|
|
|
|
"string accessor with a NULL key");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_string_value(fixture, "name", NULL),
|
|
|
|
|
"string accessor with a NULL destination");
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
IGNORE(akgl_heap_release_string(allocated));
|
|
|
|
|
IGNORE(akgl_heap_release_string(reused));
|
|
|
|
|
} PROCESS(e) {
|
|
|
|
|
} FINISH(e, true);
|
|
|
|
|
SUCCEED_RETURN(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *test_json_array_index_accessors(void)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(e);
|
|
|
|
|
json_t *integers = NULL;
|
|
|
|
|
json_t *strings = NULL;
|
|
|
|
|
json_t *objects = NULL;
|
|
|
|
|
json_t *mixed = NULL;
|
|
|
|
|
json_t *element = NULL;
|
|
|
|
|
akgl_String *strval = NULL;
|
|
|
|
|
int intval = 0;
|
|
|
|
|
|
|
|
|
|
ATTEMPT {
|
|
|
|
|
CATCH(e, akgl_get_json_array_value(fixture, "integers", &integers));
|
|
|
|
|
CATCH(e, akgl_get_json_array_value(fixture, "strings", &strings));
|
|
|
|
|
CATCH(e, akgl_get_json_array_value(fixture, "objects", &objects));
|
|
|
|
|
CATCH(e, akgl_get_json_array_value(fixture, "mixed", &mixed));
|
|
|
|
|
|
|
|
|
|
TEST_EXPECT_OK(e, akgl_get_json_array_index_integer(integers, 0, &intval), "integers[0]");
|
|
|
|
|
TEST_ASSERT(e, intval == 10, "integers[0] read as %d, expected 10", intval);
|
|
|
|
|
TEST_EXPECT_OK(e, akgl_get_json_array_index_integer(integers, 2, &intval), "integers[2]");
|
|
|
|
|
TEST_ASSERT(e, intval == 30, "integers[2] read as %d, expected 30", intval);
|
|
|
|
|
|
|
|
|
|
TEST_EXPECT_OK(e, akgl_get_json_array_index_string(strings, 1, &strval), "strings[1]");
|
|
|
|
|
TEST_ASSERT(e, strcmp((char *)&strval->data, "beta") == 0,
|
|
|
|
|
"strings[1] read as \"%s\", expected \"beta\"", (char *)&strval->data);
|
|
|
|
|
|
|
|
|
|
TEST_EXPECT_OK(e, akgl_get_json_array_index_object(objects, 1, &element), "objects[1]");
|
|
|
|
|
TEST_EXPECT_OK(e, akgl_get_json_integer_value(element, "id", &intval), "objects[1].id");
|
|
|
|
|
TEST_ASSERT(e, intval == 2, "objects[1].id read as %d, expected 2", intval);
|
|
|
|
|
|
|
|
|
|
// One past the end, and far past the end, are both out of bounds.
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_get_json_array_index_integer(integers, 3, &intval),
|
|
|
|
|
"integers[3] is one past the end");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_get_json_array_index_integer(integers, 99, &intval),
|
|
|
|
|
"integers[99] is far past the end");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_get_json_array_index_object(objects, 5, &element),
|
|
|
|
|
"objects[5] is past the end");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_get_json_array_index_string(strings, 5, &strval),
|
|
|
|
|
"strings[5] is past the end");
|
|
|
|
|
|
|
|
|
|
// The mixed array holds one of each type, so every accessor can be shown
|
|
|
|
|
// to reject the entries that are not its own.
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_array_index_integer(mixed, 1, &intval),
|
|
|
|
|
"integer accessor on a string element");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_array_index_string(mixed, 0, &strval),
|
|
|
|
|
"string accessor on an integer element");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_array_index_object(mixed, 0, &element),
|
|
|
|
|
"object accessor on an integer element");
|
|
|
|
|
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_index_integer(NULL, 0, &intval),
|
|
|
|
|
"integer index accessor on a NULL array");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_index_object(NULL, 0, &element),
|
|
|
|
|
"object index accessor on a NULL array");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_index_string(NULL, 0, &strval),
|
|
|
|
|
"string index accessor on a NULL array");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_index_string(strings, 0, NULL),
|
|
|
|
|
"string index accessor with a NULL destination");
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
IGNORE(akgl_heap_release_string(strval));
|
|
|
|
|
} PROCESS(e) {
|
|
|
|
|
} FINISH(e, true);
|
|
|
|
|
SUCCEED_RETURN(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *test_json_type_and_key_errors(void)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(e);
|
|
|
|
|
int intval = 0;
|
|
|
|
|
float floatval = 0;
|
|
|
|
|
bool boolval = false;
|
|
|
|
|
json_t *objval = NULL;
|
|
|
|
|
json_t *arrayval = NULL;
|
|
|
|
|
|
|
|
|
|
ATTEMPT {
|
|
|
|
|
// Missing keys.
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_get_json_integer_value(fixture, "absent", &intval),
|
|
|
|
|
"integer accessor on a missing key");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_get_json_number_value(fixture, "absent", &floatval),
|
|
|
|
|
"number accessor on a missing key");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_get_json_boolean_value(fixture, "absent", &boolval),
|
|
|
|
|
"boolean accessor on a missing key");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_get_json_object_value(fixture, "absent", &objval),
|
|
|
|
|
"object accessor on a missing key");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_get_json_array_value(fixture, "absent", &arrayval),
|
|
|
|
|
"array accessor on a missing key");
|
|
|
|
|
|
|
|
|
|
// Wrong types.
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_integer_value(fixture, "name", &intval),
|
|
|
|
|
"integer accessor on a string");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_integer_value(fixture, "ratio", &intval),
|
|
|
|
|
"integer accessor on a real");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_number_value(fixture, "name", &floatval),
|
|
|
|
|
"number accessor on a string");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_boolean_value(fixture, "count", &boolval),
|
|
|
|
|
"boolean accessor on an integer");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_object_value(fixture, "integers", &objval),
|
|
|
|
|
"object accessor on an array");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_TYPE, akgl_get_json_array_value(fixture, "nested", &arrayval),
|
|
|
|
|
"array accessor on an object");
|
|
|
|
|
|
|
|
|
|
// NULL containers.
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_integer_value(NULL, "count", &intval),
|
|
|
|
|
"integer accessor on a NULL object");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_number_value(NULL, "ratio", &floatval),
|
|
|
|
|
"number accessor on a NULL object");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_boolean_value(NULL, "enabled", &boolval),
|
|
|
|
|
"boolean accessor on a NULL object");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_object_value(NULL, "nested", &objval),
|
|
|
|
|
"object accessor on a NULL object");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_get_json_array_value(NULL, "integers", &arrayval),
|
|
|
|
|
"array accessor on a NULL object");
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
} PROCESS(e) {
|
|
|
|
|
} FINISH(e, true);
|
|
|
|
|
SUCCEED_RETURN(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *test_json_with_default(void)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(e);
|
|
|
|
|
int dest = 0;
|
|
|
|
|
int defval = 99;
|
|
|
|
|
akerr_ErrorContext *keyerr = NULL;
|
|
|
|
|
akerr_ErrorContext *typeerr = NULL;
|
Make savegames, optional array elements, empty text and coverage work
Closes Known-and-still-open items 7 and 13, and Defects items 18, 25 and 27.
The savegame name tables carry no length prefix, so writer and reader have to
agree on a field width exactly. The writer used each object's own maximum name
length -- 512 for a spritesheet, a filename -- and the reader used
AKGL_ACTOR_MAX_NAME_LENGTH for all four. Four AKGL_GAME_SAVE_*_NAME_WIDTH
constants drive both sides now.
The failure turned out to be worse than "cannot be read back": a reader
stepping the wrong width does not run off anything, it finds a run of zeros
inside an entry, stops early, and reports success with silently wrong maps. A
test asserting only that the load succeeded passed against the broken reader.
So akgl_game_load checks it is at EOF once the tables are read, which turns a
width disagreement into AKERR_IO instead of a corruption. That is the assertion
the new roundtrip test -- the first with all four registries populated -- hangs
on.
akgl_get_json_with_default gains a third HANDLE_GROUP for AKERR_OUTOFBOUNDS,
which is what the array index accessors report, so "this element is optional"
works for an array element and not only for an object member. The arm goes
above the one holding the memcpy: HANDLE_GROUP emits no break and every arm
falls into that body.
That test needed a second attempt. with_default returns *the context it was
given* when it does not handle the status, so TEST_EXPECT_OK -- which releases
whatever the statement returns -- double-released it against a CLEANUP block
that released it too, and a double-released context corrupts the failure rather
than reporting it. The first draft passed against the unfixed library.
akgl_text_rendertextat returns success without rasterizing for the empty
string, matching akgl_text_measure, which has always accepted it. The check
sits after the font and backend guards, so drawing nothing still refuses what
drawing something refuses. tests/text.c had this case written and unasserted
waiting for the two halves of the header to agree.
character_load_json_state_int_from_strings guards dest rather than testing
states twice. Not asserted: the function is static with one call site that
passes a real pointer, so the guard cannot fire, and reaching it from a test
would mean giving it external linkage purely for that.
Both gcovr invocations take the build tree as an explicit positional search
path. gcovr searches --root when given none, which is the source directory,
where build trees live; --object-directory does not narrow it. Verified by
building two instrumented trees with a source edit between them: the old
invocation fails with "Got function write_exact on multiple lines: 46, 48" and
exits 64, the new one exits 0 and the full coverage run passes with the stale
tree still present.
25/25 pass, reindent --check, check_api_surface and check_error_protocol clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:18:31 -04:00
|
|
|
akerr_ErrorContext *defaulted = NULL;
|
|
|
|
|
json_t *shortarray = NULL;
|
|
|
|
|
json_t *junkobj = NULL;
|
Add physics, heap, json_helpers, game, and actor test suites
Raise line coverage from 39.6 to 61.8 percent with four new suites and an
extension to the actor suite, and register every suite through a single
CMake list so a new test file cannot be left out of the coverage fixture.
Give the test targets a build-tree RPATH and prepend the build tree to
LD_LIBRARY_PATH for CTest, so a developer with a previously installed
libakgl.so exercises the library that was just compiled.
Fix six defects the new tests exposed:
- akgl_physics_simulate read self->gravity_time before its NULL check, so a
NULL backend crashed instead of reporting AKERR_NULLPOINTER.
- akgl_game_save transposed CLEANUP and PROCESS, which placed the fclose
inside the PROCESS switch. An ordinary save never flushed or closed its
stream and produced an empty file.
- akgl_game_save_actors wrote each name table terminator from the address of
a single char, emitting stack contents into the save file and a sentinel
the loader could not recognize.
- akgl_game_load_objectnamemap used CATCH directly inside while(1), where the
break leaves the loop rather than propagating, so a truncated name table
loaded as a successful game.
- akgl_Actor_cmhf_up_on and _down_on dereferenced actor->basechar with no
NULL check, unlike their left and right counterparts.
- akgl_actor_logic_movement checked actor twice instead of checking
actor->basechar before dereferencing it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:03:21 -04:00
|
|
|
int junk = 0;
|
|
|
|
|
|
|
|
|
|
ATTEMPT {
|
|
|
|
|
// A NULL error means the read succeeded, so the default is not applied.
|
|
|
|
|
dest = 1;
|
|
|
|
|
TEST_EXPECT_OK(e, akgl_get_json_with_default(NULL, (void *)&defval, (void *)&dest, sizeof(int)),
|
|
|
|
|
"with_default on a NULL error");
|
|
|
|
|
TEST_ASSERT(e, dest == 1, "with_default overwrote a successful read (dest is now %d)", dest);
|
|
|
|
|
|
|
|
|
|
// An AKERR_KEY failure substitutes the default.
|
|
|
|
|
dest = 1;
|
|
|
|
|
keyerr = akgl_get_json_integer_value(fixture, "absent", &junk);
|
|
|
|
|
TEST_ASSERT(e, keyerr != NULL, "the missing-key read unexpectedly succeeded");
|
|
|
|
|
TEST_EXPECT_OK(e, akgl_get_json_with_default(keyerr, (void *)&defval, (void *)&dest, sizeof(int)),
|
|
|
|
|
"with_default on an AKERR_KEY error");
|
|
|
|
|
TEST_ASSERT(e, dest == 99, "with_default did not apply the default (dest is %d)", dest);
|
|
|
|
|
keyerr = NULL;
|
|
|
|
|
|
|
|
|
|
// An unrelated failure is not swallowed, so the caller still sees it.
|
|
|
|
|
dest = 1;
|
|
|
|
|
typeerr = akgl_get_json_integer_value(fixture, "name", &junk);
|
|
|
|
|
TEST_ASSERT(e, typeerr != NULL, "the wrong-type read unexpectedly succeeded");
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_TYPE,
|
|
|
|
|
akgl_get_json_with_default(typeerr, (void *)&defval, (void *)&dest, sizeof(int)),
|
|
|
|
|
"with_default must propagate an unrelated error");
|
|
|
|
|
TEST_ASSERT(e, dest == 1, "with_default applied the default for an unrelated error");
|
|
|
|
|
typeerr = NULL;
|
|
|
|
|
|
Make savegames, optional array elements, empty text and coverage work
Closes Known-and-still-open items 7 and 13, and Defects items 18, 25 and 27.
The savegame name tables carry no length prefix, so writer and reader have to
agree on a field width exactly. The writer used each object's own maximum name
length -- 512 for a spritesheet, a filename -- and the reader used
AKGL_ACTOR_MAX_NAME_LENGTH for all four. Four AKGL_GAME_SAVE_*_NAME_WIDTH
constants drive both sides now.
The failure turned out to be worse than "cannot be read back": a reader
stepping the wrong width does not run off anything, it finds a run of zeros
inside an entry, stops early, and reports success with silently wrong maps. A
test asserting only that the load succeeded passed against the broken reader.
So akgl_game_load checks it is at EOF once the tables are read, which turns a
width disagreement into AKERR_IO instead of a corruption. That is the assertion
the new roundtrip test -- the first with all four registries populated -- hangs
on.
akgl_get_json_with_default gains a third HANDLE_GROUP for AKERR_OUTOFBOUNDS,
which is what the array index accessors report, so "this element is optional"
works for an array element and not only for an object member. The arm goes
above the one holding the memcpy: HANDLE_GROUP emits no break and every arm
falls into that body.
That test needed a second attempt. with_default returns *the context it was
given* when it does not handle the status, so TEST_EXPECT_OK -- which releases
whatever the statement returns -- double-released it against a CLEANUP block
that released it too, and a double-released context corrupts the failure rather
than reporting it. The first draft passed against the unfixed library.
akgl_text_rendertextat returns success without rasterizing for the empty
string, matching akgl_text_measure, which has always accepted it. The check
sits after the font and backend guards, so drawing nothing still refuses what
drawing something refuses. tests/text.c had this case written and unasserted
waiting for the two halves of the header to agree.
character_load_json_state_int_from_strings guards dest rather than testing
states twice. Not asserted: the function is static with one call site that
passes a real pointer, so the guard cannot fire, and reaching it from a test
would mean giving it external linkage purely for that.
Both gcovr invocations take the build tree as an explicit positional search
path. gcovr searches --root when given none, which is the source directory,
where build trees live; --object-directory does not narrow it. Verified by
building two instrumented trees with a source edit between them: the old
invocation fails with "Got function write_exact on multiple lines: 46, 48" and
exits 64, the new one exits 0 and the full coverage run passes with the stale
tree still present.
25/25 pass, reindent --check, check_api_surface and check_error_protocol clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:18:31 -04:00
|
|
|
// A short array. This is the one that did not work: the three array
|
|
|
|
|
// index accessors report AKERR_OUTOFBOUNDS, which with_default never
|
|
|
|
|
// handled, so "this element is optional" worked for an object member
|
|
|
|
|
// and silently did not for an array element. Nothing in tree passed an
|
|
|
|
|
// array accessor's error here, which is why it went unnoticed.
|
|
|
|
|
dest = 1;
|
|
|
|
|
CATCH(e, akgl_get_json_array_value(fixture, "integers", &shortarray));
|
|
|
|
|
keyerr = akgl_get_json_array_index_integer(shortarray, 99, &junk);
|
|
|
|
|
TEST_ASSERT(e, keyerr != NULL, "reading past the end of an array unexpectedly succeeded");
|
|
|
|
|
TEST_ASSERT(e, keyerr->status == AKERR_OUTOFBOUNDS,
|
|
|
|
|
"a short array reported %d, expected AKERR_OUTOFBOUNDS", keyerr->status);
|
|
|
|
|
// Not TEST_EXPECT_OK here. That macro releases whatever context the
|
|
|
|
|
// statement returns, and akgl_get_json_with_default returns *the context
|
|
|
|
|
// it was given* when it does not handle it -- so on the failing path the
|
|
|
|
|
// CLEANUP block below would release the same context a second time, and
|
|
|
|
|
// a double-released context corrupts the failure rather than reporting
|
|
|
|
|
// it. Ownership passes to with_default either way: it releases through
|
|
|
|
|
// FINISH when it handles the status, and hands the context back when it
|
|
|
|
|
// does not.
|
|
|
|
|
defaulted = akgl_get_json_with_default(keyerr, (void *)&defval, (void *)&dest, sizeof(int));
|
|
|
|
|
keyerr = NULL;
|
|
|
|
|
if ( defaulted != NULL ) {
|
|
|
|
|
defaulted->handled = true;
|
|
|
|
|
defaulted = akerr_release_error(defaulted);
|
|
|
|
|
FAIL_BREAK(e, AKGL_ERR_BEHAVIOR,
|
|
|
|
|
"with_default propagated AKERR_OUTOFBOUNDS instead of applying the default");
|
|
|
|
|
}
|
|
|
|
|
TEST_ASSERT(e, dest == 99,
|
|
|
|
|
"with_default did not apply the default for a short array (dest is %d)", dest);
|
|
|
|
|
|
|
|
|
|
// The same through the object index accessor, so the fix is pinned to
|
|
|
|
|
// the status rather than to one call site.
|
|
|
|
|
dest = 1;
|
|
|
|
|
keyerr = akgl_get_json_array_index_object(shortarray, 99, &junkobj);
|
|
|
|
|
TEST_ASSERT(e, keyerr != NULL, "reading an object past the end unexpectedly succeeded");
|
|
|
|
|
defaulted = akgl_get_json_with_default(keyerr, (void *)&defval, (void *)&dest, sizeof(int));
|
|
|
|
|
keyerr = NULL;
|
|
|
|
|
if ( defaulted != NULL ) {
|
|
|
|
|
defaulted->handled = true;
|
|
|
|
|
defaulted = akerr_release_error(defaulted);
|
|
|
|
|
FAIL_BREAK(e, AKGL_ERR_BEHAVIOR,
|
|
|
|
|
"with_default propagated a short object array instead of applying the default");
|
|
|
|
|
}
|
|
|
|
|
TEST_ASSERT(e, dest == 99, "with_default did not apply the default (dest is %d)", dest);
|
|
|
|
|
|
Add physics, heap, json_helpers, game, and actor test suites
Raise line coverage from 39.6 to 61.8 percent with four new suites and an
extension to the actor suite, and register every suite through a single
CMake list so a new test file cannot be left out of the coverage fixture.
Give the test targets a build-tree RPATH and prepend the build tree to
LD_LIBRARY_PATH for CTest, so a developer with a previously installed
libakgl.so exercises the library that was just compiled.
Fix six defects the new tests exposed:
- akgl_physics_simulate read self->gravity_time before its NULL check, so a
NULL backend crashed instead of reporting AKERR_NULLPOINTER.
- akgl_game_save transposed CLEANUP and PROCESS, which placed the fclose
inside the PROCESS switch. An ordinary save never flushed or closed its
stream and produced an empty file.
- akgl_game_save_actors wrote each name table terminator from the address of
a single char, emitting stack contents into the save file and a sentinel
the loader could not recognize.
- akgl_game_load_objectnamemap used CATCH directly inside while(1), where the
break leaves the loop rather than propagating, so a truncated name table
loaded as a successful game.
- akgl_Actor_cmhf_up_on and _down_on dereferenced actor->basechar with no
NULL check, unlike their left and right counterparts.
- akgl_actor_logic_movement checked actor twice instead of checking
actor->basechar before dereferencing it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:03:21 -04:00
|
|
|
// NULL arguments alongside a real error are a contract violation.
|
|
|
|
|
keyerr = akgl_get_json_integer_value(fixture, "absent", &junk);
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
|
|
|
|
akgl_get_json_with_default(keyerr, NULL, (void *)&dest, sizeof(int)),
|
|
|
|
|
"with_default with a NULL default value");
|
|
|
|
|
keyerr = NULL;
|
|
|
|
|
keyerr = akgl_get_json_integer_value(fixture, "absent", &junk);
|
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
|
|
|
|
akgl_get_json_with_default(keyerr, (void *)&defval, NULL, sizeof(int)),
|
|
|
|
|
"with_default with a NULL destination");
|
|
|
|
|
keyerr = NULL;
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
if ( keyerr != NULL ) {
|
|
|
|
|
keyerr->handled = true;
|
|
|
|
|
keyerr = akerr_release_error(keyerr);
|
|
|
|
|
}
|
|
|
|
|
if ( typeerr != NULL ) {
|
|
|
|
|
typeerr->handled = true;
|
|
|
|
|
typeerr = akerr_release_error(typeerr);
|
|
|
|
|
}
|
|
|
|
|
} 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 {
|
Migrate to the libakerror 1.0.0 status registry
libakerror 1.0.0 replaced the consumer-sized status-name array with a
private registry and made status-code ownership explicit and enforced.
AKERR_MAX_ERR_VALUE and __AKERR_ERROR_NAMES are gone, and the registry
entry points raise akerr_ErrorContext * instead of returning int. See
deps/libakerror/UPGRADING.md.
The break was not only source-level. libakgl's codes sat at
AKERR_LAST_ERRNO_VALUE + 18 through + 22, and 1.0.0 claimed exactly
those five offsets for its own AKERR_STATUS_* registry codes, so every
AKGL_ERR_* was aliasing a libakerror status. HANDLE(e,
AKGL_ERR_LOGICINTERRUPT) at physics.c:222 would have swallowed a
foreign-name refusal.
- Move the band to AKERR_FIRST_CONSUMER_STATUS (256) as fixed offsets,
so a libc that grows an errno cannot move the codes, and add
AKGL_ERR_OWNER, AKGL_ERR_LIMIT and AKGL_ERR_COUNT to describe it.
- Reserve the range and register the names through the owned entry
points, PASS-ing each: these are AKERR_NOIGNORE, and the old
akerr_name_for_status calls discarded failure silently.
- Drop the AKERR_MAX_ERR_VALUE=256 compile definition.
- Guard on AKERR_FIRST_CONSUMER_STATUS in include/akgl/error.h, which
now includes <akerror.h> so the guard is reliable. The embedded build
is fine, but the find_package path can pick up a stale installed
header, and 1.0.0 has an soname, so that pairing is an ABI mismatch
rather than a compile problem. Same guard libakstdlib already carries.
Registration also moves out of akgl_heap_init into a new akgl_error_init
in src/error.c. It was in the heap pool's initializer only because that
was the first thing akgl_game_init called, and the upgrade turned five
fire-and-forget name calls into a library-wide ownership claim that can
fail. That placement was hiding a defect: game.c raises AKGL_ERR_SDL
when SDL_CreateMutex fails, five lines before akgl_heap_init ran, so the
earliest error path in the library was guaranteed to print "Unknown
Error". akgl_error_init is now the first statement in akgl_game_init.
Callers that drive subsystems directly must call akgl_error_init first;
it is idempotent, so ordering it precisely is not required. The eleven
test suites that relied on akgl_heap_init to name their statuses now
call it explicitly, or their failure messages would have degraded to
"Unknown Error".
Add tests/error.c: assert every code reads back its registered name,
that the name table and AKGL_ERR_COUNT agree, that a foreign owner is
refused with AKERR_STATUS_NAME_FOREIGN and AKERR_STATUS_RANGE_OVERLAP,
and that repeating the init is a no-op. That last one is a live
constraint, not a triviality -- libakerror treats only an identical
reservation as a repeat, so a subset or superset raises.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 22:20:28 -04:00
|
|
|
CATCH(errctx, akgl_error_init());
|
Namespace every exported symbol, and bump to 0.5.0
Closes internal-consistency items 1 through 6, 12 and 13. Every include guard
is _AKGL_<FILE>_H_, every in-project header include is angled, and every
exported function, type and global carries the akgl_ prefix. This is an ABI
break; the soname goes to libakgl.so.0.5. TODO.md carries the full rename
table.
The renames were driven by renaming each declaration and letting the compiler
find the uses, not by pattern substitution: renderer, physics and camera are
also parameter and struct-member names, and a sed would have rewritten
map->physics and every akgl_RenderBackend *renderer parameter without a word.
Item 4 turned out not to be cosmetic. The library exported a global called
renderer and tests/character.c defined an SDL_Renderer *renderer of its own;
the executable's definition preempted the library's, akgl_sprite_load_json
read a SDL_Renderer * through an akgl_RenderBackend *, and every texture load
in that suite failed. The suite reported success anyway, because libakerror's
unhandled-error handler ends in exit(errctx->status), exit keeps only the low
byte, and AKGL_ERR_SDL is exactly 256. So character had been green while
running one of its four tests, and every suite in the tree was unable to fail
on the most common status in a library built on SDL.
Both are fixed. tests/testutil.h gains TEST_TRAP_UNHANDLED_ERRORS(), which
collapses any status a byte cannot carry onto 1, and every suite installs it.
character binds a real backend with akgl_render_2d_bind. Its fourth test then
runs for the first time and fails on a defect it has asserted all along, so
akgl_heap_release_character now walks state_sprites with
AKGL_ITERATOR_OP_RELEASE and destroys the property set before zeroing the slot
-- TODO.md Defects item 21 and half of Carried over item 1.
AKGL_TIME_ONESEC_MS said "one second in milliseconds" and held 1000000, so
akgl_game_state_lock waited roughly sixteen minutes rather than one second. It
is AKGL_TIME_ONEMS_NS now, the budget is its own named constant, and
tests/game.c holds the mutex from a second thread to assert the wait -- the
contended path had no coverage at all.
Headers are self-contained and it is enforced: AKGL_PUBLIC_HEADERS drives both
install() and a generated translation unit per header, so a header that ships
is a header that is checked. Writing that found registry.h, which used
SDL_PropertiesID in eight declarations and included no SDL header.
23/23 suites pass, memcheck is clean, reindent --check is clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:32:21 -04:00
|
|
|
TEST_TRAP_UNHANDLED_ERRORS();
|
Add physics, heap, json_helpers, game, and actor test suites
Raise line coverage from 39.6 to 61.8 percent with four new suites and an
extension to the actor suite, and register every suite through a single
CMake list so a new test file cannot be left out of the coverage fixture.
Give the test targets a build-tree RPATH and prepend the build tree to
LD_LIBRARY_PATH for CTest, so a developer with a previously installed
libakgl.so exercises the library that was just compiled.
Fix six defects the new tests exposed:
- akgl_physics_simulate read self->gravity_time before its NULL check, so a
NULL backend crashed instead of reporting AKERR_NULLPOINTER.
- akgl_game_save transposed CLEANUP and PROCESS, which placed the fclose
inside the PROCESS switch. An ordinary save never flushed or closed its
stream and produced an empty file.
- akgl_game_save_actors wrote each name table terminator from the address of
a single char, emitting stack contents into the save file and a sentinel
the loader could not recognize.
- akgl_game_load_objectnamemap used CATCH directly inside while(1), where the
break leaves the loop rather than propagating, so a truncated name table
loaded as a successful game.
- akgl_Actor_cmhf_up_on and _down_on dereferenced actor->basechar with no
NULL check, unlike their left and right counterparts.
- akgl_actor_logic_movement checked actor twice instead of checking
actor->basechar before dereferencing it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:03:21 -04:00
|
|
|
CATCH(errctx, akgl_heap_init());
|
|
|
|
|
CATCH(errctx, akgl_registry_init());
|
|
|
|
|
CATCH(errctx, load_fixture());
|
|
|
|
|
|
Stop returning past CLEANUP, and validate the arguments every sibling validates
Closes internal-consistency items 16 and 17.
Ten *_RETURN macros sat inside ATTEMPT blocks, which return past CLEANUP and
skip every release in it. The one that mattered was the success path of
akgl_get_json_tilemap_property: it leaked two of the string pool's 256 entries
on every lookup that *found* what it was asked for, and a map load does that
several times per layer. tests/tilemap.c now runs each of its three paths --
found, absent, wrong type -- twice the pool size and asserts the pool is where
it started. Against the old code that test does not merely fail, it segfaults,
which is Defects item 30 seen from the outside: pool exhaustion arriving as a
NULL strncpy rather than as AKGL_ERR_HEAP.
Two of the conversions needed more than swapping the macro. In
akgl_get_json_tilemap_property a plain break would have fallen through to the
"property not found" FAIL_RETURN after FINISH, reporting a miss for something
found, so the success path sets a flag. In akgl_collide_rectangles the eight
early exits are followed by `*collide = false;`, which would have overwritten
the hit that broke out; each corner test writes the flag itself, so that line
is gone rather than moved. The same function also released its scratch string
once per loop iteration while continuing to use it, so the slot was free while
still live -- one claim now covers the whole scan.
akgl_controller_default is the other behavioural one: its SUCCEED_RETURN was
the last statement in the ATTEMPT block, so the path that falls out of FINISH
reached the closing brace of a non-void function.
scripts/check_error_protocol.py keeps both rules enforced -- a *_RETURN inside
ATTEMPT, and a return out of a HANDLE block -- as the error_protocol test.
Neither produces a compiler diagnostic and neither fails a test run until the
pool it drains is empty, which is why both have already shipped once.
For item 17: the eight typed JSON accessors that validated their container and
then wrote through dest unconditionally now check key and dest as the two
string accessors always did; the null physics backend checks its actors like
the arcade one; and akgl_render_2d_frame_start, _frame_end and _shutdown check
self, which the first two read straight through. tests/renderer.c calls all
three with NULL, which segfaulted before.
25/25 pass, memcheck clean, reindent --check clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:56:10 -04:00
|
|
|
CATCH(errctx, test_json_accessor_null_arguments());
|
Add physics, heap, json_helpers, game, and actor test suites
Raise line coverage from 39.6 to 61.8 percent with four new suites and an
extension to the actor suite, and register every suite through a single
CMake list so a new test file cannot be left out of the coverage fixture.
Give the test targets a build-tree RPATH and prepend the build tree to
LD_LIBRARY_PATH for CTest, so a developer with a previously installed
libakgl.so exercises the library that was just compiled.
Fix six defects the new tests exposed:
- akgl_physics_simulate read self->gravity_time before its NULL check, so a
NULL backend crashed instead of reporting AKERR_NULLPOINTER.
- akgl_game_save transposed CLEANUP and PROCESS, which placed the fclose
inside the PROCESS switch. An ordinary save never flushed or closed its
stream and produced an empty file.
- akgl_game_save_actors wrote each name table terminator from the address of
a single char, emitting stack contents into the save file and a sentinel
the loader could not recognize.
- akgl_game_load_objectnamemap used CATCH directly inside while(1), where the
break leaves the loop rather than propagating, so a truncated name table
loaded as a successful game.
- akgl_Actor_cmhf_up_on and _down_on dereferenced actor->basechar with no
NULL check, unlike their left and right counterparts.
- akgl_actor_logic_movement checked actor twice instead of checking
actor->basechar before dereferencing it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:03:21 -04:00
|
|
|
CATCH(errctx, test_json_scalar_accessors());
|
|
|
|
|
CATCH(errctx, test_json_double_value());
|
|
|
|
|
CATCH(errctx, test_json_string_accessor());
|
|
|
|
|
CATCH(errctx, test_json_array_index_accessors());
|
|
|
|
|
CATCH(errctx, test_json_type_and_key_errors());
|
|
|
|
|
CATCH(errctx, test_json_with_default());
|
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>
2026-08-01 00:32:11 -04:00
|
|
|
CATCH(errctx, test_json_string_accessor_reports_pool_exhaustion());
|
Add physics, heap, json_helpers, game, and actor test suites
Raise line coverage from 39.6 to 61.8 percent with four new suites and an
extension to the actor suite, and register every suite through a single
CMake list so a new test file cannot be left out of the coverage fixture.
Give the test targets a build-tree RPATH and prepend the build tree to
LD_LIBRARY_PATH for CTest, so a developer with a previously installed
libakgl.so exercises the library that was just compiled.
Fix six defects the new tests exposed:
- akgl_physics_simulate read self->gravity_time before its NULL check, so a
NULL backend crashed instead of reporting AKERR_NULLPOINTER.
- akgl_game_save transposed CLEANUP and PROCESS, which placed the fclose
inside the PROCESS switch. An ordinary save never flushed or closed its
stream and produced an empty file.
- akgl_game_save_actors wrote each name table terminator from the address of
a single char, emitting stack contents into the save file and a sentinel
the loader could not recognize.
- akgl_game_load_objectnamemap used CATCH directly inside while(1), where the
break leaves the loop rather than propagating, so a truncated name table
loaded as a successful game.
- akgl_Actor_cmhf_up_on and _down_on dereferenced actor->basechar with no
NULL check, unlike their left and right counterparts.
- akgl_actor_logic_movement checked actor twice instead of checking
actor->basechar before dereferencing it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:03:21 -04:00
|
|
|
} CLEANUP {
|
|
|
|
|
if ( fixture != NULL ) {
|
|
|
|
|
json_decref(fixture);
|
|
|
|
|
}
|
|
|
|
|
} PROCESS(errctx) {
|
|
|
|
|
} FINISH_NORETURN(errctx);
|
|
|
|
|
}
|