/** * @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); } /** * @brief A statement typed with no line number runs; one with a number is stored. * * Direct mode. The reference only ran the verbs it marked immediate -- RUN, LIST, * NEW and the rest -- and filed everything else as program text, so `PRINT 2 + 2` * at a prompt silently became line 0 of a program instead of answering. See * TODO.md section 5. */ static akerr_ErrorContext AKERR_NOIGNORE *test_direct_mode(void) { PREPARE_ERROR(errctx); TEST_REQUIRE_OK(harness_start("PRINT 2 + 2\n" "A# = 5\n" "PRINT A# * 3\n" "10 PRINT \"STORED\"\n" "RUN\n")); PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_REPL)); PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 200)); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "4\n") != NULL, "PRINT 2 + 2 should have answered 4, got \"%s\"", HARNESS_OUTPUT); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "15\n") != NULL, "an assignment then a PRINT should both have run, got \"%s\"", HARNESS_OUTPUT); /* And the numbered line was stored rather than run when it was typed. */ TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "PRINT \"STORED\""); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "STORED") != NULL, "RUN should have run the stored line, got \"%s\"", HARNESS_OUTPUT); 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()); CATCH(errctx, test_direct_mode()); } 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; }