292 lines
8.9 KiB
C
292 lines
8.9 KiB
C
|
|
/**
|
||
|
|
* @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 <string.h>
|
||
|
|
|
||
|
|
#include <akbasic/error.h>
|
||
|
|
#include <akbasic/runtime.h>
|
||
|
|
|
||
|
|
#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;
|
||
|
|
}
|