Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m22s
akbasic CI Build / sanitizers (push) Failing after 4m43s
akbasic CI Build / coverage (push) Failing after 3m43s
akbasic CI Build / akgl_build (push) Failing after 4m48s
akbasic CI Build / mutation_test (push) Failing after 3m31s
Co-authored-by: Andrew Kesterson <andrew@aklabs.net>
481 lines
19 KiB
C
481 lines
19 KiB
C
/**
|
|
* @file graphics_verbs.c
|
|
* @brief Tests the group G verbs against the recording mock backend.
|
|
*
|
|
* These verbs emit nothing a golden file can compare, so the assertions are on
|
|
* the call log: what reached the device, in what order, with what geometry and
|
|
* what color. Running a real BASIC line rather than calling the handler directly
|
|
* is deliberate -- it exercises the dispatch-table row and the parse handler as
|
|
* well, which is where an added verb is most likely to be wrong.
|
|
*/
|
|
|
|
#include <string.h>
|
|
|
|
#include <akbasic/error.h>
|
|
#include <akbasic/graphics.h>
|
|
#include <akbasic/runtime.h>
|
|
|
|
#include "harness.h"
|
|
#include "mockdevice.h"
|
|
#include "testutil.h"
|
|
|
|
/** @brief Bring up a runtime with the mock devices attached and a program loaded. */
|
|
static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
PASS(errctx, harness_start(NULL));
|
|
mock_devices_init();
|
|
PASS(errctx, akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS,
|
|
&MOCK_AUDIO, &MOCK_INPUT, 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 values delivered through GRAPHIC's optional text-plane hook. */
|
|
static int GRAPHIC_CALLS = 0;
|
|
static int GRAPHIC_MODE = 0;
|
|
static int GRAPHIC_SPLIT = 0;
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *record_graphic(akbasic_TextSink *self, int mode, int split)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
(void)self;
|
|
GRAPHIC_CALLS += 1;
|
|
GRAPHIC_MODE = mode;
|
|
GRAPHIC_SPLIT = split;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief The white of palette index 2, which most of these draw with. */
|
|
#define WHITE "#ffffff"
|
|
/** @brief The red of palette index 3. */
|
|
#define RED "#883932"
|
|
|
|
/**
|
|
* @brief A single DRAW coordinate pair plots a point; two or more draw a
|
|
* polyline; none at all plots wherever LOCATE left the cursor.
|
|
*/
|
|
static void test_draw(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 DRAW 1, 10, 20\n"));
|
|
TEST_REQUIRE_STR(MOCK.log, "point 10.0,20.0 " WHITE "\n");
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 DRAW 1, 0, 0 TO 10, 0 TO 10, 10\n"));
|
|
TEST_REQUIRE_STR(MOCK.log,
|
|
"line 0.0,0.0-10.0,0.0 " WHITE "\n"
|
|
"line 10.0,0.0-10.0,10.0 " WHITE "\n");
|
|
harness_stop();
|
|
|
|
/* LOCATE moves the pixel cursor; a bare DRAW plots it. */
|
|
TEST_REQUIRE_OK(run_program("10 LOCATE 33, 44\n20 DRAW 1\n"));
|
|
TEST_REQUIRE_STR(MOCK.log, "point 33.0,44.0 " WHITE "\n");
|
|
harness_stop();
|
|
|
|
/* An odd number of coordinates is a typo, not half a line. */
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
mock_devices_init();
|
|
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS, NULL, NULL, NULL));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 DRAW 1, 5\n"));
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "X,Y pairs") != NULL,
|
|
"an odd coordinate count should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief COLOR rebinds a source, and every drawing verb follows the rebinding. */
|
|
static void test_color(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 COLOR 1, 3\n20 DRAW 1, 1, 1\n"));
|
|
TEST_REQUIRE_STR(MOCK.log, "point 1.0,1.0 " RED "\n");
|
|
harness_stop();
|
|
|
|
/* Both arguments are range-checked, and BASIC counts colors from 1. */
|
|
TEST_REQUIRE_OK(run_program("10 COLOR 9, 3\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "COLOR source 9") != NULL,
|
|
"an out-of-range source should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 COLOR 1, 0\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "COLOR 0") != NULL,
|
|
"color 0 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief An unrotated BOX is a rectangle; a rotated one is four lines. */
|
|
static void test_box(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 BOX 1, 0, 0, 10, 10\n"));
|
|
TEST_REQUIRE_STR(MOCK.log, "rect 0.0,0.0-10.0,10.0 " WHITE "\n");
|
|
harness_stop();
|
|
|
|
/*
|
|
* Rotated 90 degrees about its own center, a square lands back on itself --
|
|
* same four edges, walked from a different starting corner. That is the
|
|
* assertion worth making, because it pins the rotation *origin*: rotating
|
|
* about the coordinate origin instead of the box's center would put the
|
|
* square somewhere else entirely, and rotating about a corner would tilt it.
|
|
*
|
|
* The -0.0 is real and harmless: cos(pi/2) is not exactly zero, so one
|
|
* coordinate lands on negative zero, which compares equal to zero everywhere
|
|
* it matters and only shows up because the mock prints it.
|
|
*/
|
|
TEST_REQUIRE_OK(run_program("10 BOX 1, 0, 0, 10, 10, 90\n"));
|
|
TEST_REQUIRE_STR(MOCK.log,
|
|
"line 10.0,-0.0-10.0,10.0 " WHITE "\n"
|
|
"line 10.0,10.0-0.0,10.0 " WHITE "\n"
|
|
"line 0.0,10.0-0.0,0.0 " WHITE "\n"
|
|
"line 0.0,0.0-10.0,-0.0 " WHITE "\n");
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief CIRCLE is a polygon of arc-increment segments, and closes. */
|
|
static void test_circle(void)
|
|
{
|
|
int segments = 0;
|
|
const char *cursor = NULL;
|
|
|
|
/* 360 degrees at 90 per segment is four lines, whatever the radius. */
|
|
TEST_REQUIRE_OK(run_program("10 CIRCLE 1, 50, 50, 10, 10, 0, 360, 0, 90\n"));
|
|
for ( cursor = MOCK.log; (cursor = strstr(cursor, "line ")) != NULL; cursor += 5 ) {
|
|
segments += 1;
|
|
}
|
|
TEST_REQUIRE_INT(segments, 4);
|
|
/* The first vertex is directly above the center, as BASIC 7.0 starts it. */
|
|
TEST_REQUIRE(strncmp(MOCK.log, "line 50.0,40.0-", 15) == 0,
|
|
"CIRCLE should start at the top of the circle, got \"%.30s\"", MOCK.log);
|
|
harness_stop();
|
|
|
|
/* A negative radius and a zero increment are both refusals, not loops. */
|
|
TEST_REQUIRE_OK(run_program("10 CIRCLE 1, 50, 50, -5\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "radius") != NULL,
|
|
"a negative radius should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 CIRCLE 1, 50, 50, 10, 10, 0, 360, 0, 0\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "increment") != NULL,
|
|
"a zero arc increment should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief PAINT reports a partial fill rather than letting it pass as success.
|
|
*
|
|
* The device gives up when its span stack runs out, having filled some of the
|
|
* region. A program that cannot tell that happened has no way to recover, so the
|
|
* error is the feature.
|
|
*/
|
|
static void test_paint(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 PAINT 1, 5, 5\n"));
|
|
TEST_REQUIRE_STR(MOCK.log, "paint 5,5 " WHITE "\n");
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
mock_devices_init();
|
|
MOCK.paint_exhausts = true;
|
|
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS, NULL, NULL, NULL));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PAINT 1, 5, 5\n"));
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "only part of the region") != NULL,
|
|
"a partial fill should be reported, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief SCALE maps user coordinates onto the device's own size, and only then.
|
|
*
|
|
* The mock reports 640x400, which is what every scaled expectation below is
|
|
* against. If the interpreter ever stops asking the device and goes back to the
|
|
* 320x200 constants, every one of them is out by a factor of two -- which is
|
|
* the reason the mock's size is not 320x200.
|
|
*/
|
|
static void test_scale(void)
|
|
{
|
|
/* Off by default: a coordinate is a device pixel and passes through
|
|
untouched, which is what puts the whole of the host's window in reach. */
|
|
TEST_REQUIRE_OK(run_program("10 DRAW 1, 160, 100\n"));
|
|
TEST_REQUIRE_STR(MOCK.log, "point 160.0,100.0 " WHITE "\n");
|
|
harness_stop();
|
|
|
|
/* A coordinate past the old 320x200 ceiling is not clamped, refused, or
|
|
wrapped. This is item 9 in one assertion. */
|
|
TEST_REQUIRE_OK(run_program("10 DRAW 1, 639, 399\n"));
|
|
TEST_REQUIRE_STR(MOCK.log, "point 639.0,399.0 " WHITE "\n");
|
|
harness_stop();
|
|
|
|
/* On, with the 7.0 default maxima: the far corner of the user space is the
|
|
far corner of the *device*, not of a 320x200 screen inside it -- and it
|
|
is the last pixel, 639, rather than one past it. */
|
|
TEST_REQUIRE_OK(run_program("10 SCALE 1\n20 DRAW 1, 1023, 1023\n"));
|
|
TEST_REQUIRE_STR(MOCK.log, "point 639.0,399.0 " WHITE "\n");
|
|
harness_stop();
|
|
|
|
/* And a C128 listing fills the window by declaring the space it was
|
|
written for, which is the migration path for every one of them. */
|
|
TEST_REQUIRE_OK(run_program("10 SCALE 1, 319, 199\n20 DRAW 1, 319, 199\n"));
|
|
TEST_REQUIRE_STR(MOCK.log, "point 639.0,399.0 " WHITE "\n");
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 SCALE 1, 0, 400\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "positive") != NULL,
|
|
"a zero SCALE maximum should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief A backend that will not say how big it is gets the 320x200 fallback.
|
|
*
|
|
* `size` is the record's one optional entry point, so a host that filled a
|
|
* backend in before it existed has to keep working -- and what it gets is the
|
|
* space a C128 listing was written for rather than a division by zero.
|
|
*/
|
|
static void test_scale_without_a_size(void)
|
|
{
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
mock_devices_init();
|
|
MOCK_GRAPHICS.size = NULL;
|
|
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS, NULL, NULL, NULL));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 SCALE 1\n20 DRAW 1, 1023, 1023\n"));
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
TEST_REQUIRE_STR(MOCK.log, "point 319.0,199.0 " WHITE "\n");
|
|
harness_stop();
|
|
|
|
/*
|
|
* A device reporting nothing useful -- an SDL window mid-resize measures
|
|
* zero -- is not an error and must not become the space, because dividing
|
|
* by it would send every scaled coordinate to infinity.
|
|
*/
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
mock_devices_init();
|
|
MOCK.devwidth = 0;
|
|
MOCK.devheight = 0;
|
|
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS, NULL, NULL, NULL));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 SCALE 1\n20 DRAW 1, 1023, 1023\n"));
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
TEST_REQUIRE_STR(MOCK.log, "point 319.0,199.0 " WHITE "\n");
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief RGR answers the mode, the width and the height, and refuses the rest. */
|
|
static void test_rgr(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 GRAPHIC 3\n20 PRINT RGR(0)\n"));
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "3\n");
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 PRINT RGR(1)\n20 PRINT RGR(2)\n"));
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "640\n400\n");
|
|
harness_stop();
|
|
|
|
/* The whole point of asking: a program can draw to the far corner without
|
|
knowing in advance what size window the host opened. */
|
|
TEST_REQUIRE_OK(run_program("10 DRAW 1, RGR(1) - 1, RGR(2) - 1\n"));
|
|
TEST_REQUIRE_STR(MOCK.log, "point 639.0,399.0 " WHITE "\n");
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 PRINT RGR(5)\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 0..4") != NULL,
|
|
"RGR(5) should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
/*
|
|
* 3 and 4 are the character cell, and they come from the *sink* rather than
|
|
* the graphics device -- a grid belongs to a text device. The harness sink
|
|
* is stdio and has none, so what a device-less build can assert is the
|
|
* refusal; tests/console_verbs.c drives the answers through a stand-in sink
|
|
* that does have a grid.
|
|
*/
|
|
TEST_REQUIRE_OK(run_program("10 PRINT RGR(3)\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "text device with a character grid") != NULL,
|
|
"RGR(3) against a grid-less sink should refuse by name, got \"%s\"",
|
|
HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 PRINT RGR(4)\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "text device with a character grid") != NULL,
|
|
"RGR(4) against a grid-less sink should refuse by name, got \"%s\"",
|
|
HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
/*
|
|
* The mode is state and answers with no device; a size is not, and asking
|
|
* for one when there is no screen is a program bug worth reporting.
|
|
*/
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PRINT RGR(0)\n"));
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\n");
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PRINT RGR(1)\n"));
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "RGR needs a graphics device") != NULL,
|
|
"RGR(1) with no device should refuse, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief GRAPHIC records the mode, clears on request and refuses a bad mode. */
|
|
static void test_graphic(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 GRAPHIC 1, 1\n"));
|
|
TEST_REQUIRE_STR(MOCK.log, "clear #000000\n");
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.gfx.mode, 1);
|
|
harness_stop();
|
|
|
|
/* Without the clear argument nothing reaches the device. */
|
|
TEST_REQUIRE_OK(run_program("10 GRAPHIC 1\n"));
|
|
TEST_REQUIRE_STR(MOCK.log, "");
|
|
harness_stop();
|
|
|
|
/* The third argument reaches a graphical text sink as the first text row. */
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
mock_devices_init();
|
|
GRAPHIC_CALLS = 0;
|
|
HARNESS_SINK.graphic = record_graphic;
|
|
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS,
|
|
&MOCK_AUDIO, &MOCK_INPUT, NULL));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 GRAPHIC 2, 1, 19\n"));
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
TEST_REQUIRE_INT(GRAPHIC_CALLS, 1);
|
|
TEST_REQUIRE_INT(GRAPHIC_MODE, 2);
|
|
TEST_REQUIRE_INT(GRAPHIC_SPLIT, 19);
|
|
harness_stop();
|
|
|
|
/* GRAPHIC CLR drops the saved shapes and resets the whole state. */
|
|
TEST_REQUIRE_OK(run_program("10 COLOR 1, 3\n20 GRAPHIC 5\n"));
|
|
TEST_REQUIRE_STR(MOCK.log, "freeshapes\n");
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.gfx.source[1], 2);
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(run_program("10 GRAPHIC 9\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "GRAPHIC mode 9") != NULL,
|
|
"an out-of-range mode should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief SSHAPE stores a handle a later GSHAPE can use, and only that. */
|
|
static void test_shapes(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 SSHAPE A$, 0, 0, 8, 8\n20 GSHAPE A$, 32, 32\n"));
|
|
TEST_REQUIRE_STR(MOCK.log, "save 0,0-8,8 -> 0\npaste 0 at 32.0,32.0\n");
|
|
harness_stop();
|
|
|
|
/*
|
|
* The string is opaque but not magic: a string that did not come from SSHAPE
|
|
* has to be refused, or GSHAPE parses whatever digits it finds and pastes
|
|
* some unrelated slot.
|
|
*/
|
|
TEST_REQUIRE_OK(run_program("10 A$ = \"5\"\n20 GSHAPE A$, 0, 0\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "did not come from SSHAPE") != NULL,
|
|
"a hand-written shape string should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
/* A numeric variable is the wrong type for either verb. */
|
|
TEST_REQUIRE_OK(run_program("10 SSHAPE A#, 0, 0, 8, 8\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "string variable") != NULL,
|
|
"SSHAPE into a number should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief Both verbs honour a subscript on a string array.
|
|
*
|
|
* They used to ignore it. `shape_variable()` took the identifier and looked the
|
|
* variable up without evaluating the subscript, and both verbs then addressed
|
|
* element zero with a literal -- so `SSHAPE SH$(2), ...` wrote the handle into
|
|
* `SH$(0)` and `GSHAPE SH$(2)` stamped whatever was in `SH$(0)`.
|
|
*
|
|
* Ordinary assignment and `PRINT` honour the subscript, which is what made it
|
|
* expensive: a program keeping several saved shapes in an array got every one of
|
|
* them resolving to the same element, silently, and the only symptom was that
|
|
* every stamp came out as the last shape captured. The Breakout in `examples/`
|
|
* keeps its six brick stamps in six separate scalars because of this. TODO.md
|
|
* section 9 item 6.
|
|
*/
|
|
static void test_shape_subscripts(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 DIM SH$(4)\n"
|
|
"20 SSHAPE SH$(2), 0, 0, 8, 8\n"
|
|
"30 PRINT \"[\" + SH$(0) + \"] [\" + SH$(2) + \"]\"\n"));
|
|
/* The reduction from TODO.md, which used to print "[SHAPE:0] []". */
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "[] [SHAPE:0]\n");
|
|
harness_stop();
|
|
|
|
/*
|
|
* Two shapes in two elements, which is the case a program actually wants and
|
|
* the one that came out wrong: the second capture used to overwrite the
|
|
* first, so both elements named the same handle.
|
|
*/
|
|
TEST_REQUIRE_OK(run_program("10 DIM SH$(4)\n"
|
|
"20 SSHAPE SH$(1), 0, 0, 8, 8\n"
|
|
"30 SSHAPE SH$(2), 0, 0, 4, 4\n"
|
|
"40 PRINT \"[\" + SH$(1) + \"] [\" + SH$(2) + \"]\"\n"
|
|
"50 GSHAPE SH$(2), 10, 10\n"
|
|
"60 GSHAPE SH$(1), 20, 20\n"));
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "[SHAPE:0] [SHAPE:1]\n");
|
|
/* And each GSHAPE stamps the handle its own element names. */
|
|
TEST_REQUIRE_STR(MOCK.log,
|
|
"save 0,0-8,8 -> 0\n"
|
|
"save 0,0-4,4 -> 1\n"
|
|
"paste 1 at 10.0,10.0\n"
|
|
"paste 0 at 20.0,20.0\n");
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief Every verb that needs a device says so by name when it has none.
|
|
*
|
|
* This is the standalone driver's situation, so it is the common path. COLOR,
|
|
* LOCATE and SCALE are excluded on purpose: they only touch interpreter state
|
|
* and must keep working before a host has lent the script a renderer.
|
|
*/
|
|
static void test_no_device(void)
|
|
{
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
|
|
"10 COLOR 1, 3\n"
|
|
"20 LOCATE 5, 5\n"
|
|
"30 SCALE 1\n"
|
|
"40 DRAW 1, 1, 1\n"));
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "DRAW needs a graphics device") != NULL,
|
|
"DRAW without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "COLOR") == NULL,
|
|
"COLOR should not need a device, got \"%s\"", HARNESS_OUTPUT);
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.gfx.source[1], 3);
|
|
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.gfx.x, 5.0);
|
|
TEST_REQUIRE(HARNESS_RUNTIME.gfx.scaling, "SCALE should not need a device");
|
|
harness_stop();
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
test_draw();
|
|
test_color();
|
|
test_box();
|
|
test_circle();
|
|
test_paint();
|
|
test_scale();
|
|
test_scale_without_a_size();
|
|
test_rgr();
|
|
test_graphic();
|
|
test_shapes();
|
|
test_shape_subscripts();
|
|
test_no_device();
|
|
return akbasic_test_failures;
|
|
}
|