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