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
113 lines
3.6 KiB
C
113 lines
3.6 KiB
C
/**
|
|
* @file ui.c
|
|
* @brief Unit tests for the UI subsystem: lifecycle, arena sizing and validation.
|
|
*
|
|
* Everything here runs without a window, a renderer, or SDL_Init at all --
|
|
* akgl_ui_init takes its layout dimensions as parameters precisely so that a
|
|
* test can bring the subsystem up headless. Only akgl_error_init() is needed
|
|
* first, so that AKGL_ERR_UI carries its name into any trace these tests
|
|
* produce.
|
|
*/
|
|
|
|
#include <akerror.h>
|
|
|
|
#include <akgl/error.h>
|
|
#include <akgl/ui.h>
|
|
|
|
#include "testutil.h"
|
|
|
|
/**
|
|
* @brief Init must refuse bad dimensions and an uninitialized resize.
|
|
*
|
|
* Runs before anything initializes the subsystem, because the "not
|
|
* initialized" refusal is half of what it asserts.
|
|
*/
|
|
akerr_ErrorContext *test_ui_validation(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
|
|
ATTEMPT {
|
|
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_ui_init(0, 240),
|
|
"akgl_ui_init accepted a zero width");
|
|
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_ui_init(320, -1),
|
|
"akgl_ui_init accepted a negative height");
|
|
TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_resize(320, 240),
|
|
"akgl_ui_resize worked on an uninitialized subsystem");
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
/**
|
|
* @brief Init, double-init refusal, idempotent shutdown, re-init.
|
|
*
|
|
* The double init has to be refused rather than absorbed: a second
|
|
* Clay_Initialize over a live context would silently discard every element id
|
|
* and scroll position the first one held, which is the kind of reset a game
|
|
* only notices as "the menu forgot where it was".
|
|
*/
|
|
akerr_ErrorContext *test_ui_lifecycle(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
|
|
ATTEMPT {
|
|
TEST_EXPECT_OK(e, akgl_ui_init(320, 240), "first akgl_ui_init failed");
|
|
TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_init(320, 240),
|
|
"a second akgl_ui_init was not refused");
|
|
TEST_EXPECT_OK(e, akgl_ui_resize(640, 480), "resize on a live subsystem failed");
|
|
TEST_EXPECT_OK(e, akgl_ui_shutdown(), "akgl_ui_shutdown failed");
|
|
TEST_EXPECT_OK(e, akgl_ui_shutdown(), "a second akgl_ui_shutdown was not a no-op");
|
|
TEST_EXPECT_OK(e, akgl_ui_init(320, 240), "re-init after shutdown failed");
|
|
TEST_EXPECT_OK(e, akgl_ui_shutdown(), "shutdown after re-init failed");
|
|
} CLEANUP {
|
|
IGNORE(akgl_ui_shutdown());
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
/**
|
|
* @brief A too-small arena must refuse at init, loudly, and recover.
|
|
*
|
|
* The narrowed limit stands in for a consumer who raised AKGL_UI_MAX_ELEMENTS
|
|
* without raising AKGL_UI_ARENA_BYTES: the refusal at startup, with both
|
|
* numbers in the message, is the whole safety story -- past init, clay trusts
|
|
* the arena completely. Same shape as the collision arena's exhaustion test,
|
|
* for the same reason: rebuilding with a smaller ceiling is not something CI
|
|
* can do.
|
|
*/
|
|
akerr_ErrorContext *test_ui_arena_exhaustion(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
|
|
ATTEMPT {
|
|
akgl_ui_arena_limit(64);
|
|
TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_init(320, 240),
|
|
"akgl_ui_init accepted an arena clay cannot fit in");
|
|
akgl_ui_arena_limit(0);
|
|
TEST_EXPECT_OK(e, akgl_ui_init(320, 240),
|
|
"akgl_ui_init failed after the arena limit was restored");
|
|
TEST_EXPECT_OK(e, akgl_ui_shutdown(), "shutdown after the exhaustion test failed");
|
|
} CLEANUP {
|
|
akgl_ui_arena_limit(0);
|
|
IGNORE(akgl_ui_shutdown());
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_error_init());
|
|
CATCH(errctx, test_ui_validation());
|
|
CATCH(errctx, test_ui_lifecycle());
|
|
CATCH(errctx, test_ui_arena_exhaustion());
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH_NORETURN(errctx);
|
|
}
|