From 8003239116e3982b20660a5e5f1014aed3c2ab1c Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Wed, 29 Jul 2026 13:06:25 -0400 Subject: [PATCH] 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 --- CMakeLists.txt | 1 + TODO.md | 12 ++-- tests/aksl_capture.h | 32 +++++++-- tests/test_memory.c | 151 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 186 insertions(+), 10 deletions(-) create mode 100644 tests/test_memory.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 56cd6cd..17b664c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -130,6 +130,7 @@ install(FILES # it up into AKSL_TESTS. set(AKSL_TESTS linkedlist + memory tree ) diff --git a/TODO.md b/TODO.md index 8e37641..ea8e829 100644 --- a/TODO.md +++ b/TODO.md @@ -89,19 +89,19 @@ against `tests/aksl_capture.h` and added to the `AKSL_TESTS` list. ### 1.1 Memory wrappers -- [ ] `aksl_malloc` — happy path returns non-NULL and writes through `dst`. -- [ ] `aksl_malloc(n, NULL)` → `AKERR_NULLPOINTER`, message content asserted. +- [x] `aksl_malloc` — happy path returns non-NULL and writes through `dst`. +- [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 platform's `malloc(0)` returns NULL; if it does, the wrapper reports `errno`, which may be `0`. See §2.2.1. - [ ] `aksl_malloc(SIZE_MAX, &p)` → allocation failure surfaces `ENOMEM`, and `*dst` is left 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). -- [ ] `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_free` happy path, and a malloc→free round trip under ASan. +- [x] `aksl_memset` — happy path fills the buffer; `n == 0` is a no-op; `s == NULL` → `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. - [ ] `aksl_memcpy` with overlapping regions — decide and test whether this is rejected (`AKERR_VALUE`) or documented as UB like `memcpy`. diff --git a/tests/aksl_capture.h b/tests/aksl_capture.h index a13eed2..2c8675a 100644 --- a/tests/aksl_capture.h +++ b/tests/aksl_capture.h @@ -18,7 +18,7 @@ * AKSL_CHECK_STATUS() run an akerror-returning expression, assert on the * status it came back with, and release the context * 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 * recent context taken by AKSL_CHECK_STATUS. * 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 * 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 - * AKSL_CHECK_STATUS, which wraps it) so that no test leaks a pool slot. + * status 0. Failure-path assertions should go through this so that no test + * 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) { @@ -168,7 +169,30 @@ static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e) } \ } 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) \ AKSL_CHECK(strstr(aksl_capture_buf, (needle)) != NULL) diff --git a/tests/test_memory.c b/tests/test_memory.c new file mode 100644 index 0000000..a5135d1 --- /dev/null +++ b/tests/test_memory.c @@ -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); +}