process_line_runstream() chose between the stripped line and the raw buffer on obj->mode == AKBASIC_MODE_REPL, but nothing reaches that function except the RUNSTREAM arm of step(). The stripped branch was dead, so every program the driver ran from a file was stored with its own line number inside source[]: LIST printed "10 10 PRINT", and DSAVE and HELP would have echoed it the same way. No .bas in either corpus calls LIST, and runtime_verbs.c's load() helper -- which claims to load "the way RUNSTREAM would" -- already stored the scanned line, so it encoded the correct contract and could not catch the wrong one. The new test drives the real sink path instead. Keep the step-over-a-leading-number allowance in scan_line_labels() and skip_lineno(): no path inside the library stores a raw line now, but akbasic_runtime_store_line() is public and a host may hand it one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
274 lines
11 KiB
C
274 lines
11 KiB
C
/**
|
|
* @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 <stdio.h>
|
|
#include <unistd.h>
|
|
|
|
#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_store_line(&HARNESS_RUNTIME,
|
|
HARNESS_RUNTIME.environment->lineno, 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;
|
|
}
|