Make savegames, optional array elements, empty text and coverage work

Closes Known-and-still-open items 7 and 13, and Defects items 18, 25 and 27.

The savegame name tables carry no length prefix, so writer and reader have to
agree on a field width exactly. The writer used each object's own maximum name
length -- 512 for a spritesheet, a filename -- and the reader used
AKGL_ACTOR_MAX_NAME_LENGTH for all four. Four AKGL_GAME_SAVE_*_NAME_WIDTH
constants drive both sides now.

The failure turned out to be worse than "cannot be read back": a reader
stepping the wrong width does not run off anything, it finds a run of zeros
inside an entry, stops early, and reports success with silently wrong maps. A
test asserting only that the load succeeded passed against the broken reader.
So akgl_game_load checks it is at EOF once the tables are read, which turns a
width disagreement into AKERR_IO instead of a corruption. That is the assertion
the new roundtrip test -- the first with all four registries populated -- hangs
on.

akgl_get_json_with_default gains a third HANDLE_GROUP for AKERR_OUTOFBOUNDS,
which is what the array index accessors report, so "this element is optional"
works for an array element and not only for an object member. The arm goes
above the one holding the memcpy: HANDLE_GROUP emits no break and every arm
falls into that body.

That test needed a second attempt. with_default returns *the context it was
given* when it does not handle the status, so TEST_EXPECT_OK -- which releases
whatever the statement returns -- double-released it against a CLEANUP block
that released it too, and a double-released context corrupts the failure rather
than reporting it. The first draft passed against the unfixed library.

akgl_text_rendertextat returns success without rasterizing for the empty
string, matching akgl_text_measure, which has always accepted it. The check
sits after the font and backend guards, so drawing nothing still refuses what
drawing something refuses. tests/text.c had this case written and unasserted
waiting for the two halves of the header to agree.

character_load_json_state_int_from_strings guards dest rather than testing
states twice. Not asserted: the function is static with one call site that
passes a real pointer, so the guard cannot fire, and reaching it from a test
would mean giving it external linkage purely for that.

Both gcovr invocations take the build tree as an explicit positional search
path. gcovr searches --root when given none, which is the source directory,
where build trees live; --object-directory does not narrow it. Verified by
building two instrumented trees with a source edit between them: the old
invocation fails with "Got function write_exact on multiple lines: 46, 48" and
exits 64, the new one exits 0 and the full coverage run passes with the stale
tree still present.

25/25 pass, reindent --check, check_api_surface and check_error_protocol clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 07:18:31 -04:00
parent 947bc03601
commit 28fa929f6a
9 changed files with 307 additions and 93 deletions

View File

@@ -409,12 +409,29 @@ if(AKGL_COVERAGE)
set(AKGL_COVERAGE_DIR "${CMAKE_CURRENT_BINARY_DIR}/coverage")
file(MAKE_DIRECTORY "${AKGL_COVERAGE_DIR}")
# Both gcovr invocations take the build tree as an explicit positional search
# path, and neither passes --object-directory.
#
# gcovr looks for .gcda/.gcno under its search paths, and with none given it
# searches --root -- the source directory, which is where developers keep
# their build trees. --object-directory does *not* narrow that: per gcovr's
# own help it only identifies "the path between gcda files and the directory
# where the compiler was originally run". So every instrumented tree left
# inside the source directory was folded into the report alongside the one
# being measured, and with two trees describing different line numbers for the
# same function, coverage_reset failed before any test ran:
#
# AssertionError: Got function akgl_game_lowfps on multiple lines: 45, 46
#
# The `build*/` entry in .gitignore hides those trees from git status, which
# makes the state easy to get into and hard to notice.
add_test(
NAME coverage_reset
COMMAND ${GCOVR_EXECUTABLE}
--root "${CMAKE_CURRENT_SOURCE_DIR}"
--object-directory "${CMAKE_CURRENT_BINARY_DIR}"
--delete
"${CMAKE_CURRENT_BINARY_DIR}"
)
set_tests_properties(coverage_reset PROPERTIES FIXTURES_SETUP akgl_coverage)
# Any suite missing from this list runs outside the fixture and has its
@@ -430,11 +447,11 @@ if(AKGL_COVERAGE)
NAME coverage_report
COMMAND ${GCOVR_EXECUTABLE}
--root "${CMAKE_CURRENT_SOURCE_DIR}"
--object-directory "${CMAKE_CURRENT_BINARY_DIR}"
--filter "${CMAKE_CURRENT_SOURCE_DIR}/src/"
--xml-pretty
--xml "${AKGL_COVERAGE_DIR}/coverage.xml"
--html-details "${AKGL_COVERAGE_DIR}/index.html"
"${CMAKE_CURRENT_BINARY_DIR}"
)
set_tests_properties(
coverage_report