272 lines
9.6 KiB
C
272 lines
9.6 KiB
C
|
|
/**
|
||
|
|
* @file housekeeping_verbs.c
|
||
|
|
* @brief Tests the group B verbs: NEW, CLR, CONT, TRON, TROFF, SWAP and HELP.
|
||
|
|
*
|
||
|
|
* The golden case at tests/language/housekeeping/verbs.bas covers what a running
|
||
|
|
* program can see of these. What is here is the rest: the refusals, the two
|
||
|
|
* verbs that only mean anything at a REPL prompt (CONT and HELP), and the state
|
||
|
|
* a program cannot PRINT.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include "harness.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, 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 Evaluate one immediate line against the current runtime state. */
|
||
|
|
static akerr_ErrorContext AKERR_NOIGNORE *immediate(const char *line)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_ASTLeaf *leaf = NULL;
|
||
|
|
akbasic_Value *out = NULL;
|
||
|
|
|
||
|
|
PASS(errctx, akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
||
|
|
PASS(errctx, harness_parse(line, &leaf));
|
||
|
|
PASS(errctx, akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief NEW empties the program *and* the variables; CLR keeps the program. */
|
||
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_new_and_clr(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||
|
|
PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, "10 PRINT \"KEPT\"\n"));
|
||
|
|
PASS(errctx, immediate("A# = 5"));
|
||
|
|
|
||
|
|
PASS(errctx, immediate("CLR"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "PRINT \"KEPT\"");
|
||
|
|
PASS(errctx, immediate("PRINT A#"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\n");
|
||
|
|
|
||
|
|
PASS(errctx, immediate("NEW"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "");
|
||
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->lineno, 0);
|
||
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->nextline, 0);
|
||
|
|
|
||
|
|
harness_stop();
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief CLR releases the value pool, so an array can be re-DIMmed indefinitely.
|
||
|
|
*
|
||
|
|
* The pool is a bump allocator and re-DIMming larger abandons the old slice
|
||
|
|
* (see akbasic_ValuePool), so a program that grows an array in a loop exhausts
|
||
|
|
* it. CLR is the only thing that hands the storage back, which makes this the
|
||
|
|
* assertion that says CLR did something rather than merely appearing to.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_clr_reclaims_storage(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
int i = 0;
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||
|
|
|
||
|
|
/* Without CLR this runs out; the loop count is well past the pool's size. */
|
||
|
|
for ( i = 0; i < 40; i++ ) {
|
||
|
|
PASS(errctx, immediate("DIM Z#(200)"));
|
||
|
|
PASS(errctx, immediate("CLR"));
|
||
|
|
}
|
||
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.valuepool.next, 0);
|
||
|
|
|
||
|
|
harness_stop();
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief CONT refuses when nothing stopped, and resumes when something did. */
|
||
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_cont(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_ASTLeaf *leaf = NULL;
|
||
|
|
akbasic_Value *out = NULL;
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||
|
|
|
||
|
|
/* Nothing has run, so there is nothing to continue. */
|
||
|
|
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
||
|
|
TEST_REQUIRE_OK(harness_parse("CONT", &leaf));
|
||
|
|
TEST_REQUIRE_STATUS(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out),
|
||
|
|
AKBASIC_ERR_STATE);
|
||
|
|
|
||
|
|
/* A STOP part-way through, and then a CONT that finishes the program. */
|
||
|
|
PASS(errctx, run_program("10 PRINT \"BEFORE\"\n"
|
||
|
|
"20 STOP\n"
|
||
|
|
"30 PRINT \"AFTER\"\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "BEFORE\nREADY\n");
|
||
|
|
TEST_REQUIRE(HARNESS_RUNTIME.stopped, "STOP should have armed CONT");
|
||
|
|
|
||
|
|
PASS(errctx, immediate("CONT"));
|
||
|
|
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
||
|
|
/*
|
||
|
|
* The READY is STOP's, not the end of the program's: akbasic_runtime_start()
|
||
|
|
* in RUN mode sets run_finished_mode to QUIT, so a program that runs off its
|
||
|
|
* own end stops rather than dropping to a prompt.
|
||
|
|
*/
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "BEFORE\nREADY\nAFTER\n");
|
||
|
|
TEST_REQUIRE(!HARNESS_RUNTIME.stopped, "CONT should have disarmed itself");
|
||
|
|
|
||
|
|
/* And a second CONT has nothing left to resume. */
|
||
|
|
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
||
|
|
TEST_REQUIRE_OK(harness_parse("CONT", &leaf));
|
||
|
|
TEST_REQUIRE_STATUS(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out),
|
||
|
|
AKBASIC_ERR_STATE);
|
||
|
|
|
||
|
|
harness_stop();
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief A RUN takes the CONT away: the program is starting over, not resuming. */
|
||
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_run_disarms_cont(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||
|
|
PASS(errctx, run_program("10 STOP\n"));
|
||
|
|
TEST_REQUIRE(HARNESS_RUNTIME.stopped, "STOP should have armed CONT");
|
||
|
|
|
||
|
|
PASS(errctx, immediate("RUN"));
|
||
|
|
TEST_REQUIRE(!HARNESS_RUNTIME.stopped, "RUN should have disarmed CONT");
|
||
|
|
|
||
|
|
harness_stop();
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief SWAP refuses what it cannot exchange, and tolerates the identity case. */
|
||
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_swap_refusals(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_ASTLeaf *leaf = NULL;
|
||
|
|
akbasic_Value *out = NULL;
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||
|
|
PASS(errctx, immediate("A# = 1"));
|
||
|
|
PASS(errctx, immediate("S$ = \"X\""));
|
||
|
|
|
||
|
|
/* Different types: the suffix is the only type declaration there is. */
|
||
|
|
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
||
|
|
TEST_REQUIRE_OK(harness_parse("SWAP A#, S$", &leaf));
|
||
|
|
TEST_REQUIRE_STATUS(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out),
|
||
|
|
AKBASIC_ERR_TYPE);
|
||
|
|
|
||
|
|
/* Not a variable. */
|
||
|
|
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
||
|
|
TEST_REQUIRE_OK(harness_parse("SWAP A#, 5", &leaf));
|
||
|
|
TEST_REQUIRE_STATUS(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out),
|
||
|
|
AKBASIC_ERR_SYNTAX);
|
||
|
|
|
||
|
|
/* Only one of them. */
|
||
|
|
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
||
|
|
TEST_REQUIRE_OK(harness_parse("SWAP A#", &leaf));
|
||
|
|
TEST_REQUIRE_STATUS(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out),
|
||
|
|
AKBASIC_ERR_SYNTAX);
|
||
|
|
|
||
|
|
/* A variable with itself is legal and leaves it alone, rather than corrupting it. */
|
||
|
|
PASS(errctx, immediate("SWAP A#, A#"));
|
||
|
|
PASS(errctx, immediate("PRINT A#"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n");
|
||
|
|
|
||
|
|
harness_stop();
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief HELP re-lists the line the last error happened on, and says so when there is none. */
|
||
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_help(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||
|
|
PASS(errctx, immediate("HELP"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "NO ERROR TO HELP WITH\n");
|
||
|
|
|
||
|
|
/* An out-of-bounds read on line 20, then HELP. */
|
||
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||
|
|
PASS(errctx, run_program("10 DIM Q#(2)\n"
|
||
|
|
"20 PRINT Q#(9)\n"
|
||
|
|
"30 PRINT \"UNREACHED\"\n"));
|
||
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.errorline, 20);
|
||
|
|
PASS(errctx, immediate("HELP"));
|
||
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "20 PRINT Q#(9)") != NULL,
|
||
|
|
"HELP should re-list the line the error happened on, got: %s", HARNESS_OUTPUT);
|
||
|
|
|
||
|
|
harness_stop();
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief TRON and TROFF move one flag, and the flag is what the run loop reads. */
|
||
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_trace_flag(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||
|
|
TEST_REQUIRE(!HARNESS_RUNTIME.trace, "tracing starts off");
|
||
|
|
PASS(errctx, immediate("TRON"));
|
||
|
|
TEST_REQUIRE(HARNESS_RUNTIME.trace, "TRON turns tracing on");
|
||
|
|
PASS(errctx, immediate("TROFF"));
|
||
|
|
TEST_REQUIRE(!HARNESS_RUNTIME.trace, "TROFF turns tracing off");
|
||
|
|
|
||
|
|
harness_stop();
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief An interactive session that errors and then ends does not spin.
|
||
|
|
*
|
||
|
|
* Not a group B verb, but found while testing them and fixed alongside: the
|
||
|
|
* runtime's error class is deliberately sticky, and with run_finished_mode REPL
|
||
|
|
* that meant every later step re-entered REPL mode and printed READY again --
|
||
|
|
* overwriting the QUIT that end of input had just set. A session that hit one
|
||
|
|
* runtime error printed READY forever instead of exiting.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_repl_stops_after_an_error(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
int steps = 0;
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(harness_start("10 PRINT NOSUCHTHING\nRUN\n"));
|
||
|
|
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_REPL));
|
||
|
|
|
||
|
|
/* Bounded, so a regression fails this test rather than hanging the suite. */
|
||
|
|
while ( steps < 64 && HARNESS_RUNTIME.mode != AKBASIC_MODE_QUIT ) {
|
||
|
|
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1));
|
||
|
|
steps += 1;
|
||
|
|
}
|
||
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.mode, AKBASIC_MODE_QUIT);
|
||
|
|
TEST_REQUIRE(steps < 64, "the REPL should quit at end of input, not spin");
|
||
|
|
|
||
|
|
harness_stop();
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
int main(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
ATTEMPT {
|
||
|
|
CATCH(errctx, test_new_and_clr());
|
||
|
|
CATCH(errctx, test_clr_reclaims_storage());
|
||
|
|
CATCH(errctx, test_cont());
|
||
|
|
CATCH(errctx, test_run_disarms_cont());
|
||
|
|
CATCH(errctx, test_swap_refusals());
|
||
|
|
CATCH(errctx, test_help());
|
||
|
|
CATCH(errctx, test_trace_flag());
|
||
|
|
CATCH(errctx, test_repl_stops_after_an_error());
|
||
|
|
} CLEANUP {
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} HANDLE_DEFAULT(errctx) {
|
||
|
|
LOG_ERROR_WITH_MESSAGE(errctx, "housekeeping verb test failed");
|
||
|
|
akbasic_test_failures += 1;
|
||
|
|
} FINISH_NORETURN(errctx);
|
||
|
|
|
||
|
|
return akbasic_test_failures;
|
||
|
|
}
|