Build a real test harness (TODO.md section 1.0)
The suite could not fail and did not run. test_linkedlist.c asserted nothing at all -- it printed node names and returned 0 -- so every confirmed list defect passed it. test_tree.c was red on every run because parms.steps was never reset between its three searches, and four libakerror tests registered but never built, reporting Not Run. CI, meanwhile, was not fetching the submodule, so configure failed before reaching any of it. - tests/aksl_capture.h: AKSL_CHECK (NDEBUG-proof), AKSL_CHECK_STATUS/_OK to run an akerror-returning call, assert its status and release the context, a capturing akerr_log_method, aksl_slots_in_use(), and an AKSL_RUN driver that fails any test leaking an error-pool slot. - test_linkedlist.c: rewritten as 15 assertion cases over the append, iterate and pop behaviour that is correct today. - test_tree.c: each search builds its own tree and params. - Registration driven by AKSL_TESTS, plus AKSL_WILL_FAIL_TESTS (aborts by design) and AKSL_KNOWN_FAILING_TESTS, which marks WILL_FAIL the three new tests asserting correct behaviour for the confirmed defects in TODO.md 2.1.1-2.1.3. Fixing a defect flips its test to "unexpectedly passed", which is the cue to promote it into AKSL_TESTS. - add_test/set_tests_properties are shadowed across the libakerror add_subdirectory call: CMake cannot un-register a test and set_tests_properties cannot cross directory scopes. The dependency has its own CI. - AKSL_SANITIZE=ON builds library, tests and dependency with ASan+UBSan. - scripts/mutation_test.py ported and retargeted at src/stdlib.c; wired to a manual `mutation` target, not a CI gate until 1.1-1.9 exist. - CI: checkout with submodules: recursive, and ctest --output-on-failure. ctest is now 5/5 green in both the default and sanitizer builds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,10 @@ jobs:
|
|||||||
- run: echo "Triggered by ${{ gitea.event_name }} from ${{ gitea.repository }}@${{ gitea.ref }}. Building on ${{ runner.os }}."
|
- run: echo "Triggered by ${{ gitea.event_name }} from ${{ gitea.repository }}@${{ gitea.ref }}. Building on ${{ runner.os }}."
|
||||||
- name: Check out repository code
|
- name: Check out repository code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# A top-level build uses deps/libakerror via add_subdirectory, so the
|
||||||
|
# submodule has to be present or the configure step fails outright.
|
||||||
|
submodules: recursive
|
||||||
- name: dependencies
|
- name: dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update -y
|
sudo apt-get update -y
|
||||||
@@ -24,5 +28,5 @@ jobs:
|
|||||||
cmake -S . -B build
|
cmake -S . -B build
|
||||||
cmake --build build
|
cmake --build build
|
||||||
sudo cmake --install build
|
sudo cmake --install build
|
||||||
cmake --build build --target test
|
ctest --test-dir build --output-on-failure
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||||
|
|||||||
103
CMakeLists.txt
103
CMakeLists.txt
@@ -5,6 +5,21 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -ggdb -pg")
|
|||||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -g -ggdb -pg")
|
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -g -ggdb -pg")
|
||||||
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -g -ggdb -pg")
|
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -g -ggdb -pg")
|
||||||
|
|
||||||
|
# Sanitizer build, off by default:
|
||||||
|
# cmake -S . -B build-asan -DAKSL_SANITIZE=ON && ctest --test-dir build-asan
|
||||||
|
# Set before the dependency is added so libakerror is instrumented too --
|
||||||
|
# several of the defects in TODO.md section 2 (the uninitialised %s in
|
||||||
|
# aksl_realpath, the unbounded vsprintf in aksl_sprintf, the missing va_end in
|
||||||
|
# the printf family) only show up under ASan/UBSan.
|
||||||
|
option(AKSL_SANITIZE "Build the library and its tests with ASan + UBSan" OFF)
|
||||||
|
if(AKSL_SANITIZE)
|
||||||
|
set(AKSL_SANITIZE_FLAGS "-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all")
|
||||||
|
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${AKSL_SANITIZE_FLAGS}")
|
||||||
|
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${AKSL_SANITIZE_FLAGS}")
|
||||||
|
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${AKSL_SANITIZE_FLAGS}")
|
||||||
|
message(STATUS "AKSL_SANITIZE=ON: building with ASan + UBSan")
|
||||||
|
endif()
|
||||||
|
|
||||||
if(TARGET akerror::akerror)
|
if(TARGET akerror::akerror)
|
||||||
message(STATUS "FOUND akerror::akerror")
|
message(STATUS "FOUND akerror::akerror")
|
||||||
else()
|
else()
|
||||||
@@ -16,7 +31,30 @@ include(GNUInstallDirs)
|
|||||||
include(CMakePackageConfigHelpers)
|
include(CMakePackageConfigHelpers)
|
||||||
|
|
||||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||||
|
# libakerror registers its own CTest entries unconditionally, and because we
|
||||||
|
# pull it in EXCLUDE_FROM_ALL its test binaries are never built -- so every
|
||||||
|
# one of them used to show up in our suite as "Not Run" and fail. CMake has no
|
||||||
|
# way to un-register a test, and set_tests_properties cannot reach across
|
||||||
|
# directory scopes, so add_test() is shadowed for the duration of the
|
||||||
|
# add_subdirectory() call. The dependency has its own CI; this project's suite
|
||||||
|
# should only contain this project's tests.
|
||||||
|
set(AKSL_SUPPRESS_ADD_TEST TRUE)
|
||||||
|
function(add_test)
|
||||||
|
if(NOT AKSL_SUPPRESS_ADD_TEST)
|
||||||
|
_add_test(${ARGV})
|
||||||
|
endif()
|
||||||
|
endfunction()
|
||||||
|
# libakerror also marks two of those tests WILL_FAIL, which would now be
|
||||||
|
# setting properties on tests that no longer exist, so suppress that too.
|
||||||
|
function(set_tests_properties)
|
||||||
|
if(NOT AKSL_SUPPRESS_ADD_TEST)
|
||||||
|
_set_tests_properties(${ARGV})
|
||||||
|
endif()
|
||||||
|
endfunction()
|
||||||
|
|
||||||
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
|
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
|
||||||
|
|
||||||
|
set(AKSL_SUPPRESS_ADD_TEST FALSE)
|
||||||
else()
|
else()
|
||||||
if(NOT TARGET akerror::akerror)
|
if(NOT TARGET akerror::akerror)
|
||||||
find_package(PkgConfig REQUIRED)
|
find_package(PkgConfig REQUIRED)
|
||||||
@@ -75,13 +113,66 @@ install(FILES
|
|||||||
DESTINATION ${akstdlib_install_cmakedir}
|
DESTINATION ${akstdlib_install_cmakedir}
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(test_linkedlist tests/test_linkedlist.c)
|
# Each test is one source file tests/test_<name>.c, built into the executable
|
||||||
target_link_libraries(test_linkedlist PRIVATE akstdlib)
|
# test_<name> and registered as the CTest test <name>. Shared helpers live in
|
||||||
add_test(NAME linkedlist COMMAND test_linkedlist)
|
# tests/aksl_capture.h.
|
||||||
|
#
|
||||||
|
# AKSL_TESTS must exit 0.
|
||||||
|
# AKSL_WILL_FAIL_TESTS expected to abort by design (an unhandled error
|
||||||
|
# reaching FINISH_NORETURN, a deliberate contract
|
||||||
|
# violation), so a non-zero exit is a pass.
|
||||||
|
# AKSL_KNOWN_FAILING_TESTS assert the *correct* behaviour of a confirmed
|
||||||
|
# defect from TODO.md section 2.1. They fail until
|
||||||
|
# the defect is fixed, and are marked WILL_FAIL so
|
||||||
|
# the suite stays green and the gap stays visible.
|
||||||
|
# When one is fixed CTest reports it as failed with
|
||||||
|
# "unexpectedly passed" -- that is the cue to move
|
||||||
|
# it up into AKSL_TESTS.
|
||||||
|
set(AKSL_TESTS
|
||||||
|
linkedlist
|
||||||
|
tree
|
||||||
|
)
|
||||||
|
|
||||||
add_executable(test_tree tests/test_tree.c)
|
set(AKSL_WILL_FAIL_TESTS
|
||||||
target_link_libraries(test_tree PRIVATE akstdlib)
|
)
|
||||||
add_test(NAME tree COMMAND test_tree)
|
|
||||||
|
set(AKSL_KNOWN_FAILING_TESTS
|
||||||
|
list_append_chain # TODO.md 2.1.1 -- append truncates lists of 2+ nodes
|
||||||
|
list_iterate_head # TODO.md 2.1.2 -- iterate starts at the midpoint
|
||||||
|
tree_iterate_break # TODO.md 2.1.3 -- ITERATOR_BREAK does not stop a walk
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach(_test IN LISTS AKSL_TESTS AKSL_WILL_FAIL_TESTS AKSL_KNOWN_FAILING_TESTS)
|
||||||
|
add_executable(test_${_test} tests/test_${_test}.c)
|
||||||
|
target_include_directories(test_${_test} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests)
|
||||||
|
target_link_libraries(test_${_test} PRIVATE akstdlib)
|
||||||
|
add_test(NAME ${_test} COMMAND test_${_test})
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
if(AKSL_WILL_FAIL_TESTS OR AKSL_KNOWN_FAILING_TESTS)
|
||||||
|
set_tests_properties(
|
||||||
|
${AKSL_WILL_FAIL_TESTS} ${AKSL_KNOWN_FAILING_TESTS}
|
||||||
|
PROPERTIES WILL_FAIL TRUE
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Mutation testing: break the library in small ways and confirm the test suite
|
||||||
|
# notices. This is a meta-check on the tests themselves, so it is a manual
|
||||||
|
# target (it rebuilds and re-runs the whole suite once per mutant), not a CTest
|
||||||
|
# test and not yet a CI gate -- the per-function tests in TODO.md sections
|
||||||
|
# 1.1-1.9 need to exist before a threshold would mean anything.
|
||||||
|
# cmake --build build --target mutation
|
||||||
|
find_package(Python3 COMPONENTS Interpreter)
|
||||||
|
if(Python3_FOUND)
|
||||||
|
add_custom_target(mutation
|
||||||
|
COMMAND ${Python3_EXECUTABLE}
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/scripts/mutation_test.py
|
||||||
|
--source-root ${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
USES_TERMINAL
|
||||||
|
COMMENT "Running mutation tests (breaks the library, expects tests to fail)"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
# pkgconfig
|
# pkgconfig
|
||||||
|
|||||||
531
TODO.md
Normal file
531
TODO.md
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
Working notes for `libakstdlib` — the akerror-wrapped libc surface.
|
||||||
|
|
||||||
|
Scope of the current library (`include/akstdlib.h`, `src/stdlib.c`): 16 libc wrappers
|
||||||
|
(`fopen`, `fread`, `fwrite`, `fclose`, `malloc`, `free`, `memset`, `memcpy`, `printf`,
|
||||||
|
`fprintf`, `sprintf`, `atoi`, `atol`, `atoll`, `atof`, `realpath`), one hash helper
|
||||||
|
(`aksl_strhash_djb2`), a doubly-linked list (`append`/`pop`/`iterate`) and a binary tree
|
||||||
|
iterator (`aksl_tree_iterate`).
|
||||||
|
|
||||||
|
Items marked **[CONFIRMED]** were reproduced by building the library and running a probe
|
||||||
|
program against it, not just read off the source.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Unit tests that should be written
|
||||||
|
|
||||||
|
### 1.0 Test-harness prerequisites — **DONE** (branch `feature/test-harness`)
|
||||||
|
|
||||||
|
`ctest` is green: 5/5, no *Not Run* entries. Everything in §1.1–1.9 can now be written
|
||||||
|
against `tests/aksl_capture.h` and added to the `AKSL_TESTS` list.
|
||||||
|
|
||||||
|
- [x] **The current tests cannot fail.** `tests/test_linkedlist.c` asserted nothing at all;
|
||||||
|
it printed node names and returned `0` unconditionally, so any of the list bugs in
|
||||||
|
§2.1 would pass it. Rewritten as 15 assertion-based cases covering the append, iterate
|
||||||
|
and pop behaviour that is correct today.
|
||||||
|
- [x] **`tests/test_tree.c` was red.** `ctest` reported `tree (Failed)`, exit 136, because
|
||||||
|
`parms.steps` was never reset between the three searches, so the second assertion
|
||||||
|
compared an accumulated `14` against `7`. Each search now builds its own tree and
|
||||||
|
params. The file records that its step counts do not actually distinguish the three
|
||||||
|
traversal orders — real order assertions remain §1.8.
|
||||||
|
- [x] **Adopt libakerror's test conventions.** `tests/aksl_capture.h` provides `AKSL_CHECK()`
|
||||||
|
(an `NDEBUG`-proof assert), `AKSL_CHECK_STATUS()`/`AKSL_CHECK_OK()` which run an
|
||||||
|
akerror-returning call, assert on its status and release the context,
|
||||||
|
`AKSL_CHECK_MSG_CONTAINS()`, a capturing `akerr_log_method`, `aksl_slots_in_use()` for
|
||||||
|
pool-leak checks, and an `AKSL_RUN()` driver that additionally fails any test which
|
||||||
|
leaks an error-pool slot.
|
||||||
|
- [x] **One test source per behaviour, registered with CTest via a list variable.**
|
||||||
|
`tests/test_<name>.c` → executable `test_<name>` → CTest test `<name>`, driven by
|
||||||
|
`AKSL_TESTS` in `CMakeLists.txt`.
|
||||||
|
- [x] **Add a `WILL_FAIL` category.** Two lists: `AKSL_WILL_FAIL_TESTS` for tests that abort
|
||||||
|
by design (empty for now) and `AKSL_KNOWN_FAILING_TESTS` for the confirmed defects in
|
||||||
|
§2.1 — see the note at the head of §2.1.
|
||||||
|
- [x] **Fix the four permanently-failing submodule tests.** `deps/libakerror` is added
|
||||||
|
`EXCLUDE_FROM_ALL`, so akerror's `add_test` entries registered but its test binaries
|
||||||
|
were never built, and `ctest` reported `err_catch`, `err_cleanup`, `err_trace`,
|
||||||
|
`err_improper_closure` as *Not Run* → failed on every run. Neither the pinned commit
|
||||||
|
nor upstream `main` guards that registration, CMake has no way to un-register a test,
|
||||||
|
and `set_tests_properties` cannot reach across directory scopes — so `add_test` and
|
||||||
|
`set_tests_properties` are shadowed for the duration of the `add_subdirectory` call.
|
||||||
|
The dependency has its own CI; this suite now contains only this project's tests.
|
||||||
|
- [x] **Wire in a memory-error build.** `cmake -S . -B build-asan -DAKSL_SANITIZE=ON` adds
|
||||||
|
`-fsanitize=address,undefined -fno-sanitize-recover=all` to the library, the tests and
|
||||||
|
the vendored libakerror. Currently clean, and it is the intended way to test the items
|
||||||
|
in §2 that only misbehave under instrumentation (uninitialised `%s` in
|
||||||
|
`aksl_realpath`, unbounded `vsprintf`, missing `va_end`).
|
||||||
|
- [x] **Port `scripts/mutation_test.py` from libakerror.** Retargeted at `src/stdlib.c` and
|
||||||
|
`include/akstdlib.h` (178 mutants) and exposed as
|
||||||
|
`cmake --build build --target mutation`. Deliberately *not* a CI gate yet, and no
|
||||||
|
`--threshold` is set: a mutation score only means something once §1.1–1.9 exist, and
|
||||||
|
against today's tests it would do little but confirm that most of the library is
|
||||||
|
untested.
|
||||||
|
- [x] **CI could not have run any of this.** `actions/checkout@v4` was not fetching
|
||||||
|
submodules, so `add_subdirectory(deps/libakerror)` had nothing to descend into and the
|
||||||
|
configure step failed before ever reaching the tests. Added `submodules: recursive`,
|
||||||
|
and swapped `cmake --build build --target test` for `ctest --output-on-failure` so a
|
||||||
|
failure is diagnosable from the job log. The deeper problem — CI installing
|
||||||
|
`libakerror@main` while the build actually compiles the pinned submodule — is §2.3.
|
||||||
|
|
||||||
|
### 1.1 Memory wrappers
|
||||||
|
|
||||||
|
- [ ] `aksl_malloc` — happy path returns non-NULL and writes through `dst`.
|
||||||
|
- [ ] `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
|
||||||
|
`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` →
|
||||||
|
`AKERR_NULLPOINTER`.
|
||||||
|
- [ ] `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`.
|
||||||
|
|
||||||
|
### 1.2 File I/O
|
||||||
|
|
||||||
|
- [ ] `aksl_fopen` happy path on a temp file; `*fp` is written.
|
||||||
|
- [ ] `aksl_fopen("/nonexistent/path", "r", &fp)` → `ENOENT` propagated as the status, with
|
||||||
|
the pathname in the message.
|
||||||
|
- [ ] `aksl_fopen` on a mode-denied path (e.g. `/proc/1/mem`, or a `chmod 000` temp file) →
|
||||||
|
`EACCES`.
|
||||||
|
- [ ] `aksl_fopen(path, mode, NULL)` → `AKERR_NULLPOINTER`.
|
||||||
|
- [ ] `aksl_fopen(NULL, "r", &fp)` and `aksl_fopen(path, NULL, &fp)` → currently unchecked,
|
||||||
|
see §2.2.2. Test once the guards exist.
|
||||||
|
- [ ] `aksl_fread` full read; short read at EOF → `AKERR_EOF`; read from a write-only stream
|
||||||
|
→ `AKERR_IO`; `fp == NULL` → `AKERR_NULLPOINTER`; `ptr == NULL` → should be
|
||||||
|
`AKERR_NULLPOINTER` (see §2.2.3).
|
||||||
|
- [ ] `aksl_fread` **partial read that is neither EOF nor error** — assert whatever the fixed
|
||||||
|
contract is; today this silently returns success and the caller cannot tell how many
|
||||||
|
members were read.
|
||||||
|
- [ ] `aksl_fwrite` happy path; write to a read-only stream → `AKERR_IO`; write to a full
|
||||||
|
device (`/dev/full`) → `ENOSPC`/`AKERR_IO`; `fp == NULL` → `AKERR_NULLPOINTER`.
|
||||||
|
- [ ] `aksl_fclose` happy path; `NULL` → `AKERR_NULLPOINTER`; double-close is caught or
|
||||||
|
documented as UB.
|
||||||
|
- [ ] `aksl_fclose` on a stream whose buffered flush fails (`/dev/full`) → non-zero `fclose`
|
||||||
|
surfaces `errno`.
|
||||||
|
- [ ] Round-trip test: `fopen` → `fwrite` → `fclose` → `fopen` → `fread` → compare bytes.
|
||||||
|
|
||||||
|
### 1.3 Formatted output
|
||||||
|
|
||||||
|
- [ ] `aksl_printf` / `aksl_fprintf` / `aksl_sprintf` happy paths, asserting both the byte
|
||||||
|
count written through `count` and the produced text.
|
||||||
|
- [ ] Each of the three with every pointer argument NULL in turn → `AKERR_NULLPOINTER`.
|
||||||
|
- [ ] `aksl_fprintf` to a closed / read-only stream → error path, and confirm `*count` is not
|
||||||
|
left holding `-1` as if it were a valid length.
|
||||||
|
- [ ] `aksl_sprintf` with a format that overflows the destination — currently unbounded
|
||||||
|
(§2.2.4). Test the `aksl_snprintf` replacement once it exists.
|
||||||
|
- [ ] Regression test for the missing `va_end` (§2.1.4) — a test that calls each variadic
|
||||||
|
wrapper many times in a loop, run under valgrind/ASan.
|
||||||
|
- [ ] Format-string/arg mismatch is caught at compile time once
|
||||||
|
`__attribute__((format(printf, ...)))` is added (§2.2.5) — a negative compile test.
|
||||||
|
|
||||||
|
### 1.4 String → number
|
||||||
|
|
||||||
|
- [ ] `aksl_atoi` / `atol` / `atoll` / `atof` happy paths, including negative values and
|
||||||
|
leading whitespace.
|
||||||
|
- [ ] NULL `nptr` and NULL `dest` for each → `AKERR_NULLPOINTER`.
|
||||||
|
- [ ] **Non-numeric input** (`"not a number"`) — **[CONFIRMED]** currently returns *success*
|
||||||
|
with `*dest == 0`. Test the `AKERR_VALUE` behaviour once §2.1.5 is fixed.
|
||||||
|
- [ ] **Overflow** (`"99999999999999999999"`) — **[CONFIRMED]** currently returns success with
|
||||||
|
a garbage value (`-1` on this box). Test for `ERANGE`.
|
||||||
|
- [ ] Empty string, `" "`, `"12abc"` (trailing junk), `"0x10"`, `"inf"`/`"nan"` for `atof`.
|
||||||
|
- [ ] `LONG_MIN`/`LONG_MAX`/`LLONG_MIN`/`LLONG_MAX` boundary strings round-trip exactly.
|
||||||
|
|
||||||
|
### 1.5 `aksl_realpath`
|
||||||
|
|
||||||
|
- [ ] Happy path on an existing file and on a symlink chain; result matches `realpath(3)`.
|
||||||
|
- [ ] Non-existent path → `ENOENT`.
|
||||||
|
- [ ] A path component that is not a directory → `ENOTDIR`.
|
||||||
|
- [ ] Symlink loop → `ELOOP`.
|
||||||
|
- [ ] `path == NULL` → `AKERR_NULLPOINTER`.
|
||||||
|
- [ ] `resolved_path == NULL` — currently unchecked and leaks (§2.1.6). Test once fixed.
|
||||||
|
- [ ] A failure case where `resolved_path` is an *uninitialised* buffer — this is the crash
|
||||||
|
case in §2.1.6; run it under ASan/MSan.
|
||||||
|
|
||||||
|
### 1.6 `aksl_strhash_djb2`
|
||||||
|
|
||||||
|
- [ ] Known-answer vectors: `djb2("")` == 5381; a handful of fixed strings with their
|
||||||
|
pre-computed 32-bit values.
|
||||||
|
- [ ] `len == 0` returns 5381 regardless of `str` contents.
|
||||||
|
- [ ] NULL `str` and NULL `hashval` → `AKERR_NULLPOINTER`.
|
||||||
|
- [ ] **High-bit bytes** (`"\xff\xfe"`) — pins down the sign-extension bug in §2.2.6; the
|
||||||
|
expected value must be the `unsigned char` one.
|
||||||
|
- [ ] Embedded NUL bytes are hashed (the function is length-driven, not NUL-driven).
|
||||||
|
- [ ] Same input → same output across two calls (no hidden state).
|
||||||
|
|
||||||
|
### 1.7 Linked list
|
||||||
|
|
||||||
|
- [ ] **`aksl_list_append` builds a correct chain of N nodes** — **[CONFIRMED BROKEN]**, see
|
||||||
|
§2.1.1. Appending `n1..n4` to `n0` yields the chain `n0 -> n4`; `n1`, `n2`, `n3` are
|
||||||
|
silently dropped. This is the single most important test to add.
|
||||||
|
- [ ] `append` sets `obj->prev` to the real tail and `obj->next` to NULL.
|
||||||
|
- [ ] `append` onto an empty (single, zeroed) node.
|
||||||
|
- [ ] `append` NULL list / NULL obj → `AKERR_NULLPOINTER`.
|
||||||
|
- [ ] `append` onto a list containing a cycle → `AKERR_CIRCULAR_REFERENCE`, for a self-loop,
|
||||||
|
a 2-node cycle, and a cycle that does not include the head.
|
||||||
|
- [ ] `append` a node that is already in the list (aliasing) — define and test the contract.
|
||||||
|
- [ ] **`aksl_list_iterate` visits every node exactly once, starting at the head** —
|
||||||
|
**[CONFIRMED BROKEN]**, see §2.1.2: it starts iterating from the *midpoint* left behind
|
||||||
|
by the cycle detector, so the head is never visited. Assert both the visit count and
|
||||||
|
the visit order.
|
||||||
|
- [ ] `iterate` over a single-node list visits exactly one node.
|
||||||
|
- [ ] `iterate` NULL list / NULL iter → `AKERR_NULLPOINTER`.
|
||||||
|
- [ ] `iterate` over a cyclic list → `AKERR_CIRCULAR_REFERENCE` (self-loop, 2-node,
|
||||||
|
tail-to-middle).
|
||||||
|
- [ ] `iterate` where the callback raises `AKERR_ITERATOR_BREAK` → iteration stops at that
|
||||||
|
node, the wrapper returns success, and the visit count proves the early exit.
|
||||||
|
- [ ] `iterate` where the callback raises some *other* error → that error propagates out
|
||||||
|
unchanged, with the callback's message intact.
|
||||||
|
- [ ] `aksl_list_pop` on a middle node relinks `prev`/`next` correctly and clears the popped
|
||||||
|
node's pointers.
|
||||||
|
- [ ] `pop` on the head node, on the tail node, and on a single-node list.
|
||||||
|
- [ ] `pop(NULL)` → `AKERR_NULLPOINTER`.
|
||||||
|
- [ ] `pop` then `iterate` — the list is still traversable and the popped node is gone.
|
||||||
|
- [ ] Pool-accounting test: after a long sequence of list operations including failures,
|
||||||
|
`akerr_slots_in_use() == 0`.
|
||||||
|
|
||||||
|
### 1.8 Tree
|
||||||
|
|
||||||
|
- [ ] Visit **order** assertions, not just counts, for `DFS_PREORDER`, `DFS_INORDER` and
|
||||||
|
`DFS_POSTORDER` over the 7-node tree — record the visited node pointers into an array
|
||||||
|
and compare against the expected sequence. The current test only counts steps, which
|
||||||
|
cannot distinguish the three orders (all three visit 7 nodes).
|
||||||
|
- [ ] **`AKERR_ITERATOR_BREAK` aborts the whole traversal** — **[CONFIRMED BROKEN]**, see
|
||||||
|
§2.1.3. Break at `tree[3]` in pre-order and assert 4 visits; today it visits all 7.
|
||||||
|
Add the equivalent for in-order and post-order.
|
||||||
|
- [ ] `AKSL_TREE_SEARCH_BFS` and `AKSL_TREE_SEARCH_BFS_RIGHT` → `AKERR_NOT_IMPLEMENTED`
|
||||||
|
today; replace with real order assertions once implemented (§3 / §2.2.9).
|
||||||
|
- [ ] **Unknown `searchmode`** (e.g. `99`) — **[CONFIRMED]** currently returns *success*
|
||||||
|
having visited nothing. Should be `AKERR_VALUE`.
|
||||||
|
- [ ] **`AKSL_TREE_SEARCH_VISIT`** (defined in the header, `5`) — **[CONFIRMED]** falls into
|
||||||
|
the same silent-success hole. Either implement it or reject it.
|
||||||
|
- [ ] `root == NULL` / `iter == NULL` → `AKERR_NULLPOINTER`.
|
||||||
|
- [ ] Single-node tree (no children) visits exactly once, in all three orders.
|
||||||
|
- [ ] Left-only and right-only degenerate chains.
|
||||||
|
- [ ] Callback raising a non-`ITERATOR_BREAK` error propagates out of the recursion with the
|
||||||
|
original status and message.
|
||||||
|
- [ ] Custom `lalloc`/`lfree` are actually invoked — **currently they are stored and never
|
||||||
|
called** (§2.2.8), so this test will fail until BFS lands.
|
||||||
|
- [ ] Deep/degenerate tree (e.g. 100k-node left chain) — documents the recursion-depth limit
|
||||||
|
(§2.2.7).
|
||||||
|
- [ ] A tree containing a cycle (child pointing back at an ancestor) — currently infinite
|
||||||
|
recursion; test for `AKERR_CIRCULAR_REFERENCE` once guarded.
|
||||||
|
|
||||||
|
### 1.9 Cross-cutting
|
||||||
|
|
||||||
|
- [ ] **Error-pool accounting**: for *every* wrapper, a test that drives the failure path
|
||||||
|
`AKERR_MAX_ARRAY_ERROR + 10` times and asserts the pool does not leak
|
||||||
|
(`akerr_slots_in_use() == 0` after each handled error).
|
||||||
|
- [ ] **Stack-trace content**: assert that the file/function/line recorded by each wrapper's
|
||||||
|
`FAIL` points at `src/stdlib.c` and the right function name.
|
||||||
|
- [ ] **`AKERR_NOIGNORE` is effective**: a negative compile test where a wrapper's return is
|
||||||
|
discarded and `-Werror=unused-result` fires.
|
||||||
|
- [ ] **Thread safety**: libakerror's `AKERR_ARRAY_ERROR` is a process-global array with no
|
||||||
|
locking. If `libakstdlib` is meant to be callable from threads, add a concurrent
|
||||||
|
smoke test under TSan — and if it is not, say so in the README.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Corner cases in existing functionality that should be resolved
|
||||||
|
|
||||||
|
### 2.1 Confirmed defects (reproduced against the built library)
|
||||||
|
|
||||||
|
The first three now have a failing test apiece, registered in `AKSL_KNOWN_FAILING_TESTS`
|
||||||
|
and therefore marked `WILL_FAIL` so the suite stays green while the gap stays visible:
|
||||||
|
`tests/test_list_append_chain.c`, `tests/test_list_iterate_head.c` and
|
||||||
|
`tests/test_tree_iterate_break.c`. When one of these defects is fixed, CTest reports that
|
||||||
|
test as failed with *unexpectedly passed* — that is the cue to move it into `AKSL_TESTS`.
|
||||||
|
|
||||||
|
1. **`aksl_list_append` does not find the tail — it silently truncates the list.**
|
||||||
|
`src/stdlib.c:194`. The function conflates Floyd cycle detection with tail-finding:
|
||||||
|
`tail` is set to `slow` *before* `slow` advances, so it tracks the node *behind the
|
||||||
|
midpoint*, not the tail. Appending `n1`, `n2`, `n3`, `n4` to `n0` produces the chain
|
||||||
|
`n0 -> n4`; `n1`–`n3` are unlinked and lost. The fix is to keep the cycle check but walk
|
||||||
|
a separate cursor to the real tail (`while (tail->next) tail = tail->next;`), or run
|
||||||
|
Floyd first and then walk to the end.
|
||||||
|
|
||||||
|
2. **`aksl_list_iterate` skips the first half of the list.** `src/stdlib.c:324`. After the
|
||||||
|
cycle-detection loop, `slow` is left at the list midpoint, and the visiting loop then
|
||||||
|
starts from `slow` instead of from `list`. The head node is never passed to the callback.
|
||||||
|
Fix: iterate from `list`, not from `slow`.
|
||||||
|
|
||||||
|
3. **`AKERR_ITERATOR_BREAK` does not stop a tree traversal.** `src/stdlib.c:251`. The
|
||||||
|
recursive frame in which the callback raises the break handles it in its own
|
||||||
|
`PROCESS`/`HANDLE(e, AKERR_ITERATOR_BREAK)` block and returns *success*; the parent's
|
||||||
|
`PASS` therefore sees no error and continues on to the sibling subtree. Breaking at
|
||||||
|
`tree[3]` in pre-order still visits all 7 nodes. Fix by splitting the recursion: an
|
||||||
|
internal helper that propagates `AKERR_ITERATOR_BREAK` unhandled, and a public entry point
|
||||||
|
that swallows it exactly once at the top. `tests/test_tree.c` currently passes only
|
||||||
|
because the search target is the last node in all three orders.
|
||||||
|
|
||||||
|
4. **`va_end` is never called.** `src/stdlib.c:96`, `:109`, `:122` each call `va_start` with
|
||||||
|
no matching `va_end` on any path — happy or error. This is undefined behaviour per the C
|
||||||
|
standard and leaks register-save state on some ABIs.
|
||||||
|
|
||||||
|
5. **`aksl_atoi`/`atol`/`atoll`/`atof` cannot report a conversion failure.**
|
||||||
|
`src/stdlib.c:135`–`169`. `atoi("not a number")` returns success with `0`;
|
||||||
|
`atoi("99999999999999999999")` returns success with a wrapped value. Since the entire
|
||||||
|
point of this library is turning silent libc failures into error contexts, these should be
|
||||||
|
reimplemented over `strtol`/`strtoll`/`strtod` with `errno = 0` before the call, an
|
||||||
|
`endptr` check for "no digits consumed" and "trailing junk", and a range check —
|
||||||
|
raising `AKERR_VALUE` and `ERANGE` respectively. Keep the `atoi`-compatible names but
|
||||||
|
document the stricter contract, or add `aksl_strtol`-family wrappers alongside.
|
||||||
|
|
||||||
|
6. **`aksl_realpath` mishandles `resolved_path`.** `src/stdlib.c:171`.
|
||||||
|
- `resolved_path` is never NULL-checked. `realpath(path, NULL)` is valid and mallocs a
|
||||||
|
buffer, but the wrapper discards `result`, so the caller gets nothing and the buffer
|
||||||
|
leaks.
|
||||||
|
- On failure the message formats `resolved_path` with `%s` while `realpath` leaves it
|
||||||
|
unspecified — for the normal caller who passed an uninitialised stack buffer, the error
|
||||||
|
path itself reads uninitialised memory and can crash.
|
||||||
|
- There is no way to express the buffer's size, so callers must know to supply `PATH_MAX`
|
||||||
|
bytes. Consider `aksl_realpath(const char *path, char *buf, size_t buflen)`, or an
|
||||||
|
allocating variant that returns the malloc'd pointer through an out-param.
|
||||||
|
|
||||||
|
### 2.2 Latent issues and API contract gaps
|
||||||
|
|
||||||
|
1. **`errno` is used as an error status where it may be stale.** `aksl_malloc` reports
|
||||||
|
`errno` when `malloc` returns NULL, but `malloc(0)` may legitimately return NULL without
|
||||||
|
setting `errno`, producing a `FAIL` with `status == 0` — an error context that every
|
||||||
|
`DETECT`/`CATCH` will read as *success* while still holding a pool slot. Guard every
|
||||||
|
`errno`-sourced status with a fallback (`errno ? errno : AKERR_IO`) and set `errno = 0`
|
||||||
|
before the call being wrapped.
|
||||||
|
|
||||||
|
2. **`aksl_fopen` does not validate `pathname` or `mode`.** `fopen(NULL, ...)` is UB. Add
|
||||||
|
`AKERR_NULLPOINTER` guards.
|
||||||
|
|
||||||
|
3. **`aksl_fread`/`aksl_fwrite` lose the transfer count and hide short transfers.**
|
||||||
|
- `ptr` is never NULL-checked in either function.
|
||||||
|
- Neither has an out-param for the number of members actually transferred, so a caller who
|
||||||
|
gets `AKERR_EOF` cannot tell how much data arrived. Add `size_t *nmemb_out`.
|
||||||
|
- If `nmemr != nmemb` but neither `feof` nor `ferror` is set, both functions fall through
|
||||||
|
to `SUCCEED_RETURN` — a short transfer reported as complete success.
|
||||||
|
- `aksl_fwrite`'s error message reads `"Error reading file"` (`src/stdlib.c:83`), and
|
||||||
|
checking `feof()` on a write path is meaningless.
|
||||||
|
|
||||||
|
4. **`aksl_sprintf` wraps `vsprintf`, which is unbounded.** There is no way for a caller to
|
||||||
|
bound the destination. Add `aksl_snprintf`/`aksl_vsnprintf` and consider deprecating the
|
||||||
|
`sprintf` wrapper, or reject it outright — an "error-handling" wrapper around an
|
||||||
|
unbounded write is a sharp edge the library exists to remove.
|
||||||
|
|
||||||
|
5. **No `format` attribute on the variadic wrappers.** Adding
|
||||||
|
`__attribute__((format(printf, 2, 3)))` (and the equivalents) restores the compile-time
|
||||||
|
format/argument checking that callers lose by going through the wrapper.
|
||||||
|
|
||||||
|
6. **`aksl_strhash_djb2` sign-extends bytes ≥ 0x80.** `src/stdlib.c:181` iterates a
|
||||||
|
`char *`, which is signed on x86/ARM Linux, so high bytes contribute a sign-extended
|
||||||
|
negative value and the hash differs from the canonical djb2 — and differs across
|
||||||
|
platforms where `char` is unsigned. Iterate a `const unsigned char *`. Also take
|
||||||
|
`const char *` in the signature so callers need not cast away constness.
|
||||||
|
|
||||||
|
7. **`aksl_tree_iterate` recursion is unbounded and cycle-blind.** A deep or degenerate tree
|
||||||
|
overflows the stack, and a child pointer that loops back to an ancestor recurses forever.
|
||||||
|
Add a depth limit (raising `AKERR_OUTOFBOUNDS`) and/or a visited check raising
|
||||||
|
`AKERR_CIRCULAR_REFERENCE`, consistent with what the list functions already do.
|
||||||
|
|
||||||
|
8. **`lalloc`, `lfree` and `queue` are dead parameters.** `lalloc`/`lfree` are defaulted and
|
||||||
|
then never called; `queue` is entirely unused (it is the only `-Wunused-parameter` warning
|
||||||
|
in the file). The doc comment even tells the caller to "pass NULL here", which is a sign
|
||||||
|
the queue belongs in an internal helper rather than the public signature. Either
|
||||||
|
implement BFS so they are used, or drop them from the public API until it is.
|
||||||
|
|
||||||
|
9. **Unknown `searchmode` values return success.** The `switch` in `aksl_tree_iterate` has no
|
||||||
|
`default:`; `searchmode == 99` and the header-defined `AKSL_TREE_SEARCH_VISIT` (5) both
|
||||||
|
fall straight through to `SUCCEED_RETURN` having visited nothing. Add
|
||||||
|
`default: FAIL_RETURN(e, AKERR_VALUE, ...)`.
|
||||||
|
|
||||||
|
10. **`AKSL_TREE_SEARCH_BFS`/`BFS_RIGHT` are unimplemented** (`AKERR_NOT_IMPLEMENTED`), and
|
||||||
|
`AKSL_TREE_SEARCH_VISIT` is declared in the header with a documented meaning but no
|
||||||
|
implementation anywhere.
|
||||||
|
|
||||||
|
11. **`aksl_memset`'s and `aksl_memcpy`'s inner checks are dead code.** `memset` returns `s`
|
||||||
|
and `memcpy` returns `d`, both unconditionally; neither can fail. The
|
||||||
|
`FAIL_ZERO_RETURN(e, memset(...), errno, ...)` and `(memcpy(...) == d)` checks can never
|
||||||
|
fire and only obscure the intent. Also, `memcpy` with overlapping ranges is UB — either
|
||||||
|
document that or dispatch to `memmove`.
|
||||||
|
|
||||||
|
12. **`aksl_list_pop` cannot tell the caller the new head.** Popping the head leaves the
|
||||||
|
caller's head pointer dangling at a now-detached node. Add an out-param for the new head,
|
||||||
|
or a `aksl_list_pop(aksl_ListNode **head, aksl_ListNode *node)` form.
|
||||||
|
|
||||||
|
13. **`aksl_free` does not clear the caller's pointer**, so double-free remains easy. Consider
|
||||||
|
an `aksl_freep(void **ptr)` that frees and NULLs.
|
||||||
|
|
||||||
|
14. **No initialisers for the public structs.** Every caller must remember to `memset` an
|
||||||
|
`aksl_ListNode`/`aksl_TreeNode` to zero before use (both existing tests do). Add
|
||||||
|
`aksl_list_node_init`/`aksl_tree_node_init` or `AKSL_LIST_NODE_INIT` macros.
|
||||||
|
|
||||||
|
15. **`aksl_TreeNode.parent` is declared but never set or read** by any library function.
|
||||||
|
|
||||||
|
16. **Header hygiene**: no `extern "C" { }` guard for C++ consumers; no version macros
|
||||||
|
(`AKSL_VERSION_MAJOR`…); `akstdlib.h` pulls in `stdio.h`/`stdlib.h`/`string.h`/`stdint.h`
|
||||||
|
into every consumer's namespace.
|
||||||
|
|
||||||
|
17. **Doxygen coverage is 2 functions out of 20.** A `Doxyfile` exists but only
|
||||||
|
`aksl_tree_iterate` and `aksl_list_iterate` have doc comments. Every public function
|
||||||
|
needs `@param`/`@throws`/`@return`, especially the ones whose contract deviates from libc
|
||||||
|
(`aksl_free(NULL)` is an error; `aksl_atoi` will become strict).
|
||||||
|
|
||||||
|
### 2.3 Build, CI and repository
|
||||||
|
|
||||||
|
- [ ] **`CMakeLists.txt:4-6` sets `CMAKE_CXX_FLAGS` in a C-only project**, so `-g -ggdb -pg`
|
||||||
|
never reach the C compiler. Use `CMAKE_C_FLAGS` — or better, drop `-pg` from the
|
||||||
|
default build entirely (it is what produces the stray `gmon.out` sitting untracked in
|
||||||
|
the repo root) and let `CMAKE_BUILD_TYPE` control debug info.
|
||||||
|
- [ ] **No warning flags.** Add `-Wall -Wextra` (and ideally `-Werror` in CI). The only
|
||||||
|
current warning is the unused `queue` parameter, so the cost of turning them on is low.
|
||||||
|
- [ ] `src/stdlib.c` triggers **"ISO C99 requires at least one argument for the `...`"** on
|
||||||
|
roughly 20 lines under `-Wpedantic`, because `FAIL_*` is called with a bare message and
|
||||||
|
no varargs. Either always pass an argument, or add a zero-arg-safe form in libakerror.
|
||||||
|
- [ ] **The `deps/libakerror` submodule is pinned 11 commits behind `main`** (pinned at
|
||||||
|
`4fad0ce "Add gitea workflow"`; upstream is at `4212ff0`). The pin predates the
|
||||||
|
refcount-leak fix, the stack-trace buffer-overflow fix, the `AKERR_MAX_ERR_VALUE`
|
||||||
|
correction and the format-string fix. Bump it.
|
||||||
|
- [ ] **CI does not build against the submodule it pins.** `.gitea/workflows/ci.yaml` clones
|
||||||
|
`libakerror@main` and installs it, while the build it then runs is top-level and so
|
||||||
|
compiles `deps/libakerror` at the pinned commit — two different libakerror versions
|
||||||
|
depending on where you build, and the installed one is never actually linked. Pick
|
||||||
|
one. (The submodule is at least *present* in CI now: §1.0 added
|
||||||
|
`submodules: recursive` to the checkout, without which configure failed outright.)
|
||||||
|
- [x] **`ctest` was red** (`tree` failing plus four *Not Run* submodule tests) — fixed in
|
||||||
|
§1.0. The build badge in `README.md` means something again.
|
||||||
|
- [ ] **Untracked build litter** in the repo root: `build/`, `gmon.out`,
|
||||||
|
`CMakeLists.txt-akstdlib`, and a dozen `*~` editor backups. Add a `.gitignore`
|
||||||
|
(libakerror has one; this repo does not).
|
||||||
|
- [ ] **`README.md` is a build badge and nothing else.** It needs at minimum: what the
|
||||||
|
library is, the akerror wrapper contract, the list of wrapped functions, and the
|
||||||
|
deviations from libc semantics.
|
||||||
|
- [ ] **Indentation is inconsistent** — `src/stdlib.c` mixes hard tabs and 4-space indents
|
||||||
|
within the same functions. libakerror standardised on Stroustrup style
|
||||||
|
(commit `e5f7616`); apply the same here, ideally with a checked-in `.clang-format`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. libc functions not yet wrapped
|
||||||
|
|
||||||
|
Ordered roughly by how much a caller of this library would miss them. Each entry means "add
|
||||||
|
an `aksl_`-prefixed wrapper that turns the documented failure modes into an
|
||||||
|
`akerr_ErrorContext`".
|
||||||
|
|
||||||
|
### 3.1 High priority — gaps in areas the library already covers
|
||||||
|
|
||||||
|
**Memory (`stdlib.h`, `string.h`)**
|
||||||
|
- [ ] `calloc`, `realloc`, `reallocarray` — `realloc`'s "returns NULL and the old pointer is
|
||||||
|
still valid" trap is exactly what this library should be hiding.
|
||||||
|
- [ ] `aligned_alloc`, `posix_memalign`
|
||||||
|
- [ ] `memmove`, `memcmp`, `memchr`
|
||||||
|
|
||||||
|
**Bounded string formatting (`stdio.h`)**
|
||||||
|
- [ ] `snprintf`, `vsnprintf` — needed to make §2.2.4 fixable.
|
||||||
|
- [ ] `vprintf`, `vfprintf`, `vsprintf` — the `va_list` forms, so consumers can build their
|
||||||
|
own variadic wrappers on top.
|
||||||
|
- [ ] `asprintf` / `vasprintf` (GNU) as an allocating alternative.
|
||||||
|
|
||||||
|
**String → number (`stdlib.h`)**
|
||||||
|
- [ ] `strtol`, `strtoll`, `strtoul`, `strtoull`, `strtod`, `strtof`, `strtold` — the correct
|
||||||
|
foundation for §2.1.5.
|
||||||
|
|
||||||
|
**Strings (`string.h`)**
|
||||||
|
- [ ] `strlen`, `strnlen`
|
||||||
|
- [ ] `strcpy`, `strncpy`, `strcat`, `strncat` — with truncation reported as an error rather
|
||||||
|
than silently accepted, which is the whole value proposition here.
|
||||||
|
- [ ] `strdup`, `strndup`
|
||||||
|
- [ ] `strcmp`, `strncmp`, `strcasecmp`, `strncasecmp`, `strcoll`
|
||||||
|
- [ ] `strchr`, `strrchr`, `strstr`, `strcasestr`, `strpbrk`, `strspn`, `strcspn`
|
||||||
|
- [ ] `strtok_r`, `strsep`
|
||||||
|
- [ ] `strerror_r`
|
||||||
|
|
||||||
|
**Stream I/O (`stdio.h`)**
|
||||||
|
- [ ] `fseek`, `ftell`, `rewind`, `fgetpos`, `fsetpos`, `fseeko`, `ftello`
|
||||||
|
- [ ] `fflush`
|
||||||
|
- [ ] `fgets`, `fputs`, `fgetc`/`getc`/`getchar`, `fputc`/`putc`/`putchar`, `ungetc`
|
||||||
|
- [ ] `getline`, `getdelim`
|
||||||
|
- [ ] `freopen`, `fdopen`, `fileno`
|
||||||
|
- [ ] `setvbuf`, `setbuf`
|
||||||
|
- [ ] `clearerr`, `feof`, `ferror` — thin, but worth exposing so callers never touch raw
|
||||||
|
`FILE *` internals.
|
||||||
|
- [ ] `sscanf`, `fscanf`, `scanf` — the return-value semantics (items matched vs `EOF`) are a
|
||||||
|
classic silent-failure source.
|
||||||
|
- [ ] `remove`, `rename`, `tmpfile`, `mkstemp`, `mkdtemp`
|
||||||
|
- [ ] `perror` — or an akerror-native equivalent.
|
||||||
|
|
||||||
|
### 3.2 POSIX file and process API (likely the next major surface)
|
||||||
|
|
||||||
|
**`unistd.h` / `fcntl.h`**
|
||||||
|
- [ ] `open`, `close`, `read`, `write`, `pread`, `pwrite`, `lseek`
|
||||||
|
- [ ] `readv`, `writev`
|
||||||
|
- [ ] `dup`, `dup2`, `pipe`, `fcntl`
|
||||||
|
- [ ] `fsync`, `fdatasync`, `truncate`, `ftruncate`
|
||||||
|
- [ ] `unlink`, `link`, `symlink`, `readlink`, `rmdir`, `mkdir`
|
||||||
|
- [ ] `access`, `faccessat`, `chmod`, `fchmod`, `chown`, `fchown`, `umask`
|
||||||
|
- [ ] `chdir`, `fchdir`, `getcwd`
|
||||||
|
- [ ] `isatty`, `ttyname_r`
|
||||||
|
- [ ] `sysconf`, `pathconf`
|
||||||
|
- [ ] `sleep`, `usleep`, `nanosleep`
|
||||||
|
|
||||||
|
**`sys/stat.h`**
|
||||||
|
- [ ] `stat`, `fstat`, `lstat`, `fstatat`
|
||||||
|
- [ ] `statvfs`, `fstatvfs`
|
||||||
|
|
||||||
|
**`dirent.h`**
|
||||||
|
- [ ] `opendir`, `fdopendir`, `readdir`, `readdir_r`, `closedir`, `rewinddir`, `scandir`
|
||||||
|
|
||||||
|
**Process control**
|
||||||
|
- [ ] `fork`, `execve` / `execvp` / `execl` family, `waitpid`, `wait`
|
||||||
|
- [ ] `posix_spawn`
|
||||||
|
- [ ] `system`, `popen`, `pclose`
|
||||||
|
- [ ] `getpid`, `getppid`, `getuid`, `geteuid`, `setuid`, `setgid`
|
||||||
|
- [ ] `atexit`, `exit`, `_exit`, `abort` — mostly to give akerror a hook on shutdown.
|
||||||
|
- [ ] `getenv`, `setenv`, `unsetenv`, `putenv`, `clearenv`
|
||||||
|
|
||||||
|
**`sys/mman.h`**
|
||||||
|
- [ ] `mmap`, `munmap`, `mprotect`, `msync`, `madvise`
|
||||||
|
|
||||||
|
### 3.3 Time
|
||||||
|
|
||||||
|
- [ ] `time`, `clock_gettime`, `clock_getres`, `gettimeofday`
|
||||||
|
- [ ] `localtime_r`, `gmtime_r`, `mktime`, `timegm`, `difftime`
|
||||||
|
- [ ] `strftime`, `strptime`
|
||||||
|
- [ ] `clock`, `times`
|
||||||
|
|
||||||
|
### 3.4 Sorting, searching and misc `stdlib.h`
|
||||||
|
|
||||||
|
- [ ] `qsort`, `qsort_r`, `bsearch` — comparator errors currently have nowhere to go; an
|
||||||
|
akerror-aware comparator signature would be a genuine improvement.
|
||||||
|
- [ ] `abs`, `labs`, `llabs`, `div`, `ldiv`, `lldiv`
|
||||||
|
- [ ] `rand`, `srand`, `random`, `srandom`, `getrandom`/`arc4random`
|
||||||
|
|
||||||
|
### 3.5 Lower priority / larger projects
|
||||||
|
|
||||||
|
- [ ] **Sockets**: `socket`, `bind`, `listen`, `accept`, `connect`, `send`/`sendto`/`sendmsg`,
|
||||||
|
`recv`/`recvfrom`/`recvmsg`, `shutdown`, `setsockopt`/`getsockopt`, `getaddrinfo`/
|
||||||
|
`freeaddrinfo`/`gai_strerror`, `inet_ntop`/`inet_pton`
|
||||||
|
- [ ] **Multiplexing**: `select`, `poll`, `ppoll`, `epoll_create1`/`epoll_ctl`/`epoll_wait`
|
||||||
|
- [ ] **Signals**: `sigaction`, `sigprocmask`, `sigemptyset`/`sigaddset`, `kill`, `raise`,
|
||||||
|
`signalfd`
|
||||||
|
- [ ] **Threads**: `pthread_create`/`join`/`detach`, `pthread_mutex_*`, `pthread_cond_*`,
|
||||||
|
`pthread_rwlock_*`, `sem_*` — blocked on resolving the thread-safety question in §1.9
|
||||||
|
(libakerror's error pool is an unlocked process-global array).
|
||||||
|
- [ ] **Dynamic loading**: `dlopen`, `dlsym`, `dlclose`, `dlerror`
|
||||||
|
- [ ] **Locale / wide chars**: `setlocale`, `mbstowcs`, `wcstombs`, `iconv_*`
|
||||||
|
- [ ] **Math**: the `math.h` functions that set `errno`/raise FP exceptions (`sqrt`, `log`,
|
||||||
|
`pow`, `acos`, …) — probably better served by a dedicated `libakmath`.
|
||||||
|
|
||||||
|
### 3.6 Non-libc additions the current data structures imply
|
||||||
|
|
||||||
|
Not libc wrappers, but the existing list/tree API is visibly incomplete:
|
||||||
|
|
||||||
|
- [ ] List: `prepend`, `insert_after`/`insert_before`, `length`, `find`, `reverse`,
|
||||||
|
`concat`, `free_all`, reverse iteration, and a head/tail-tracking container type so
|
||||||
|
`append` is O(1) instead of O(n).
|
||||||
|
- [ ] Tree: `insert`, `remove`, `find`, `height`, `count`, `free_all`, and the BFS traversal
|
||||||
|
the `lalloc`/`lfree`/`queue` parameters were designed for.
|
||||||
|
- [ ] Hash map built on `aksl_strhash_djb2`, plus additional hashes (FNV-1a, xxHash) and a
|
||||||
|
NUL-terminated `aksl_strhash_djb2_str` convenience form.
|
||||||
|
- [ ] Growable buffer / string-builder type, to make the `snprintf` and `strcat` wrappers
|
||||||
|
pleasant to use.
|
||||||
436
scripts/mutation_test.py
Executable file
436
scripts/mutation_test.py
Executable file
@@ -0,0 +1,436 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Mutation testing harness for libakstdlib.
|
||||||
|
|
||||||
|
Mutation testing measures how good the test suite is at catching bugs. It works
|
||||||
|
by making many small, deliberate breakages ("mutants") to the library source --
|
||||||
|
flipping a comparison, deleting a statement, swapping true/false -- and then
|
||||||
|
running the whole CTest suite against each one. If the tests fail, the mutant is
|
||||||
|
"killed" (good: the tests noticed the bug). If the tests still pass, the mutant
|
||||||
|
"survived" (bad: a real bug of that shape would slip through unnoticed).
|
||||||
|
|
||||||
|
The mutation score is killed / (killed + survived). Surviving mutants are printed
|
||||||
|
with file:line and the exact change so they can be turned into new test cases.
|
||||||
|
|
||||||
|
This harness has no third-party dependencies (Python stdlib + the project's
|
||||||
|
normal cmake/ctest toolchain). It never mutates the real working tree: it copies
|
||||||
|
the repo to a scratch directory and mutates there.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
scripts/mutation_test.py [options]
|
||||||
|
|
||||||
|
--source-root DIR repo root to copy (default: parent of this script's dir)
|
||||||
|
--target FILE source file to mutate, relative to root; repeatable.
|
||||||
|
Default: src/stdlib.c and include/akstdlib.h
|
||||||
|
--work DIR scratch dir for the mutated copy (default: a temp dir)
|
||||||
|
--timeout SECONDS per-suite ctest timeout (default: 120)
|
||||||
|
--threshold PCT exit non-zero if mutation score < PCT (default: 0 = off)
|
||||||
|
--list only list the mutants that would be run, then exit
|
||||||
|
--keep keep the scratch working copy on exit (for debugging)
|
||||||
|
-j N (reserved) currently runs sequentially
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Mutation operators
|
||||||
|
#
|
||||||
|
# Each operator yields zero or more (start, end, replacement) edits for a single
|
||||||
|
# line of source. The driver applies exactly one edit per mutant so every mutant
|
||||||
|
# differs from the original by one localized change.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
# Relational operator replacement: map each operator to the alternatives that
|
||||||
|
# meaningfully change behaviour (not merely the strict negation).
|
||||||
|
_REL = {
|
||||||
|
"==": ["!="],
|
||||||
|
"!=": ["=="],
|
||||||
|
"<=": ["<", "=="],
|
||||||
|
">=": [">", "=="],
|
||||||
|
"<": ["<=", ">"],
|
||||||
|
">": [">=", "<"],
|
||||||
|
}
|
||||||
|
# Match a relational operator that is NOT part of ->, <<, >>, =>, <=, >=, ==, !=
|
||||||
|
# unless we intend it. We tokenize the two-char operators first, then single.
|
||||||
|
_REL_TWO = re.compile(r"(==|!=|<=|>=)")
|
||||||
|
_REL_ONE = re.compile(r"(?<![-<>=!+])([<>])(?![=<>])")
|
||||||
|
|
||||||
|
_LOGICAL = {"&&": "||", "||": "&&"}
|
||||||
|
_LOG_RE = re.compile(r"(&&|\|\|)")
|
||||||
|
|
||||||
|
_BOOL = {"true": "false", "false": "true"}
|
||||||
|
_BOOL_RE = re.compile(r"\b(true|false)\b")
|
||||||
|
|
||||||
|
# Arithmetic / compound-assignment on whitespace-delimited operands only, to
|
||||||
|
# avoid touching ++, --, ->, unary signs, or pointer/format punctuation.
|
||||||
|
_ARITH_RE = re.compile(r"(?<=\s)([+\-])(?=\s)")
|
||||||
|
_ARITH = {"+": "-", "-": "+"}
|
||||||
|
_COMPOUND_RE = re.compile(r"(\+=|-=)")
|
||||||
|
_COMPOUND = {"+=": "-=", "-=": "+="}
|
||||||
|
|
||||||
|
# Integer literal replacement: 0 <-> 1 (word-bounded, not inside identifiers or
|
||||||
|
# larger numbers, not a float).
|
||||||
|
_INT_RE = re.compile(r"(?<![\w.])([01])(?![\w.])")
|
||||||
|
_INT = {"0": "1", "1": "0"}
|
||||||
|
|
||||||
|
|
||||||
|
def _op_edits(line):
|
||||||
|
"""Yield (tag, start, end, replacement) for every candidate mutation."""
|
||||||
|
# Relational (two-char first so we don't split them with the one-char pass)
|
||||||
|
for m in _REL_TWO.finditer(line):
|
||||||
|
for alt in _REL[m.group(1)]:
|
||||||
|
yield ("ROR", m.start(1), m.end(1), alt)
|
||||||
|
for m in _REL_ONE.finditer(line):
|
||||||
|
for alt in _REL[m.group(1)]:
|
||||||
|
yield ("ROR", m.start(1), m.end(1), alt)
|
||||||
|
for m in _LOG_RE.finditer(line):
|
||||||
|
yield ("LCR", m.start(1), m.end(1), _LOGICAL[m.group(1)])
|
||||||
|
for m in _BOOL_RE.finditer(line):
|
||||||
|
yield ("BCR", m.start(1), m.end(1), _BOOL[m.group(1)])
|
||||||
|
for m in _COMPOUND_RE.finditer(line):
|
||||||
|
yield ("AOR", m.start(1), m.end(1), _COMPOUND[m.group(1)])
|
||||||
|
for m in _ARITH_RE.finditer(line):
|
||||||
|
yield ("AOR", m.start(1), m.end(1), _ARITH[m.group(1)])
|
||||||
|
for m in _INT_RE.finditer(line):
|
||||||
|
yield ("ICR", m.start(1), m.end(1), _INT[m.group(1)])
|
||||||
|
|
||||||
|
|
||||||
|
# Statement-deletion: neutralize a whole statement. We only delete statements
|
||||||
|
# that are safe to drop without guaranteeing a compile error, so a surviving
|
||||||
|
# deletion is a genuine test gap rather than compiler noise.
|
||||||
|
_STMT_DELETABLE = re.compile(
|
||||||
|
r"""^\s*(
|
||||||
|
break |
|
||||||
|
return\b[^;]* |
|
||||||
|
[A-Za-z_][-\w>().\[\]* ]*\s*=\s*[^;]* | # assignments
|
||||||
|
[A-Za-z_][\w]*\s*\([^;]*\) # bare function calls
|
||||||
|
)\s*;\s*(\\?)\s*$""",
|
||||||
|
re.VERBOSE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Deciding which lines are eligible to mutate
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
# Skip preprocessor control and the block of constant/error-code #defines in the
|
||||||
|
# template header: mutating buffer sizes or renumbering error codes produces
|
||||||
|
# equivalent or uninteresting mutants that swamp the signal.
|
||||||
|
_SKIP_LINE = re.compile(
|
||||||
|
r"""^\s*(
|
||||||
|
\#\s*(include|ifn?def|ifdef|if|elif|else|endif|error|pragma|undef) |
|
||||||
|
\#\s*define\s+AKERR_(MAX|LAST|NULLPOINTER|OUTOFBOUNDS|API|ATTRIBUTE|
|
||||||
|
TYPE|KEY|INDEX|FORMAT|IO|VALUE|RELATIONSHIP|EOF|CIRCULAR_REFERENCE|
|
||||||
|
ITERATOR_BREAK|NOT_IMPLEMENTED|BADEXC|NOIGNORE|USE_STDLIB)\b |
|
||||||
|
\* | // # comment bodies / line comments
|
||||||
|
)""",
|
||||||
|
re.VERBOSE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_comment_or_blank(line):
|
||||||
|
s = line.strip()
|
||||||
|
return (not s) or s.startswith("//") or s.startswith("/*") or s.startswith("*")
|
||||||
|
|
||||||
|
|
||||||
|
def eligible(line):
|
||||||
|
if _is_comment_or_blank(line):
|
||||||
|
return False
|
||||||
|
if _SKIP_LINE.match(line):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class Mutant:
|
||||||
|
__slots__ = ("path", "lineno", "op", "before", "after", "col")
|
||||||
|
|
||||||
|
def __init__(self, path, lineno, op, before, after, col):
|
||||||
|
self.path = path
|
||||||
|
self.lineno = lineno
|
||||||
|
self.op = op
|
||||||
|
self.before = before
|
||||||
|
self.after = after
|
||||||
|
self.col = col
|
||||||
|
|
||||||
|
def describe(self):
|
||||||
|
return (f"{self.path}:{self.lineno} [{self.op}] "
|
||||||
|
f"col{self.col}: {self.before.strip()} -> {self.after.strip()}")
|
||||||
|
|
||||||
|
|
||||||
|
def generate_mutants(root, rel_target):
|
||||||
|
"""Enumerate all mutants for one target file."""
|
||||||
|
abspath = os.path.join(root, rel_target)
|
||||||
|
with open(abspath, "r") as fh:
|
||||||
|
lines = fh.readlines()
|
||||||
|
|
||||||
|
mutants = []
|
||||||
|
for i, line in enumerate(lines, start=1):
|
||||||
|
if not eligible(line):
|
||||||
|
continue
|
||||||
|
# substitution operators
|
||||||
|
seen = set()
|
||||||
|
for tag, s, e, repl in _op_edits(line):
|
||||||
|
key = (s, e, repl)
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
mutated = line[:s] + repl + line[e:]
|
||||||
|
if mutated == line:
|
||||||
|
continue
|
||||||
|
mutants.append(Mutant(rel_target, i, tag, line, mutated, s))
|
||||||
|
# statement deletion
|
||||||
|
m = _STMT_DELETABLE.match(line)
|
||||||
|
if m:
|
||||||
|
indent = line[: len(line) - len(line.lstrip())]
|
||||||
|
cont = "\\" if line.rstrip().endswith("\\") else ""
|
||||||
|
deleted = f"{indent}/* mutant: deleted */ {cont}\n" if cont else f"{indent};\n"
|
||||||
|
mutants.append(Mutant(rel_target, i, "SDL", line, deleted, 0))
|
||||||
|
return mutants
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Build / test orchestration against a scratch copy
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
class Runner:
|
||||||
|
def __init__(self, work, timeout):
|
||||||
|
self.work = work
|
||||||
|
self.build = os.path.join(work, "build")
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
|
def _run(self, cmd, timeout=None):
|
||||||
|
return subprocess.run(
|
||||||
|
cmd, cwd=self.work, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
def configure(self):
|
||||||
|
r = self._run(["cmake", "-S", ".", "-B", "build"], timeout=self.timeout)
|
||||||
|
return r.returncode == 0, r.stdout
|
||||||
|
|
||||||
|
def build_and_test(self):
|
||||||
|
"""Return ('killed-compile' | 'killed-test' | 'killed-timeout' | 'survived')."""
|
||||||
|
try:
|
||||||
|
b = self._run(["cmake", "--build", "build"], timeout=self.timeout)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return "killed-timeout"
|
||||||
|
if b.returncode != 0:
|
||||||
|
return "killed-compile"
|
||||||
|
try:
|
||||||
|
t = subprocess.run(
|
||||||
|
["ctest", "--test-dir", "build", "--output-on-failure",
|
||||||
|
"--stop-on-failure"],
|
||||||
|
cwd=self.work, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||||
|
timeout=self.timeout,
|
||||||
|
)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return "killed-timeout"
|
||||||
|
return "survived" if t.returncode == 0 else "killed-test"
|
||||||
|
|
||||||
|
|
||||||
|
def _xml_escape(s):
|
||||||
|
return (s.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||||
|
.replace('"', """))
|
||||||
|
|
||||||
|
|
||||||
|
def write_junit(path, records, targets):
|
||||||
|
"""Write a JUnit XML report. One <testcase> per mutant; a surviving mutant
|
||||||
|
is a <failure> (test-suite gap), a killed mutant is a passing case."""
|
||||||
|
by_file = {t: [] for t in targets}
|
||||||
|
for m, result in records:
|
||||||
|
by_file.setdefault(m.path, []).append((m, result))
|
||||||
|
|
||||||
|
total = len(records)
|
||||||
|
total_fail = sum(1 for _, r in records if r == "survived")
|
||||||
|
out = ['<?xml version="1.0" encoding="UTF-8"?>',
|
||||||
|
f'<testsuites name="mutation" tests="{total}" failures="{total_fail}">']
|
||||||
|
for f, items in by_file.items():
|
||||||
|
if not items:
|
||||||
|
continue
|
||||||
|
fails = sum(1 for _, r in items if r == "survived")
|
||||||
|
out.append(f' <testsuite name="mutation:{_xml_escape(f)}" '
|
||||||
|
f'tests="{len(items)}" failures="{fails}">')
|
||||||
|
for m, result in items:
|
||||||
|
name = _xml_escape(m.describe())
|
||||||
|
cls = "mutation." + _xml_escape(m.path)
|
||||||
|
if result == "survived":
|
||||||
|
detail = _xml_escape(f"{m.before.strip()} -> {m.after.strip()}")
|
||||||
|
out.append(f' <testcase name="{name}" classname="{cls}" time="0">')
|
||||||
|
out.append(f' <failure message="survived mutant '
|
||||||
|
f'({_xml_escape(m.op)} at {_xml_escape(m.path)}:'
|
||||||
|
f'{m.lineno})">{detail}</failure>')
|
||||||
|
out.append(' </testcase>')
|
||||||
|
else:
|
||||||
|
out.append(f' <testcase name="{name}" classname="{cls}" '
|
||||||
|
f'time="0"><system-out>{_xml_escape(result)}'
|
||||||
|
'</system-out></testcase>')
|
||||||
|
out.append(' </testsuite>')
|
||||||
|
out.append('</testsuites>')
|
||||||
|
with open(path, "w") as fh:
|
||||||
|
fh.write("\n".join(out) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def copy_tree(src, dst):
|
||||||
|
ignore = shutil.ignore_patterns("build", ".git", "*.o", "*.so", "*~",
|
||||||
|
"#*#", "*.iso", "*.png")
|
||||||
|
shutil.copytree(src, dst, ignore=ignore, symlinks=True)
|
||||||
|
|
||||||
|
|
||||||
|
def read_lines(path):
|
||||||
|
with open(path) as fh:
|
||||||
|
return fh.readlines()
|
||||||
|
|
||||||
|
|
||||||
|
def write_lines(path, lines):
|
||||||
|
with open(path, "w") as fh:
|
||||||
|
fh.writelines(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Line-buffer stdout so progress is visible live under CI / the cmake target.
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(line_buffering=True)
|
||||||
|
except (AttributeError, ValueError):
|
||||||
|
pass
|
||||||
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
default_root = os.path.dirname(here)
|
||||||
|
|
||||||
|
ap = argparse.ArgumentParser(description="Mutation testing for libakstdlib")
|
||||||
|
ap.add_argument("--source-root", default=default_root)
|
||||||
|
ap.add_argument("--target", action="append", default=None)
|
||||||
|
ap.add_argument("--work", default=None)
|
||||||
|
ap.add_argument("--timeout", type=int, default=120)
|
||||||
|
ap.add_argument("--threshold", type=float, default=0.0)
|
||||||
|
ap.add_argument("--junit", default=None,
|
||||||
|
help="write a JUnit XML report to this path")
|
||||||
|
ap.add_argument("--max-mutants", type=int, default=0,
|
||||||
|
help="cap the run at N evenly-sampled mutants (0 = all)")
|
||||||
|
ap.add_argument("--list", action="store_true")
|
||||||
|
ap.add_argument("--keep", action="store_true")
|
||||||
|
ap.add_argument("-j", type=int, default=1)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
root = os.path.abspath(args.source_root)
|
||||||
|
targets = args.target or ["src/stdlib.c", "include/akstdlib.h"]
|
||||||
|
|
||||||
|
# Enumerate mutants from the pristine sources.
|
||||||
|
all_mutants = []
|
||||||
|
for t in targets:
|
||||||
|
all_mutants.extend(generate_mutants(root, t))
|
||||||
|
|
||||||
|
print(f"Generated {len(all_mutants)} mutants across {len(targets)} file(s):")
|
||||||
|
for t in targets:
|
||||||
|
n = sum(1 for m in all_mutants if m.path == t)
|
||||||
|
print(f" {t}: {n}")
|
||||||
|
|
||||||
|
# Optional even-strided sampling to bound run time (CI / smoke tests).
|
||||||
|
if args.max_mutants and len(all_mutants) > args.max_mutants:
|
||||||
|
step = len(all_mutants) / args.max_mutants
|
||||||
|
sampled = [all_mutants[int(i * step)] for i in range(args.max_mutants)]
|
||||||
|
print(f"Sampling {len(sampled)} of {len(all_mutants)} mutants "
|
||||||
|
f"(--max-mutants {args.max_mutants}).")
|
||||||
|
all_mutants = sampled
|
||||||
|
|
||||||
|
if args.list:
|
||||||
|
for m in all_mutants:
|
||||||
|
print(" " + m.describe())
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if not all_mutants:
|
||||||
|
print("No mutants generated; nothing to do.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Scratch working copy.
|
||||||
|
work_parent = args.work or tempfile.mkdtemp(prefix="akerr_mut_")
|
||||||
|
work = os.path.join(work_parent, "src") if args.work else work_parent
|
||||||
|
if os.path.exists(work):
|
||||||
|
shutil.rmtree(work)
|
||||||
|
print(f"\nCopying sources to scratch dir: {work}")
|
||||||
|
copy_tree(root, work)
|
||||||
|
|
||||||
|
runner = Runner(work, args.timeout)
|
||||||
|
|
||||||
|
print("Configuring baseline ...")
|
||||||
|
ok, out = runner.configure()
|
||||||
|
if not ok:
|
||||||
|
sys.stderr.write(out.decode(errors="replace"))
|
||||||
|
sys.stderr.write("\nBaseline configure FAILED; aborting.\n")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
print("Verifying baseline is green (no mutation) ...")
|
||||||
|
baseline = runner.build_and_test()
|
||||||
|
if baseline != "survived":
|
||||||
|
sys.stderr.write(f"Baseline is not green ({baseline}); aborting. "
|
||||||
|
"Fix the suite before mutation testing.\n")
|
||||||
|
return 2
|
||||||
|
print("Baseline OK.\n")
|
||||||
|
|
||||||
|
# Group mutants by file so we mutate one file at a time and restore it.
|
||||||
|
killed = {"killed-compile": 0, "killed-test": 0, "killed-timeout": 0}
|
||||||
|
survivors = []
|
||||||
|
records = []
|
||||||
|
total = len(all_mutants)
|
||||||
|
|
||||||
|
# Cache pristine contents per target.
|
||||||
|
pristine = {t: read_lines(os.path.join(work, t)) for t in targets}
|
||||||
|
|
||||||
|
for idx, m in enumerate(all_mutants, start=1):
|
||||||
|
tgt_abs = os.path.join(work, m.path)
|
||||||
|
lines = list(pristine[m.path])
|
||||||
|
lines[m.lineno - 1] = m.after
|
||||||
|
write_lines(tgt_abs, lines)
|
||||||
|
try:
|
||||||
|
result = runner.build_and_test()
|
||||||
|
finally:
|
||||||
|
write_lines(tgt_abs, pristine[m.path]) # always restore
|
||||||
|
|
||||||
|
records.append((m, result))
|
||||||
|
if result == "survived":
|
||||||
|
survivors.append(m)
|
||||||
|
mark = "SURVIVED"
|
||||||
|
else:
|
||||||
|
killed[result] += 1
|
||||||
|
mark = result.upper()
|
||||||
|
print(f"[{idx}/{total}] {mark:16} {m.describe()}")
|
||||||
|
|
||||||
|
total_killed = sum(killed.values())
|
||||||
|
score = 100.0 * total_killed / total if total else 100.0
|
||||||
|
|
||||||
|
print("\n" + "=" * 72)
|
||||||
|
print("MUTATION TESTING SUMMARY")
|
||||||
|
print("=" * 72)
|
||||||
|
print(f" total mutants : {total}")
|
||||||
|
print(f" killed (test) : {killed['killed-test']}")
|
||||||
|
print(f" killed (compile): {killed['killed-compile']}")
|
||||||
|
print(f" killed (timeout): {killed['killed-timeout']}")
|
||||||
|
print(f" survived : {len(survivors)}")
|
||||||
|
print(f" mutation score : {score:.1f}%")
|
||||||
|
if survivors:
|
||||||
|
print("\nSurviving mutants (test-suite gaps -- turn these into tests):")
|
||||||
|
for m in survivors:
|
||||||
|
print(" " + m.describe())
|
||||||
|
|
||||||
|
if args.junit:
|
||||||
|
junit_path = os.path.abspath(args.junit)
|
||||||
|
write_junit(junit_path, records, targets)
|
||||||
|
print(f"\nJUnit report written to: {junit_path}")
|
||||||
|
|
||||||
|
if not args.keep and not args.work:
|
||||||
|
shutil.rmtree(work_parent, ignore_errors=True)
|
||||||
|
else:
|
||||||
|
print(f"\nScratch working copy kept at: {work}")
|
||||||
|
|
||||||
|
if args.threshold > 0 and score < args.threshold:
|
||||||
|
print(f"\nFAIL: mutation score {score:.1f}% < threshold {args.threshold:.1f}%")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
216
tests/aksl_capture.h
Normal file
216
tests/aksl_capture.h
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
#ifndef AKSL_TEST_CAPTURE_H
|
||||||
|
#define AKSL_TEST_CAPTURE_H
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Shared test helpers for libakstdlib.
|
||||||
|
*
|
||||||
|
* Modelled on libakerror's tests/err_capture.h, with additions for the shape of
|
||||||
|
* this library: almost every akstdlib entry point returns an
|
||||||
|
* akerr_ErrorContext * that the caller owns and must release, so the common
|
||||||
|
* assertion is "this call returned status X" rather than "this call logged Y".
|
||||||
|
*
|
||||||
|
* What is here:
|
||||||
|
*
|
||||||
|
* AKSL_CHECK() an NDEBUG-proof assertion. Fails the test by
|
||||||
|
* returning 1 from the enclosing function (unlike
|
||||||
|
* assert(), which is compiled out in release builds
|
||||||
|
* and would silently turn a test into a no-op).
|
||||||
|
* 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_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
|
||||||
|
* assert on the *content* of stack traces and
|
||||||
|
* unhandled-error output.
|
||||||
|
* aksl_slots_in_use() how many slots are currently checked out of
|
||||||
|
* AKERR_ARRAY_ERROR, for pool-leak assertions.
|
||||||
|
* AKSL_RUN() run one test function and tally the result.
|
||||||
|
*
|
||||||
|
* Tests are written as a set of `static int test_xxx(void)` functions that
|
||||||
|
* return 0 on success and non-zero on failure, driven from main() by AKSL_RUN.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <akstdlib.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Log capture */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
#define AKSL_CAPTURE_BUFSZ 65536
|
||||||
|
static char aksl_capture_buf[AKSL_CAPTURE_BUFSZ];
|
||||||
|
static size_t aksl_capture_len = 0;
|
||||||
|
|
||||||
|
static void __attribute__((unused)) aksl_capture_logger(const char *fmt, ...)
|
||||||
|
{
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, fmt);
|
||||||
|
int n = vsnprintf(aksl_capture_buf + aksl_capture_len,
|
||||||
|
AKSL_CAPTURE_BUFSZ - aksl_capture_len, fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
if ( n > 0 ) {
|
||||||
|
aksl_capture_len += (size_t)n;
|
||||||
|
if ( aksl_capture_len >= AKSL_CAPTURE_BUFSZ ) {
|
||||||
|
aksl_capture_len = AKSL_CAPTURE_BUFSZ - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __attribute__((unused)) aksl_capture_reset(void)
|
||||||
|
{
|
||||||
|
aksl_capture_len = 0;
|
||||||
|
aksl_capture_buf[0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Install the capturing logger. akerr_init() only assigns a default logger when
|
||||||
|
* akerr_log_method is NULL, and it is idempotent, so calling this either before
|
||||||
|
* or after the first PREPARE_ERROR keeps our logger in place.
|
||||||
|
*/
|
||||||
|
static void __attribute__((unused)) aksl_capture_install(void)
|
||||||
|
{
|
||||||
|
aksl_capture_reset();
|
||||||
|
akerr_log_method = &aksl_capture_logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Error pool accounting */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* Count array slots currently checked out of the pool (refcount != 0). */
|
||||||
|
static int __attribute__((unused)) aksl_slots_in_use(void)
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
||||||
|
if ( AKERR_ARRAY_ERROR[i].refcount != 0 ) {
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Taking ownership of a returned error context */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int aksl_last_status = 0;
|
||||||
|
static char aksl_last_message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH];
|
||||||
|
/* Sized to match akerr_ErrorContext.function, which akerror declares with
|
||||||
|
* AKERR_MAX_ERROR_FNAME_LENGTH rather than AKERR_MAX_ERROR_FUNCTION_LENGTH. */
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
|
||||||
|
{
|
||||||
|
akerr_ErrorContext *released = NULL;
|
||||||
|
|
||||||
|
aksl_last_message[0] = '\0';
|
||||||
|
aksl_last_function[0] = '\0';
|
||||||
|
if ( e == NULL ) {
|
||||||
|
aksl_last_status = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
aksl_last_status = e->status;
|
||||||
|
snprintf(aksl_last_message, sizeof(aksl_last_message), "%s", e->message);
|
||||||
|
snprintf(aksl_last_function, sizeof(aksl_last_function), "%s", e->function);
|
||||||
|
/*
|
||||||
|
* akerr_release_error is marked warn_unused_result, and a (void) cast does
|
||||||
|
* not silence that in GCC, so the result is assigned and discarded.
|
||||||
|
*/
|
||||||
|
released = akerr_release_error(e);
|
||||||
|
(void)released;
|
||||||
|
return aksl_last_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Assertions */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
#define AKSL_CHECK(cond) \
|
||||||
|
do { \
|
||||||
|
if ( !(cond) ) { \
|
||||||
|
fprintf(stderr, " CHECK FAILED: %s at %s:%d\n", \
|
||||||
|
#cond, __FILE__, __LINE__); \
|
||||||
|
return 1; \
|
||||||
|
} \
|
||||||
|
} while ( 0 )
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run an akerr_ErrorContext *-returning expression and assert on its status.
|
||||||
|
* The context is always released, including on the failure path, so a failing
|
||||||
|
* assertion does not also corrupt the pool-leak checks that follow it.
|
||||||
|
*/
|
||||||
|
#define AKSL_CHECK_STATUS(__expr, __expected) \
|
||||||
|
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; \
|
||||||
|
} \
|
||||||
|
} while ( 0 )
|
||||||
|
|
||||||
|
#define AKSL_CHECK_OK(__expr) AKSL_CHECK_STATUS(__expr, 0)
|
||||||
|
|
||||||
|
#define AKSL_CHECK_CONTAINS(needle) \
|
||||||
|
AKSL_CHECK(strstr(aksl_capture_buf, (needle)) != NULL)
|
||||||
|
|
||||||
|
#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)
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Test driver */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run one test function, report it, and tally failures. Also asserts that the
|
||||||
|
* test left the error pool as it found it -- a wrapper that fails to release a
|
||||||
|
* context is a bug in the library, not just in the test.
|
||||||
|
*/
|
||||||
|
#define AKSL_RUN(__failures, __fn) \
|
||||||
|
do { \
|
||||||
|
int __before = aksl_slots_in_use(); \
|
||||||
|
int __r = __fn(); \
|
||||||
|
int __after = aksl_slots_in_use(); \
|
||||||
|
if ( __r != 0 ) { \
|
||||||
|
fprintf(stderr, "FAIL %s\n", #__fn); \
|
||||||
|
(__failures)++; \
|
||||||
|
} else if ( __after != __before ) { \
|
||||||
|
fprintf(stderr, \
|
||||||
|
"FAIL %s (leaked %d error pool slot(s))\n", \
|
||||||
|
#__fn, __after - __before); \
|
||||||
|
(__failures)++; \
|
||||||
|
} else { \
|
||||||
|
fprintf(stderr, "ok %s\n", #__fn); \
|
||||||
|
} \
|
||||||
|
} while ( 0 )
|
||||||
|
|
||||||
|
#define AKSL_REPORT(__failures) \
|
||||||
|
do { \
|
||||||
|
fprintf(stderr, "%s: %d failure(s)\n", __FILE__, (__failures)); \
|
||||||
|
return (__failures) == 0 ? 0 : 1; \
|
||||||
|
} while ( 0 )
|
||||||
|
|
||||||
|
#endif // AKSL_TEST_CAPTURE_H
|
||||||
@@ -1,75 +1,319 @@
|
|||||||
#include <stdio.h>
|
/*
|
||||||
#include <akstdlib.h>
|
* Linked-list behaviour that the library gets right today.
|
||||||
|
*
|
||||||
|
* The two confirmed list defects (TODO.md 2.1.1 aksl_list_append truncating the
|
||||||
|
* chain, and 2.1.2 aksl_list_iterate skipping the head) are asserted separately
|
||||||
|
* in tests/test_list_append_chain.c and tests/test_list_iterate_head.c, which
|
||||||
|
* are registered as known-failing. Everything in this file must pass.
|
||||||
|
*
|
||||||
|
* Note that the list-shape assertions here build their lists by hand rather
|
||||||
|
* than with aksl_list_append: append cannot be trusted to produce a chain
|
||||||
|
* longer than two nodes until 2.1.1 is fixed, and using it would make these
|
||||||
|
* tests fail for a reason that has nothing to do with what they are checking.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
// This iterator does nothing but print the node names it is visiting
|
#define MAX_VISITS 16
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *myiter(aksl_ListNode *node, void *data)
|
|
||||||
|
typedef struct VisitLog
|
||||||
{
|
{
|
||||||
int count = 0;
|
int count;
|
||||||
|
aksl_ListNode *seen[MAX_VISITS];
|
||||||
|
int break_at; /* visit index to raise ITERATOR_BREAK on, or -1 */
|
||||||
|
int fail_at; /* visit index to raise AKERR_VALUE on, or -1 */
|
||||||
|
} VisitLog;
|
||||||
|
|
||||||
|
static void visitlog_init(VisitLog *log)
|
||||||
|
{
|
||||||
|
memset((void *)log, 0x00, sizeof(VisitLog));
|
||||||
|
log->break_at = -1;
|
||||||
|
log->fail_at = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext AKERR_NOIGNORE *record_visit(aksl_ListNode *node, void *data)
|
||||||
|
{
|
||||||
|
VisitLog *log = NULL;
|
||||||
|
int idx = 0;
|
||||||
|
|
||||||
PREPARE_ERROR(e);
|
PREPARE_ERROR(e);
|
||||||
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
|
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
|
||||||
FAIL_ZERO_RETURN(e, node->data, AKERR_NULLPOINTER, "node->data");
|
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
|
||||||
PASS(e, aksl_fprintf(&count, stderr, "Visiting node : %s\n", node->data));
|
log = (VisitLog *)data;
|
||||||
|
idx = log->count;
|
||||||
|
if ( idx < MAX_VISITS ) {
|
||||||
|
log->seen[idx] = node;
|
||||||
|
}
|
||||||
|
log->count += 1;
|
||||||
|
if ( log->fail_at == idx ) {
|
||||||
|
FAIL_RETURN(e, AKERR_VALUE, "iterator failed at visit %d", idx);
|
||||||
|
}
|
||||||
|
if ( log->break_at == idx ) {
|
||||||
|
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop at visit %d", idx);
|
||||||
|
}
|
||||||
SUCCEED_RETURN(e);
|
SUCCEED_RETURN(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This iterator function exits early once the index in `(int *)data` is reached
|
/* ---------------------------------------------------------------------- */
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *myiter_earlyhalt(aksl_ListNode *node, void *data)
|
/* aksl_list_append */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_append_single_node(void)
|
||||||
{
|
{
|
||||||
int *idx;
|
aksl_ListNode head;
|
||||||
int count = 0;
|
aksl_ListNode tail;
|
||||||
PREPARE_ERROR(e);
|
|
||||||
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
|
memset((void *)&head, 0x00, sizeof(head));
|
||||||
FAIL_ZERO_RETURN(e, node->data, AKERR_NULLPOINTER, "node->data");
|
memset((void *)&tail, 0x00, sizeof(tail));
|
||||||
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
|
|
||||||
idx = (int *)data;
|
AKSL_CHECK_OK(aksl_list_append(&head, &tail));
|
||||||
if ( *idx == 1 ) {
|
AKSL_CHECK(head.next == &tail);
|
||||||
// This exception is eaten by the iterator, we will never see it
|
AKSL_CHECK(head.prev == NULL);
|
||||||
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop");
|
AKSL_CHECK(tail.prev == &head);
|
||||||
}
|
AKSL_CHECK(tail.next == NULL);
|
||||||
*idx += 1;
|
return 0;
|
||||||
SUCCEED_RETURN(e);
|
}
|
||||||
|
|
||||||
|
static int test_append_null_arguments(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode node;
|
||||||
|
|
||||||
|
memset((void *)&node, 0x00, sizeof(node));
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_append(NULL, &node), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_append(&node, NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_append_detects_self_cycle(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode head;
|
||||||
|
aksl_ListNode node;
|
||||||
|
|
||||||
|
memset((void *)&head, 0x00, sizeof(head));
|
||||||
|
memset((void *)&node, 0x00, sizeof(node));
|
||||||
|
head.next = &head;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_append(&head, &node), AKERR_CIRCULAR_REFERENCE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_append_detects_two_node_cycle(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode a;
|
||||||
|
aksl_ListNode b;
|
||||||
|
aksl_ListNode node;
|
||||||
|
|
||||||
|
memset((void *)&a, 0x00, sizeof(a));
|
||||||
|
memset((void *)&b, 0x00, sizeof(b));
|
||||||
|
memset((void *)&node, 0x00, sizeof(node));
|
||||||
|
a.next = &b;
|
||||||
|
b.prev = &a;
|
||||||
|
b.next = &a;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_append(&a, &node), AKERR_CIRCULAR_REFERENCE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* aksl_list_iterate */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_iterate_null_arguments(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode node;
|
||||||
|
VisitLog log;
|
||||||
|
|
||||||
|
memset((void *)&node, 0x00, sizeof(node));
|
||||||
|
visitlog_init(&log);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_iterate(NULL, &record_visit, &log), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_iterate(&node, NULL, &log), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(log.count == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_iterate_single_node(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode node;
|
||||||
|
VisitLog log;
|
||||||
|
|
||||||
|
memset((void *)&node, 0x00, sizeof(node));
|
||||||
|
visitlog_init(&log);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_iterate(&node, &record_visit, &log));
|
||||||
|
AKSL_CHECK(log.count == 1);
|
||||||
|
AKSL_CHECK(log.seen[0] == &node);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_iterate_detects_self_cycle(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode head;
|
||||||
|
VisitLog log;
|
||||||
|
|
||||||
|
memset((void *)&head, 0x00, sizeof(head));
|
||||||
|
head.next = &head;
|
||||||
|
visitlog_init(&log);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_iterate(&head, &record_visit, &log),
|
||||||
|
AKERR_CIRCULAR_REFERENCE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_iterate_detects_two_node_cycle(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode a;
|
||||||
|
aksl_ListNode b;
|
||||||
|
VisitLog log;
|
||||||
|
|
||||||
|
memset((void *)&a, 0x00, sizeof(a));
|
||||||
|
memset((void *)&b, 0x00, sizeof(b));
|
||||||
|
a.next = &b;
|
||||||
|
b.prev = &a;
|
||||||
|
b.next = &a;
|
||||||
|
visitlog_init(&log);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_iterate(&a, &record_visit, &log),
|
||||||
|
AKERR_CIRCULAR_REFERENCE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* An error other than ITERATOR_BREAK must come back out of the iteration with
|
||||||
|
* its status and message intact. */
|
||||||
|
static int test_iterate_propagates_callback_error(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode node;
|
||||||
|
VisitLog log;
|
||||||
|
|
||||||
|
memset((void *)&node, 0x00, sizeof(node));
|
||||||
|
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(log.count == 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ITERATOR_BREAK is a control signal, not a failure: the caller sees success. */
|
||||||
|
static int test_iterate_break_is_not_an_error(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode node;
|
||||||
|
VisitLog log;
|
||||||
|
|
||||||
|
memset((void *)&node, 0x00, sizeof(node));
|
||||||
|
visitlog_init(&log);
|
||||||
|
log.break_at = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_iterate(&node, &record_visit, &log));
|
||||||
|
AKSL_CHECK(log.count == 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* aksl_list_pop */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_pop_middle_node(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode a;
|
||||||
|
aksl_ListNode b;
|
||||||
|
aksl_ListNode c;
|
||||||
|
|
||||||
|
memset((void *)&a, 0x00, sizeof(a));
|
||||||
|
memset((void *)&b, 0x00, sizeof(b));
|
||||||
|
memset((void *)&c, 0x00, sizeof(c));
|
||||||
|
a.next = &b;
|
||||||
|
b.prev = &a;
|
||||||
|
b.next = &c;
|
||||||
|
c.prev = &b;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_pop(&b));
|
||||||
|
AKSL_CHECK(a.next == &c);
|
||||||
|
AKSL_CHECK(c.prev == &a);
|
||||||
|
AKSL_CHECK(b.next == NULL);
|
||||||
|
AKSL_CHECK(b.prev == NULL);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_pop_head_node(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode a;
|
||||||
|
aksl_ListNode b;
|
||||||
|
|
||||||
|
memset((void *)&a, 0x00, sizeof(a));
|
||||||
|
memset((void *)&b, 0x00, sizeof(b));
|
||||||
|
a.next = &b;
|
||||||
|
b.prev = &a;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_pop(&a));
|
||||||
|
AKSL_CHECK(b.prev == NULL);
|
||||||
|
AKSL_CHECK(b.next == NULL);
|
||||||
|
AKSL_CHECK(a.next == NULL);
|
||||||
|
AKSL_CHECK(a.prev == NULL);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_pop_tail_node(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode a;
|
||||||
|
aksl_ListNode b;
|
||||||
|
|
||||||
|
memset((void *)&a, 0x00, sizeof(a));
|
||||||
|
memset((void *)&b, 0x00, sizeof(b));
|
||||||
|
a.next = &b;
|
||||||
|
b.prev = &a;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_pop(&b));
|
||||||
|
AKSL_CHECK(a.next == NULL);
|
||||||
|
AKSL_CHECK(a.prev == NULL);
|
||||||
|
AKSL_CHECK(b.next == NULL);
|
||||||
|
AKSL_CHECK(b.prev == NULL);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_pop_only_node(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode a;
|
||||||
|
|
||||||
|
memset((void *)&a, 0x00, sizeof(a));
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_pop(&a));
|
||||||
|
AKSL_CHECK(a.next == NULL);
|
||||||
|
AKSL_CHECK(a.prev == NULL);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_pop_null_argument(void)
|
||||||
|
{
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_pop(NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(e);
|
int failures = 0;
|
||||||
int idx = 0;
|
|
||||||
int count = 0;
|
|
||||||
aksl_ListNode mylist;
|
|
||||||
aksl_ListNode node1;
|
|
||||||
aksl_ListNode node2;
|
|
||||||
|
|
||||||
ATTEMPT {
|
akerr_init();
|
||||||
memset((void *)&mylist, 0x00, sizeof(aksl_ListNode));
|
|
||||||
memset((void *)&node1, 0x00, sizeof(aksl_ListNode));
|
|
||||||
memset((void *)&node2, 0x00, sizeof(aksl_ListNode));
|
|
||||||
|
|
||||||
mylist.data = "Root";
|
AKSL_RUN(failures, test_append_single_node);
|
||||||
|
AKSL_RUN(failures, test_append_null_arguments);
|
||||||
node1.data = "Node 1";
|
AKSL_RUN(failures, test_append_detects_self_cycle);
|
||||||
CATCH(e, aksl_list_append(&mylist, &node1));
|
AKSL_RUN(failures, test_append_detects_two_node_cycle);
|
||||||
|
|
||||||
node2.data = "Node 2";
|
|
||||||
CATCH(e, aksl_list_append(&mylist, &node2));
|
|
||||||
|
|
||||||
// Iterate over all nodes in the list using the myiter() function
|
AKSL_RUN(failures, test_iterate_null_arguments);
|
||||||
CATCH(e, aksl_list_iterate(&mylist, &myiter, NULL));
|
AKSL_RUN(failures, test_iterate_single_node);
|
||||||
|
AKSL_RUN(failures, test_iterate_detects_self_cycle);
|
||||||
|
AKSL_RUN(failures, test_iterate_detects_two_node_cycle);
|
||||||
|
AKSL_RUN(failures, test_iterate_propagates_callback_error);
|
||||||
|
AKSL_RUN(failures, test_iterate_break_is_not_an_error);
|
||||||
|
|
||||||
// Iterate over up to the first 2 nodes in the list and then exit early
|
AKSL_RUN(failures, test_pop_middle_node);
|
||||||
idx = 0;
|
AKSL_RUN(failures, test_pop_head_node);
|
||||||
CATCH(e, aksl_list_iterate(&mylist, &myiter_earlyhalt, &idx));
|
AKSL_RUN(failures, test_pop_tail_node);
|
||||||
CATCH(e, aksl_fprintf(&count, stderr, "Iterator exited early at index %d\n", idx));
|
AKSL_RUN(failures, test_pop_only_node);
|
||||||
|
AKSL_RUN(failures, test_pop_null_argument);
|
||||||
|
|
||||||
// Break the list with a circular reference, and iterate it again
|
AKSL_REPORT(failures);
|
||||||
node2.next = &mylist;
|
|
||||||
CATCH(e, aksl_list_iterate(&mylist, &myiter, NULL));
|
|
||||||
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} HANDLE(e, AKERR_CIRCULAR_REFERENCE) {
|
|
||||||
fprintf(stderr, "Circular reference error caught\n");
|
|
||||||
} FINISH_NORETURN(e);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
59
tests/test_list_append_chain.c
Normal file
59
tests/test_list_append_chain.c
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* KNOWN FAILING -- TODO.md section 2.1.1
|
||||||
|
*
|
||||||
|
* aksl_list_append conflates Floyd cycle detection with finding the tail: the
|
||||||
|
* `tail` cursor is assigned from `slow` *before* `slow` advances, so it tracks
|
||||||
|
* the node behind the list midpoint rather than the last node. Appending to any
|
||||||
|
* list of two or more nodes therefore overwrites an interior link and silently
|
||||||
|
* drops every node after it.
|
||||||
|
*
|
||||||
|
* Appending n1..n4 to n0 produces the chain "n0 -> n4"; this test asserts the
|
||||||
|
* correct "n0 -> n1 -> n2 -> n3 -> n4" and so fails until append is fixed.
|
||||||
|
* It is registered in AKSL_KNOWN_FAILING_TESTS, which marks it WILL_FAIL; when
|
||||||
|
* the fix lands, CTest will report it as unexpectedly passing, which is the
|
||||||
|
* signal to move it into AKSL_TESTS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
#define CHAIN_LEN 5
|
||||||
|
|
||||||
|
static int test_append_builds_full_chain(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode node[CHAIN_LEN];
|
||||||
|
aksl_ListNode *walk = NULL;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
memset((void *)node, 0x00, sizeof(node));
|
||||||
|
|
||||||
|
for ( i = 1; i < CHAIN_LEN; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_list_append(&node[0], &node[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forward links, head to tail. */
|
||||||
|
walk = &node[0];
|
||||||
|
for ( i = 0; i < CHAIN_LEN; i++ ) {
|
||||||
|
AKSL_CHECK(walk == &node[i]);
|
||||||
|
walk = walk->next;
|
||||||
|
}
|
||||||
|
AKSL_CHECK(walk == NULL);
|
||||||
|
|
||||||
|
/* Back links, tail to head. */
|
||||||
|
walk = &node[CHAIN_LEN - 1];
|
||||||
|
for ( i = CHAIN_LEN - 1; i >= 0; i-- ) {
|
||||||
|
AKSL_CHECK(walk == &node[i]);
|
||||||
|
walk = walk->prev;
|
||||||
|
}
|
||||||
|
AKSL_CHECK(walk == NULL);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
akerr_init();
|
||||||
|
AKSL_RUN(failures, test_append_builds_full_chain);
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
71
tests/test_list_iterate_head.c
Normal file
71
tests/test_list_iterate_head.c
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* KNOWN FAILING -- TODO.md section 2.1.2
|
||||||
|
*
|
||||||
|
* aksl_list_iterate runs a Floyd cycle check that leaves `slow` sitting at the
|
||||||
|
* list midpoint, and then starts the visiting loop from `slow` instead of from
|
||||||
|
* `list`. Every node before the midpoint -- including the head -- is never
|
||||||
|
* passed to the callback.
|
||||||
|
*
|
||||||
|
* The list here is built by hand rather than with aksl_list_append so that this
|
||||||
|
* test fails only for the iterate defect, not for the separate append defect in
|
||||||
|
* TODO.md 2.1.1.
|
||||||
|
*
|
||||||
|
* Registered in AKSL_KNOWN_FAILING_TESTS (WILL_FAIL). When iterate is fixed,
|
||||||
|
* CTest reports this as unexpectedly passing -- move it into AKSL_TESTS then.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
#define CHAIN_LEN 3
|
||||||
|
|
||||||
|
typedef struct VisitLog
|
||||||
|
{
|
||||||
|
int count;
|
||||||
|
aksl_ListNode *seen[CHAIN_LEN];
|
||||||
|
} VisitLog;
|
||||||
|
|
||||||
|
static akerr_ErrorContext AKERR_NOIGNORE *record_visit(aksl_ListNode *node, void *data)
|
||||||
|
{
|
||||||
|
VisitLog *log = NULL;
|
||||||
|
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
|
||||||
|
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
|
||||||
|
log = (VisitLog *)data;
|
||||||
|
if ( log->count < CHAIN_LEN ) {
|
||||||
|
log->seen[log->count] = node;
|
||||||
|
}
|
||||||
|
log->count += 1;
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_iterate_visits_every_node_from_the_head(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode node[CHAIN_LEN];
|
||||||
|
VisitLog log;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
memset((void *)node, 0x00, sizeof(node));
|
||||||
|
memset((void *)&log, 0x00, sizeof(log));
|
||||||
|
|
||||||
|
for ( i = 1; i < CHAIN_LEN; i++ ) {
|
||||||
|
node[i - 1].next = &node[i];
|
||||||
|
node[i].prev = &node[i - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_iterate(&node[0], &record_visit, &log));
|
||||||
|
AKSL_CHECK(log.count == CHAIN_LEN);
|
||||||
|
for ( i = 0; i < CHAIN_LEN; i++ ) {
|
||||||
|
AKSL_CHECK(log.seen[i] == &node[i]);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
akerr_init();
|
||||||
|
AKSL_RUN(failures, test_iterate_visits_every_node_from_the_head);
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
@@ -1,7 +1,23 @@
|
|||||||
#include <stdio.h>
|
/*
|
||||||
#include <akstdlib.h>
|
* Depth-first tree search.
|
||||||
|
*
|
||||||
|
* Previously this test shared one TreeSearchParams across all three searches
|
||||||
|
* without resetting it, so `steps` accumulated (7, then 14, then 21) and the
|
||||||
|
* second assertion failed -- `tree` was a red test on every run. Each search
|
||||||
|
* now gets its own params.
|
||||||
|
*
|
||||||
|
* Caveat worth knowing when reading the step counts below: the value is hidden
|
||||||
|
* in tree[6], which is the last node visited in pre-, in- and post-order alike,
|
||||||
|
* so "7 steps" holds for all three orders and does not actually distinguish
|
||||||
|
* them -- nor does it prove that AKERR_ITERATOR_BREAK stopped anything (it does
|
||||||
|
* not; see tests/test_tree_iterate_break.c and TODO.md 2.1.3). Visit-order
|
||||||
|
* assertions that tell the three traversals apart are TODO.md section 1.8.
|
||||||
|
*/
|
||||||
|
|
||||||
#define MAX_LEAVES 7
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
#define MAX_LEAVES 7
|
||||||
|
#define HIDDEN_VALUE ((void *)17336)
|
||||||
|
|
||||||
typedef struct TreeSearchParams
|
typedef struct TreeSearchParams
|
||||||
{
|
{
|
||||||
@@ -10,11 +26,10 @@ typedef struct TreeSearchParams
|
|||||||
aksl_TreeNode *node;
|
aksl_TreeNode *node;
|
||||||
} TreeSearchParams;
|
} TreeSearchParams;
|
||||||
|
|
||||||
// This iterator does nothing but print the node names it is visiting
|
static akerr_ErrorContext AKERR_NOIGNORE *find_value(aksl_TreeNode *node, void *data)
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *myiter(aksl_TreeNode *node, void *data)
|
|
||||||
{
|
{
|
||||||
int count = 0;
|
|
||||||
TreeSearchParams *parms = NULL;
|
TreeSearchParams *parms = NULL;
|
||||||
|
|
||||||
PREPARE_ERROR(e);
|
PREPARE_ERROR(e);
|
||||||
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
|
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
|
||||||
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
|
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
|
||||||
@@ -27,70 +42,72 @@ akerr_ErrorContext AKERR_NOIGNORE *myiter(aksl_TreeNode *node, void *data)
|
|||||||
SUCCEED_RETURN(e);
|
SUCCEED_RETURN(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Build the 3-level tree used by every case here, with the search value hidden
|
||||||
|
* in the bottom-right leaf.
|
||||||
|
*
|
||||||
|
* LEFT RIGHT
|
||||||
|
* TREE[0]
|
||||||
|
* +--------^^---------+
|
||||||
|
* | |
|
||||||
|
* TREE[1] TREE[2]
|
||||||
|
* +---^^---+ +---^^---+
|
||||||
|
* | | | |
|
||||||
|
*TREE[3] TREE[4] TREE[5] TREE[6]
|
||||||
|
*/
|
||||||
|
static void build_tree(aksl_TreeNode *tree, TreeSearchParams *parms)
|
||||||
|
{
|
||||||
|
memset((void *)tree, 0x00, sizeof(aksl_TreeNode) * MAX_LEAVES);
|
||||||
|
tree[0].left = &tree[1];
|
||||||
|
tree[0].right = &tree[2];
|
||||||
|
tree[1].left = &tree[3];
|
||||||
|
tree[1].right = &tree[4];
|
||||||
|
tree[2].left = &tree[5];
|
||||||
|
tree[2].right = &tree[6];
|
||||||
|
tree[6].leaf = HIDDEN_VALUE;
|
||||||
|
|
||||||
|
memset((void *)parms, 0x00, sizeof(TreeSearchParams));
|
||||||
|
parms->value = HIDDEN_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int search_finds_hidden_value(uint8_t searchmode)
|
||||||
|
{
|
||||||
|
aksl_TreeNode tree[MAX_LEAVES];
|
||||||
|
TreeSearchParams parms;
|
||||||
|
|
||||||
|
build_tree(tree, &parms);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &find_value, NULL, NULL,
|
||||||
|
searchmode, &parms, NULL));
|
||||||
|
AKSL_CHECK(parms.node == &tree[6]);
|
||||||
|
AKSL_CHECK(parms.steps == MAX_LEAVES);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_dfs_preorder(void)
|
||||||
|
{
|
||||||
|
return search_finds_hidden_value(AKSL_TREE_SEARCH_DFS_PREORDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_dfs_inorder(void)
|
||||||
|
{
|
||||||
|
return search_finds_hidden_value(AKSL_TREE_SEARCH_DFS_INORDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_dfs_postorder(void)
|
||||||
|
{
|
||||||
|
return search_finds_hidden_value(AKSL_TREE_SEARCH_DFS_POSTORDER);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(e);
|
int failures = 0;
|
||||||
aksl_TreeNode tree[MAX_LEAVES];
|
|
||||||
TreeSearchParams parms = {
|
|
||||||
.value = (void *)17336,
|
|
||||||
.steps = 0,
|
|
||||||
.node = NULL
|
|
||||||
};
|
|
||||||
|
|
||||||
ATTEMPT {
|
akerr_init();
|
||||||
memset((void *)&tree, 0x00, sizeof(aksl_TreeNode) * MAX_LEAVES);
|
|
||||||
/*
|
|
||||||
* Here we build a 3 level tree
|
|
||||||
*
|
|
||||||
* LEFT RIGHT
|
|
||||||
* TREE[0]
|
|
||||||
* +--------^^---------+
|
|
||||||
* | |
|
|
||||||
* TREE[1] TREE[2]
|
|
||||||
* +---^^---+ +---^^---+
|
|
||||||
* | | | |
|
|
||||||
*TREE[3] TREE[4] TREE[5] TREE[6]
|
|
||||||
*/
|
|
||||||
tree[0].left = &tree[1];
|
|
||||||
tree[0].right = &tree[2];
|
|
||||||
tree[1].left = &tree[3];
|
|
||||||
tree[1].right = &tree[4];
|
|
||||||
tree[2].left = &tree[5];
|
|
||||||
tree[2].right = &tree[6];
|
|
||||||
|
|
||||||
// Hide a value in tree[6]
|
AKSL_RUN(failures, test_dfs_preorder);
|
||||||
tree[6].leaf = (void *)17336;
|
AKSL_RUN(failures, test_dfs_inorder);
|
||||||
|
AKSL_RUN(failures, test_dfs_postorder);
|
||||||
|
|
||||||
// Search for the value 17336 using DFS_PREORDER
|
AKSL_REPORT(failures);
|
||||||
CATCH(e, aksl_tree_iterate(&tree[0], &myiter, NULL, NULL, AKSL_TREE_SEARCH_DFS_PREORDER, &parms, NULL));
|
|
||||||
if ( parms.node != &tree[6] ) {
|
|
||||||
FAIL_BREAK(e, AKERR_API, "DFS_PREORDER_SEARCH didn't find the node");
|
|
||||||
}
|
|
||||||
if ( parms.steps != 7 ) {
|
|
||||||
FAIL_BREAK(e, AKERR_API, "DFS_PREORDER_SEARCH should've found the node in 7 steps, instead took %d", parms.steps);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search for the value 17336 using DFS_INORDER
|
|
||||||
CATCH(e, aksl_tree_iterate(&tree[0], &myiter, NULL, NULL, AKSL_TREE_SEARCH_DFS_INORDER, &parms, NULL));
|
|
||||||
if ( parms.node != &tree[6] ) {
|
|
||||||
FAIL_BREAK(e, AKERR_API, "DFS_INORDER_SEARCH didn't find the node");
|
|
||||||
}
|
|
||||||
if ( parms.steps != 7 ) {
|
|
||||||
FAIL_BREAK(e, AKERR_API, "DFS_INORDER_SEARCH should've found the node in 7 steps, instead took %d", parms.steps);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search for the value 17336 using DFS_PREORDER
|
|
||||||
CATCH(e, aksl_tree_iterate(&tree[0], &myiter, NULL, NULL, AKSL_TREE_SEARCH_DFS_POSTORDER, &parms, NULL));
|
|
||||||
if ( parms.node != &tree[6] ) {
|
|
||||||
FAIL_BREAK(e, AKERR_API, "DFS_POSTORDER_SEARCH didn't find the node");
|
|
||||||
}
|
|
||||||
if ( parms.steps != 7 ) {
|
|
||||||
FAIL_BREAK(e, AKERR_API, "DFS_POSTORDER_SEARCH should've found the node in 7 steps, instead took %d", parms.steps);
|
|
||||||
}
|
|
||||||
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} FINISH_NORETURN(e);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
89
tests/test_tree_iterate_break.c
Normal file
89
tests/test_tree_iterate_break.c
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* KNOWN FAILING -- TODO.md section 2.1.3
|
||||||
|
*
|
||||||
|
* AKERR_ITERATOR_BREAK does not abort a tree traversal. aksl_tree_iterate
|
||||||
|
* recurses into itself, and the frame in which the callback raises the break
|
||||||
|
* handles it in its own PROCESS/HANDLE(e, AKERR_ITERATOR_BREAK) block and
|
||||||
|
* returns *success*. The parent frame's PASS therefore sees no error at all and
|
||||||
|
* carries straight on into the sibling subtree, so the traversal runs to
|
||||||
|
* completion.
|
||||||
|
*
|
||||||
|
* The 7-node tree below is walked pre-order (0, 1, 3, 4, 2, 5, 6) with the
|
||||||
|
* callback breaking on tree[3], the third node visited. A working break stops
|
||||||
|
* the walk at 3 visits; today the callback is invoked all 7 times.
|
||||||
|
*
|
||||||
|
* tests/test_tree.c cannot catch this: it hides its value in tree[6], which is
|
||||||
|
* the last node visited in all three depth-first orders, so a break that never
|
||||||
|
* fires is indistinguishable from one that fires on the final node.
|
||||||
|
*
|
||||||
|
* Registered in AKSL_KNOWN_FAILING_TESTS (WILL_FAIL). When the break semantics
|
||||||
|
* are fixed, CTest reports this as unexpectedly passing -- move it into
|
||||||
|
* AKSL_TESTS then.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
#define MAX_LEAVES 7
|
||||||
|
|
||||||
|
typedef struct BreakParams
|
||||||
|
{
|
||||||
|
aksl_TreeNode *stop_at;
|
||||||
|
int visits;
|
||||||
|
} BreakParams;
|
||||||
|
|
||||||
|
static akerr_ErrorContext AKERR_NOIGNORE *break_at_node(aksl_TreeNode *node, void *data)
|
||||||
|
{
|
||||||
|
BreakParams *parms = NULL;
|
||||||
|
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
|
||||||
|
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
|
||||||
|
parms = (BreakParams *)data;
|
||||||
|
parms->visits += 1;
|
||||||
|
if ( node == parms->stop_at ) {
|
||||||
|
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop");
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_break_aborts_the_whole_traversal(void)
|
||||||
|
{
|
||||||
|
aksl_TreeNode tree[MAX_LEAVES];
|
||||||
|
BreakParams parms;
|
||||||
|
|
||||||
|
memset((void *)tree, 0x00, sizeof(tree));
|
||||||
|
memset((void *)&parms, 0x00, sizeof(parms));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TREE[0]
|
||||||
|
* +--------^^---------+
|
||||||
|
* | |
|
||||||
|
* TREE[1] TREE[2]
|
||||||
|
* +---^^---+ +---^^---+
|
||||||
|
* | | | |
|
||||||
|
*TREE[3] TREE[4] TREE[5] TREE[6]
|
||||||
|
*/
|
||||||
|
tree[0].left = &tree[1];
|
||||||
|
tree[0].right = &tree[2];
|
||||||
|
tree[1].left = &tree[3];
|
||||||
|
tree[1].right = &tree[4];
|
||||||
|
tree[2].left = &tree[5];
|
||||||
|
tree[2].right = &tree[6];
|
||||||
|
|
||||||
|
parms.stop_at = &tree[3];
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &break_at_node, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_PREORDER, &parms, NULL));
|
||||||
|
/* Pre-order visits 0, 1, 3 -- the break on tree[3] must stop the walk there. */
|
||||||
|
AKSL_CHECK(parms.visits == 3);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
akerr_init();
|
||||||
|
AKSL_RUN(failures, test_break_aborts_the_whole_traversal);
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user