/** * @file runtime_verbs.c * @brief Tests the verbs the golden corpus never reaches. * * LIST, DELETE, AUTO, INPUT, DLOAD, DSAVE, EXIT, STOP and the memory verbs are * all real code with no `.bas`/`.txt` pair behind them, because the reference's * own corpus never exercises them either. They are driven through a whole * runtime here rather than unit-tested in pieces: most of them exist to mutate * runtime state, and that is the thing worth asserting. */ #include #include #include "harness.h" /* Run one line as though it had been typed at the REPL. */ static akerr_ErrorContext AKERR_NOIGNORE *run_line(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); } /* Load a program into the source table the way RUNSTREAM would. */ static void load(const char *lines[], int count) { char scanned[AKBASIC_MAX_LINE_LENGTH]; int i = 0; for ( i = 0; i < count; i++ ) { TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment)); TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, lines[i], scanned, sizeof(scanned))); TEST_REQUIRE_OK(akbasic_runtime_file_line(&HARNESS_RUNTIME, scanned)); } } static void clear_output(void) { memset(HARNESS_OUTPUT, 0, sizeof(HARNESS_OUTPUT)); rewind(HARNESS_OUT); } /** * @brief A program read through RUNSTREAM is stored without its line numbers. * * The load() helper above files what the scanner stripped, which is what every * caller inside the library does -- but RUNSTREAM used to file the raw buffer, * so a program run from a file held "10 PRINT" under slot 10 and LIST, DSAVE * and HELP would each have printed the number twice. This drives the real sink * path rather than the helper, because the helper is exactly what was not * happening. */ static void test_runstream_strips_line_numbers(void) { TEST_REQUIRE_OK(harness_start("10 PRINT \"ONE\"\n20 PRINT \"TWO\"\n")); TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUNSTREAM)); TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0)); TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "PRINT \"ONE\""); TEST_REQUIRE_STR(HARNESS_RUNTIME.source[20].code, "PRINT \"TWO\""); /* And LIST therefore prints each number once. */ clear_output(); TEST_REQUIRE_OK(run_line("LIST")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "10 PRINT \"ONE\"\n20 PRINT \"TWO\"\n"); harness_stop(); } int main(void) { static const char *PROGRAM[] = { "10 PRINT \"ONE\"", "20 PRINT \"TWO\"", "30 PRINT \"THREE\"", }; char tmpname[] = "/tmp/akbasic_dsave_XXXXXX"; char buffer[256]; akbasic_Value *out = NULL; FILE *saved = NULL; int fd = -1; TEST_REQUIRE_OK(harness_start("42\n")); /* PRINT with no argument emits a bare newline. */ clear_output(); TEST_REQUIRE_OK(run_line("PRINT")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "\n"); /* * LET does nothing; the assignment already happened during evaluation. A * bare LET with no assignment after it is a parse error, in the reference * too -- its parse path is just assignment(), which needs an expression. */ TEST_REQUIRE_OK(run_line("LET A# = 5")); TEST_REQUIRE_ANY_ERROR(run_line("LET")); /* LIST prints the program with its line numbers restored. */ load(PROGRAM, 3); clear_output(); TEST_REQUIRE_OK(run_line("LIST")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "10 PRINT \"ONE\"\n20 PRINT \"TWO\"\n30 PRINT \"THREE\"\n"); /* LIST n lists from n to the end. */ clear_output(); TEST_REQUIRE_OK(run_line("LIST 20")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "20 PRINT \"TWO\"\n30 PRINT \"THREE\"\n"); /* LIST -n lists from the start through n. */ clear_output(); TEST_REQUIRE_OK(run_line("LIST -20")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "10 PRINT \"ONE\"\n20 PRINT \"TWO\"\n"); /* LIST n-n lists an inclusive range. */ clear_output(); TEST_REQUIRE_OK(run_line("LIST 20-30")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "20 PRINT \"TWO\"\n30 PRINT \"THREE\"\n"); /* DSAVE writes the program out; DLOAD reads it back. */ fd = mkstemp(tmpname); TEST_REQUIRE(fd >= 0, "could not create a temporary file"); if ( fd >= 0 ) { close(fd); TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment)); snprintf(buffer, sizeof(buffer), "DSAVE \"%s\"", tmpname); TEST_REQUIRE_OK(run_line(buffer)); saved = fopen(tmpname, "r"); TEST_REQUIRE(saved != NULL, "DSAVE produced no file"); if ( saved != NULL ) { TEST_REQUIRE(fgets(buffer, sizeof(buffer), saved) != NULL, "DSAVE wrote nothing"); TEST_REQUIRE_STR(buffer, "10 PRINT \"ONE\"\n"); fclose(saved); } /* DELETE with no range clears the whole program. */ TEST_REQUIRE_OK(run_line("DELETE")); clear_output(); TEST_REQUIRE_OK(run_line("LIST")); TEST_REQUIRE_STR(HARNESS_OUTPUT, ""); /* DLOAD brings it back. */ snprintf(buffer, sizeof(buffer), "DLOAD \"%s\"", tmpname); TEST_REQUIRE_OK(run_line(buffer)); clear_output(); TEST_REQUIRE_OK(run_line("LIST")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "10 PRINT \"ONE\"\n20 PRINT \"TWO\"\n30 PRINT \"THREE\"\n"); remove(tmpname); } /* DELETE n-n removes an inclusive range. */ TEST_REQUIRE_OK(run_line("DELETE 20-30")); clear_output(); TEST_REQUIRE_OK(run_line("LIST")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "10 PRINT \"ONE\"\n"); /* A missing file is an error, not a crash. */ TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment)); TEST_REQUIRE_ANY_ERROR(run_line("DLOAD \"/nonexistent/akbasic/nope.bas\"")); /* An empty filename is refused before it reaches aksl_fopen. */ TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment)); TEST_REQUIRE_STATUS(run_line("DLOAD \"\""), AKBASIC_ERR_VALUE); TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment)); TEST_REQUIRE_STATUS(run_line("DLOAD 5"), AKBASIC_ERR_TYPE); /* AUTO sets and clears the line-numbering increment. */ TEST_REQUIRE_OK(run_line("AUTO 10")); TEST_REQUIRE_INT(HARNESS_RUNTIME.autoLineNumber, 10); TEST_REQUIRE_OK(run_line("AUTO")); TEST_REQUIRE_INT(HARNESS_RUNTIME.autoLineNumber, 0); /* INPUT prompts and stores. The harness input stream holds "42". */ clear_output(); TEST_REQUIRE_OK(run_line("INPUT \"AGE? \" N#")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "AGE? "); TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment)); { akbasic_ASTLeaf *leaf = NULL; TEST_REQUIRE_OK(harness_parse("A# = N#", &leaf)); TEST_REQUIRE_OK(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out)); TEST_REQUIRE_INT(out->intval, 42); } /* Mode transitions. */ TEST_REQUIRE_OK(run_line("STOP")); TEST_REQUIRE_INT(HARNESS_RUNTIME.mode, AKBASIC_MODE_REPL); TEST_REQUIRE_OK(run_line("RUN")); TEST_REQUIRE_INT(HARNESS_RUNTIME.mode, AKBASIC_MODE_RUN); TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->nextline, 0); TEST_REQUIRE_OK(run_line("RUN 20")); TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->nextline, 20); /* * QUIT sets a mode. It must not call exit(): a host game decides what * quitting means, and this test would not survive it doing otherwise. */ TEST_REQUIRE_OK(run_line("QUIT")); TEST_REQUIRE_INT(HARNESS_RUNTIME.mode, AKBASIC_MODE_QUIT); TEST_REQUIRE_OK(akbasic_runtime_set_mode(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); /* State errors: verbs used outside the structure they need. */ TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment)); TEST_REQUIRE_STATUS(run_line("RETURN"), AKBASIC_ERR_STATE); TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment)); TEST_REQUIRE_STATUS(run_line("EXIT"), AKBASIC_ERR_STATE); TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment)); TEST_REQUIRE_STATUS(run_line("NEXT I#"), AKBASIC_ERR_STATE); /* GOTO and GOSUB want an integer. */ TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment)); TEST_REQUIRE_STATUS(run_line("GOTO \"nope\""), AKBASIC_ERR_TYPE); /* GOTO sets the next line. */ TEST_REQUIRE_OK(run_line("GOTO 30")); TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->nextline, 30); /* GOSUB pushes an environment; RETURN pops it and restores the line. */ { akbasic_Environment *before = HARNESS_RUNTIME.environment; TEST_REQUIRE_OK(akbasic_environment_zero(before)); before->lineno = 100; TEST_REQUIRE_OK(run_line("GOSUB 500")); TEST_REQUIRE(HARNESS_RUNTIME.environment != before, "GOSUB must push an environment"); TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->nextline, 500); TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->gosubReturnLine, 101); TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment)); TEST_REQUIRE_OK(run_line("RETURN")); TEST_REQUIRE(HARNESS_RUNTIME.environment == before, "RETURN must pop back"); TEST_REQUIRE_INT(before->nextline, 101); } /* LABEL records the current line under a name. */ TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment)); HARNESS_RUNTIME.environment->lineno = 70; TEST_REQUIRE_OK(run_line("LABEL AGAIN")); { int64_t where = 0; TEST_REQUIRE_OK(akbasic_environment_get_label(HARNESS_RUNTIME.environment, "AGAIN", &where)); TEST_REQUIRE_INT(where, 70); } /* POKE, PEEK and POINTER round-trip through real memory. */ TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment)); TEST_REQUIRE_OK(run_line("V# = 255")); TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment)); TEST_REQUIRE_OK(run_line("P# = POINTER(V#)")); TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment)); TEST_REQUIRE_OK(run_line("Q# = PEEK(P#)")); TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment)); TEST_REQUIRE_OK(run_line("POKE P#, 127")); TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment)); TEST_REQUIRE_OK(run_line("R# = POINTERVAR(V#)")); /* PEEK through a null address is refused rather than segfaulting. */ TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment)); TEST_REQUIRE_OK(run_line("Z# = 0")); TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment)); TEST_REQUIRE_STATUS(run_line("Y# = PEEK(Z#)"), AKBASIC_ERR_VALUE); harness_stop(); test_runstream_strips_line_numbers(); return akbasic_test_failures; }