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>
This commit is contained in:
366
tests/json_helpers.c
Normal file
366
tests/json_helpers.c
Normal file
@@ -0,0 +1,366 @@
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
|
||||
// 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 {
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init());
|
||||
CATCH(errctx, load_fixture());
|
||||
|
||||
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());
|
||||
} CLEANUP {
|
||||
if ( fixture != NULL ) {
|
||||
json_decref(fixture);
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
Reference in New Issue
Block a user