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>
This commit is contained in:
2026-07-31 23:56:10 -04:00
parent 3a262bee54
commit 76ea04205c
14 changed files with 428 additions and 27 deletions

View File

@@ -316,6 +316,23 @@ add_test(
) )
set_tests_properties(api_surface PROPERTIES SKIP_RETURN_CODE 2) set_tests_properties(api_surface PROPERTIES SKIP_RETURN_CODE 2)
# The two akerror control-flow rules whose failure mode is silent: a *_RETURN
# inside an ATTEMPT block, and a return out of a HANDLE block. 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.
#
# The mutation target below wants the same interpreter; find_package caches, so
# asking here as well costs nothing and keeps this block self-contained.
find_package(Python3 COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
add_test(
NAME error_protocol
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/scripts/check_error_protocol.py
${CMAKE_CURRENT_SOURCE_DIR}/src
)
endif()
# One translation unit per public header, each including exactly that header and # One translation unit per public header, each including exactly that header and
# nothing before it. This is the only shape that proves a header is # nothing before it. This is the only shape that proves a header is
# self-contained: a second #include in the same file proves nothing about the # self-contained: a second #include in the same file proves nothing about the

145
scripts/check_error_protocol.py Executable file
View File

@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""Enforce the two akerror control-flow rules whose failure mode is silent.
AGENTS.md states both, and both have already cost this library a leak:
1. Never use a ``*_RETURN`` macro inside an ``ATTEMPT`` block. It returns past
``CLEANUP``, so every release, ``fclose`` and free in ``CLEANUP`` is
skipped. ``akgl_get_json_tilemap_property`` leaked two pooled strings on
every *successful* lookup this way.
2. Never ``return`` from inside a ``HANDLE`` block. ``FINISH`` is what carries
``RELEASE_ERROR``, so leaving early never gives the handled context back to
``AKERR_ARRAY_ERROR``. That cost ``akgl_path_relative`` one slot per call
and killed the process on the 129th.
Neither produces a compiler diagnostic, and neither shows up in a passing test
run until the pool it drains is empty. Hence a linter.
Usage: check_error_protocol.py <file-or-directory> [...]
Exits 0 when clean, 1 on a violation.
"""
import os
import re
import sys
# A ``*_RETURN`` macro invocation: SUCCEED_RETURN, FAIL_RETURN, FAIL_ZERO_RETURN,
# FAIL_NONZERO_RETURN, and anything later shaped like them.
RETURN_MACRO = re.compile(r"\b[A-Z][A-Z0-9_]*_RETURN\s*\(")
BARE_RETURN = re.compile(r"\breturn\b")
ATTEMPT_OPEN = re.compile(r"\bATTEMPT\s*\{")
CLEANUP_OPEN = re.compile(r"\}\s*CLEANUP\s*\{")
HANDLE_OPEN = re.compile(r"\}\s*HANDLE(?:_GROUP)?\s*\(")
FINISH_ANY = re.compile(r"\}\s*FINISH(?:_NORETURN)?\s*\(")
def strip_comments(text):
"""Blank out comments, keeping line numbering intact.
Comments are removed rather than skipped because several of these keywords
appear in prose -- "which should never return AKERR_KEY" is a sentence, not
a violation.
"""
out = []
i = 0
n = len(text)
while i < n:
ch = text[i]
if ch == '"' or ch == "'":
quote = ch
out.append(ch)
i += 1
while i < n:
if text[i] == "\\" and (i + 1) < n:
out.append(" ")
i += 2
continue
out.append(text[i])
if text[i] == quote:
i += 1
break
i += 1
continue
if text.startswith("/*", i):
j = text.find("*/", i + 2)
j = n if j == -1 else (j + 2)
out.append("".join(c if c == "\n" else " " for c in text[i:j]))
i = j
continue
if text.startswith("//", i):
j = text.find("\n", i)
j = n if j == -1 else j
out.append(" " * (j - i))
i = j
continue
out.append(ch)
i += 1
return "".join(out)
def check(path):
findings = []
lines = strip_comments(open(path, encoding="utf-8", errors="replace").read()).split("\n")
in_attempt = False
in_handle = False
for number, line in enumerate(lines, 1):
if ATTEMPT_OPEN.search(line):
in_attempt = True
continue
if CLEANUP_OPEN.search(line):
in_attempt = False
if HANDLE_OPEN.search(line):
in_handle = True
continue
if FINISH_ANY.search(line):
in_handle = False
continue
if in_attempt and RETURN_MACRO.search(line):
findings.append(
(number, "a *_RETURN macro inside an ATTEMPT block returns past CLEANUP; "
"use the matching *_BREAK variant"))
if in_handle and BARE_RETURN.search(line):
findings.append(
(number, "returning from inside a HANDLE block skips the RELEASE_ERROR that "
"FINISH carries; set a flag and act on it after FINISH"))
return findings
def sources(targets):
for target in targets:
if os.path.isdir(target):
for root, _dirs, files in os.walk(target):
for name in sorted(files):
if name.endswith(".c") and not name.endswith("~"):
yield os.path.join(root, name)
else:
yield target
def main(argv):
if len(argv) < 2:
print(__doc__, file=sys.stderr)
return 1
total = 0
for path in sources(argv[1:]):
for number, message in check(path):
print("%s:%d: %s" % (path, number, message), file=sys.stderr)
total += 1
if total:
print("", file=sys.stderr)
print("check_error_protocol: %d violation(s). See the Error-Handling Protocol "
"section of AGENTS.md." % total, file=sys.stderr)
return 1
print("check_error_protocol: clean")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))

View File

@@ -446,10 +446,10 @@ akerr_ErrorContext *akgl_controller_pushmap(int controlmapid, akgl_Control *cont
int newmapid = 0; int newmapid = 0;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
FAIL_ZERO_RETURN(errctx, control, AKERR_NULLPOINTER, "NULL Control"); FAIL_ZERO_BREAK(errctx, control, AKERR_NULLPOINTER, "NULL Control");
FAIL_NONZERO_RETURN(errctx, (controlmapid >= AKGL_MAX_CONTROL_MAPS), AKERR_OUTOFBOUNDS, "ID %d exceeds maximum %d", controlmapid, AKGL_MAX_CONTROL_MAPS); FAIL_NONZERO_BREAK(errctx, (controlmapid >= AKGL_MAX_CONTROL_MAPS), AKERR_OUTOFBOUNDS, "ID %d exceeds maximum %d", controlmapid, AKGL_MAX_CONTROL_MAPS);
newmapid = akgl_controlmaps[controlmapid].nextMap; newmapid = akgl_controlmaps[controlmapid].nextMap;
FAIL_ZERO_RETURN(errctx, (AKGL_MAX_CONTROLS - newmapid), AKERR_OUTOFBOUNDS, "Control map ID %d is full", controlmapid); FAIL_ZERO_BREAK(errctx, (AKGL_MAX_CONTROLS - newmapid), AKERR_OUTOFBOUNDS, "Control map ID %d is full", controlmapid);
memcpy((void *)&akgl_controlmaps[controlmapid].controls[newmapid], control, sizeof(akgl_Control)); memcpy((void *)&akgl_controlmaps[controlmapid].controls[newmapid], control, sizeof(akgl_Control));
akgl_controlmaps[controlmapid].nextMap = newmapid + 1; akgl_controlmaps[controlmapid].nextMap = newmapid + 1;
} CLEANUP { } CLEANUP {
@@ -466,7 +466,7 @@ akerr_ErrorContext *akgl_controller_default(int controlmapid, char *actorname, i
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
// set up the control map // set up the control map
FAIL_NONZERO_RETURN(errctx, (controlmapid >= AKGL_MAX_CONTROL_MAPS), AKERR_OUTOFBOUNDS, "ID %d exceeds maximum %d", controlmapid, AKGL_MAX_CONTROL_MAPS); FAIL_NONZERO_BREAK(errctx, (controlmapid >= AKGL_MAX_CONTROL_MAPS), AKERR_OUTOFBOUNDS, "ID %d exceeds maximum %d", controlmapid, AKGL_MAX_CONTROL_MAPS);
memset((void *)&control, 0x00, sizeof(akgl_Control)); memset((void *)&control, 0x00, sizeof(akgl_Control));
controlmap = &akgl_controlmaps[controlmapid]; controlmap = &akgl_controlmaps[controlmapid];
controlmap->kbid = kbid; controlmap->kbid = kbid;
@@ -542,12 +542,13 @@ akerr_ErrorContext *akgl_controller_default(int controlmapid, char *actorname, i
control.handler_on = &akgl_actor_cmhf_right_on; control.handler_on = &akgl_actor_cmhf_right_on;
control.handler_off = &akgl_actor_cmhf_right_off; control.handler_off = &akgl_actor_cmhf_right_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control)); CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
SUCCEED_RETURN(errctx);
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
// The SUCCEED_RETURN used to sit at the end of the ATTEMPT block, which
// returned past CLEANUP and left this function with no return statement at
// all on the path that falls out of FINISH.
SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akgl_controller_poll_key(int *keycode, bool *available) akerr_ErrorContext *akgl_controller_poll_key(int *keycode, bool *available)

View File

@@ -600,12 +600,12 @@ akerr_ErrorContext *akgl_game_load(char *fpath)
CATCH(e, read_exact((void *)&savegame, 1, sizeof(akgl_Game), fp)); CATCH(e, read_exact((void *)&savegame, 1, sizeof(akgl_Game), fp));
CATCH(e, akgl_game_load_versioncmp("library", (char *)&savegame.libversion, (char *)AKGL_VERSION)); CATCH(e, akgl_game_load_versioncmp("library", (char *)&savegame.libversion, (char *)AKGL_VERSION));
CATCH(e, akgl_game_load_versioncmp("game", (char *)&savegame.version, (char *)&akgl_game.version)); CATCH(e, akgl_game_load_versioncmp("game", (char *)&savegame.version, (char *)&akgl_game.version));
FAIL_NONZERO_RETURN( FAIL_NONZERO_BREAK(
e, e,
strncmp((char *)&savegame.name, (char *)&akgl_game.name, 256), strncmp((char *)&savegame.name, (char *)&akgl_game.name, 256),
AKERR_API, AKERR_API,
"Savegame is not compatible with this game"); "Savegame is not compatible with this game");
FAIL_NONZERO_RETURN( FAIL_NONZERO_BREAK(
e, e,
strncmp((char *)&savegame.uri, (char *)&akgl_game.uri, 256), strncmp((char *)&savegame.uri, (char *)&akgl_game.uri, 256),
AKERR_API, AKERR_API,

View File

@@ -17,6 +17,8 @@ akerr_ErrorContext *akgl_get_json_object_value(json_t *obj, char *key, json_t **
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_object_get(obj, key); json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key); FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_object(value)), AKERR_TYPE, "Key %s in object has incorrect type", key); FAIL_ZERO_RETURN(errctx, (json_is_object(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
@@ -28,6 +30,8 @@ akerr_ErrorContext *akgl_get_json_boolean_value(json_t *obj, char *key, bool *de
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_object_get(obj, key); json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key); FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_boolean(value)), AKERR_TYPE, "Key %s in object has incorrect type", key); FAIL_ZERO_RETURN(errctx, (json_is_boolean(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
@@ -39,6 +43,8 @@ akerr_ErrorContext *akgl_get_json_integer_value(json_t *obj, char *key, int *des
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_object_get(obj, key); json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key); FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), AKERR_TYPE, "Key %s in object has incorrect type", key); FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
@@ -50,6 +56,8 @@ akerr_ErrorContext *akgl_get_json_number_value(json_t *obj, char *key, float *de
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_object_get(obj, key); json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key); FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key); FAIL_ZERO_RETURN(errctx, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
@@ -61,6 +69,8 @@ akerr_ErrorContext *akgl_get_json_double_value(json_t *obj, char *key, double *d
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_object_get(obj, key); json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key); FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key); FAIL_ZERO_RETURN(errctx, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
@@ -73,8 +83,8 @@ akerr_ErrorContext *akgl_get_json_string_value(json_t *obj, char *key, akgl_Stri
json_t *value = NULL; json_t *value = NULL;
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
value = json_object_get(obj, key); value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key); FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
@@ -96,6 +106,8 @@ akerr_ErrorContext *akgl_get_json_array_value(json_t *obj, char *key, json_t **d
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_object_get(obj, key); json_t *value = json_object_get(obj, key);
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key); FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
FAIL_ZERO_RETURN(errctx, (json_is_array(value)), AKERR_TYPE, "Key %s in object has incorrect type", key); FAIL_ZERO_RETURN(errctx, (json_is_array(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
@@ -107,6 +119,7 @@ akerr_ErrorContext *akgl_get_json_array_index_object(json_t *array, int index, j
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_array_get(array, index); json_t *value = json_array_get(array, index);
FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index); FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
FAIL_ZERO_RETURN(errctx, (json_is_object(value)), AKERR_TYPE, "Index %d in object has incorrect type", index); FAIL_ZERO_RETURN(errctx, (json_is_object(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
@@ -118,6 +131,7 @@ akerr_ErrorContext *akgl_get_json_array_index_integer(json_t *array, int index,
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_array_get(array, index); json_t *value = json_array_get(array, index);
FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index); FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), AKERR_TYPE, "Index %d in object has incorrect type", index); FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
@@ -129,7 +143,7 @@ akerr_ErrorContext *akgl_get_json_array_index_string(json_t *array, int index, a
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference"); FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer reference"); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer");
json_t *value = json_array_get(array, index); json_t *value = json_array_get(array, index);
FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index); FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
FAIL_ZERO_RETURN(errctx, (json_is_string(value)), AKERR_TYPE, "Index %d in object has incorrect type", index); FAIL_ZERO_RETURN(errctx, (json_is_string(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);

View File

@@ -16,6 +16,7 @@ akerr_ErrorContext *akgl_physics_null_gravity(akgl_PhysicsBackend *self, akgl_Ac
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(e, actor, AKERR_NULLPOINTER, "actor");
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
@@ -23,13 +24,16 @@ akerr_ErrorContext *akgl_physics_null_collide(akgl_PhysicsBackend *self, akgl_Ac
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(e, a1, AKERR_NULLPOINTER, "a1");
FAIL_ZERO_RETURN(e, a2, AKERR_NULLPOINTER, "a2");
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext *akgl_physics_null_move(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt) akerr_ErrorContext *akgl_physics_null_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(e, actor, AKERR_NULLPOINTER, "actor");
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
@@ -77,7 +81,7 @@ akerr_ErrorContext *akgl_physics_arcade_collide(akgl_PhysicsBackend *self, akgl_
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext *akgl_physics_arcade_move(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt) akerr_ErrorContext *akgl_physics_arcade_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");

View File

@@ -68,12 +68,14 @@ akerr_ErrorContext *akgl_render_2d_bind(akgl_RenderBackend *self)
akerr_ErrorContext *akgl_render_2d_shutdown(akgl_RenderBackend *self) akerr_ErrorContext *akgl_render_2d_shutdown(akgl_RenderBackend *self)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
SUCCEED_RETURN(e); SUCCEED_RETURN(e);
} }
akerr_ErrorContext *akgl_render_2d_frame_start(akgl_RenderBackend *self) akerr_ErrorContext *akgl_render_2d_frame_start(akgl_RenderBackend *self)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(e, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend"); FAIL_ZERO_RETURN(e, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
SDL_SetRenderDrawColor(self->sdl_renderer, 0, 0, 0, 255); SDL_SetRenderDrawColor(self->sdl_renderer, 0, 0, 0, 255);
SDL_RenderClear(self->sdl_renderer); SDL_RenderClear(self->sdl_renderer);
@@ -83,6 +85,7 @@ akerr_ErrorContext *akgl_render_2d_frame_start(akgl_RenderBackend *self)
akerr_ErrorContext *akgl_render_2d_frame_end(akgl_RenderBackend *self) akerr_ErrorContext *akgl_render_2d_frame_end(akgl_RenderBackend *self)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
FAIL_ZERO_RETURN(e, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend"); FAIL_ZERO_RETURN(e, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
SDL_RenderPresent(self->sdl_renderer); SDL_RenderPresent(self->sdl_renderer);
SUCCEED_RETURN(e); SUCCEED_RETURN(e);

View File

@@ -30,6 +30,7 @@ akerr_ErrorContext *akgl_get_json_tilemap_property(json_t *obj, char *key, char
json_t *property = NULL; json_t *property = NULL;
akgl_String *tmpstr = NULL; akgl_String *tmpstr = NULL;
akgl_String *typestr = NULL; akgl_String *typestr = NULL;
bool found = false;
int i = 0; int i = 0;
// This is not a generic JSON helper. It assumes we are receiving an object with a 'properties' key // This is not a generic JSON helper. It assumes we are receiving an object with a 'properties' key
// inside of it. That key is an array of objects, and each object has a name, type, and value. // inside of it. That key is an array of objects, and each object has a name, type, and value.
@@ -39,9 +40,13 @@ akerr_ErrorContext *akgl_get_json_tilemap_property(json_t *obj, char *key, char
CATCH(errctx, akgl_get_json_array_value(obj, "properties", &properties)); CATCH(errctx, akgl_get_json_array_value(obj, "properties", &properties));
for (i = 0; i < json_array_size(properties); i++) { for (i = 0; i < json_array_size(properties); i++) {
CATCH(errctx, akgl_get_json_array_index_object(properties, i, &property)); CATCH(errctx, akgl_get_json_array_index_object(properties, i, &property));
// One scratch string for the whole scan. akgl_get_json_string_value
// reuses a non-NULL *dest without taking another reference, so
// releasing it per iteration -- which this used to do -- dropped the
// refcount to zero while the slot was still in use, and the next
// claim anywhere could have been handed the same one.
CATCH(errctx, akgl_get_json_string_value(property, "name", &tmpstr)); CATCH(errctx, akgl_get_json_string_value(property, "name", &tmpstr));
if ( strcmp(tmpstr->data, key) != 0 ) { if ( strcmp(tmpstr->data, key) != 0 ) {
CATCH(errctx, akgl_heap_release_string(tmpstr));
continue; continue;
} }
CATCH(errctx, akgl_get_json_string_value(property, "type", &typestr)); CATCH(errctx, akgl_get_json_string_value(property, "type", &typestr));
@@ -49,7 +54,8 @@ akerr_ErrorContext *akgl_get_json_tilemap_property(json_t *obj, char *key, char
FAIL_BREAK(errctx, AKERR_TYPE, "Property %s is present but is incorrect type(expected %s got %s)", key, type, (char *)typestr->data); FAIL_BREAK(errctx, AKERR_TYPE, "Property %s is present but is incorrect type(expected %s got %s)", key, type, (char *)typestr->data);
} }
*dest = property; *dest = property;
SUCCEED_RETURN(errctx); found = true;
break;
} }
} CLEANUP { } CLEANUP {
if ( tmpstr != NULL ) { if ( tmpstr != NULL ) {
@@ -61,6 +67,13 @@ akerr_ErrorContext *akgl_get_json_tilemap_property(json_t *obj, char *key, char
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
// The success path used to be a SUCCEED_RETURN from inside the ATTEMPT
// block, which returned past CLEANUP and leaked both scratch strings on
// every successful lookup. The pool is 256 entries and a map load does this
// many times per layer.
if ( found == true ) {
SUCCEED_RETURN(errctx);
}
FAIL_RETURN(errctx, AKERR_KEY, "Property not found in properties map"); FAIL_RETURN(errctx, AKERR_KEY, "Property not found in properties map");
} }
@@ -567,7 +580,7 @@ akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
dest->orientation = 0; dest->orientation = 0;
if ( (dest->width * dest->height) >= (AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT) ) { if ( (dest->width * dest->height) >= (AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT) ) {
FAIL_RETURN(errctx, AKERR_OUTOFBOUNDS, "Map exceeds the maximum size"); FAIL_BREAK(errctx, AKERR_OUTOFBOUNDS, "Map exceeds the maximum size");
} }
CATCH(errctx, akgl_tilemap_load_layers((akgl_Tilemap *)dest, (json_t *)json, dirnamestr)); CATCH(errctx, akgl_tilemap_load_layers((akgl_Tilemap *)dest, (json_t *)json, dirnamestr));

View File

@@ -73,7 +73,7 @@ static akerr_ErrorContext *path_relative_root(char *root, char *path, akgl_Strin
rootlen = strlen(root); rootlen = strlen(root);
pathlen = strlen(path); pathlen = strlen(path);
if ( (rootlen + pathlen) >= AKGL_MAX_STRING_LENGTH ) { if ( (rootlen + pathlen) >= AKGL_MAX_STRING_LENGTH ) {
FAIL_RETURN(e, AKERR_OUTOFBOUNDS, "Total path length (%d) is greater than maximum akgl_String length (%d)", (rootlen + pathlen), AKGL_MAX_STRING_LENGTH); FAIL_BREAK(e, AKERR_OUTOFBOUNDS, "Total path length (%d) is greater than maximum akgl_String length (%d)", (rootlen + pathlen), AKGL_MAX_STRING_LENGTH);
} }
DISABLE_GCC_WARNING_FORMAT_TRUNCATION DISABLE_GCC_WARNING_FORMAT_TRUNCATION
CATCH(e, aksl_snprintf( CATCH(e, aksl_snprintf(
@@ -180,41 +180,43 @@ akerr_ErrorContext *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *
// is the upper left corner of r1 contacting r2? // is the upper left corner of r1 contacting r2?
CATCH(errctx, akgl_collide_point_rectangle(&r1p.topleft, &r2p, collide)); CATCH(errctx, akgl_collide_point_rectangle(&r1p.topleft, &r2p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); } if ( *collide == true ) { break; }
// is the upper left corner of r2 contacting r1? // is the upper left corner of r2 contacting r1?
CATCH(errctx, akgl_collide_point_rectangle(&r2p.topleft, &r1p, collide)); CATCH(errctx, akgl_collide_point_rectangle(&r2p.topleft, &r1p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); } if ( *collide == true ) { break; }
// is the top right corner of r1 contacting r2? // is the top right corner of r1 contacting r2?
CATCH(errctx, akgl_collide_point_rectangle(&r1p.topright, &r2p, collide)); CATCH(errctx, akgl_collide_point_rectangle(&r1p.topright, &r2p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); } if ( *collide == true ) { break; }
// is the top right corner of r2 contacting r1? // is the top right corner of r2 contacting r1?
CATCH(errctx, akgl_collide_point_rectangle(&r2p.topright, &r1p, collide)); CATCH(errctx, akgl_collide_point_rectangle(&r2p.topright, &r1p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); } if ( *collide == true ) { break; }
// is the bottom left corner of r1 contacting r2? // is the bottom left corner of r1 contacting r2?
CATCH(errctx, akgl_collide_point_rectangle(&r1p.bottomleft, &r2p, collide)); CATCH(errctx, akgl_collide_point_rectangle(&r1p.bottomleft, &r2p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); } if ( *collide == true ) { break; }
// is the bottom left corner of r2 contacting r1? // is the bottom left corner of r2 contacting r1?
CATCH(errctx, akgl_collide_point_rectangle(&r2p.bottomleft, &r1p, collide)); CATCH(errctx, akgl_collide_point_rectangle(&r2p.bottomleft, &r1p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); } if ( *collide == true ) { break; }
// is the bottom right corner of r1 contacting r2? // is the bottom right corner of r1 contacting r2?
CATCH(errctx, akgl_collide_point_rectangle(&r1p.bottomright, &r2p, collide)); CATCH(errctx, akgl_collide_point_rectangle(&r1p.bottomright, &r2p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); } if ( *collide == true ) { break; }
// is the bottom right corner of r2 contacting r1? // is the bottom right corner of r2 contacting r1?
CATCH(errctx, akgl_collide_point_rectangle(&r2p.bottomright, &r1p, collide)); CATCH(errctx, akgl_collide_point_rectangle(&r2p.bottomright, &r1p, collide));
if ( *collide == true ) { SUCCEED_RETURN(errctx); } if ( *collide == true ) { break; }
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} FINISH(errctx, true); } FINISH(errctx, true);
*collide = false; // No trailing `*collide = false;` here. Each corner test writes the flag
// itself, so the eighth one leaves it false when nothing hit; assigning
// after FINISH would overwrite the hit that broke out of the block early.
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -48,6 +48,68 @@ static akerr_ErrorContext *load_fixture(void)
SUCCEED_RETURN(e); 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.
*/
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) akerr_ErrorContext *test_json_scalar_accessors(void)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
@@ -353,6 +415,7 @@ int main(void)
CATCH(errctx, akgl_registry_init()); CATCH(errctx, akgl_registry_init());
CATCH(errctx, load_fixture()); CATCH(errctx, load_fixture());
CATCH(errctx, test_json_accessor_null_arguments());
CATCH(errctx, test_json_scalar_accessors()); CATCH(errctx, test_json_scalar_accessors());
CATCH(errctx, test_json_double_value()); CATCH(errctx, test_json_double_value());
CATCH(errctx, test_json_string_accessor()); CATCH(errctx, test_json_string_accessor());

View File

@@ -145,6 +145,18 @@ akerr_ErrorContext *test_physics_null_backend_is_inert(void)
"null move with NULL self"); "null move with NULL self");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_null_collide(NULL, &actor, &reference), TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_null_collide(NULL, &actor, &reference),
"null collide with NULL self"); "null collide with NULL self");
// The actor arguments too. The arcade backend has always checked these;
// the null one checked only `self`, so which pointers a caller could get
// away with passing depended on which backend it happened to have.
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_null_gravity(&backend, NULL, 1.0f),
"null gravity with a NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_null_move(&backend, NULL, 1.0f),
"null move with a NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_null_collide(&backend, NULL, &reference),
"null collide with a NULL first actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_physics_null_collide(&backend, &actor, NULL),
"null collide with a NULL second actor");
} CLEANUP { } CLEANUP {
} PROCESS(e) { } PROCESS(e) {
} FINISH(e, true); } FINISH(e, true);

View File

@@ -92,6 +92,16 @@ akerr_ErrorContext *test_render_backend_without_a_renderer(void)
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, empty.frame_end(&empty), TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, empty.frame_end(&empty),
"ending a frame on a backend with no renderer"); "ending a frame on a backend with no renderer");
// And a NULL backend outright. frame_start and frame_end used to read
// self->sdl_renderer with no check on self at all, which is a segfault
// rather than an error, while draw_texture beside them checked it.
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_render_2d_frame_start(NULL),
"starting a frame on a NULL backend");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_render_2d_frame_end(NULL),
"ending a frame on a NULL backend");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_render_2d_shutdown(NULL),
"shutting down a NULL backend");
// Shutdown is the one that has nothing to release yet, so it succeeds. // Shutdown is the one that has nothing to release yet, so it succeeds.
TEST_EXPECT_OK(errctx, empty.shutdown(&empty), "shutting down an unused backend"); TEST_EXPECT_OK(errctx, empty.shutdown(&empty), "shutting down an unused backend");
} CLEANUP { } CLEANUP {

View File

@@ -20,6 +20,28 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <akerror.h> #include <akerror.h>
#include <akgl/error.h> #include <akgl/error.h>
#include <akgl/heap.h>
/**
* @brief How many pool string slots are currently claimed.
*
* The string pool is 256 entries and every loader borrows scratch strings from
* it, so "does this function give back what it took" is a question a test can
* answer directly by counting either side of a call. A leak that would take
* fifty map loads to show up in production shows up here on the first one.
*/
static inline int test_string_pool_used(void)
{
int used = 0;
int i = 0;
for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
if ( akgl_heap_strings[i].refcount != 0 ) {
used += 1;
}
}
return used;
}
/** /**
* @brief Exit with a status the shell can actually see. * @brief Exit with a status the shell can actually see.

View File

@@ -13,6 +13,100 @@
#include "testutil.h" #include "testutil.h"
/**
* @brief akgl_get_json_tilemap_property must give back its scratch strings on every path.
*
* It claims two pool strings per call -- one for the property name it is
* comparing, one for the declared type -- and used to leave the block through a
* `SUCCEED_RETURN` from inside `ATTEMPT` on the path where it *found* what it
* was asked for. That returns past `CLEANUP`, so the ordinary, successful
* lookup was the leaking one, at two of the pool's 256 entries a time. A map
* load does this several times per layer.
*
* Three paths, run more times than the pool has entries: found, absent, and
* present-but-wrong-type. Against the old code the found loop exhausts the pool
* and comes back AKGL_ERR_HEAP long before it finishes.
*/
akerr_ErrorContext *test_tilemap_property_lookup_releases_strings(void)
{
PREPARE_ERROR(errctx);
json_t *jsondoc = NULL;
json_error_t jsonerr;
akgl_String *pathstr = NULL;
int baseline = 0;
int propnum = 0;
int i = 0;
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&pathstr));
snprintf(
(char *)&pathstr->data,
AKGL_MAX_STRING_LENGTH,
"%s%s",
SDL_GetBasePath(),
"assets/snippets/test_tilemap_get_json_tilemap_property.json"
);
jsondoc = json_load_file((char *)&pathstr->data, 0, (json_error_t *)&jsonerr);
FAIL_ZERO_BREAK(errctx, jsondoc, AKERR_NULLPOINTER, "Failure loading json fixture: %s", (char *)jsonerr.text);
baseline = test_string_pool_used();
// The success path. Twice the pool size, so a leak of even one entry
// per call cannot finish.
for ( i = 0; i < (AKGL_MAX_HEAP_STRING * 2); i++ ) {
CATCH(errctx, akgl_get_json_properties_integer(jsondoc, "state", &propnum));
}
TEST_ASSERT(
errctx,
test_string_pool_used() == baseline,
"%d successful property lookups left %d pool strings claimed, expected %d",
(AKGL_MAX_HEAP_STRING * 2),
test_string_pool_used(),
baseline);
// The absent path, which reports AKERR_KEY from after FINISH.
for ( i = 0; i < (AKGL_MAX_HEAP_STRING * 2); i++ ) {
TEST_EXPECT_STATUS(
errctx,
AKERR_KEY,
akgl_get_json_properties_integer(jsondoc, "no_such_property", &propnum),
"looking up a property the object does not have");
}
TEST_ASSERT(
errctx,
test_string_pool_used() == baseline,
"absent-property lookups left %d pool strings claimed, expected %d",
test_string_pool_used(),
baseline);
// The wrong-type path, which fails from inside the ATTEMPT block with
// both scratch strings already claimed.
for ( i = 0; i < (AKGL_MAX_HEAP_STRING * 2); i++ ) {
TEST_EXPECT_STATUS(
errctx,
AKERR_TYPE,
akgl_get_json_properties_integer(jsondoc, "character", &propnum),
"reading a string property as an int");
}
TEST_ASSERT(
errctx,
test_string_pool_used() == baseline,
"wrong-type lookups left %d pool strings claimed, expected %d",
test_string_pool_used(),
baseline);
} CLEANUP {
if ( jsondoc != NULL ) {
json_decref(jsondoc);
jsondoc = NULL;
}
if ( pathstr != NULL ) {
IGNORE(akgl_heap_release_string(pathstr));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_tilemap_akgl_get_json_tilemap_property(void) akerr_ErrorContext *test_tilemap_akgl_get_json_tilemap_property(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
@@ -438,6 +532,7 @@ int main(void)
CATCH(errctx, akgl_character_load_json("assets/testcharacter.json")); CATCH(errctx, akgl_character_load_json("assets/testcharacter.json"));
CATCH(errctx, test_tilemap_akgl_get_json_tilemap_property()); CATCH(errctx, test_tilemap_akgl_get_json_tilemap_property());
CATCH(errctx, test_tilemap_property_lookup_releases_strings());
CATCH(errctx, test_akgl_tilemap_compute_tileset_offsets()); CATCH(errctx, test_akgl_tilemap_compute_tileset_offsets());
CATCH(errctx, test_akgl_tilemap_load_layer_objects()); CATCH(errctx, test_akgl_tilemap_load_layer_objects());
CATCH(errctx, test_akgl_tilemap_load_layer_tile()); CATCH(errctx, test_akgl_tilemap_load_layer_tile());