/** * @file graphics_verbs.c * @brief Tests the group G verbs against the recording mock backend. * * These verbs emit nothing a golden file can compare, so the assertions are on * the call log: what reached the device, in what order, with what geometry and * what color. Running a real BASIC line rather than calling the handler directly * is deliberate -- it exercises the dispatch-table row and the parse handler as * well, which is where an added verb is most likely to be wrong. */ #include #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)); 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 white of palette index 2, which most of these draw with. */ #define WHITE "#ffffff" /** @brief The red of palette index 3. */ #define RED "#883932" /** * @brief A single DRAW coordinate pair plots a point; two or more draw a * polyline; none at all plots wherever LOCATE left the cursor. */ static void test_draw(void) { TEST_REQUIRE_OK(run_program("10 DRAW 1, 10, 20\n")); TEST_REQUIRE_STR(MOCK.log, "point 10.0,20.0 " WHITE "\n"); harness_stop(); TEST_REQUIRE_OK(run_program("10 DRAW 1, 0, 0 TO 10, 0 TO 10, 10\n")); TEST_REQUIRE_STR(MOCK.log, "line 0.0,0.0-10.0,0.0 " WHITE "\n" "line 10.0,0.0-10.0,10.0 " WHITE "\n"); harness_stop(); /* LOCATE moves the pixel cursor; a bare DRAW plots it. */ TEST_REQUIRE_OK(run_program("10 LOCATE 33, 44\n20 DRAW 1\n")); TEST_REQUIRE_STR(MOCK.log, "point 33.0,44.0 " WHITE "\n"); harness_stop(); /* An odd number of coordinates is a typo, not half a line. */ TEST_REQUIRE_OK(harness_start(NULL)); mock_devices_init(); TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS, NULL, NULL)); TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 DRAW 1, 5\n")); TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0)); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "X,Y pairs") != NULL, "an odd coordinate count should be refused, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } /** @brief COLOR rebinds a source, and every drawing verb follows the rebinding. */ static void test_color(void) { TEST_REQUIRE_OK(run_program("10 COLOR 1, 3\n20 DRAW 1, 1, 1\n")); TEST_REQUIRE_STR(MOCK.log, "point 1.0,1.0 " RED "\n"); harness_stop(); /* Both arguments are range-checked, and BASIC counts colors from 1. */ TEST_REQUIRE_OK(run_program("10 COLOR 9, 3\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "COLOR source 9") != NULL, "an out-of-range source should be refused, got \"%s\"", HARNESS_OUTPUT); harness_stop(); TEST_REQUIRE_OK(run_program("10 COLOR 1, 0\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "COLOR 0") != NULL, "color 0 should be refused, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } /** @brief An unrotated BOX is a rectangle; a rotated one is four lines. */ static void test_box(void) { TEST_REQUIRE_OK(run_program("10 BOX 1, 0, 0, 10, 10\n")); TEST_REQUIRE_STR(MOCK.log, "rect 0.0,0.0-10.0,10.0 " WHITE "\n"); harness_stop(); /* * Rotated 90 degrees about its own center, a square lands back on itself -- * same four edges, walked from a different starting corner. That is the * assertion worth making, because it pins the rotation *origin*: rotating * about the coordinate origin instead of the box's center would put the * square somewhere else entirely, and rotating about a corner would tilt it. * * The -0.0 is real and harmless: cos(pi/2) is not exactly zero, so one * coordinate lands on negative zero, which compares equal to zero everywhere * it matters and only shows up because the mock prints it. */ TEST_REQUIRE_OK(run_program("10 BOX 1, 0, 0, 10, 10, 90\n")); TEST_REQUIRE_STR(MOCK.log, "line 10.0,-0.0-10.0,10.0 " WHITE "\n" "line 10.0,10.0-0.0,10.0 " WHITE "\n" "line 0.0,10.0-0.0,0.0 " WHITE "\n" "line 0.0,0.0-10.0,-0.0 " WHITE "\n"); harness_stop(); } /** @brief CIRCLE is a polygon of arc-increment segments, and closes. */ static void test_circle(void) { int segments = 0; const char *cursor = NULL; /* 360 degrees at 90 per segment is four lines, whatever the radius. */ TEST_REQUIRE_OK(run_program("10 CIRCLE 1, 50, 50, 10, 10, 0, 360, 0, 90\n")); for ( cursor = MOCK.log; (cursor = strstr(cursor, "line ")) != NULL; cursor += 5 ) { segments += 1; } TEST_REQUIRE_INT(segments, 4); /* The first vertex is directly above the center, as BASIC 7.0 starts it. */ TEST_REQUIRE(strncmp(MOCK.log, "line 50.0,40.0-", 15) == 0, "CIRCLE should start at the top of the circle, got \"%.30s\"", MOCK.log); harness_stop(); /* A negative radius and a zero increment are both refusals, not loops. */ TEST_REQUIRE_OK(run_program("10 CIRCLE 1, 50, 50, -5\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "radius") != NULL, "a negative radius should be refused, got \"%s\"", HARNESS_OUTPUT); harness_stop(); TEST_REQUIRE_OK(run_program("10 CIRCLE 1, 50, 50, 10, 10, 0, 360, 0, 0\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "increment") != NULL, "a zero arc increment should be refused, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } /** * @brief PAINT reports a partial fill rather than letting it pass as success. * * The device gives up when its span stack runs out, having filled some of the * region. A program that cannot tell that happened has no way to recover, so the * error is the feature. */ static void test_paint(void) { TEST_REQUIRE_OK(run_program("10 PAINT 1, 5, 5\n")); TEST_REQUIRE_STR(MOCK.log, "paint 5,5 " WHITE "\n"); harness_stop(); TEST_REQUIRE_OK(harness_start(NULL)); mock_devices_init(); MOCK.paint_exhausts = true; TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS, NULL, NULL)); TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PAINT 1, 5, 5\n")); TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0)); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "only part of the region") != NULL, "a partial fill should be reported, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } /** @brief SCALE maps user coordinates onto the 320x200 space, and only then. */ static void test_scale(void) { /* Off by default: a coordinate passes through untouched. */ TEST_REQUIRE_OK(run_program("10 DRAW 1, 160, 100\n")); TEST_REQUIRE_STR(MOCK.log, "point 160.0,100.0 " WHITE "\n"); harness_stop(); /* On, with the 7.0 default maxima: the middle of the user space is the middle of the screen. */ TEST_REQUIRE_OK(run_program("10 SCALE 1\n20 DRAW 1, 1023, 1023\n")); TEST_REQUIRE_STR(MOCK.log, "point 320.0,200.0 " WHITE "\n"); harness_stop(); TEST_REQUIRE_OK(run_program("10 SCALE 1, 640, 400\n20 DRAW 1, 320, 200\n")); TEST_REQUIRE_STR(MOCK.log, "point 160.0,100.0 " WHITE "\n"); harness_stop(); TEST_REQUIRE_OK(run_program("10 SCALE 1, 0, 400\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "positive") != NULL, "a zero SCALE maximum should be refused, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } /** @brief GRAPHIC records the mode, clears on request and refuses a bad mode. */ static void test_graphic(void) { TEST_REQUIRE_OK(run_program("10 GRAPHIC 1, 1\n")); TEST_REQUIRE_STR(MOCK.log, "clear #000000\n"); TEST_REQUIRE_INT(HARNESS_RUNTIME.gfx.mode, 1); harness_stop(); /* Without the clear argument nothing reaches the device. */ TEST_REQUIRE_OK(run_program("10 GRAPHIC 1\n")); TEST_REQUIRE_STR(MOCK.log, ""); harness_stop(); /* GRAPHIC CLR drops the saved shapes and resets the whole state. */ TEST_REQUIRE_OK(run_program("10 COLOR 1, 3\n20 GRAPHIC 5\n")); TEST_REQUIRE_STR(MOCK.log, "freeshapes\n"); TEST_REQUIRE_INT(HARNESS_RUNTIME.gfx.source[1], 2); harness_stop(); TEST_REQUIRE_OK(run_program("10 GRAPHIC 9\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "GRAPHIC mode 9") != NULL, "an out-of-range mode should be refused, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } /** @brief SSHAPE stores a handle a later GSHAPE can use, and only that. */ static void test_shapes(void) { TEST_REQUIRE_OK(run_program("10 SSHAPE A$, 0, 0, 8, 8\n20 GSHAPE A$, 32, 32\n")); TEST_REQUIRE_STR(MOCK.log, "save 0,0-8,8 -> 0\npaste 0 at 32.0,32.0\n"); harness_stop(); /* * The string is opaque but not magic: a string that did not come from SSHAPE * has to be refused, or GSHAPE parses whatever digits it finds and pastes * some unrelated slot. */ TEST_REQUIRE_OK(run_program("10 A$ = \"5\"\n20 GSHAPE A$, 0, 0\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "did not come from SSHAPE") != NULL, "a hand-written shape string should be refused, got \"%s\"", HARNESS_OUTPUT); harness_stop(); /* A numeric variable is the wrong type for either verb. */ TEST_REQUIRE_OK(run_program("10 SSHAPE A#, 0, 0, 8, 8\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "string variable") != NULL, "SSHAPE into a number should be refused, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } /** * @brief Every verb that needs a device says so by name when it has none. * * This is the standalone driver's situation, so it is the common path. COLOR, * LOCATE and SCALE are excluded on purpose: they only touch interpreter state * and must keep working before a host has lent the script a renderer. */ static void test_no_device(void) { TEST_REQUIRE_OK(harness_start(NULL)); TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 COLOR 1, 3\n" "20 LOCATE 5, 5\n" "30 SCALE 1\n" "40 DRAW 1, 1, 1\n")); TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0)); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "DRAW needs a graphics device") != NULL, "DRAW without a device should name itself, got \"%s\"", HARNESS_OUTPUT); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "COLOR") == NULL, "COLOR should not need a device, got \"%s\"", HARNESS_OUTPUT); TEST_REQUIRE_INT(HARNESS_RUNTIME.gfx.source[1], 3); TEST_REQUIRE_FEQ(HARNESS_RUNTIME.gfx.x, 5.0); TEST_REQUIRE(HARNESS_RUNTIME.gfx.scaling, "SCALE should not need a device"); harness_stop(); } int main(void) { test_draw(); test_color(); test_box(); test_circle(); test_paint(); test_scale(); test_graphic(); test_shapes(); test_no_device(); return akbasic_test_failures; }