libakgl does not call malloc at runtime. That is stated in seven places in the manual and is AGENTS.md's second standing rule, and every object comes from a fixed array in akgl/heap.h. libccd's EPA path does not know that: it builds its expanding polytope out of realloc and free, and ccdGJKPenetration documents a -2 return for when that fails. The alternative was to compile only the three files MPR needs and leave EPA out of the build. That works and it puts a copy of somebody else's source list in this repository, where it rots silently on the next submodule bump. This instead compiles all of libccd and points its allocator at a bump allocator over static BSS, which keeps the promise literally true -- and keeps EPA available rather than amputated, for the day a precise contact manifold is worth having. The arena is reset at the top of each query rather than freed block by block, so the lifetime is one narrowphase call, `free` is a no-op, and allocation is a pointer bump. Exhaustion returns NULL, which libccd already handles by unwinding to -2, which becomes AKGL_ERR_COLLISION naming the high-water mark -- a loud failure with a number in it rather than a silently missed collision, which would be a floor an actor falls through reported as success. One GJK/EPA box pair costs 7,264 bytes, measured and printed by the suite. The 64 KB ceiling is that with about nine times headroom, and the high-water mark is reported so the next reader can re-derive it rather than trust this line. The redirect lives in src/ccd_arena_shim.h, injected with -include, and not in CMake's COMPILE_DEFINITIONS. It was in COMPILE_DEFINITIONS first, and that is a mistake worth recording: CMake cannot carry a function-like macro through a -D, so it dropped __CCD_ALLOC_MEMORY without a diagnostic. libccd went on calling the C library's realloc while the shim's `free` quietly discarded the results -- strictly worse than doing nothing, and invisible, because it leaks rather than crashes. What caught it was insisting the test prove the wiring rather than the outcome. The first version asserted the arena balanced back to zero after a query, which turned out to be the wrong assertion for a different reason -- free is a no-op by design, so it cannot balance -- but a test that had merely checked "two boxes collide" would have passed throughout, against an allocator nothing was using. The suite now asserts what is actually provable: that a query allocates from the arena at all, and that the process survives, since glibc aborts when the real free(3) is handed a pointer it never issued. Also here: AKGL_ERR_COLLISION, with the name registered -- tests/error.c asserts the band and the names agree, and it caught the missing one immediately. -fvisibility=hidden and CCD_STATIC_DEFINE keep every ccd* symbol out of libakgl.so's dynamic table, which `nm -D` confirms is empty; AGENTS.md records a shipped defect where an exported `renderer` was preempted by a same-named symbol elsewhere, and a game linking a system libccd would hit exactly that. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
107 lines
3.7 KiB
C
107 lines
3.7 KiB
C
/**
|
|
* @file error.c
|
|
* @brief Unit tests for the libakgl status band: reservation, ownership and names.
|
|
*
|
|
* The libakerror registry is process-global, so these tests assert against
|
|
* whatever akgl_error_init() left behind rather than building their own state.
|
|
*/
|
|
|
|
#include <string.h>
|
|
#include <akerror.h>
|
|
|
|
#include <akgl/error.h>
|
|
|
|
#include "testutil.h"
|
|
|
|
/**
|
|
* @brief akgl_error_init() must own the libakgl status band and name every code in it.
|
|
*
|
|
* A code whose name never registered degrades to "Unknown Error" in every stack
|
|
* trace that carries it, and a band we never reserved is one another component
|
|
* can name out from under us. Both stay silent until something has already gone
|
|
* wrong, so assert them directly rather than waiting to read a useless trace.
|
|
*/
|
|
akerr_ErrorContext *test_error_init_owns_the_status_band(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
static const struct {
|
|
int status;
|
|
const char *name;
|
|
} expected[] = {
|
|
{ AKGL_ERR_SDL, "SDL Error" },
|
|
{ AKGL_ERR_REGISTRY, "Registry Error" },
|
|
{ AKGL_ERR_HEAP, "Heap Error" },
|
|
{ AKGL_ERR_BEHAVIOR, "Behavior Error" },
|
|
{ AKGL_ERR_LOGICINTERRUPT, "Logic Interrupt" },
|
|
{ AKGL_ERR_COLLISION, "Collision Error" }
|
|
};
|
|
bool named = true;
|
|
int i = 0;
|
|
|
|
ATTEMPT {
|
|
CATCH(e, akgl_error_init());
|
|
|
|
TEST_ASSERT(e, (int)(sizeof(expected) / sizeof(expected[0])) == AKGL_ERR_COUNT,
|
|
"the libakgl status band holds %d codes but %d are named here",
|
|
AKGL_ERR_COUNT, (int)(sizeof(expected) / sizeof(expected[0])));
|
|
|
|
for ( i = 0; i < (int)(sizeof(expected) / sizeof(expected[0])); i++ ) {
|
|
TEST_ASSERT_FLAG(named,
|
|
strcmp(akerr_name_for_status(expected[i].status, NULL),
|
|
expected[i].name) == 0);
|
|
}
|
|
TEST_ASSERT(e, named,
|
|
"akgl_error_init did not register the expected name for every AKGL_ERR_* code");
|
|
|
|
// The reservation is what makes those names ours. Without it the
|
|
// registrations above would still succeed for anyone who asked.
|
|
TEST_EXPECT_STATUS(e, AKERR_STATUS_NAME_FOREIGN,
|
|
akerr_register_status_name("not-libakgl", AKGL_ERR_HEAP, "Squatter"),
|
|
"a foreign owner was allowed to rename a libakgl status");
|
|
TEST_EXPECT_STATUS(e, AKERR_STATUS_RANGE_OVERLAP,
|
|
akerr_reserve_status_range(AKGL_ERR_BASE, AKGL_ERR_COUNT, "not-libakgl"),
|
|
"a foreign owner was allowed to reserve the libakgl status band");
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
/**
|
|
* @brief Calling akgl_error_init() twice must be a no-op, not a self-collision.
|
|
*
|
|
* Nothing in libakgl orders initialization for an embedding program, so a second
|
|
* call has to be harmless. libakerror only treats an *identical* reservation as
|
|
* a repeat -- a subset or superset raises -- which makes this a real constraint
|
|
* on AKGL_ERR_BASE and AKGL_ERR_COUNT, not a triviality.
|
|
*/
|
|
akerr_ErrorContext *test_error_init_is_idempotent(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
|
|
ATTEMPT {
|
|
TEST_EXPECT_OK(e, akgl_error_init(), "the second akgl_error_init failed");
|
|
TEST_EXPECT_OK(e, akgl_error_init(), "the third akgl_error_init failed");
|
|
TEST_ASSERT(e, strcmp(akerr_name_for_status(AKGL_ERR_SDL, NULL), "SDL Error") == 0,
|
|
"re-running akgl_error_init lost the name for AKGL_ERR_SDL");
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
ATTEMPT {
|
|
// Unlike every other suite, this one has no akgl_error_init() in
|
|
// 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());
|
|
CATCH(errctx, test_error_init_is_idempotent());
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH_NORETURN(errctx);
|
|
}
|