Add memory wrapper tests

Cover deterministic TODO 1.1 memory-wrapper behavior with a new CTest target. Exercise malloc/free round trips, NULL argument handling, memset fill/no-op semantics, and memcpy copy/no-op/null-pointer paths.

Harden AKSL_CHECK_OK so success requires no returned error context, catching wrappers that accidentally raise status-0 errors from stale errno.

Co-authored-by: Codex GPT-5.5 Default <codex-gpt-5.5-default@openai.com>
This commit is contained in:
2026-07-29 13:06:25 -04:00
parent 80205d5c4f
commit 8003239116
4 changed files with 186 additions and 10 deletions

View File

@@ -130,6 +130,7 @@ install(FILES
# it up into AKSL_TESTS. # it up into AKSL_TESTS.
set(AKSL_TESTS set(AKSL_TESTS
linkedlist linkedlist
memory
tree tree
) )

12
TODO.md
View File

@@ -89,19 +89,19 @@ against `tests/aksl_capture.h` and added to the `AKSL_TESTS` list.
### 1.1 Memory wrappers ### 1.1 Memory wrappers
- [ ] `aksl_malloc` — happy path returns non-NULL and writes through `dst`. - [x] `aksl_malloc` — happy path returns non-NULL and writes through `dst`.
- [ ] `aksl_malloc(n, NULL)``AKERR_NULLPOINTER`, message content asserted. - [x] `aksl_malloc(n, NULL)``AKERR_NULLPOINTER`, message content asserted.
- [ ] `aksl_malloc(0, &p)` — pin down the contract. Today the result depends on whether the - [ ] `aksl_malloc(0, &p)` — pin down the contract. Today the result depends on whether the
platform's `malloc(0)` returns NULL; if it does, the wrapper reports `errno`, which may platform's `malloc(0)` returns NULL; if it does, the wrapper reports `errno`, which may
be `0`. See §2.2.1. be `0`. See §2.2.1.
- [ ] `aksl_malloc(SIZE_MAX, &p)` → allocation failure surfaces `ENOMEM`, and `*dst` is left - [ ] `aksl_malloc(SIZE_MAX, &p)` → allocation failure surfaces `ENOMEM`, and `*dst` is left
NULL rather than garbage. NULL rather than garbage.
- [ ] `aksl_free(NULL)``AKERR_NULLPOINTER` (assert the documented behaviour, since - [x] `aksl_free(NULL)``AKERR_NULLPOINTER` (assert the documented behaviour, since
`free(NULL)` is legal C and callers will be surprised). `free(NULL)` is legal C and callers will be surprised).
- [ ] `aksl_free` happy path, and a malloc→free round trip under ASan. - [x] `aksl_free` happy path, and a malloc→free round trip under ASan.
- [ ] `aksl_memset` — happy path fills the buffer; `n == 0` is a no-op; `s == NULL` - [x] `aksl_memset` — happy path fills the buffer; `n == 0` is a no-op; `s == NULL`
`AKERR_NULLPOINTER`. `AKERR_NULLPOINTER`.
- [ ] `aksl_memcpy` — happy path copies; `d == NULL` and `s == NULL` each → - [x] `aksl_memcpy` — happy path copies; `d == NULL` and `s == NULL` each →
`AKERR_NULLPOINTER`; `n == 0` with valid pointers succeeds. `AKERR_NULLPOINTER`; `n == 0` with valid pointers succeeds.
- [ ] `aksl_memcpy` with overlapping regions — decide and test whether this is rejected - [ ] `aksl_memcpy` with overlapping regions — decide and test whether this is rejected
(`AKERR_VALUE`) or documented as UB like `memcpy`. (`AKERR_VALUE`) or documented as UB like `memcpy`.

View File

@@ -18,7 +18,7 @@
* AKSL_CHECK_STATUS() run an akerror-returning expression, assert on the * AKSL_CHECK_STATUS() run an akerror-returning expression, assert on the
* status it came back with, and release the context * status it came back with, and release the context
* so the error pool does not leak. * so the error pool does not leak.
* AKSL_CHECK_OK() the status == 0 (success) case of the above. * AKSL_CHECK_OK() assert a call returned no error context at all.
* aksl_last_status/... the status, message and function name of the most * aksl_last_status/... the status, message and function name of the most
* recent context taken by AKSL_CHECK_STATUS. * recent context taken by AKSL_CHECK_STATUS.
* aksl_capture_install() swap in a capturing akerr_log_method so a test can * aksl_capture_install() swap in a capturing akerr_log_method so a test can
@@ -106,8 +106,9 @@ static char aksl_last_function[AKERR_MAX_ERROR_FNAME_LENGTH];
/* /*
* Record the status/message/function of a returned context, release it back to * Record the status/message/function of a returned context, release it back to
* the pool, and hand back the status. A NULL context means success, which is * the pool, and hand back the status. A NULL context means success, which is
* status 0. Every akstdlib call in a test should go through this (or through * status 0. Failure-path assertions should go through this so that no test
* AKSL_CHECK_STATUS, which wraps it) so that no test leaks a pool slot. * leaks a pool slot. Success-path assertions use AKSL_CHECK_OK, which also
* verifies that no bogus status-0 context was returned.
*/ */
static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e) static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
{ {
@@ -168,7 +169,30 @@ static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
} \ } \
} while ( 0 ) } while ( 0 )
#define AKSL_CHECK_OK(__expr) AKSL_CHECK_STATUS(__expr, 0) /*
* Success is represented by a NULL error context, not just status 0. This catches
* wrappers that incorrectly raise an error using a stale errno value of 0.
*/
#define AKSL_CHECK_OK(__expr) \
do { \
akerr_ErrorContext *__e = (__expr); \
if ( __e != NULL ) { \
int __st = aksl_take(__e); \
fprintf(stderr, \
" CHECK FAILED: %s\n" \
" returned error context with status %d (%s) \"%s\"\n" \
" expected no error context\n" \
" at %s:%d\n", \
#__expr, \
__st, akerr_name_for_status(__st, NULL), \
aksl_last_message, \
__FILE__, __LINE__); \
return 1; \
} \
aksl_last_status = 0; \
aksl_last_message[0] = '\0'; \
aksl_last_function[0] = '\0'; \
} while ( 0 )
#define AKSL_CHECK_CONTAINS(needle) \ #define AKSL_CHECK_CONTAINS(needle) \
AKSL_CHECK(strstr(aksl_capture_buf, (needle)) != NULL) AKSL_CHECK(strstr(aksl_capture_buf, (needle)) != NULL)

151
tests/test_memory.c Normal file
View File

@@ -0,0 +1,151 @@
/*
* Memory-wrapper behaviour that is deterministic today.
*
* TODO.md section 1.1 also calls out malloc(0), malloc(SIZE_MAX), and
* overlapping memcpy. Those are intentionally not pinned here yet: malloc(0)
* is platform-dependent under the current wrapper, huge allocations need
* sanitizer-safe handling, and overlapping memcpy is still an API-contract
* decision.
*/
#include "aksl_capture.h"
static int test_malloc_writes_nonnull_pointer(void)
{
void *ptr = NULL;
AKSL_CHECK_OK(aksl_malloc(32, &ptr));
AKSL_CHECK(ptr != NULL);
AKSL_CHECK_OK(aksl_free(ptr));
return 0;
}
static int test_malloc_rejects_null_destination(void)
{
AKSL_CHECK_STATUS(aksl_malloc(32, NULL), AKERR_NULLPOINTER);
AKSL_CHECK_MSG_CONTAINS("NULL");
return 0;
}
static int test_free_rejects_null_pointer(void)
{
AKSL_CHECK_STATUS(aksl_free(NULL), AKERR_NULLPOINTER);
AKSL_CHECK_MSG_CONTAINS("NULL");
return 0;
}
static int test_malloc_free_round_trip(void)
{
unsigned char *buf = NULL;
size_t i = 0;
AKSL_CHECK_OK(aksl_malloc(64, (void **)&buf));
AKSL_CHECK(buf != NULL);
for ( i = 0; i < 64; i++ ) {
buf[i] = (unsigned char)i;
}
for ( i = 0; i < 64; i++ ) {
AKSL_CHECK(buf[i] == (unsigned char)i);
}
AKSL_CHECK_OK(aksl_free(buf));
return 0;
}
static int test_memset_fills_buffer(void)
{
unsigned char buf[8];
size_t i = 0;
memset((void *)buf, 0x00, sizeof(buf));
AKSL_CHECK_OK(aksl_memset(buf, 0x5a, sizeof(buf)));
for ( i = 0; i < sizeof(buf); i++ ) {
AKSL_CHECK(buf[i] == 0x5a);
}
return 0;
}
static int test_memset_zero_length_is_noop(void)
{
unsigned char buf[4] = { 1, 2, 3, 4 };
AKSL_CHECK_OK(aksl_memset(buf, 0xff, 0));
AKSL_CHECK(buf[0] == 1);
AKSL_CHECK(buf[1] == 2);
AKSL_CHECK(buf[2] == 3);
AKSL_CHECK(buf[3] == 4);
return 0;
}
static int test_memset_rejects_null_buffer(void)
{
AKSL_CHECK_STATUS(aksl_memset(NULL, 0x00, 4), AKERR_NULLPOINTER);
AKSL_CHECK_MSG_CONTAINS("s=");
return 0;
}
static int test_memcpy_copies_bytes(void)
{
unsigned char src[6] = { 0, 1, 2, 3, 4, 5 };
unsigned char dst[6] = { 9, 9, 9, 9, 9, 9 };
size_t i = 0;
AKSL_CHECK_OK(aksl_memcpy(dst, src, sizeof(src)));
for ( i = 0; i < sizeof(src); i++ ) {
AKSL_CHECK(dst[i] == src[i]);
}
return 0;
}
static int test_memcpy_zero_length_is_noop(void)
{
unsigned char src[3] = { 1, 2, 3 };
unsigned char dst[3] = { 4, 5, 6 };
AKSL_CHECK_OK(aksl_memcpy(dst, src, 0));
AKSL_CHECK(dst[0] == 4);
AKSL_CHECK(dst[1] == 5);
AKSL_CHECK(dst[2] == 6);
return 0;
}
static int test_memcpy_rejects_null_destination(void)
{
unsigned char src[1] = { 1 };
AKSL_CHECK_STATUS(aksl_memcpy(NULL, src, sizeof(src)), AKERR_NULLPOINTER);
AKSL_CHECK_MSG_CONTAINS("d=");
return 0;
}
static int test_memcpy_rejects_null_source(void)
{
unsigned char dst[1] = { 0 };
AKSL_CHECK_STATUS(aksl_memcpy(dst, NULL, sizeof(dst)), AKERR_NULLPOINTER);
AKSL_CHECK_MSG_CONTAINS("s=");
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_malloc_writes_nonnull_pointer);
AKSL_RUN(failures, test_malloc_rejects_null_destination);
AKSL_RUN(failures, test_free_rejects_null_pointer);
AKSL_RUN(failures, test_malloc_free_round_trip);
AKSL_RUN(failures, test_memset_fills_buffer);
AKSL_RUN(failures, test_memset_zero_length_is_noop);
AKSL_RUN(failures, test_memset_rejects_null_buffer);
AKSL_RUN(failures, test_memcpy_copies_bytes);
AKSL_RUN(failures, test_memcpy_zero_length_is_noop);
AKSL_RUN(failures, test_memcpy_rejects_null_destination);
AKSL_RUN(failures, test_memcpy_rejects_null_source);
AKSL_REPORT(failures);
}