Compare commits
5 Commits
feature/te
...
a87cbfb26d
| Author | SHA1 | Date | |
|---|---|---|---|
|
a87cbfb26d
|
|||
|
566004afd6
|
|||
|
11c04923f8
|
|||
|
01734f511b
|
|||
| 8003239116 |
64
AGENTS.md
Normal file
64
AGENTS.md
Normal 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.
|
||||
@@ -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
12
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`.
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
150
tests/test_memory.c
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user