Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of those turned out to have been fixed or never ported and nobody had written it down; the audit records the evidence for each. Two of the seventeen were real. math_plus mutated its left operand when the operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT coverage because NEXT relied on the mutation, so tests/for_next.c came first and NEXT now writes the counter back itself. And the binary operators summed both numeric fields of their right operand, which no BASIC program can reach -- that one needed a test written against the value API. Writing the tests turned up eight defects nobody had listed. Seven are fixed: IF A = 2 THEN was a parse error; only == worked IF ... AND ... was a parse error, because a condition parsed as one relation IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN EXIT before any NEXT restarted the program and exhausted the variable pool READ never found a DATA line above it, and swallowed the lines between PRINT 2 + 2 at the prompt was filed as program text instead of answering a short read discarded its bytes, so COPY produced empty files every verb taking an argument list said "peek() returned nil token!" on none The eighth is not fixed and cannot be quietly: a FOR whose step overshoots runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two errors cancel for a step of 1, which is why neither was noticed. Correcting them changes the expected output of a checked-in acceptance file, and tests/reference/README.md forbids editing one to suit this interpreter. It is tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct contract, and TODO.md items 19 and 20. Sprites are real libakgl actors with a renderfunc of their own, because akgl_actor_render draws every sprite square and an actor has no per-axis scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle or a 63-element integer array -- a string here cannot hold a zero byte. Verbs that need hardware that does not exist are refused by name with the reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream. 94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen clean. The Go acceptance corpus stayed green throughout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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, NULL));
|
|
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, 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;
|
|
}
|