5 Commits

Author SHA1 Message Date
a87cbfb26d Namespace the embedded mutation target
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m48s
libakstdlib CI Build / mutation_test (push) Successful in 8m0s
2026-07-29 18:02:21 -04:00
566004afd6 Add CLAUDE.md
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m45s
libakstdlib CI Build / mutation_test (push) Successful in 8m2s
2026-07-29 17:42:38 -04:00
11c04923f8 Add AGENTS.md
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m47s
libakstdlib CI Build / mutation_test (push) Successful in 8m3s
2026-07-29 17:29:49 -04:00
01734f511b Make error-status assertions authoritative
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m46s
libakstdlib CI Build / mutation_test (push) Successful in 8m1s
Replace standalone message assertions with a helper that checks the returned akerr status first, then validates message content only as secondary context. Drop ambiguous memcpy message checks where both NULL guards use the same format string.
2026-07-29 15:47:17 -04:00
8003239116 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>
2026-07-29 14:42:27 -04:00
7 changed files with 299 additions and 16 deletions

64
AGENTS.md Normal file
View File

@@ -0,0 +1,64 @@
# Repository Guidelines
## Project Structure & Module Organization
`libakstdlib` is a C shared library that wraps libc calls and small data
structures in `libakerror` error contexts. Public API declarations live in
`include/akstdlib.h`; implementation lives in `src/stdlib.c`. Tests are
one-file CTest executables under `tests/test_<name>.c`, with shared test helpers
in `tests/aksl_capture.h`. CMake package templates are in `cmake/` and
`akstdlib.pc.in`. The vendored dependency is `deps/libakerror`; update it as a
submodule rather than editing generated files under `build/` or `build-asan/`.
## Build, Test, and Development Commands
Initialize dependencies before a fresh build:
```sh
git submodule update --init --recursive
cmake -S . -B build
cmake --build build
```
Run the normal suite with `ctest --test-dir build --output-on-failure`. Use the
instrumented build for memory and undefined-behavior checks:
```sh
cmake -S . -B build-asan -DAKSL_SANITIZE=ON
cmake --build build-asan
ctest --test-dir build-asan --output-on-failure
```
Run `cmake --build build --target mutation` only when you need the slower
mutation harness. `rebuild.sh` installs to `/home/andrew/local` and removes the
local build directory, so treat it as a local convenience script.
## Coding Style & Naming Conventions
Use C with 4-space indentation; existing files sometimes use tabs for continued
statements, so match the surrounding block. Public symbols use the `aksl_`
prefix, structs use `aksl_<Name>`, and tests use `test_<feature>.c` plus static
`test_<case>` functions. Preserve the `akerr_ErrorContext AKERR_NOIGNORE *`
return convention and the `PREPARE_ERROR` / `FAIL_*` / `SUCCEED_RETURN` pattern.
## Testing Guidelines
Add a new test by creating `tests/test_mything.c` and adding `mything` to the
right list in `CMakeLists.txt`. `AKSL_TESTS` must exit zero.
`AKSL_WILL_FAIL_TESTS` are deliberate abort/contract tests.
`AKSL_KNOWN_FAILING_TESTS` assert documented defects from `TODO.md`; when one
starts unexpectedly passing, move it into `AKSL_TESTS` with the fix.
## Commit & Pull Request Guidelines
Recent commits use short imperative summaries, for example `Add memory wrapper
tests` and `Make error-status assertions authoritative`. Keep commits focused
and include tests with behavior changes. Pull requests should describe the
changed API or behavior, list the CTest/sanitizer/mutation commands run, and
link the relevant `TODO.md` item or issue when fixing a known defect.
## Agent-Specific Instructions
Do not modify generated build trees, profiling artifacts, or untracked scratch
files unless explicitly asked. Prefer small, test-backed changes and update
`README.md` or `TODO.md` when changing documented workflows or known failures.

1
CLAUDE.md Normal file
View File

@@ -0,0 +1 @@
See @AGENTS.md

View File

@@ -130,6 +130,7 @@ install(FILES
# it up into AKSL_TESTS.
set(AKSL_TESTS
linkedlist
memory
tree
)
@@ -172,12 +173,19 @@ set_tests_properties(
# re-runs the whole suite once per mutant, so it is a manual target rather than
# a CTest test:
# cmake --build build --target mutation
# When embedded in another project, use a namespaced target to avoid collisions
# with mutation targets provided by sibling dependencies.
# This target covers both src/stdlib.c and include/akstdlib.h. CI runs the
# narrower, faster src/stdlib.c set with a --threshold gate; see
# .gitea/workflows/ci.yaml.
find_package(Python3 COMPONENTS Interpreter)
if(Python3_FOUND)
add_custom_target(mutation
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(AKSL_MUTATION_TARGET mutation)
else()
set(AKSL_MUTATION_TARGET akstdlib_mutation)
endif()
add_custom_target(${AKSL_MUTATION_TARGET}
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/scripts/mutation_test.py
--source-root ${CMAKE_CURRENT_SOURCE_DIR}

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
- [ ] `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`.

View File

@@ -18,7 +18,9 @@
* 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_STATUS_MSG_CONTAINS()
* assert status first, then message content.
* 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 +108,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 +171,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)
@@ -176,9 +202,42 @@ static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
#define AKSL_CHECK_NOT_CONTAINS(needle) \
AKSL_CHECK(strstr(aksl_capture_buf, (needle)) == NULL)
/* Assert on the message of the context most recently taken. */
#define AKSL_CHECK_MSG_CONTAINS(needle) \
AKSL_CHECK(strstr(aksl_last_message, (needle)) != NULL)
/*
* Message checks are secondary: the returned akerr status is the contract. Keep
* the status and message assertions coupled so tests cannot pass on text alone.
*/
#define AKSL_CHECK_STATUS_MSG_CONTAINS(__expr, __expected, __needle) \
do { \
int __st = aksl_take(__expr); \
if ( __st != (__expected) ) { \
fprintf(stderr, \
" CHECK FAILED: %s\n" \
" got %d (%s) \"%s\"\n" \
" expected %d (%s)\n" \
" at %s:%d\n", \
#__expr, \
__st, akerr_name_for_status(__st, NULL), \
aksl_last_message, \
(__expected), \
akerr_name_for_status((__expected), NULL), \
__FILE__, __LINE__); \
return 1; \
} \
if ( strstr(aksl_last_message, (__needle)) == NULL ) { \
fprintf(stderr, \
" CHECK FAILED: message from %s\n" \
" got \"%s\"\n" \
" expected substring \"%s\"\n" \
" status %d (%s)\n" \
" at %s:%d\n", \
#__expr, \
aksl_last_message, \
(__needle), \
__st, akerr_name_for_status(__st, NULL), \
__FILE__, __LINE__); \
return 1; \
} \
} while ( 0 )
/* ---------------------------------------------------------------------- */
/* Test driver */

View File

@@ -190,8 +190,9 @@ static int test_iterate_propagates_callback_error(void)
visitlog_init(&log);
log.fail_at = 0;
AKSL_CHECK_STATUS(aksl_list_iterate(&node, &record_visit, &log), AKERR_VALUE);
AKSL_CHECK_MSG_CONTAINS("iterator failed at visit 0");
AKSL_CHECK_STATUS_MSG_CONTAINS(
aksl_list_iterate(&node, &record_visit, &log),
AKERR_VALUE, "iterator failed at visit 0");
AKSL_CHECK(log.count == 1);
return 0;
}

150
tests/test_memory.c Normal file
View File

@@ -0,0 +1,150 @@
/*
* 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_MSG_CONTAINS(aksl_malloc(32, NULL),
AKERR_NULLPOINTER, "NULL");
return 0;
}
static int test_free_rejects_null_pointer(void)
{
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_free(NULL),
AKERR_NULLPOINTER, "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_MSG_CONTAINS(aksl_memset(NULL, 0x00, 4),
AKERR_NULLPOINTER, "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);
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);
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);
}