Take libakerror 2.0.1 and drop the workaround it makes obsolete
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:
@@ -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());
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) ) { \
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user