Files
libakgl/tests/error.c
Andrew Kesterson b7ff54b09f Vendor clay and bring the UI subsystem up: arena, lifecycle, status band
clay v0.14 (deps/clay, zlib licence) supplies the layout engine for the new
akgl_ui subsystem. Sources-listed rather than add_subdirectory()'d, on the
semver/libccd precedent: clay's CMakeLists declares no library target, builds
its examples by default, and requires CMake 3.27 against this project's 3.10.
src/ui_clay.c is the one CLAY_IMPLEMENTATION translation unit, compiled -w on
the vendored-code terms but with clay's symbols deliberately exported --
consumers' CLAY() macros resolve against libakgl.so, and akgl/ui.h warns them
never to link a second clay.

The subsystem is runtime-optional the way collision is: always compiled in,
inert until akgl_ui_init(), which bounds clay to the overridable AKGL_UI_*
ceilings, checks Clay_MinMemorySize() against a static BSS arena (no malloc),
and refuses with both numbers in the message when it does not fit. Measured:
812544 bytes at the default 1024 elements / 4096 measured words, against a
1 MiB arena. clay's void-callback layout errors are logged as they happen and
the first is stashed for the error protocol to raise later.

AKGL_ERR_UI joins the status band before AKGL_ERR_LIMIT, named in
akgl_error_init. New `ui` test suite covers validation, the init/shutdown
lifecycle, and arena exhaustion via the akgl_ui_arena_limit test hook (same
contract as akgl_ccd_arena_set_limit). clay.h is installed beside semver.h,
its licence beside libccd's, and the headers suite proves akgl/ui.h
self-contained -- clay.h compiles clean under -Wall.

Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 10:53:11 -04:00

108 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" },
{ AKGL_ERR_UI, "UI 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);
}