Take libakerror 2.0.1 and drop the workaround it makes obsolete
Some checks failed
libakgl CI Build / cmake_build (push) Failing after 20s
libakgl CI Build / performance (push) Failing after 19s
libakgl CI Build / memory_check (push) Failing after 15s
libakgl CI Build / mutation_test (push) Failing after 17s

Every libakgl test suite could report success while failing. libakerror's
default unhandled-error handler ended in exit(errctx->status), an exit
status is one byte wide, and libakgl's band starts at 256 -- so
AKGL_ERR_SDL, the most common failure a library built on SDL can have,
exited 0 and CTest recorded a pass. tests/character.c aborted at its
second of four tests on a bad renderer and was green for months.

0.5.0 worked around that here with TEST_TRAP_UNHANDLED_ERRORS() in
tests/testutil.h, and TODO.md ended the entry saying any consumer's
suites have the same problem and it was worth raising upstream. It was.
2.0.1 fixes it at the source: akerr_exit() owns the mapping and the
default handler calls it, so 0 exits 0, 1 through 255 exit themselves,
and anything else exits AKERR_EXIT_STATUS_UNREPRESENTABLE (125). The
trap and its 21 call sites are gone.

Verified by putting the original failure back rather than by reading the
release notes: a FAIL_BREAK(AKGL_ERR_SDL) in tests/character.c's main
exits 125 and CTest reports a failure. A standalone consumer raising the
same status unhandled exits 125 where it exited 0 before.

tests/actor.c installs its own handler and called exit(errctx->status)
from it, which is the same defect one layer up. It calls akerr_exit()
now.

2.0.0 also makes the error pool and the status registry thread safe,
which libakgl needs more than it knew: audio_stream_callback raises
error contexts on SDL's audio thread. With an unlocked pool that
callback and the main thread could scan AKERR_ARRAY_ERROR at the same
time and be handed the same slot. The comment there says so.

This is a hard dependency floor, not a preference. 2.0.0 moved
__akerr_last_ignored to thread-local storage and made akerr_next_error()
return a context that already holds its reference, and both expand at
libakgl's call sites -- and at a consumer's, because akerror.h is part
of libakgl's public interface. Mixing headers and libraries across that
line double-counts every reference and never returns a pool slot. The
soname moved to libakerror.so.2; include/akgl/error.h now also feature-
tests AKERR_EXIT_STATUS_UNREPRESENTABLE, which is the narrowest probe
for 2.0.1 since libakerror publishes no version macro.

0.7.0 for that reason: libakgl's own ABI is unchanged, but the one it
re-exports through its headers is not.

TODO.md records the pkg-config gap this makes sharper -- akgl.pc names
no dependencies at all, so nothing tells a pkg-config consumer which
libakerror it needs.

Clean build, 26/26 ctest, memcheck clean, warning-clean at -Wall
-Werror. libakgl.so.0.7 links libakerror.so.2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8T5FAYXE8HEJqFLCYwNNc
This commit is contained in:
2026-08-01 13:05:43 -04:00
parent 1b90f2ebd5
commit e4aa6a5084
28 changed files with 92 additions and 85 deletions

View File

@@ -30,7 +30,9 @@ void handle_unhandled_error_noexit(akerr_ErrorContext *errctx)
return;
}
if ( UNHANDLED_ERROR_BEHAVIOR == UNHANDLED_ERROR_EXIT ) {
exit(errctx->status);
// akerr_exit rather than exit(3): a status past 255 does not survive a
// wait status, and this suite's are AKGL_* codes, which all are.
akerr_exit(errctx->status);
}
if ( UNHANDLED_ERROR_BEHAVIOR == UNHANDLED_ERROR_SET ) {
unhandled_error_context = errctx;
@@ -1037,7 +1039,6 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
CATCH(errctx, akgl_registry_init_actor());
CATCH(errctx, akgl_registry_init_sprite());
CATCH(errctx, akgl_registry_init_spritesheet());

View File

@@ -602,7 +602,6 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
FAIL_ZERO_BREAK(
errctx,
SDL_Init(SDL_INIT_AUDIO),

View File

@@ -286,7 +286,6 @@ int main(void)
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
akgl_renderer = &akgl_default_renderer;
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");

View File

@@ -858,7 +858,6 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
FAIL_ZERO_BREAK(
errctx,
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD),

View File

@@ -638,7 +638,6 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
akgl_renderer = &akgl_default_renderer;
FAIL_ZERO_BREAK(

View File

@@ -95,12 +95,9 @@ int main(void)
ATTEMPT {
// Unlike every other suite, this one has no akgl_error_init() in
// main() -- the first test is what brings the subsystem up. The trap
// therefore goes after it: libakerror installs its own default handler
// from the lazy akerr_init() inside that first call, and would
// overwrite anything set beforehand.
// main() -- the first test is what brings the subsystem up, and
// asserting that it does is the point of it.
CATCH(errctx, test_error_init_owns_the_status_band());
TEST_TRAP_UNHANDLED_ERRORS();
CATCH(errctx, test_error_init_is_idempotent());
} CLEANUP {
} PROCESS(errctx) {

View File

@@ -775,7 +775,6 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());

View File

@@ -362,7 +362,6 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());

View File

@@ -528,7 +528,6 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
CATCH(errctx, load_fixture());

View File

@@ -901,7 +901,6 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
CATCH(errctx, akgl_registry_init_properties());

View File

@@ -752,7 +752,6 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
akgl_renderer = &akgl_default_renderer;
akgl_physics = &akgl_default_physics;
akgl_camera = &akgl_default_camera;

View File

@@ -784,7 +784,6 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
CATCH(errctx, akgl_registry_init_properties());

View File

@@ -616,7 +616,6 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
// SDL_GetTicksNS() counts from SDL's own initialization, and
// akgl_physics_simulate measures dt against it. Without this the clock
// epoch would be established by the first call inside the first

View File

@@ -288,7 +288,6 @@ int main(void)
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
CATCH(errctx, test_akgl_registry_init_creation_failures());
CATCH(errctx, test_akgl_get_property_copies_only_the_value());
CATCH(errctx, test_actor_state_string_names());

View File

@@ -250,7 +250,6 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
memset(&bound, 0x00, sizeof(akgl_RenderBackend));
FAIL_ZERO_BREAK(

View File

@@ -242,7 +242,6 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
akgl_renderer = &akgl_default_renderer;
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");

View File

@@ -241,7 +241,6 @@ int main(void)
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
printf("test_fresh_heap_gives_string ....\n");
test_fresh_heap_gives_strings();
reset_string_heap();

View File

@@ -43,40 +43,6 @@ static inline int test_string_pool_used(void)
return used;
}
/**
* @brief Exit with a status the shell can actually see.
*
* libakerror's default unhandled-error handler ends in `exit(errctx->status)`.
* `exit` keeps only the low byte of what it is given, and libakgl's own
* statuses start at `AKERR_FIRST_CONSUMER_STATUS`, which is 256 -- so
* #AKGL_ERR_SDL exits 0, and CTest records a pass. Every suite in this
* directory that failed for the most common reason in this library therefore
* reported success. `tests/character.c` did exactly that: it aborted at its
* second of four tests on a bad renderer and was green for months.
*
* This collapses any status that a byte cannot carry onto 1. It does not log --
* `FINISH_NORETURN` has already logged the context and its stack by the time it
* calls the handler.
*/
static inline void test_handler_unhandled_error(akerr_ErrorContext *errctx)
{
int status = ( errctx == NULL ) ? 1 : (errctx->status & 0xFF);
if ( status == 0 ) {
status = 1;
}
exit(status);
}
/**
* @brief Install test_handler_unhandled_error(). Call once, after akgl_error_init().
*
* akgl_error_init() reaches akerr_init(), which installs libakerror's default
* handler, so this has to come after it rather than before.
*/
#define TEST_TRAP_UNHANDLED_ERRORS() \
(akerr_handler_unhandled_error = &test_handler_unhandled_error)
/** @brief Fail the enclosing ATTEMPT block unless @p cond holds. */
#define TEST_ASSERT(e, cond, ...) \
if ( ! (cond) ) { \

View File

@@ -392,7 +392,6 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
memset(&testbackend, 0x00, sizeof(akgl_RenderBackend));
FAIL_ZERO_BREAK(
errctx,

View File

@@ -742,7 +742,6 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");

View File

@@ -457,7 +457,6 @@ int main(void)
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
CATCH(errctx, test_akgl_rectangle_points_nullpointers());
CATCH(errctx, test_akgl_rectangle_points_math());
CATCH(errctx, test_akgl_collide_point_rectangle_nullpointers());

View File

@@ -99,7 +99,6 @@ int main(void)
ATTEMPT {
CATCH(errctx, akgl_error_init());
TEST_TRAP_UNHANDLED_ERRORS();
CATCH(errctx, test_version_string_matches_components());
CATCH(errctx, test_version_linked_matches_compiled());
CATCH(errctx, test_version_at_least_boundaries());