Finish the language: every remaining verb group, and the defects that blocked them

Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and
RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of
those turned out to have been fixed or never ported and nobody had written it
down; the audit records the evidence for each.

Two of the seventeen were real. math_plus mutated its left operand when the
operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT
coverage because NEXT relied on the mutation, so tests/for_next.c came first
and NEXT now writes the counter back itself. And the binary operators summed
both numeric fields of their right operand, which no BASIC program can reach
-- that one needed a test written against the value API.

Writing the tests turned up eight defects nobody had listed. Seven are fixed:

  IF A = 2 THEN was a parse error; only == worked
  IF ... AND ... was a parse error, because a condition parsed as one relation
  IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN
  EXIT before any NEXT restarted the program and exhausted the variable pool
  READ never found a DATA line above it, and swallowed the lines between
  PRINT 2 + 2 at the prompt was filed as program text instead of answering
  a short read discarded its bytes, so COPY produced empty files
  every verb taking an argument list said "peek() returned nil token!" on none

The eighth is not fixed and cannot be quietly: a FOR whose step overshoots
runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two
errors cancel for a step of 1, which is why neither was noticed. Correcting
them changes the expected output of a checked-in acceptance file, and
tests/reference/README.md forbids editing one to suit this interpreter. It is
tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct
contract, and TODO.md items 19 and 20.

Sprites are real libakgl actors with a renderfunc of their own, because
akgl_actor_render draws every sprite square and an actor has no per-axis
scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle
or a 63-element integer array -- a string here cannot hold a zero byte.

Verbs that need hardware that does not exist are refused by name with the
reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and
DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream.

94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen
clean. The Go acceptance corpus stayed green throughout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 21:50:37 -04:00
parent 3aade4947a
commit 9845e77a5c
87 changed files with 11024 additions and 325 deletions

View File

@@ -32,11 +32,14 @@
* deps/libakgl/tests/draw.c does.
*/
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/renderer.h>
#include <akgl/text.h>
#include <akbasic/akgl.h>
#include <akbasic/error.h>
#include <akbasic/sprite.h>
#include <akbasic/runtime.h>
#include <akbasic/sink.h>
@@ -54,6 +57,8 @@ static akbasic_TextSink AKGLSINK;
static akbasic_GraphicsBackend GRAPHICS;
static akbasic_AkglGraphics GRAPHICSSTATE;
static akbasic_InputBackend INPUT;
static akbasic_SpriteBackend SPRITES;
static akbasic_AkglSprites SPRITESSTATE;
static TTF_Font *font = NULL;
static char OUTPUT[8192];
static FILE *OUT = NULL;
@@ -100,7 +105,8 @@ static akerr_ErrorContext AKERR_NOIGNORE *start_runtime(const char *source)
PASS(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, OUT, NULL));
PASS(errctx, akbasic_runtime_init(&RUNTIME, &SINK));
PASS(errctx, akbasic_graphics_init_akgl(&GRAPHICS, &GRAPHICSSTATE, renderer));
PASS(errctx, akbasic_runtime_set_devices(&RUNTIME, &GRAPHICS, NULL, NULL));
PASS(errctx, akbasic_sprite_init_akgl(&SPRITES, &SPRITESSTATE, renderer, &GRAPHICSSTATE));
PASS(errctx, akbasic_runtime_set_devices(&RUNTIME, &GRAPHICS, NULL, NULL, &SPRITES));
PASS(errctx, akbasic_runtime_load(&RUNTIME, source));
PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUN));
PASS(errctx, akbasic_runtime_run(&RUNTIME, 0));
@@ -335,6 +341,130 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_input_backend(void)
SUCCEED_RETURN(errctx);
}
/**
* @brief A sprite loaded from an image file lands on the target where MOVSPR put it.
*
* The one assertion in this repository that exercises the whole file path:
* a BASIC string that is not an SSHAPE handle, resolved by akgl_path_relative()
* against the working directory, decoded by SDL_image inside
* akgl_spritesheet_initialize(), and blitted through the actor's renderfunc.
*
* The fixture is 8x8 rather than 24x21 on purpose -- a sprite from a file takes
* the image's own size, and an assertion at (43, 33) rather than (40, 30) is
* what says the size came from the image rather than from a constant.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_sprite_from_file(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
PASS(errctx, clear_target());
PASS(errctx, start_runtime("10 SPRSAV \"assets/sprite8x8.png\", 1\n"
"20 SPRITE 1, 1\n"
"30 MOVSPR 1, 40, 30\n"));
TEST_REQUIRE_STR(OUTPUT, "");
PASS(errctx, akbasic_sprite_akgl_render(&SPRITES));
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
TEST_REQUIRE(shot != NULL, "could not read the render target back");
/*
* Magenta, not white: SPRITE's colour argument modulates the texture and
* this program never gave one, so the image's own colour comes through.
*/
TEST_REQUIRE(pixel_is(shot, 43, 33, 0xff, 0x00, 0xff),
"the loaded sprite should be drawn at MOVSPR's coordinate");
TEST_REQUIRE(pixel_is(shot, 47, 37, 0xff, 0x00, 0xff),
"the whole 8x8 image should have been drawn, not one pixel of it");
TEST_REQUIRE(!pixel_is(shot, 49, 39, 0xff, 0x00, 0xff),
"an 8x8 sprite should stop after eight pixels");
SDL_DestroySurface(shot);
/* A path that names nothing is the program's mistake, and is reported. */
stop_runtime();
PASS(errctx, start_runtime("10 SPRSAV \"assets/nosuchfile.png\", 1\n"));
TEST_REQUIRE(strstr(OUTPUT, "No such sprite image") != NULL,
"a missing image should be reported by name, got \"%s\"", OUTPUT);
stop_runtime();
SUCCEED_RETURN(errctx);
}
/**
* @brief A sprite defined from 63 pattern bytes reaches real pixels, in its colour.
*
* The DATA-driven path a type-in listing uses, end to end: an integer array, the
* bit unpacking, a surface built a pixel at a time, and a texture. The pattern
* is one set bit in the top-left corner, so what is asserted is both that the
* pixel is lit *and* that its neighbour is not -- which is what catches a bit
* order that came through reversed.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_sprite_from_pattern(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
PASS(errctx, clear_target());
PASS(errctx, start_runtime("10 DIM P#(63)\n"
"20 P#(0) = 128\n"
"30 SPRSAV P#, 1\n"
"40 SPRITE 1, 1, 6\n"
"50 MOVSPR 1, 20, 20\n"));
TEST_REQUIRE_STR(OUTPUT, "");
PASS(errctx, akbasic_sprite_akgl_render(&SPRITES));
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
TEST_REQUIRE(shot != NULL, "could not read the render target back");
/* Palette index 6 is green: 0x55, 0xa0, 0x49. */
TEST_REQUIRE(pixel_is(shot, 20, 20, 0x55, 0xa0, 0x49),
"the pattern's one set bit should be the sprite's top-left pixel");
TEST_REQUIRE(!pixel_is(shot, 21, 20, 0x55, 0xa0, 0x49),
"only one bit was set, so only one pixel should be lit");
TEST_REQUIRE(!pixel_is(shot, 43, 20, 0x55, 0xa0, 0x49),
"a reversed bit order would have lit the far end of the row");
SDL_DestroySurface(shot);
stop_runtime();
SUCCEED_RETURN(errctx);
}
/**
* @brief A region SSHAPE saved becomes a sprite, which is the shared-format claim.
*
* The C128 documents SPRSAV's string as the SSHAPE data format at a fixed 24x21,
* so reusing the shape pool here is faithful rather than a shortcut -- and this
* is what says the handle actually survives the trip through a BASIC string and
* into the sprite device.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_sprite_from_shape(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
PASS(errctx, clear_target());
PASS(errctx, start_runtime("10 COLOR 1, 3\n"
"20 BOX 1, 0, 0, 10, 10\n"
"30 SSHAPE A$, 0, 0, 10, 10\n"
"40 SPRSAV A$, 1\n"
"50 SPRITE 1, 1\n"
"60 MOVSPR 1, 60, 60\n"));
TEST_REQUIRE_STR(OUTPUT, "");
PASS(errctx, akbasic_sprite_akgl_render(&SPRITES));
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
TEST_REQUIRE(shot != NULL, "could not read the render target back");
/*
* Palette index 3 is red, which is what the BOX was outlined in. The corner
* of the box is the (0,0) pixel of the saved region, so it lands exactly
* where MOVSPR put the sprite -- and its interior does not, which is what
* says a region was copied rather than a rectangle drawn.
*/
TEST_REQUIRE(pixel_is(shot, 60, 60, 0x88, 0x39, 0x32),
"the saved region's corner should have been drawn as a sprite");
TEST_REQUIRE(!pixel_is(shot, 65, 65, 0x88, 0x39, 0x32),
"the region was an outline, so its middle should not be red");
SDL_DestroySurface(shot);
stop_runtime();
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
@@ -366,6 +496,20 @@ int main(void)
*/
CATCH(errctx, akgl_render_bind2d(renderer));
/*
* The three things akgl_game_init() would have done and this file, being
* a host that deliberately never calls it, has to do itself: the object
* pools, the name registries, and a camera for akgl_actor_render() to
* dereference. src/frontend_akgl.c does the same, and says why at length.
*/
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
camera = &_akgl_camera;
camera->x = 0.0f;
camera->y = 0.0f;
camera->w = (float)TARGET_SIZE;
camera->h = (float)TARGET_SIZE;
/*
* libakgl's own monospaced fixture, borrowed rather than copied. Being
* monospaced is what makes a character grid meaningful to assert on.
@@ -381,6 +525,9 @@ int main(void)
CATCH(errctx, test_sink_grid_and_wrap());
CATCH(errctx, test_sink_renders());
CATCH(errctx, test_input_backend());
CATCH(errctx, test_sprite_from_file());
CATCH(errctx, test_sprite_from_pattern());
CATCH(errctx, test_sprite_from_shape());
} CLEANUP {
if ( font != NULL ) {
TTF_CloseFont(font);

View File

@@ -28,9 +28,16 @@
#include <akerror.h>
#include <akgl/actor.h>
#include <akgl/character.h>
#include <akgl/controller.h>
#include <akgl/draw.h>
#include <akgl/error.h>
/* game.h for the `camera` global, which akgl_actor_render() reads. */
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/sprite.h>
#include <akbasic/error.h>
#include <akbasic/frontend.h>
@@ -832,6 +839,128 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_repl_session(void)
SUCCEED_RETURN(errctx);
}
/**
* @brief The frontend leaves libakgl able to draw an actor, which is what a sprite is.
*
* Three things the host owes libakgl and that akgl_game_init() would normally
* have done -- the object pools, the name registries, and the camera. Nothing in
* the interpreter needed any of them until sprites, because text and the drawing
* primitives go straight at the renderer; an actor goes through all three.
*
* Asserted by building one actor by hand and drawing it, rather than by reading
* the globals back. Without akgl_registry_init() the actor cannot be named and
* akgl_actor_initialize raises AKERR_KEY; without akgl_heap_init() there is no
* pool to take it from; without a camera akgl_actor_render() dereferences NULL
* and the test does not report a failure so much as stop existing.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_host_can_draw_an_actor(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *pattern = NULL;
SDL_Surface *shot = NULL;
SDL_Texture *texture = NULL;
akgl_SpriteSheet *sheet = NULL;
akgl_Sprite *sprite = NULL;
akgl_Character *basechar = NULL;
akgl_Actor *actor = NULL;
/*
* 128 bytes each, not string literals: akgl_sprite_initialize() and
* akgl_actor_initialize() memcpy a fixed AKGL_*_MAX_NAME_LENGTH from what
* they are handed, so a shorter buffer is read past its end.
*/
char sheetname[AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH];
char spritename[AKGL_SPRITE_MAX_NAME_LENGTH];
char charname[AKGL_SPRITE_MAX_NAME_LENGTH];
char actorname[AKGL_ACTOR_MAX_NAME_LENGTH];
PASS(errctx, start_frontend(NULL));
TEST_REQUIRE(camera != NULL, "the frontend should have pointed the camera somewhere");
TEST_REQUIRE_INT((int)camera->w, WINDOW_W);
TEST_REQUIRE_INT((int)camera->h, WINDOW_H);
TEST_REQUIRE(AKGL_REGISTRY_ACTOR != 0, "the frontend should have created the registries");
memset(sheetname, 0, sizeof(sheetname));
memset(spritename, 0, sizeof(spritename));
memset(charname, 0, sizeof(charname));
memset(actorname, 0, sizeof(actorname));
SDL_strlcpy(sheetname, "akbasic:test:sheet", sizeof(sheetname));
SDL_strlcpy(spritename, "akbasic:test:sprite", sizeof(spritename));
SDL_strlcpy(charname, "akbasic:test:character", sizeof(charname));
SDL_strlcpy(actorname, "akbasic:test:actor", sizeof(actorname));
/* A solid 8x8 white square, built in memory rather than loaded from a file. */
pattern = SDL_CreateSurface(8, 8, SDL_PIXELFORMAT_RGBA32);
TEST_REQUIRE(pattern != NULL, "could not create the pattern surface: %s", SDL_GetError());
TEST_REQUIRE(SDL_FillSurfaceRect(pattern, NULL,
SDL_MapSurfaceRGBA(pattern, 0xff, 0xff, 0xff, 0xff)),
"could not fill the pattern surface: %s", SDL_GetError());
texture = SDL_CreateTextureFromSurface(FRONTEND.renderer->sdl_renderer, pattern);
SDL_DestroySurface(pattern);
TEST_REQUIRE(texture != NULL, "could not upload the pattern: %s", SDL_GetError());
/*
* The sheet is assembled rather than initialized: akgl_spritesheet_initialize
* loads an image from a path, and there is no file here. Everything below it
* is the ordinary API.
*/
PASS(errctx, akgl_heap_next_spritesheet(&sheet));
memset(sheet, 0, sizeof(*sheet));
sheet->texture = texture;
SDL_strlcpy(sheet->name, sheetname, sizeof(sheet->name));
sheet->refcount = 1;
PASS(errctx, akgl_heap_next_sprite(&sprite));
PASS(errctx, akgl_sprite_initialize(sprite, spritename, sheet));
sprite->frames = 1;
sprite->frameids[0] = 0;
sprite->width = 8;
sprite->height = 8;
PASS(errctx, akgl_heap_next_character(&basechar));
PASS(errctx, akgl_character_initialize(basechar, charname));
PASS(errctx, akgl_character_sprite_add(basechar, sprite, 0));
PASS(errctx, akgl_heap_next_actor(&actor));
PASS(errctx, akgl_actor_initialize(actor, actorname));
PASS(errctx, akgl_actor_set_character(actor, charname));
actor->state = 0;
actor->visible = true;
actor->x = 40.0f;
actor->y = 24.0f;
/* The text layer paints every cell it owns, so draw the actor over it. */
PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink));
PASS(errctx, akgl_actor_render(actor));
shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL);
TEST_REQUIRE(shot != NULL, "could not read the render target back");
TEST_REQUIRE(anything_drawn(shot, 42, 26, 2, 2),
"the actor should have been drawn at its own coordinates");
SDL_DestroySurface(shot);
stop_frontend();
/*
* And the pools start over. akgl_heap_init() is not needed to make the
* *first* frontend work -- the pool arrays are file-scope and start zeroed --
* so the only thing that shows it ran is a second frontend handing back the
* slot the first one was still holding. Without it the actor above survives
* SDL_Quit with a live refcount, pointing at a texture that no longer exists,
* and the next program's sprites are allocated around the corpse.
*/
{
akgl_Actor *reused = NULL;
PASS(errctx, start_frontend(NULL));
PASS(errctx, akgl_heap_next_actor(&reused));
TEST_REQUIRE(reused == actor,
"a new frontend should hand back the released pool slot, not the one after it");
stop_frontend();
}
SUCCEED_RETURN(errctx);
}
/** @brief Every entry point validates its pointers before touching SDL. */
static akerr_ErrorContext AKERR_NOIGNORE *test_arguments_refused(void)
{
@@ -880,6 +1009,7 @@ int main(void)
CATCH(errctx, test_block_cursor_blinks());
CATCH(errctx, test_frame_owns_every_pixel());
CATCH(errctx, test_repl_session());
CATCH(errctx, test_host_can_draw_an_actor());
} CLEANUP {
stop_frontend();
} PROCESS(errctx) {

22
tests/assets/README.md Normal file
View File

@@ -0,0 +1,22 @@
# tests/assets
Fixtures for the tests that need a real file on disk. Only one so far.
## sprite8x8.png
An 8x8 solid magenta square, 76 bytes, generated rather than drawn -- it is a
PNG header, one IDAT of a single repeated RGBA pixel, and an IEND.
It exists for one assertion in `tests/akgl_backends.c`: that
`SPRSAV "assets/sprite8x8.png", 1` reaches SDL_image, decodes, and lands on the
render target as magenta pixels at the coordinate `MOVSPR` put it. Magenta
because nothing else in that test file draws in it, so a stray pixel cannot be
mistaken for this one.
Eight by eight rather than a Commodore sprite's 24x21, deliberately: a sprite
loaded from a file takes the image's own size, and a fixture that happened to be
24x21 could not tell that apart from a hardcoded constant.
libakgl's own fixtures under `deps/libakgl/tests/assets/` are borrowed where they
fit -- the monospaced font is used from there rather than copied. There was no
image small enough to be worth borrowing: the smallest is a 576x384 spritesheet.

BIN
tests/assets/sprite8x8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 B

View File

@@ -31,7 +31,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
PASS(errctx, harness_start(NULL));
mock_devices_init();
PASS(errctx, akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS,
&MOCK_AUDIO, &MOCK_INPUT));
&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));
@@ -176,7 +176,7 @@ static void test_sound(void)
mock_devices_init();
MOCK_AUDIO.sweep = NULL;
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS,
&MOCK_AUDIO, &MOCK_INPUT));
&MOCK_AUDIO, &MOCK_INPUT, NULL));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 SOUND 1, 1000, 60, 1, 500, 10\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
@@ -268,7 +268,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *parse_notes(const char *notes)
PASS(errctx, harness_start(NULL));
mock_devices_init();
PASS(errctx, akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, &MOCK_AUDIO, NULL));
PASS(errctx, akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, &MOCK_AUDIO, NULL, NULL));
PASS(errctx, akbasic_play_parse(&HARNESS_RUNTIME, notes));
SUCCEED_RETURN(errctx);
}
@@ -351,7 +351,7 @@ static void test_play_is_not_blocking(void)
{
TEST_REQUIRE_OK(harness_start(NULL));
mock_devices_init();
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, &MOCK_AUDIO, NULL));
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, &MOCK_AUDIO, NULL, NULL));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PLAY \"QCD\"\n20 PRINT \"AFTER\"\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));

179
tests/console_verbs.c Normal file
View File

@@ -0,0 +1,179 @@
/**
* @file console_verbs.c
* @brief Tests the group E verbs: SLEEP, WAIT, KEY, WINDOW and the TI clock.
*
* The two that wait are the interesting ones, and what they have to prove is
* that they *do not block*: a held step still returns, so a bounded
* akbasic_runtime_run() comes back on time and a host keeps its frame rate.
* Section 1.6 is the rule and this is where it is checked for group E.
*/
#include <string.h>
#include <akbasic/error.h>
#include <akbasic/runtime.h>
#include "harness.h"
#include "testutil.h"
/** @brief Load a program and put the runtime in RUN mode without stepping it. */
static akerr_ErrorContext AKERR_NOIGNORE *load_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));
SUCCEED_RETURN(errctx);
}
/** @brief Run a program to completion in RUN mode. */
static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
{
PREPARE_ERROR(errctx);
PASS(errctx, load_program(source));
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 0));
SUCCEED_RETURN(errctx);
}
/** @brief SLEEP holds the step loop until the host's clock passes its deadline. */
static void test_sleep(void)
{
TEST_REQUIRE_OK(load_program("1 PRINT \"BEFORE\"\n"
"2 SLEEP 5\n"
"3 PRINT \"AFTER\"\n"));
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 1000));
/* Line 0, line 1, line 2 -- the SLEEP arms and the program stops advancing. */
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 3));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "BEFORE\n");
TEST_REQUIRE(HARNESS_RUNTIME.console_state.sleeping, "SLEEP should be holding");
/*
* Twenty more steps with the clock standing still change nothing -- and,
* crucially, they *return*. A SLEEP that blocked would never get here.
*/
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 20));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "BEFORE\n");
/* Past the deadline, it wakes. */
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 6500));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 2));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "BEFORE\nAFTER\n");
TEST_REQUIRE(!HARNESS_RUNTIME.console_state.sleeping, "SLEEP should have finished");
harness_stop();
}
/** @brief A host that never sets a clock does not hang on SLEEP. */
static void test_sleep_without_a_clock(void)
{
TEST_REQUIRE_OK(run_program("1 SLEEP 10\n2 PRINT \"THROUGH\"\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "THROUGH\n");
harness_stop();
}
/** @brief SLEEP refuses a negative interval rather than waiting forever. */
static void test_sleep_refuses_negative(void)
{
TEST_REQUIRE_OK(run_program("10 SLEEP -1\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "negative number of seconds") != NULL,
"SLEEP -1 should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/**
* @brief WAIT polls a byte and holds until it matches, without blocking.
*
* The address is a real one -- this test's own variable -- which is the only
* honest way to exercise it: on a C128 the byte is a hardware register, and here
* the only thing that can change it is something outside the program.
*/
static void test_wait(void)
{
static volatile uint8_t watched = 0;
char source[256];
watched = 0;
snprintf(source, sizeof(source),
"1 WAIT %llu, 1\n2 PRINT \"RELEASED\"\n",
(unsigned long long)(uintptr_t)&watched);
TEST_REQUIRE_OK(load_program(source));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 4));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "");
TEST_REQUIRE(HARNESS_RUNTIME.console_state.waiting, "WAIT should be holding");
/* Still returning, which is the whole point. */
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 20));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "");
watched = 1;
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 2));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "RELEASED\n");
harness_stop();
}
/** @brief KEY stores a macro per key, and bare KEY lists all eight. */
static void test_key(void)
{
TEST_REQUIRE_OK(run_program("10 KEY 1, \"RUN\"\n"
"20 KEY 3, \"LIST\"\n"
"30 KEY\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "KEY 1, \"RUN\"") != NULL,
"KEY should list its macros, got \"%s\"", HARNESS_OUTPUT);
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "KEY 3, \"LIST\"") != NULL,
"KEY should list its macros, got \"%s\"", HARNESS_OUTPUT);
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "KEY 8, \"\"") != NULL,
"KEY should list undefined keys too, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
TEST_REQUIRE_OK(run_program("10 KEY 9, \"NOPE\"\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 1..8") != NULL,
"KEY 9 should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/** @brief WINDOW refuses against a sink with no character grid, which is stdio. */
static void test_window_needs_a_grid(void)
{
TEST_REQUIRE_OK(run_program("10 WINDOW 1, 1, 20, 10\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "character grid") != NULL,
"WINDOW against a stdio sink should refuse by name, got \"%s\"",
HARNESS_OUTPUT);
harness_stop();
/* An inside-out rectangle is refused before the device is even consulted. */
TEST_REQUIRE_OK(run_program("10 WINDOW 20, 10, 1, 1\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "bottom right") != NULL,
"an inverted WINDOW should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/** @brief TI# counts jiffies and TI$ formats them, both from the host's clock. */
static void test_ti(void)
{
TEST_REQUIRE_OK(load_program("1 PRINT TI#\n2 PRINT TI$\n"));
/* 3661 seconds is 01:01:01, and 3661 * 60 jiffies. */
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 3661000));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 3));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "219660\n010101\n");
harness_stop();
/* A host with no clock has a stopped one rather than a wrong one. */
TEST_REQUIRE_OK(run_program("1 PRINT TI$\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "000000\n");
harness_stop();
}
int main(void)
{
test_sleep();
test_sleep_without_a_clock();
test_sleep_refuses_negative();
test_wait();
test_key();
test_window_needs_a_grid();
test_ti();
return akbasic_test_failures;
}

View File

@@ -45,18 +45,18 @@ static void test_set_devices(void)
mock_devices_init();
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS,
&MOCK_AUDIO, &MOCK_INPUT));
&MOCK_AUDIO, &MOCK_INPUT, NULL));
TEST_REQUIRE(HARNESS_RUNTIME.graphics == &MOCK_GRAPHICS, "graphics backend was not stored");
TEST_REQUIRE(HARNESS_RUNTIME.audio == &MOCK_AUDIO, "audio backend was not stored");
TEST_REQUIRE(HARNESS_RUNTIME.input == &MOCK_INPUT, "input backend was not stored");
/* Withholding a capability is spelled NULL, and must actually detach. */
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS, NULL, NULL));
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS, NULL, NULL, NULL));
TEST_REQUIRE(HARNESS_RUNTIME.graphics == &MOCK_GRAPHICS, "graphics backend should have stayed");
TEST_REQUIRE(HARNESS_RUNTIME.audio == NULL, "audio backend should have been detached");
TEST_REQUIRE(HARNESS_RUNTIME.input == NULL, "input backend should have been detached");
TEST_REQUIRE_STATUS(akbasic_runtime_set_devices(NULL, NULL, NULL, NULL), AKERR_NULLPOINTER);
TEST_REQUIRE_STATUS(akbasic_runtime_set_devices(NULL, NULL, NULL, NULL, NULL), AKERR_NULLPOINTER);
harness_stop();
}

280
tests/disk_verbs.c Normal file
View File

@@ -0,0 +1,280 @@
/**
* @file disk_verbs.c
* @brief Tests the group F verbs: channels, files, and the ones that are refused.
*
* These touch the filesystem, so they work in a temporary directory of their own
* and clean up after themselves. What is asserted is the seam between BASIC and
* `aksl_f*` -- that a verb reaches the right call with the right arguments --
* plus, for the five that need a real 1541, that they refuse *by name* rather
* than doing something plausible and wrong.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.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 Read a whole file, for checking what a verb wrote. */
static bool file_contents(const char *name, char *dest, size_t len)
{
FILE *fp = fopen(name, "rb");
size_t got = 0;
if ( fp == NULL ) {
return false;
}
got = fread(dest, 1, len - 1, fp);
dest[got] = '\0';
fclose(fp);
return true;
}
/** @brief A channel written and then read back gives the same lines. */
static void test_channel_round_trip(void)
{
char contents[256];
remove("t_chan.txt");
TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_chan.txt\", W\n"
"20 PRINT #1, \"FIRST\"\n"
"30 PRINT #1, \"SECOND\"\n"
"40 DCLOSE 1\n"
"50 DOPEN 2, \"t_chan.txt\"\n"
"60 INPUT #2, A$\n"
"70 INPUT #2, B$\n"
"80 DCLOSE 2\n"
"90 PRINT A$\n"
"100 PRINT B$\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "FIRST\nSECOND\n");
TEST_REQUIRE(file_contents("t_chan.txt", contents, sizeof(contents)),
"the file should exist");
TEST_REQUIRE_STR(contents, "FIRST\nSECOND\n");
harness_stop();
remove("t_chan.txt");
}
/** @brief A numeric variable reads a numeric line, and end of file gives zero. */
static void test_channel_numeric(void)
{
remove("t_num.txt");
TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_num.txt\", W\n"
"20 PRINT #1, 42\n"
"30 DCLOSE 1\n"
"40 DOPEN 2, \"t_num.txt\"\n"
"50 INPUT #2, A#\n"
"60 INPUT #2, B#\n"
"70 DCLOSE 2\n"
"80 PRINT A#\n"
"90 PRINT B#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "42\n0\n");
harness_stop();
remove("t_num.txt");
}
/** @brief APPEND adds to a file rather than replacing it. */
static void test_append(void)
{
char contents[256];
remove("t_app.txt");
TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_app.txt\", W\n"
"20 PRINT #1, \"ONE\"\n"
"30 DCLOSE 1\n"
"40 APPEND 1, \"t_app.txt\"\n"
"50 PRINT #1, \"TWO\"\n"
"60 DCLOSE 1\n"));
TEST_REQUIRE(file_contents("t_app.txt", contents, sizeof(contents)),
"the file should exist");
TEST_REQUIRE_STR(contents, "ONE\nTWO\n");
harness_stop();
remove("t_app.txt");
}
/** @brief Reading a channel opened for writing is refused, and the reverse too. */
static void test_channel_direction(void)
{
remove("t_dir.txt");
TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_dir.txt\", W\n"
"20 INPUT #1, A$\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "opened for writing") != NULL,
"reading a write channel should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_dir.txt\"\n"
"20 PRINT #1, \"NO\"\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "opened for reading") != NULL,
"writing a read channel should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
remove("t_dir.txt");
}
/** @brief An unopened or already-open channel is reported rather than silently reused. */
static void test_channel_state(void)
{
TEST_REQUIRE_OK(run_program("10 PRINT #3, \"NOWHERE\"\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "is not open") != NULL,
"an unopened channel should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
remove("t_two.txt");
TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_two.txt\", W\n"
"20 DOPEN 1, \"t_two.txt\", W\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "already open") != NULL,
"re-opening a channel should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
remove("t_two.txt");
TEST_REQUIRE_OK(run_program("10 DOPEN 99, \"x\"\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 0..9") != NULL,
"channel 99 should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/** @brief COPY duplicates a file and CONCAT appends one to another. */
static void test_copy_and_concat(void)
{
char contents[256];
remove("t_a.txt");
remove("t_b.txt");
TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_a.txt\", W\n"
"20 PRINT #1, \"AAA\"\n"
"30 DCLOSE 1\n"
"40 COPY \"t_a.txt\", \"t_b.txt\"\n"
"50 CONCAT \"t_a.txt\", \"t_b.txt\"\n"));
TEST_REQUIRE(file_contents("t_b.txt", contents, sizeof(contents)),
"the copy should exist");
TEST_REQUIRE_STR(contents, "AAA\nAAA\n");
harness_stop();
remove("t_a.txt");
remove("t_b.txt");
}
/** @brief RENAME moves a file and SCRATCH deletes one. */
static void test_rename_and_scratch(void)
{
char contents[256];
remove("t_from.txt");
remove("t_to.txt");
TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_from.txt\", W\n"
"20 PRINT #1, \"MOVED\"\n"
"30 DCLOSE 1\n"
"40 RENAME \"t_from.txt\", \"t_to.txt\"\n"));
TEST_REQUIRE(!file_contents("t_from.txt", contents, sizeof(contents)),
"the original should be gone");
TEST_REQUIRE(file_contents("t_to.txt", contents, sizeof(contents)),
"the new name should exist");
harness_stop();
TEST_REQUIRE_OK(run_program("10 SCRATCH \"t_to.txt\"\n"));
TEST_REQUIRE(!file_contents("t_to.txt", contents, sizeof(contents)),
"SCRATCH should have deleted it");
harness_stop();
/* Deleting something that is not there is reported rather than ignored. */
TEST_REQUIRE_OK(run_program("10 SCRATCH \"t_nosuch.txt\"\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "could not delete") != NULL,
"deleting a missing file should be reported, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/** @brief BSAVE writes a memory range and BLOAD reads one back. */
static void test_bsave_bload(void)
{
remove("t_bin.dat");
TEST_REQUIRE_OK(run_program("10 A$ = \"BINARY\"\n"
"20 BSAVE \"t_bin.dat\", POINTER(A$), POINTER(A$) + 7\n"
"30 B$ = \".......\"\n"
"40 BLOAD \"t_bin.dat\", POINTER(B$), 7\n"
"50 PRINT B$\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "BINARY\n");
harness_stop();
remove("t_bin.dat");
/* BLOAD needs a length, unlike a C128's; see TODO.md section 5. */
TEST_REQUIRE_OK(run_program("10 A$ = \"X\"\n20 BLOAD \"t_bin.dat\", POINTER(A$)\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "maximum length") != NULL,
"BLOAD without a length should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/**
* @brief The five verbs that need a real drive refuse by name, with the reason.
*
* The point is that they do not do something plausible instead. `HEADER` on a
* filesystem would have to mean "delete everything here", which is a
* spectacularly bad thing to do to somebody who typed a C128 verb.
*/
static void test_no_drive_verbs(void)
{
struct { const char *program; const char *expect; } cases[] = {
{ "10 HEADER \"DISK\"\n", "formats a disk" },
{ "10 COLLECT\n", "block allocation map" },
{ "10 BACKUP\n", "duplicates one disk" },
{ "10 BOOT\n", "boot sector" },
};
size_t i = 0;
for ( i = 0; i < sizeof(cases) / sizeof(cases[0]); i++ ) {
TEST_REQUIRE_OK(run_program(cases[i].program));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, cases[i].expect) != NULL,
"expected \"%s\" in the refusal, got \"%s\"",
cases[i].expect, HARNESS_OUTPUT);
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "no disk drive") != NULL,
"the refusal should say why, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/* DIRECTORY is refused for a different reason, and says which. */
TEST_REQUIRE_OK(run_program("10 DIRECTORY\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "libakstdlib") != NULL,
"DIRECTORY should name the missing wrapper, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/** @brief DCLEAR closes every channel, which is the half of it that means something. */
static void test_dclear(void)
{
remove("t_clear.txt");
TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_clear.txt\", W\n"
"20 DCLEAR\n"
"30 PRINT #1, \"NO\"\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "is not open") != NULL,
"DCLEAR should have closed the channel, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
remove("t_clear.txt");
}
int main(void)
{
test_channel_round_trip();
test_channel_numeric();
test_append();
test_channel_direction();
test_channel_state();
test_copy_and_concat();
test_rename_and_scratch();
test_bsave_bload();
test_no_drive_verbs();
test_dclear();
return akbasic_test_failures;
}

193
tests/for_next.c Normal file
View File

@@ -0,0 +1,193 @@
/**
* @file for_next.c
* @brief Full coverage of FOR/NEXT, which TODO.md section 6 item 4 is gated on.
*
* That item is `math_plus` mutating its left operand in place when the operand
* is mutable, where every other operator clones. It is a real defect -- `A# + 1`
* modifying `A#` depending on where the value came from -- and it was left alone
* because `NEXT`'s loop increment *relied* on the mutation: fixing one without
* the other silently breaks every FOR loop in the language.
*
* The golden corpus covers a plain loop, a nested pair and the
* `waitingForCommand` case. What it does not cover is `STEP`, a negative step, a
* float counter, `EXIT`, or a body that assigns to the loop variable -- and
* those are the cases where an increment that writes to the wrong storage shows
* up. This file is what made the fix safe to make.
*/
#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 counter advances by one and stops *after* the limit, not on it. */
static void test_plain_loop(void)
{
TEST_REQUIRE_OK(run_program("10 FOR I# = 1 TO 4\n"
"20 PRINT I#\n"
"30 NEXT I#\n"));
/*
* Every value from the start to the limit, once each. That the body sees the
* *incremented* value at all is what says the increment reached the variable
* rather than a scratch copy of it -- the whole point of this file.
*
* What the counter reads after the loop is a separate question, and this
* interpreter answers it differently from a C128: see tests/for_semantics.c.
*/
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n2\n3\n4\n");
harness_stop();
}
/** @brief STEP advances by what it says, and the limit is still a limit. */
static void test_step(void)
{
/*
* A step that lands exactly on the limit. The overshooting case is a known
* defect and lives in tests/for_semantics.c; what is asserted here is only
* that STEP is read and applied.
*/
TEST_REQUIRE_OK(run_program("10 FOR I# = 0 TO 9 STEP 3\n"
"20 PRINT I#\n"
"30 NEXT I#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\n3\n6\n9\n");
harness_stop();
}
/** @brief A negative step counts down, and the comparison flips with it. */
static void test_negative_step(void)
{
TEST_REQUIRE_OK(run_program("10 FOR I# = 5 TO 1 STEP -2\n"
"20 PRINT I#\n"
"30 NEXT I#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "5\n3\n1\n");
harness_stop();
}
/** @brief A float counter accumulates in the float field, not the integer one. */
static void test_float_counter(void)
{
TEST_REQUIRE_OK(run_program("10 FOR I% = 1.0 TO 2.0 STEP 0.5\n"
"20 PRINT I%\n"
"30 NEXT I%\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1.000000\n1.500000\n2.000000\n");
harness_stop();
}
/**
* @brief A body that assigns to the loop variable changes where the loop goes.
*
* The counter is ordinary storage, so writing to it is legal and the loop reads
* the written value. This is the case that tells an increment landing in the
* variable apart from one landing in a scratch value that happens to be read
* back: with a scratch increment the assignment on line 20 would be overwritten
* rather than built on.
*/
static void test_body_assigns_to_counter(void)
{
TEST_REQUIRE_OK(run_program("10 FOR I# = 1 TO 10\n"
"20 I# = I# + 3\n"
"30 PRINT I#\n"
"40 NEXT I#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "4\n8\n12\n");
harness_stop();
}
/** @brief EXIT leaves the loop and lands after its NEXT, with the wait cleared. */
static void test_exit(void)
{
TEST_REQUIRE_OK(run_program("10 FOR I# = 1 TO 10\n"
"20 PRINT I#\n"
"30 IF I# = 2 THEN EXIT\n"
"40 NEXT I#\n"
"50 PRINT \"OUT\"\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n2\nOUT\n");
harness_stop();
/* And a second loop afterwards still runs, so the wait really was cleared. */
TEST_REQUIRE_OK(run_program("10 FOR I# = 1 TO 10\n"
"20 IF I# = 1 THEN EXIT\n"
"30 NEXT I#\n"
"40 FOR J# = 1 TO 2\n"
"50 PRINT J#\n"
"60 NEXT J#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n2\n");
harness_stop();
}
/** @brief Nested loops each advance their own counter and unwind in order. */
static void test_nested(void)
{
TEST_REQUIRE_OK(run_program("10 FOR I# = 1 TO 2\n"
"20 FOR J# = 1 TO 2\n"
"30 PRINT I# * 10 + J#\n"
"40 NEXT J#\n"
"50 NEXT I#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "11\n12\n21\n22\n");
harness_stop();
}
/**
* @brief `A# + 1` does not modify `A#`.
*
* The defect itself, stated as a program. Addition on a variable read out of the
* environment used to update the variable in place, so this printed `2` and then
* `2` -- the first PRINT changing what the second one saw.
*/
static void test_addition_does_not_mutate(void)
{
TEST_REQUIRE_OK(run_program("10 A# = 1\n"
"20 PRINT A# + 1\n"
"30 PRINT A#\n"
"40 PRINT A# + 1\n"
"50 PRINT A#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "2\n1\n2\n1\n");
harness_stop();
/* The same for a float, and for a string, which concatenates. */
TEST_REQUIRE_OK(run_program("10 A% = 1.5\n"
"20 PRINT A% + 1.0\n"
"30 PRINT A%\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "2.500000\n1.500000\n");
harness_stop();
TEST_REQUIRE_OK(run_program("10 A$ = \"X\"\n"
"20 PRINT A$ + \"Y\"\n"
"30 PRINT A$\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "XY\nX\n");
harness_stop();
/* And inside an expression used twice on one line. */
TEST_REQUIRE_OK(run_program("10 A# = 5\n"
"20 PRINT (A# + 1) + (A# + 1)\n"
"30 PRINT A#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "12\n5\n");
harness_stop();
}
int main(void)
{
test_plain_loop();
test_step();
test_negative_step();
test_float_counter();
test_body_assigns_to_counter();
test_exit();
test_nested();
test_addition_does_not_mutate();
return akbasic_test_failures;
}

103
tests/for_semantics.c Normal file
View File

@@ -0,0 +1,103 @@
/**
* @file for_semantics.c
* @brief Known-failing. Asserts what FOR/NEXT *should* do, which is not what it does.
*
* Registered in `AKBASIC_KNOWN_FAILING_TESTS`, so CTest expects it to fail. When
* it starts passing, CTest reports "unexpectedly passed" and that is the cue to
* move it into `AKBASIC_TESTS` along with the fix.
*
* Two defects, both found while writing tests/for_next.c and both recorded in
* TODO.md section 6. Neither is the Go reference's fault alone -- the port
* reproduced them faithfully -- and neither can be fixed without moving a golden
* file, which is why they are written down rather than quietly corrected.
*
* **1. The body runs once with the overshot value.** The loop condition is
* tested against the counter *before* the increment, so the increment's result
* reaches the body before anything checks it against the limit.
* `FOR I = 1 TO 9 STEP 3` runs its body with 1, 4, 7 and then 10.
*
* **2. The counter does not survive the loop.** It lives in the environment the
* loop pushed, which pops when the loop ends, so reading it afterwards finds a
* fresh variable holding zero. A C128 leaves the counter at the value that ended
* the loop, and plenty of published listings read it afterwards.
*
* They are related but not the same fix: the first is an ordering bug in
* akbasic_cmd_next(), and the second is a scoping decision about where a loop
* counter is created.
*/
#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 A step that overshoots the limit must not run the body again. */
static void test_step_does_not_overshoot(void)
{
TEST_REQUIRE_OK(run_program("10 FOR I# = 1 TO 9 STEP 3\n"
"20 PRINT I#\n"
"30 NEXT I#\n"));
/* Today: "1\n4\n7\n10\n" -- the body runs with 10, which is past the limit. */
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n4\n7\n");
harness_stop();
TEST_REQUIRE_OK(run_program("10 FOR I# = 1 TO 2 STEP 100\n"
"20 PRINT I#\n"
"30 NEXT I#\n"));
/* Today: "1\n101\n". */
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n");
harness_stop();
}
/** @brief The loop counter is readable after the loop, holding where it stopped. */
static void test_counter_survives_the_loop(void)
{
TEST_REQUIRE_OK(run_program("10 FOR I# = 1 TO 4\n"
"20 NEXT I#\n"
"30 PRINT I#\n"));
/* Today: "0\n" -- the loop's environment took the variable with it. */
TEST_REQUIRE_STR(HARNESS_OUTPUT, "5\n");
harness_stop();
}
/**
* @brief A loop whose limit equals its start still runs its body once.
*
* `FOR I = 1 TO 1` executes the body one time on every BASIC there has ever
* been. Here the entry test treats "the counter has reached the limit" as
* "do not enter", so the body is skipped entirely -- and
* tests/reference/language/flowcontrol/forloopwaitingforcommand.bas pins that,
* which is why this cannot simply be corrected.
*/
static void test_single_iteration_loop(void)
{
TEST_REQUIRE_OK(run_program("10 FOR I# = 1 TO 1\n"
"20 PRINT \"ONCE\"\n"
"30 NEXT I#\n"));
/* Today: "" -- the body never runs. */
TEST_REQUIRE_STR(HARNESS_OUTPUT, "ONCE\n");
harness_stop();
}
int main(void)
{
test_step_does_not_overshoot();
test_counter_survives_the_loop();
test_single_iteration_loop();
return akbasic_test_failures;
}

273
tests/format_verbs.c Normal file
View File

@@ -0,0 +1,273 @@
/**
* @file format_verbs.c
* @brief Tests the group D verbs: PRINT USING, PUDEF, WIDTH and CHAR.
*
* Field formatting has more edge cases than a verb usually does, and
* akbasic_format_using() is a pure function of a format string and a value, so
* most of this exercises it directly rather than through a program. What goes
* through a program is the part a program can get wrong: the parse of
* `PRINT USING fmt; value`, and the verbs that only set state.
*/
#include <string.h>
#include <akbasic/error.h>
#include <akbasic/format.h>
#include <akbasic/runtime.h>
#include "harness.h"
#include "testutil.h"
static akbasic_FormatState FORMAT;
/** @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 Format one value, for the direct tests below. */
static akerr_ErrorContext AKERR_NOIGNORE *fmt_number(const char *format, double number, char *dest, size_t len)
{
PREPARE_ERROR(errctx);
akbasic_Value value;
PASS(errctx, akbasic_value_zero(&value));
value.valuetype = AKBASIC_TYPE_FLOAT;
value.floatval = number;
PASS(errctx, akbasic_format_using(&FORMAT, format, &value, dest, len));
SUCCEED_RETURN(errctx);
}
/** @brief Format one string, for the direct tests below. */
static akerr_ErrorContext AKERR_NOIGNORE *fmt_string(const char *format, const char *text, char *dest, size_t len)
{
PREPARE_ERROR(errctx);
akbasic_Value value;
PASS(errctx, akbasic_value_zero(&value));
value.valuetype = AKBASIC_TYPE_STRING;
snprintf(value.stringval, sizeof(value.stringval), "%s", text);
PASS(errctx, akbasic_format_using(&FORMAT, format, &value, dest, len));
SUCCEED_RETURN(errctx);
}
/** @brief Digit positions right-justify, and the decimal places are fixed. */
static void test_numeric_fields(void)
{
char out[256];
TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
TEST_REQUIRE_OK(fmt_number("###.##", 3.14159, out, sizeof(out)));
TEST_REQUIRE_STR(out, " 3.14");
/* Rounded to the field, not truncated. */
TEST_REQUIRE_OK(fmt_number("###.#", 2.06, out, sizeof(out)));
TEST_REQUIRE_STR(out, " 2.1");
/* No decimal point in the field means no fractional part printed. */
TEST_REQUIRE_OK(fmt_number("####", 42.9, out, sizeof(out)));
TEST_REQUIRE_STR(out, " 43");
/* Zero fills the whole field with blanks but the one digit. */
TEST_REQUIRE_OK(fmt_number("####", 0.0, out, sizeof(out)));
TEST_REQUIRE_STR(out, " 0");
}
/** @brief Literal text around the field is copied through. */
static void test_literal_text(void)
{
char out[256];
TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
TEST_REQUIRE_OK(fmt_number("TOTAL: ###.## EACH", 12.5, out, sizeof(out)));
TEST_REQUIRE_STR(out, "TOTAL: 12.50 EACH");
}
/** @brief A comma in the field groups the integer part in threes. */
static void test_grouping(void)
{
char out[256];
TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
TEST_REQUIRE_OK(fmt_number("#,###,###", 1234567.0, out, sizeof(out)));
TEST_REQUIRE_STR(out, "1,234,567");
/* Padding counts the separators, so a column stays a column. */
TEST_REQUIRE_OK(fmt_number("#,###,###", 12.0, out, sizeof(out)));
TEST_REQUIRE_STR(out, " 12");
}
/** @brief A currency sign and a sign position are part of the field. */
static void test_decorations(void)
{
char out[256];
TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
TEST_REQUIRE_OK(fmt_number("$#,###.##", 1234.5, out, sizeof(out)));
TEST_REQUIRE_STR(out, "$1,234.50");
TEST_REQUIRE_OK(fmt_number("+####", 42.0, out, sizeof(out)));
TEST_REQUIRE_STR(out, "+ 42");
TEST_REQUIRE_OK(fmt_number("+####", -42.0, out, sizeof(out)));
TEST_REQUIRE_STR(out, "- 42");
/* A trailing sign position puts the sign after the digits. */
TEST_REQUIRE_OK(fmt_number("####-", -42.0, out, sizeof(out)));
TEST_REQUIRE_STR(out, " 42-");
}
/**
* @brief A value too wide for its field fills the field with stars.
*
* BASIC 7.0's answer, and deliberately loud: printing more digits than the field
* asked for would push every later column out of line, which is exactly what
* PRINT USING is for avoiding.
*/
static void test_overflow(void)
{
char out[256];
TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
TEST_REQUIRE_OK(fmt_number("###", 99999.0, out, sizeof(out)));
TEST_REQUIRE_STR(out, "***");
/*
* A negative number in a field with no sign position overflows too. Dropping
* the minus would print -5 as 5, which is worse than a row of stars.
*/
TEST_REQUIRE_OK(fmt_number("###", -5.0, out, sizeof(out)));
TEST_REQUIRE_STR(out, "***");
}
/** @brief `=` centres a string in its field and `>` right-justifies it. */
static void test_string_fields(void)
{
char out[256];
TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
TEST_REQUIRE_OK(fmt_string("==========", "MID", out, sizeof(out)));
TEST_REQUIRE_STR(out, " MID ");
TEST_REQUIRE_OK(fmt_string(">>>>>>>>>>", "RIGHT", out, sizeof(out)));
TEST_REQUIRE_STR(out, " RIGHT");
/* A string longer than its field is cut, not starred. */
TEST_REQUIRE_OK(fmt_string("===", "TOOLONG", out, sizeof(out)));
TEST_REQUIRE_STR(out, "TOO");
}
/** @brief A field and a value of the wrong kind for each other are refused. */
static void test_type_mismatch(void)
{
char out[256];
TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
TEST_REQUIRE_STATUS(fmt_string("###", "TEXT", out, sizeof(out)), AKBASIC_ERR_TYPE);
TEST_REQUIRE_STATUS(fmt_number("===", 1.0, out, sizeof(out)), AKBASIC_ERR_TYPE);
/* A format with no field at all is a mistake rather than a literal. */
TEST_REQUIRE_STATUS(fmt_number("NO FIELD HERE", 1.0, out, sizeof(out)), AKBASIC_ERR_SYNTAX);
}
/** @brief PUDEF replaces the fill characters, as many as it was given. */
static void test_pudef(void)
{
char out[256];
TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
TEST_REQUIRE_OK(akbasic_format_pudef(&FORMAT, "*"));
TEST_REQUIRE_OK(fmt_number("#####", 42.0, out, sizeof(out)));
TEST_REQUIRE_STR(out, "***42");
/* The other three are untouched by a one-character PUDEF. */
TEST_REQUIRE_OK(fmt_number("#,###.##", 1234.5, out, sizeof(out)));
TEST_REQUIRE_STR(out, "1,234.50");
/* All four at once. */
TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
TEST_REQUIRE_OK(akbasic_format_pudef(&FORMAT, "_. ,"));
TEST_REQUIRE_OK(fmt_number("$#,###.##", 1234.5, out, sizeof(out)));
TEST_REQUIRE_STR(out, ",1.234 50");
}
/** @brief PRINT USING parses and prints through a running program. */
static void test_print_using(void)
{
TEST_REQUIRE_OK(run_program("10 PRINT USING \"###.##\"; 3.14159\n"
"20 PRINT USING \"TOTAL: $#,###.##\"; 1234.5\n"
"30 PRINT USING \"###\"; 99999\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, " 3.14\nTOTAL: $1,234.50\n***\n");
harness_stop();
/* A plain PRINT still works, which the shared parse handler could break. */
TEST_REQUIRE_OK(run_program("10 PRINT \"PLAIN\"\n20 PRINT 1 + 2\n30 PRINT\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "PLAIN\n3\n\n");
harness_stop();
/* And PUDEF reaches PRINT USING through the runtime's own state. */
TEST_REQUIRE_OK(run_program("10 PUDEF \"*\"\n"
"20 PRINT USING \"#####\"; 42\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "***42\n");
harness_stop();
/* The semicolon is required; without it the format has no value to print. */
TEST_REQUIRE_OK(run_program("10 PRINT USING \"###\" 42\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "semicolon") != NULL,
"a missing semicolon should say so, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/** @brief WIDTH takes 1 or 2 and refuses anything else. */
static void test_width(void)
{
TEST_REQUIRE_OK(run_program("10 WIDTH 2\n20 PRINT \"OK\"\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "OK\n");
TEST_REQUIRE_INT(HARNESS_RUNTIME.gfx.linewidth, 2);
harness_stop();
TEST_REQUIRE_OK(run_program("10 WIDTH 3\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "WIDTH is 1 or 2") != NULL,
"WIDTH 3 should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
/* No device needed: it is drawing state, like COLOR's bindings. */
TEST_REQUIRE_OK(run_program("10 WIDTH 2\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "device") == NULL,
"WIDTH should not need a device, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/** @brief CHAR refuses against a sink with no cursor, which is the stdio one. */
static void test_char_needs_a_cursor(void)
{
TEST_REQUIRE_OK(run_program("10 CHAR 1, 5, 3, \"HI\"\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "position its cursor") != NULL,
"CHAR against a stdio sink should refuse by name, got \"%s\"",
HARNESS_OUTPUT);
harness_stop();
}
int main(void)
{
test_numeric_fields();
test_literal_text();
test_grouping();
test_decorations();
test_overflow();
test_string_fields();
test_type_mismatch();
test_pudef();
test_print_using();
test_width();
test_char_needs_a_cursor();
return akbasic_test_failures;
}

View File

@@ -27,7 +27,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
PASS(errctx, harness_start(NULL));
mock_devices_init();
PASS(errctx, akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS,
&MOCK_AUDIO, &MOCK_INPUT));
&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));
@@ -63,7 +63,7 @@ static void test_draw(void)
/* 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));
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));
@@ -163,7 +163,7 @@ static void test_paint(void)
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));
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));

View File

@@ -247,6 +247,40 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_repl_stops_after_an_error(void)
SUCCEED_RETURN(errctx);
}
/**
* @brief A statement typed with no line number runs; one with a number is stored.
*
* Direct mode. The reference only ran the verbs it marked immediate -- RUN, LIST,
* NEW and the rest -- and filed everything else as program text, so `PRINT 2 + 2`
* at a prompt silently became line 0 of a program instead of answering. See
* TODO.md section 5.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_direct_mode(void)
{
PREPARE_ERROR(errctx);
TEST_REQUIRE_OK(harness_start("PRINT 2 + 2\n"
"A# = 5\n"
"PRINT A# * 3\n"
"10 PRINT \"STORED\"\n"
"RUN\n"));
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_REPL));
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 200));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "4\n") != NULL,
"PRINT 2 + 2 should have answered 4, got \"%s\"", HARNESS_OUTPUT);
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "15\n") != NULL,
"an assignment then a PRINT should both have run, got \"%s\"",
HARNESS_OUTPUT);
/* And the numbered line was stored rather than run when it was typed. */
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "PRINT \"STORED\"");
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "STORED") != NULL,
"RUN should have run the stored line, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
@@ -260,6 +294,7 @@ int main(void)
CATCH(errctx, test_help());
CATCH(errctx, test_trace_flag());
CATCH(errctx, test_repl_stops_after_an_error());
CATCH(errctx, test_direct_mode());
} CLEANUP {
} PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) {

View File

@@ -26,7 +26,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
PASS(errctx, harness_start(NULL));
mock_devices_init();
PASS(errctx, akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS,
&MOCK_AUDIO, &MOCK_INPUT));
&MOCK_AUDIO, &MOCK_INPUT, NULL));
PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
SUCCEED_RETURN(errctx);
@@ -146,7 +146,7 @@ static void test_getkey_device_withdrawn(void)
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 20));
TEST_REQUIRE(HARNESS_RUNTIME.input_state.waiting, "GETKEY should be holding");
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, NULL, NULL));
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, NULL, NULL, NULL));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "RELEASED\n");
harness_stop();

366
tests/interrupts.c Normal file
View File

@@ -0,0 +1,366 @@
/**
* @file interrupts.c
* @brief Tests the interrupt machinery: arm, raise, enter a handler, RETURN.
*
* This is the part of COLLISION that has nothing to do with sprites, and it is
* tested on its own for two reasons. It is what group C's TRAP will reuse, so it
* has to be correct before anything is built on it; and driving it through the
* API rather than through a verb is the only way to fire an interrupt at an
* exactly known point in a program, which is what most of these assertions turn
* on.
*
* **Every program here is numbered 1, 2, 3, ...** One step of the runtime
* advances by one source line *index*, empty ones included -- so consecutive
* numbering is what makes a step count mean a statement count. The first step of
* any run is line 0, which is always empty, and is accounted for in each count
* below.
*/
#include "harness.h"
/**
* @brief A program with a handler at 3 and a main loop that never ends.
*
* The endless loop is on purpose: every test here steps a fixed number of times
* and asserts what came out, so a regression that fails to enter or fails to
* return shows up as the wrong output rather than as a run that stops early and
* looks plausible.
*/
static const char *PROGRAM_BY_LINE =
"1 PRINT \"START\"\n"
"2 GOTO 5\n"
"3 PRINT \"HANDLER\"\n"
"4 RETURN\n"
"5 PRINT \"MAIN\"\n"
"6 GOTO 5\n";
/** @brief Load a program and put the runtime in RUN mode without stepping it. */
static akerr_ErrorContext AKERR_NOIGNORE *load_program(const char *source)
{
PREPARE_ERROR(errctx);
PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
SUCCEED_RETURN(errctx);
}
/** @brief A handler named by line number runs, and RETURN resumes where it left. */
static akerr_ErrorContext AKERR_NOIGNORE *test_enter_and_return(void)
{
PREPARE_ERROR(errctx);
TEST_REQUIRE_OK(harness_start(NULL));
PASS(errctx, load_program(PROGRAM_BY_LINE));
PASS(errctx, akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE, 3, NULL));
/* Line 0, then 1 and 2, so the program is sitting on 5 with nothing pending. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 3));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "START\n");
PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
TEST_REQUIRE(HARNESS_RUNTIME.handlerenv == NULL, "raising must not enter anything by itself");
/* One step: the handler is entered and its first line runs. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "START\nHANDLER\n");
TEST_REQUIRE(HARNESS_RUNTIME.handlerenv != NULL, "the runtime should know it is in a handler");
TEST_REQUIRE(!HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].pending,
"entering the handler should have consumed the event");
/* The RETURN, then the line the interrupt took the program away from. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 2));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "START\nHANDLER\nMAIN\n");
TEST_REQUIRE(HARNESS_RUNTIME.handlerenv == NULL, "RETURN should have left the handler");
/* Still armed: one collision handled is not a subscription cancelled. */
TEST_REQUIRE(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].armed,
"the handler should still be armed after it returns");
harness_stop();
SUCCEED_RETURN(errctx);
}
/**
* @brief A handler named by label, on a line the program never falls into.
*
* This is the shape a handler is actually written in -- jumped over by the main
* flow, reachable only through the interrupt -- and it works only because
* akbasic_runtime_scan_labels() files LABEL before anything runs. Without the
* prescan the label does not exist when the interrupt fires and the run stops
* with AKBASIC_ERR_UNDEFINED.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_label_handler(void)
{
PREPARE_ERROR(errctx);
TEST_REQUIRE_OK(harness_start(NULL));
PASS(errctx, load_program("1 GOTO 5\n"
"2 LABEL BUMPED\n"
"3 PRINT \"HANDLER\"\n"
"4 RETURN\n"
"5 PRINT \"MAIN\"\n"
"6 GOTO 5\n"));
PASS(errctx, akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE,
0, "BUMPED"));
/* Line 0, the GOTO, then the main loop's PRINT. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 3));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\n");
PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
/* The LABEL line the handler is entered on, then the handler's PRINT. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 2));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\nHANDLER\n");
/* RETURN, then back into the main loop where it left off. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 3));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\nHANDLER\nMAIN\n");
harness_stop();
SUCCEED_RETURN(errctx);
}
/**
* @brief An interrupt does not interrupt an interrupt.
*
* Without the guard a collision that is still true while its own handler runs
* re-enters on the next line, and keeps re-entering until the environment pool
* is gone -- the failure would be AKBASIC_ERR_ENVIRONMENT from somewhere with
* nothing to do with sprites. The event raised inside the handler is not lost,
* though: it is taken as soon as the RETURN lands.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_no_reentry(void)
{
PREPARE_ERROR(errctx);
TEST_REQUIRE_OK(harness_start(NULL));
PASS(errctx, load_program("1 GOTO 5\n"
"2 PRINT \"IN\"\n"
"3 PRINT \"OUT\"\n"
"4 RETURN\n"
"5 PRINT \"MAIN\"\n"
"6 GOTO 5\n"));
PASS(errctx, akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE, 2, NULL));
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 3));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\n");
PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\nIN\n");
/* Fires again while the handler is still running. */
PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\nIN\nOUT\n");
TEST_REQUIRE(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].pending,
"an event raised inside a handler should be held, not dropped");
/* The RETURN lands, and the held event is taken on the very next step. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 2));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\nIN\nOUT\nIN\n");
harness_stop();
SUCCEED_RETURN(errctx);
}
/** @brief A GOSUB the handler itself makes returns without re-arming interrupts. */
static akerr_ErrorContext AKERR_NOIGNORE *test_nested_gosub_keeps_the_flag(void)
{
PREPARE_ERROR(errctx);
TEST_REQUIRE_OK(harness_start(NULL));
PASS(errctx, load_program("1 GOTO 5\n"
"2 GOSUB 8\n"
"3 RETURN\n"
"5 PRINT \"MAIN\"\n"
"6 GOTO 5\n"
"8 PRINT \"INNER\"\n"
"9 RETURN\n"));
PASS(errctx, akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE, 2, NULL));
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 3));
PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
/* Enter the handler, which immediately GOSUBs deeper. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1));
TEST_REQUIRE(HARNESS_RUNTIME.handlerenv != NULL, "the handler should be running");
/* The inner PRINT and the inner RETURN: still inside the handler. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 2));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\nINNER\n");
TEST_REQUIRE(HARNESS_RUNTIME.handlerenv != NULL,
"the inner RETURN belongs to the inner GOSUB, not to the handler");
/* The handler's own RETURN is the one that clears it. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1));
TEST_REQUIRE(HARNESS_RUNTIME.handlerenv == NULL, "the handler's RETURN should have cleared it");
harness_stop();
SUCCEED_RETURN(errctx);
}
/** @brief Raising an unarmed source records nothing; disarming drops what is held. */
static akerr_ErrorContext AKERR_NOIGNORE *test_arm_and_disarm(void)
{
PREPARE_ERROR(errctx);
TEST_REQUIRE_OK(harness_start(NULL));
PASS(errctx, load_program(PROGRAM_BY_LINE));
/* Unarmed. A backend may raise every frame without asking who is listening. */
PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
TEST_REQUIRE(!HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].pending,
"an unarmed source should record nothing");
/* Armed, raised, then taken away again before it could be taken. */
PASS(errctx, akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE, 3, NULL));
PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
PASS(errctx, akbasic_runtime_disarm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
TEST_REQUIRE(!HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].pending,
"disarming should drop a pending event rather than deferring it");
/* And the program runs straight through without ever seeing line 3. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 6));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "START\nMAIN\nMAIN\n");
harness_stop();
SUCCEED_RETURN(errctx);
}
/** @brief NEW disarms everything: the handler lines belong to a program that is gone. */
static akerr_ErrorContext AKERR_NOIGNORE *test_new_disarms(void)
{
PREPARE_ERROR(errctx);
akbasic_ASTLeaf *leaf = NULL;
akbasic_Value *out = NULL;
TEST_REQUIRE_OK(harness_start(NULL));
PASS(errctx, load_program(PROGRAM_BY_LINE));
PASS(errctx, akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE, 3, NULL));
PASS(errctx, akbasic_environment_zero(HARNESS_RUNTIME.environment));
PASS(errctx, harness_parse("NEW", &leaf));
PASS(errctx, akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
TEST_REQUIRE(!HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].armed,
"NEW should have disarmed the interrupt");
harness_stop();
SUCCEED_RETURN(errctx);
}
/**
* @brief A handler label that names nothing is the program's error, not the host's.
*
* It has to be *reported* and stop the run. Letting it out of
* akbasic_runtime_step() would tear down an embedding game over a typo in a
* script, which is what goal 3 exists to prevent.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_undefined_label_is_reported(void)
{
PREPARE_ERROR(errctx);
TEST_REQUIRE_OK(harness_start(NULL));
PASS(errctx, load_program("1 PRINT \"MAIN\"\n"
"2 GOTO 1\n"));
PASS(errctx, akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE,
0, "NOSUCHLABEL"));
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 2));
PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
/* Returns cleanly -- the error is on the sink, not in the return value. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "NOSUCHLABEL") != NULL,
"the report should name the label that could not be found, got: %s",
HARNESS_OUTPUT);
TEST_REQUIRE_INT(HARNESS_RUNTIME.mode, AKBASIC_MODE_QUIT);
harness_stop();
SUCCEED_RETURN(errctx);
}
/** @brief Arming refuses a source out of range, and a target that is both or neither. */
static akerr_ErrorContext AKERR_NOIGNORE *test_arm_refusals(void)
{
PREPARE_ERROR(errctx);
TEST_REQUIRE_OK(harness_start(NULL));
TEST_REQUIRE_STATUS(akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME,
(akbasic_InterruptSource)AKBASIC_MAX_INTERRUPTS,
3, NULL),
AKBASIC_ERR_BOUNDS);
TEST_REQUIRE_STATUS(akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE,
0, NULL),
AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE,
3, "BUMPED"),
AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME,
(akbasic_InterruptSource)-1),
AKBASIC_ERR_BOUNDS);
TEST_REQUIRE_STATUS(akbasic_runtime_disarm_interrupt(&HARNESS_RUNTIME,
(akbasic_InterruptSource)AKBASIC_MAX_INTERRUPTS),
AKBASIC_ERR_BOUNDS);
TEST_REQUIRE_STATUS(akbasic_runtime_arm_interrupt(NULL, AKBASIC_INTERRUPT_SPRITE, 3, NULL),
AKERR_NULLPOINTER);
TEST_REQUIRE_STATUS(akbasic_runtime_service_interrupts(NULL, NULL), AKERR_NULLPOINTER);
TEST_REQUIRE_STATUS(akbasic_runtime_scan_labels(NULL), AKERR_NULLPOINTER);
harness_stop();
SUCCEED_RETURN(errctx);
}
/**
* @brief The label prescan reads LABEL where it is a statement, and nowhere else.
*
* The forward jump itself is covered by the golden case at
* tests/language/statements/label_forward.bas. What is here is the part a
* running program cannot show: that a string literal holding the word LABEL
* files nothing, and that a second statement on a line is still a statement.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_prescan_boundaries(void)
{
PREPARE_ERROR(errctx);
int64_t line = 0;
TEST_REQUIRE_OK(harness_start(NULL));
PASS(errctx, load_program("1 PRINT \"LABEL DECOY\"\n"
"2 A# = 1 : LABEL SECOND\n"
"3 LABELLED# = 2\n"));
PASS(errctx, akbasic_environment_get_label(HARNESS_RUNTIME.environment, "SECOND", &line));
TEST_REQUIRE_INT(line, 2);
TEST_REQUIRE_STATUS(akbasic_environment_get_label(HARNESS_RUNTIME.environment, "DECOY", &line),
AKBASIC_ERR_UNDEFINED);
/* LABELLED# is an identifier that starts with the word, not the verb. */
TEST_REQUIRE_STATUS(akbasic_environment_get_label(HARNESS_RUNTIME.environment, "ED", &line),
AKBASIC_ERR_UNDEFINED);
harness_stop();
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, test_enter_and_return());
CATCH(errctx, test_label_handler());
CATCH(errctx, test_no_reentry());
CATCH(errctx, test_nested_gosub_keeps_the_flag());
CATCH(errctx, test_arm_and_disarm());
CATCH(errctx, test_new_disarms());
CATCH(errctx, test_undefined_label_is_reported());
CATCH(errctx, test_arm_refusals());
CATCH(errctx, test_prescan_boundaries());
} CLEANUP {
} PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) {
LOG_ERROR_WITH_MESSAGE(errctx, "interrupt test failed");
akbasic_test_failures += 1;
} FINISH_NORETURN(errctx);
return akbasic_test_failures;
}

View File

@@ -0,0 +1,13 @@
10 REM TRAP catches a BASIC error and sends the program to a handler.
20 TRAP HANDLER
30 DIM Q#(2)
40 PRINT "BEFORE"
50 PRINT Q#(9)
60 PRINT "RESUMED"
70 TRAP
80 REM Disarmed again, so this one is reported and stops the program.
90 PRINT Q#(9)
100 PRINT "NEVER SEEN"
200 LABEL HANDLER
210 PRINT "CAUGHT " + ERR(ER#) + " ON LINE " + EL#
220 RESUME NEXT

View File

@@ -0,0 +1,5 @@
BEFORE
CAUGHT Out Of Bounds ON LINE 50
RESUMED
? 90 : RUNTIME ERROR Variable index access out of bounds at dimension 0: 9 (max 1)

View File

@@ -0,0 +1,12 @@
10 REM PRINT USING lays a number out in a fixed field.
20 PRINT USING "###.##"; 3.14159
30 PRINT USING "TOTAL: $#,###.##"; 1234.5
40 PRINT USING "####-"; -42
50 REM A value too wide for its field fills the field with stars.
60 PRINT USING "###"; 99999
70 REM String fields centre and right-justify.
80 PRINT USING "=========="; "MID"
90 PRINT USING ">>>>>>>>>>"; "RIGHT"
100 REM PUDEF changes what a numeric field pads and punctuates with.
110 PUDEF "*"
120 PRINT USING "#####"; 42

View File

@@ -0,0 +1,7 @@
3.14
TOTAL: $1,234.50
42-
***
MID
RIGHT
***42

View File

@@ -0,0 +1,11 @@
10 REM The standalone driver lends the script no sprite device.
20 REM SPRCOLOR and the RSP* readbacks only touch interpreter state.
30 SPRCOLOR 5, 7
40 PRINT RSPCOLOR(1)
50 PRINT RSPCOLOR(2)
60 REM An untouched sprite reads back at the origin, in the default colour.
70 PRINT RSPPOS(1, 0)
80 PRINT RSPRITE(1, 1)
90 REM SPRSAV needs the device, and has to say which verb wanted it.
100 SPRSAV "ship.png", 1
110 PRINT "UNREACHED"

View File

@@ -0,0 +1,6 @@
5
7
0
2
? 100 : RUNTIME ERROR SPRSAV needs a sprite device and this runtime has none

View File

@@ -0,0 +1,12 @@
10 GOTO SKIPAHEAD
20 PRINT "SKIPPED"
30 LABEL SKIPAHEAD
40 PRINT "ARRIVED"
50 GOSUB LATER
60 PRINT "BACK"
70 GOTO DONE
80 LABEL LATER
90 PRINT "IN SUB"
100 RETURN
110 LABEL DONE
120 PRINT "FINISHED"

View File

@@ -0,0 +1,4 @@
ARRIVED
IN SUB
BACK
FINISHED

View File

@@ -0,0 +1,33 @@
10 REM DO/LOOP in all four shapes, plus ON and a multi-line IF block.
20 I# = 0
30 DO WHILE I# < 3
40 PRINT "WHILE " + I#
50 I# = I# + 1
60 LOOP
70 I# = 0
80 DO
90 PRINT "UNTIL " + I#
100 I# = I# + 1
110 LOOP UNTIL I# = 2
120 REM A condition already false runs no body at all.
130 DO WHILE 1 == 2
140 PRINT "NEVER SEEN"
150 LOOP
160 REM ON picks the nth target, one-based, and falls through past the end.
170 X# = 2
180 ON X# GOSUB 500, 600
190 X# = 7
200 ON X# GOTO 500, 600
210 PRINT "FELL THROUGH"
220 REM BEGIN/BEND makes an IF span lines.
230 IF I# = 2 THEN BEGIN
240 PRINT "IN BLOCK"
250 BEND
260 IF I# = 99 THEN BEGIN
270 PRINT "NEVER SEEN EITHER"
280 BEND
290 END
500 PRINT "SUB ONE"
510 RETURN
600 PRINT "SUB TWO"
610 RETURN

View File

@@ -0,0 +1,8 @@
WHILE 0
WHILE 1
WHILE 2
UNTIL 0
UNTIL 1
SUB TWO
FELL THROUGH
IN BLOCK

115
tests/machine_verbs.c Normal file
View File

@@ -0,0 +1,115 @@
/**
* @file machine_verbs.c
* @brief Tests the group J verbs: FETCH, STASH and SYS.
*
* These operate on real process memory, exactly as POKE, PEEK and POINTER
* already do, so the tests give them real addresses -- their own buffers, taken
* with POINTER the way a BASIC program would have to.
*/
#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 STASH copies bytes, and FETCH copies them back.
*
* Addresses come from POINTER, which is the only way a BASIC program can name a
* real one. Two string variables give two buffers to copy between.
*/
static void test_copy(void)
{
TEST_REQUIRE_OK(run_program("10 A$ = \"SOURCE\"\n"
"20 B$ = \"XXXXXX\"\n"
"30 STASH 7, POINTER(A$), POINTER(B$)\n"
"40 PRINT B$\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "SOURCE\n");
harness_stop();
/* FETCH is the same copy; there is no expansion RAM to distinguish them. */
TEST_REQUIRE_OK(run_program("10 A$ = \"HELLO!\"\n"
"20 B$ = \"......\"\n"
"30 FETCH 7, POINTER(A$), POINTER(B$)\n"
"40 PRINT B$\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "HELLO!\n");
harness_stop();
}
/** @brief A zero-length copy does nothing, and a negative one is refused. */
static void test_copy_bounds(void)
{
TEST_REQUIRE_OK(run_program("10 A$ = \"ABC\"\n"
"20 B$ = \"XYZ\"\n"
"30 STASH 0, POINTER(A$), POINTER(B$)\n"
"40 PRINT B$\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "XYZ\n");
harness_stop();
TEST_REQUIRE_OK(run_program("10 A$ = \"ABC\"\n"
"20 STASH -1, POINTER(A$), POINTER(A$)\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "negative number of bytes") != NULL,
"a negative count should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
/* Address zero is refused rather than dereferenced. */
TEST_REQUIRE_OK(run_program("10 A$ = \"ABC\"\n"
"20 STASH 3, 0, POINTER(A$)\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "address zero") != NULL,
"address zero should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
/* Too few arguments is a syntax error naming the form. */
TEST_REQUIRE_OK(run_program("10 STASH 3\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "source address") != NULL,
"an incomplete STASH should name the form, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/**
* @brief SYS parses and is then refused by name.
*
* Parsing first is what keeps a listing containing SYS loadable and listable; it
* is only running it that has no meaning.
*/
static void test_sys_refused(void)
{
TEST_REQUIRE_OK(run_program("10 SYS 65490\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "no 6502") != NULL,
"SYS should be refused with a reason, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
/*
* A SYS with no address is a syntax error, not the device refusal -- and it
* names the verb. It used to fail deep in the expression parser with
* "peek() returned nil token!", which every verb taking an argument list
* shared.
*/
TEST_REQUIRE_OK(run_program("10 SYS\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "SYS expected at least one argument") != NULL,
"a bare SYS should name the verb, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
int main(void)
{
test_copy();
test_copy_bounds();
test_sys_refused();
return akbasic_test_failures;
}

View File

@@ -24,6 +24,7 @@
#include <akbasic/error.h>
#include <akbasic/graphics.h>
#include <akbasic/input.h>
#include <akbasic/sprite.h>
/** @brief Room for the call log. Longer than any test needs; overflow truncates loudly. */
#define MOCK_LOG_SIZE 8192
@@ -50,12 +51,17 @@ typedef struct
int keys[MOCK_MAX_KEYS];
int keycount;
int keynext;
/* Sprites */
uint16_t collisions; /* what the next collisions() call reports */
int patternbytes[AKBASIC_MAX_SPRITES]; /* bytes the last define() carried, per sprite */
} akbasic_MockDevice;
static akbasic_MockDevice MOCK;
static akbasic_GraphicsBackend MOCK_GRAPHICS;
static akbasic_AudioBackend MOCK_AUDIO;
static akbasic_InputBackend MOCK_INPUT;
static akbasic_SpriteBackend MOCK_SPRITES;
/**
* @brief Append one formatted call to the log.
@@ -288,6 +294,120 @@ static akerr_ErrorContext *mock_flush_keys(akbasic_InputBackend *self)
SUCCEED_RETURN(errctx);
}
/* ---------------------------------------------------------------- sprites -- */
/**
* @brief Refuse a sprite number the way a real backend has to.
*
* Sprites are 1-based at this boundary -- see sprite.h -- so an off-by-one in a
* verb handler shows up here as AKBASIC_ERR_BOUNDS rather than as a silently
* mis-addressed sprite.
*/
static akerr_ErrorContext AKERR_NOIGNORE *mock_sprite_range(int n)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (n >= 1 && n <= AKBASIC_MAX_SPRITES), AKBASIC_ERR_BOUNDS,
"mock sprite %d out of range", n);
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *mock_spr_define(akbasic_SpriteBackend *self, int n, const uint8_t *pattern, int bytes, akbasic_Color fg, akbasic_Color bg)
{
PREPARE_ERROR(errctx);
(void)self;
PASS(errctx, mock_sprite_range(n));
FAIL_ZERO_RETURN(errctx, (pattern != NULL), AKERR_NULLPOINTER, "NULL pattern in define");
MOCK.patternbytes[n - 1] = bytes;
/*
* The first and last bytes rather than all 63: enough to tell one pattern
* from another and to catch a byte order that came through reversed,
* without a log line no human can read.
*/
mock_log("sprdef %d %d bytes %02x..%02x fg=%d,%d,%d bg=%d,%d,%d\n",
n, bytes, pattern[0], pattern[bytes - 1],
fg.r, fg.g, fg.b, bg.r, bg.g, bg.b);
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *mock_spr_define_shape(akbasic_SpriteBackend *self, int n, int handle)
{
PREPARE_ERROR(errctx);
(void)self;
PASS(errctx, mock_sprite_range(n));
mock_log("sprshape %d from %d\n", n, handle);
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *mock_spr_define_file(akbasic_SpriteBackend *self, int n, const char *path, const char *root)
{
PREPARE_ERROR(errctx);
(void)self;
PASS(errctx, mock_sprite_range(n));
FAIL_ZERO_RETURN(errctx, (path != NULL), AKERR_NULLPOINTER, "NULL path in define_file");
/*
* The root is logged as well as the path. Whether the interpreter passed the
* running program's directory through is the whole of what this repository
* owns about relative asset paths -- resolving one is libakgl's job, and
* there is no way to see it happen from a mock.
*/
mock_log("sprfile %d %s root=%s\n", n, path, (root != NULL ? root : "(none)"));
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *mock_spr_show(akbasic_SpriteBackend *self, int n, bool visible)
{
PREPARE_ERROR(errctx);
(void)self;
PASS(errctx, mock_sprite_range(n));
mock_log("sprshow %d %d\n", n, (visible ? 1 : 0));
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *mock_spr_move(akbasic_SpriteBackend *self, int n, double x, double y)
{
PREPARE_ERROR(errctx);
(void)self;
PASS(errctx, mock_sprite_range(n));
mock_log("sprmove %d %.2f,%.2f\n", n, x, y);
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *mock_spr_configure(akbasic_SpriteBackend *self, int n, akbasic_Color color, bool xexpand, bool yexpand, bool behind)
{
PREPARE_ERROR(errctx);
(void)self;
PASS(errctx, mock_sprite_range(n));
mock_log("sprcfg %d %d,%d,%d x%d y%d b%d\n", n, color.r, color.g, color.b,
(xexpand ? 1 : 0), (yexpand ? 1 : 0), (behind ? 1 : 0));
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *mock_spr_shared_colors(akbasic_SpriteBackend *self, akbasic_Color c1, akbasic_Color c2)
{
PREPARE_ERROR(errctx);
(void)self;
mock_log("sprshared %d,%d,%d %d,%d,%d\n", c1.r, c1.g, c1.b, c2.r, c2.g, c2.b);
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *mock_spr_collisions(akbasic_SpriteBackend *self, uint16_t *mask)
{
PREPARE_ERROR(errctx);
(void)self;
FAIL_ZERO_RETURN(errctx, (mask != NULL), AKERR_NULLPOINTER, "NULL mask in collisions");
*mask = MOCK.collisions;
SUCCEED_RETURN(errctx);
}
/* ---------------------------------------------------------------- fixture -- */
/** @brief Reset the recorder and populate all three vtables. */
@@ -325,6 +445,23 @@ static void mock_devices_init(void)
MOCK_INPUT.self = &MOCK;
MOCK_INPUT.poll_key = mock_poll_key;
MOCK_INPUT.flush_keys = mock_flush_keys;
MOCK_SPRITES.self = &MOCK;
MOCK_SPRITES.define = mock_spr_define;
MOCK_SPRITES.define_shape = mock_spr_define_shape;
MOCK_SPRITES.define_file = mock_spr_define_file;
MOCK_SPRITES.show = mock_spr_show;
MOCK_SPRITES.move = mock_spr_move;
MOCK_SPRITES.configure = mock_spr_configure;
MOCK_SPRITES.shared_colors = mock_spr_shared_colors;
MOCK_SPRITES.collisions = mock_spr_collisions;
}
/** @brief Set what the next collisions() call will report. Bit n-1 is sprite n. */
__attribute__((unused))
static void mock_set_collisions(uint16_t mask)
{
MOCK.collisions = mask;
}
/** @brief Prime the input backend with keystrokes GET will drain in order. */

201
tests/read_data.c Normal file
View File

@@ -0,0 +1,201 @@
/**
* @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 <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 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();
}
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_float_items();
return akbasic_test_failures;
}

243
tests/renumber.c Normal file
View File

@@ -0,0 +1,243 @@
/**
* @file renumber.c
* @brief Tests RENUMBER: the lines move, and every reference to one moves with it.
*
* The reason this verb was deferred rather than written early is that the easy
* half is worthless on its own. Moving lines is a permutation; a RENUMBER that
* did only that would silently break every branch in the program. So most of
* what is asserted here is the *rewriting*, including the two things a textual
* rewrite can get wrong: a number inside a string literal, and a target naming a
* line that does not exist.
*/
#include <string.h>
#include <akbasic/error.h>
#include <akbasic/runtime.h>
#include "harness.h"
#include "testutil.h"
/** @brief Load a program without running it. */
static akerr_ErrorContext AKERR_NOIGNORE *load(const char *source)
{
PREPARE_ERROR(errctx);
PASS(errctx, harness_start(NULL));
PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
SUCCEED_RETURN(errctx);
}
/** @brief Run whatever is loaded, bounded so a broken branch fails rather than hangs. */
static akerr_ErrorContext AKERR_NOIGNORE *run_loaded(void)
{
PREPARE_ERROR(errctx);
HARNESS_RUNTIME.environment->nextline = 0;
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 40000));
SUCCEED_RETURN(errctx);
}
/** @brief Lines land on the numbers asked for, in source order. */
static void test_lines_move(void)
{
TEST_REQUIRE_OK(load("5 PRINT \"A\"\n"
"7 PRINT \"B\"\n"
"9 PRINT \"C\"\n"));
TEST_REQUIRE_OK(akbasic_renumber(&HARNESS_RUNTIME, 10, 10, 0));
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "PRINT \"A\"");
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[20].code, "PRINT \"B\"");
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[30].code, "PRINT \"C\"");
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[5].code, "");
harness_stop();
/* A different start and increment. */
TEST_REQUIRE_OK(load("1 PRINT \"A\"\n2 PRINT \"B\"\n"));
TEST_REQUIRE_OK(akbasic_renumber(&HARNESS_RUNTIME, 100, 5, 0));
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[100].code, "PRINT \"A\"");
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[105].code, "PRINT \"B\"");
harness_stop();
}
/** @brief GOTO and GOSUB targets follow the lines they name. */
static void test_branches_rewritten(void)
{
TEST_REQUIRE_OK(load("5 GOTO 9\n"
"7 PRINT \"SKIPPED\"\n"
"9 PRINT \"ARRIVED\"\n"));
TEST_REQUIRE_OK(akbasic_renumber(&HARNESS_RUNTIME, 10, 10, 0));
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "GOTO 30");
TEST_REQUIRE_OK(run_loaded());
TEST_REQUIRE_STR(HARNESS_OUTPUT, "ARRIVED\n");
harness_stop();
TEST_REQUIRE_OK(load("5 GOSUB 9\n"
"6 END\n"
"9 PRINT \"SUB\"\n"
"11 RETURN\n"));
TEST_REQUIRE_OK(akbasic_renumber(&HARNESS_RUNTIME, 10, 10, 0));
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "GOSUB 30");
TEST_REQUIRE_OK(run_loaded());
TEST_REQUIRE_STR(HARNESS_OUTPUT, "SUB\n");
harness_stop();
}
/** @brief A GOTO after THEN or ELSE is rewritten; it is not at a statement start. */
static void test_branch_inside_a_condition(void)
{
TEST_REQUIRE_OK(load("5 IF 1 == 1 THEN GOTO 9 ELSE GOTO 7\n"
"7 PRINT \"ELSE\"\n"
"9 PRINT \"THEN\"\n"));
TEST_REQUIRE_OK(akbasic_renumber(&HARNESS_RUNTIME, 10, 10, 0));
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "IF 1 == 1 THEN GOTO 30 ELSE GOTO 20");
harness_stop();
}
/** @brief ON's whole target list is rewritten, not just the first entry. */
static void test_on_list_rewritten(void)
{
TEST_REQUIRE_OK(load("5 ON 2 GOTO 7, 9\n"
"7 PRINT \"ONE\"\n"
"9 PRINT \"TWO\"\n"));
TEST_REQUIRE_OK(akbasic_renumber(&HARNESS_RUNTIME, 10, 10, 0));
/* The original spacing survives; only the numbers change. */
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "ON 2 GOTO 20, 30");
TEST_REQUIRE_OK(run_loaded());
TEST_REQUIRE_STR(HARNESS_OUTPUT, "TWO\n");
harness_stop();
}
/** @brief RESTORE, TRAP and COLLISION targets are rewritten too. */
static void test_other_verbs_rewritten(void)
{
TEST_REQUIRE_OK(load("5 RESTORE 9\n"
"7 TRAP 9\n"
"8 COLLISION 1, 9\n"
"9 DATA 1\n"));
TEST_REQUIRE_OK(akbasic_renumber(&HARNESS_RUNTIME, 10, 10, 0));
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "RESTORE 40");
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[20].code, "TRAP 40");
/* COLLISION's handler is its *second* argument, so the type is left alone. */
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[30].code, "COLLISION 1, 40");
harness_stop();
}
/**
* @brief A number inside a string literal is not a line reference.
*
* The one thing a textual rewrite is most likely to get wrong, and the reason
* the rewrite has to understand quotes at all.
*/
static void test_string_literals_untouched(void)
{
TEST_REQUIRE_OK(load("5 PRINT \"GOTO 9 IS TEXT\"\n"
"9 PRINT \"END\"\n"));
TEST_REQUIRE_OK(akbasic_renumber(&HARNESS_RUNTIME, 10, 10, 0));
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "PRINT \"GOTO 9 IS TEXT\"");
harness_stop();
}
/**
* @brief A target naming no line is left alone rather than invented.
*
* `GOTO 9999` in a program with no line 9999 is already broken; rewriting it to
* point somewhere would hide that rather than fix it.
*/
static void test_dangling_target_left_alone(void)
{
TEST_REQUIRE_OK(load("5 GOTO 9999\n"
"9 PRINT \"HERE\"\n"));
TEST_REQUIRE_OK(akbasic_renumber(&HARNESS_RUNTIME, 10, 10, 0));
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "GOTO 9999");
harness_stop();
}
/** @brief A label target needs no rewriting, which is the case for using labels. */
static void test_labels_need_no_rewriting(void)
{
TEST_REQUIRE_OK(load("5 GOTO DONE\n"
"7 PRINT \"SKIPPED\"\n"
"9 LABEL DONE\n"
"11 PRINT \"ARRIVED\"\n"));
TEST_REQUIRE_OK(akbasic_renumber(&HARNESS_RUNTIME, 10, 10, 0));
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "GOTO DONE");
TEST_REQUIRE_OK(run_loaded());
TEST_REQUIRE_STR(HARNESS_OUTPUT, "ARRIVED\n");
harness_stop();
}
/** @brief RENUMBER from a line leaves everything before it where it was. */
static void test_partial_renumber(void)
{
TEST_REQUIRE_OK(load("10 PRINT \"KEPT\"\n"
"55 PRINT \"MOVED\"\n"
"57 PRINT \"ALSO MOVED\"\n"));
TEST_REQUIRE_OK(akbasic_renumber(&HARNESS_RUNTIME, 100, 10, 50));
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "PRINT \"KEPT\"");
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[100].code, "PRINT \"MOVED\"");
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[110].code, "PRINT \"ALSO MOVED\"");
harness_stop();
}
/** @brief A renumbering that would land on a kept line is refused, not applied. */
static void test_collision_refused(void)
{
TEST_REQUIRE_OK(load("10 PRINT \"KEPT\"\n"
"55 PRINT \"MOVED\"\n"));
TEST_REQUIRE_STATUS(akbasic_renumber(&HARNESS_RUNTIME, 10, 10, 50), AKBASIC_ERR_VALUE);
/* And nothing moved: the map is built and checked before anything is written. */
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "PRINT \"KEPT\"");
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[55].code, "PRINT \"MOVED\"");
harness_stop();
}
/** @brief A non-positive increment is refused rather than looping forever. */
static void test_bad_arguments(void)
{
TEST_REQUIRE_OK(load("10 PRINT \"A\"\n"));
TEST_REQUIRE_STATUS(akbasic_renumber(&HARNESS_RUNTIME, 10, 0, 0), AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(akbasic_renumber(&HARNESS_RUNTIME, 10, -1, 0), AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(akbasic_renumber(&HARNESS_RUNTIME, 0, 10, 0), AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(akbasic_renumber(NULL, 10, 10, 0), AKERR_NULLPOINTER);
harness_stop();
}
/** @brief The verb reaches all of it, with its defaults. */
static void test_verb(void)
{
akbasic_ASTLeaf *leaf = NULL;
akbasic_Value *out = NULL;
TEST_REQUIRE_OK(load("5 GOTO 9\n7 PRINT \"SKIPPED\"\n9 PRINT \"ARRIVED\"\n"));
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(harness_parse("RENUMBER", &leaf));
TEST_REQUIRE_OK(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "GOTO 30");
harness_stop();
/* And with arguments. */
TEST_REQUIRE_OK(load("5 GOTO 9\n9 PRINT \"X\"\n"));
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(harness_parse("RENUMBER 100, 5", &leaf));
TEST_REQUIRE_OK(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
TEST_REQUIRE_STR(HARNESS_RUNTIME.source[100].code, "GOTO 105");
harness_stop();
}
int main(void)
{
test_lines_move();
test_branches_rewritten();
test_branch_inside_a_condition();
test_on_list_rewritten();
test_other_verbs_rewritten();
test_string_literals_untouched();
test_dangling_target_left_alone();
test_labels_need_no_rewriting();
test_partial_renumber();
test_collision_refused();
test_bad_arguments();
test_verb();
return akbasic_test_failures;
}

525
tests/sprite_verbs.c Normal file
View File

@@ -0,0 +1,525 @@
/**
* @file sprite_verbs.c
* @brief Tests the group H verbs against the recording mock backend.
*
* Same shape as tests/graphics_verbs.c and for the same reason: 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. Running a real
* BASIC line rather than calling the handler directly is deliberate; it
* exercises the dispatch-table row and the parse handler too, which is where an
* added verb is most likely to be wrong. MOVSPR in particular has a parse
* handler all its own.
*/
#include <string.h>
#include <akbasic/error.h>
#include <akbasic/runtime.h>
#include <akbasic/sprite.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, &MOCK_SPRITES));
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 SPRITE sets what it is given and leaves alone what it is not.
*
* The "leaves alone" half is the one worth asserting: BASIC 7.0 lets every
* argument after the number be omitted, so a handler that treated a missing
* argument as zero would silently turn a sprite off on its way to changing the
* colour.
*/
static void test_sprite(void)
{
TEST_REQUIRE_OK(run_program("10 SPRITE 1, 1, 3, 0, 1, 0\n"));
/* Palette index 3 is red: 0x88, 0x39, 0x32 in src/graphics_tables.c. */
TEST_REQUIRE_STR(MOCK.log,
"sprcfg 1 136,57,50 x1 y0 b0\n"
"sprshow 1 1\n");
TEST_REQUIRE(HARNESS_RUNTIME.sprite_state.sprites[0].enabled, "SPRITE 1,1 should enable it");
TEST_REQUIRE_INT(HARNESS_RUNTIME.sprite_state.sprites[0].colorindex, 3);
harness_stop();
/* A second SPRITE naming only the number changes nothing at all. */
TEST_REQUIRE_OK(run_program("10 SPRITE 1, 1, 3, 0, 1, 0\n"
"20 SPRITE 1\n"));
TEST_REQUIRE_INT(HARNESS_RUNTIME.sprite_state.sprites[0].colorindex, 3);
TEST_REQUIRE(HARNESS_RUNTIME.sprite_state.sprites[0].xexpand,
"an omitted argument should leave x-expand alone");
TEST_REQUIRE(HARNESS_RUNTIME.sprite_state.sprites[0].enabled,
"an omitted argument should leave the enable bit alone");
harness_stop();
/* Sprite 0 and sprite 9 do not exist. */
TEST_REQUIRE_OK(run_program("10 SPRITE 9, 1\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 1..8") != NULL,
"sprite 9 should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
TEST_REQUIRE_OK(run_program("10 SPRITE 1, 1, 17\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 1..16") != NULL,
"colour 17 should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/**
* @brief All four MOVSPR forms, which is really a test of its parse handler.
*
* The separators are the whole difference between them and they exist nowhere
* else in the language, so this is the only thing that says `;` and `#` are
* scanned at all.
*/
static void test_movspr(void)
{
/* Absolute. */
TEST_REQUIRE_OK(run_program("10 MOVSPR 1, 100, 50\n"));
TEST_REQUIRE_STR(MOCK.log, "sprmove 1 100.00,50.00\n");
harness_stop();
/* Relative: a signed coordinate is an offset, not a position. */
TEST_REQUIRE_OK(run_program("10 MOVSPR 1, 100, 50\n"
"20 MOVSPR 1, +10, -20\n"));
TEST_REQUIRE_STR(MOCK.log,
"sprmove 1 100.00,50.00\n"
"sprmove 1 110.00,30.00\n");
harness_stop();
/*
* Polar: distance first, then a bearing clockwise from vertical. 90 degrees
* is due right, so the whole distance lands on x and none of it on y --
* which is the assertion that catches a sine and cosine swapped over.
*/
TEST_REQUIRE_OK(run_program("10 MOVSPR 1, 100, 50\n"
"20 MOVSPR 1, 10 ; 90\n"));
TEST_REQUIRE_STR(MOCK.log,
"sprmove 1 100.00,50.00\n"
"sprmove 1 110.00,50.00\n");
harness_stop();
/* And 0 degrees is straight up, which is *minus* y on a screen. */
TEST_REQUIRE_OK(run_program("10 MOVSPR 1, 100, 50\n"
"20 MOVSPR 1, 10 ; 0\n"));
TEST_REQUIRE_STR(MOCK.log,
"sprmove 1 100.00,50.00\n"
"sprmove 1 100.00,40.00\n");
harness_stop();
/*
* Continuous. Nothing moves on the statement itself -- the sprite starts
* moving on the next step, paced off the host's clock -- so what is asserted
* here is that the speed was recorded and the device was *not* told to move.
*/
TEST_REQUIRE_OK(run_program("10 MOVSPR 1, 45 # 8\n"));
TEST_REQUIRE_STR(MOCK.log, "");
TEST_REQUIRE_INT(HARNESS_RUNTIME.sprite_state.sprites[0].speed, 8);
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.sprite_state.sprites[0].angle, 45.0);
harness_stop();
/* Speed 16 does not exist. */
TEST_REQUIRE_OK(run_program("10 MOVSPR 1, 45 # 16\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 0..15") != NULL,
"speed 16 should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/**
* @brief Continuous motion is paced off the host's clock, not off step count.
*
* The service runs on every step, so a program that does nothing still moves --
* and a host that never sets a clock still gets a still picture rather than a
* sprite tearing across the screen at step rate.
*/
static void test_continuous_motion(void)
{
double stopped = 0.0;
TEST_REQUIRE_OK(harness_start(NULL));
mock_devices_init();
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, NULL, NULL,
&MOCK_SPRITES));
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 1000));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
"1 MOVSPR 1, 90 # 10\n"
"2 GOTO 2\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
/* Line 0 and line 1, with the clock standing still. */
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 2));
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.sprite_state.sprites[0].x, 0.0);
/*
* One second later: speed 10 at AKBASIC_SPRITE_SPEED_PIXELS_PER_SECOND
* apiece, due right, so 50 pixels of x and none of y.
*/
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 2000));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 1));
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.sprite_state.sprites[0].x,
10.0 * AKBASIC_SPRITE_SPEED_PIXELS_PER_SECOND);
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.sprite_state.sprites[0].y, 0.0);
/*
* Speed 0 stops it where it is. The step that runs the MOVSPR services the
* motion *before* it runs the line -- the sprite is still moving when that
* step begins -- so one more second's worth lands first; where it stopped is
* whatever it reads after that, and the point is that six more seconds do
* not move it again.
*/
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "1 MOVSPR 1, 90 # 0\n"));
HARNESS_RUNTIME.environment->nextline = 1;
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 3000));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 1));
stopped = HARNESS_RUNTIME.sprite_state.sprites[0].x;
TEST_REQUIRE_INT(HARNESS_RUNTIME.sprite_state.sprites[0].speed, 0);
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 9000));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 1));
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.sprite_state.sprites[0].x, stopped);
harness_stop();
}
/** @brief SPRCOLOR sets the two shared registers, and RSPCOLOR reads them back. */
static void test_sprcolor(void)
{
TEST_REQUIRE_OK(run_program("10 SPRCOLOR 5, 7\n"
"20 PRINT RSPCOLOR(1)\n"
"30 PRINT RSPCOLOR(2)\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "5\n7\n");
harness_stop();
/* One argument leaves the other register alone. */
TEST_REQUIRE_OK(run_program("10 SPRCOLOR 5, 7\n"
"20 SPRCOLOR 9\n"
"30 PRINT RSPCOLOR(1)\n"
"40 PRINT RSPCOLOR(2)\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "9\n7\n");
harness_stop();
}
/**
* @brief The pattern bit order, asserted once here rather than trusted per backend.
*
* Three bytes per row, most significant bit leftmost. Getting it backwards would
* mirror every published sprite horizontally, and nothing else in the suite can
* see it: a SPRSAV test fills the array with 0xff, where a reversed bit order
* produces the identical picture.
*/
static void test_pattern_bit_order(void)
{
uint8_t pattern[AKBASIC_SPRITE_PATTERN_BYTES];
uint8_t pixels[AKBASIC_SPRITE_WIDTH * AKBASIC_SPRITE_HEIGHT];
int i = 0;
memset(pattern, 0, sizeof(pattern));
/* Row 0: the leftmost pixel and nothing else. */
pattern[0] = 0x80;
/* Row 1: the rightmost pixel of all 24, which is the low bit of the third byte. */
pattern[5] = 0x01;
/* Row 2: the ninth pixel across, which is the high bit of the second byte. */
pattern[7] = 0x80;
TEST_REQUIRE_OK(akbasic_sprite_unpack(pattern, sizeof(pattern), pixels));
TEST_REQUIRE_INT(pixels[0], 1);
TEST_REQUIRE_INT(pixels[1], 0);
TEST_REQUIRE_INT(pixels[AKBASIC_SPRITE_WIDTH + (AKBASIC_SPRITE_WIDTH - 1)], 1);
TEST_REQUIRE_INT(pixels[AKBASIC_SPRITE_WIDTH + (AKBASIC_SPRITE_WIDTH - 2)], 0);
TEST_REQUIRE_INT(pixels[(2 * AKBASIC_SPRITE_WIDTH) + 8], 1);
TEST_REQUIRE_INT(pixels[(2 * AKBASIC_SPRITE_WIDTH) + 7], 0);
/* And every other pixel is clear. */
for ( i = 3 * AKBASIC_SPRITE_WIDTH; i < AKBASIC_SPRITE_WIDTH * AKBASIC_SPRITE_HEIGHT; i++ ) {
TEST_REQUIRE_INT(pixels[i], 0);
}
/* A pattern that is not 63 bytes is not a pattern. */
TEST_REQUIRE_STATUS(akbasic_sprite_unpack(pattern, 8, pixels), AKBASIC_ERR_BOUNDS);
TEST_REQUIRE_STATUS(akbasic_sprite_unpack(NULL, sizeof(pattern), pixels), AKERR_NULLPOINTER);
}
/**
* @brief SPRSAV takes a pattern from a DIMmed integer array.
*
* The array form is ours rather than BASIC 7.0's, because a value in this
* interpreter carries a NUL-terminated string and a C128 puts 63 raw bytes in
* one -- a string that cannot hold a zero byte cannot hold a sprite. See
* TODO.md section 5.
*/
static void test_sprsav_from_array(void)
{
TEST_REQUIRE_OK(run_program("10 DIM P#(63)\n"
"20 FOR I# = 0 TO 62\n"
"30 P#(I#) = 255\n"
"40 NEXT I#\n"
"50 P#(62) = 129\n"
"60 SPRSAV P#, 1\n"));
TEST_REQUIRE(strstr(MOCK.log, "sprdef 1 63 bytes ff..81") != NULL,
"the whole pattern should have reached the device, got \"%s\"", MOCK.log);
TEST_REQUIRE(HARNESS_RUNTIME.sprite_state.sprites[0].defined,
"SPRSAV should have marked the sprite defined");
harness_stop();
/* An array too small to be a pattern is a program that has miscounted. */
TEST_REQUIRE_OK(run_program("10 DIM P#(8)\n"
"20 SPRSAV P#, 1\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "sprite pattern is 63") != NULL,
"a short array should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/** @brief SPRSAV takes a region SSHAPE saved, told apart by its handle prefix. */
static void test_sprsav_from_shape(void)
{
TEST_REQUIRE_OK(run_program("10 SSHAPE A$, 0, 0, 24, 21\n"
"20 SPRSAV A$, 2\n"));
TEST_REQUIRE(strstr(MOCK.log, "sprshape 2 from 0") != NULL,
"the saved region should have reached the sprite device, got \"%s\"",
MOCK.log);
harness_stop();
}
/**
* @brief SPRSAV takes an image file path, and the program's directory goes with it.
*
* Any string that is not an SSHAPE handle is a path. What can be asserted from a
* mock is the dispatch and the root -- whether a relative path actually resolves
* is libakgl's, and is asserted against real pixels in tests/akgl_backends.c.
*/
static void test_sprsav_from_file(void)
{
TEST_REQUIRE_OK(harness_start(NULL));
mock_devices_init();
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS, NULL, NULL,
&MOCK_SPRITES));
TEST_REQUIRE_OK(akbasic_runtime_set_source_path(&HARNESS_RUNTIME, "/games/asteroids/main.bas"));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 SPRSAV \"ship.png\", 3\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,
"sprfile 3 ship.png root=/games/asteroids\n"
"sprcfg 3 255,255,255 x0 y0 b0\n");
harness_stop();
/* No source path -- a REPL session, or a host that handed over a string. */
TEST_REQUIRE_OK(run_program("10 SPRSAV \"ship.png\", 3\n"));
TEST_REQUIRE(strstr(MOCK.log, "root=(none)") != NULL,
"with no program file the backend should be told so, got \"%s\"", MOCK.log);
harness_stop();
}
/** @brief Writing a sprite back out is a disk operation, and is refused by name. */
static void test_sprsav_refuses_to_write(void)
{
TEST_REQUIRE_OK(run_program("10 SPRSAV 1, \"ship.png\"\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "disk operation") != NULL,
"SPRSAV out to a file should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/** @brief COLLISION arms the interrupt, and refuses the two types nothing implements. */
static void test_collision_arming(void)
{
TEST_REQUIRE_OK(run_program("10 COLLISION 1, 100\n"));
TEST_REQUIRE(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].armed,
"COLLISION 1 should have armed the sprite interrupt");
TEST_REQUIRE_INT(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].line, 100);
harness_stop();
/* A label is stored by name, not resolved here: the handler may be anywhere. */
TEST_REQUIRE_OK(run_program("10 COLLISION 1, BUMPED\n"));
TEST_REQUIRE_STR(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].label, "BUMPED");
harness_stop();
/* Omitting the target disarms. */
TEST_REQUIRE_OK(run_program("10 COLLISION 1, 100\n"
"20 COLLISION 1\n"));
TEST_REQUIRE(!HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].armed,
"COLLISION with no target should disarm");
harness_stop();
TEST_REQUIRE_OK(run_program("10 COLLISION 2, 100\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "read back every frame") != NULL,
"COLLISION 2 should be refused with a reason, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
TEST_REQUIRE_OK(run_program("10 COLLISION 3, 100\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "light pen") != NULL,
"COLLISION 3 should be refused with a reason, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
TEST_REQUIRE_OK(run_program("10 COLLISION 4, 100\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 1..3") != NULL,
"COLLISION 4 should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/**
* @brief A collision the device reports enters the handler and shows up in BUMP.
*
* The whole path: the mock says two sprites overlap, the service ORs that into
* the accumulator and raises the interrupt, the step loop enters the handler,
* and BUMP() reports it and clears.
*/
static void test_collision_fires(void)
{
TEST_REQUIRE_OK(harness_start(NULL));
mock_devices_init();
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, NULL, NULL,
&MOCK_SPRITES));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
"1 COLLISION 1, 4\n"
"2 PRINT \"MAIN\"\n"
"3 GOTO 2\n"
"4 PRINT \"HIT\"\n"
"5 PRINT BUMP(1)\n"
"6 RETURN\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
/* Line 0 and the COLLISION, with nothing overlapping yet. */
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 2));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "");
/* Sprites 1 and 3 meet. */
mock_set_collisions(0x05);
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 2));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "HIT") != NULL,
"the handler should have been entered, got \"%s\"", HARNESS_OUTPUT);
/* BUMP reports the mask, and clears it. */
mock_set_collisions(0);
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 1));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "5\n") != NULL,
"BUMP should have reported sprites 1 and 3, got \"%s\"", HARNESS_OUTPUT);
TEST_REQUIRE_INT(HARNESS_RUNTIME.sprite_state.bumped, 0);
harness_stop();
}
/** @brief The readbacks answer from interpreter state, so they need no device. */
static void test_readbacks(void)
{
TEST_REQUIRE_OK(harness_start(NULL));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
"10 MOVSPR 1, 120, 90\n"
"20 PRINT RSPPOS(1, 0)\n"
"30 PRINT RSPPOS(1, 1)\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
/*
* MOVSPR refuses for want of a device *after* it has moved the sprite, so
* the program stops on line 10 -- but the position took effect, which is
* what makes the state readable at all. Read it directly rather than through
* PRINT, since the program never got that far.
*/
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "MOVSPR needs a sprite device") != NULL,
"MOVSPR without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.sprite_state.sprites[0].x, 120.0);
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.sprite_state.sprites[0].y, 90.0);
harness_stop();
/* With a device attached the same program prints. */
TEST_REQUIRE_OK(run_program("10 MOVSPR 1, 120, 90\n"
"20 PRINT RSPPOS(1, 0)\n"
"30 PRINT RSPPOS(1, 1)\n"
"40 PRINT RSPPOS(1, 2)\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "120\n90\n0\n");
harness_stop();
TEST_REQUIRE_OK(run_program("10 SPRITE 1, 1, 6, 1, 1, 0\n"
"20 PRINT RSPRITE(1, 0)\n"
"30 PRINT RSPRITE(1, 1)\n"
"40 PRINT RSPRITE(1, 2)\n"
"50 PRINT RSPRITE(1, 3)\n"
"60 PRINT RSPRITE(1, 4)\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n6\n1\n1\n0\n");
harness_stop();
/* Out-of-range fields are refused rather than answered with a zero. */
TEST_REQUIRE_OK(run_program("10 PRINT RSPPOS(1, 9)\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 0..2") != NULL,
"RSPPOS field 9 should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
TEST_REQUIRE_OK(run_program("10 PRINT RSPCOLOR(3)\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "is 1 or 2") != NULL,
"RSPCOLOR register 3 should be refused, got \"%s\"", HARNESS_OUTPUT);
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. SPRCOLOR
* and the readbacks are excluded on purpose: they only touch interpreter state
* and must keep working before a host has lent the script anything.
*/
static void test_no_device(void)
{
TEST_REQUIRE_OK(harness_start(NULL));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 SPRSAV \"ship.png\", 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, "SPRSAV needs a sprite device") != NULL,
"SPRSAV without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
TEST_REQUIRE_OK(harness_start(NULL));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
"10 SPRCOLOR 5, 7\n"
"20 PRINT RSPCOLOR(1)\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, "5\n");
harness_stop();
}
/** @brief NEW takes the sprites down with the program that defined them. */
static void test_new_clears_sprites(void)
{
akbasic_ASTLeaf *leaf = NULL;
akbasic_Value *out = NULL;
TEST_REQUIRE_OK(run_program("10 SPRITE 1, 1, 3\n"));
TEST_REQUIRE(HARNESS_RUNTIME.sprite_state.sprites[0].enabled, "SPRITE should have enabled it");
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(harness_parse("NEW", &leaf));
TEST_REQUIRE_OK(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
TEST_REQUIRE(!HARNESS_RUNTIME.sprite_state.sprites[0].enabled,
"NEW should have taken the sprite state down");
TEST_REQUIRE(strstr(MOCK.log, "sprshow 1 0") != NULL,
"NEW should have hidden the sprites on the device, got \"%s\"", MOCK.log);
harness_stop();
}
int main(void)
{
test_sprite();
test_movspr();
test_continuous_motion();
test_sprcolor();
test_pattern_bit_order();
test_sprsav_from_array();
test_sprsav_from_shape();
test_sprsav_from_file();
test_sprsav_refuses_to_write();
test_collision_arming();
test_collision_fires();
test_readbacks();
test_no_device();
test_new_clears_sprites();
return akbasic_test_failures;
}

291
tests/structure_verbs.c Normal file
View File

@@ -0,0 +1,291 @@
/**
* @file structure_verbs.c
* @brief Tests the group A verbs: DO, LOOP, BEGIN, BEND, ON and END.
*
* All of them are built on `waitingForCommand` -- a scope records the verb it is
* skipping forward to and nothing runs until that verb turns up -- so most of
* what is asserted here is *what did not run*. A block that should be skipped
* printing nothing is the whole point, and it is invisible unless the test says
* so explicitly.
*/
#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));
/*
* Bounded. A defect in any of these verbs is a loop that never ends, and a
* suite that hangs tells you far less than one that fails.
*/
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 2000));
SUCCEED_RETURN(errctx);
}
/** @brief A condition on the top runs the body while it holds, and not at all when it does not. */
static void test_do_while(void)
{
TEST_REQUIRE_OK(run_program("10 I# = 0\n"
"20 DO WHILE I# < 3\n"
"30 PRINT I#\n"
"40 I# = I# + 1\n"
"50 LOOP\n"
"60 PRINT \"DONE\"\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\n1\n2\nDONE\n");
harness_stop();
/* Already false: the body must not run once "just to see". */
TEST_REQUIRE_OK(run_program("10 I# = 5\n"
"20 DO WHILE I# < 3\n"
"30 PRINT \"NEVER\"\n"
"40 LOOP\n"
"50 PRINT \"AFTER\"\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "AFTER\n");
harness_stop();
}
/** @brief A condition on the bottom runs the body at least once. */
static void test_loop_until(void)
{
TEST_REQUIRE_OK(run_program("10 I# = 0\n"
"20 DO\n"
"30 PRINT I#\n"
"40 I# = I# + 1\n"
"50 LOOP UNTIL I# = 3\n"
"60 PRINT \"DONE\"\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\n1\n2\nDONE\n");
harness_stop();
/*
* Already true at the bottom, so exactly one pass. This is the difference
* between a bottom-tested loop and a top-tested one, and it is why BASIC has
* both forms.
*/
TEST_REQUIRE_OK(run_program("10 I# = 99\n"
"20 DO\n"
"30 PRINT \"ONCE\"\n"
"40 LOOP UNTIL I# = 99\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "ONCE\n");
harness_stop();
}
/** @brief DO WHILE re-tests its own condition at the bottom, or it never ends. */
static void test_do_while_retests(void)
{
TEST_REQUIRE_OK(run_program("10 I# = 0\n"
"20 DO WHILE I# < 2\n"
"30 I# = I# + 1\n"
"40 LOOP\n"
"50 PRINT I#\n"));
/*
* That it printed at all is the assertion: a DO WHILE that failed to re-test
* its condition at the bottom never reaches line 50, and the bounded run
* above is what turns that into a failure rather than a hang.
*/
TEST_REQUIRE_STR(HARNESS_OUTPUT, "2\n");
harness_stop();
}
/** @brief EXIT leaves a DO loop as well as a FOR loop. */
static void test_exit_from_do(void)
{
TEST_REQUIRE_OK(run_program("10 I# = 0\n"
"20 DO\n"
"30 I# = I# + 1\n"
"40 IF I# = 2 THEN EXIT\n"
"50 LOOP\n"
"60 PRINT \"EXITED AT \" + I#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "EXITED AT 2\n");
harness_stop();
/* And EXIT outside any loop is refused rather than doing something quiet. */
TEST_REQUIRE_OK(run_program("10 EXIT\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside the context of FOR or DO") != NULL,
"a bare EXIT should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/** @brief An infinite DO...LOOP is left by GOTO, and the step bound proves it ran. */
static void test_bare_do_loop(void)
{
TEST_REQUIRE_OK(run_program("10 I# = 0\n"
"20 DO\n"
"30 I# = I# + 1\n"
"40 IF I# = 3 THEN GOTO 60\n"
"50 LOOP\n"
"60 PRINT I#\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "3\n");
harness_stop();
}
/** @brief ON picks the nth target, one-based, and falls through when there is none. */
static void test_on_goto(void)
{
TEST_REQUIRE_OK(run_program("10 X# = 2\n"
"20 ON X# GOTO 100, 200\n"
"30 PRINT \"FELL THROUGH\"\n"
"40 END\n"
"100 PRINT \"ONE\"\n"
"110 END\n"
"200 PRINT \"TWO\"\n"
"210 END\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "TWO\n");
harness_stop();
/*
* Out of range is not an error. BASIC 7.0 falls through to the next
* statement, which is what lets `ON X GOTO ...` be written without a bounds
* check in front of it. Zero and negative fall through the same way.
*/
TEST_REQUIRE_OK(run_program("10 X# = 9\n"
"20 ON X# GOTO 100, 200\n"
"30 PRINT \"FELL THROUGH\"\n"
"40 END\n"
"100 PRINT \"ONE\"\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "FELL THROUGH\n");
harness_stop();
TEST_REQUIRE_OK(run_program("10 X# = 0\n"
"20 ON X# GOTO 100\n"
"30 PRINT \"FELL THROUGH\"\n"
"40 END\n"
"100 PRINT \"ONE\"\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "FELL THROUGH\n");
harness_stop();
}
/** @brief ON ... GOSUB returns to the line after the ON. */
static void test_on_gosub(void)
{
TEST_REQUIRE_OK(run_program("10 X# = 1\n"
"20 ON X# GOSUB 100, 200\n"
"30 PRINT \"BACK\"\n"
"40 END\n"
"100 PRINT \"SUB ONE\"\n"
"110 RETURN\n"
"200 PRINT \"SUB TWO\"\n"
"210 RETURN\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "SUB ONE\nBACK\n");
harness_stop();
}
/** @brief A label is a legal ON target, since it evaluates to a line number. */
static void test_on_label(void)
{
TEST_REQUIRE_OK(run_program("10 X# = 1\n"
"20 ON X# GOTO TARGET\n"
"30 PRINT \"FELL THROUGH\"\n"
"40 END\n"
"100 LABEL TARGET\n"
"110 PRINT \"AT LABEL\"\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "AT LABEL\n");
harness_stop();
}
/** @brief BEGIN/BEND makes an IF span lines, and the arm not taken skips all of them. */
static void test_begin_bend(void)
{
TEST_REQUIRE_OK(run_program("10 A# = 1\n"
"20 IF A# = 1 THEN BEGIN\n"
"30 PRINT \"IN BLOCK\"\n"
"40 PRINT \"STILL IN\"\n"
"50 BEND\n"
"60 PRINT \"AFTER\"\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "IN BLOCK\nSTILL IN\nAFTER\n");
harness_stop();
/*
* The skipped case is the one that needs the wait. `skiprestofline` only
* reaches the end of the IF's own line; without arming a skip to BEND the
* block's lines would run as ordinary top-level lines.
*/
TEST_REQUIRE_OK(run_program("10 A# = 0\n"
"20 IF A# = 1 THEN BEGIN\n"
"30 PRINT \"NOT SEEN\"\n"
"40 PRINT \"ALSO NOT SEEN\"\n"
"50 BEND\n"
"60 PRINT \"AFTER\"\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "AFTER\n");
harness_stop();
}
/** @brief END stops the program without quitting a REPL session. */
static void test_end(void)
{
TEST_REQUIRE_OK(run_program("10 PRINT \"BEFORE\"\n"
"20 END\n"
"30 PRINT \"AFTER\"\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "BEFORE\n");
TEST_REQUIRE_INT(HARNESS_RUNTIME.mode, AKBASIC_MODE_QUIT);
harness_stop();
/*
* Started from a REPL, END goes back to the prompt rather than ending the
* interpreter -- which is the whole difference between END and QUIT.
*/
TEST_REQUIRE_OK(harness_start(NULL));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PRINT \"HI\"\n20 END\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_REPL));
HARNESS_RUNTIME.mode = AKBASIC_MODE_RUN;
HARNESS_RUNTIME.environment->nextline = 0;
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 40));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "HI") != NULL,
"the program should have run, got \"%s\"", HARNESS_OUTPUT);
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "READY") != NULL,
"END from a REPL session should return to the prompt, got \"%s\"",
HARNESS_OUTPUT);
harness_stop();
}
/** @brief Nested DO loops each keep their own condition and unwind in order. */
static void test_nested_do(void)
{
TEST_REQUIRE_OK(run_program("10 I# = 0\n"
"20 DO WHILE I# < 2\n"
"30 J# = 0\n"
"40 DO WHILE J# < 2\n"
"50 PRINT I# * 10 + J#\n"
"60 J# = J# + 1\n"
"70 LOOP\n"
"80 I# = I# + 1\n"
"90 LOOP\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\n1\n10\n11\n");
harness_stop();
}
/** @brief LOOP without a DO is refused rather than silently doing nothing. */
static void test_loop_without_do(void)
{
TEST_REQUIRE_OK(run_program("10 LOOP\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside the context of DO") != NULL,
"a bare LOOP should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
int main(void)
{
test_do_while();
test_loop_until();
test_do_while_retests();
test_exit_from_do();
test_bare_do_loop();
test_on_goto();
test_on_gosub();
test_on_label();
test_begin_bend();
test_end();
test_nested_do();
test_loop_without_do();
return akbasic_test_failures;
}

195
tests/trap_verbs.c Normal file
View File

@@ -0,0 +1,195 @@
/**
* @file trap_verbs.c
* @brief Tests the group C verbs: TRAP, RESUME and ERR().
*
* The interesting half of error trapping is what *stops* happening: an armed
* TRAP suppresses the "? line : CLASS message" report and stops the run from
* ending, and neither is visible unless a test says the output is empty of one
* and the program kept going.
*
* `ER` and `EL` are `ER#` and `EL#` here -- ordinary globals, because this
* dialect has no bare variable names. See TODO.md section 5.
*/
#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));
/* Bounded: a bare RESUME that never fixes anything is an infinite retry. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 4000));
SUCCEED_RETURN(errctx);
}
/** @brief An armed TRAP swallows the report and enters the handler instead. */
static void test_trap_catches(void)
{
TEST_REQUIRE_OK(run_program("10 TRAP 100\n"
"20 DIM Q#(2)\n"
"30 PRINT Q#(9)\n"
"40 PRINT \"AFTER\"\n"
"50 END\n"
"100 PRINT \"TRAPPED \" + ER# + \" ON LINE \" + EL#\n"
"110 RESUME NEXT\n"));
/*
* AKBASIC_ERR_BOUNDS is 515: base 512 plus SYNTAX, TYPE, UNDEFINED. Spelled
* out rather than referenced so that a renumbering of the error band shows up
* here as a failure rather than as a silently different program.
*/
TEST_REQUIRE_STR(HARNESS_OUTPUT, "TRAPPED 515 ON LINE 30\nAFTER\n");
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "RUNTIME ERROR") == NULL,
"a trapped error must not also be reported, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/** @brief With no TRAP the error is reported and the program stops, as before. */
static void test_untrapped_still_reports(void)
{
TEST_REQUIRE_OK(run_program("10 DIM Q#(2)\n"
"20 PRINT Q#(9)\n"
"30 PRINT \"UNREACHED\"\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "RUNTIME ERROR") != NULL,
"an untrapped error should be reported, got \"%s\"", HARNESS_OUTPUT);
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "UNREACHED") == NULL,
"an untrapped error should stop the program, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/** @brief Bare TRAP disarms, and the next error is reported normally again. */
static void test_trap_disarms(void)
{
TEST_REQUIRE_OK(run_program("10 TRAP 100\n"
"20 TRAP\n"
"30 DIM Q#(2)\n"
"40 PRINT Q#(9)\n"
"50 PRINT \"UNREACHED\"\n"
"100 PRINT \"NOT SEEN\"\n"
"110 RESUME NEXT\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "RUNTIME ERROR") != NULL,
"a disarmed TRAP should let the error through, got \"%s\"", HARNESS_OUTPUT);
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "NOT SEEN") == NULL,
"the handler should not have run, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/** @brief A handler may be named by label, which is where a handler usually sits. */
static void test_trap_by_label(void)
{
TEST_REQUIRE_OK(run_program("10 TRAP HANDLER\n"
"20 DIM Q#(2)\n"
"30 PRINT Q#(9)\n"
"40 PRINT \"AFTER\"\n"
"50 END\n"
"100 LABEL HANDLER\n"
"110 PRINT \"CAUGHT\"\n"
"120 RESUME NEXT\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "CAUGHT\nAFTER\n");
harness_stop();
}
/** @brief RESUME's three forms land on three different lines. */
static void test_resume_forms(void)
{
/* RESUME <line> goes where it is told. */
TEST_REQUIRE_OK(run_program("10 TRAP 100\n"
"20 DIM Q#(2)\n"
"30 PRINT Q#(9)\n"
"40 PRINT \"NOT HERE\"\n"
"50 PRINT \"HERE\"\n"
"60 END\n"
"100 RESUME 50\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "HERE\n");
harness_stop();
/*
* Bare RESUME retries the failing line. The handler fixes the array first,
* so the retry succeeds -- which is the only way a bare RESUME terminates,
* and the reason the verb exists.
*/
TEST_REQUIRE_OK(run_program("10 TRAP 100\n"
"20 DIM Q#(2)\n"
"30 PRINT Q#(N#)\n"
"40 PRINT \"AFTER\"\n"
"50 END\n"
"100 N# = 1\n"
"110 RESUME\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\nAFTER\n");
harness_stop();
}
/** @brief RESUME outside a handler is refused rather than popping something else. */
static void test_resume_outside_handler(void)
{
TEST_REQUIRE_OK(run_program("10 RESUME NEXT\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside the context of a TRAP handler") != NULL,
"a bare RESUME should be refused, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/**
* @brief An error inside a handler is reported rather than re-entering it.
*
* Otherwise a broken handler traps its own failure forever and the program can
* never be stopped. A C128 behaves the same way.
*/
static void test_error_inside_handler(void)
{
TEST_REQUIRE_OK(run_program("10 TRAP 100\n"
"20 DIM Q#(2)\n"
"30 PRINT Q#(9)\n"
"40 END\n"
"100 PRINT \"IN HANDLER\"\n"
"110 PRINT Q#(9)\n"
"120 RESUME NEXT\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "IN HANDLER") != NULL,
"the handler should have run once, got \"%s\"", HARNESS_OUTPUT);
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "RUNTIME ERROR") != NULL,
"an error inside the handler should be reported, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
}
/** @brief ERR() names a status code, using the registry every code is named in. */
static void test_err_function(void)
{
TEST_REQUIRE_OK(run_program("10 TRAP 100\n"
"20 DIM Q#(2)\n"
"30 PRINT Q#(9)\n"
"40 END\n"
"100 PRINT ERR(ER#)\n"
"110 RESUME NEXT\n"));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "Out Of Bounds") != NULL,
"ERR should name the status, got \"%s\"", HARNESS_OUTPUT);
harness_stop();
/*
* A code nobody reserved reads back as libakerror's own "Unknown Error",
* which is the same string a stack trace would print for it.
*/
TEST_REQUIRE_OK(run_program("10 PRINT ERR(30000)\n"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "Unknown Error\n");
harness_stop();
}
int main(void)
{
test_trap_catches();
test_untrapped_still_reports();
test_trap_disarms();
test_trap_by_label();
test_resume_forms();
test_resume_outside_handler();
test_error_inside_handler();
test_err_function();
return akbasic_test_failures;
}

View File

@@ -191,17 +191,22 @@ int main(void)
TEST_REQUIRE_STR(out->stringval, "ababab");
/*
* mathPlus mutates self in place when self is mutable, where every other
* operator always clones. CommandNEXT's loop increment depends on it --
* TODO.md section 12 item 4. Pinning the asymmetry so a "cleanup" fails here
* rather than silently breaking every FOR loop.
* math_plus clones like every other operator, mutable operand or not.
*
* This used to assert the opposite. The reference mutates `self` in place
* when it is mutable, which made `A# + 1` modify `A#` whenever the left
* operand came out of a variable -- TODO.md section 6 item 4. It was left
* alone because NEXT's loop increment relied on the mutation to advance the
* counter; NEXT writes the result back itself now, so the asymmetry is gone
* and this asserts that it stays gone.
*/
set_int(&A, 10);
A.mutable_ = true;
set_int(&B, 5);
TEST_REQUIRE_OK(akbasic_value_math_plus(&A, &B, &SCRATCH, &out));
TEST_REQUIRE(out == &A, "math_plus on a mutable value must return self, not the scratch");
TEST_REQUIRE_INT(A.intval, 15);
TEST_REQUIRE(out == &SCRATCH, "math_plus must use the scratch even for a mutable operand");
TEST_REQUIRE_INT(A.intval, 10);
TEST_REQUIRE_INT(SCRATCH.intval, 15);
set_int(&A, 10);
A.mutable_ = false;
@@ -209,6 +214,34 @@ int main(void)
TEST_REQUIRE(out == &SCRATCH, "math_plus on an immutable value must use the scratch");
TEST_REQUIRE_INT(A.intval, 10);
/*
* The unused numeric field is ignored, not added in.
*
* The reference reads a right-hand operand as `intval + int64(floatval)`,
* which gives the right answer only for as long as whichever field is unused
* happens to be zero -- TODO.md section 6 item 5, filed as a landmine
* because no program can reach it. It is reachable from here: a value is a
* plain struct and nothing stops one carrying both. Without this the fix is
* untested, and the old code passes every other test in the tree.
*/
set_int(&A, 2);
set_int(&B, 5);
B.floatval = 3.0; /* stale, from whatever B held before */
TEST_REQUIRE_OK(akbasic_value_math_plus(&A, &B, &SCRATCH, &out));
TEST_REQUIRE_INT(out->intval, 7);
set_float(&A, 2.0);
set_float(&B, 5.0);
B.intval = 3; /* stale the other way round */
TEST_REQUIRE_OK(akbasic_value_math_plus(&A, &B, &SCRATCH, &out));
TEST_REQUIRE_FEQ(out->floatval, 7.0);
/* And a truth value used as a number is -1, which is what AND/OR rely on. */
set_int(&A, 0);
TEST_REQUIRE_OK(akbasic_value_set_bool(&B, true));
TEST_REQUIRE_OK(akbasic_value_math_plus(&A, &B, &SCRATCH, &out));
TEST_REQUIRE_INT(out->intval, -1);
/* Type errors. */
set_string(&A, "text");
set_int(&B, 1);

View File

@@ -64,9 +64,10 @@ int main(void)
}
/*
* The four rows with no exec handler are consumed by another verb's parse
* path and are never evaluated on their own. Any other missing handler is a
* verb that would report "Unknown command" at runtime.
* These rows have no exec handler because another verb's parse path consumes
* them; they are never evaluated on their own. `WHILE` and `UNTIL` belong to
* DO and LOOP the way `TO` and `STEP` belong to FOR. Any other missing
* handler is a verb that would report "Unknown command" at runtime.
*/
for ( i = 0; i < count; i++ ) {
if ( table[i].exec != NULL ) {
@@ -78,6 +79,9 @@ int main(void)
strcmp(table[i].name, "OR") == 0 ||
strcmp(table[i].name, "REM") == 0 ||
strcmp(table[i].name, "STEP") == 0 ||
strcmp(table[i].name, "UNTIL") == 0 ||
strcmp(table[i].name, "USING") == 0 ||
strcmp(table[i].name, "WHILE") == 0 ||
strcmp(table[i].name, "THEN") == 0 ||
strcmp(table[i].name, "TO") == 0,
"verb \"%s\" has no exec handler and is not a known passive token",