/** * @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 #include #include #include #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; }