509 lines
20 KiB
C
509 lines
20 KiB
C
|
|
/**
|
||
|
|
* @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;
|
||
|
|
}
|