252 lines
9.3 KiB
C
252 lines
9.3 KiB
C
|
|
/**
|
||
|
|
* @file unnumbered.c
|
||
|
|
* @brief Programs loaded without line numbers.
|
||
|
|
*
|
||
|
|
* A script written against `LABEL` and `GOTO NAME` never names a line number, so
|
||
|
|
* the numbers it used to be forced to carry were decoration. A loaded line that
|
||
|
|
* arrives without one now gets the slot after the last line filed, which is the
|
||
|
|
* rule `akbasic_runtime_file_line()` holds for all three loading paths.
|
||
|
|
*
|
||
|
|
* What is asserted here is the seam, not the language: which slot a line lands
|
||
|
|
* in, that both spellings execute identically, that the prompt's rule is
|
||
|
|
* untouched, and that a collision is refused rather than losing a line.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <stdio.h>
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
#include <akbasic/error.h>
|
||
|
|
#include <akbasic/runtime.h>
|
||
|
|
|
||
|
|
#include "harness.h"
|
||
|
|
#include "testutil.h"
|
||
|
|
|
||
|
|
/** @brief Load a program from a string and run it to completion. */
|
||
|
|
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 Read a program through the sink, the way the file driver does. */
|
||
|
|
static akerr_ErrorContext AKERR_NOIGNORE *runstream_program(const char *source)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
PASS(errctx, harness_start(source));
|
||
|
|
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUNSTREAM));
|
||
|
|
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief A program with no numbers at all runs top to bottom. */
|
||
|
|
static void test_runs_in_order(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("PRINT \"ONE\"\n"
|
||
|
|
"PRINT \"TWO\"\n"
|
||
|
|
"PRINT \"THREE\"\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "ONE\nTWO\nTHREE\n");
|
||
|
|
|
||
|
|
/* Slot 0 is left empty and the program starts at 1. */
|
||
|
|
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[0].code, "");
|
||
|
|
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[1].code, "PRINT \"ONE\"");
|
||
|
|
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[2].code, "PRINT \"TWO\"");
|
||
|
|
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[3].code, "PRINT \"THREE\"");
|
||
|
|
|
||
|
|
/* And every one of them knows it was assigned rather than written. */
|
||
|
|
TEST_REQUIRE(!HARNESS_RUNTIME.source[1].numbered, "line 1 should not be marked numbered");
|
||
|
|
TEST_REQUIRE(!HARNESS_RUNTIME.source[3].numbered, "line 3 should not be marked numbered");
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Two unnumbered lines in a row are two lines.
|
||
|
|
*
|
||
|
|
* This is the defect the feature replaces, stated on its own: the second used to
|
||
|
|
* be filed under the cursor unchanged, on top of the first, with no error and no
|
||
|
|
* output. A test that only checked the *last* line would have passed throughout.
|
||
|
|
*/
|
||
|
|
static void test_second_line_does_not_replace_the_first(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("PRINT \"FIRST\"\n"
|
||
|
|
"PRINT \"SECOND\"\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "FIRST\nSECOND\n");
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief A blank line is whitespace, and costs no slot. */
|
||
|
|
static void test_blank_lines_cost_nothing(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("PRINT \"ONE\"\n"
|
||
|
|
"\n"
|
||
|
|
"\n"
|
||
|
|
"PRINT \"TWO\"\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "ONE\nTWO\n");
|
||
|
|
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[2].code, "PRINT \"TWO\"");
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief A numbered line moves the cursor; unnumbered ones follow from there.
|
||
|
|
*
|
||
|
|
* Execution order is slot order, not file order, which is the whole reason the
|
||
|
|
* two can be mixed at all: `50 PRINT "C"` runs before `100 PRINT "A"` however
|
||
|
|
* they were written down.
|
||
|
|
*/
|
||
|
|
static void test_mixed_numbering(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("100 PRINT \"A\"\n"
|
||
|
|
"PRINT \"B\"\n"
|
||
|
|
"50 PRINT \"C\"\n"
|
||
|
|
"PRINT \"D\"\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "C\nD\nA\nB\n");
|
||
|
|
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[50].code, "PRINT \"C\"");
|
||
|
|
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[51].code, "PRINT \"D\"");
|
||
|
|
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[100].code, "PRINT \"A\"");
|
||
|
|
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[101].code, "PRINT \"B\"");
|
||
|
|
|
||
|
|
/* The written numbers are marked as such; the two that followed are not. */
|
||
|
|
TEST_REQUIRE(HARNESS_RUNTIME.source[50].numbered, "line 50 was written and should be marked");
|
||
|
|
TEST_REQUIRE(HARNESS_RUNTIME.source[100].numbered, "line 100 was written and should be marked");
|
||
|
|
TEST_REQUIRE(!HARNESS_RUNTIME.source[51].numbered, "line 51 was assigned, not written");
|
||
|
|
TEST_REQUIRE(!HARNESS_RUNTIME.source[101].numbered, "line 101 was assigned, not written");
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief Branching by label needs no numbers anywhere. */
|
||
|
|
static void test_label_branching(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("GOSUB GREET\n"
|
||
|
|
"GOTO DONE\n"
|
||
|
|
"PRINT \"NOT REACHED\"\n"
|
||
|
|
"LABEL GREET\n"
|
||
|
|
"PRINT \"HELLO\"\n"
|
||
|
|
"RETURN\n"
|
||
|
|
"LABEL DONE\n"
|
||
|
|
"PRINT \"BYE\"\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "HELLO\nBYE\n");
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief A collision involving an assigned number is refused, not overwritten.
|
||
|
|
*
|
||
|
|
* `1 PRINT "C"` names a slot two unnumbered lines have already been given. Losing
|
||
|
|
* one of them to that is not recoverable, so the load fails instead.
|
||
|
|
*/
|
||
|
|
static void test_collision_is_refused(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||
|
|
TEST_REQUIRE_STATUS(akbasic_runtime_load(&HARNESS_RUNTIME,
|
||
|
|
"PRINT \"A\"\n"
|
||
|
|
"PRINT \"B\"\n"
|
||
|
|
"1 PRINT \"C\"\n"),
|
||
|
|
AKBASIC_ERR_BOUNDS);
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/* The mirror image: an assigned line landing on one already written. */
|
||
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||
|
|
TEST_REQUIRE_STATUS(akbasic_runtime_load(&HARNESS_RUNTIME,
|
||
|
|
"2 PRINT \"A\"\n"
|
||
|
|
"1 PRINT \"B\"\n"
|
||
|
|
"PRINT \"C\"\n"),
|
||
|
|
AKBASIC_ERR_BOUNDS);
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief Two lines carrying the same written number still keep the last. */
|
||
|
|
static void test_duplicate_written_numbers_still_replace(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("10 PRINT \"OLD\"\n"
|
||
|
|
"10 PRINT \"NEW\"\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "NEW\n");
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief The same program through the sink lands in the same slots. */
|
||
|
|
static void test_runstream_assigns_the_same_slots(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(runstream_program("PRINT \"ONE\"\n"
|
||
|
|
"PRINT \"TWO\"\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "ONE\nTWO\n");
|
||
|
|
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[1].code, "PRINT \"ONE\"");
|
||
|
|
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[2].code, "PRINT \"TWO\"");
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief A file ending in a blank line keeps its last line.
|
||
|
|
*
|
||
|
|
* `tests/reference/language/arithmetic/integer.bas` is this shape, and both this
|
||
|
|
* interpreter and the reference used to erase the last line before running it --
|
||
|
|
* the blank line was filed under the cursor, which for a line with no number of
|
||
|
|
* its own is the number of the line before it.
|
||
|
|
*/
|
||
|
|
static void test_trailing_blank_line_keeps_the_last_line(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(runstream_program("PRINT \"ONE\"\n"
|
||
|
|
"PRINT \"LAST\"\n"
|
||
|
|
"\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "ONE\nLAST\n");
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief The prompt still tells a program line from a statement by its number.
|
||
|
|
*
|
||
|
|
* The one rule the feature does not touch. `PRINT 2 + 2` typed at a prompt must
|
||
|
|
* answer 4 rather than quietly becoming line 1 of a program.
|
||
|
|
*/
|
||
|
|
static void test_prompt_still_runs_direct_mode(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(harness_start("PRINT 2 + 2\n10 PRINT \"FILED\"\nLIST\n"));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_REPL));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
||
|
|
|
||
|
|
/* 4 came out now; the numbered line went into the program and LIST found it. */
|
||
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "4\n") != NULL,
|
||
|
|
"PRINT 2 + 2 at the prompt should have answered 4, got \"%s\"", HARNESS_OUTPUT);
|
||
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "10 PRINT \"FILED\"") != NULL,
|
||
|
|
"the numbered line should have been filed and listed, got \"%s\"", HARNESS_OUTPUT);
|
||
|
|
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[1].code, "");
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief RENUMBER numbers everything, which is what asking for it means. */
|
||
|
|
static void test_renumber_marks_every_line_numbered(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
|
||
|
|
"PRINT \"ONE\"\n"
|
||
|
|
"PRINT \"TWO\"\n"));
|
||
|
|
TEST_REQUIRE(!HARNESS_RUNTIME.source[1].numbered, "line 1 starts out assigned");
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(akbasic_renumber(&HARNESS_RUNTIME, 10, 10, 0));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "PRINT \"ONE\"");
|
||
|
|
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[20].code, "PRINT \"TWO\"");
|
||
|
|
TEST_REQUIRE(HARNESS_RUNTIME.source[10].numbered, "RENUMBER should mark line 10 numbered");
|
||
|
|
TEST_REQUIRE(HARNESS_RUNTIME.source[20].numbered, "RENUMBER should mark line 20 numbered");
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
int main(void)
|
||
|
|
{
|
||
|
|
test_runs_in_order();
|
||
|
|
test_second_line_does_not_replace_the_first();
|
||
|
|
test_blank_lines_cost_nothing();
|
||
|
|
test_mixed_numbering();
|
||
|
|
test_label_branching();
|
||
|
|
test_collision_is_refused();
|
||
|
|
test_duplicate_written_numbers_still_replace();
|
||
|
|
test_runstream_assigns_the_same_slots();
|
||
|
|
test_trailing_blank_line_keeps_the_last_line();
|
||
|
|
test_prompt_still_runs_direct_mode();
|
||
|
|
test_renumber_marks_every_line_numbered();
|
||
|
|
return akbasic_test_failures;
|
||
|
|
}
|