/** * @file machine_verbs.c * @brief Tests the group J verbs: FETCH, STASH and SYS. * * These operate on real process memory, exactly as POKE, PEEK and POINTER * already do, so the tests give them real addresses -- their own buffers, taken * with POINTER the way a BASIC program would have to. */ #include #include #include #include "harness.h" #include "testutil.h" /** @brief Run a program to completion in RUN mode, from a string. */ static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source) { PREPARE_ERROR(errctx); PASS(errctx, harness_start(NULL)); 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 STASH copies bytes, and FETCH copies them back. * * Addresses come from POINTER, which is the only way a BASIC program can name a * real one. Two string variables give two buffers to copy between. */ static void test_copy(void) { TEST_REQUIRE_OK(run_program("10 A$ = \"SOURCE\"\n" "20 B$ = \"XXXXXX\"\n" "30 STASH 7, POINTER(A$), POINTER(B$)\n" "40 PRINT B$\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "SOURCE\n"); harness_stop(); /* FETCH is the same copy; there is no expansion RAM to distinguish them. */ TEST_REQUIRE_OK(run_program("10 A$ = \"HELLO!\"\n" "20 B$ = \"......\"\n" "30 FETCH 7, POINTER(A$), POINTER(B$)\n" "40 PRINT B$\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "HELLO!\n"); harness_stop(); } /** @brief A zero-length copy does nothing, and a negative one is refused. */ static void test_copy_bounds(void) { TEST_REQUIRE_OK(run_program("10 A$ = \"ABC\"\n" "20 B$ = \"XYZ\"\n" "30 STASH 0, POINTER(A$), POINTER(B$)\n" "40 PRINT B$\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "XYZ\n"); harness_stop(); TEST_REQUIRE_OK(run_program("10 A$ = \"ABC\"\n" "20 STASH -1, POINTER(A$), POINTER(A$)\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "negative number of bytes") != NULL, "a negative count should be refused, got \"%s\"", HARNESS_OUTPUT); harness_stop(); /* Address zero is refused rather than dereferenced. */ TEST_REQUIRE_OK(run_program("10 A$ = \"ABC\"\n" "20 STASH 3, 0, POINTER(A$)\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "address zero") != NULL, "address zero should be refused, got \"%s\"", HARNESS_OUTPUT); harness_stop(); /* Too few arguments is a syntax error naming the form. */ TEST_REQUIRE_OK(run_program("10 STASH 3\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "source address") != NULL, "an incomplete STASH should name the form, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } /** * @brief SYS parses and is then refused by name. * * Parsing first is what keeps a listing containing SYS loadable and listable; it * is only running it that has no meaning. */ static void test_sys_refused(void) { TEST_REQUIRE_OK(run_program("10 SYS 65490\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "no 6502") != NULL, "SYS should be refused with a reason, got \"%s\"", HARNESS_OUTPUT); harness_stop(); /* * A SYS with no address is a syntax error, not the device refusal -- and it * names the verb. It used to fail deep in the expression parser with * "peek() returned nil token!", which every verb taking an argument list * shared. */ TEST_REQUIRE_OK(run_program("10 SYS\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "SYS expected at least one argument") != NULL, "a bare SYS should name the verb, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } int main(void) { test_copy(); test_copy_bounds(); test_sys_refused(); return akbasic_test_failures; }