diff --git a/CMakeLists.txt b/CMakeLists.txt index c2db4e4..e9dbcf2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -127,6 +127,7 @@ set(AKGL_TEST_SUITES actor bitmasks character + controller game heap json_helpers diff --git a/TODO.md b/TODO.md index 691b070..d9088eb 100644 --- a/TODO.md +++ b/TODO.md @@ -1,8 +1,8 @@ # TODO -## Coverage baseline +## Coverage status -Generated 2026-07-30 with: +Generated with: ```sh cmake -S . -B build-coverage -DAKGL_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug @@ -12,528 +12,199 @@ 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. +**Line coverage 72.2%, function coverage 78.6%** (1534/2125 lines), up from a +39.6% / 44.3% baseline. `character` is the one intentionally failing suite; +everything else passes. -Baseline with that fixed — `character` is the known intentionally-failing test, -everything else passes: +| File | Lines | Functions | +|---|---|---| +| `src/actor.c` | 205/258 (80%) | 16/18 | +| `src/assets.c` | 0/21 (0%) | 0/1 | +| `src/character.c` | 104/118 (88%) | 6/7 | +| `src/controller.c` | 220/243 (90%) | 9/9 | +| `src/draw.c` | 0/13 (0%) | 0/1 | +| `src/game.c` | 124/230 (54%) | 10/15 | +| `src/heap.c` | 116/116 (100%) | 12/12 | +| `src/json_helpers.c` | 111/111 (100%) | 11/11 | +| `src/physics.c` | 140/140 (100%) | 10/10 | +| `src/registry.c` | 76/102 (74%) | 11/12 | +| `src/renderer.c` | 7/70 (10%) | 1/7 | +| `src/sprite.c` | 93/101 (92%) | 5/5 | +| `src/staticstring.c` | 16/17 (94%) | 2/2 | +| `src/text.c` | 0/28 (0%) | 0/2 | +| `src/tilemap.c` | 201/426 (47%) | 10/20 | +| `src/util.c` | 121/131 (92%) | 7/8 | -| 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%** | +Branch coverage reads 18.6% 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 them unreachable in normal +operation — `src/game.c` reports over 1700 branches across 230 lines. Track line +and function coverage; treat branch coverage as a relative signal within a file. -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. +### Suites -Goal for the work below: **line coverage 39.6% → ~70%, function coverage 44.3% → -~80%**, without adding any test that needs a real display. +Every suite is registered through the `AKGL_TEST_SUITES` list in +`CMakeLists.txt`, which drives target creation, CTest registration, the +`WORKING_DIRECTORY`/`TIMEOUT` properties, the link line, and the +`FIXTURES_REQUIRED akgl_coverage` list together. Adding `tests/.c` and the +name to that list is all a new suite needs; it can no longer be accidentally +left out of the coverage fixture. ---- +Shared assertion helpers are in `tests/testutil.h`: `TEST_ASSERT`, +`TEST_ASSERT_FEQ`, `TEST_EXPECT_STATUS`, `TEST_EXPECT_OK`, `TEST_EXPECT_ANY_ERROR`, +and `TEST_ASSERT_FLAG`. All except the last expand to a `break` on failure, so +they belong directly inside an `ATTEMPT` block, not inside a loop nested in one. -## Suspected defects found while reading the uncovered code +Done: -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. +- `tests/physics.c` — both backends, the factory, and the full simulation loop + including thrust clamping, drag, layer masking, parent/child positioning, and + logic-interrupt handling. 100%. +- `tests/heap.c` — pool exhaustion for all five pools, refcount clamping, + recursive child release, registry cleanup on release. 100%. +- `tests/json_helpers.c` — every typed accessor, both string accessors' + allocate-vs-reuse paths, array bounds, and `akgl_get_json_with_default`. 100%. +- `tests/controller.c` — control map push and capacity, the default binding set, + keyboard and gamepad dispatch including cross-device rejection, the dpad + handlers, and device add/remove against the dummy drivers. 90%. +- `tests/game.c` — version gating, the save/load roundtrip, foreign-save + rejection, truncated-table detection, the state lock, and FPS accounting. 54%. +- `tests/actor.c` — extended with the eight control-map handlers, automatic + facing, movement logic, the animation frame state machine, `akgl_actor_update`, + and character/sprite binding lookups. 80%. -1. **`akgl_render_and_compare()` compares a texture against itself.** - `src/util.c:228` and `src/util.c:245` both draw `t1`; `t2` is never rendered. - The function therefore always passes, which silently neuters the image - assertions in `tests/sprite.c`. → Suite U2. -2. **`akgl_tilemap_release()` double-frees tileset textures.** +## Remaining work + +### Needs the offscreen renderer harness + +`src/renderer.c` (63 lines), `src/text.c` (28), `src/draw.c` (13), +`src/assets.c` (21), `akgl_actor_render`/`actor_visible` in `src/actor.c` (53), +and the drawing half of `src/tilemap.c` all need a live `renderer` global. + +Build `tests/harness.c` / `tests/harness.h` with `akgl_test_init_headless()` and +`akgl_test_shutdown_headless()`: set the dummy video and audio drivers, +`SDL_Init()`, `akgl_heap_init()`, `akgl_registry_init()`, create a software +`SDL_CreateWindowAndRenderer`, and point the global `renderer` at 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. The new suites set the driver hints inline and need +no window, so they are unaffected. + +Then: + +- **`tests/renderer.c`** — `akgl_render_init2d` populating all six function + pointers and the camera; frame start/end against a NULL `sdl_renderer`; the + rotated `draw_texture` path including `angle != 0` with a NULL center; the + `draw_mesh` "not implemented" stub; and `draw_world` layer ordering. Note + `defflags` at `src/renderer.c:113` is uninitialized until the `if` body runs. +- **`tests/text.c`**, **`tests/draw.c`**, **`tests/assets.c`** — font loading + into `AKGL_REGISTRY_FONT`, text rendering with wrap on and off, background + drawing at zero/negative/oversized dimensions, and BGM loading into + `AKGL_REGISTRY_MUSIC` under the dummy audio driver. +- **`tests/tilemap.c` extensions** — `akgl_tilemap_draw`, `_draw_tileset`, and + `akgl_tilemap_load_layer_image`. + +### Does not need a renderer + +- **`src/tilemap.c`** — `akgl_tilemap_scale_actor` is pure math over three + branches (`src/tilemap.c:824-830`); `akgl_get_json_properties_number`, + `_float`, and `_double` need only a JSON snippet; `akgl_tilemap_load_physics` + needs a fixture with the physics property block present, absent, and + malformed. Together roughly 100 of the 225 uncovered lines. +- **`src/game.c`** — `akgl_game_init`, `akgl_game_update`, `akgl_game_lowfps`, + and `akgl_game_updateFPS`'s frame loop need a window; revisit after the + harness lands. +- **`src/registry.c`** — `akgl_registry_load_properties` needs a fixture with a + `properties` object, plus the missing-file, missing-key, and wrong-value-type + cases. Assert the loop at `src/registry.c:148-158` does not leak the string + heap. + +## Defects + +### Fixed while building the suites + +Each was found by a test written to assert correct behavior. + +1. **`akgl_physics_simulate` dereferenced `self` before its NULL check.** + `src/physics.c:132` read `self->gravity_time` at declaration time, three + lines above `FAIL_ZERO_RETURN(e, self, ...)`. A NULL backend segfaulted. +2. **`akgl_game_save` never flushed or closed its stream.** `CLEANUP` and + `PROCESS` were transposed, which put the `fclose` inside the `PROCESS` + switch, where it only ran if an error context existed and reported success. + An ordinary save produced an empty file. +3. **`akgl_game_save_actors` wrote name-table terminators from a single char.** + `aksl_fwrite((void *)&nullval, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp)` emitted + 127 bytes of adjacent stack memory into the save file and produced a sentinel + the loader could not recognize. +4. **`akgl_game_load_objectnamemap` swallowed read failures.** `CATCH` used + directly inside `while (1)` breaks the loop, not the function, so a truncated + or corrupt name table loaded as a successful game. +5. **`akgl_Actor_cmhf_up_on` and `_down_on` dereferenced `basechar` unguarded**, + unlike their left and right counterparts. +6. **`akgl_actor_logic_movement` checked `actor` twice** instead of checking + `actor->basechar` before dereferencing it. +7. **The gamepad handlers checked `appstate` three times each**, so a NULL event + or a missing player actor was never caught and `player->state` was + dereferenced regardless. + +### Known and still open + +1. **`akgl_render_and_compare` compares a texture against itself.** + `src/util.c:228` and `src/util.c:245` both draw `t1`; `t2` is never rendered, + so the function always passes and the image assertions in `tests/sprite.c` + assert nothing. +2. **`akgl_tilemap_release` double-frees tileset textures.** `src/tilemap.c:849` destroys `dest->tilesets[i].texture` inside the *layers* - loop; it should be `dest->layers[i].texture`. It also never NULLs the pointers - or memsets the map, so a second release is a use-after-free. → Suite T3. -3. **`akgl_physics_simulate()` dereferences `self` before the NULL check.** - `src/physics.c:132` reads `self->gravity_time` at declaration time; - `FAIL_ZERO_RETURN(e, self, ...)` is not until line 135. Passing `NULL` - segfaults instead of returning `AKERR_NULLPOINTER`. → Suite P4. -4. **`akgl_path_relative_from()` is a stub that leaks.** - `src/util.c:105` acquires `dirnamestr` from the string heap, never writes - `*dst`, and never releases the string. Calling it `AKGL_MAX_HEAP_STRING` (256) - times exhausts the heap. → Suite U1. -5. **`akgl_registry_init()` never initializes the properties registry.** + loop; it should be `dest->layers[i].texture`. It also never NULLs the + pointers, so a second release is a use-after-free. +3. **`akgl_registry_init` never initializes the properties registry.** `akgl_registry_init_properties()` is not called from `akgl_registry_init()` - (`src/registry.c:27`), so `AKGL_REGISTRY_PROPERTIES` stays 0. Every - `akgl_set_property()` is then a silent no-op against property set 0 and - `akgl_get_property()` always returns the caller's default — which means - `akgl_physics_init_arcade()` and `akgl_render_init2d()` silently ignore - configuration. → Suite R1. -6. **`akgl_compare_sdl_surfaces()` memcmps without checking geometry.** + (`src/registry.c:27`), so `AKGL_REGISTRY_PROPERTIES` stays 0 unless the + caller initializes it separately. `akgl_set_property` is then a silent no-op + and `akgl_get_property` always returns the caller's default, which means + `akgl_physics_init_arcade` and `akgl_render_init2d` silently ignore + configuration. `akgl_game_init` does call it; a caller that does not use + `akgl_game_init` does not get it. +4. **`akgl_path_relative_from` is a stub that leaks.** `src/util.c:105` claims a + heap string, never writes `*dst`, and never releases it. 256 calls exhaust + the string pool. +5. **`akgl_compare_sdl_surfaces` memcmps without checking geometry.** `src/util.c:208` compares `s1->pitch * s1->h` bytes of `s2` without verifying - the two surfaces share dimensions, pitch, or format. → Suite U2. -7. **Heap acquire functions are asymmetric.** - `akgl_heap_next_string()` increments `refcount`; `next_actor`, `next_sprite`, - `next_spritesheet`, and `next_character` do 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. -8. **`tests/util.c` defines `test_akgl_collide_point_rectangle_logic()` but - `main()` never calls it** (`tests/util.c:134` vs. `tests/util.c:312-317`). - One-line fix; do it as part of Suite U3. -9. **`akgl_actor_logic_movement()` null-checks the wrong pointer.** - `src/actor.c:130` checks `actor` twice; the message says `actor->basechar` - but `basechar` is never validated before it is dereferenced on the next line. - → Suite A2. -10. **`akgl_Actor_cmhf_up_on()` / `_down_on()` dereference `basechar` unguarded.** - `src/actor.c:385` and `:413` read `obj->basechar->ay` with no - `FAIL_ZERO_RETURN` on `basechar`, unlike their left/right counterparts - (`:326`, `:355`). → Suite A1. + the surfaces share dimensions, pitch, or format. +6. **`akgl_string_initialize` overflows by four bytes when `init` is NULL.** + `src/staticstring.c:17` does `memset(&obj->data, 0x00, sizeof(akgl_String))`, + but `data` starts four bytes into the struct (after `refcount`), so the memset + runs four bytes past the end. +7. **Savegame name lengths disagree between writer and reader.** + `akgl_game_save_actors` writes spritesheet names at + `AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH` (512) and character names at + `AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH` (128), but `akgl_game_load` reads + every table at `AKGL_ACTOR_MAX_NAME_LENGTH` (128). A save with any registered + spritesheet cannot be read back. The current roundtrip test passes only + because empty registries write nothing but zeroed sentinels. +8. **Heap acquire functions are asymmetric.** `akgl_heap_next_string` increments + `refcount`; `next_actor`, `next_sprite`, `next_spritesheet`, and + `next_character` do not. `tests/heap.c` pins the current behavior and says so; + decide whether to make them symmetric or document the split. +9. **`tests/util.c` defines `test_akgl_collide_point_rectangle_logic` but + `main()` never calls it.** +10. **`controller.h` declares functions that do not exist.** It declares + `akgl_controller_handle_button_down`, `_button_up`, `_added`, and `_removed`, + but `src/controller.c` defines them as `gamepad_handle_*`. Anything compiled + against the header alone fails to link. +11. **`akgl_controller_pushmap` and `akgl_controller_default` accept negative map + ids.** Both check `controlmapid >= AKGL_MAX_CONTROL_MAPS` but not + `controlmapid < 0`, so a negative id indexes before `GAME_ControlMaps`. ---- +## Build notes -## 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()` installs - `akgl_physics_null_{gravity,collide,move}` and `akgl_physics_simulate`; assert - each field is the expected function pointer. Assert `AKERR_NULLPOINTER` for - `init_null(NULL)` and for each null-backend function called with `self == NULL`. - Assert the null gravity/move functions leave a populated actor's `x/y/z`, - `ex/ey/ez`, and `vx/vy/vz` bit-identical. -- **P2 `test_physics_arcade_gravity`** — table-driven over - `gravity_{x,y,z}` ∈ {0, positive, negative} × `dt` ∈ {0, 0.5, 1.0}. Assert the - sign conventions in `src/physics.c:56-67`: `ex -= gx*dt`, `ey += gy*dt`, - `ez -= gz*dt`, and that a zero component leaves its axis untouched. Assert - `AKERR_NULLPOINTER` for null `self` and null `actor`. -- **P3 `test_physics_arcade_move`** — assert `x += vx*dt` on all three axes for - positive, negative, and zero `dt`; assert `dt == 0` is a no-op. Assert - `akgl_physics_arcade_collide()` returns `AKERR_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()`, then `akgl_heap_next_actor()` + `akgl_actor_initialize()`), - attach a stub `movementlogicfunc`, and drive `akgl_physics_simulate()` with a - null backend to isolate the loop. Assert: - - actors with `refcount == 0` are skipped; - - actors with `basechar == NULL` are skipped; - - a child actor (`parent != NULL`) gets `x = parent->x + vx` and does **not** - go through gravity/move; - - `AKGL_ITERATOR_OP_LAYERMASK` skips actors whose `layer != opflags->layerid`, - and `opflags == NULL` selects the documented defaults; - - thrust clamps: set `tx` beyond `sx` in both directions and assert - `tx == ±sx` exactly (`src/physics.c:174-194`), including the `sz` axis which - has a clamp but no accumulate; - - `MOVING_LEFT`/`MOVING_RIGHT` accumulate `tx += ax*dt`, `MOVING_UP`/`_DOWN` - accumulate `ty += ay*dt`, and neither fires for an idle actor; - - drag: `ex -= ex*drag_x*dt` for nonzero drag, untouched for zero drag; - - `vx == ex + tx` after the pass; - - a `movementlogicfunc` that raises `AKGL_ERR_LOGICINTERRUPT` is swallowed and - the loop continues to the next actor. - - **Defect 3**: `akgl_physics_simulate(NULL, NULL)` must return - `AKERR_NULLPOINTER`, not crash. -- **P5 `test_physics_factory`** — `"null"` and `"arcade"` dispatch to the right - initializer; an unknown name returns `AKERR_KEY`; null `self` and null `type` - return `AKERR_NULLPOINTER`. Add cases for the `strncmp` prefix matching in - `src/physics.c:232-239`: `"nullx"` and `"arcade_extra"` currently match — pin - whichever behavior is intended. -- **P6 `test_physics_arcade_properties`** — with `AKGL_REGISTRY_PROPERTIES` - initialized and `physics.gravity.y` / `physics.drag.x` set via - `akgl_set_property()`, assert `akgl_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 plain - `akgl_registry_init()` leaves properties usable — this is expected to fail - until `akgl_registry_init_properties()` is added to `akgl_registry_init()`. -- **R2 `test_registry_property_nullpointers`** — `akgl_set_property(NULL, x)`, - `(x, NULL)`, `akgl_get_property(NULL, ...)`, `(x, NULL, ...)` all - `AKERR_NULLPOINTER`. Assert `akgl_get_property` with `*dest == NULL` allocates - from the string heap (`src/registry.c:178`) and with a preallocated `*dest` - reuses it. -- **R3 `test_registry_load_properties`** — new fixture - `tests/assets/snippets/test_properties.json` with a `properties` object of - several string values. Assert every key lands in the registry. Error cases: - `NULL` filename → `AKERR_NULLPOINTER`; nonexistent file → error not crash; - a file with no `properties` key → `AKERR_KEY`; a `properties` value that is a - number rather than a string → `AKERR_TYPE`. Also assert the loop at - `src/registry.c:148-158` does not leak the string heap: record free slots - before and after and require them equal. -- **R4 `test_registry_init_actor_state_strings`** — assert all - `AKGL_ACTOR_MAX_STATES` (32) names from `AKGL_ACTOR_STATE_STRING_NAMES` map to - `1 << 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 previous `SDL_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_KEY` for a missing key, `AKERR_TYPE` - for a string value, `AKERR_NULLPOINTER` for a null object. Assert it accepts - both a JSON integer and a JSON real (it uses `json_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 a `NULL` error is a no-op success; that an `AKERR_KEY` error copies - `defsize` bytes from `defval` into `dest`; that an `AKERR_INDEX`-group error - does the same; and that an unrelated error (e.g. `AKERR_TYPE`) is *not* - swallowed. Assert null `defval`/`dest` with a non-null error → - `AKERR_NULLPOINTER`. -- **J3 `test_json_string_heap_allocation`** — for both - `akgl_get_json_string_value()` and `akgl_get_json_array_index_string()`, - assert the `*dest == NULL` path 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 than `AKGL_MAX_STRING_LENGTH` is truncated rather - than overflowing (`strncpy` at `:91` does not NUL-terminate on truncation — - assert the intended behavior explicitly). -- **J4 `test_json_array_index_bounds`** — negative index, index == length, and - index > length all return `AKERR_OUTOFBOUNDS` for the object/integer/string - variants. Wrong element type returns `AKERR_TYPE`. -- **J5 `test_json_helpers_nullpointers`** — sweep all 11 entry points with null - `obj`, null `key`, and null `dest`. Several currently omit the `key`/`dest` - checks that `akgl_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 returns `AKGL_ERR_HEAP`: actors (`AKGL_MAX_HEAP_ACTOR` = 64), - sprites (1024), spritesheets (1024), characters (256), strings (256). Because - only `akgl_heap_next_string()` increments the refcount (**defect 7**), the - non-string loops must bump `refcount` by 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 > 0` guard); 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 full `AKGL_ACTOR_MAX_CHILDREN` (8) set is - walked. -- **H4 `test_heap_release_nullpointers`** — all five release functions return - `AKERR_NULLPOINTER` for `NULL`. -- **H5 `test_heap_init_clears`** — dirty every pool, call `akgl_heap_init()`, - assert all slots are zeroed and `akgl_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 the - `strncpy` failure branch (`src/staticstring.c:32`), which is unreachable; - instead pin the `count == 0` → `AKGL_MAX_STRING_LENGTH` default - (`:28`) versus an explicit short `count`, and assert a short count does not - NUL-terminate (current `strncpy` semantics) so callers know. -- **S2 `test_string_initialize_truncation`** — initialize with a string longer - than `AKGL_MAX_STRING_LENGTH` (`PATH_MAX`) and assert the documented - truncation. Assert the `init == NULL` branch zeroes `data` and still sets - `refcount = 1` — note `src/staticstring.c:17` memsets `sizeof(akgl_String)` - starting at `&obj->data`, which writes past `data` if `data` is 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 eight `akgl_Actor_cmhf_*` - functions. For each `_on`: assert `FACE_ALL | MOVING_ALL` is cleared, the - correct `MOVING_*` and `FACE_*` bits are set, and `ax`/`ay` picks up - `±basechar->ax/ay`. For each `_off`: assert `ax/ex/tx/vx` (or the `y` set) are - zeroed and only the matching `MOVING_*` bit is cleared — note `_off` does not - clear the `FACE_*` bit, which is deliberate; pin it. Assert - `AKERR_NULLPOINTER` for null actor and null event on all eight. - **Defect 10**: `up_on`/`down_on` with `basechar == NULL` must return - `AKERR_NULLPOINTER` rather than segfault. -- **A2 `test_actor_logic_movement`** — assert `sx/sy/sz` are copied from - `basechar`, and the four `MOVING_*` states select the right `ax`/`ay` sign. - Assert the "neither left nor right" case leaves `ax` at its prior value - (current behavior — there is no else branch). **Defect 9**: null `basechar` - must return `AKERR_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 when `loop && loopReverse`; leave reverse at frame 0; - single-frame sprite (`frames == 1`); `frames == 0` (currently underflows - `curSpriteFrameId` — assert the intended guard). Assert null actor → - `AKERR_NULLPOINTER`; note `curSprite` is dereferenced unchecked at - `src/actor.c:96`, so add a null-`curSprite` case too. -- **A4 `test_actor_automatic_face`** — `movement_controls_face == false` leaves - state untouched; `true` clears `FACE_ALL` and 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, assert - `changeframefunc` fires only when `curtime - curSpriteFrameTimer >= speed`, - that `curSpriteFrameTimer` is then updated, and that a state with no bound - sprite (`AKERR_KEY` from `akgl_character_sprite_get`) is swallowed as success - (`src/actor.c:167`). Null actor and null `basechar` → `AKERR_NULLPOINTER`. -- **A6 `test_actor_add_child_limits`** — extend the existing coverage: filling - all 8 slots then adding a 9th returns `AKERR_OUTOFBOUNDS`; adding a child that - already has a parent returns `AKERR_RELATIONSHIP`; the child's `refcount` is - incremented exactly once. -- **A7 `test_registry_iterate_actor_flags`** — drive - `akgl_registry_iterate_actor()` with each `AKGL_ITERATOR_OP_*` combination via - stub `updatefunc`/`renderfunc` that record calls. Assert `LAYERMASK` filters, - `UPDATE` and `RENDER` fire independently, and the `else` branch resets - `scale` to 1.0 when `TILEMAPSCALE` is absent. Note the `break` at - `src/actor.c:303` exits the `ATTEMPT` block — 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 returns `AKERR_KEY`; assert null - `basechar` and null `dest` return `AKERR_NULLPOINTER`; assert state 0 and a - negative state are handled (`SDL_itoa` renders both). -- **C2 `test_character_state_sprites_iterate`** — with `AKGL_ITERATOR_OP_RELEASE` - set, assert each bound sprite's refcount drops by one; without the flag, - assert nothing changes; assert a null `userdata` and a null `name` are logged - and skipped rather than crashing. -- **C3 `test_character_load_json_errors`** — new snippet fixtures for: a - `sprite_mappings` entry naming a sprite absent from the registry - (`AKERR_NULLPOINTER`, `src/character.c:163`); a `state` array containing an - unknown state name (`AKERR_KEY`, `:115`); a missing `speedtime` key. Assert - that on failure the character heap slot is released and the registry entry is - not left dangling. Note `src/character.c:223` logs `obj->name` after 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 return `AKERR_API`; a - malformed current version and a malformed save version each return - `AKERR_VALUE` with the right message; all three null arguments return - `AKERR_NULLPOINTER`. Assert `semver_free()` runs on both paths (no leak across - 1000 iterations). -- **G2 `test_game_save_load_roundtrip`** — write to a temp path with - `akgl_game_save()`, read back with `akgl_game_load()`, assert `game` is - restored bit-identical. Then assert the rejection paths: a save whose `name` - differs, whose `uri` differs, and whose `libversion` differs each return - `AKERR_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 that - `akgl_game_load_objectnamemap()` looks for. -- **G4 `test_game_load_objectnamemap_truncation`** — feed a deliberately - truncated table (EOF before the sentinel). The `while (1)` loop at - `src/game.c:340` relies on `aksl_fread` raising; assert it returns an error - rather than looping forever. Give this test an aggressive CTest `TIMEOUT`. -- **G5 `test_game_state_lock`** — assert `akgl_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 - resolved `from`, and that null `path`/`from` return `AKERR_NULLPOINTER`. - **Defect 4**: this is expected to fail — the function never assigns `*dst` and - 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 assert `akgl_render_and_compare()` reports - `AKERR_VALUE`. **Defect 1**: expected to fail today. Add - `test_compare_sdl_surfaces_geometry`: surfaces with mismatched `w`/`h`/`pitch` - must 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 under `root`, and a path that - resolves under neither (assert `ENOENT` propagates). Assert - `akgl_path_relative_root()` returns `AKERR_OUTOFBOUNDS` when - `strlen(root) + strlen(path) >= AKGL_MAX_STRING_LENGTH`, and that both - functions release their heap strings on the failure path. **Defect 8**: wire - the existing `test_akgl_collide_point_rectangle_logic()` into `main()`. - ---- - -## Tier 2 — suites that need the offscreen renderer harness - -### Test infrastructure (do this first) - -1. **Fix the loader path.** Set `BUILD_RPATH`/`INSTALL_RPATH` on the test targets - to the vendored dependency directories, or add - `ENVIRONMENT_MODIFICATION "LD_LIBRARY_PATH=path_list_prepend:..."` to the - `set_tests_properties()` block in `CMakeLists.txt:141`. Without this, CI - coverage numbers are meaningless (see the note at the top). -2. **Add `tests/harness.c` / `tests/harness.h`** with - `akgl_test_init_headless()` and `akgl_test_shutdown_headless()`: set - `SDL_VIDEODRIVER=dummy` and `SDL_AUDIODRIVER=dummy`, `SDL_Init()`, - `akgl_heap_init()`, `akgl_registry_init()`, create a software - `SDL_CreateWindowAndRenderer` and point the global `renderer` at 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. -3. **Add a `tests/assets/snippets/` fixture per new JSON case** rather than - embedding JSON in C string literals, matching the existing - `test_tilemap_get_json_tilemap_property.json` convention. - -### 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 `_double` are all uncovered. Assert each reads the right - `type` discriminator; note `akgl_get_json_properties_double()` asks for type - `"float"` (`src/tilemap.c:138`) — pin whether that is intended. Assert a - type mismatch returns `AKERR_TYPE` and a missing property returns `AKERR_KEY`. -- **T2 `test_tilemap_scale_actor`** — pure math, no renderer. Three branches at - `src/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**. Load `tests/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 yields `AKGL_ERR_SDL` rather than a null-texture crash later. -- **T6 `test_tilemap_load_bounds`** — assert a map declaring more than - `AKGL_TILEMAP_MAX_TILESETS` (16) tilesets or `AKGL_TILEMAP_MAX_LAYERS` (16) - layers returns `AKERR_OUTOFBOUNDS` rather than writing past the arrays. -- **T7 `test_tilemap_draw`** — with the dummy renderer, assert - `akgl_tilemap_draw()` and `akgl_tilemap_draw_tileset()` complete for each - layer index, that an out-of-range `layeridx` is 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`** — with `game.screenwidth`/`screenheight` - properties set, assert `akgl_render_init2d()` populates all six function - pointers and sets `camera` to `{0, 0, w, h}`. Assert null `self` → - `AKERR_NULLPOINTER` and that the two heap strings it acquires are released - (`src/renderer.c:31-32`) even on the failure path — they currently are not, - because `PASS` returns early on `SDL_CreateWindowAndRenderer` failure. -- **N2 `test_render_frame_start_end`** — assert `AKERR_NULLPOINTER` when - `self->sdl_renderer` is NULL; assert success against the dummy renderer. -- **N3 `test_render_2d_draw_texture`** — the rotated path - (`angle != 0`) is uncovered: assert `angle != 0` with `center == NULL` returns - `AKERR_NULLPOINTER`, and that a valid rotation succeeds. Assert null `self` - and null `texture` are rejected. -- **N4 `test_render_2d_draw_mesh`** — pin the `AKERR_API` "Not implemented" stub. -- **N5 `test_render_2d_draw_world`** — assert layer ordering (tilemap layer *i* - drawn before the actors on layer *i*), that `refcount == 0` actors are - skipped, and that `opflags == NULL` zero-initializes the default iterator. - Note `defflags` at `src/renderer.c:113` is uninitialized until the `if` body - runs — assert the non-null-`opflags` path 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 new `tests/assets/` fixture; - assert it lands in `AKGL_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 that `wraplength` 0 vs. positive both - work. -- **X3 `test_draw_background`** — `akgl_draw_background()` returns `void` and - 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 into `AKGL_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_Event` structs directly and call - `akgl_controller_handle_event()` — no physical device needed for - `SDL_EVENT_KEY_DOWN`/`_UP`, and `SDL_EVENT_GAMEPAD_ADDED`/`_REMOVED` can be - pushed onto the SDL event queue. -- `akgl_controller_pushmap()` and `akgl_controller_default()` are the most - testable: assert a control map is registered and retrievable, that an - out-of-range `controlmapid` is rejected, and that `akgl_controller_default()` - binds the eight `akgl_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`: - -```cmake -add_executable(test_ tests/.c) -add_test(NAME COMMAND test_) -target_link_libraries(test_ 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`. - ---- +The vendored SDL satellite libraries are built into per-project subdirectories +that are not on the loader's default search path, and `LD_LIBRARY_PATH` is +searched ahead of RPATH — so a developer who has previously run `rebuild.sh` had +their installed `libakgl.so` shadow the one under test, and a developer who had +not saw every test abort before `main()` with "cannot open shared object file". +`CMakeLists.txt` now sets `BUILD_RPATH` on the library, the utility, and every +test target, and prepends the build tree to `LD_LIBRARY_PATH` for the CTest run. ## Carried over @@ -551,5 +222,6 @@ New suites in dependency order: `physics`, `json_helpers`, `heap`, `game`, 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. + `akgl_character_state_sprites_iterate` is still uncovered and is the natural + place to start; `tests/actor.c` now covers `akgl_character_sprite_get` and the + composite-state binding behavior it depends on. diff --git a/src/controller.c b/src/controller.c index a48446c..c328699 100644 --- a/src/controller.c +++ b/src/controller.c @@ -125,9 +125,9 @@ akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event) PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate"); - FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL event"); + FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event"); player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL); - FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "Player actor does not exist"); + FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist"); if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN || event->key.key == SDLK_DOWN ) { @@ -183,10 +183,10 @@ akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event) PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate"); - FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL event"); + FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event"); player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL); - FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "Player actor does not exist"); - + FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist"); + if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN || event->key.key == SDLK_DOWN ) { SDL_Log("processing down release"); @@ -230,7 +230,7 @@ akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event) PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate"); - FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL event"); + FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event"); which = event->gbutton.which; gamepad = SDL_GetGamepadFromID(which); @@ -266,7 +266,7 @@ akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event) PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate"); - FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL event"); + FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event"); which = event->gbutton.which; gamepad = SDL_GetGamepadFromID(which); diff --git a/tests/controller.c b/tests/controller.c new file mode 100644 index 0000000..d51bf0b --- /dev/null +++ b/tests/controller.c @@ -0,0 +1,557 @@ +/** + * @file controller.c + * @brief Unit tests for control maps and SDL input dispatch. + * + * SDL events are synthesized directly rather than pumped through the event + * queue, so none of this needs a physical keyboard or gamepad. The device + * enumeration helpers are exercised against the dummy drivers, where the + * expected outcome is "no devices, no crash". + */ + +#include +#include +#include + +#include +// akgl/controller.h uses akgl_Actor without declaring it, so actor.h has to +// come first, the same way src/controller.c reaches it through akgl/game.h. +#include +#include +#include +#include +#include +#include + +#include "testutil.h" + +/* + * akgl/controller.h declares these as akgl_controller_handle_*, but src has + * always defined them as gamepad_handle_*. Declared here under the names that + * actually link, so the tests can reach them without renaming shipped symbols. + */ +akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event); +akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event); +akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event); +akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event); + +/** @brief Keyboard id the tests bind their control maps to. */ +#define TEST_KBID 11 +/** @brief Gamepad id the tests bind their control maps to. */ +#define TEST_JSID 22 + +/** @brief Stand-in for the application state pointer SDL hands to callbacks. */ +static int appstate_placeholder = 0; + +/** @brief The actor every control map in this file targets. */ +static akgl_Actor *player = NULL; +/** @brief Base character supplying the player's acceleration constants. */ +static akgl_Character *playerchar = NULL; + +/** @brief Clear every control map so each test starts from an empty binding set. */ +static void reset_control_maps(void) +{ + int i = 0; + for ( i = 0; i < AKGL_MAX_CONTROL_MAPS; i++ ) { + memset(&GAME_ControlMaps[i], 0x00, sizeof(akgl_ControlMap)); + } +} + +/** @brief Register the "player" actor the gamepad handlers look up by name. */ +static akerr_ErrorContext *make_player(void) +{ + PREPARE_ERROR(e); + ATTEMPT { + CATCH(e, akgl_registry_init_actor()); + CATCH(e, akgl_registry_init_character()); + CATCH(e, akgl_heap_init()); + + CATCH(e, akgl_heap_next_character(&playerchar)); + CATCH(e, akgl_character_initialize(playerchar, "playerchar")); + playerchar->ax = 6.0f; + playerchar->ay = 8.0f; + + CATCH(e, akgl_heap_next_actor(&player)); + CATCH(e, akgl_actor_initialize(player, "player")); + player->basechar = playerchar; + } CLEANUP { + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +/** @brief Build a synthetic keyboard event. */ +static void make_key_event(SDL_Event *event, uint32_t type, SDL_KeyboardID which, SDL_Keycode key) +{ + memset(event, 0x00, sizeof(SDL_Event)); + event->type = type; + event->key.which = which; + event->key.key = key; +} + +/** @brief Build a synthetic gamepad button event. */ +static void make_button_event(SDL_Event *event, uint32_t type, SDL_JoystickID which, uint8_t button) +{ + memset(event, 0x00, sizeof(SDL_Event)); + event->type = type; + event->gbutton.which = which; + event->gbutton.button = button; +} + +akerr_ErrorContext *test_controller_pushmap(void) +{ + PREPARE_ERROR(e); + akgl_Control control; + int i = 0; + + ATTEMPT { + reset_control_maps(); + memset(&control, 0x00, sizeof(akgl_Control)); + control.key = SDLK_SPACE; + control.event_on = SDL_EVENT_KEY_DOWN; + control.event_off = SDL_EVENT_KEY_UP; + control.handler_on = &akgl_Actor_cmhf_left_on; + control.handler_off = &akgl_Actor_cmhf_left_off; + + TEST_EXPECT_OK(e, akgl_controller_pushmap(0, &control), "pushing one control"); + TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == 1, + "pushing one control left nextMap at %d, expected 1", + GAME_ControlMaps[0].nextMap); + TEST_ASSERT(e, GAME_ControlMaps[0].controls[0].key == SDLK_SPACE, + "the pushed control did not land in slot 0"); + TEST_ASSERT(e, GAME_ControlMaps[0].controls[0].handler_on == &akgl_Actor_cmhf_left_on, + "the pushed control lost its press handler"); + + // Pushes accumulate rather than overwrite. + TEST_EXPECT_OK(e, akgl_controller_pushmap(0, &control), "pushing a second control"); + TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == 2, + "pushing a second control left nextMap at %d, expected 2", + GAME_ControlMaps[0].nextMap); + + // Maps are independent of each other. + TEST_EXPECT_OK(e, akgl_controller_pushmap(3, &control), "pushing into a different map"); + TEST_ASSERT(e, GAME_ControlMaps[3].nextMap == 1, + "map 3 nextMap is %d, expected 1", GAME_ControlMaps[3].nextMap); + TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == 2, + "pushing into map 3 disturbed map 0 (nextMap %d)", GAME_ControlMaps[0].nextMap); + + // Filling a map exactly to capacity still succeeds. + reset_control_maps(); + for ( i = 0; i < AKGL_MAX_CONTROLS; i++ ) { + akerr_ErrorContext *pushresult = akgl_controller_pushmap(0, &control); + if ( pushresult != NULL ) { + pushresult->handled = true; + pushresult = akerr_release_error(pushresult); + FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, "pushing control %d of %d failed", i, AKGL_MAX_CONTROLS); + } + } + TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == AKGL_MAX_CONTROLS, + "a full map reports nextMap %d, expected %d", + GAME_ControlMaps[0].nextMap, AKGL_MAX_CONTROLS); + + // One past capacity is refused. + TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_controller_pushmap(0, &control), + "pushing into a full control map"); + + TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_pushmap(0, NULL), + "pushing a NULL control"); + TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, + akgl_controller_pushmap(AKGL_MAX_CONTROL_MAPS, &control), + "pushing into a control map id at the limit"); + TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, + akgl_controller_pushmap(AKGL_MAX_CONTROL_MAPS + 5, &control), + "pushing into a control map id past the limit"); + } CLEANUP { + reset_control_maps(); + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext *test_controller_default_bindings(void) +{ + PREPARE_ERROR(e); + + ATTEMPT { + reset_control_maps(); + CATCH(e, make_player()); + + TEST_EXPECT_OK(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID), + "installing the default control map"); + + TEST_ASSERT(e, GAME_ControlMaps[0].target == player, + "the default map did not target the player actor"); + TEST_ASSERT(e, GAME_ControlMaps[0].kbid == TEST_KBID, + "the default map recorded keyboard %d, expected %d", + GAME_ControlMaps[0].kbid, TEST_KBID); + TEST_ASSERT(e, GAME_ControlMaps[0].jsid == TEST_JSID, + "the default map recorded gamepad %d, expected %d", + GAME_ControlMaps[0].jsid, TEST_JSID); + + // Four keyboard bindings then four gamepad bindings. + TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == 8, + "the default map installed %d controls, expected 8", + GAME_ControlMaps[0].nextMap); + + TEST_ASSERT(e, GAME_ControlMaps[0].controls[0].key == SDLK_DOWN, + "the first default binding is not the down arrow"); + TEST_ASSERT(e, GAME_ControlMaps[0].controls[0].handler_on == &akgl_Actor_cmhf_down_on, + "the down arrow is not bound to the down handler"); + TEST_ASSERT(e, GAME_ControlMaps[0].controls[1].key == SDLK_UP, + "the second default binding is not the up arrow"); + TEST_ASSERT(e, GAME_ControlMaps[0].controls[2].key == SDLK_LEFT, + "the third default binding is not the left arrow"); + TEST_ASSERT(e, GAME_ControlMaps[0].controls[3].key == SDLK_RIGHT, + "the fourth default binding is not the right arrow"); + + // The gamepad half binds buttons and leaves the keycode clear, so a + // keyboard event cannot accidentally match a dpad binding. + TEST_ASSERT(e, GAME_ControlMaps[0].controls[4].button == SDL_GAMEPAD_BUTTON_DPAD_DOWN, + "the fifth default binding is not the dpad down button"); + TEST_ASSERT(e, GAME_ControlMaps[0].controls[4].key == 0, + "a gamepad binding also carries a keycode"); + TEST_ASSERT(e, GAME_ControlMaps[0].controls[7].button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT, + "the eighth default binding is not the dpad right button"); + + // An unknown actor name is a registry error, not a crash. + reset_control_maps(); + TEST_EXPECT_STATUS(e, AKGL_ERR_REGISTRY, + akgl_controller_default(1, "no_such_actor", TEST_KBID, TEST_JSID), + "installing a default map for an unregistered actor"); + + TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, + akgl_controller_default(AKGL_MAX_CONTROL_MAPS, "player", TEST_KBID, TEST_JSID), + "installing a default map at a control map id past the limit"); + } CLEANUP { + reset_control_maps(); + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext *test_controller_handle_keyboard_events(void) +{ + PREPARE_ERROR(e); + SDL_Event event; + + ATTEMPT { + reset_control_maps(); + CATCH(e, make_player()); + CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID)); + + // Pressing left routes to the left press handler. + player->state = 0; + make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT); + TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), + "dispatching a left key press"); + TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT), + "a left key press did not start the actor moving left (state %d)", player->state); + TEST_ASSERT_FEQ(e, player->ax, -6.0f, + "a left key press set ax to %f, expected -6", player->ax); + + // Releasing it routes to the release handler. + make_key_event(&event, SDL_EVENT_KEY_UP, TEST_KBID, SDLK_LEFT); + TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), + "dispatching a left key release"); + TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_LEFT), + "a left key release did not stop the actor (state %d)", player->state); + + // The other three directions route the same way. + player->state = 0; + make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_RIGHT); + TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), + "dispatching a right key press"); + TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT), + "a right key press did not start the actor moving right (state %d)", player->state); + + player->state = 0; + make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_UP); + TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), + "dispatching an up key press"); + TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP), + "an up key press did not start the actor moving up (state %d)", player->state); + + player->state = 0; + make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_DOWN); + TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), + "dispatching a down key press"); + TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_DOWN), + "a down key press did not start the actor moving down (state %d)", player->state); + + // An unbound key is ignored. + player->state = 0; + make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_F12); + TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), + "dispatching an unbound key"); + TEST_ASSERT(e, player->state == 0, + "an unbound key changed the actor state to %d", player->state); + + // A bound key from a different keyboard is ignored, so split-keyboard + // local multiplayer does not cross-talk. + make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID + 1, SDLK_LEFT); + TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), + "dispatching a bound key from an unbound keyboard"); + TEST_ASSERT(e, player->state == 0, + "a key from another keyboard changed the actor state to %d", player->state); + } CLEANUP { + reset_control_maps(); + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext *test_controller_handle_gamepad_events(void) +{ + PREPARE_ERROR(e); + SDL_Event event; + + ATTEMPT { + reset_control_maps(); + CATCH(e, make_player()); + CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID)); + + player->state = 0; + make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT); + TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), + "dispatching a dpad left press"); + TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT), + "a dpad left press did not start the actor moving left (state %d)", player->state); + + make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_UP, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT); + TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), + "dispatching a dpad left release"); + TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_LEFT), + "a dpad left release did not stop the actor (state %d)", player->state); + + player->state = 0; + make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_UP); + TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), + "dispatching a dpad up press"); + TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP), + "a dpad up press did not start the actor moving up (state %d)", player->state); + + // A press from an unbound gamepad is ignored. + player->state = 0; + make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID + 1, SDL_GAMEPAD_BUTTON_DPAD_LEFT); + TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), + "dispatching a dpad press from an unbound gamepad"); + TEST_ASSERT(e, player->state == 0, + "a press from another gamepad changed the actor state to %d", player->state); + + // An unbound button on the bound gamepad is ignored. + make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_START); + TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), + "dispatching an unbound gamepad button"); + TEST_ASSERT(e, player->state == 0, + "an unbound gamepad button changed the actor state to %d", player->state); + } CLEANUP { + reset_control_maps(); + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext *test_controller_handle_event_edge_cases(void) +{ + PREPARE_ERROR(e); + SDL_Event event; + + ATTEMPT { + reset_control_maps(); + CATCH(e, make_player()); + + // With no control maps installed at all, every event is a no-op. + make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT); + TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), + "dispatching into an empty control map table"); + + // A map in a later slot is still reached, so the scan does not stop at + // the first unpopulated entry. + CATCH(e, akgl_controller_default(5, "player", TEST_KBID, TEST_JSID)); + player->state = 0; + TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), + "dispatching to a control map in a later slot"); + TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT), + "a control map in slot 5 was not consulted (state %d)", player->state); + + // An event type that no binding uses falls through harmlessly. + memset(&event, 0x00, sizeof(SDL_Event)); + event.type = SDL_EVENT_MOUSE_MOTION; + TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), + "dispatching an event type with no bindings"); + + TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_handle_event(NULL, &event), + "dispatching with a NULL appstate"); + TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, + akgl_controller_handle_event(&appstate_placeholder, NULL), + "dispatching a NULL event"); + } CLEANUP { + reset_control_maps(); + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext *test_controller_gamepad_button_handlers(void) +{ + PREPARE_ERROR(e); + SDL_Event event; + + ATTEMPT { + CATCH(e, make_player()); + + // These handlers find their actor by looking up "player" in the registry + // rather than taking it as an argument. + player->state = 0; + player->movement_controls_face = false; + make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN); + TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), + "dpad down press handler"); + TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_DOWN), + "the dpad down handler did not set MOVING_DOWN (state %d)", player->state); + // With automatic facing off, the handler sets the facing itself. + TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_FACE_DOWN), + "the dpad down handler did not set FACE_DOWN (state %d)", player->state); + + TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), + "dpad down release handler"); + TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_DOWN), + "the dpad down release handler did not clear MOVING_DOWN (state %d)", player->state); + TEST_ASSERT(e, player->curSpriteFrameId == 0, + "the release handler did not reset the animation frame"); + + player->state = 0; + make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_UP); + TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad up press handler"); + TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP), + "the dpad up handler did not set MOVING_UP (state %d)", player->state); + TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad up release handler"); + + player->state = 0; + make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT); + TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad left press handler"); + TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT), + "the dpad left handler did not set MOVING_LEFT (state %d)", player->state); + TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad left release handler"); + + player->state = 0; + make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_RIGHT); + TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad right press handler"); + TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT), + "the dpad right handler did not set MOVING_RIGHT (state %d)", player->state); + TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad right release handler"); + + // With automatic facing on, the handler leaves facing to the actor's own + // face function. + player->state = 0; + player->movement_controls_face = true; + make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN); + TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), + "dpad down press with automatic facing on"); + TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_FACE_DOWN), + "the handler set facing even though the actor faces automatically (state %d)", + player->state); + + TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_button_down(NULL, &event), + "dpad press handler with a NULL appstate"); + TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, + gamepad_handle_button_down(&appstate_placeholder, NULL), + "dpad press handler with a NULL event"); + TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_button_up(NULL, &event), + "dpad release handler with a NULL appstate"); + TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, + gamepad_handle_button_up(&appstate_placeholder, NULL), + "dpad release handler with a NULL event"); + + // With no actor named "player" registered there is nothing to drive. + CATCH(e, akgl_registry_init_actor()); + make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN); + TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, + gamepad_handle_button_down(&appstate_placeholder, &event), + "dpad press handler with no player actor registered"); + TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, + gamepad_handle_button_up(&appstate_placeholder, &event), + "dpad release handler with no player actor registered"); + } CLEANUP { + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext *test_controller_device_events(void) +{ + PREPARE_ERROR(e); + SDL_Event event; + + ATTEMPT { + // The dummy drivers present no gamepads, so the add and remove handlers + // take their "unknown device" paths. Neither may crash. + make_button_event(&event, SDL_EVENT_GAMEPAD_ADDED, 999, 0); + TEST_EXPECT_OK(e, gamepad_handle_added(&appstate_placeholder, &event), + "handling an add for a device that cannot be opened"); + + make_button_event(&event, SDL_EVENT_GAMEPAD_REMOVED, 999, 0); + TEST_EXPECT_OK(e, gamepad_handle_removed(&appstate_placeholder, &event), + "handling a remove for a device that was never open"); + + TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_added(NULL, &event), + "add handler with a NULL appstate"); + TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, + gamepad_handle_added(&appstate_placeholder, NULL), + "add handler with a NULL event"); + TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_removed(NULL, &event), + "remove handler with a NULL appstate"); + TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, + gamepad_handle_removed(&appstate_placeholder, NULL), + "remove handler with a NULL event"); + } CLEANUP { + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext *test_controller_device_enumeration(void) +{ + PREPARE_ERROR(e); + + ATTEMPT { + // Enumeration has to succeed on a machine with no input devices, which + // is the normal state under the dummy drivers and in CI. + TEST_EXPECT_OK(e, akgl_controller_list_keyboards(), "listing keyboards"); + TEST_EXPECT_OK(e, akgl_controller_open_gamepads(), "opening gamepads"); + } CLEANUP { + } 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 { + FAIL_ZERO_BREAK( + errctx, + SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD), + AKGL_ERR_SDL, + "Couldn't initialize SDL: %s", + SDL_GetError()); + + CATCH(errctx, akgl_heap_init()); + CATCH(errctx, akgl_registry_init()); + + CATCH(errctx, test_controller_pushmap()); + CATCH(errctx, test_controller_default_bindings()); + CATCH(errctx, test_controller_handle_keyboard_events()); + CATCH(errctx, test_controller_handle_gamepad_events()); + CATCH(errctx, test_controller_handle_event_edge_cases()); + CATCH(errctx, test_controller_gamepad_button_handlers()); + CATCH(errctx, test_controller_device_events()); + CATCH(errctx, test_controller_device_enumeration()); + } CLEANUP { + SDL_Quit(); + } PROCESS(errctx) { + } FINISH_NORETURN(errctx); +}