#include #include #include #include #include #include #include #include "testutil.h" /** * @brief How many entries of AKERR_ARRAY_ERROR are currently held by somebody. * * The error contexts are a fixed pool exactly like the object pools: a context * is in use while its reference count is non-zero, and a function that finishes * without releasing one has leaked a slot out of AKERR_MAX_ARRAY_ERROR. */ static int live_error_contexts(void) { int live = 0; int i = 0; for ( i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) { if ( AKERR_ARRAY_ERROR[i].refcount != 0 ) { live += 1; } } return live; } akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void) { akgl_RectanglePoints points; // Zeroed for the same reason as the fixtures in // test_akgl_collide_point_rectangle_nullpointers: the last case here is a // real call, and feeding it stack garbage is noise under `memcheck`. SDL_FRect testrect = {.x = 0, .y = 0, .w = 0, .h = 0}; PREPARE_ERROR(errctx); memset((void *)&points, 0x00, sizeof(akgl_RectanglePoints)); ATTEMPT { CATCH(errctx, akgl_rectangle_points(NULL, NULL)); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with all NULL pointers"); } CLEANUP { } PROCESS(errctx) { } HANDLE(errctx, AKERR_NULLPOINTER) { // noop } FINISH(errctx, true); ATTEMPT { CATCH(errctx, akgl_rectangle_points(NULL, &testrect)); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL SDL_FRect pointer"); } CLEANUP { } PROCESS(errctx) { } HANDLE(errctx, AKERR_NULLPOINTER) { // noop } FINISH(errctx, true); ATTEMPT { CATCH(errctx, akgl_rectangle_points(&points, NULL)); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL akgl_RectanglePoints pointer"); } CLEANUP { } PROCESS(errctx) { } HANDLE(errctx, AKERR_NULLPOINTER) { // noop } FINISH(errctx, true); ATTEMPT { CATCH(errctx, akgl_rectangle_points(&points, &testrect)); } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *test_akgl_rectangle_points_math(void) { akgl_RectanglePoints points; SDL_FRect testrect = {.x = 0, .y = 0, .w = 32, .h = 32}; memset((void *)&points, 0x00, sizeof(akgl_RectanglePoints)); PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, akgl_rectangle_points(&points, &testrect)); if ( points.topleft.x != 0 || points.topleft.y != 0 || points.topright.x != 32 || points.topright.y != 0 || points.bottomleft.x != 0 || points.bottomleft.y != 32 || points.bottomright.x != 32 || points.bottomright.y != 32 ) { FAIL_BREAK( errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points incorrectly calculated points for {x=0, y=0, w=32, h=32} to {topleft={%d, %d}, topright={%d, %d}, bottomleft={%d, %d}, bottomright={%d, %d}}", points.topleft.x, points.topleft.y, points.topright.x, points.topright.y, points.bottomleft.x, points.bottomleft.y, points.bottomright.x, points.bottomright.y ); } } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *test_akgl_collide_point_rectangle_nullpointers(void) { // Zeroed rather than left as whatever the stack held. The last case in this // function is a real call with real arguments, and reading uninitialised // floats out of it is sixteen findings under `memcheck` for a test that is // not about coordinates at all. akgl_Point testpoint = { .x = 0, .y = 0 }; akgl_RectanglePoints testrectpoints; bool testcollide = false; PREPARE_ERROR(errctx); memset(&testrectpoints, 0x00, sizeof(akgl_RectanglePoints)); ATTEMPT { CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, NULL)); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(*, *, NULL) failed"); } CLEANUP { } PROCESS(errctx) { } HANDLE(errctx, AKERR_NULLPOINTER) { // noop } FINISH(errctx, true); ATTEMPT { CATCH(errctx, akgl_collide_point_rectangle(&testpoint, NULL, &testcollide)); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(*, NULL, *) failed"); } CLEANUP { } PROCESS(errctx) { } HANDLE(errctx, AKERR_NULLPOINTER) { // noop } FINISH(errctx, true); ATTEMPT { CATCH(errctx, akgl_collide_point_rectangle(NULL, &testrectpoints, &testcollide)); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(NULL, *, *) failed"); } CLEANUP { } PROCESS(errctx) { } HANDLE(errctx, AKERR_NULLPOINTER) { // noop } FINISH(errctx, true); ATTEMPT { CATCH(errctx, akgl_collide_point_rectangle(NULL, NULL, NULL)); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(NULL, NULL, NULL) failed"); } CLEANUP { } PROCESS(errctx) { } HANDLE(errctx, AKERR_NULLPOINTER) { // noop } FINISH(errctx, true); ATTEMPT { CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide)); } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *test_akgl_collide_point_rectangle_logic(void) { akgl_Point testpoint = {.x = 16, .y = 16}; SDL_FRect testrect = { .x = 0, .y = 0, .w = 32, .h = 32}; akgl_RectanglePoints testrectpoints; bool testcollide = false; PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, akgl_rectangle_points(&testrectpoints, &testrect)); CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide)); if ( testcollide == false ) { FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed"); } testpoint.x = 48; testpoint.y = 48; CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide)); if ( testcollide == true ) { FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Invalid collision reported"); } } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void) { SDL_FRect testrect1 = {.x = 0, .y = 0, .w = 0, .h = 0}; SDL_FRect testrect2 = {.x = 0, .y = 0, .w = 0, .h = 0}; bool testcollide = false; PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, NULL)); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(*, *, NULL) failed"); } CLEANUP { } PROCESS(errctx) { } HANDLE(errctx, AKERR_NULLPOINTER) { // noop } FINISH(errctx, true); ATTEMPT { CATCH(errctx, akgl_collide_rectangles(&testrect1, NULL, &testcollide)); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(*, NULL, *) failed"); } CLEANUP { } PROCESS(errctx) { } HANDLE(errctx, AKERR_NULLPOINTER) { // noop } FINISH(errctx, true); ATTEMPT { CATCH(errctx, akgl_collide_rectangles(NULL, &testrect2, &testcollide)); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(NULL, *, *) failed"); } CLEANUP { } PROCESS(errctx) { } HANDLE(errctx, AKERR_NULLPOINTER) { // noop } FINISH(errctx, true); ATTEMPT { CATCH(errctx, akgl_collide_rectangles(NULL, NULL, NULL)); FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(NULL, NULL, NULL) failed"); } CLEANUP { } PROCESS(errctx) { } HANDLE(errctx, AKERR_NULLPOINTER) { // noop } FINISH(errctx, true); ATTEMPT { CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide)); } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } /** * @brief The arrangements corner containment cannot see, and the ones it must keep. * * Eight corner-in-rectangle tests answer "do these overlap" correctly only when * one rectangle encloses a corner of the other. A tall thin rectangle crossing a * short wide one overlaps in a plus sign with no corner inside either, and eight * corner tests all report false. That was documented on akgl_collide_rectangles * as a known limitation rather than fixed, and there was no test for it. * * The three cases below the cross are the ones a rewrite can break while fixing * it: touching edges must keep counting as a collision, because * akgl_collide_point_rectangle is inclusive on all four edges and both headers * promise it; full containment must keep working; and a separation of less than * one pixel must be seen, which the old implementation could not do because it * routed through akgl_Point and truncated float to int. */ akerr_ErrorContext *test_akgl_collide_rectangles_arrangements(void) { SDL_FRect tall = { .x = 10.0f, .y = 0.0f, .w = 4.0f, .h = 40.0f }; SDL_FRect wide = { .x = 0.0f, .y = 10.0f, .w = 40.0f, .h = 4.0f }; SDL_FRect outer = { .x = 0.0f, .y = 0.0f, .w = 64.0f, .h = 64.0f }; SDL_FRect inner = { .x = 16.0f, .y = 16.0f, .w = 8.0f, .h = 8.0f }; SDL_FRect left = { .x = 0.0f, .y = 0.0f, .w = 10.0f, .h = 10.0f }; SDL_FRect right = { .x = 10.0f, .y = 0.0f, .w = 10.0f, .h = 10.0f }; SDL_FRect near1 = { .x = 0.0f, .y = 0.0f, .w = 10.9f, .h = 10.0f }; SDL_FRect near2 = { .x = 10.5f, .y = 0.0f, .w = 10.0f, .h = 10.0f }; bool collide = false; PREPARE_ERROR(errctx); ATTEMPT { // The cross. Neither rectangle encloses a corner of the other, and they // plainly overlap in the middle. CATCH(errctx, akgl_collide_rectangles(&tall, &wide, &collide)); TEST_ASSERT(errctx, (collide == true), "a tall rectangle crossing a wide one was reported as not colliding"); // The same pair the other way round. The test is symmetric and has to be. CATCH(errctx, akgl_collide_rectangles(&wide, &tall, &collide)); TEST_ASSERT(errctx, (collide == true), "the crossing pair was reported as not colliding with the arguments swapped"); // Full containment, both orders. CATCH(errctx, akgl_collide_rectangles(&outer, &inner, &collide)); TEST_ASSERT(errctx, (collide == true), "a contained rectangle was missed"); CATCH(errctx, akgl_collide_rectangles(&inner, &outer, &collide)); TEST_ASSERT(errctx, (collide == true), "a containing rectangle was missed"); // A shared edge and nothing more. akgl_collide_point_rectangle is // inclusive on all four edges, so touching counts; util.h and chapter 18 // both say so, and a span test written with < rather than <= silently // changes that. CATCH(errctx, akgl_collide_rectangles(&left, &right, &collide)); TEST_ASSERT(errctx, (collide == true), "two rectangles sharing exactly one edge were reported as not colliding"); // Overlapping by four tenths of a pixel. The corner tests truncated // float to int on the way in, so this read as a shared edge at 10. CATCH(errctx, akgl_collide_rectangles(&near1, &near2, &collide)); TEST_ASSERT(errctx, (collide == true), "a sub-pixel overlap was missed"); // Separated by four tenths of a pixel. Truncation read this as touching. near2.x = 11.3f; CATCH(errctx, akgl_collide_rectangles(&near1, &near2, &collide)); TEST_ASSERT(errctx, (collide == false), "a sub-pixel gap was reported as a collision"); // Disjoint on one axis only, which is the case a span test gets wrong // when it forgets to check both. tall.x = 100.0f; CATCH(errctx, akgl_collide_rectangles(&tall, &wide, &collide)); TEST_ASSERT(errctx, (collide == false), "rectangles separated on x were reported as colliding"); tall.x = 10.0f; tall.y = 100.0f; CATCH(errctx, akgl_collide_rectangles(&tall, &wide, &collide)); TEST_ASSERT(errctx, (collide == false), "rectangles separated on y were reported as colliding"); } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *test_akgl_collide_rectangles_logic(void) { SDL_FRect testrect1 = { .x = 0, .y = 0, .w = 32, .h = 32}; SDL_FRect testrect2 = { .x = 30, .y = 30, .w = 40, .h = 40}; bool testcollide = false; PREPARE_ERROR(errctx); ATTEMPT { // Collision overlapping on the top left CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide)); if ( testcollide == false ) { FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed"); } // Collision overlapping on the top right testrect1.x = 64; CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide)); if ( testcollide == false ) { FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed"); } // Collision overlapping on the bottom left testrect1.x = 0; testrect1.y = 32; CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide)); if ( testcollide == false ) { FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed"); } // Collision overlapping on the bottom right testrect1.x = 32; testrect1.y = 32; CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide)); if ( testcollide == false ) { FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed"); } // Collision overlapping the top edge testrect1.x = 0; testrect1.y = 0; testrect1.w = 60; testrect1.h = 32; CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide)); if ( testcollide == false ) { FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed"); } // Collision overlapping the left edge testrect1.x = 0; testrect1.y = 0; testrect1.w = 35; testrect1.h = 80; CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide)); if ( testcollide == false ) { FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed"); } // Collision overlapping the right edge testrect1.x = 65; testrect1.y = 0; testrect1.w = 60; testrect1.h = 80; CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide)); if ( testcollide == false ) { FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed"); } // Collision overlapping the bottom edge testrect1.x = 0; testrect1.y = 65; testrect1.w = 80; testrect1.h = 32; CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide)); if ( testcollide == false ) { FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed"); } // Not colliding testrect1.x = 0; testrect1.y = 0; testrect1.w = 16; testrect1.h = 16; CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide)); if ( testcollide == true ) { FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Invalid collision reported"); } } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } /** * @brief Resolving a path through the root fallback must give its context back. * * akgl_path_relative tries the working directory first and falls back to * resolving against @p root when that reports ENOENT. That fallback used to be * taken by returning from inside the HANDLE block, which skips the * RELEASE_ERROR that FINISH ends with -- so every call down that branch leaked * one entry of AKERR_ARRAY_ERROR, and the 129th call aborted the whole process * with "Unable to pull an error context from the array!". A single map load * resolves several paths this way. * * The loop runs well past AKERR_MAX_ARRAY_ERROR on purpose: at the old * behaviour this test does not fail, it terminates the suite. */ /** * @brief akgl_compare_sdl_surfaces must check geometry before it memcmps. * * It compared `s1->pitch * s1->h` bytes out of both surfaces without looking at * the second one's dimensions, so a smaller s2 was read past its end rather * than reported as a mismatch. Benign in practice and immediately fatal under a * memory checker, which is the reason to fix it rather than leave it. */ akerr_ErrorContext *test_akgl_compare_sdl_surfaces_checks_geometry(void) { PREPARE_ERROR(errctx); SDL_Surface *big = NULL; SDL_Surface *small = NULL; SDL_Surface *twin = NULL; ATTEMPT { big = SDL_CreateSurface(32, 32, SDL_PIXELFORMAT_RGBA8888); twin = SDL_CreateSurface(32, 32, SDL_PIXELFORMAT_RGBA8888); small = SDL_CreateSurface(8, 8, SDL_PIXELFORMAT_RGBA8888); FAIL_ZERO_BREAK(errctx, big, AKGL_ERR_SDL, "%s", SDL_GetError()); FAIL_ZERO_BREAK(errctx, twin, AKGL_ERR_SDL, "%s", SDL_GetError()); FAIL_ZERO_BREAK(errctx, small, AKGL_ERR_SDL, "%s", SDL_GetError()); FAIL_ZERO_BREAK(errctx, SDL_FillSurfaceRect(big, NULL, 0), AKGL_ERR_SDL, "%s", SDL_GetError()); FAIL_ZERO_BREAK(errctx, SDL_FillSurfaceRect(twin, NULL, 0), AKGL_ERR_SDL, "%s", SDL_GetError()); FAIL_ZERO_BREAK(errctx, SDL_FillSurfaceRect(small, NULL, 0), AKGL_ERR_SDL, "%s", SDL_GetError()); TEST_EXPECT_OK(errctx, akgl_compare_sdl_surfaces(big, twin), "comparing two identical surfaces"); // The one that used to read 4 KiB past the end of an 8x8 surface. TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_compare_sdl_surfaces(big, small), "comparing a 32x32 surface against an 8x8 one"); TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_compare_sdl_surfaces(small, big), "comparing an 8x8 surface against a 32x32 one"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_compare_sdl_surfaces(NULL, big), "comparing a NULL first surface"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_compare_sdl_surfaces(big, NULL), "comparing a NULL second surface"); // Same dimensions, different format: the pixels are not comparable even // though the byte count might be. SDL_DestroySurface(small); small = SDL_CreateSurface(32, 32, SDL_PIXELFORMAT_RGB24); FAIL_ZERO_BREAK(errctx, small, AKGL_ERR_SDL, "%s", SDL_GetError()); TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_compare_sdl_surfaces(big, small), "comparing two surfaces of different pixel formats"); } CLEANUP { if ( big != NULL ) { SDL_DestroySurface(big); } if ( twin != NULL ) { SDL_DestroySurface(twin); } if ( small != NULL ) { SDL_DestroySurface(small); } } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *test_akgl_path_relative_releases_contexts(void) { PREPARE_ERROR(errctx); akgl_String *dst = NULL; int before = 0; int after = 0; int i = 0; PASS(errctx, akgl_heap_init()); PASS(errctx, akgl_heap_next_string(&dst)); before = live_error_contexts(); for ( i = 0; i < (AKERR_MAX_ARRAY_ERROR * 2); i++ ) { PASS(errctx, akgl_path_relative("assets", "testcharacter.json", dst)); } after = live_error_contexts(); ATTEMPT { if ( after != before ) { FAIL_BREAK( errctx, AKGL_ERR_BEHAVIOR, "akgl_path_relative leaked %d error context(s) over %d root-fallback resolutions", (after - before), (AKERR_MAX_ARRAY_ERROR * 2)); } } CLEANUP { IGNORE(akgl_heap_release_string(dst)); } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } int main(void) { PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, akgl_error_init()); CATCH(errctx, test_akgl_rectangle_points_nullpointers()); CATCH(errctx, test_akgl_rectangle_points_math()); CATCH(errctx, test_akgl_collide_point_rectangle_nullpointers()); // Defined since forever and never called until 0.5.0. TODO.md, "Known // and still open" item 9. CATCH(errctx, test_akgl_collide_point_rectangle_logic()); CATCH(errctx, test_akgl_collide_rectangles_nullpointers()); CATCH(errctx, test_akgl_collide_rectangles_logic()); CATCH(errctx, test_akgl_collide_rectangles_arrangements()); CATCH(errctx, test_akgl_compare_sdl_surfaces_checks_geometry()); CATCH(errctx, test_akgl_path_relative_releases_contexts()); } CLEANUP { } PROCESS(errctx) { } FINISH_NORETURN(errctx); }