Files
libakgl/tests/util.c
Andrew Kesterson 4e32328681
Some checks failed
libakgl CI Build / cmake_build (push) Failing after 21s
libakgl CI Build / performance (push) Failing after 21s
libakgl CI Build / memory_check (push) Failing after 17s
libakgl CI Build / mutation_test (push) Failing after 19s
Remove the corner helpers akgl_collide_rectangles no longer uses
akgl_rectangle_points, akgl_collide_point_rectangle, akgl_Point and
akgl_RectanglePoints go. They were the intermediate form of an implementation
that changed: akgl_collide_rectangles was eight corner-containment tests built on
them, and it has been four span comparisons since the cross-case fix. Nothing
outside tests/ called either function, and a point-in-rectangle test is four
comparisons a caller can write without a struct conversion in front of them.

akgl_collide_rectangles stays. It has two correct callers in the sidescroller
asking a game-level overlap question -- a coin, a hazard, from an updatefunc --
where a bool is the whole answer and a proxy plus a narrowphase call would be
computing a normal nothing reads. TODO.md records the split rather than leaving
it to be rediscovered.

Public API removal, so 194 exported akgl_ symbols against 196, and the manual's
counts move with them. The perf suite loses its rectangle_points row; the
all-pairs sweep stays as the control it is now labelled, and PERFORMANCE.md says
what 0.8.0 measured against it -- 188.5 us for 32,640 pairs at 256 actors, where
a whole step with collision attached is 54.1 us doing strictly more.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 08:09:42 -04:00

385 lines
14 KiB
C

#include <SDL3/SDL.h>
#include <string.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/heap.h>
#include <akgl/staticstring.h>
#include <akgl/util.h>
#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_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 the rewrite could have broken
* while fixing it: touching edges must keep counting as a collision, because the
* corner form was inclusive on all four edges and the header promised 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 the
* since-removed 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. Touching counts; util.h and chapter 19
// 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_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);
}