Files
akbasic/tests/input_verbs.c

198 lines
7.3 KiB
C
Raw Normal View History

Read the keyboard: implement GET, GETKEY and SCNCLR The poll_key half of group E, against the akbasic_InputBackend record. The libakgl implementation behind it is akgl_controller_poll_key(), which drains a ring the library fills from SDL events the host pumps -- so the interpreter can answer "is there a key waiting" without owning an event loop, which is what goal 3 requires. GET and GETKEY differ in one way and it is the interesting one. GET takes whatever is there including nothing, and an empty buffer is success with the empty string rather than an error -- that is what happens on most iterations of every GET loop ever written, and upstream is explicit that its poll reports it the same way. GETKEY waits, and since the library may not block, waiting is spelled as holding the step loop: the verb sets a flag and akbasic_runtime_step() declines to advance until a key arrives. Every step still returns and a bounded run() still comes back, so a host keeps its frame rate; the program simply does not move past the GETKEY. The PLAY queue is serviced before that check on purpose -- music should keep playing while a program waits for a keypress. Withdrawing the input device while a GETKEY is holding releases it rather than wedging the script on a device that no longer exists. Both verbs accept an integer variable as well as a string one and give it the raw key code, which is what a program testing for cursor or function keys needs. No key is code zero, matching what a C128 reports. A float variable is refused: it is neither a character nor a code. SCNCLR goes through the text sink rather than a device, because the sink is where PRINT already goes and is the only thing that knows what a screen means for this host. The stdio sink treats it as a no-op; clearing a pipe means nothing. One thing worth knowing before writing any bounded-run test, and now commented in tests/input_verbs.c: source lines are stored indexed by line number and the cursor starts at zero, so a step is spent on each empty slot along the way. A program at line 10 needs eleven steps before it has run anything. 70/70 ctest, clean under -Wall -Wextra, doxygen clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:35:05 -04:00
/**
* @file input_verbs.c
* @brief Tests GET, GETKEY and SCNCLR against the recording mock backend.
*
* The interesting assertion in here is that GETKEY holds the program without
* blocking the caller. The test drives the step loop by hand and checks that the
* program counter does not move while the key buffer is empty, that the step
* still returns, and that the program resumes the moment a key turns up.
*/
#include <string.h>
#include <akbasic/error.h>
#include <akbasic/input.h>
#include <akbasic/runtime.h>
#include "harness.h"
#include "mockdevice.h"
#include "testutil.h"
/** @brief Bring up a runtime with the mock devices 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_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS,
&MOCK_AUDIO, &MOCK_INPUT));
PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
SUCCEED_RETURN(errctx);
}
/**
* @brief GET on an empty buffer yields the empty string and no error.
*
* This is the common case, not the edge case: it is what happens on most
* iterations of every GET loop ever written.
*/
static void test_get_empty(void)
{
TEST_REQUIRE_OK(run_program("10 GET A$\n20 PRINT \"[\" + A$ + \"]\"\n"));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "[]\n");
harness_stop();
}
/** @brief GET takes one key per call, in the order they were typed. */
static void test_get_drains_in_order(void)
{
TEST_REQUIRE_OK(run_program("10 GET A$\n20 PRINT A$\n30 GET A$\n40 PRINT A$\n"));
mock_push_keys("HI");
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "H\nI\n");
harness_stop();
}
/**
* @brief A numeric variable receives the key code rather than the character.
*
* That is what a program testing for a cursor key or a function key wants, and
* no key is code zero -- which is what a C128 reports too.
*/
static void test_get_numeric(void)
{
TEST_REQUIRE_OK(run_program("10 GET A#\n20 PRINT A#\n"));
mock_push_keys("A");
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "65\n");
harness_stop();
TEST_REQUIRE_OK(run_program("10 GET A#\n20 PRINT A#\n"));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\n");
harness_stop();
}
/**
* @brief GETKEY holds the program without blocking the caller.
*
* The assertion that matters. A C128 sits on the keyboard inside GETKEY; here
* every step still returns, a bounded run() still comes back, and the program
* simply does not advance -- which is the part a program author cares about.
*/
static void test_getkey_holds_without_blocking(void)
{
int i = 0;
/*
* Lines 1 and 2 rather than the customary 10 and 20, because a step budget
* has to clear them. Source lines are stored indexed by line number and the
* cursor starts at zero, so a step spends itself on each empty slot on the
* way: a program at line 10 needs eleven steps before it has run anything.
* That is worth knowing before writing any bounded-run test.
*/
TEST_REQUIRE_OK(run_program("1 GETKEY A$\n2 PRINT \"GOT \" + A$\n"));
/*
* A bounded run with an empty buffer returns rather than hanging, and the
* program has not reached line 2. If GETKEY blocked, this call would never
* come back and the test would time out instead of failing.
*/
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 20));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "");
TEST_REQUIRE(HARNESS_RUNTIME.input_state.waiting,
"GETKEY should be holding the program");
/* Any number of further steps changes nothing while the buffer is empty. */
for ( i = 0; i < 10; i++ ) {
TEST_REQUIRE_OK(akbasic_runtime_step(&HARNESS_RUNTIME));
}
TEST_REQUIRE_STR(HARNESS_OUTPUT, "");
/* A keystroke releases it, and the program picks up where it left off. */
mock_push_keys("X");
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "GOT X\n");
TEST_REQUIRE(!HARNESS_RUNTIME.input_state.waiting,
"GETKEY should have released once a key arrived");
harness_stop();
}
/** @brief A key already waiting satisfies GETKEY on the spot. */
static void test_getkey_immediate(void)
{
TEST_REQUIRE_OK(run_program("10 GETKEY A$\n20 PRINT \"GOT \" + A$\n"));
mock_push_keys("Z");
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "GOT Z\n");
TEST_REQUIRE(!HARNESS_RUNTIME.input_state.waiting,
"GETKEY should not have held when a key was already waiting");
harness_stop();
}
/**
* @brief Taking the device away mid-GETKEY releases the hold rather than wedging.
*
* A host is allowed to change its mind about what it lends out, and a script
* stuck forever on a device that no longer exists is the worst of the available
* outcomes.
*/
static void test_getkey_device_withdrawn(void)
{
TEST_REQUIRE_OK(run_program("1 GETKEY A$\n2 PRINT \"RELEASED\"\n"));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 20));
TEST_REQUIRE(HARNESS_RUNTIME.input_state.waiting, "GETKEY should be holding");
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, NULL, NULL));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "RELEASED\n");
harness_stop();
}
/** @brief SCNCLR goes through the sink, so it needs no input device at all. */
static void test_scnclr(void)
{
TEST_REQUIRE_OK(harness_start(NULL));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 SCNCLR\n20 PRINT \"AFTER\"\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
/* The stdio sink treats a clear as a no-op: clearing a pipe means nothing. */
TEST_REQUIRE_STR(HARNESS_OUTPUT, "AFTER\n");
harness_stop();
}
/** @brief GET and GETKEY name themselves when there is no device, and check types. */
static void test_errors(void)
{
TEST_REQUIRE_OK(harness_start(NULL));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 GET 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, "GET needs an input device") != NULL,
"GET without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
/* A float variable is neither a character nor a key code. */
TEST_REQUIRE_OK(run_program("10 GET A%\n"));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "string or integer variable") != NULL,
"GET into a float should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
int main(void)
{
test_get_empty();
test_get_drains_in_order();
test_get_numeric();
test_getkey_holds_without_blocking();
test_getkey_immediate();
test_getkey_device_withdrawn();
test_scnclr();
test_errors();
return akbasic_test_failures;
}