Files
akbasic/tests/ui_verbs.c

509 lines
20 KiB
C
Raw Normal View History

Give BASIC menus, dialogs and HUD labels over libakgl's UI helpers Group K, and the first verbs to reach the akgl_ui subsystem 0.9.0 brought in: MENU and GETMENU and RMENU, DIALOG, HUD and UISTYLE. A program that wanted a title screen had to draw one out of CHAR and GETKEY, which is what both breakout tutorials make a reader do. The interesting part is the impedance mismatch. libakgl's UI is immediate mode -- widgets are re-declared inside a frame bracket every frame and clay borrows their text until the bracket closes -- and a BASIC program says MENU 1, "START" on line 100 and expects it up on line 900, several hundred frames later. So src/ui_akgl.c is retained on this side and immediate on that one: the record's entry points are setters that copy into akbasic_AkglUi, and akbasic_ui_akgl_render() replays the whole set once a frame from the host's pump. No BASIC string, which lives in the per-line value pool, is ever what clay is handed. The shapes are borrowed rather than invented. MENU retires the way SOLID does -- no entries retires one, no arguments retire them all. GETMENU holds the step loop the way GETKEY does, so parking is not blocking: the step still returns, the host keeps its frame rate, and the sprite, audio and collision services keep running underneath because they run before the blocking checks. RMENU(n,1) reads and clears the way BUMP() does. Withdrawing the device or retiring the menu releases a holding GETMENU with 0 rather than wedging the script, which is akbasic_input_service()'s rule for a withdrawn keyboard. One thing a program has to know, and docs/19-user-interface.md says it twice: a menu that is up owns the cursor keys and Return. It has to, and retiring it gives them back -- forget the MENU n before an INPUT and the INPUT never sees the Return that ends it. akbasic_runtime_set_ui() is its own function rather than a fifth argument to akbasic_runtime_set_devices(), whose signature has twenty-eight call sites in tests and documentation that are about something else. deps/libakgl is not touched. akgl_UiAnchor has the four corners and dead centre, so HUD offers exactly those five; TODO.md records what a top-centre and bottom-centre would cost upstream, along with the three other things this deliberately leaves out. No new error code either -- DEVICE, BOUNDS, SYNTAX and TYPE cover the group, and 520 stays free. tools/screenshot.c had to learn that "needs a font" and "draws the text grid" are two questions. They were one, and a UI figure came out black: the text layer owns every pixel of the rows it covers and painted over the widgets. The new ui=1 fence attribute asks for the first without the second; MAINTENANCE.md documents it. 112/112 in both configurations, 112/112 under ASan and UBSan, coverage 94.1% against the 90% gate with src/runtime_ui.c at 99% of lines and 100% of functions, doxygen clean, and the four new figures byte-identical on a re-render. TODO.md section 8's gate table was stale on several counts besides these and is refreshed with measured numbers. Co-Authored-By: Tachikoma (Claude Code Opus 5 1M) <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 18:37:10 -04:00
/**
* @file ui_verbs.c
* @brief Tests the group K verbs against the recording mock backend.
*
* Same arrangement as tests/graphics_verbs.c and for the same reason: these
* verbs produce nothing a golden file can compare, so the assertions are on what
* reached the device. Running a real BASIC line rather than calling the handler
* directly exercises the dispatch-table row and the parse handler too, which is
* where an added verb is most likely to be wrong.
*
* The one thing a mock has to fake is a *choice*: a real activation comes from a
* keystroke libakgl consumed, and there is no keyboard here. mock_menu_choose()
* is that, and it is the only thing in this file that is not a BASIC program.
*/
#include <string.h>
#include <akbasic/error.h>
#include <akbasic/runtime.h>
#include <akbasic/ui.h>
#include "harness.h"
#include "mockdevice.h"
#include "testutil.h"
/** @brief Bring up a runtime with the mock UI attached and a program loaded. */
static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
{
PREPARE_ERROR(errctx);
PASS(errctx, harness_start(NULL));
mock_devices_init();
PASS(errctx, akbasic_runtime_set_ui(&HARNESS_RUNTIME, &MOCK_UI));
PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 0));
SUCCEED_RETURN(errctx);
}
/** @brief The white of palette index 2, and the black of index 1. */
#define WHITE "#ffffff"
#define BLACK "#000000"
/**
* @brief MENU defines, redefines, retires one and retires all.
*/
static void test_menu(void)
{
TEST_REQUIRE_OK(run_program("10 MENU 1, \"START\", \"QUIT\"\n"));
TEST_REQUIRE_STR(MOCK.log, "menu 0 count 2 [START] [QUIT]\n");
TEST_REQUIRE(HARNESS_RUNTIME.ui_state.menudefined[0],
"MENU 1 should have marked menu 1 defined");
harness_stop();
/* A number and nothing else retires that one. */
TEST_REQUIRE_OK(run_program("10 MENU 2, \"A\"\n20 MENU 2\n"));
TEST_REQUIRE_STR(MOCK.log,
"menu 1 count 1 [A]\n"
"menu 1 count 0\n");
TEST_REQUIRE(!HARNESS_RUNTIME.ui_state.menudefined[1],
"a bare MENU n should have retired menu n");
harness_stop();
/* No arguments retires them all, the way a bare SOLID does. */
TEST_REQUIRE_OK(run_program("10 MENU 1, \"A\"\n20 MENU 3, \"B\"\n30 MENU\n"));
TEST_REQUIRE_STR(MOCK.log,
"menu 0 count 1 [A]\n"
"menu 2 count 1 [B]\n"
"menu 0 count 0\n"
"menu 1 count 0\n"
"menu 2 count 0\n"
"menu 3 count 0\n");
harness_stop();
/* Entries can be expressions, not only literals. */
TEST_REQUIRE_OK(run_program("10 A$ = \"LOAD\"\n20 MENU 1, A$ + \" GAME\"\n"));
TEST_REQUIRE_STR(MOCK.log, "menu 0 count 1 [LOAD GAME]\n");
harness_stop();
}
/**
* @brief MENU refuses a slot outside 1..4, a number where a string belongs, and
* more entries than libakgl's menu can hold.
*/
static void test_menu_refusals(void)
{
TEST_REQUIRE_OK(run_program("10 MENU 0, \"A\"\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "menu 0 is outside 1..4") != NULL,
"MENU 0 should refuse by name, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
TEST_REQUIRE_OK(run_program("10 MENU 5, \"A\"\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "menu 5 is outside 1..4") != NULL,
"MENU 5 should refuse by name, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
TEST_REQUIRE_OK(run_program("10 MENU 1, 42\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "MENU expected a string") != NULL,
"a numeric MENU entry should be a type error, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
/*
* A seventeenth entry is refused by the verb, and there is deliberately no
* test for it here: AKBASIC_MAX_TOKENS is 32, so seventeen strings and their
* commas exceed the line's token ceiling and the scanner refuses first. The
* check in akbasic_cmd_menu() is unreachable from BASIC as the scanner
* stands and is kept anyway -- it is the thing that would matter the day
* that ceiling moves, and a MENU that silently wrote past
* akgl_UiMenu::items would be a memory error rather than a diagnostic.
*/
}
/**
* @brief RMENU reads the highlight without consuming it, and the latch with.
*
* The read-and-clear on field 1 is the whole contract: without it a program
* polling in a loop sees the same choice forever. Same rule BUMP() carries.
*/
static void test_rmenu(void)
{
TEST_REQUIRE_OK(harness_start(NULL));
mock_devices_init();
TEST_REQUIRE_OK(akbasic_runtime_set_ui(&HARNESS_RUNTIME, &MOCK_UI));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
"10 MENU 1, \"A\", \"B\", \"C\"\n"
"20 PRINT RMENU(1,0)\n"
"30 PRINT RMENU(1,1)\n"
"40 PRINT RMENU(1,1)\n"
"50 PRINT RMENU(1,0)\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
/*
* The MENU line first, *then* the choice. Defining a menu resets its
* selection and its latch, so a choice made before line 10 runs is a choice
* into a menu that does not exist yet -- and line 10 would wipe it.
*
* Fifteen steps, not one. A step advances the program counter by one *line
* number*, not one statement, so line 10 runs on step 11 and line 20 on step
* 21; fifteen lands between them. Same reason the frontend's per-frame
* budget is 256.
*/
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 15));
mock_menu_choose(0, 1);
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
/*
* 2 -- entries are numbered from one, so the second is 2 and not 1.
* -1 -- BASIC true, once.
* 0 -- because reading it cleared it.
* 2 -- and reading the latch did not disturb the highlight.
*/
TEST_REQUIRE_STR(HARNESS_OUTPUT, "2\n-1\n0\n2\n");
harness_stop();
}
/** @brief RMENU refuses a bad slot and a field outside 0..1. */
static void test_rmenu_refusals(void)
{
TEST_REQUIRE_OK(run_program("10 PRINT RMENU(9,0)\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "menu 9 is outside 1..4") != NULL,
"RMENU should refuse menu 9, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
TEST_REQUIRE_OK(run_program("10 PRINT RMENU(1,7)\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "field 7 is outside 0..1") != NULL,
"RMENU should refuse field 7, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/**
* @brief GETMENU holds the step loop, then assigns the entry that was chosen.
*
* The property being asserted is that the step *returns* while the program does
* not advance -- section 1.6's whole point. A run of a hundred steps with
* nothing chosen must leave the program on the GETMENU line and must come back.
*/
static void test_getmenu_holds(void)
{
TEST_REQUIRE_OK(harness_start(NULL));
mock_devices_init();
TEST_REQUIRE_OK(akbasic_runtime_set_ui(&HARNESS_RUNTIME, &MOCK_UI));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
"10 MENU 1, \"A\", \"B\", \"C\"\n"
"20 GETMENU 1, C%\n"
"30 PRINT C%\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 100));
TEST_REQUIRE(HARNESS_RUNTIME.ui_state.waiting,
"GETMENU should still be holding after a hundred steps");
TEST_REQUIRE_STR(HARNESS_OUTPUT, "");
mock_menu_choose(0, 2);
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE(!HARNESS_RUNTIME.ui_state.waiting, "GETMENU should have released");
TEST_REQUIRE_STR(HARNESS_OUTPUT, "3\n");
harness_stop();
}
/**
* @brief A GETMENU that arrives after the choice does not wait at all.
*
* The latch survives between frames precisely so an unread activation is never
* lost, so a program that gets round to its GETMENU late must find the answer
* waiting rather than park for a second one.
*/
static void test_getmenu_takes_a_waiting_choice(void)
{
TEST_REQUIRE_OK(harness_start(NULL));
mock_devices_init();
TEST_REQUIRE_OK(akbasic_runtime_set_ui(&HARNESS_RUNTIME, &MOCK_UI));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
"10 MENU 1, \"A\", \"B\", \"C\"\n"
"20 GETMENU 1, C%\n"
"30 PRINT C%\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
/* Through line 10 only, then choose before line 20 ever runs. */
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 15));
mock_menu_choose(0, 2);
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE(!HARNESS_RUNTIME.ui_state.waiting,
"GETMENU should not have held when the choice was already made");
TEST_REQUIRE_STR(HARNESS_OUTPUT, "3\n");
harness_stop();
}
/**
* @brief Retiring the menu underneath a holding GETMENU releases it with 0.
*
* Reached from BASIC only through an interrupt handler, which is the one thing
* that can run a line while the program is parked; the menu is retired directly
* here because building a collision interrupt to do it would be testing
* something else.
*/
static void test_getmenu_released_by_retirement(void)
{
TEST_REQUIRE_OK(harness_start(NULL));
mock_devices_init();
TEST_REQUIRE_OK(akbasic_runtime_set_ui(&HARNESS_RUNTIME, &MOCK_UI));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
"10 MENU 1, \"A\", \"B\"\n"
"20 C% = 9\n"
"30 GETMENU 1, C%\n"
"40 PRINT C%\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 35));
TEST_REQUIRE(HARNESS_RUNTIME.ui_state.waiting, "GETMENU should be holding");
TEST_REQUIRE_OK(MOCK_UI.menu(&MOCK_UI, 0, NULL, 0));
HARNESS_RUNTIME.ui_state.menudefined[0] = false;
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE(!HARNESS_RUNTIME.ui_state.waiting,
"retiring the menu should have released the GETMENU");
TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\n");
harness_stop();
}
/**
* @brief Withdrawing the device releases a holding GETMENU rather than wedging it.
*
* A host is allowed to change its mind about what it lends out, and a script
* parked forever on a device that no longer exists is the worse outcome. Same
* rule akbasic_input_service() keeps for a withdrawn keyboard.
*/
static void test_getmenu_released_by_withdrawal(void)
{
TEST_REQUIRE_OK(harness_start(NULL));
mock_devices_init();
TEST_REQUIRE_OK(akbasic_runtime_set_ui(&HARNESS_RUNTIME, &MOCK_UI));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
"10 MENU 1, \"A\", \"B\"\n"
"20 C% = 9\n"
"30 GETMENU 1, C%\n"
"40 PRINT C%\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
/* Through line 30, where the GETMENU is; see the note in test_rmenu(). */
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 35));
TEST_REQUIRE(HARNESS_RUNTIME.ui_state.waiting, "GETMENU should be holding");
TEST_REQUIRE_OK(akbasic_runtime_set_ui(&HARNESS_RUNTIME, NULL));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE(!HARNESS_RUNTIME.ui_state.waiting,
"withdrawing the UI should have released the GETMENU");
/* Released, not chosen: 0 rather than the 9 the variable held. */
TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\n");
harness_stop();
}
/** @brief GETMENU refuses a menu that has no entries rather than holding forever. */
static void test_getmenu_needs_a_menu(void)
{
TEST_REQUIRE_OK(run_program("10 GETMENU 1, C%\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "has no entries") != NULL,
"GETMENU on an undefined menu should refuse, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
TEST_REQUIRE_OK(run_program("10 MENU 1, \"A\"\n20 GETMENU 1, A$\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "expected a numeric variable") != NULL,
"GETMENU into a string should be a type error, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/** @brief DIALOG puts the panel up with a string and takes it down with nothing. */
static void test_dialog(void)
{
TEST_REQUIRE_OK(run_program("10 DIALOG \"HELLO\"\n20 DIALOG\n"));
TEST_REQUIRE_STR(MOCK.log,
"dialog HELLO\n"
"dialog (none)\n");
harness_stop();
TEST_REQUIRE_OK(run_program("10 DIALOG 42\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "DIALOG expected a string") != NULL,
"a numeric DIALOG should be a type error, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/**
* @brief HUD sets each of the five anchors, retires one, and retires all.
*
* Five and not seven. libakgl's akgl_UiAnchor has the four corners and dead
* centre; there is no top-centre or bottom-centre, and this asserts that 5 is
* refused rather than quietly mapped onto something.
*/
static void test_hud(void)
{
TEST_REQUIRE_OK(run_program("10 HUD 1, 0, \"TL\"\n"
"20 HUD 2, 1, \"TR\"\n"
"30 HUD 3, 2, \"BL\"\n"
"40 HUD 4, 3, \"BR\"\n"
"50 HUD 5, 4, \"MID\"\n"));
TEST_REQUIRE_STR(MOCK.log,
"label 0 anchor 0 TL\n"
"label 1 anchor 1 TR\n"
"label 2 anchor 2 BL\n"
"label 3 anchor 3 BR\n"
"label 4 anchor 4 MID\n");
harness_stop();
TEST_REQUIRE_OK(run_program("10 HUD 2, 0, \"X\"\n20 HUD 2\n"));
TEST_REQUIRE_STR(MOCK.log,
"label 1 anchor 0 X\n"
"label 1 anchor 0 (none)\n");
harness_stop();
/* No arguments retires all eight, the way a bare MENU retires all four. */
TEST_REQUIRE_OK(run_program("10 HUD 1, 0, \"X\"\n20 HUD\n"));
TEST_REQUIRE_STR(MOCK.log,
"label 0 anchor 0 X\n"
"label 0 anchor 0 (none)\n"
"label 1 anchor 0 (none)\n"
"label 2 anchor 0 (none)\n"
"label 3 anchor 0 (none)\n"
"label 4 anchor 0 (none)\n"
"label 5 anchor 0 (none)\n"
"label 6 anchor 0 (none)\n"
"label 7 anchor 0 (none)\n");
harness_stop();
TEST_REQUIRE_OK(run_program("10 HUD 1, 5, \"NOPE\"\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "anchor 5 is outside 0..4") != NULL,
"HUD should refuse anchor 5, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
TEST_REQUIRE_OK(run_program("10 HUD 9, 0, \"NOPE\"\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "slot 9 is outside 1..8") != NULL,
"HUD should refuse slot 9, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
/* A slot and an anchor with nothing to say is a typo, not an empty label. */
TEST_REQUIRE_OK(run_program("10 HUD 1, 0\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "a slot, an anchor and a string") != NULL,
"a two-argument HUD should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/** @brief UISTYLE converts palette indices, defaults its tail, and resets. */
static void test_uistyle(void)
{
TEST_REQUIRE_OK(run_program("10 UISTYLE 1, 2, 2\n"));
TEST_REQUIRE_STR(MOCK.log,
"style " BLACK " " WHITE " " WHITE " pad 8.0 radius 0.0\n");
harness_stop();
TEST_REQUIRE_OK(run_program("10 UISTYLE 1, 2, 2, 12, 4\n"));
TEST_REQUIRE_STR(MOCK.log,
"style " BLACK " " WHITE " " WHITE " pad 12.0 radius 4.0\n");
harness_stop();
TEST_REQUIRE_OK(run_program("10 UISTYLE\n"));
TEST_REQUIRE_STR(MOCK.log, "style default\n");
harness_stop();
TEST_REQUIRE_OK(run_program("10 UISTYLE 17, 2, 2\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "RUNTIME ERROR") != NULL,
"UISTYLE should refuse palette index 17, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
TEST_REQUIRE_OK(run_program("10 UISTYLE 1, 2\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "a fill, an edge and an ink") != NULL,
"UISTYLE with two colours should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/** @brief NEW takes every widget down; a deleted program's menu must not survive it. */
static void test_new_retires_everything(void)
{
TEST_REQUIRE_OK(harness_start(NULL));
mock_devices_init();
TEST_REQUIRE_OK(akbasic_runtime_set_ui(&HARNESS_RUNTIME, &MOCK_UI));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 MENU 1, \"A\"\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE(HARNESS_RUNTIME.ui_state.menudefined[0], "the menu should be up");
mock_log_reset();
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 NEW\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE(strstr(MOCK.log, "uiclear") != NULL,
"NEW should have retired the widgets, log was \"%s\"", MOCK.log);
TEST_REQUIRE(!HARNESS_RUNTIME.ui_state.menudefined[0],
"NEW should have forgotten the menu");
harness_stop();
}
/**
* @brief Every verb in the group refuses by name when no UI device was lent.
*
* This is the standalone driver's situation and every no-SDL build's, so it is
* the common path rather than an edge case.
*/
static void test_no_device(void)
{
TEST_REQUIRE_OK(harness_start(NULL));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 MENU 1, \"A\"\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "MENU needs a UI device") != NULL,
"MENU without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
TEST_REQUIRE_OK(harness_start(NULL));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 DIALOG \"X\"\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "DIALOG needs a UI device") != NULL,
"DIALOG without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
TEST_REQUIRE_OK(harness_start(NULL));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 HUD 1, 0, \"X\"\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "HUD needs a UI device") != NULL,
"HUD without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
TEST_REQUIRE_OK(harness_start(NULL));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 UISTYLE 1, 2, 2\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "UISTYLE needs a UI device") != NULL,
"UISTYLE without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
TEST_REQUIRE_OK(harness_start(NULL));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 GETMENU 1, C%\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "GETMENU needs a UI device") != NULL,
"GETMENU without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
TEST_REQUIRE_OK(harness_start(NULL));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PRINT RMENU(1,0)\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "RMENU needs a UI device") != NULL,
"RMENU without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/** @brief A NULL runtime is refused rather than dereferenced. */
static void test_set_ui_validation(void)
{
TEST_REQUIRE_STATUS(akbasic_runtime_set_ui(NULL, NULL), AKERR_NULLPOINTER);
TEST_REQUIRE_STATUS(akbasic_ui_state_init(NULL), AKERR_NULLPOINTER);
}
int main(void)
{
test_menu();
test_menu_refusals();
test_rmenu();
test_rmenu_refusals();
test_getmenu_holds();
test_getmenu_takes_a_waiting_choice();
test_getmenu_released_by_retirement();
test_getmenu_released_by_withdrawal();
test_getmenu_needs_a_menu();
test_dialog();
test_hud();
test_uistyle();
test_new_retires_everything();
test_no_device();
test_set_ui_validation();
return akbasic_test_failures;
}