/** * @file structure_verbs.c * @brief Tests the group A verbs: DO, LOOP, BEGIN, BEND, ON and END. * * All of them are built on `waitingForCommand` -- a scope records the verb it is * skipping forward to and nothing runs until that verb turns up -- so most of * what is asserted here is *what did not run*. A block that should be skipped * printing nothing is the whole point, and it is invisible unless the test says * so explicitly. */ #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)); /* * Bounded. A defect in any of these verbs is a loop that never ends, and a * suite that hangs tells you far less than one that fails. */ PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 2000)); SUCCEED_RETURN(errctx); } /** @brief A condition on the top runs the body while it holds, and not at all when it does not. */ static void test_do_while(void) { TEST_REQUIRE_OK(run_program("10 I# = 0\n" "20 DO WHILE I# < 3\n" "30 PRINT I#\n" "40 I# = I# + 1\n" "50 LOOP\n" "60 PRINT \"DONE\"\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\n1\n2\nDONE\n"); harness_stop(); /* Already false: the body must not run once "just to see". */ TEST_REQUIRE_OK(run_program("10 I# = 5\n" "20 DO WHILE I# < 3\n" "30 PRINT \"NEVER\"\n" "40 LOOP\n" "50 PRINT \"AFTER\"\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "AFTER\n"); harness_stop(); } /** @brief A condition on the bottom runs the body at least once. */ static void test_loop_until(void) { TEST_REQUIRE_OK(run_program("10 I# = 0\n" "20 DO\n" "30 PRINT I#\n" "40 I# = I# + 1\n" "50 LOOP UNTIL I# = 3\n" "60 PRINT \"DONE\"\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\n1\n2\nDONE\n"); harness_stop(); /* * Already true at the bottom, so exactly one pass. This is the difference * between a bottom-tested loop and a top-tested one, and it is why BASIC has * both forms. */ TEST_REQUIRE_OK(run_program("10 I# = 99\n" "20 DO\n" "30 PRINT \"ONCE\"\n" "40 LOOP UNTIL I# = 99\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "ONCE\n"); harness_stop(); } /** @brief DO WHILE re-tests its own condition at the bottom, or it never ends. */ static void test_do_while_retests(void) { TEST_REQUIRE_OK(run_program("10 I# = 0\n" "20 DO WHILE I# < 2\n" "30 I# = I# + 1\n" "40 LOOP\n" "50 PRINT I#\n")); /* * That it printed at all is the assertion: a DO WHILE that failed to re-test * its condition at the bottom never reaches line 50, and the bounded run * above is what turns that into a failure rather than a hang. */ TEST_REQUIRE_STR(HARNESS_OUTPUT, "2\n"); harness_stop(); } /** @brief EXIT leaves a DO loop as well as a FOR loop. */ static void test_exit_from_do(void) { TEST_REQUIRE_OK(run_program("10 I# = 0\n" "20 DO\n" "30 I# = I# + 1\n" "40 IF I# = 2 THEN EXIT\n" "50 LOOP\n" "60 PRINT \"EXITED AT \" + I#\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "EXITED AT 2\n"); harness_stop(); /* And EXIT outside any loop is refused rather than doing something quiet. */ TEST_REQUIRE_OK(run_program("10 EXIT\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside the context of FOR or DO") != NULL, "a bare EXIT should be refused, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } /** @brief An infinite DO...LOOP is left by GOTO, and the step bound proves it ran. */ static void test_bare_do_loop(void) { TEST_REQUIRE_OK(run_program("10 I# = 0\n" "20 DO\n" "30 I# = I# + 1\n" "40 IF I# = 3 THEN GOTO 60\n" "50 LOOP\n" "60 PRINT I#\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "3\n"); harness_stop(); } /** @brief ON picks the nth target, one-based, and falls through when there is none. */ static void test_on_goto(void) { TEST_REQUIRE_OK(run_program("10 X# = 2\n" "20 ON X# GOTO 100, 200\n" "30 PRINT \"FELL THROUGH\"\n" "40 END\n" "100 PRINT \"ONE\"\n" "110 END\n" "200 PRINT \"TWO\"\n" "210 END\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "TWO\n"); harness_stop(); /* * Out of range is not an error. BASIC 7.0 falls through to the next * statement, which is what lets `ON X GOTO ...` be written without a bounds * check in front of it. Zero and negative fall through the same way. */ TEST_REQUIRE_OK(run_program("10 X# = 9\n" "20 ON X# GOTO 100, 200\n" "30 PRINT \"FELL THROUGH\"\n" "40 END\n" "100 PRINT \"ONE\"\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "FELL THROUGH\n"); harness_stop(); TEST_REQUIRE_OK(run_program("10 X# = 0\n" "20 ON X# GOTO 100\n" "30 PRINT \"FELL THROUGH\"\n" "40 END\n" "100 PRINT \"ONE\"\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "FELL THROUGH\n"); harness_stop(); } /** @brief ON ... GOSUB returns to the line after the ON. */ static void test_on_gosub(void) { TEST_REQUIRE_OK(run_program("10 X# = 1\n" "20 ON X# GOSUB 100, 200\n" "30 PRINT \"BACK\"\n" "40 END\n" "100 PRINT \"SUB ONE\"\n" "110 RETURN\n" "200 PRINT \"SUB TWO\"\n" "210 RETURN\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "SUB ONE\nBACK\n"); harness_stop(); } /** @brief A label is a legal ON target, since it evaluates to a line number. */ static void test_on_label(void) { TEST_REQUIRE_OK(run_program("10 X# = 1\n" "20 ON X# GOTO TARGET\n" "30 PRINT \"FELL THROUGH\"\n" "40 END\n" "100 LABEL TARGET\n" "110 PRINT \"AT LABEL\"\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "AT LABEL\n"); harness_stop(); } /** @brief BEGIN/BEND makes an IF span lines, and the arm not taken skips all of them. */ static void test_begin_bend(void) { TEST_REQUIRE_OK(run_program("10 A# = 1\n" "20 IF A# = 1 THEN BEGIN\n" "30 PRINT \"IN BLOCK\"\n" "40 PRINT \"STILL IN\"\n" "50 BEND\n" "60 PRINT \"AFTER\"\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "IN BLOCK\nSTILL IN\nAFTER\n"); harness_stop(); /* * The skipped case is the one that needs the wait. `skiprestofline` only * reaches the end of the IF's own line; without arming a skip to BEND the * block's lines would run as ordinary top-level lines. */ TEST_REQUIRE_OK(run_program("10 A# = 0\n" "20 IF A# = 1 THEN BEGIN\n" "30 PRINT \"NOT SEEN\"\n" "40 PRINT \"ALSO NOT SEEN\"\n" "50 BEND\n" "60 PRINT \"AFTER\"\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "AFTER\n"); harness_stop(); } /** @brief END stops the program without quitting a REPL session. */ static void test_end(void) { TEST_REQUIRE_OK(run_program("10 PRINT \"BEFORE\"\n" "20 END\n" "30 PRINT \"AFTER\"\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "BEFORE\n"); TEST_REQUIRE_INT(HARNESS_RUNTIME.mode, AKBASIC_MODE_QUIT); harness_stop(); /* * Started from a REPL, END goes back to the prompt rather than ending the * interpreter -- which is the whole difference between END and QUIT. */ TEST_REQUIRE_OK(harness_start(NULL)); TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PRINT \"HI\"\n20 END\n")); TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_REPL)); HARNESS_RUNTIME.mode = AKBASIC_MODE_RUN; HARNESS_RUNTIME.environment->nextline = 0; TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 40)); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "HI") != NULL, "the program should have run, got \"%s\"", HARNESS_OUTPUT); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "READY") != NULL, "END from a REPL session should return to the prompt, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } /** @brief Nested DO loops each keep their own condition and unwind in order. */ static void test_nested_do(void) { TEST_REQUIRE_OK(run_program("10 I# = 0\n" "20 DO WHILE I# < 2\n" "30 J# = 0\n" "40 DO WHILE J# < 2\n" "50 PRINT I# * 10 + J#\n" "60 J# = J# + 1\n" "70 LOOP\n" "80 I# = I# + 1\n" "90 LOOP\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\n1\n10\n11\n"); harness_stop(); } /** @brief LOOP without a DO is refused rather than silently doing nothing. */ static void test_loop_without_do(void) { TEST_REQUIRE_OK(run_program("10 LOOP\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside the context of DO") != NULL, "a bare LOOP should be refused, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } int main(void) { test_do_while(); test_loop_until(); test_do_while_retests(); test_exit_from_do(); test_bare_do_loop(); test_on_goto(); test_on_gosub(); test_on_label(); test_begin_bend(); test_end(); test_nested_do(); test_loop_without_do(); return akbasic_test_failures; }