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>
369 lines
15 KiB
C
369 lines
15 KiB
C
/**
|
|
* @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_error_init());
|
|
TEST_TRAP_UNHANDLED_ERRORS();
|
|
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);
|
|
}
|