/** * @file draw.c * @brief Unit tests for the immediate-mode drawing primitives. * * Everything here draws into a small software renderer under the dummy video * driver and then reads the target back with SDL_RenderReadPixels, so the * assertions are about pixels that actually changed rather than about SDL * having been called. No window is shown and no display is required. * * The target is deliberately tiny: at 64x64 a full readback is 16 KB, which * makes it cheap to read the whole thing back after every operation. */ #include #include #include #include #include #include #include #include "testutil.h" /** @brief Width and height of the offscreen target every test draws into. */ #define TEST_TARGET_SIZE 64 /** @brief Opaque black, what each test clears the target to. */ static const SDL_Color testblack = { 0x00, 0x00, 0x00, 0xff }; /** @brief The color most tests draw with. */ static const SDL_Color testred = { 0xff, 0x00, 0x00, 0xff }; /** @brief A second color, for tests that need to tell two marks apart. */ static const SDL_Color testgreen = { 0x00, 0xff, 0x00, 0xff }; /** @brief Clear the whole target to opaque black. */ static akerr_ErrorContext *clear_target(void) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN( errctx, SDL_SetRenderDrawColor(akgl_renderer->sdl_renderer, 0x00, 0x00, 0x00, 0xff), AKGL_ERR_SDL, "%s", SDL_GetError()); FAIL_ZERO_RETURN( errctx, SDL_RenderClear(akgl_renderer->sdl_renderer), AKGL_ERR_SDL, "%s", SDL_GetError()); SUCCEED_RETURN(errctx); } /** * @brief Report whether one pixel of @p shot carries @p color. * * Alpha is not compared: the render target's own alpha depends on the format * SDL picked for it, and none of these tests draw translucently. */ static bool pixel_is(SDL_Surface *shot, int x, int y, SDL_Color color) { uint8_t r = 0; uint8_t g = 0; uint8_t b = 0; uint8_t a = 0; if ( shot == NULL ) { return false; } if ( !SDL_ReadSurfacePixel(shot, x, y, &r, &g, &b, &a) ) { return false; } return ((r == color.r) && (g == color.g) && (b == color.b)); } akerr_ErrorContext *test_draw_point(void) { PREPARE_ERROR(errctx); SDL_Surface *shot = NULL; ATTEMPT { CATCH(errctx, clear_target()); TEST_EXPECT_OK(errctx, akgl_draw_point(akgl_renderer, 10.0f, 20.0f, testred), "plotting one pixel"); shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL); FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError()); TEST_ASSERT(errctx, pixel_is(shot, 10, 20, testred), "the plotted pixel at 10,20 is not the color it was drawn with"); TEST_ASSERT(errctx, pixel_is(shot, 11, 20, testblack), "plotting one pixel also changed its neighbour at 11,20"); TEST_ASSERT(errctx, pixel_is(shot, 10, 21, testblack), "plotting one pixel also changed the pixel below it"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_point(NULL, 0.0f, 0.0f, testred), "plotting through a NULL backend"); } CLEANUP { if ( shot != NULL ) { SDL_DestroySurface(shot); } } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *test_draw_line(void) { PREPARE_ERROR(errctx); SDL_Surface *shot = NULL; bool onthe_line = true; int i = 0; ATTEMPT { CATCH(errctx, clear_target()); // A vertical line, so every pixel of it is known without reasoning // about how SDL rasterises a diagonal. TEST_EXPECT_OK(errctx, akgl_draw_line(akgl_renderer, 5.0f, 4.0f, 5.0f, 12.0f, testred), "drawing a vertical line"); shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL); FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError()); for ( i = 4; i <= 12; i++ ) { TEST_ASSERT_FLAG(onthe_line, pixel_is(shot, 5, i, testred)); } TEST_ASSERT(errctx, onthe_line == true, "the vertical line from 5,4 to 5,12 has a gap in it"); TEST_ASSERT(errctx, pixel_is(shot, 5, 3, testblack), "the line ran past its first endpoint"); TEST_ASSERT(errctx, pixel_is(shot, 5, 13, testblack), "the line ran past its second endpoint"); TEST_ASSERT(errctx, pixel_is(shot, 6, 8, testblack), "the line is wider than one pixel"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_line(NULL, 0.0f, 0.0f, 1.0f, 1.0f, testred), "drawing a line through a NULL backend"); } CLEANUP { if ( shot != NULL ) { SDL_DestroySurface(shot); } } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *test_draw_rects(void) { PREPARE_ERROR(errctx); SDL_Surface *shot = NULL; SDL_FRect box; ATTEMPT { box.x = 8.0f; box.y = 8.0f; box.w = 16.0f; box.h = 16.0f; // The outline touches the border and leaves the middle alone. CATCH(errctx, clear_target()); TEST_EXPECT_OK(errctx, akgl_draw_rect(akgl_renderer, &box, testred), "outlining a rectangle"); shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL); FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError()); TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred), "the outline is missing its top left corner"); TEST_ASSERT(errctx, pixel_is(shot, 23, 23, testred), "the outline is missing its bottom right corner"); TEST_ASSERT(errctx, pixel_is(shot, 16, 8, testred), "the outline is missing its top edge"); TEST_ASSERT(errctx, pixel_is(shot, 16, 16, testblack), "the outline filled its interior"); SDL_DestroySurface(shot); shot = NULL; // The filled form covers the interior as well. CATCH(errctx, clear_target()); TEST_EXPECT_OK(errctx, akgl_draw_filled_rect(akgl_renderer, &box, testred), "filling a rectangle"); shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL); FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError()); TEST_ASSERT(errctx, pixel_is(shot, 16, 16, testred), "the fill left its interior empty"); TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred), "the fill missed its top left corner"); TEST_ASSERT(errctx, pixel_is(shot, 24, 24, testblack), "the fill ran one pixel past its bottom right corner"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_rect(NULL, &box, testred), "outlining through a NULL backend"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_rect(akgl_renderer, NULL, testred), "outlining a NULL rectangle"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_filled_rect(NULL, &box, testred), "filling through a NULL backend"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_filled_rect(akgl_renderer, NULL, testred), "filling a NULL rectangle"); } CLEANUP { if ( shot != NULL ) { SDL_DestroySurface(shot); } } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *test_draw_circle(void) { PREPARE_ERROR(errctx); SDL_Surface *shot = NULL; bool symmetric = true; int x = 0; int y = 0; ATTEMPT { CATCH(errctx, clear_target()); TEST_EXPECT_OK(errctx, akgl_draw_circle(akgl_renderer, 32.0f, 32.0f, 10.0f, testred), "drawing a circle"); shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL); FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError()); // The four axis points are exact for any correct midpoint circle. TEST_ASSERT(errctx, pixel_is(shot, 42, 32, testred), "the circle is missing its rightmost pixel"); TEST_ASSERT(errctx, pixel_is(shot, 22, 32, testred), "the circle is missing its leftmost pixel"); TEST_ASSERT(errctx, pixel_is(shot, 32, 42, testred), "the circle is missing its bottom pixel"); TEST_ASSERT(errctx, pixel_is(shot, 32, 22, testred), "the circle is missing its top pixel"); // It is an outline, not a disc. TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testblack), "the circle filled its center"); // ...and nothing lands outside the radius. TEST_ASSERT(errctx, pixel_is(shot, 43, 32, testred) == false, "the circle drew a pixel one past its radius"); // Every plotted pixel has a mirror in the other three quadrants. The // circle is drawn one octant at a time and reflected seven ways, so a // sign error in any single reflection breaks this and nothing else -- // the four axis points above stay put either way. for ( y = 22; y <= 42; y++ ) { for ( x = 22; x <= 42; x++ ) { if ( !pixel_is(shot, x, y, testred) ) { continue; } TEST_ASSERT_FLAG(symmetric, pixel_is(shot, 64 - x, y, testred)); TEST_ASSERT_FLAG(symmetric, pixel_is(shot, x, 64 - y, testred)); TEST_ASSERT_FLAG(symmetric, pixel_is(shot, 64 - x, 64 - y, testred)); } } TEST_ASSERT(errctx, symmetric == true, "the circle is not symmetric about its center; an octant is reflected wrong"); SDL_DestroySurface(shot); shot = NULL; // A zero radius is the degenerate case, not an error: one pixel. CATCH(errctx, clear_target()); TEST_EXPECT_OK(errctx, akgl_draw_circle(akgl_renderer, 5.0f, 5.0f, 0.0f, testred), "drawing a circle of radius zero"); shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL); FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError()); TEST_ASSERT(errctx, pixel_is(shot, 5, 5, testred), "a circle of radius zero did not plot its center"); TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_draw_circle(akgl_renderer, 5.0f, 5.0f, -1.0f, testred), "drawing a circle of negative radius"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_circle(NULL, 5.0f, 5.0f, 4.0f, testred), "drawing a circle through a NULL backend"); } CLEANUP { if ( shot != NULL ) { SDL_DestroySurface(shot); } } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *test_draw_flood_fill(void) { PREPARE_ERROR(errctx); SDL_Surface *shot = NULL; SDL_FRect box; ATTEMPT { // A red box outline on black. Filling inside it must stay inside it, // which is the whole contract of PAINT. box.x = 10.0f; box.y = 10.0f; box.w = 20.0f; box.h = 20.0f; CATCH(errctx, clear_target()); CATCH(errctx, akgl_draw_rect(akgl_renderer, &box, testred)); TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(akgl_renderer, 20, 20, testgreen), "flooding the inside of a box"); shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL); FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError()); TEST_ASSERT(errctx, pixel_is(shot, 20, 20, testgreen), "the seed pixel was not filled"); TEST_ASSERT(errctx, pixel_is(shot, 11, 11, testgreen), "the fill did not reach the top left of the interior"); TEST_ASSERT(errctx, pixel_is(shot, 28, 28, testgreen), "the fill did not reach the bottom right of the interior"); TEST_ASSERT(errctx, pixel_is(shot, 10, 10, testred), "the fill overwrote the boundary"); TEST_ASSERT(errctx, pixel_is(shot, 20, 10, testred), "the fill overwrote the top edge"); TEST_ASSERT(errctx, pixel_is(shot, 20, 5, testblack), "the fill leaked outside the box"); TEST_ASSERT(errctx, pixel_is(shot, 40, 40, testblack), "the fill leaked into the rest of the target"); SDL_DestroySurface(shot); shot = NULL; // Filling a region that is already the requested color changes nothing // and is not an error. TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(akgl_renderer, 20, 20, testgreen), "flooding a region that is already that color"); shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL); FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError()); TEST_ASSERT(errctx, pixel_is(shot, 20, 20, testgreen), "refilling a region disturbed it"); TEST_ASSERT(errctx, pixel_is(shot, 10, 10, testred), "refilling a region disturbed its boundary"); SDL_DestroySurface(shot); shot = NULL; // Flooding the outside reaches every pixel that is not the box. TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(akgl_renderer, 0, 0, testgreen), "flooding the area around a box"); shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL); FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError()); TEST_ASSERT(errctx, pixel_is(shot, 0, 0, testgreen), "the seed pixel was not filled"); TEST_ASSERT(errctx, pixel_is(shot, TEST_TARGET_SIZE - 1, TEST_TARGET_SIZE - 1, testgreen), "the fill did not reach the far corner of the target"); TEST_ASSERT(errctx, pixel_is(shot, 10, 10, testred), "the fill from outside overwrote the boundary"); TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_draw_flood_fill(akgl_renderer, -1, 0, testred), "flooding from a seed left of the target"); TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_draw_flood_fill(akgl_renderer, 0, TEST_TARGET_SIZE, testred), "flooding from a seed below the target"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_flood_fill(NULL, 0, 0, testred), "flooding through a NULL backend"); } CLEANUP { if ( shot != NULL ) { SDL_DestroySurface(shot); } } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *test_draw_copy_and_paste_region(void) { PREPARE_ERROR(errctx); SDL_Surface *shot = NULL; SDL_Surface *saved = NULL; SDL_Surface *reused = NULL; SDL_Rect region; SDL_FRect box; ATTEMPT { // Put something recognisable in the top left corner and save it. box.x = 0.0f; box.y = 0.0f; box.w = 8.0f; box.h = 8.0f; CATCH(errctx, clear_target()); CATCH(errctx, akgl_draw_filled_rect(akgl_renderer, &box, testred)); region.x = 0; region.y = 0; region.w = 8; region.h = 8; TEST_EXPECT_OK(errctx, akgl_draw_copy_region(akgl_renderer, ®ion, &saved), "saving a region of the target"); TEST_ASSERT(errctx, saved != NULL, "akgl_draw_copy_region did not allocate a surface"); TEST_ASSERT(errctx, saved->w == 8 && saved->h == 8, "the saved surface is %dx%d, expected 8x8", saved->w, saved->h); // Wipe the screen and put it back somewhere else. CATCH(errctx, clear_target()); TEST_EXPECT_OK(errctx, akgl_draw_paste_region(akgl_renderer, saved, 32.0f, 32.0f), "pasting a saved region"); shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL); FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError()); TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testred), "the pasted region did not land at its destination"); TEST_ASSERT(errctx, pixel_is(shot, 39, 39, testred), "the pasted region is smaller than what was saved"); TEST_ASSERT(errctx, pixel_is(shot, 40, 40, testblack), "the pasted region is larger than what was saved"); TEST_ASSERT(errctx, pixel_is(shot, 0, 0, testblack), "pasting also redrew the region at its original position"); SDL_DestroySurface(shot); shot = NULL; // A surface the caller already owns is reused rather than replaced, so // saving the same region repeatedly does not churn allocations. reused = SDL_CreateSurface(8, 8, SDL_PIXELFORMAT_RGBA32); FAIL_ZERO_BREAK(errctx, reused, AKGL_ERR_SDL, "%s", SDL_GetError()); region.x = 32; region.y = 32; TEST_EXPECT_OK(errctx, akgl_draw_copy_region(akgl_renderer, ®ion, &reused), "saving into a caller-owned surface"); TEST_ASSERT(errctx, pixel_is(reused, 0, 0, testred), "the caller-owned surface did not receive the region"); // Wrong-sized destinations and regions off the edge of the target are // refused rather than silently clipped. region.w = 4; TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_draw_copy_region(akgl_renderer, ®ion, &reused), "saving into a destination of the wrong size"); region.x = TEST_TARGET_SIZE - 4; region.y = 0; region.w = 8; region.h = 8; TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_draw_copy_region(akgl_renderer, ®ion, &reused), "saving a region that runs off the right edge"); region.x = 0; region.w = 0; TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_draw_copy_region(akgl_renderer, ®ion, &reused), "saving a region with no area"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_copy_region(NULL, ®ion, &reused), "saving through a NULL backend"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_copy_region(akgl_renderer, NULL, &reused), "saving a NULL region"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_copy_region(akgl_renderer, ®ion, NULL), "saving into a NULL destination"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_paste_region(NULL, saved, 0.0f, 0.0f), "pasting through a NULL backend"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_paste_region(akgl_renderer, NULL, 0.0f, 0.0f), "pasting a NULL surface"); } CLEANUP { if ( shot != NULL ) { SDL_DestroySurface(shot); } if ( saved != NULL ) { SDL_DestroySurface(saved); } if ( reused != NULL ) { SDL_DestroySurface(reused); } } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *test_draw_preserves_render_draw_color(void) { PREPARE_ERROR(errctx); SDL_FRect box; uint8_t r = 0; uint8_t g = 0; uint8_t b = 0; uint8_t a = 0; ATTEMPT { box.x = 0.0f; box.y = 0.0f; box.w = 4.0f; box.h = 4.0f; // Drawing must not leave the renderer's color set to whatever it drew // with, or the host's next SDL_RenderClear() paints the wrong color. FAIL_ZERO_BREAK( errctx, SDL_SetRenderDrawColor(akgl_renderer->sdl_renderer, 0x11, 0x22, 0x33, 0x44), AKGL_ERR_SDL, "%s", SDL_GetError()); CATCH(errctx, akgl_draw_point(akgl_renderer, 1.0f, 1.0f, testred)); CATCH(errctx, akgl_draw_line(akgl_renderer, 0.0f, 0.0f, 3.0f, 3.0f, testred)); CATCH(errctx, akgl_draw_rect(akgl_renderer, &box, testred)); CATCH(errctx, akgl_draw_filled_rect(akgl_renderer, &box, testred)); CATCH(errctx, akgl_draw_circle(akgl_renderer, 20.0f, 20.0f, 4.0f, testred)); FAIL_ZERO_BREAK( errctx, SDL_GetRenderDrawColor(akgl_renderer->sdl_renderer, &r, &g, &b, &a), AKGL_ERR_SDL, "%s", SDL_GetError()); TEST_ASSERT(errctx, (r == 0x11) && (g == 0x22) && (b == 0x33) && (a == 0x44), "drawing left the render draw color at %02x%02x%02x%02x, expected 11223344", r, g, b, a); } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *test_draw_backend_without_a_renderer(void) { PREPARE_ERROR(errctx); akgl_RenderBackend empty; SDL_Surface *saved = NULL; SDL_Surface *scratch = NULL; SDL_FRect box; SDL_Rect region; ATTEMPT { // A backend that exists but was never given an SDL_Renderer. Every // entry point has to say so rather than dereference it -- this is the // state a host is in between allocating a backend and initializing it. memset(&empty, 0x00, sizeof(akgl_RenderBackend)); box.x = 0.0f; box.y = 0.0f; box.w = 4.0f; box.h = 4.0f; region.x = 0; region.y = 0; region.w = 4; region.h = 4; TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_point(&empty, 0.0f, 0.0f, testred), "plotting through an uninitialized backend"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_line(&empty, 0.0f, 0.0f, 1.0f, 1.0f, testred), "drawing a line through an uninitialized backend"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_rect(&empty, &box, testred), "outlining through an uninitialized backend"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_filled_rect(&empty, &box, testred), "filling through an uninitialized backend"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_circle(&empty, 4.0f, 4.0f, 2.0f, testred), "drawing a circle through an uninitialized backend"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_flood_fill(&empty, 0, 0, testred), "flooding through an uninitialized backend"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_copy_region(&empty, ®ion, &saved), "saving through an uninitialized backend"); TEST_ASSERT(errctx, saved == NULL, "a refused save still wrote something to the destination"); scratch = SDL_CreateSurface(4, 4, SDL_PIXELFORMAT_RGBA32); FAIL_ZERO_BREAK(errctx, scratch, AKGL_ERR_SDL, "%s", SDL_GetError()); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_paste_region(&empty, scratch, 0.0f, 0.0f), "pasting through an uninitialized backend"); } CLEANUP { if ( saved != NULL ) { SDL_DestroySurface(saved); } if ( scratch != NULL ) { SDL_DestroySurface(scratch); } } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } /** * @brief akgl_draw_background paints its checkerboard and restores the draw colour. * * Until 0.5.0 this function returned `void`, drew through the global renderer * with no check on it, and left the draw colour changed. TODO.md listed it * under "needs the offscreen renderer harness" purely because of that global; * taking a backend is what makes it testable here alongside everything else in * this file. */ akerr_ErrorContext *test_draw_background(void) { PREPARE_ERROR(errctx); SDL_Surface *shot = NULL; SDL_Color after; SDL_Color light = { 0x99, 0x99, 0x99, 0xff }; SDL_Color dark = { 0x66, 0x66, 0x66, 0xff }; ATTEMPT { CATCH(errctx, clear_target()); // A deliberately odd colour, so "restored" cannot be confused with // "happened to already be black". FAIL_ZERO_BREAK( errctx, SDL_SetRenderDrawColor(akgl_renderer->sdl_renderer, 0x12, 0x34, 0x56, 0x78), AKGL_ERR_SDL, "%s", SDL_GetError()); TEST_EXPECT_OK(errctx, akgl_draw_background(akgl_renderer, TEST_TARGET_SIZE, TEST_TARGET_SIZE), "painting the transparency checkerboard"); shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL); FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError()); // The pattern is 8x8 cells alternating on ((x ^ y) >> 3) & 1, so (0,0) // and (8,8) share a colour and (8,0) is the other one. TEST_ASSERT(errctx, pixel_is(shot, 0, 0, dark), "cell (0,0) is not the first checker colour"); TEST_ASSERT(errctx, pixel_is(shot, 7, 7, dark), "cell (0,0) is not filled to its edge"); TEST_ASSERT(errctx, pixel_is(shot, 8, 0, light), "cell (1,0) is not the second checker colour"); TEST_ASSERT(errctx, pixel_is(shot, 0, 8, light), "cell (0,1) is not the second checker colour"); TEST_ASSERT(errctx, pixel_is(shot, 8, 8, dark), "cell (1,1) is not the first checker colour"); FAIL_ZERO_BREAK( errctx, SDL_GetRenderDrawColor(akgl_renderer->sdl_renderer, &after.r, &after.g, &after.b, &after.a), AKGL_ERR_SDL, "%s", SDL_GetError()); TEST_ASSERT(errctx, (after.r == 0x12) && (after.g == 0x34) && (after.b == 0x56) && (after.a == 0x78), "the draw colour was left at %02x%02x%02x%02x rather than restored", after.r, after.g, after.b, after.a); // Degenerate sizes paint nothing and are not an error. CATCH(errctx, clear_target()); TEST_EXPECT_OK(errctx, akgl_draw_background(akgl_renderer, 0, 0), "painting a zero-sized background"); TEST_EXPECT_OK(errctx, akgl_draw_background(akgl_renderer, -8, -8), "painting a negative-sized background"); SDL_DestroySurface(shot); shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL); FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError()); TEST_ASSERT(errctx, pixel_is(shot, 0, 0, testblack), "a zero or negative sized background painted something"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_background(NULL, 8, 8), "painting a background through a NULL backend"); } CLEANUP { if ( shot != NULL ) { SDL_DestroySurface(shot); } } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } int main(void) { PREPARE_ERROR(errctx); SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy"); SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software"); ATTEMPT { CATCH(errctx, akgl_error_init()); akgl_renderer = &akgl_default_renderer; FAIL_ZERO_BREAK( errctx, SDL_Init(SDL_INIT_VIDEO), AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError()); FAIL_ZERO_BREAK( errctx, SDL_CreateWindowAndRenderer( "net/aklabs/libakgl/test_draw", TEST_TARGET_SIZE, TEST_TARGET_SIZE, 0, &akgl_window, &akgl_renderer->sdl_renderer), AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError()); CATCH(errctx, test_draw_point()); CATCH(errctx, test_draw_line()); CATCH(errctx, test_draw_rects()); CATCH(errctx, test_draw_circle()); CATCH(errctx, test_draw_flood_fill()); CATCH(errctx, test_draw_copy_and_paste_region()); CATCH(errctx, test_draw_preserves_render_draw_color()); CATCH(errctx, test_draw_backend_without_a_renderer()); CATCH(errctx, test_draw_background()); } CLEANUP { SDL_Quit(); } PROCESS(errctx) { } FINISH_NORETURN(errctx); }