Compare commits
1 Commits
feature/te
...
trees
| Author | SHA1 | Date | |
|---|---|---|---|
|
21b2a3955d
|
@@ -1,77 +0,0 @@
|
|||||||
name: libakstdlib CI Build
|
|
||||||
run-name: ${{ gitea.actor }} libakstdlib test
|
|
||||||
on: [push]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
cmake_build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: echo "Triggered by ${{ gitea.event_name }} from ${{ gitea.repository }}@${{ gitea.ref }}. Building on ${{ runner.os }}."
|
|
||||||
- name: Check out repository code
|
|
||||||
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
|
|
||||||
run: |
|
|
||||||
sudo apt-get update -y
|
|
||||||
sudo apt-get install -y cmake gcc moreutils
|
|
||||||
# Depends on libakerror@main
|
|
||||||
git clone https://source.starfort.tech/andrew/libakerror.git
|
|
||||||
cd libakerror
|
|
||||||
cmake -S . -B build
|
|
||||||
cmake --build build
|
|
||||||
cmake --install build
|
|
||||||
- name: build and test
|
|
||||||
run: |
|
|
||||||
cmake -S . -B build
|
|
||||||
cmake --build build
|
|
||||||
sudo cmake --install build
|
|
||||||
ctest --test-dir build --output-on-failure
|
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
|
||||||
|
|
||||||
mutation_test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out repository code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
# The harness copies the repo and builds the copy top-level, so it
|
|
||||||
# needs deps/libakerror present just like the main job does.
|
|
||||||
submodules: recursive
|
|
||||||
- name: dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update -y
|
|
||||||
sudo apt-get install -y cmake gcc moreutils python3
|
|
||||||
# Verify the tests actually catch bugs: break the library many ways and
|
|
||||||
# confirm the suite fails. Gated on src/stdlib.c (fast, deterministic);
|
|
||||||
# run the full default target locally for the macro header as well.
|
|
||||||
#
|
|
||||||
# The threshold is a ratchet, not a quality bar. The score on the section
|
|
||||||
# 1.0 suite is 46.8% (81/173 killed): the list and tree functions are
|
|
||||||
# covered, and everything else in src/stdlib.c -- the printf family, the
|
|
||||||
# ato* family, realpath, the memory and stream wrappers -- has no tests
|
|
||||||
# yet, so its mutants survive. 40 leaves headroom for the runner while
|
|
||||||
# still failing on a real regression (tests deleted, or new untested code
|
|
||||||
# added). Raise it as TODO.md sections 1.1-1.9 land.
|
|
||||||
- name: mutation testing
|
|
||||||
run: |
|
|
||||||
python3 scripts/mutation_test.py \
|
|
||||||
--target src/stdlib.c \
|
|
||||||
--junit mutation-junit.xml \
|
|
||||||
--threshold 40
|
|
||||||
# Publish even when the threshold gate fails, so survivors are visible --
|
|
||||||
# each one is a missing test. Display-only (fail_on_failure: false); the
|
|
||||||
# --threshold above is the gate. annotate_only avoids the Checks API 404
|
|
||||||
# on Gitea (mikepenz/action-junit-report#23).
|
|
||||||
- name: publish mutation results
|
|
||||||
if: always()
|
|
||||||
uses: mikepenz/action-junit-report@v4
|
|
||||||
with:
|
|
||||||
report_paths: 'mutation-junit.xml'
|
|
||||||
annotate_only: true
|
|
||||||
detailed_summary: true
|
|
||||||
include_passed: true
|
|
||||||
fail_on_failure: 'false'
|
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
#
|
|
||||||
# pre-push: build libakstdlib and run the test harnesses before anything leaves
|
|
||||||
# this machine.
|
|
||||||
#
|
|
||||||
# Install (once per clone):
|
|
||||||
#
|
|
||||||
# git config core.hooksPath .githooks
|
|
||||||
#
|
|
||||||
# Runs by default, a few seconds all in:
|
|
||||||
#
|
|
||||||
# * default build + ctest
|
|
||||||
# * ASan/UBSan build + ctest
|
|
||||||
#
|
|
||||||
# Opt in to the slow harness (~25 minutes -- it rebuilds and re-runs the whole
|
|
||||||
# suite once per mutant):
|
|
||||||
#
|
|
||||||
# AKSL_HOOK_MUTATION=1 git push
|
|
||||||
#
|
|
||||||
# Bypass everything (git's own escape hatch):
|
|
||||||
#
|
|
||||||
# git push --no-verify
|
|
||||||
#
|
|
||||||
# Builds go under .git/aksl-prepush so the hook never disturbs whatever is in
|
|
||||||
# your own build/ directory. Override with AKSL_HOOK_BUILD_DIR if you want them
|
|
||||||
# somewhere else.
|
|
||||||
|
|
||||||
set -u
|
|
||||||
|
|
||||||
# Keep in sync with the --threshold in .gitea/workflows/ci.yaml, so a push that
|
|
||||||
# would fail CI fails here first.
|
|
||||||
MUTATION_THRESHOLD="${AKSL_MUTATION_THRESHOLD:-40}"
|
|
||||||
|
|
||||||
ZERO_SHA=0000000000000000000000000000000000000000
|
|
||||||
|
|
||||||
root=$(git rev-parse --show-toplevel) || exit 1
|
|
||||||
cd "$root" || exit 1
|
|
||||||
|
|
||||||
# git feeds us one line per ref being pushed. A push that only *deletes* refs
|
|
||||||
# has no commits to test, so there is nothing to do. An empty stdin (nothing to
|
|
||||||
# push) lands here too, which is equally fine to skip.
|
|
||||||
has_updates=0
|
|
||||||
while read -r _local_ref local_sha _remote_ref _remote_sha; do
|
|
||||||
if [ "$local_sha" != "$ZERO_SHA" ]; then
|
|
||||||
has_updates=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ "$has_updates" -eq 0 ]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f deps/libakerror/CMakeLists.txt ]; then
|
|
||||||
echo "pre-push: deps/libakerror is empty. Run:" >&2
|
|
||||||
echo " git submodule update --init --recursive" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
builddir="${AKSL_HOOK_BUILD_DIR:-$(git rev-parse --git-dir)/aksl-prepush}"
|
|
||||||
mkdir -p "$builddir" || exit 1
|
|
||||||
logfile="$builddir/last.log"
|
|
||||||
|
|
||||||
# Run a step quietly; on failure, dump what it said and abort the push.
|
|
||||||
run() {
|
|
||||||
if ! "$@" > "$logfile" 2>&1; then
|
|
||||||
echo >&2
|
|
||||||
echo "pre-push: FAILED: $*" >&2
|
|
||||||
echo "---------------------------------------------------------------" >&2
|
|
||||||
cat "$logfile" >&2
|
|
||||||
echo "---------------------------------------------------------------" >&2
|
|
||||||
echo "pre-push: push aborted. Use 'git push --no-verify' to override." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "pre-push: default build + ctest"
|
|
||||||
run cmake -S . -B "$builddir/default"
|
|
||||||
run cmake --build "$builddir/default"
|
|
||||||
run ctest --test-dir "$builddir/default" --output-on-failure
|
|
||||||
|
|
||||||
echo "pre-push: sanitizer build + ctest"
|
|
||||||
run cmake -S . -B "$builddir/asan" -DAKSL_SANITIZE=ON
|
|
||||||
run cmake --build "$builddir/asan"
|
|
||||||
run ctest --test-dir "$builddir/asan" --output-on-failure
|
|
||||||
|
|
||||||
if [ "${AKSL_HOOK_MUTATION:-0}" = "1" ]; then
|
|
||||||
echo "pre-push: mutation testing, threshold ${MUTATION_THRESHOLD}% (this takes a while)"
|
|
||||||
# Deliberately not wrapped in run(): this one is slow enough that you want
|
|
||||||
# to watch it make progress.
|
|
||||||
if ! python3 scripts/mutation_test.py \
|
|
||||||
--target src/stdlib.c \
|
|
||||||
--threshold "$MUTATION_THRESHOLD"; then
|
|
||||||
echo >&2
|
|
||||||
echo "pre-push: mutation score below ${MUTATION_THRESHOLD}%. Push aborted." >&2
|
|
||||||
echo "pre-push: use 'git push --no-verify' to override." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "pre-push: OK"
|
|
||||||
exit 0
|
|
||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[submodule "deps/libakerror"]
|
|
||||||
path = deps/libakerror
|
|
||||||
url = https://source.starfort.tech/andrew/libakerror.git
|
|
||||||
121
CMakeLists.txt
121
CMakeLists.txt
@@ -1,25 +1,6 @@
|
|||||||
cmake_minimum_required(VERSION 3.10)
|
cmake_minimum_required(VERSION 3.10)
|
||||||
project(akstdlib LANGUAGES C)
|
project(akstdlib LANGUAGES C)
|
||||||
|
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_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")
|
|
||||||
|
|
||||||
# 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()
|
||||||
@@ -30,37 +11,10 @@ include(CTest)
|
|||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
include(CMakePackageConfigHelpers)
|
include(CMakePackageConfigHelpers)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
set(AKSL_SUPPRESS_ADD_TEST FALSE)
|
|
||||||
else()
|
|
||||||
if(NOT TARGET akerror::akerror)
|
if(NOT TARGET akerror::akerror)
|
||||||
find_package(PkgConfig REQUIRED)
|
find_package(PkgConfig REQUIRED)
|
||||||
find_package(akerror REQUIRED)
|
find_package(akerror REQUIRED)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
|
||||||
|
|
||||||
set(akstdlib_install_cmakedir "${CMAKE_INSTALL_LIBDIR}/cmake/akstdlib")
|
set(akstdlib_install_cmakedir "${CMAKE_INSTALL_LIBDIR}/cmake/akstdlib")
|
||||||
set(prefix ${CMAKE_INSTALL_PREFIX})
|
set(prefix ${CMAKE_INSTALL_PREFIX})
|
||||||
@@ -113,79 +67,4 @@ install(FILES
|
|||||||
DESTINATION ${akstdlib_install_cmakedir}
|
DESTINATION ${akstdlib_install_cmakedir}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Each test is one source file tests/test_<name>.c, built into the executable
|
|
||||||
# test_<name> and registered as the CTest test <name>. Shared helpers live in
|
|
||||||
# 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
|
|
||||||
)
|
|
||||||
|
|
||||||
set(AKSL_WILL_FAIL_TESTS
|
|
||||||
)
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
# Cap every test. The list and tree code is full of loops whose termination
|
|
||||||
# depends on a single condition, so a plausible bug -- or a mutant from the
|
|
||||||
# target below -- turns a test into an infinite loop. Without this, ctest waits
|
|
||||||
# forever; the mutation harness then kills ctest at its own timeout and the
|
|
||||||
# spinning test binary is left orphaned, holding the pipe open and burning a
|
|
||||||
# core. The whole suite runs in well under a second, so 30s is pure headroom.
|
|
||||||
set_tests_properties(
|
|
||||||
${AKSL_TESTS} ${AKSL_WILL_FAIL_TESTS} ${AKSL_KNOWN_FAILING_TESTS}
|
|
||||||
PROPERTIES TIMEOUT 30
|
|
||||||
)
|
|
||||||
|
|
||||||
# Mutation testing: break the library in small ways and confirm the test suite
|
|
||||||
# notices. This is a meta-check on the tests themselves, and it rebuilds and
|
|
||||||
# re-runs the whole suite once per mutant, so it is a manual target rather than
|
|
||||||
# a CTest test:
|
|
||||||
# cmake --build build --target mutation
|
|
||||||
# This target covers both src/stdlib.c and include/akstdlib.h. CI runs the
|
|
||||||
# narrower, faster src/stdlib.c set with a --threshold gate; see
|
|
||||||
# .gitea/workflows/ci.yaml.
|
|
||||||
find_package(Python3 COMPONENTS Interpreter)
|
|
||||||
if(Python3_FOUND)
|
|
||||||
add_custom_target(mutation
|
|
||||||
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
|
||||||
|
|||||||
132
README.md
132
README.md
@@ -1,132 +0,0 @@
|
|||||||
# README
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
`libakstdlib` wraps C standard library functions so that they report failures
|
|
||||||
through [libakerror](https://source.starfort.tech/andrew/libakerror)'s
|
|
||||||
`ATTEMPT { ... } HANDLE { ... }` error contexts instead of through return codes
|
|
||||||
and `errno`. It also provides a few data structures built on the same
|
|
||||||
convention (a doubly-linked list and a binary tree).
|
|
||||||
|
|
||||||
Every entry point returns `akerr_ErrorContext *` and is marked `AKERR_NOIGNORE`.
|
|
||||||
See `TODO.md` for the current state of the library: what is covered by tests,
|
|
||||||
which corner cases are still open, and which libc functions are not yet wrapped.
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
```sh
|
|
||||||
git submodule update --init --recursive # deps/libakerror
|
|
||||||
cmake -S . -B build
|
|
||||||
cmake --build build
|
|
||||||
cmake --install build
|
|
||||||
```
|
|
||||||
|
|
||||||
A top-level build compiles the vendored `deps/libakerror`. When `libakstdlib` is
|
|
||||||
consumed as a subproject, it uses whatever `akerror::akerror` target or installed
|
|
||||||
package the parent provides instead.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
There are three harnesses. The first two take seconds; the third takes about
|
|
||||||
half an hour.
|
|
||||||
|
|
||||||
### 1. The test suite
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cmake -S . -B build
|
|
||||||
cmake --build build
|
|
||||||
ctest --test-dir build --output-on-failure
|
|
||||||
```
|
|
||||||
|
|
||||||
Tests live one per file in `tests/test_<name>.c` and share the helpers in
|
|
||||||
`tests/aksl_capture.h` — `AKSL_CHECK()` for plain assertions (unlike `assert()`
|
|
||||||
it survives `-DNDEBUG`), `AKSL_CHECK_STATUS(call, expected)` to run a wrapper and
|
|
||||||
assert on the status it returns, and an `AKSL_RUN()` driver that additionally
|
|
||||||
fails any test which leaks a slot from libakerror's error pool.
|
|
||||||
|
|
||||||
To add a test, drop `tests/test_mything.c` in place and add `mything` to
|
|
||||||
`AKSL_TESTS` in `CMakeLists.txt`.
|
|
||||||
|
|
||||||
**Reading the results.** `CMakeLists.txt` splits tests into three lists, and two
|
|
||||||
of them invert the meaning of "Passed":
|
|
||||||
|
|
||||||
| List | Meaning |
|
|
||||||
|---|---|
|
|
||||||
| `AKSL_TESTS` | Ordinary tests. Must exit 0. |
|
|
||||||
| `AKSL_WILL_FAIL_TESTS` | Expected to abort by design — an unhandled error reaching `FINISH_NORETURN`, or a deliberate contract violation. Marked `WILL_FAIL`, so a non-zero exit is a pass. |
|
|
||||||
| `AKSL_KNOWN_FAILING_TESTS` | Assert the *correct* behaviour of a confirmed defect (see `TODO.md` §2.1). Also marked `WILL_FAIL`. |
|
|
||||||
|
|
||||||
So `ctest` reporting all green does **not** mean the library is defect-free — it
|
|
||||||
means the known-good tests passed and the known-bad ones are still failing in the
|
|
||||||
documented way. When a defect is fixed, its test starts passing, CTest reports it
|
|
||||||
as failed with *unexpectedly passed*, and that is the cue to move it from
|
|
||||||
`AKSL_KNOWN_FAILING_TESTS` into `AKSL_TESTS`.
|
|
||||||
|
|
||||||
Every test is capped with a 30-second CTest `TIMEOUT`. The list and tree code is
|
|
||||||
full of loops whose termination hangs on a single condition, so a bug of that
|
|
||||||
shape hangs the suite rather than failing it.
|
|
||||||
|
|
||||||
### 2. Sanitizers
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cmake -S . -B build-asan -DAKSL_SANITIZE=ON
|
|
||||||
cmake --build build-asan
|
|
||||||
ctest --test-dir build-asan --output-on-failure
|
|
||||||
```
|
|
||||||
|
|
||||||
Builds the library, the tests and the vendored libakerror with ASan + UBSan and
|
|
||||||
`-fno-sanitize-recover=all`. Several of the open items in `TODO.md` §2 only
|
|
||||||
misbehave under instrumentation — the uninitialised `%s` in `aksl_realpath`, the
|
|
||||||
unbounded `vsprintf` behind `aksl_sprintf`, the missing `va_end` in the `printf`
|
|
||||||
family — so new tests for those should be run this way.
|
|
||||||
|
|
||||||
### 3. Mutation testing
|
|
||||||
|
|
||||||
The suite tells you the library works. Mutation testing tells you the *suite*
|
|
||||||
works: it breaks the library in small ways, one at a time, and checks that the
|
|
||||||
tests notice.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cmake --build build --target mutation # src/stdlib.c + include/akstdlib.h
|
|
||||||
```
|
|
||||||
|
|
||||||
or drive the script directly for a faster or narrower run:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
scripts/mutation_test.py --target src/stdlib.c # C source only
|
|
||||||
scripts/mutation_test.py --target src/stdlib.c --list # enumerate, build nothing
|
|
||||||
scripts/mutation_test.py --target src/stdlib.c --max-mutants 20
|
|
||||||
scripts/mutation_test.py --target src/stdlib.c --threshold 40
|
|
||||||
```
|
|
||||||
|
|
||||||
A mutant that makes the tests fail is *killed* (good); one the tests still pass
|
|
||||||
is a *survivor*, and names a missing test. The score is `killed / total`, and the
|
|
||||||
run prints every survivor with `file:line` and the exact edit. The harness never
|
|
||||||
touches your working tree — it copies the repo to a scratch directory and mutates
|
|
||||||
the copy.
|
|
||||||
|
|
||||||
CI runs the `src/stdlib.c` set with `--threshold 40`. That is a regression
|
|
||||||
ratchet rather than a quality bar: the current score is 46.8%, and the survivors
|
|
||||||
are concentrated in the wrappers that have no tests yet. Raise the threshold as
|
|
||||||
coverage lands.
|
|
||||||
|
|
||||||
## The pre-push hook
|
|
||||||
|
|
||||||
`.githooks/pre-push` runs the fast harnesses — the default build and the
|
|
||||||
sanitizer build, each followed by `ctest` — before letting a push out. Enable it
|
|
||||||
once per clone:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
git config core.hooksPath .githooks
|
|
||||||
```
|
|
||||||
|
|
||||||
It only builds when there are commits to push (a branch deletion is a no-op), and
|
|
||||||
it builds under `.git/aksl-prepush` so it never disturbs your own `build/`.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
AKSL_HOOK_MUTATION=1 git push # also run the mutation gate (slow)
|
|
||||||
git push --no-verify # skip the hook entirely
|
|
||||||
```
|
|
||||||
|
|
||||||
Other knobs: `AKSL_MUTATION_THRESHOLD` (default 40, keep it in step with
|
|
||||||
`.gitea/workflows/ci.yaml`) and `AKSL_HOOK_BUILD_DIR`.
|
|
||||||
553
TODO.md
553
TODO.md
@@ -1,553 +0,0 @@
|
|||||||
# 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`. CI runs the narrower `src/stdlib.c` set (173
|
|
||||||
mutants) as its own `mutation_test` job with `--threshold 40` and a JUnit report.
|
|
||||||
**Current score: 46.8%** — 81 killed, 92 survived. The survivors are concentrated
|
|
||||||
exactly where §1.1–1.9 have yet to be written:
|
|
||||||
|
|
||||||
| surviving mutants | function |
|
|
||||||
|---|---|
|
|
||||||
| 10 | `aksl_tree_iterate` |
|
|
||||||
| 6 each | `aksl_fread`, `aksl_fwrite`, `aksl_fprintf`, `aksl_sprintf` |
|
|
||||||
| 5 | `aksl_printf` |
|
|
||||||
| 4 each | `aksl_memcpy`, `aksl_realpath`, `aksl_strhash_djb2`, `aksl_list_append` |
|
|
||||||
| 3 each | `aksl_malloc`, `aksl_free`, `aksl_memset`, `aksl_fopen`, `aksl_fclose`, `aksl_atoi`, `aksl_atol`, `aksl_atoll`, `aksl_atof` |
|
|
||||||
| 1 | `aksl_list_iterate` |
|
|
||||||
|
|
||||||
The threshold is a regression ratchet, not a quality bar; raise it as sections below
|
|
||||||
land. Each surviving mutant is a concrete missing test — `mutation-junit.xml` lists
|
|
||||||
them with `file:line` and the exact edit.
|
|
||||||
- [x] **Cap every test with a CTest `TIMEOUT`.** Found while measuring the above: a mutant
|
|
||||||
that deletes `fast = fast->next->next` turns `aksl_list_iterate` into an infinite
|
|
||||||
loop, so `ctest` waited forever, the mutation harness killed `ctest` at its own
|
|
||||||
120 s timeout, and the orphaned test binary kept spinning at 100% CPU while holding
|
|
||||||
the pipe open — the run wedged and had to be killed by hand. `TIMEOUT 30` on every
|
|
||||||
test makes `ctest` reap its own child, so those mutants are now killed in 30 s and
|
|
||||||
leave nothing behind. It also guards the real suite against the same class of bug.
|
|
||||||
- [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. The pre-order case exists as `tests/test_tree_iterate_break.c`: pre-order
|
|
||||||
visits 0, 1, 3, so breaking at `tree[3]` must stop the walk at **3** visits, and today
|
|
||||||
it runs on to all 7. Add the equivalent for in-order (3, 1, 4, 0, 5, 2, 6 → breaking at
|
|
||||||
`tree[4]` gives 3) and post-order (3, 4, 1, 5, 6, 2, 0 → breaking at `tree[5]` gives 4).
|
|
||||||
- [ ] `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.
|
|
||||||
1
deps/libakerror
vendored
1
deps/libakerror
vendored
Submodule deps/libakerror deleted from 4fad0cec59
@@ -43,6 +43,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_memset(void *s, int c, size_t n);
|
|||||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_memcpy(void *d, void *s, size_t n);
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_memcpy(void *d, void *s, size_t n);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_free(void *ptr);
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_free(void *ptr);
|
||||||
|
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_printf(int *count, const char *restrict format, ...);
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_printf(int *count, const char *restrict format, ...);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fprintf(int *count, FILE *restrict stream, const char *restrict format, ...);
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_fprintf(int *count, FILE *restrict stream, const char *restrict format, ...);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_sprintf(int *count, char *restrict str, const char *restrict format, ...);
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_sprintf(int *count, char *restrict str, const char *restrict format, ...);
|
||||||
@@ -54,10 +55,8 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_atof(const char *nptr, double *dest);
|
|||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_realpath(const char *restrict path, char *restrict resolved_path);
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_realpath(const char *restrict path, char *restrict resolved_path);
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2(char *str, size_t len, uint32_t *hashval);
|
|
||||||
|
|
||||||
// Linked list functions
|
// Linked list functions
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_append(aksl_ListNode *list, aksl_ListNode *obj);
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_push(aksl_ListNode *list, aksl_ListNode *obj);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_pop(aksl_ListNode *node);
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_pop(aksl_ListNode *node);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate(aksl_ListNode *list, aksl_ListNodeIterator iter, void *data);
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate(aksl_ListNode *list, aksl_ListNodeIterator iter, void *data);
|
||||||
|
|
||||||
|
|||||||
@@ -1,436 +0,0 @@
|
|||||||
#!/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())
|
|
||||||
67
src/stdlib.c
67
src/stdlib.c
@@ -4,7 +4,6 @@
|
|||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_malloc(size_t size, void **dst)
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_malloc(size_t size, void **dst)
|
||||||
{
|
{
|
||||||
@@ -178,20 +177,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_realpath(const char *restrict path, char
|
|||||||
SUCCEED_RETURN(e);
|
SUCCEED_RETURN(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2(char *str, size_t len, uint32_t *hashval)
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_push(aksl_ListNode *list, aksl_ListNode *obj)
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
FAIL_ZERO_RETURN(e, str, AKERR_NULLPOINTER, "str");
|
|
||||||
FAIL_ZERO_RETURN(e, hashval, AKERR_NULLPOINTER, "hashval");
|
|
||||||
uint32_t h = 5381;
|
|
||||||
while (len--) {
|
|
||||||
h = ((h << 5) + h) + *str++;
|
|
||||||
}
|
|
||||||
*hashval = h;
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_append(aksl_ListNode *list, aksl_ListNode *obj)
|
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(e);
|
PREPARE_ERROR(e);
|
||||||
FAIL_ZERO_RETURN(e, list, AKERR_NULLPOINTER, "list");
|
FAIL_ZERO_RETURN(e, list, AKERR_NULLPOINTER, "list");
|
||||||
@@ -199,20 +185,19 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_append(aksl_ListNode *list, aksl_Li
|
|||||||
aksl_ListNode *slow = list;
|
aksl_ListNode *slow = list;
|
||||||
aksl_ListNode *fast = list;
|
aksl_ListNode *fast = list;
|
||||||
aksl_ListNode *tail = list;
|
aksl_ListNode *tail = list;
|
||||||
while ( fast != NULL && fast->next != NULL ) {
|
do {
|
||||||
|
if ( fast != NULL && fast->next != NULL ) {
|
||||||
|
fast = fast->next->next;
|
||||||
|
}
|
||||||
tail = slow;
|
tail = slow;
|
||||||
slow = slow->next;
|
slow = slow->next;
|
||||||
fast = fast->next->next;
|
if ( fast != NULL && fast == slow) {
|
||||||
if ( fast == slow) {
|
FAIL(e, AKERR_CIRCULAR_REFERENCE, "%p", list);
|
||||||
FAIL_RETURN(e, AKERR_CIRCULAR_REFERENCE, "%p", list);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ( fast != NULL ) {
|
|
||||||
tail = fast;
|
|
||||||
}
|
}
|
||||||
|
} while ( slow != NULL || (fast != NULL && fast->next != NULL) );
|
||||||
tail->next = obj;
|
tail->next = obj;
|
||||||
obj->next = NULL;
|
obj->next = NULL;
|
||||||
obj->prev = tail;
|
obj->prev = slow;
|
||||||
SUCCEED_RETURN(e);
|
SUCCEED_RETURN(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,10 +251,9 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate(
|
|||||||
if ( lfree == NULL ) {
|
if ( lfree == NULL ) {
|
||||||
lfree = &aksl_free;
|
lfree = &aksl_free;
|
||||||
}
|
}
|
||||||
ATTEMPT {
|
|
||||||
switch ( searchmode ) {
|
switch ( searchmode ) {
|
||||||
case AKSL_TREE_SEARCH_DFS_PREORDER:
|
case AKSL_TREE_SEARCH_DFS_PREORDER:
|
||||||
CATCH(e, iter(root, data));
|
PASS(e, iter(root, data));
|
||||||
if ( root->left != NULL ) {
|
if ( root->left != NULL ) {
|
||||||
PASS(e, aksl_tree_iterate(root->left, iter, lalloc, lfree, searchmode, data, NULL));
|
PASS(e, aksl_tree_iterate(root->left, iter, lalloc, lfree, searchmode, data, NULL));
|
||||||
}
|
}
|
||||||
@@ -284,13 +268,13 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate(
|
|||||||
if ( root-> right != NULL ) {
|
if ( root-> right != NULL ) {
|
||||||
PASS(e, aksl_tree_iterate(root->right, iter, lalloc, lfree, searchmode, data, NULL));
|
PASS(e, aksl_tree_iterate(root->right, iter, lalloc, lfree, searchmode, data, NULL));
|
||||||
}
|
}
|
||||||
CATCH(e, iter(root, data));
|
PASS(e, iter(root, data));
|
||||||
break;
|
break;
|
||||||
case AKSL_TREE_SEARCH_DFS_INORDER:
|
case AKSL_TREE_SEARCH_DFS_INORDER:
|
||||||
if ( root->left != NULL ) {
|
if ( root->left != NULL ) {
|
||||||
PASS(e, aksl_tree_iterate(root->left, iter, lalloc, lfree, searchmode, data, NULL));
|
PASS(e, aksl_tree_iterate(root->left, iter, lalloc, lfree, searchmode, data, NULL));
|
||||||
}
|
}
|
||||||
CATCH(e, iter(root, data));
|
PASS(e, iter(root, data));
|
||||||
if ( root-> right != NULL ) {
|
if ( root-> right != NULL ) {
|
||||||
PASS(e, aksl_tree_iterate(root->right, iter, lalloc, lfree, searchmode, data, NULL));
|
PASS(e, aksl_tree_iterate(root->right, iter, lalloc, lfree, searchmode, data, NULL));
|
||||||
}
|
}
|
||||||
@@ -300,12 +284,6 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate(
|
|||||||
FAIL_RETURN(e, AKERR_NOT_IMPLEMENTED, "Searchmode %d", searchmode);
|
FAIL_RETURN(e, AKERR_NOT_IMPLEMENTED, "Searchmode %d", searchmode);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} HANDLE(e, AKERR_ITERATOR_BREAK) {
|
|
||||||
// This is not an error condition, it's just telling us to stop early
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
} FINISH(e, true);
|
|
||||||
SUCCEED_RETURN(e);
|
SUCCEED_RETURN(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,23 +306,16 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate(aksl_ListNode *list, aksl_L
|
|||||||
FAIL_ZERO_RETURN(e, iter, AKERR_NULLPOINTER, "iter");
|
FAIL_ZERO_RETURN(e, iter, AKERR_NULLPOINTER, "iter");
|
||||||
aksl_ListNode *slow = list;
|
aksl_ListNode *slow = list;
|
||||||
aksl_ListNode *fast = list;
|
aksl_ListNode *fast = list;
|
||||||
while ( fast != NULL && fast->next != NULL ) {
|
aksl_ListNode *tail = list;
|
||||||
slow = slow->next;
|
do {
|
||||||
|
if ( fast != NULL && fast->next != NULL ) {
|
||||||
fast = fast->next->next;
|
fast = fast->next->next;
|
||||||
if ( fast == slow) {
|
|
||||||
FAIL_RETURN(e, AKERR_CIRCULAR_REFERENCE, "%p", list);
|
|
||||||
}
|
}
|
||||||
}
|
PASS(e, iter(slow, data));
|
||||||
while ( slow != NULL ) {
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(e, iter(slow, data));
|
|
||||||
slow = slow->next;
|
slow = slow->next;
|
||||||
} CLEANUP {
|
if ( fast != NULL && fast == slow) {
|
||||||
} PROCESS(e) {
|
FAIL(e, AKERR_CIRCULAR_REFERENCE, "%p", list);
|
||||||
} HANDLE(e, AKERR_ITERATOR_BREAK) {
|
|
||||||
// This is not an error condition, it's just telling us to stop early
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
} FINISH(e, true);
|
|
||||||
}
|
}
|
||||||
|
} while ( slow != NULL || (fast != NULL && fast->next != NULL) );
|
||||||
SUCCEED_RETURN(e);
|
SUCCEED_RETURN(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,216 +0,0 @@
|
|||||||
#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,319 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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"
|
|
||||||
|
|
||||||
#define MAX_VISITS 16
|
|
||||||
|
|
||||||
typedef struct VisitLog
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
|
|
||||||
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------- */
|
|
||||||
/* aksl_list_append */
|
|
||||||
/* ---------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
static int test_append_single_node(void)
|
|
||||||
{
|
|
||||||
aksl_ListNode head;
|
|
||||||
aksl_ListNode tail;
|
|
||||||
|
|
||||||
memset((void *)&head, 0x00, sizeof(head));
|
|
||||||
memset((void *)&tail, 0x00, sizeof(tail));
|
|
||||||
|
|
||||||
AKSL_CHECK_OK(aksl_list_append(&head, &tail));
|
|
||||||
AKSL_CHECK(head.next == &tail);
|
|
||||||
AKSL_CHECK(head.prev == NULL);
|
|
||||||
AKSL_CHECK(tail.prev == &head);
|
|
||||||
AKSL_CHECK(tail.next == NULL);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 failures = 0;
|
|
||||||
|
|
||||||
akerr_init();
|
|
||||||
|
|
||||||
AKSL_RUN(failures, test_append_single_node);
|
|
||||||
AKSL_RUN(failures, test_append_null_arguments);
|
|
||||||
AKSL_RUN(failures, test_append_detects_self_cycle);
|
|
||||||
AKSL_RUN(failures, test_append_detects_two_node_cycle);
|
|
||||||
|
|
||||||
AKSL_RUN(failures, test_iterate_null_arguments);
|
|
||||||
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);
|
|
||||||
|
|
||||||
AKSL_RUN(failures, test_pop_middle_node);
|
|
||||||
AKSL_RUN(failures, test_pop_head_node);
|
|
||||||
AKSL_RUN(failures, test_pop_tail_node);
|
|
||||||
AKSL_RUN(failures, test_pop_only_node);
|
|
||||||
AKSL_RUN(failures, test_pop_null_argument);
|
|
||||||
|
|
||||||
AKSL_REPORT(failures);
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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,113 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "aksl_capture.h"
|
|
||||||
|
|
||||||
#define MAX_LEAVES 7
|
|
||||||
#define HIDDEN_VALUE ((void *)17336)
|
|
||||||
|
|
||||||
typedef struct TreeSearchParams
|
|
||||||
{
|
|
||||||
void *value;
|
|
||||||
int steps;
|
|
||||||
aksl_TreeNode *node;
|
|
||||||
} TreeSearchParams;
|
|
||||||
|
|
||||||
static akerr_ErrorContext AKERR_NOIGNORE *find_value(aksl_TreeNode *node, void *data)
|
|
||||||
{
|
|
||||||
TreeSearchParams *parms = NULL;
|
|
||||||
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
|
|
||||||
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
|
|
||||||
parms = (TreeSearchParams *)data;
|
|
||||||
parms->steps += 1;
|
|
||||||
if ( node->leaf == parms->value ) {
|
|
||||||
parms->node = node;
|
|
||||||
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop");
|
|
||||||
}
|
|
||||||
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 failures = 0;
|
|
||||||
|
|
||||||
akerr_init();
|
|
||||||
|
|
||||||
AKSL_RUN(failures, test_dfs_preorder);
|
|
||||||
AKSL_RUN(failures, test_dfs_inorder);
|
|
||||||
AKSL_RUN(failures, test_dfs_postorder);
|
|
||||||
|
|
||||||
AKSL_REPORT(failures);
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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