Files
akbasic/tests/read_data.c
Logikoma d715bc0625
Some checks failed
akbasic CI Build / coverage (push) Failing after 27s
akbasic CI Build / cmake_build (push) Failing after 33s
akbasic CI Build / akgl_build (push) Failing after 18s
akbasic CI Build / sanitizers (push) Failing after 1m7s
akbasic CI Build / mutation_test (push) Failing after 39s
Report prescan errors on their source lines
2026-08-05 06:40:14 -04:00

264 lines
8.1 KiB
C

/**
* @file read_data.c
* @brief Tests READ, DATA and RESTORE against the pre-scanned item list.
*
* The reference's READ does not read: it records its identifiers, sets the scope
* waiting for a DATA verb, and lets execution skip forward until one turns up.
* Two things follow, and both are asserted here as the defects they were --
* TODO.md section 6.
*/
#include <stdio.h>
#include <string.h>
#include <akbasic/error.h>
#include <akbasic/runtime.h>
#include "harness.h"
#include "testutil.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, harness_start(NULL));
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 The ordinary case: DATA below its READ, filled in order. */
static void test_read_in_order(void)
{
TEST_REQUIRE_OK(run_program("10 READ A$, B#\n"
"20 DATA \"HELLO\", 12345\n"
"30 PRINT A$\n"
"40 PRINT B#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "HELLO\n12345\n");
harness_stop();
}
/**
* @brief A DATA line *above* its READ is found.
*
* It was not: skipping forward from the READ ran off the end of the program in
* silence, and a second READ re-read the same line.
*/
static void test_data_before_read(void)
{
TEST_REQUIRE_OK(run_program("10 DATA 1, 2\n"
"20 READ A#, B#\n"
"30 PRINT A#\n"
"40 PRINT B#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n2\n");
harness_stop();
}
/**
* @brief Lines between a READ and its DATA are not skipped.
*
* They were, because the skip-forward *was* the implementation. A C128 runs
* them: READ takes the next item and execution carries on normally.
*/
static void test_no_skipping(void)
{
TEST_REQUIRE_OK(run_program("10 READ A#\n"
"20 PRINT \"BETWEEN\"\n"
"30 DATA 5\n"
"40 PRINT A#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "BETWEEN\n5\n");
harness_stop();
}
/** @brief Successive READs walk the cursor along rather than starting over. */
static void test_cursor_advances(void)
{
TEST_REQUIRE_OK(run_program("10 DATA 1, 2, 3\n"
"20 READ A#\n"
"30 READ B#\n"
"40 READ C#\n"
"50 PRINT A# + B# + C#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "6\n");
harness_stop();
}
/** @brief Bare RESTORE goes back to the first item. */
static void test_restore(void)
{
TEST_REQUIRE_OK(run_program("10 DATA 1, 2, 3\n"
"20 READ A#\n"
"30 RESTORE\n"
"40 READ B#\n"
"50 PRINT A#\n"
"60 PRINT B#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n1\n");
harness_stop();
}
/** @brief RESTORE (line) starts again at the first item on or after that line. */
static void test_restore_line(void)
{
TEST_REQUIRE_OK(run_program("10 DATA 1, 2\n"
"20 DATA 30, 40\n"
"30 RESTORE 20\n"
"40 READ A#, B#\n"
"50 PRINT A#\n"
"60 PRINT B#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "30\n40\n");
harness_stop();
/*
* A line with no DATA on it finds the next one that has some -- a program
* names the line it wants to read *from*, not one that must hold items.
*/
TEST_REQUIRE_OK(run_program("10 DATA 1, 2\n"
"20 REM NOTHING HERE\n"
"30 DATA 99\n"
"40 RESTORE 20\n"
"50 READ A#\n"
"60 PRINT A#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "99\n");
harness_stop();
}
/** @brief Reading past the last item is reported rather than yielding zeroes. */
static void test_out_of_data(void)
{
TEST_REQUIRE_OK(run_program("10 DATA 1\n"
"20 READ A#, B#\n"
"30 PRINT \"UNREACHED\"\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "OUT OF DATA") != NULL,
"reading past the end should say so, got \"%s\"", HARNESS_OUTPUT);
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "UNREACHED") == NULL,
"the program should have stopped, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/** @brief A quoted item read into a number is a mistake worth reporting. */
static void test_type_mismatch(void)
{
TEST_REQUIRE_OK(run_program("10 DATA \"TEXT\"\n"
"20 READ A#\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "is a string") != NULL,
"a quoted item read into a number should be refused, got \"%s\"",
HARNESS_OUTPUT);
harness_stop();
/* An unquoted number read into a string is fine: it becomes its text. */
TEST_REQUIRE_OK(run_program("10 DATA 42\n"
"20 READ A$\n"
"30 PRINT A$\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "42\n");
harness_stop();
}
/** @brief A quoted item may hold commas and colons; that is what quotes are for. */
static void test_quoted_items(void)
{
TEST_REQUIRE_OK(run_program("10 DATA \"ONE, TWO\", \"A:B\"\n"
"20 READ A$, B$\n"
"30 PRINT A$\n"
"40 PRINT B$\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "ONE, TWO\nA:B\n");
harness_stop();
}
/** @brief A colon ends a DATA statement, as it does on a Commodore. */
static void test_colon_ends_data(void)
{
TEST_REQUIRE_OK(run_program("10 DATA 1, 2 : PRINT \"AFTER DATA\"\n"
"20 READ A#, B#\n"
"30 PRINT A# + B#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "AFTER DATA\n3\n");
harness_stop();
}
/** @brief DATA overflow reports the line where the item limit was crossed. */
static void test_data_prescan_error_line(void)
{
char source[4096] = "";
size_t used = 0;
int line = 0;
int item = 0;
for ( line = 1; line <= 34; line++ ) {
used += (size_t)snprintf(source + used, sizeof(source) - used, "%d DATA ", line);
for ( item = 0; item < 15; item++ ) {
used += (size_t)snprintf(source + used, sizeof(source) - used,
"%s1", (item == 0 ? "" : ","));
}
used += (size_t)snprintf(source + used, sizeof(source) - used, "\n");
}
used += (size_t)snprintf(source + used, sizeof(source) - used, "100 DATA 1,1\n");
(void)snprintf(source + used, sizeof(source) - used, "101 DATA 1\n200 PRINT 1\n");
TEST_REQUIRE_OK(harness_start(NULL));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, source));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "? 101 : PARSE ERROR") != NULL,
"DATA overflow should report its source line, got \"%s\"",
HARNESS_OUTPUT);
harness_stop();
}
/** @brief A float item fills a float variable with its fractional part intact. */
static void test_float_items(void)
{
TEST_REQUIRE_OK(run_program("10 DATA 1.5\n"
"20 READ A%\n"
"30 PRINT A%\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1.500000\n");
harness_stop();
}
/**
* @brief A `DATA` line holding a negative number can be read *and* walked past.
*
* Two different mechanisms, and only one of them was ever wrong. `READ` scans
* the source text directly, so it always returned the -5 intact; the statement's
* own argument parse refused a leading minus, so reaching the line raised
* "Expected literal". Section 4 settled that `DATA` at run time is a no-op --
* reaching it means walking past it -- which is what made this reachable at all.
*
* TODO.md section 9 item 8. A table of coordinates or velocities is full of
* negative numbers, which is how a game found it.
*/
static void test_negative_items(void)
{
TEST_REQUIRE_OK(run_program("10 READ A#\n"
"20 PRINT \"READ GAVE \" + A#\n"
"30 DATA -5\n"
"40 PRINT \"PAST IT\"\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "READ GAVE -5\nPAST IT\n");
harness_stop();
/* Mixed signs across a real table, and a negative float. */
TEST_REQUIRE_OK(run_program("10 DATA -1, 2, -3\n"
"20 DATA -0.5\n"
"30 READ A#, B#, C#\n"
"40 READ D%\n"
"50 PRINT \"\" + A# + \" \" + B# + \" \" + C# + \" \" + D%\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "-1 2 -3 -0.500000\n");
harness_stop();
}
int main(void)
{
test_read_in_order();
test_data_before_read();
test_no_skipping();
test_cursor_advances();
test_restore();
test_restore_line();
test_out_of_data();
test_type_mismatch();
test_quoted_items();
test_colon_ends_data();
test_data_prescan_error_line();
test_float_items();
test_negative_items();
return akbasic_test_failures;
}