/** * @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 #include #include #include #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; }