Record the 39.6 percent line and 44.3 percent function coverage baseline measured with the AKGL_COVERAGE build, and plan twelve test suites to raise it to roughly 70 percent lines and 80 percent functions. Tier the work by what each suite needs: pure logic suites for physics, json_helpers, heap, and savegame handling that require no renderer; renderer suites gated behind a shared offscreen harness; and controller and frame-loop coverage deferred. Note that vendored dependency shared objects are absent from the loader path in a fresh out-of-tree build, which reports every test as zero coverage, and record ten suspected defects found while reading the uncovered code so the new tests assert correct behavior rather than pinning the current one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
31 KiB
TODO
Coverage baseline
Generated 2026-07-30 with:
cmake -S . -B build-coverage -DAKGL_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
cmake --build build-coverage --parallel
ctest --test-dir build-coverage --output-on-failure
Reports land in build-coverage/coverage/ (index.html, coverage.xml).
Note: the vendored SDL/akerror/akstdlib/jansson shared objects are not on the
default loader path in a fresh out-of-tree build, so every test that links them
dies with error while loading shared libraries before main() runs. Export
LD_LIBRARY_PATH over build-coverage/deps/{SDL,SDL_image,SDL_ttf,SDL_mixer,libakerror,libakstdlib}
first, or the report will read 0% for reasons that have nothing to do with the
tests. Fixing this properly (see "Test infrastructure" below) is a prerequisite
for trustworthy CI coverage numbers.
Baseline with that fixed — character is the known intentionally-failing test,
everything else passes:
| File | Lines | Branches | Functions |
|---|---|---|---|
src/assets.c |
0/21 (0%) | 0/88 | 0/1 |
src/controller.c |
0/243 (0%) | 0/660 | 0/9 |
src/draw.c |
0/13 (0%) | 0/4 | 0/1 |
src/game.c |
0/224 (0%) | 0/1702 | 0/15 |
src/physics.c |
0/138 (0%) | 0/765 | 0/10 |
src/text.c |
0/28 (0%) | 0/146 | 0/2 |
src/renderer.c |
7/70 (10%) | 5/430 | 1/7 |
src/actor.c |
61/256 (24%) | 76/1020 | 4/18 |
src/tilemap.c |
201/426 (47%) | 209/3150 | 10/20 |
src/registry.c |
54/102 (53%) | 63/582 | 8/12 |
src/json_helpers.c |
86/111 (78%) | 73/710 | 9/11 |
src/character.c |
93/118 (79%) | 74/806 | 5/7 |
src/sprite.c |
88/101 (87%) | 112/810 | 4/5 |
src/util.c |
121/131 (92%) | 185/1057 | 7/8 |
src/staticstring.c |
16/17 (94%) | 9/64 | 2/2 |
src/heap.c |
111/116 (96%) | 87/266 | 12/12 |
| TOTAL | 838/2115 (39.6%) | 893/12260 (7.3%) | 44.3% |
The 7.3% branch figure is misleading and should not be used as a target. The
akerror control-flow macros (ATTEMPT/CATCH/PROCESS/FINISH,
FAIL_*_RETURN) expand into large branch trees per call site, most of which are
unreachable in normal operation — src/game.c reports 1702 branches across only
224 lines. Track line and function coverage; treat branch coverage as a
relative signal within a single file, not an absolute goal.
Goal for the work below: line coverage 39.6% → ~70%, function coverage 44.3% → ~80%, without adding any test that needs a real display.
Suspected defects found while reading the uncovered code
Write the tests below to assert correct behavior, so these fail loudly rather than getting baked in. Each is listed against the suite that will surface it.
akgl_render_and_compare()compares a texture against itself.src/util.c:228andsrc/util.c:245both drawt1;t2is never rendered. The function therefore always passes, which silently neuters the image assertions intests/sprite.c. → Suite U2.akgl_tilemap_release()double-frees tileset textures.src/tilemap.c:849destroysdest->tilesets[i].textureinside the layers loop; it should bedest->layers[i].texture. It also never NULLs the pointers or memsets the map, so a second release is a use-after-free. → Suite T3.akgl_physics_simulate()dereferencesselfbefore the NULL check.src/physics.c:132readsself->gravity_timeat declaration time;FAIL_ZERO_RETURN(e, self, ...)is not until line 135. PassingNULLsegfaults instead of returningAKERR_NULLPOINTER. → Suite P4.akgl_path_relative_from()is a stub that leaks.src/util.c:105acquiresdirnamestrfrom the string heap, never writes*dst, and never releases the string. Calling itAKGL_MAX_HEAP_STRING(256) times exhausts the heap. → Suite U1.akgl_registry_init()never initializes the properties registry.akgl_registry_init_properties()is not called fromakgl_registry_init()(src/registry.c:27), soAKGL_REGISTRY_PROPERTIESstays 0. Everyakgl_set_property()is then a silent no-op against property set 0 andakgl_get_property()always returns the caller's default — which meansakgl_physics_init_arcade()andakgl_render_init2d()silently ignore configuration. → Suite R1.akgl_compare_sdl_surfaces()memcmps without checking geometry.src/util.c:208comparess1->pitch * s1->hbytes ofs2without verifying the two surfaces share dimensions, pitch, or format. → Suite U2.- Heap acquire functions are asymmetric.
akgl_heap_next_string()incrementsrefcount;next_actor,next_sprite,next_spritesheet, andnext_characterdo not (src/heap.c:57-121). Callers that acquire-then-release a non-string object drive the refcount negative-ish or free a live object. → Suite H1; decide whether to fix or document. tests/util.cdefinestest_akgl_collide_point_rectangle_logic()butmain()never calls it (tests/util.c:134vs.tests/util.c:312-317). One-line fix; do it as part of Suite U3.akgl_actor_logic_movement()null-checks the wrong pointer.src/actor.c:130checksactortwice; the message saysactor->basecharbutbasecharis never validated before it is dereferenced on the next line. → Suite A2.akgl_Actor_cmhf_up_on()/_down_on()dereferencebasecharunguarded.src/actor.c:385and:413readobj->basechar->aywith noFAIL_ZERO_RETURNonbasechar, unlike their left/right counterparts (:326,:355). → Suite A1.
Tier 1 — new suites, no renderer required
These are pure logic or property-map code. They need akgl_heap_init() and the
relevant akgl_registry_init_*() calls, nothing else. Highest coverage per unit
of effort.
Suite P — tests/physics.c → target test_physics, CTest name physics
Covers src/physics.c (0% → target ~85%). 10 uncovered functions.
- P1
test_physics_null_backend—akgl_physics_init_null()installsakgl_physics_null_{gravity,collide,move}andakgl_physics_simulate; assert each field is the expected function pointer. AssertAKERR_NULLPOINTERforinit_null(NULL)and for each null-backend function called withself == NULL. Assert the null gravity/move functions leave a populated actor'sx/y/z,ex/ey/ez, andvx/vy/vzbit-identical. - P2
test_physics_arcade_gravity— table-driven overgravity_{x,y,z}∈ {0, positive, negative} ×dt∈ {0, 0.5, 1.0}. Assert the sign conventions insrc/physics.c:56-67:ex -= gx*dt,ey += gy*dt,ez -= gz*dt, and that a zero component leaves its axis untouched. AssertAKERR_NULLPOINTERfor nullselfand nullactor. - P3
test_physics_arcade_move— assertx += vx*dton all three axes for positive, negative, and zerodt; assertdt == 0is a no-op. Assertakgl_physics_arcade_collide()returnsAKERR_API("Not implemented") so the stub is pinned until it is written. - P4
test_physics_simulate_iteration— the big one. Build a small heap by hand (akgl_heap_init(), thenakgl_heap_next_actor()+akgl_actor_initialize()), attach a stubmovementlogicfunc, and driveakgl_physics_simulate()with a null backend to isolate the loop. Assert:- actors with
refcount == 0are skipped; - actors with
basechar == NULLare skipped; - a child actor (
parent != NULL) getsx = parent->x + vxand does not go through gravity/move; AKGL_ITERATOR_OP_LAYERMASKskips actors whoselayer != opflags->layerid, andopflags == NULLselects the documented defaults;- thrust clamps: set
txbeyondsxin both directions and asserttx == ±sxexactly (src/physics.c:174-194), including theszaxis which has a clamp but no accumulate; MOVING_LEFT/MOVING_RIGHTaccumulatetx += ax*dt,MOVING_UP/_DOWNaccumulatety += ay*dt, and neither fires for an idle actor;- drag:
ex -= ex*drag_x*dtfor nonzero drag, untouched for zero drag; vx == ex + txafter the pass;- a
movementlogicfuncthat raisesAKGL_ERR_LOGICINTERRUPTis swallowed and the loop continues to the next actor. - Defect 3:
akgl_physics_simulate(NULL, NULL)must returnAKERR_NULLPOINTER, not crash.
- actors with
- P5
test_physics_factory—"null"and"arcade"dispatch to the right initializer; an unknown name returnsAKERR_KEY; nullselfand nulltypereturnAKERR_NULLPOINTER. Add cases for thestrncmpprefix matching insrc/physics.c:232-239:"nullx"and"arcade_extra"currently match — pin whichever behavior is intended. - P6
test_physics_arcade_properties— withAKGL_REGISTRY_PROPERTIESinitialized andphysics.gravity.y/physics.drag.xset viaakgl_set_property(), assertakgl_physics_init_arcade()reads them into the backend. Assert the documented defaults (0.0) when unset. Depends on Suite R1 resolving defect 5.
Suite R — extend tests/registry.c
Covers akgl_registry_init_properties, akgl_set_property, akgl_get_property,
akgl_registry_load_properties (src/registry.c 53% → target ~90%).
- R1
test_registry_properties_roundtrip—akgl_registry_init_properties()then set/get a value; assert the default is returned for a missing key and the stored value wins for a present key. Defect 5: assert that a plainakgl_registry_init()leaves properties usable — this is expected to fail untilakgl_registry_init_properties()is added toakgl_registry_init(). - R2
test_registry_property_nullpointers—akgl_set_property(NULL, x),(x, NULL),akgl_get_property(NULL, ...),(x, NULL, ...)allAKERR_NULLPOINTER. Assertakgl_get_propertywith*dest == NULLallocates from the string heap (src/registry.c:178) and with a preallocated*destreuses it. - R3
test_registry_load_properties— new fixturetests/assets/snippets/test_properties.jsonwith apropertiesobject of several string values. Assert every key lands in the registry. Error cases:NULLfilename →AKERR_NULLPOINTER; nonexistent file → error not crash; a file with nopropertieskey →AKERR_KEY; apropertiesvalue that is a number rather than a string →AKERR_TYPE. Also assert the loop atsrc/registry.c:148-158does not leak the string heap: record free slots before and after and require them equal. - R4
test_registry_init_actor_state_strings— assert allAKGL_ACTOR_MAX_STATES(32) names fromAKGL_ACTOR_STATE_STRING_NAMESmap to1 << i, and that a name not in the table reads back as 0. - R5
test_registry_init_idempotent—akgl_registry_init_actor()destroys and recreates on re-init (src/registry.c:47) while the other seven initializers leak their previousSDL_PropertiesID. Pin the intended behavior; if the leak is unintended, this is the test that catches the fix.
Suite J — extend tests/ for src/json_helpers.c
New file tests/json_helpers.c → target test_json_helpers, CTest name
json_helpers (78% → target ~95%). Currently json_helpers has no dedicated
suite; it is covered only incidentally through sprite/character/tilemap loading.
- J1
test_json_double_value—akgl_get_json_double_value()is entirely uncovered. Assert value retrieval,AKERR_KEYfor a missing key,AKERR_TYPEfor a string value,AKERR_NULLPOINTERfor a null object. Assert it accepts both a JSON integer and a JSON real (it usesjson_is_number). - J2
test_json_with_default—akgl_get_json_with_default()is entirely uncovered and is the odd one out: it takes a prior error context. Assert that aNULLerror is a no-op success; that anAKERR_KEYerror copiesdefsizebytes fromdefvalintodest; that anAKERR_INDEX-group error does the same; and that an unrelated error (e.g.AKERR_TYPE) is not swallowed. Assert nulldefval/destwith a non-null error →AKERR_NULLPOINTER. - J3
test_json_string_heap_allocation— for bothakgl_get_json_string_value()andakgl_get_json_array_index_string(), assert the*dest == NULLpath acquires and initializes a heap string (src/json_helpers.c:83,:137) and the non-null path overwrites in place. Assert a JSON string longer thanAKGL_MAX_STRING_LENGTHis truncated rather than overflowing (strncpyat:91does not NUL-terminate on truncation — assert the intended behavior explicitly). - J4
test_json_array_index_bounds— negative index, index == length, and index > length all returnAKERR_OUTOFBOUNDSfor the object/integer/string variants. Wrong element type returnsAKERR_TYPE. - J5
test_json_helpers_nullpointers— sweep all 11 entry points with nullobj, nullkey, and nulldest. Several currently omit thekey/destchecks thatakgl_get_json_string_value()has; pin what each should do.
Fixture: one tests/assets/snippets/test_json_helpers.json holding an object
with a string, integer, real, boolean, nested object, and a mixed-type array.
Suite H — extend tests/ for src/heap.c
New file tests/heap.c → target test_heap, CTest name heap (96% lines but
the five uncovered lines are every exhaustion path).
- H1
test_heap_exhaustion— for each pool, claim every slot and assert the next acquire returnsAKGL_ERR_HEAP: actors (AKGL_MAX_HEAP_ACTOR= 64), sprites (1024), spritesheets (1024), characters (256), strings (256). Because onlyakgl_heap_next_string()increments the refcount (defect 7), the non-string loops must bumprefcountby hand — write the test to make that asymmetry explicit and add an assertion recording which behavior is intended. - H2
test_heap_release_refcounting— release below zero is clamped (refcount > 0guard); release at 1 zeroes the object and clears its registry entry; release of a still-referenced object leaves data intact. - H3
test_heap_release_actor_children— an actor with children releases them recursively (src/heap.c:132-136); assert a child shared by two parents is not double-released, and that a fullAKGL_ACTOR_MAX_CHILDREN(8) set is walked. - H4
test_heap_release_nullpointers— all five release functions returnAKERR_NULLPOINTERforNULL. - H5
test_heap_init_clears— dirty every pool, callakgl_heap_init(), assert all slots are zeroed andakgl_heap_init_actor()alone clears only actors.
Suite S — extend tests/staticstring.c
- S1
test_string_copy_count— the only uncovered line in the file is thestrncpyfailure branch (src/staticstring.c:32), which is unreachable; instead pin thecount == 0→AKGL_MAX_STRING_LENGTHdefault (:28) versus an explicit shortcount, and assert a short count does not NUL-terminate (currentstrncpysemantics) so callers know. - S2
test_string_initialize_truncation— initialize with a string longer thanAKGL_MAX_STRING_LENGTH(PATH_MAX) and assert the documented truncation. Assert theinit == NULLbranch zeroesdataand still setsrefcount = 1— notesrc/staticstring.c:17memsetssizeof(akgl_String)starting at&obj->data, which writes pastdataifdatais not the last member; assert the struct's trailing fields survive.
Suite A — extend tests/actor.c
Covers the 14 uncovered actor functions (24% → target ~75%). Everything except
akgl_actor_render and actor_visible is renderer-free.
- A1
test_actor_control_map_handlers— the eightakgl_Actor_cmhf_*functions. For each_on: assertFACE_ALL | MOVING_ALLis cleared, the correctMOVING_*andFACE_*bits are set, andax/aypicks up±basechar->ax/ay. For each_off: assertax/ex/tx/vx(or theyset) are zeroed and only the matchingMOVING_*bit is cleared — note_offdoes not clear theFACE_*bit, which is deliberate; pin it. AssertAKERR_NULLPOINTERfor null actor and null event on all eight. Defect 10:up_on/down_onwithbasechar == NULLmust returnAKERR_NULLPOINTERrather than segfault. - A2
test_actor_logic_movement— assertsx/sy/szare copied frombasechar, and the fourMOVING_*states select the rightax/aysign. Assert the "neither left nor right" case leavesaxat its prior value (current behavior — there is no else branch). Defect 9: nullbasecharmust returnAKERR_NULLPOINTER. - A3
test_actor_logic_changeframe— pure state machine, table-driven over (loop,loopReverse,curSpriteReversing,curSpriteFrameId,frames): forward advance mid-animation; wrap to 0 at the end when not reversing; enter reverse at the end whenloop && loopReverse; leave reverse at frame 0; single-frame sprite (frames == 1);frames == 0(currently underflowscurSpriteFrameId— assert the intended guard). Assert null actor →AKERR_NULLPOINTER; notecurSpriteis dereferenced unchecked atsrc/actor.c:96, so add a null-curSpritecase too. - A4
test_actor_automatic_face—movement_controls_face == falseleaves state untouched;trueclearsFACE_ALLand applies the documented left > right > up > down precedence; simultaneous left+up resolves to left; no movement bits leaves no face bits. - A5
test_actor_update— with a character/sprite pair wired up, assertchangeframefuncfires only whencurtime - curSpriteFrameTimer >= speed, thatcurSpriteFrameTimeris then updated, and that a state with no bound sprite (AKERR_KEYfromakgl_character_sprite_get) is swallowed as success (src/actor.c:167). Null actor and nullbasechar→AKERR_NULLPOINTER. - A6
test_actor_add_child_limits— extend the existing coverage: filling all 8 slots then adding a 9th returnsAKERR_OUTOFBOUNDS; adding a child that already has a parent returnsAKERR_RELATIONSHIP; the child'srefcountis incremented exactly once. - A7
test_registry_iterate_actor_flags— driveakgl_registry_iterate_actor()with eachAKGL_ITERATOR_OP_*combination via stubupdatefunc/renderfuncthat record calls. AssertLAYERMASKfilters,UPDATEandRENDERfire independently, and theelsebranch resetsscaleto 1.0 whenTILEMAPSCALEis absent. Note thebreakatsrc/actor.c:303exits theATTEMPTblock — assert it skips the actor rather than aborting the enumeration.
Suite C — extend tests/character.c
Covers akgl_character_sprite_get and akgl_character_state_sprites_iterate
(79% → target ~95%). The existing character test is the intentionally-failing
one; add these as separate functions so they can be enabled independently.
- C1
test_character_sprite_get— add a sprite for a composite state and read it back; assert an unbound state returnsAKERR_KEY; assert nullbasecharand nulldestreturnAKERR_NULLPOINTER; assert state 0 and a negative state are handled (SDL_itoarenders both). - C2
test_character_state_sprites_iterate— withAKGL_ITERATOR_OP_RELEASEset, assert each bound sprite's refcount drops by one; without the flag, assert nothing changes; assert a nulluserdataand a nullnameare logged and skipped rather than crashing. - C3
test_character_load_json_errors— new snippet fixtures for: asprite_mappingsentry naming a sprite absent from the registry (AKERR_NULLPOINTER,src/character.c:163); astatearray containing an unknown state name (AKERR_KEY,:115); a missingspeedtimekey. Assert that on failure the character heap slot is released and the registry entry is not left dangling. Notesrc/character.c:223logsobj->nameafter the cleanup block has potentially zeroed it — assert the failure path does not read freed data.
Suite G — tests/game.c → target test_game, CTest name game
Covers the renderer-free half of src/game.c (0% → target ~45%). Skip
akgl_game_init, akgl_game_update, akgl_game_lowfps, and akgl_game_updateFPS
for now; they need a window and a running loop.
- G1
test_game_load_versioncmp— pure function, entirely testable. Equal versions succeed; differing major/minor/patch returnAKERR_API; a malformed current version and a malformed save version each returnAKERR_VALUEwith the right message; all three null arguments returnAKERR_NULLPOINTER. Assertsemver_free()runs on both paths (no leak across 1000 iterations). - G2
test_game_save_load_roundtrip— write to a temp path withakgl_game_save(), read back withakgl_game_load(), assertgameis restored bit-identical. Then assert the rejection paths: a save whosenamediffers, whoseuridiffers, and whoselibversiondiffers each returnAKERR_API. Null path →AKERR_NULLPOINTER; unwritable path → IO error. - G3
test_game_save_actors_tables— with a couple of actors, sprites, spritesheets, and characters registered, save and then parse the resulting file by hand: assert each of the four name→pointer tables is present, in order, and terminated by the NUL name + NUL pointer sentinel thatakgl_game_load_objectnamemap()looks for. - G4
test_game_load_objectnamemap_truncation— feed a deliberately truncated table (EOF before the sentinel). Thewhile (1)loop atsrc/game.c:340relies onaksl_freadraising; assert it returns an error rather than looping forever. Give this test an aggressive CTestTIMEOUT. - G5
test_game_state_lock— assertakgl_game_state_lock()/_unlock()pair correctly and that a double-lock and an unlock-without-lock behave as documented.
Suite U — extend tests/util.c
- U1
test_path_relative_from—akgl_path_relative_from()is entirely uncovered. Assert it writes*dst, that the result is the dirname of the resolvedfrom, and that nullpath/fromreturnAKERR_NULLPOINTER. Defect 4: this is expected to fail — the function never assigns*dstand leaks its heap string. Add a loop of 300 calls asserting the string heap does not run dry. - U2
test_render_and_compare_detects_difference— build two textures with deliberately different pixels and assertakgl_render_and_compare()reportsAKERR_VALUE. Defect 1: expected to fail today. Addtest_compare_sdl_surfaces_geometry: surfaces with mismatchedw/h/pitchmust be rejected before the memcmp (defect 6). This suite needs a renderer — see Tier 2 harness. - U3
test_path_relative_errors—akgl_path_relative()with a path that resolves in the CWD, a path that only resolves underroot, and a path that resolves under neither (assertENOENTpropagates). Assertakgl_path_relative_root()returnsAKERR_OUTOFBOUNDSwhenstrlen(root) + strlen(path) >= AKGL_MAX_STRING_LENGTH, and that both functions release their heap strings on the failure path. Defect 8: wire the existingtest_akgl_collide_point_rectangle_logic()intomain().
Tier 2 — suites that need the offscreen renderer harness
Test infrastructure (do this first)
- Fix the loader path. Set
BUILD_RPATH/INSTALL_RPATHon the test targets to the vendored dependency directories, or addENVIRONMENT_MODIFICATION "LD_LIBRARY_PATH=path_list_prepend:..."to theset_tests_properties()block inCMakeLists.txt:141. Without this, CI coverage numbers are meaningless (see the note at the top). - Add
tests/harness.c/tests/harness.hwithakgl_test_init_headless()andakgl_test_shutdown_headless(): setSDL_VIDEODRIVER=dummyandSDL_AUDIODRIVER=dummy,SDL_Init(),akgl_heap_init(),akgl_registry_init(), create a softwareSDL_CreateWindowAndRendererand point the globalrendererat it. Four existing tests hand-roll this today (tests/sprite.c:194,tests/character.c:200,tests/tilemap.c:421,tests/charviewer.c:42) — collapse them onto the shared harness in the same change. - Add a
tests/assets/snippets/fixture per new JSON case rather than embedding JSON in C string literals, matching the existingtest_tilemap_get_json_tilemap_property.jsonconvention.
Suite T — extend tests/tilemap.c
Covers the 10 uncovered tilemap functions (47% → target ~75%).
- T1
test_tilemap_properties_numeric—akgl_get_json_properties_number,_float, and_doubleare all uncovered. Assert each reads the righttypediscriminator; noteakgl_get_json_properties_double()asks for type"float"(src/tilemap.c:138) — pin whether that is intended. Assert a type mismatch returnsAKERR_TYPEand a missing property returnsAKERR_KEY. - T2
test_tilemap_scale_actor— pure math, no renderer. Three branches atsrc/tilemap.c:824-830:y <= p_vanishing_y→p_vanishing_scale;y >= p_foreground_y→p_foreground_scale; between → the linear interpolation. Assert both exact boundaries and a midpoint. Null map and null actor →AKERR_NULLPOINTER. - T3
test_tilemap_release— defect 2. Loadtests/assets/testmap.tmj, release, and assert every tileset and layer texture pointer is NULLed and the map is safe to release a second time. Under the current implementation the layers loop frees tileset textures again; this test should fail until fixed. Run it under ASan if available. - T4
test_tilemap_load_physics—akgl_tilemap_load_physics()is uncovered. Fixture snippets for a map with a full physics property block, one with the block absent (assert documented defaults), and one with a malformed value. - T5
test_tilemap_load_layer_image— uncovered. Fixture with an image layer; assert the texture loads, the layer is recorded, and a missing image file yieldsAKGL_ERR_SDLrather than a null-texture crash later. - T6
test_tilemap_load_bounds— assert a map declaring more thanAKGL_TILEMAP_MAX_TILESETS(16) tilesets orAKGL_TILEMAP_MAX_LAYERS(16) layers returnsAKERR_OUTOFBOUNDSrather than writing past the arrays. - T7
test_tilemap_draw— with the dummy renderer, assertakgl_tilemap_draw()andakgl_tilemap_draw_tileset()complete for each layer index, that an out-of-rangelayeridxis rejected, and that a viewport entirely outside the map draws nothing without erroring.
Suite N — tests/renderer.c → target test_renderer, CTest name renderer
Covers src/renderer.c (10% → target ~70%).
- N1
test_render_init2d— withgame.screenwidth/screenheightproperties set, assertakgl_render_init2d()populates all six function pointers and setscamerato{0, 0, w, h}. Assert nullself→AKERR_NULLPOINTERand that the two heap strings it acquires are released (src/renderer.c:31-32) even on the failure path — they currently are not, becausePASSreturns early onSDL_CreateWindowAndRendererfailure. - N2
test_render_frame_start_end— assertAKERR_NULLPOINTERwhenself->sdl_rendereris NULL; assert success against the dummy renderer. - N3
test_render_2d_draw_texture— the rotated path (angle != 0) is uncovered: assertangle != 0withcenter == NULLreturnsAKERR_NULLPOINTER, and that a valid rotation succeeds. Assert nullselfand nulltextureare rejected. - N4
test_render_2d_draw_mesh— pin theAKERR_API"Not implemented" stub. - N5
test_render_2d_draw_world— assert layer ordering (tilemap layer i drawn before the actors on layer i), thatrefcount == 0actors are skipped, and thatopflags == NULLzero-initializes the default iterator. Notedefflagsatsrc/renderer.c:113is uninitialized until theifbody runs — assert the non-null-opflagspath does not read it.
Suite X — tests/text.c, tests/draw.c, tests/assets.c
Small files, 0% each, cheap wins once the harness exists.
- X1
test_text_loadfont— load a TTF from a newtests/assets/fixture; assert it lands inAKGL_REGISTRY_FONT, that a missing file errors, and that a null name/path is rejected. - X2
test_text_rendertextat— render into the dummy renderer; assert a null font and null text are rejected and thatwraplength0 vs. positive both work. - X3
test_draw_background—akgl_draw_background()returnsvoidand cannot report failure; assert it does not crash for zero, negative, and oversized dimensions. - X4
test_load_start_bgm— with the dummy audio driver, assert a valid file loads intoAKGL_REGISTRY_MUSIC, a missing file errors, and a null filename is rejected.
Tier 3 — deferred
Suite K — tests/controller.c → target test_controller
src/controller.c is 0% across 9 functions and 243 lines, but every entry point
is driven by real SDL gamepad/keyboard events. Worth doing, but only after
Tier 1 and 2 land.
- Synthesize
SDL_Eventstructs directly and callakgl_controller_handle_event()— no physical device needed forSDL_EVENT_KEY_DOWN/_UP, andSDL_EVENT_GAMEPAD_ADDED/_REMOVEDcan be pushed onto the SDL event queue. akgl_controller_pushmap()andakgl_controller_default()are the most testable: assert a control map is registered and retrievable, that an out-of-rangecontrolmapidis rejected, and thatakgl_controller_default()binds the eightakgl_Actor_cmhf_*handlers to the expected keys.akgl_controller_list_keyboards()and_open_gamepads()should at minimum be asserted not to crash with zero devices attached.
Deferred src/game.c entry points
akgl_game_init, akgl_game_update, akgl_game_updateFPS, and
akgl_game_lowfps need a window and a live frame loop. Revisit once Suite N
proves the dummy renderer is stable enough to run a bounded number of frames.
Registration checklist
Each new suite needs, in CMakeLists.txt:
add_executable(test_<feature> tests/<feature>.c)
add_test(NAME <feature> COMMAND test_<feature>)
target_link_libraries(test_<feature> PRIVATE akstdlib::akstdlib akerror::akerror akgl
SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
and its name added to the set_tests_properties(...) list at
CMakeLists.txt:141 and to the FIXTURES_REQUIRED akgl_coverage list at
CMakeLists.txt:167 — a suite missing from the second list runs outside the
coverage fixture and its counters are discarded by coverage_reset.
New suites in dependency order: physics, json_helpers, heap, game,
renderer, text, draw, assets, controller.
Carried over
-
Make character-to-sprite state bindings release their references symmetrically.
akgl_character_sprite_add()increments the sprite refcount for every state-map binding, but no corresponding removal API exists. Replacing a state binding also leaves the previous sprite's refcount incremented. Whenakgl_heap_release_character()drops the character refcount to zero, it clears the character registry entry and zeroes the structure without enumeratingstate_sprites, decrementing the bound sprites, or destroying the SDL property map. Implement binding removal/replacement so each removed binding releases exactly one sprite reference. On final character release, enumerate every remaining binding (the existingakgl_character_state_sprites_iterate()release path may be reusable), release each reference, destroystate_sprites, and then clear the character. Add tests for removal, replacement, duplicate sprite bindings across multiple states, and final character release.Suites C2 and H2 above are the coverage side of this item; the API work is still outstanding.