Files
akbasic/tests/housekeeping_verbs.c

272 lines
9.6 KiB
C
Raw Normal View History

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