Files
libakgl/tests/json_helpers.c
Tachikoma eabb9ad376
Some checks failed
libakgl CI Build / cmake_build (push) Successful in 9m7s
libakgl CI Build / performance (push) Successful in 9m44s
libakgl CI Build / mutation_test (push) Has been cancelled
libakgl CI Build / memory_check (push) Has been cancelled
Move outstanding work from TODO.md into the issue tracker
TODO.md carried two records in one file: what had been done, with the
measurements behind it, and what was left. The second half is what a tracker
is for, and keeping it here has already cost something -- AGENTS.md records a
round where eleven entries described code that had already changed, and this
file admitted to three more.

Every open item is now an issue on source.starfort.tech/andrew/libakgl,
labelled by kind and blast radius and milestoned by what it can land in: 0.9.x
for anything that breaks no ABI, 0.10.0 for new or changed public symbols,
1.0.0 for the design work. Four are epics: the performance plan (#60),
coverage (#61), actor rotation (#62), and the false header comments (#63).

Verified against the tree before filing rather than transcribed. Three entries
were already fixed and were not filed: the akgl_path_relative context leak, the
akgl_draw_background test extension, and the SDL enumeration audit -- keyboards,
gamepads and mappings are all freed in CLEANUP today. Two were reworded because
the code had moved: the fonts item is a missing teardown entry point rather than
a missing API, since akgl_text_unloadallfonts exists, and draw_world's tilemap
call is already bounded by numlayers, so only the per-layer actor rescan remains.

TODO.md keeps the part a tracker has no place for: why a decision went the way
it did, what the measurement was, and which arguments turned out to be wrong.

TODO.txt is deleted. Four of its eight entries had shipped -- actor-to-actor
collision, actor-to-world collision, automatic facing, image layers -- and the
four that had not are #74 through #77, with the GPU renderer's research links
kept because that is the part that took the time.

Every reference that named an item number or a moved section is repointed, in
the manual, the headers, the tests and the examples.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 18:47:34 -04:00

550 lines
23 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);
}
/**
* @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.
*/
/**
* @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 the tilemap string-pool leak looked like from the outside: not
* "the tilemap loader leaks five strings a load", but a crash somewhere else
* entirely, fifty levels later.
*/
akerr_ErrorContext *test_json_string_accessor_reports_pool_exhaustion(void)
{
PREPARE_ERROR(e);
akgl_String *claimed[AKGL_MAX_HEAP_STRING];
akgl_String *dest = NULL;
json_t *strings = NULL;
int held = 0;
int i = 0;
memset(&claimed, 0x00, sizeof(claimed));
ATTEMPT {
CATCH(e, akgl_get_json_array_value(fixture, "strings", &strings));
// Take every slot the pool has. Whatever else is holding one already
// simply means this stops sooner.
while ( held < AKGL_MAX_HEAP_STRING ) {
akerr_ErrorContext *claim = akgl_heap_next_string(&claimed[held]);
if ( claim != NULL ) {
claim->handled = true;
claim = akerr_release_error(claim);
claimed[held] = NULL;
break;
}
held += 1;
}
TEST_ASSERT(e, held > 0, "could not claim any pool strings");
TEST_ASSERT(e, test_string_pool_used() == AKGL_MAX_HEAP_STRING,
"the string pool is not full: %d of %d claimed",
test_string_pool_used(), AKGL_MAX_HEAP_STRING);
// dest is NULL, so both accessors have to claim -- and cannot.
dest = NULL;
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP,
akgl_get_json_string_value(fixture, "name", &dest),
"reading a string with the pool exhausted");
TEST_ASSERT(e, dest == NULL,
"a refused string accessor wrote to its destination");
dest = NULL;
TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP,
akgl_get_json_array_index_string(strings, 0, &dest),
"reading an array string with the pool exhausted");
TEST_ASSERT(e, dest == NULL,
"a refused array string accessor wrote to its destination");
} CLEANUP {
for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
if ( claimed[i] != NULL ) {
IGNORE(akgl_heap_release_string(claimed[i]));
}
}
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_json_accessor_null_arguments(void)
{
PREPARE_ERROR(e);
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);
}
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;
akerr_ErrorContext *defaulted = NULL;
json_t *shortarray = NULL;
json_t *junkobj = 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;
// 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);
// 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());
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
CATCH(errctx, load_fixture());
CATCH(errctx, test_json_accessor_null_arguments());
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());
CATCH(errctx, test_json_string_accessor_reports_pool_exhaustion());
} CLEANUP {
if ( fixture != NULL ) {
json_decref(fixture);
}
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}