Files
libakgl/tests/staticstring.c
Andrew Kesterson e4aa6a5084
Some checks failed
libakgl CI Build / cmake_build (push) Failing after 20s
libakgl CI Build / performance (push) Failing after 19s
libakgl CI Build / memory_check (push) Failing after 15s
libakgl CI Build / mutation_test (push) Failing after 17s
Take libakerror 2.0.1 and drop the workaround it makes obsolete
Every libakgl test suite could report success while failing. libakerror's
default unhandled-error handler ended in exit(errctx->status), an exit
status is one byte wide, and libakgl's band starts at 256 -- so
AKGL_ERR_SDL, the most common failure a library built on SDL can have,
exited 0 and CTest recorded a pass. tests/character.c aborted at its
second of four tests on a bad renderer and was green for months.

0.5.0 worked around that here with TEST_TRAP_UNHANDLED_ERRORS() in
tests/testutil.h, and TODO.md ended the entry saying any consumer's
suites have the same problem and it was worth raising upstream. It was.
2.0.1 fixes it at the source: akerr_exit() owns the mapping and the
default handler calls it, so 0 exits 0, 1 through 255 exit themselves,
and anything else exits AKERR_EXIT_STATUS_UNREPRESENTABLE (125). The
trap and its 21 call sites are gone.

Verified by putting the original failure back rather than by reading the
release notes: a FAIL_BREAK(AKGL_ERR_SDL) in tests/character.c's main
exits 125 and CTest reports a failure. A standalone consumer raising the
same status unhandled exits 125 where it exited 0 before.

tests/actor.c installs its own handler and called exit(errctx->status)
from it, which is the same defect one layer up. It calls akerr_exit()
now.

2.0.0 also makes the error pool and the status registry thread safe,
which libakgl needs more than it knew: audio_stream_callback raises
error contexts on SDL's audio thread. With an unlocked pool that
callback and the main thread could scan AKERR_ARRAY_ERROR at the same
time and be handed the same slot. The comment there says so.

This is a hard dependency floor, not a preference. 2.0.0 moved
__akerr_last_ignored to thread-local storage and made akerr_next_error()
return a context that already holds its reference, and both expand at
libakgl's call sites -- and at a consumer's, because akerror.h is part
of libakgl's public interface. Mixing headers and libraries across that
line double-counts every reference and never returns a pool slot. The
soname moved to libakerror.so.2; include/akgl/error.h now also feature-
tests AKERR_EXIT_STATUS_UNREPRESENTABLE, which is the narrowest probe
for 2.0.1 since libakerror publishes no version macro.

0.7.0 for that reason: libakgl's own ABI is unchanged, but the one it
re-exports through its headers is not.

TODO.md records the pkg-config gap this makes sharper -- akgl.pc names
no dependencies at all, so nothing tells a pkg-config consumer which
libakerror it needs.

Clean build, 26/26 ctest, memcheck clean, warning-clean at -Wall
-Werror. libakgl.so.0.7 links libakerror.so.2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8T5FAYXE8HEJqFLCYwNNc
2026-08-01 13:05:43 -04:00

270 lines
8.3 KiB
C

#include <string.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/heap.h>
#include <akgl/staticstring.h>
#include "testutil.h"
void reset_string_heap(void);
akerr_ErrorContext *test_fresh_heap_gives_strings(void)
{
akgl_String *ptr = NULL;
PREPARE_ERROR(errctx);
for ( int i = 0; i < AKGL_MAX_HEAP_STRING - 1; i++ ) {
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&ptr));
} CLEANUP {
reset_string_heap();
} PROCESS(errctx) {
} FINISH(errctx, true);
}
return 0;
}
akerr_ErrorContext *test_string_heap_error_when_no_strings_left(void)
{
akgl_String *ptr;
PREPARE_ERROR(errctx);
for ( int i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
akgl_heap_strings[i].refcount = 1;
}
for ( int i = 0; i < AKGL_MAX_HEAP_STRING - 1; i++ ) {
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&ptr));
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
return 0;
} FINISH(errctx, true);
}
FAIL_RETURN(errctx, AKERR_OUTOFBOUNDS, "Expected AKERR_NULLPOINTER when accessing beyond string heap bounds");
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_string_heap_honors_refcount(void)
{
akgl_String *firstptr = &akgl_heap_strings[0];
akgl_String *secondptr = &akgl_heap_strings[1];
akgl_String *testptr = NULL;
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&testptr));
if ( testptr != firstptr ) {
FAIL_RETURN(
errctx,
AKERR_VALUE,
"Expected testptr to equal (akgl_heap_strings[0] = %p) but got %p",
firstptr,
testptr
);
}
CATCH(errctx, akgl_string_initialize(testptr, NULL));
if ( testptr->refcount == 0 ) {
FAIL_RETURN(errctx, AKERR_VALUE, "Expected string reference count to be nonzero but got 0");
}
if ( testptr != firstptr ) {
FAIL_RETURN(
errctx,
AKERR_VALUE,
"Expected testptr to equal (akgl_heap_strings[1] = %p) but got %p",
secondptr,
testptr
);
}
CATCH(errctx, akgl_heap_next_string(&testptr));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_strcpy_to_all_strings_no_segfault(void)
{
char copybuf[AKGL_MAX_STRING_LENGTH];
akgl_String *ptr;
memset((void *)&copybuf, 'a', AKGL_MAX_STRING_LENGTH);
PREPARE_ERROR(errctx);
ATTEMPT {
for ( int i = 0; i < AKGL_MAX_HEAP_STRING - 1; i++ ) {
CATCH(errctx, akgl_heap_next_string(&ptr));
strncpy(ptr->data, (char *)&copybuf, AKGL_MAX_STRING_LENGTH);
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_string_initialize(void)
{
akgl_String *ptr;
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&ptr));
CATCH(errctx, akgl_string_initialize(ptr, NULL));
FAIL_NONZERO_BREAK(errctx, ptr->data[0], AKERR_VALUE, "Expected empty zero length string data");
CATCH(errctx, akgl_heap_release_string(ptr));
CATCH(errctx, akgl_heap_next_string(&ptr));
CATCH(errctx, akgl_string_initialize(ptr, "Test value"));
FAIL_NONZERO_BREAK(errctx, strcmp((char *)&ptr->data, "Test value"), AKERR_VALUE, "Expected 'Test value', got %s", (char *)&ptr->data);
CATCH(errctx, akgl_heap_release_string(NULL));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Failure to properly handle NULL pointer");
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
void reset_string_heap(void)
{
for ( int i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
memset(&akgl_heap_strings[i], 0x00, sizeof(akgl_String));
}
}
/**
* @brief akgl_string_initialize must not write past the buffer it is zeroing.
*
* The `NULL` init path zeroed `sizeof(akgl_String)` bytes starting at `data`.
* `data` begins after the `int refcount` in front of it, so that ran four bytes
* past the end of the object -- straight onto the *next* pool slot's refcount,
* which is the field the allocator uses to decide whether a slot is free.
*
* Claiming two adjacent slots and initializing the first is enough to catch it:
* the second's refcount goes to zero and the pool believes it is free while the
* caller is still holding it.
*/
akerr_ErrorContext *test_akgl_string_initialize_stays_in_bounds(void)
{
PREPARE_ERROR(errctx);
akgl_String *first = NULL;
akgl_String *second = NULL;
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&first));
CATCH(errctx, akgl_heap_next_string(&second));
TEST_ASSERT(errctx, second == (first + 1),
"the pool did not hand out adjacent slots; this test needs them");
// A sentinel the overrun would land on. akgl_heap_next_string has
// already set it to 1; make it something an accidental write cannot
// coincide with.
second->refcount = 0x5A;
CATCH(errctx, akgl_string_initialize(first, NULL));
TEST_ASSERT(errctx, second->refcount == 0x5A,
"initializing a string wrote past its buffer: the next slot's "
"refcount is %d, expected %d",
second->refcount, 0x5A);
TEST_ASSERT(errctx, first->refcount == 1,
"initializing a string left its own refcount at %d, expected 1",
first->refcount);
TEST_ASSERT(errctx, first->data[0] == '\0',
"initializing a string with NULL did not zero its buffer");
TEST_ASSERT(errctx, first->data[AKGL_MAX_STRING_LENGTH - 1] == '\0',
"initializing a string with NULL did not zero its last byte");
} CLEANUP {
if ( second != NULL ) {
second->refcount = 1;
IGNORE(akgl_heap_release_string(second));
}
if ( first != NULL ) {
IGNORE(akgl_heap_release_string(first));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief akgl_string_copy must refuse a count that would leave both buffers.
*
* Both slots are exactly AKGL_MAX_STRING_LENGTH bytes, so a larger count read
* past the end of one and wrote past the end of the other. The header used to
* document that as behaviour.
*/
akerr_ErrorContext *test_akgl_string_copy_bounds_its_count(void)
{
PREPARE_ERROR(errctx);
akgl_String *src = NULL;
akgl_String *dest = NULL;
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&src));
CATCH(errctx, akgl_heap_next_string(&dest));
CATCH(errctx, akgl_string_initialize(src, "bounded"));
CATCH(errctx, akgl_string_initialize(dest, NULL));
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_string_copy(src, dest, AKGL_MAX_STRING_LENGTH + 1),
"copying one byte more than a pool string holds");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_string_copy(src, dest, -1),
"copying a negative number of bytes");
// The boundary itself is legal: it is exactly the buffer.
TEST_EXPECT_OK(errctx, akgl_string_copy(src, dest, AKGL_MAX_STRING_LENGTH),
"copying exactly a pool string's length");
TEST_ASSERT(errctx, strcmp((char *)&dest->data, "bounded") == 0,
"a full-length copy did not transfer the contents");
// 0 still means "the whole buffer" rather than "nothing".
CATCH(errctx, akgl_string_initialize(dest, NULL));
TEST_EXPECT_OK(errctx, akgl_string_copy(src, dest, 0),
"copying with a count of zero");
TEST_ASSERT(errctx, strcmp((char *)&dest->data, "bounded") == 0,
"a zero-count copy did not transfer the whole buffer");
} CLEANUP {
if ( dest != NULL ) {
IGNORE(akgl_heap_release_string(dest));
}
if ( src != NULL ) {
IGNORE(akgl_heap_release_string(src));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
printf("test_fresh_heap_gives_string ....\n");
test_fresh_heap_gives_strings();
reset_string_heap();
printf("test_string_heap_error_when_no_strings_left ...\n");
test_string_heap_error_when_no_strings_left();
reset_string_heap();
printf("test_string_heap_honors_refcount ...\n");
test_string_heap_honors_refcount();
reset_string_heap();
printf("test_strcpy_to_all_strings_no_segfault ...\n");
test_strcpy_to_all_strings_no_segfault();
reset_string_heap();
printf("test_akgl_string_initialize....\n");
test_akgl_string_initialize();
reset_string_heap();
printf("test_akgl_string_initialize_stays_in_bounds ...\n");
CATCH(errctx, test_akgl_string_initialize_stays_in_bounds());
reset_string_heap();
printf("test_akgl_string_copy_bounds_its_count ...\n");
CATCH(errctx, test_akgl_string_copy_bounds_its_count());
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
return 0;
}