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