Compare commits
20 Commits
trees
...
fd71bcc67b
| Author | SHA1 | Date | |
|---|---|---|---|
|
fd71bcc67b
|
|||
|
95e5002512
|
|||
|
07c448508b
|
|||
|
a37ba3fb89
|
|||
|
437da2960b
|
|||
|
82c47ed773
|
|||
|
a87cbfb26d
|
|||
|
566004afd6
|
|||
|
11c04923f8
|
|||
|
01734f511b
|
|||
| 8003239116 | |||
|
80205d5c4f
|
|||
|
54fb44cd2b
|
|||
|
68009ea0e3
|
|||
|
3e296c3bff
|
|||
|
5793f6a178
|
|||
|
57929be1af
|
|||
|
3e3ea41dc8
|
|||
|
e4597aef7d
|
|||
|
06b2c8ad8a
|
122
.gitea/workflows/ci.yaml
Normal file
122
.gitea/workflows/ci.yaml
Normal file
@@ -0,0 +1,122 @@
|
||||
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 }}."
|
||||
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y cmake gcc python3
|
||||
# scripts/coverage.py needs nothing but python3 and gcc's own gcov, so
|
||||
# there is no lcov/gcovr to install here.
|
||||
#
|
||||
# The gate is a ratchet, not a target: src/stdlib.c is at 99.1% of lines
|
||||
# and 45.1% of branches, so 90/40 fails on a real regression (a test
|
||||
# deleted, or new untested code added) without tripping over rounding.
|
||||
# The report is printed either way -- the uncovered lines it lists are the
|
||||
# missing tests.
|
||||
#
|
||||
# Branch coverage is back over 45 (the aksl_version_* functions are fully
|
||||
# covered, which lifted it from 44.3%), but the gate stays at 40: 0.1
|
||||
# points of headroom is not a ratchet, it is a coin flip on the next
|
||||
# rounding change.
|
||||
#
|
||||
# The branch gate was 45 against 51.0% until the libakerror 1.0.0 bump.
|
||||
# Nothing about this library's tests changed: line coverage held at
|
||||
# 99.0% (200/202) and function coverage at 100% (21/21), but the branch
|
||||
# denominator went from 661 to 1087 because the 1.0.0 PREPARE_ERROR /
|
||||
# FAIL_* macros expand to more branches at every call site in
|
||||
# src/stdlib.c, and most of the added branches are not reachable from the
|
||||
# way this library calls them. 337/661 became 481/1087 -- 144 more
|
||||
# branches covered, 426 more branches counted. Chasing them here would be
|
||||
# testing libakerror's macros, which is libakerror's mutation suite's job
|
||||
# (macros expand at the call site, so coverage cannot see them properly
|
||||
# from either side). Re-ratcheted rather than papered over.
|
||||
- name: coverage
|
||||
run: |
|
||||
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON \
|
||||
-DAKSL_COVERAGE_THRESHOLD=90 \
|
||||
-DAKSL_COVERAGE_BRANCH_THRESHOLD=40
|
||||
cmake --build build-coverage
|
||||
ctest --test-dir build-coverage --output-on-failure
|
||||
cat build-coverage/coverage-summary.txt
|
||||
- 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 is 89.6%
|
||||
# (155/173 killed) now that TODO.md sections 1.2-1.6 have tests; it was
|
||||
# 46.8% when only the list and tree functions were covered. 80 leaves
|
||||
# headroom for the runner while still failing on a real regression (tests
|
||||
# deleted, or new untested code added). The 18 survivors are listed in the
|
||||
# published report -- each one is a missing assertion.
|
||||
- name: mutation testing
|
||||
run: |
|
||||
python3 scripts/mutation_test.py \
|
||||
--target src/stdlib.c \
|
||||
--junit mutation-junit.xml \
|
||||
--threshold 80
|
||||
# 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 }}."
|
||||
100
.githooks/pre-push
Executable file
100
.githooks/pre-push
Executable file
@@ -0,0 +1,100 @@
|
||||
#!/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:-80}"
|
||||
|
||||
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
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "deps/libakerror"]
|
||||
path = deps/libakerror
|
||||
url = https://source.starfort.tech/andrew/libakerror.git
|
||||
82
AGENTS.md
Normal file
82
AGENTS.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
`libakstdlib` is a C shared library that wraps libc calls and small data
|
||||
structures in `libakerror` error contexts. Public API declarations live in
|
||||
`include/akstdlib.h`; implementation lives in `src/stdlib.c`. Tests are
|
||||
one-file CTest executables under `tests/test_<name>.c`, with shared test helpers
|
||||
in `tests/aksl_capture.h`. CMake package templates are in `cmake/` and
|
||||
`akstdlib.pc.in`. The vendored dependency is `deps/libakerror`; update it as a
|
||||
submodule rather than editing generated files under `build/`, `build-asan/` or
|
||||
`build-coverage/`.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
Initialize dependencies before a fresh build:
|
||||
|
||||
```sh
|
||||
git submodule update --init --recursive
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
Run the normal suite with `ctest --test-dir build --output-on-failure`. Use the
|
||||
instrumented build for memory and undefined-behavior checks:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build-asan -DAKSL_SANITIZE=ON
|
||||
cmake --build build-asan
|
||||
ctest --test-dir build-asan --output-on-failure
|
||||
```
|
||||
|
||||
For coverage, configure a third tree; the report is part of that suite (the
|
||||
`coverage_reset` / `coverage_report` CTest entries) and also lands in
|
||||
`build-coverage/coverage-summary.txt`:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON
|
||||
cmake --build build-coverage --target coverage
|
||||
```
|
||||
|
||||
Run `cmake --build build --target mutation` only when you need the slower
|
||||
mutation harness. `rebuild.sh` installs to `/home/andrew/local` and removes the
|
||||
local build directory, so treat it as a local convenience script.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
Use C with 4-space indentation; existing files sometimes use tabs for continued
|
||||
statements, so match the surrounding block. Public symbols use the `aksl_`
|
||||
prefix, structs use `aksl_<Name>`, and tests use `test_<feature>.c` plus static
|
||||
`test_<case>` functions. Preserve the `akerr_ErrorContext AKERR_NOIGNORE *`
|
||||
return convention and the `PREPARE_ERROR` / `FAIL_*` / `SUCCEED_RETURN` pattern.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
Add a new test by creating `tests/test_mything.c` and adding `mything` to the
|
||||
right list in `CMakeLists.txt`. `AKSL_TESTS` must exit zero.
|
||||
`AKSL_WILL_FAIL_TESTS` are deliberate abort/contract tests.
|
||||
`AKSL_KNOWN_FAILING_TESTS` assert documented defects from `TODO.md`; when one
|
||||
starts unexpectedly passing, move it into `AKSL_TESTS` with the fix.
|
||||
|
||||
`src/stdlib.c` is at 99.1% line coverage and CI gates it at 90 (line) / 40
|
||||
(branch), so new code needs tests in the same commit. Run
|
||||
`cmake --build build-coverage --target coverage` and check the uncovered-line
|
||||
listing before proposing a change. Tests for behaviour that `TODO.md` records as
|
||||
defective belong in `AKSL_KNOWN_FAILING_TESTS` asserting the *correct* contract —
|
||||
do not pin current-but-wrong behaviour in `AKSL_TESTS`, since that turns the
|
||||
eventual fix into a test failure.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
Recent commits use short imperative summaries, for example `Add memory wrapper
|
||||
tests` and `Make error-status assertions authoritative`. Keep commits focused
|
||||
and include tests with behavior changes. Pull requests should describe the
|
||||
changed API or behavior, list the CTest/sanitizer/mutation commands run, and
|
||||
link the relevant `TODO.md` item or issue when fixing a known defect.
|
||||
|
||||
## Agent-Specific Instructions
|
||||
|
||||
Do not modify generated build trees, profiling artifacts, or untracked scratch
|
||||
files unless explicitly asked. Prefer small, test-backed changes and update
|
||||
`README.md` or `TODO.md` when changing documented workflows or known failures.
|
||||
355
CMakeLists.txt
355
CMakeLists.txt
@@ -1,5 +1,95 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(akstdlib LANGUAGES C)
|
||||
# The single source of truth for the version. It flows from here into
|
||||
# include/akstdlib_version.h (generated from the .in template next to it), the
|
||||
# shared library's VERSION/SOVERSION, the Version: field in akstdlib.pc, and
|
||||
# akstdlibConfigVersion.cmake. Nothing else should spell a version number.
|
||||
#
|
||||
# 0.x on purpose: TODO.md section 2.1 still records four confirmed defects whose
|
||||
# fixes change documented behaviour (aksl_atoi's contract, aksl_realpath's
|
||||
# signature), so this library is not promising a stable API yet.
|
||||
project(akstdlib VERSION 0.1.0 LANGUAGES C)
|
||||
|
||||
# The granularity at which the ABI is allowed to break, and therefore the
|
||||
# soname: libakstdlib.so.0.1. Pre-1.0 that is MAJOR.MINOR, because a 0.x library
|
||||
# makes no compatibility promise across a minor bump. At 1.0 this becomes
|
||||
# ${PROJECT_VERSION_MAJOR} alone -- change it here, and AKSL_VERSION_SONAME in
|
||||
# the header template follows automatically because it is configured from this.
|
||||
if(PROJECT_VERSION_MAJOR EQUAL 0)
|
||||
set(AKSL_SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
|
||||
else()
|
||||
set(AKSL_SOVERSION "${PROJECT_VERSION_MAJOR}")
|
||||
endif()
|
||||
|
||||
set(AKSL_GENERATED_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated/include")
|
||||
set(AKSL_VERSION_HEADER "${AKSL_GENERATED_INCLUDE_DIR}/akstdlib_version.h")
|
||||
configure_file(
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/include/akstdlib_version.h.in"
|
||||
"${AKSL_VERSION_HEADER}"
|
||||
@ONLY
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
# Coverage build, off by default:
|
||||
# cmake -S . -B build-coverage -DAKSL_COVERAGE=ON
|
||||
# cmake --build build-coverage --target coverage
|
||||
# Instrumentation is applied per target below (the library and the test
|
||||
# binaries) rather than through CMAKE_C_FLAGS, so deps/libakerror is left
|
||||
# uninstrumented -- it has its own suite, and its .gcda would only be noise in
|
||||
# this project's report.
|
||||
option(AKSL_COVERAGE "Build the library and its tests with gcov instrumentation" OFF)
|
||||
# Minimum total line / branch coverage for the `coverage_report` CTest entry
|
||||
# that a -DAKSL_COVERAGE=ON build adds. 0 disables the gate and reports only.
|
||||
set(AKSL_COVERAGE_THRESHOLD 0 CACHE STRING
|
||||
"Fail the coverage_report test below this total line coverage percentage")
|
||||
set(AKSL_COVERAGE_BRANCH_THRESHOLD 0 CACHE STRING
|
||||
"Fail the coverage_report test below this total branch coverage percentage")
|
||||
|
||||
if(AKSL_COVERAGE)
|
||||
if(NOT CMAKE_C_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$")
|
||||
message(FATAL_ERROR
|
||||
"AKSL_COVERAGE=ON needs a gcov-compatible compiler; "
|
||||
"CMAKE_C_COMPILER_ID is ${CMAKE_C_COMPILER_ID}")
|
||||
endif()
|
||||
# -O0 because the optimizer folds and reorders lines until per-line counts
|
||||
# stop matching the source. -fprofile-abs-path makes gcov record absolute
|
||||
# source paths, which is what lets the report be read from any directory.
|
||||
set(AKSL_COVERAGE_COMPILE_FLAGS --coverage -O0 -g)
|
||||
if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
|
||||
list(APPEND AKSL_COVERAGE_COMPILE_FLAGS -fprofile-abs-path)
|
||||
endif()
|
||||
message(STATUS "AKSL_COVERAGE=ON: instrumenting akstdlib and its tests for gcov")
|
||||
endif()
|
||||
|
||||
# Add gcov instrumentation to one target. No-op unless AKSL_COVERAGE is set.
|
||||
function(aksl_target_coverage target)
|
||||
if(NOT AKSL_COVERAGE)
|
||||
return()
|
||||
endif()
|
||||
target_compile_options(${target} PRIVATE ${AKSL_COVERAGE_COMPILE_FLAGS})
|
||||
if(CMAKE_VERSION VERSION_LESS 3.13)
|
||||
set_property(TARGET ${target} APPEND_STRING PROPERTY LINK_FLAGS " --coverage")
|
||||
else()
|
||||
target_link_options(${target} PRIVATE --coverage)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
if(TARGET akerror::akerror)
|
||||
message(STATUS "FOUND akerror::akerror")
|
||||
@@ -11,9 +101,52 @@ include(CTest)
|
||||
include(GNUInstallDirs)
|
||||
include(CMakePackageConfigHelpers)
|
||||
|
||||
if(NOT TARGET akerror::akerror)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
find_package(akerror REQUIRED)
|
||||
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()
|
||||
|
||||
# libakerror namespaces its `mutation` target when it is embedded but not its
|
||||
# `coverage` target (deps/libakerror/CMakeLists.txt:172 vs :189), so a
|
||||
# -DAKSL_COVERAGE=ON build hits "another target with the same name already
|
||||
# exists" against the `coverage` target this project adds, and fails to
|
||||
# configure at all. Rename the dependency's on the way past rather than
|
||||
# dropping it: its coverage script drives its own instrumented build tree, so
|
||||
# `cmake --build build-coverage --target akerror_coverage` still does the right
|
||||
# thing. Remove this once the dependency namespaces it upstream -- see TODO.md.
|
||||
function(add_custom_target _name)
|
||||
if(AKSL_SUPPRESS_ADD_TEST AND _name STREQUAL "coverage")
|
||||
_add_custom_target(akerror_coverage ${ARGN})
|
||||
else()
|
||||
_add_custom_target(${ARGV})
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
|
||||
|
||||
set(AKSL_SUPPRESS_ADD_TEST FALSE)
|
||||
else()
|
||||
if(NOT TARGET akerror::akerror)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
find_package(akerror REQUIRED)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(akstdlib_install_cmakedir "${CMAKE_INSTALL_LIBDIR}/cmake/akstdlib")
|
||||
@@ -29,14 +162,29 @@ add_library(akstdlib SHARED
|
||||
|
||||
add_library(akstdlib::akstdlib ALIAS akstdlib)
|
||||
|
||||
# Specify include directories for the library's headers (if applicable)
|
||||
# Specify include directories for the library's headers (if applicable).
|
||||
# The generated directory carries akstdlib_version.h, which akstdlib.h includes;
|
||||
# it is PUBLIC because consumers building against the build tree need it too. On
|
||||
# install both headers land side by side in ${CMAKE_INSTALL_INCLUDEDIR}, so the
|
||||
# install interface needs no second entry.
|
||||
target_include_directories(akstdlib PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
$<BUILD_INTERFACE:${AKSL_GENERATED_INCLUDE_DIR}>
|
||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/>
|
||||
)
|
||||
|
||||
# VERSION gives the real file (libakstdlib.so.0.1.0); SOVERSION gives the symlink
|
||||
# and the ELF soname recorded in every consumer (libakstdlib.so.0.1), so a
|
||||
# consumer built against 0.1 will not silently load an ABI-incompatible 0.2.
|
||||
set_target_properties(akstdlib PROPERTIES
|
||||
VERSION ${PROJECT_VERSION}
|
||||
SOVERSION ${AKSL_SOVERSION}
|
||||
)
|
||||
|
||||
target_link_libraries(akstdlib PUBLIC akerror::akerror)
|
||||
|
||||
aksl_target_coverage(akstdlib)
|
||||
|
||||
set(main_lib_dest "lib/my_library-${MY_LIBRARY_VERSION}")
|
||||
install(TARGETS akstdlib
|
||||
EXPORT akstdlibTargets
|
||||
@@ -47,6 +195,7 @@ install(TARGETS akstdlib
|
||||
)
|
||||
|
||||
install(FILES "include/akstdlib.h" DESTINATION "include/")
|
||||
install(FILES "${AKSL_VERSION_HEADER}" DESTINATION "include/")
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akstdlib.pc DESTINATION "lib/pkgconfig/")
|
||||
|
||||
|
||||
@@ -62,9 +211,205 @@ configure_package_config_file(
|
||||
INSTALL_DESTINATION ${akstdlib_install_cmakedir}
|
||||
)
|
||||
|
||||
# Without this, find_package(akstdlib 0.1 REQUIRED) is refused for want of a
|
||||
# version file no matter what is installed -- which is exactly the gap that
|
||||
# stops cmake/akstdlib.cmake.in from requesting a version of akerror.
|
||||
#
|
||||
# SameMinorVersion mirrors the soname: pre-1.0, 0.1 and 0.2 are different ABIs.
|
||||
# It arrived in CMake 3.11 and this project declares 3.10, so fall back to
|
||||
# ExactVersion on older CMake. That is stricter than the soname rule -- it pins
|
||||
# the patch level too, so a 0.1.0 request refuses a compatible 0.1.1 -- but it is
|
||||
# never laxer, and wrongly refusing a good pairing beats wrongly accepting a bad
|
||||
# one. Drop the branch when the minimum moves past 3.11.
|
||||
if(CMAKE_VERSION VERSION_LESS 3.11)
|
||||
set(AKSL_VERSION_COMPATIBILITY ExactVersion)
|
||||
else()
|
||||
set(AKSL_VERSION_COMPATIBILITY SameMinorVersion)
|
||||
endif()
|
||||
|
||||
write_basic_package_version_file(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfigVersion.cmake"
|
||||
VERSION ${PROJECT_VERSION}
|
||||
COMPATIBILITY ${AKSL_VERSION_COMPATIBILITY}
|
||||
)
|
||||
|
||||
install(FILES
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfig.cmake"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfigVersion.cmake"
|
||||
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
|
||||
convert
|
||||
format
|
||||
linkedlist
|
||||
memory
|
||||
path
|
||||
status_registry
|
||||
stream
|
||||
strhash
|
||||
tree
|
||||
version
|
||||
)
|
||||
|
||||
set(AKSL_WILL_FAIL_TESTS
|
||||
)
|
||||
|
||||
set(AKSL_KNOWN_FAILING_TESTS
|
||||
convert_strict # TODO.md 2.1.5 -- ato* cannot report a bad conversion
|
||||
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)
|
||||
# The test binaries are instrumented too. The default report filters them out
|
||||
# (only src/ and include/ are shown), but `scripts/coverage.py --include
|
||||
# tests` then answers a different question: whether every test case and
|
||||
# helper branch in tests/ actually runs, which is how a test function that was
|
||||
# written but never wired into AKSL_RUN shows up.
|
||||
aksl_target_coverage(test_${_test})
|
||||
add_test(NAME ${_test} COMMAND test_${_test})
|
||||
list(APPEND AKSL_TEST_TARGETS 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
|
||||
)
|
||||
|
||||
# Both the coverage report and the mutation harness are Python scripts.
|
||||
find_package(Python3 COMPONENTS Interpreter)
|
||||
|
||||
# Code coverage. A -DAKSL_COVERAGE=ON build wires the report into the suite
|
||||
# itself, so `ctest --test-dir build-coverage` both runs the tests and prints
|
||||
# what they touched:
|
||||
#
|
||||
# coverage_reset deletes the accumulated .gcda counters. gcov counts are
|
||||
# cumulative, so without this every report would fold in
|
||||
# earlier runs and overstate coverage. FIXTURES_SETUP makes
|
||||
# CTest run it before any test that needs the fixture, even
|
||||
# under `ctest -j`.
|
||||
# coverage_report aggregates gcov output and prints the summary plus the
|
||||
# uncovered lines. FIXTURES_CLEANUP makes CTest run it after
|
||||
# the last test in the fixture, which is exactly when the
|
||||
# counters are complete.
|
||||
#
|
||||
# The report is a plain report unless AKSL_COVERAGE_THRESHOLD is set, in which
|
||||
# case coverage_report fails below that percentage. Same ratchet idea as the
|
||||
# mutation threshold: gate on the number you have, raise it as tests land.
|
||||
#
|
||||
# CTest hides the output of a passing test, so coverage_report also writes
|
||||
# <build>/coverage-summary.txt and <build>/coverage.xml (Cobertura). The
|
||||
# `coverage` target below builds, runs and prints in one step.
|
||||
if(AKSL_COVERAGE AND Python3_FOUND)
|
||||
set(AKSL_COVERAGE_ARGS
|
||||
${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/scripts/coverage.py
|
||||
--build ${CMAKE_CURRENT_BINARY_DIR}
|
||||
--source-root ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
add_test(NAME coverage_reset COMMAND ${AKSL_COVERAGE_ARGS} --zero)
|
||||
add_test(NAME coverage_report COMMAND ${AKSL_COVERAGE_ARGS}
|
||||
--threshold ${AKSL_COVERAGE_THRESHOLD}
|
||||
--branch-threshold ${AKSL_COVERAGE_BRANCH_THRESHOLD}
|
||||
--output ${CMAKE_CURRENT_BINARY_DIR}/coverage-summary.txt
|
||||
--cobertura ${CMAKE_CURRENT_BINARY_DIR}/coverage.xml)
|
||||
|
||||
set_tests_properties(coverage_reset PROPERTIES FIXTURES_SETUP AKSL_GCDA)
|
||||
set_tests_properties(coverage_report PROPERTIES FIXTURES_CLEANUP AKSL_GCDA)
|
||||
set_tests_properties(
|
||||
${AKSL_TESTS} ${AKSL_WILL_FAIL_TESTS} ${AKSL_KNOWN_FAILING_TESTS}
|
||||
PROPERTIES FIXTURES_REQUIRED AKSL_GCDA
|
||||
)
|
||||
# gcov has to be spawned once per .gcda, so give the report more room than the
|
||||
# 30s the test binaries get.
|
||||
set_tests_properties(coverage_reset coverage_report PROPERTIES TIMEOUT 300)
|
||||
|
||||
# Namespaced when embedded in another project, for the same reason the mutation
|
||||
# target below is: a sibling dependency may well ship a `coverage` target too.
|
||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
set(AKSL_COVERAGE_TARGET coverage)
|
||||
else()
|
||||
set(AKSL_COVERAGE_TARGET akstdlib_coverage)
|
||||
endif()
|
||||
|
||||
# Convenience entry point that also builds the test binaries first. It runs
|
||||
# the suite rather than the script directly, because the two fixture tests
|
||||
# above already reset the counters and produce the report -- and CTest pulls a
|
||||
# required fixture back in even when it is filtered out, so there is no way to
|
||||
# run the tests without them. The second command re-reads the same counters to
|
||||
# print the report that CTest suppressed for the passing coverage_report test
|
||||
# (it only spawns gcov again, it does not re-run anything).
|
||||
add_custom_target(${AKSL_COVERAGE_TARGET}
|
||||
COMMAND ctest --test-dir ${CMAKE_CURRENT_BINARY_DIR} --output-on-failure
|
||||
COMMAND ${AKSL_COVERAGE_ARGS}
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
USES_TERMINAL
|
||||
COMMENT "Running the test suite under gcov and reporting coverage"
|
||||
)
|
||||
add_dependencies(${AKSL_COVERAGE_TARGET} ${AKSL_TEST_TARGETS})
|
||||
elseif(AKSL_COVERAGE)
|
||||
message(WARNING "AKSL_COVERAGE=ON but Python3 was not found: "
|
||||
"instrumenting the build, but the coverage report "
|
||||
"(scripts/coverage.py) will not be wired into CTest")
|
||||
endif()
|
||||
|
||||
# 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
|
||||
# When embedded in another project, use a namespaced target to avoid collisions
|
||||
# with mutation targets provided by sibling dependencies.
|
||||
# This target covers both src/stdlib.c and include/akstdlib.h. CI runs the
|
||||
# narrower, faster src/stdlib.c set with a --threshold gate; see
|
||||
# .gitea/workflows/ci.yaml.
|
||||
if(Python3_FOUND)
|
||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
set(AKSL_MUTATION_TARGET mutation)
|
||||
else()
|
||||
set(AKSL_MUTATION_TARGET akstdlib_mutation)
|
||||
endif()
|
||||
add_custom_target(${AKSL_MUTATION_TARGET}
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/scripts/mutation_test.py
|
||||
--source-root ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
USES_TERMINAL
|
||||
COMMENT "Running mutation tests (breaks the library, expects tests to fail)"
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
# pkgconfig
|
||||
|
||||
338
README.md
Normal file
338
README.md
Normal file
@@ -0,0 +1,338 @@
|
||||
# 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.
|
||||
|
||||
### This library's own version
|
||||
|
||||
`libakstdlib` is at **0.1.0**. The version lives in exactly one place — the
|
||||
`project()` call in `CMakeLists.txt` — and flows from there into everything
|
||||
else, so a bump is a one-line edit:
|
||||
|
||||
| Artifact | Value at 0.1.0 | From |
|
||||
| --- | --- | --- |
|
||||
| `AKSL_VERSION_MAJOR` / `_MINOR` / `_PATCH` | `0` / `1` / `0` | `include/akstdlib_version.h.in` |
|
||||
| `AKSL_VERSION_STRING` | `"0.1.0"` | same |
|
||||
| `AKSL_VERSION_NUMBER` | `100` | same |
|
||||
| `AKSL_VERSION_SONAME` | `"0.1"` | same |
|
||||
| shared library | `libakstdlib.so.0.1.0`, soname `libakstdlib.so.0.1` | `VERSION` / `SOVERSION` |
|
||||
| `pkg-config --modversion akstdlib` | `0.1.0` | `akstdlib.pc` |
|
||||
| `find_package(akstdlib 0.1)` | accepted; `0.2` and `1.0` refused | `akstdlibConfigVersion.cmake` |
|
||||
|
||||
`akstdlib_version.h` is **generated** — that is why there is no such file in the
|
||||
source tree, only the `.in` template beside `akstdlib.h`. Don't hand-edit the
|
||||
copy in your build directory; change the template or `project()`.
|
||||
|
||||
It is `0.x` deliberately. `TODO.md` §2.1 still records four confirmed defects
|
||||
whose fixes change documented behaviour, so the API is not being promised yet.
|
||||
While the major version is `0`, **the soname carries `MAJOR.MINOR`**: 0.1 and 0.2
|
||||
are different ABIs and the loader will not substitute one for the other. At 1.0
|
||||
the soname becomes `MAJOR` alone — the `if(PROJECT_VERSION_MAJOR EQUAL 0)` in
|
||||
`CMakeLists.txt` and the matching `#if` in `tests/test_version.c` are the two
|
||||
places that encode this, and they are tested against each other.
|
||||
|
||||
`AKSL_VERSION_NUMBER` is computed rather than written as a literal, because a
|
||||
literal `000100` is *octal* in C and would make 0.1.0 compare as 64.
|
||||
|
||||
#### Compiled-against vs. loaded
|
||||
|
||||
The macros above record what a caller was **compiled** against. What it actually
|
||||
**loaded** is a different question, and the two can disagree:
|
||||
|
||||
```c
|
||||
int major, minor, patch;
|
||||
akerr_ErrorContext *e = aksl_version(&major, &minor, &patch); /* the loaded .so */
|
||||
const char *v = aksl_version_string(); /* likewise */
|
||||
|
||||
e = AKSL_VERSION_CHECK(); /* compares the two; AKERR_VALUE on a mismatch */
|
||||
```
|
||||
|
||||
`AKSL_VERSION_CHECK()` is a macro on purpose: it expands at *your* call site, so
|
||||
it captures the `AKSL_VERSION_*` you were built with and passes them to a
|
||||
function that compares against the values baked into the library. Calling
|
||||
`aksl_version_check()` with hand-written numbers defeats the whole mechanism.
|
||||
|
||||
Compatibility is defined as "same soname", so pre-1.0 both major and minor must
|
||||
match and the patch level is ignored — a caller built against 0.1.0 keeps working
|
||||
against 0.1.7, which is exactly the promise the shared soname makes.
|
||||
|
||||
In normal use the soname catches the mismatch first, at load time, and the check
|
||||
never fires. It earns its keep when the soname is bypassed: a hand-install that
|
||||
drops a 0.2.0 build in under the 0.1 filename, or a package that strips
|
||||
versioning. Then the loader is happy and only the check notices:
|
||||
|
||||
```
|
||||
compiled against : 0.1.0 (soname 0.1)
|
||||
loaded : 0.2.0 (0.2.0)
|
||||
MISMATCH DETECTED: compiled against libakstdlib 0.1.0, loaded 0.2.0 (soname 0.2)
|
||||
```
|
||||
|
||||
### The libakerror version floor
|
||||
|
||||
**libakerror 1.0.0 or newer is required.** That release made the status-name
|
||||
table private, moved consumer status codes into a band starting at
|
||||
`AKERR_FIRST_CONSUMER_STATUS` (256), made range ownership enforced rather than
|
||||
advisory, and gave the library an soname — see
|
||||
`deps/libakerror/UPGRADING.md`. It is a source *and* ABI break, so pairing this
|
||||
header with an older `akerror.h` is not a compile problem you can work around;
|
||||
the pairing is simply invalid.
|
||||
|
||||
Three things enforce the floor, because no single one covers every way the
|
||||
library gets consumed:
|
||||
|
||||
| Mechanism | Where | Catches |
|
||||
| --- | --- | --- |
|
||||
| `#error` on a missing `AKERR_FIRST_CONSUMER_STATUS` | `include/akstdlib.h` | a stale `akerror.h` earlier on the include path, at the first diagnostic rather than as a pile of errors inside `src/stdlib.c` |
|
||||
| `Requires: akerror >= 1.0.0` | `akstdlib.pc.in` | a pkg-config consumer, which also now gets `-lakerror` transitively |
|
||||
| `find_dependency(akerror)` | `cmake/akstdlib.cmake.in` | a `find_package(akstdlib)` consumer, which previously failed with a bare *"akerror::akerror not found"* out of the generated targets file |
|
||||
|
||||
The header guard feature-tests rather than version-tests because libakerror
|
||||
publishes no version macro; `AKERR_FIRST_CONSUMER_STATUS` is the symbol 1.0.0
|
||||
introduced, so its absence is what "older than 1.0.0" actually looks like. The
|
||||
CMake path requests no version for the same kind of reason: libakerror installs
|
||||
no `akerrorConfigVersion.cmake`, so `find_dependency(akerror 1.0.0)` would be
|
||||
refused for want of a version file no matter which akerror is installed.
|
||||
|
||||
**libakstdlib defines no status codes of its own.** It raises libakerror's
|
||||
`AKERR_*` codes and propagates the host's `errno` values, all of which live in
|
||||
libakerror's reserved `0`–`255` band, so it reserves no range and an application
|
||||
is free to allocate from `AKERR_FIRST_CONSUMER_STATUS` without coordinating with
|
||||
it. `tests/test_status_registry.c` pins that, along with the requirement that
|
||||
every status this library raises actually has a name registered — an unnamed one
|
||||
degrades to `"Unknown Error"` in every later stack trace, which nothing else
|
||||
would notice.
|
||||
|
||||
## Testing
|
||||
|
||||
There are four harnesses. The first three take seconds; the fourth 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, `aksl_temp_file()` for tests that need a real
|
||||
file to work on, and an `AKSL_RUN()` driver that additionally fails any test which
|
||||
leaks a slot from libakerror's error pool.
|
||||
|
||||
One file per area of the API: `convert` (the `ato*` family), `format` (the
|
||||
`printf` family), `stream` (`fopen`/`fread`/`fwrite`/`fclose`), `path`
|
||||
(`aksl_realpath`), `strhash`, `memory`, `linkedlist`, `tree`, and
|
||||
`status_registry` (this library's side of the libakerror status-registry
|
||||
contract — see "The libakerror version floor" above).
|
||||
|
||||
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. Code coverage
|
||||
|
||||
```sh
|
||||
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON
|
||||
cmake --build build-coverage --target coverage
|
||||
```
|
||||
|
||||
`-DAKSL_COVERAGE=ON` compiles the library and the tests with `--coverage -O0`,
|
||||
and wires the report into the suite itself, so a plain
|
||||
`ctest --test-dir build-coverage` also produces it. Two extra CTest entries
|
||||
appear, held in place by a CTest fixture rather than by declaration order, so
|
||||
they work under `ctest -j` too:
|
||||
|
||||
| Test | When | Does |
|
||||
|---|---|---|
|
||||
| `coverage_reset` | before every other test | deletes the accumulated `.gcda` counters |
|
||||
| `coverage_report` | after every other test | aggregates `gcov` output, prints the summary, applies the threshold gate |
|
||||
|
||||
The reset matters: gcov counters are cumulative, so without it each report would
|
||||
fold in every earlier run and overstate coverage.
|
||||
|
||||
`coverage` here is this project's target. libakerror ships a `coverage` target of
|
||||
its own and, unlike its `mutation` target, does not namespace it when embedded,
|
||||
so a top-level `-DAKSL_COVERAGE=ON` build would collide on the name and fail to
|
||||
configure at all. `CMakeLists.txt` renames the dependency's to `akerror_coverage`
|
||||
on the way past — it drives its own instrumented build tree, so
|
||||
`cmake --build build-coverage --target akerror_coverage` still works. The
|
||||
workaround goes away when libakerror namespaces it upstream; see `TODO.md` §2.3.
|
||||
|
||||
CTest hides the output of a passing test, so `coverage_report` also writes
|
||||
`build-coverage/coverage-summary.txt` (the same text report) and
|
||||
`build-coverage/coverage.xml` (Cobertura, for CI publishers). The `coverage`
|
||||
target above prints the report to the terminal for you; otherwise read the file
|
||||
or use `ctest --test-dir build-coverage -V -R coverage_report`.
|
||||
|
||||
The report lists per-file line, branch and function coverage, then every
|
||||
uncovered line and every function the suite never called — that listing is the
|
||||
actionable part, the same way surviving mutants are for the harness below.
|
||||
|
||||
Drive the script directly for anything narrower:
|
||||
|
||||
```sh
|
||||
scripts/coverage.py --build build-coverage # report on disk counters
|
||||
scripts/coverage.py --build build-coverage --summary-only # totals only
|
||||
scripts/coverage.py --build build-coverage --include tests # coverage of the tests themselves
|
||||
scripts/coverage.py --build build-coverage --run-tests # reset, run ctest, report
|
||||
scripts/coverage.py --build build-coverage --threshold 90 --branch-threshold 40
|
||||
```
|
||||
|
||||
It needs nothing but Python 3 and gcc's own `gcov` — no lcov, gcovr or genhtml.
|
||||
|
||||
To gate on coverage, set the threshold at configure time; `coverage_report` then
|
||||
fails below it, and the same regression-ratchet logic applies as for the mutation
|
||||
score:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON \
|
||||
-DAKSL_COVERAGE_THRESHOLD=90 -DAKSL_COVERAGE_BRANCH_THRESHOLD=40
|
||||
```
|
||||
|
||||
**Where it stands.** `src/stdlib.c` is at **99.1% of lines (217/219)**, **45.1% of
|
||||
branches (519/1151)** and **25/25 functions**, so 90/40 above is a ratchet with
|
||||
headroom rather than a target. The two uncovered lines are both
|
||||
`} HANDLE(e, AKERR_ITERATOR_BREAK) {` — in libakerror that macro begins with the
|
||||
`break;` belonging to `PROCESS`'s `case 0:` arm, which is only reachable when a
|
||||
callback returns a non-NULL error context whose status is *zero*. That is the
|
||||
pathological case §2.2.1 of `TODO.md` exists to remove, so it is left uncovered
|
||||
deliberately rather than pinned by a test.
|
||||
|
||||
Branch coverage sits far below line coverage because most branches in this file
|
||||
are inside the `FAIL_*`/`ATTEMPT`/`FINISH` macro expansions — pool exhaustion,
|
||||
stack-trace buffer limits, `akerr_valid_error_address` failures — and belong to
|
||||
libakerror's own suite rather than to this one. The libakerror 1.0.0 bump made
|
||||
that gap wider without changing a line of this library: the branch denominator
|
||||
went from 661 to 1087 as those macros grew, so the same tests that scored 51.0%
|
||||
(337/661) dropped to 44.3% (481/1087). The gate moved 45 → 40 to match; line and
|
||||
function coverage did not move at all. Adding the `aksl_version_*` family and its
|
||||
tests brought the figure back to 45.1% (519/1151), but the gate stays at 40 —
|
||||
0.1 points of headroom is not a ratchet.
|
||||
|
||||
Two caveats. Coverage is measured at `-O0`, because the optimizer reorders lines
|
||||
until per-line counts stop matching the source — so a coverage build is not the
|
||||
build to profile. And gcov flushes its counters at normal process exit, which an
|
||||
`AKSL_WILL_FAIL_TESTS` entry that aborts by design never reaches: such a test
|
||||
contributes no coverage data at all, so lines only it reaches are reported as
|
||||
uncovered.
|
||||
|
||||
### 4. 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 80
|
||||
```
|
||||
|
||||
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 80`. That is a regression ratchet
|
||||
rather than a quality bar: the current score is **89.6% (155/173 killed)**, up
|
||||
from 46.8% before the wrapper tests landed. Raise the threshold as the remaining
|
||||
survivors are turned into assertions.
|
||||
|
||||
The 18 survivors cluster in three places, and each names a real gap rather than a
|
||||
test-harness artifact:
|
||||
|
||||
- **Statements whose absence nothing observes** — deleting `free(ptr)`,
|
||||
`obj->next = NULL`, or a `SUCCEED_RETURN` leaves behaviour the suite does not
|
||||
look at (a leak, a stale pointer, a success that was already NULL).
|
||||
- **The `aksl_list_append` cycle/tail walk** (`tail = slow`, `slow = slow->next`,
|
||||
`tail = fast`) — the function is broken in exactly this area (`TODO.md` §2.1.1),
|
||||
so its known-failing test cannot pin the internals yet.
|
||||
- **`lalloc`/`lfree` defaulting in `aksl_tree_iterate`** — dead parameters
|
||||
(§2.2.8): they are defaulted and then never called, so inverting the guard
|
||||
changes nothing observable.
|
||||
|
||||
## 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 80, keep it in step with
|
||||
`.gitea/workflows/ci.yaml`) and `AKSL_HOOK_BUILD_DIR`.
|
||||
715
TODO.md
Normal file
715
TODO.md
Normal file
@@ -0,0 +1,715 @@
|
||||
# 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 80` and a JUnit report.
|
||||
**Current score: 89.6%** — 155 killed, 18 survived. It was 46.8% (81 killed, 92
|
||||
survived) before §1.2–1.6 were written; the remaining survivors are:
|
||||
|
||||
| surviving mutants | where | why |
|
||||
|---|---|---|
|
||||
| 5 | deleted statements whose absence nothing observes (`free(ptr)`, `obj->next = NULL`, three `SUCCEED_RETURN`s) | needs an assertion on the side effect, not on the status |
|
||||
| 4 | `aksl_list_append`'s cycle/tail walk | the function is broken here (§2.1.1); its known-failing test cannot pin the internals yet |
|
||||
| 4 | `lalloc`/`lfree` defaulting in `aksl_tree_iterate` | dead parameters (§2.2.8) — defaulted, then never called |
|
||||
| 3 | `*count == -1` in the `printf` family | `*count` on the error path is itself a contract gap (§1.3) |
|
||||
| 2 | `feof` in `aksl_fwrite`, `break` after the BFS `FAIL_RETURN` | unreachable in practice |
|
||||
|
||||
The threshold is a regression ratchet, not a quality bar; raise it as those survivors
|
||||
become assertions. Each one 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
|
||||
|
||||
- [x] `aksl_malloc` — happy path returns non-NULL and writes through `dst`.
|
||||
- [x] `aksl_malloc(n, NULL)` → `AKERR_NULLPOINTER`, message content asserted.
|
||||
- [ ] `aksl_malloc(0, &p)` — pin down the contract. Today the result depends on whether the
|
||||
platform's `malloc(0)` returns NULL; if it does, the wrapper reports `errno`, which may
|
||||
be `0`. See §2.2.1.
|
||||
- [ ] `aksl_malloc(SIZE_MAX, &p)` → allocation failure surfaces `ENOMEM`, and `*dst` is left
|
||||
NULL rather than garbage.
|
||||
- [x] `aksl_free(NULL)` → `AKERR_NULLPOINTER` (assert the documented behaviour, since
|
||||
`free(NULL)` is legal C and callers will be surprised).
|
||||
- [x] `aksl_free` happy path, and a malloc→free round trip under ASan.
|
||||
- [x] `aksl_memset` — happy path fills the buffer; `n == 0` is a no-op; `s == NULL` →
|
||||
`AKERR_NULLPOINTER`.
|
||||
- [x] `aksl_memcpy` — happy path copies; `d == NULL` and `s == NULL` each →
|
||||
`AKERR_NULLPOINTER`; `n == 0` with valid pointers succeeds.
|
||||
- [ ] `aksl_memcpy` with overlapping regions — decide and test whether this is rejected
|
||||
(`AKERR_VALUE`) or documented as UB like `memcpy`.
|
||||
|
||||
### 1.2 File I/O
|
||||
|
||||
Covered by `tests/test_stream.c`.
|
||||
|
||||
- [x] `aksl_fopen` happy path on a temp file; `*fp` is written.
|
||||
- [x] `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`.
|
||||
- [x] `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.
|
||||
- [x] `aksl_fread` full read; short read at EOF → `AKERR_EOF`; read from a write-only stream
|
||||
→ `AKERR_IO`; `fp == NULL` → `AKERR_NULLPOINTER`. `ptr == NULL` is still unchecked and
|
||||
untested (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.
|
||||
- [x] `aksl_fwrite` happy path; write to a read-only stream → `AKERR_IO`; `fp == NULL` →
|
||||
`AKERR_NULLPOINTER`. The full-device (`/dev/full`) case is still open.
|
||||
- [x] `aksl_fclose` happy path; `NULL` → `AKERR_NULLPOINTER`. Double-close is still
|
||||
undecided, so it is neither caught nor tested.
|
||||
- [ ] `aksl_fclose` on a stream whose buffered flush fails (`/dev/full`) → non-zero `fclose`
|
||||
surfaces `errno`.
|
||||
- [x] Round-trip test: `fopen` → `fwrite` → `fclose` → `fopen` → `fread` → compare bytes.
|
||||
|
||||
### 1.3 Formatted output
|
||||
|
||||
Covered by `tests/test_format.c`.
|
||||
|
||||
- [x] `aksl_printf` / `aksl_fprintf` / `aksl_sprintf` happy paths, asserting both the byte
|
||||
count written through `count` and the produced text. (`aksl_printf` is checked by
|
||||
pointing `stdout` at a temp file for the duration of the call.)
|
||||
- [x] Each of the three with every pointer argument NULL in turn → `AKERR_NULLPOINTER`.
|
||||
- [x] `aksl_fprintf` to a read-only stream → the `errno` it saw (`EBADF`) with "Short write"
|
||||
in the message. `*count` is still left holding `-1`; the test asserts the status only,
|
||||
so it will not have to change when that is fixed.
|
||||
- [ ] `aksl_sprintf` with a format that overflows the destination — currently unbounded
|
||||
(§2.2.4). Test the `aksl_snprintf` replacement once it exists.
|
||||
- [x] Regression test for the missing `va_end` (§2.1.4) — 512 variadic calls in a loop, which
|
||||
the sanitizer build also runs.
|
||||
- [ ] 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
|
||||
|
||||
Happy paths and NULL guards are covered by `tests/test_convert.c`; the strict-conversion
|
||||
contract is asserted by `tests/test_convert_strict.c`, which is registered as a known
|
||||
failure until §2.1.5 is fixed.
|
||||
|
||||
- [x] `aksl_atoi` / `atol` / `atoll` / `atof` happy paths, including negative values and
|
||||
leading whitespace.
|
||||
- [x] NULL `nptr` and NULL `dest` for each → `AKERR_NULLPOINTER`.
|
||||
- [x] **Non-numeric input** (`"not a number"`) — **[CONFIRMED]** currently returns *success*
|
||||
with `*dest == 0`. `AKERR_VALUE` is asserted by the known-failing test.
|
||||
- [x] **Overflow** (`"99999999999999999999"`) — **[CONFIRMED]** currently returns success with
|
||||
a garbage value (`-1` on this box). `ERANGE` is asserted by the known-failing test.
|
||||
- [x] Empty string, `" "` and `"12abc"` (trailing junk) → `AKERR_VALUE`, in the known-failing
|
||||
test. `"0x10"` and `"inf"`/`"nan"` for `atof` are still open.
|
||||
- [ ] `LONG_MIN`/`LONG_MAX`/`LLONG_MIN`/`LLONG_MAX` boundary strings round-trip exactly.
|
||||
|
||||
### 1.5 `aksl_realpath`
|
||||
|
||||
Covered by `tests/test_path.c`. Every failure case there passes a zeroed `resolved_path`,
|
||||
because the wrapper's own error path formats that buffer with `%s` (§2.1.6).
|
||||
|
||||
- [x] Happy path on an existing file and on a symlink chain; result matches `realpath(3)`.
|
||||
- [x] Non-existent path → `ENOENT`.
|
||||
- [x] A path component that is not a directory → `ENOTDIR`.
|
||||
- [ ] Symlink loop → `ELOOP`.
|
||||
- [x] `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`
|
||||
|
||||
Covered by `tests/test_strhash.c`.
|
||||
|
||||
- [x] Known-answer vectors: `djb2("")` == 5381; a handful of fixed strings with their
|
||||
pre-computed 32-bit values.
|
||||
- [x] `len == 0` returns 5381 regardless of `str` contents.
|
||||
- [x] 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 (5868578; the wrapper returns the
|
||||
sign-extended 5859874 today). Every vector in the test file is 7-bit ASCII precisely so
|
||||
that it says nothing about this case either way.
|
||||
- [x] Embedded NUL bytes are hashed (the function is length-driven, not NUL-driven).
|
||||
- [x] 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).
|
||||
- [x] `AKSL_TREE_SEARCH_BFS` and `AKSL_TREE_SEARCH_BFS_RIGHT` → `AKERR_NOT_IMPLEMENTED`
|
||||
today, asserted in `tests/test_tree.c` along with the callback never being called;
|
||||
replace with real order assertions once implemented (§3 / §2.2.9).
|
||||
- [x] `aksl_tree_iterate` NULL `root` / NULL `iter` → `AKERR_NULLPOINTER`, and a callback error
|
||||
that is *not* `AKERR_ITERATOR_BREAK` propagates out to the caller.
|
||||
- [ ] **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)
|
||||
|
||||
Four of these 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`,
|
||||
`tests/test_tree_iterate_break.c` and `tests/test_convert_strict.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.
|
||||
`tests/test_convert_strict.c` asserts that contract and is registered as a known failure.
|
||||
|
||||
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; `akstdlib.h` pulls in
|
||||
`stdio.h`/`stdlib.h`/`string.h`/`stdint.h` into every consumer's namespace. The version
|
||||
macros this item also asked for are **done**: `AKSL_VERSION_MAJOR`/`_MINOR`/`_PATCH`/
|
||||
`_STRING`/`_NUMBER`/`_SONAME` are generated into `akstdlib_version.h` from
|
||||
`project(akstdlib VERSION …)`, with `aksl_version()`/`aksl_version_string()`/
|
||||
`aksl_version_soname()` and `AKSL_VERSION_CHECK()` reporting the *loaded* library so a
|
||||
header/`.so` mismatch is nameable. See `tests/test_version.c` and `README.md`.
|
||||
|
||||
17. **Doxygen coverage is 2 functions out of 24.** 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). The four
|
||||
`aksl_version_*` entry points have prose comments in the header but no Doxygen tags.
|
||||
|
||||
### 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.
|
||||
- [x] **The `deps/libakerror` submodule was pinned 22 commits behind `main`** (pinned at
|
||||
`4fad0ce "Add gitea workflow"`). Bumped to `5ff8790`, which is libakerror **1.0.0** —
|
||||
the release that made the status-name table private, moved consumer status codes to a
|
||||
band starting at `AKERR_FIRST_CONSUMER_STATUS` (256), made range ownership enforced,
|
||||
and gave the library an soname. See `deps/libakerror/UPGRADING.md`. The bump also
|
||||
brings the refcount-leak fix, the stack-trace buffer-overflow fix and the
|
||||
format-string fix. Nothing in this library's sources used the removed
|
||||
`AKERR_MAX_ERR_VALUE`, `__AKERR_ERROR_NAMES`, `AKERR_STATUS_RANGE_OK` or
|
||||
`AKERR_STATUS_NAME_OK`, so the source change was confined to the build, the packaging
|
||||
and one compile-time guard. What it did move is recorded in the three items below.
|
||||
- [ ] **libakerror does not namespace its `coverage` target when embedded**, only its
|
||||
`mutation` target (`deps/libakerror/CMakeLists.txt:172` vs `:189`). A
|
||||
`-DAKSL_COVERAGE=ON` top-level build therefore failed to configure at all —
|
||||
*"another target with the same name already exists"* — the moment the dependency
|
||||
gained a coverage target. Worked around in `CMakeLists.txt` by shadowing
|
||||
`add_custom_target` for the duration of the `add_subdirectory()` call and renaming the
|
||||
dependency's to `akerror_coverage`, alongside the existing `add_test` shadow. **Fix
|
||||
upstream and delete the workaround**: libakerror should apply the same
|
||||
`CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR` test to `coverage` that it
|
||||
already applies to `mutation`.
|
||||
- [ ] **Branch coverage of `src/stdlib.c` fell from 51.0% to 44.3% on the 1.0.0 bump**, with
|
||||
no change to this library's sources or tests. Line coverage held at 99.0% (200/202)
|
||||
and function coverage at 100% (21/21); the branch *denominator* went from 661 to 1087
|
||||
because the 1.0.0 `PREPARE_ERROR`/`FAIL_*` macros expand to more branches at every
|
||||
call site. 337/661 became 481/1087 — 144 more branches covered, 426 more counted. The
|
||||
CI branch gate was re-ratcheted 45 → 40 to match. Most of the added branches are
|
||||
unreachable from the way this library calls the macros, so raising the number here
|
||||
would mean testing libakerror's macros rather than this library; that belongs to
|
||||
libakerror's mutation suite, since macros expand at the call site and coverage cannot
|
||||
see them properly from either side. Revisit if the gap ever hides a real regression.
|
||||
- [x] **`project()` declared no `VERSION`**, so `@PROJECT_VERSION@` expanded to nothing, the
|
||||
installed `akstdlib.pc` shipped an empty `Version:` field, and `libakstdlib.so` carried
|
||||
no soname. Now `project(akstdlib VERSION 0.1.0)`, with `SOVERSION` `0.1`
|
||||
(`MAJOR.MINOR` while major is 0, `MAJOR` from 1.0 — the `if()` in `CMakeLists.txt` and
|
||||
the matching `#if` in `tests/test_version.c` encode the rule and are tested against
|
||||
each other), an `akstdlibConfigVersion.cmake` at `SameMinorVersion`, and version macros
|
||||
generated into `akstdlib_version.h`. Verified: `find_package(akstdlib 0.1)` is accepted
|
||||
while `0.2` and `1.0` are refused, and a 0.2.0 build dropped in under the 0.1 filename
|
||||
is caught by `AKSL_VERSION_CHECK()`.
|
||||
- [ ] **libakerror still installs no `akerrorConfigVersion.cmake`**, so
|
||||
`cmake/akstdlib.cmake.in` has to call `find_dependency(akerror)` with no version — a
|
||||
request for one would be refused for want of a version file regardless of what is
|
||||
installed. libakstdlib now does this correctly and libakerror does not; fix it there
|
||||
with the same `write_basic_package_version_file()` call, then add the `1.0.0` floor to
|
||||
the `find_dependency` here. Until then the floor rests on `akstdlib.pc`'s `Requires:`
|
||||
and the `#error` guard in `akstdlib.h`.
|
||||
- [ ] **`aksl_version_check()` ignores its `patch` argument** (`src/stdlib.c`, the `(void)patch`),
|
||||
which is correct for the current "same soname" rule but means the parameter exists only
|
||||
so the error message can name the caller's full version. If a future rule needs patch
|
||||
to participate, that is the line to change.
|
||||
- [ ] **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.
|
||||
|
||||
## 4. Evidence from the first full consumer
|
||||
|
||||
`akbasic` (`source.starfort.tech/andrew/akbasic`) is a ~6,300-line C interpreter built on this
|
||||
library and `libakerror`. It is the first consumer to exercise the whole surface rather than a
|
||||
corner of it, so what it *could not* use is worth recording: it prioritizes the wishlist in §3
|
||||
by what a real port actually reaches for, and it confirms which §2 entries have teeth.
|
||||
|
||||
**The headline number.** Across `src/`, akbasic makes **10 calls into this library** and
|
||||
**116 calls to raw libc**:
|
||||
|
||||
| `aksl_*` used | Count |
|
||||
|---|---|
|
||||
| `aksl_fopen` / `aksl_fclose` | 6 |
|
||||
| `aksl_fprintf` | 2 |
|
||||
| `aksl_fwrite` | 1 |
|
||||
| `aksl_strhash_djb2` | 1 |
|
||||
|
||||
| Raw libc it had to use instead | Count | Wanted from §3 |
|
||||
|---|---|---|
|
||||
| `snprintf` | 28 | §3.1 bounded string formatting |
|
||||
| `strlen` | 37 | §3.1 strings |
|
||||
| `strcmp` | 16 | §3.1 strings |
|
||||
| `strncpy` | 15 | §3.1 strings |
|
||||
| `memcpy` / `memset` | 16 | wrapped, but see §2.2.11 |
|
||||
| `strtoll` / `strtod` | 2 | §3.1 string → number |
|
||||
| `fgets` | 2 | §3.1 stream I/O |
|
||||
| `strstr` | 1 | §3.1 strings |
|
||||
|
||||
A library whose value proposition is "turn silent libc failures into error contexts" is being
|
||||
bypassed 92% of the time by the consumer most committed to it. Every one of those calls is a
|
||||
place where a failure goes unreported.
|
||||
|
||||
### 4.1 What the port had to write for itself
|
||||
|
||||
1. **A strict `strtoll`/`strtod` wrapper** (`akbasic/src/convert.c`, ~60 lines). This is §2.1.5
|
||||
biting, and it is not cosmetic: the Go reference this port reproduces checks `strconv`'s
|
||||
error at four sites and turns each into a BASIC error. Routing them through `aksl_atoi`
|
||||
would have converted four *diagnosable errors* into *wrong answers* — `VAL("garbage")`
|
||||
silently returning `0.0` rather than raising, and a malformed line number becoming line 0.
|
||||
akbasic's own `TODO.md` §1.9 formally bans the `aksl_ato*` family for this reason. **The
|
||||
fix wanted is exactly §3.1's `strtol` family plus the endptr/`errno`/range contract §2.1.5
|
||||
describes**; when it lands, `src/convert.c` gets deleted.
|
||||
|
||||
2. **A fixed-capacity string-keyed hash table** (`akbasic/src/symtab.c`, ~130 lines). §3.6
|
||||
already calls for a "hash map built on `aksl_strhash_djb2`". This is that, three times over:
|
||||
the interpreter needs one for variables, one for functions and one for labels per scope. It
|
||||
is open-addressed with linear probing over a caller-supplied slot count, refuses rather than
|
||||
resizes when full, and is the single most obviously-missing data structure in the library.
|
||||
Worth lifting more or less verbatim if you want a starting point.
|
||||
|
||||
3. **The bounded-copy-with-truncation-as-error idiom, ten times.** `strncpy` followed by an
|
||||
explicit NUL and a preceding length check appears at ten sites across six files. §3.1 lists
|
||||
`strncpy` "with truncation reported as an error rather than silently accepted, which is the
|
||||
whole value proposition here" — agreed, and the repetition is the evidence.
|
||||
|
||||
4. **Uppercase folding for case-insensitive lookup, three times.** Verb and function names are
|
||||
case-insensitive in BASIC while variable names are not, so the port folds case before every
|
||||
keyword lookup: by hand at `src/environment.c:197` and `src/parser_commands.c:113`, and
|
||||
through `<ctype.h>`'s `toupper` in `src/verbs.c`. §3.1's `strcasecmp`/`strncasecmp` would
|
||||
remove all three.
|
||||
|
||||
### 4.2 Confirmed with impact
|
||||
|
||||
- **§2.2.4 (`aksl_sprintf` is unbounded)** — akbasic never calls it, deliberately. Its 28
|
||||
`snprintf` calls all write into fixed buffers whose size is the whole point, and there is no
|
||||
`aksl_snprintf` to route them through. This is the single highest-value item in §3.1 for a
|
||||
consumer of this shape.
|
||||
- **§2.2.2 (`aksl_fopen` does not validate `pathname`/`mode`)** — real, and reached from user
|
||||
input. akbasic's `DLOAD`/`DSAVE` take a filename from a BASIC string expression, so an empty
|
||||
or unevaluated one is reachable from a running program; `akbasic_cmd_dload` validates the
|
||||
name itself before handing it over, with a comment pointing here.
|
||||
- **§2.2.6 (`aksl_strhash_djb2` sign-extends high bytes)** — benign for akbasic, which only
|
||||
ever hashes 7-bit ASCII identifiers, and noted in its §1.3 as a hazard for anyone who later
|
||||
keys a table on a filename or a string literal. Still worth fixing; the wrong answer is
|
||||
platform-dependent, which is the bad kind of benign.
|
||||
- **§2.1.4 (`va_end` is never called)** — akbasic's stdio text sink is built on `aksl_fprintf`
|
||||
and therefore runs this UB on every line of program output. Nothing has misbehaved on
|
||||
x86-64 glibc, which is exactly what makes it worth fixing before something does.
|
||||
|
||||
### 4.3 Not needed, and why
|
||||
|
||||
Recorded so the wishlist does not get prioritized purely by this consumer's shape. akbasic
|
||||
uses **no** allocator (§3.1 memory), **no** lists or trees (§3.6), and **no** `aksl_realpath`:
|
||||
it draws everything from fixed pools by design, and the `ak*` no-dynamic-allocation rule means
|
||||
`calloc`/`realloc` would go unused here even if they existed. A consumer that does allocate
|
||||
would weight §3.1's memory section far higher. The list and tree defects in §2.1.1–3 were
|
||||
avoided rather than survived — akbasic's symbol tables are open-addressed precisely because
|
||||
`aksl_list_append` truncates.
|
||||
@@ -6,5 +6,11 @@ includedir=${exec_prefix}/include
|
||||
Name: akstdlib
|
||||
Description: C stdlib with akerror sanity wrappers
|
||||
Version: @PROJECT_VERSION@
|
||||
# akerror is a public dependency, not a private one: akstdlib.h includes
|
||||
# akerror.h and every entry point returns an akerr_ErrorContext *, so a consumer
|
||||
# compiles against both headers and links both libraries. 1.0.0 is the floor --
|
||||
# it is the release that made the status registry private and gave the library an
|
||||
# soname, and akstdlib.h refuses to compile against anything older.
|
||||
Requires: akerror >= 1.0.0
|
||||
Cflags: -I${includedir}/
|
||||
Libs: -L${libdir} -lakstdlib
|
||||
@@ -1,5 +1,17 @@
|
||||
# cmake/MyLibraryConfig.cmake.in
|
||||
include(CMakeFindDependencyMacro) # If your library has dependencies
|
||||
# find_dependency(AnotherDependency REQUIRED) # Example dependency
|
||||
# cmake/akstdlib.cmake.in -- installed as akstdlibConfig.cmake
|
||||
#
|
||||
# akstdlibTargets.cmake names akerror::akerror in the INTERFACE_LINK_LIBRARIES of
|
||||
# the imported akstdlib target, because akstdlib links it PUBLIC. Without the
|
||||
# find_dependency below, a consumer that has not already found akerror itself
|
||||
# gets a bare "target not found" out of the generated targets file rather than a
|
||||
# message naming the dependency it is missing.
|
||||
#
|
||||
# No version is requested here. libakerror installs no akerrorConfigVersion.cmake,
|
||||
# so find_dependency(akerror 1.0.0) would be refused for want of a version file
|
||||
# regardless of which akerror is actually installed. The 1.0.0 floor is carried by
|
||||
# the `Requires: akerror >= 1.0.0` line in akstdlib.pc and enforced at compile
|
||||
# time by the #error guard at the top of akstdlib.h.
|
||||
include(CMakeFindDependencyMacro)
|
||||
find_dependency(akerror)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/akstdlibTargets.cmake")
|
||||
|
||||
1
deps/libakerror
vendored
Submodule
1
deps/libakerror
vendored
Submodule
Submodule deps/libakerror added at 5ff87908e7
@@ -2,9 +2,99 @@
|
||||
#define _AKSTDLIB_H_
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
/*
|
||||
* libakerror 1.0.0 is the floor. That release moved the status-name table into a
|
||||
* private registry -- AKERR_MAX_ERR_VALUE and __AKERR_ERROR_NAMES are gone, the
|
||||
* registry entry points raise akerr_ErrorContext * instead of returning int, and
|
||||
* the library gained an soname -- so a translation unit that pairs this header
|
||||
* with a pre-1.0.0 akerror.h is an ABI mismatch, not just a compile problem.
|
||||
*
|
||||
* libakerror publishes no version macro, so this feature-tests on
|
||||
* AKERR_FIRST_CONSUMER_STATUS, which that release introduced, rather than on a
|
||||
* version number that does not exist. Without the guard a stale installed header
|
||||
* fails much further in, as a pile of unrelated errors inside src/stdlib.c.
|
||||
*
|
||||
* See deps/libakerror/UPGRADING.md.
|
||||
*/
|
||||
#ifndef AKERR_FIRST_CONSUMER_STATUS
|
||||
#error "libakstdlib requires libakerror >= 1.0.0: the akerror.h on the include path predates the status registry. Rebuild and reinstall libakerror."
|
||||
#endif
|
||||
|
||||
/*
|
||||
* AKSL_VERSION_MAJOR/MINOR/PATCH/STRING/NUMBER and AKSL_VERSION_SONAME.
|
||||
* Generated by CMake from include/akstdlib_version.h.in, which is why there is
|
||||
* no such file in the source tree -- it is configured into the build tree and
|
||||
* installed alongside this header.
|
||||
*/
|
||||
#include <akstdlib_version.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct aksl_ListNode {
|
||||
void *data;
|
||||
struct aksl_ListNode *next;
|
||||
struct aksl_ListNode *prev;
|
||||
} aksl_ListNode;
|
||||
|
||||
typedef struct aksl_TreeNode {
|
||||
struct aksl_TreeNode *parent;
|
||||
struct aksl_TreeNode *left;
|
||||
struct aksl_TreeNode *right;
|
||||
void *leaf;
|
||||
} aksl_TreeNode;
|
||||
|
||||
#define AKSL_TREE_SEARCH_BFS 0 /** Breadth-first search mode for tree nodes. Currently unsupported. */
|
||||
#define AKSL_TREE_SEARCH_BFS_RIGHT 1 /** Right-hand breadth-first search mode for tree nodes. Currentl unsupported. */
|
||||
#define AKSL_TREE_SEARCH_DFS 2 /** Alias for AKSL_TREE_SEARCH_DFS_PREORDER */
|
||||
#define AKSL_TREE_SEARCH_DFS_PREORDER 2 /** Depth first pre-order (root, left, right) search mode for tree nodes */
|
||||
#define AKSL_TREE_SEARCH_DFS_INORDER 3 /** Depth first in-order (left, root, right) search mode for tree nodes. Currently unsupported. */
|
||||
#define AKSL_TREE_SEARCH_DFS_POSTORDER 4 /** Depth first post-order (left, right, root) search mode for tree nodes. Currently unsupported. */
|
||||
#define AKSL_TREE_SEARCH_VISIT 5 /** Used when iterating through a tree structure as a control flag: don't traverse the children, just visit the node */
|
||||
|
||||
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_ListNodeIterator)(aksl_ListNode *node, void *data);
|
||||
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_TreeNodeIterator)(aksl_TreeNode *node, void *data);
|
||||
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_AllocFunc)(size_t size, void **dest);
|
||||
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_FreeFunc)(void *ptr);
|
||||
|
||||
/*
|
||||
* Version of the shared library actually loaded, as opposed to the
|
||||
* AKSL_VERSION_* macros above, which record what the caller was *compiled*
|
||||
* against. The two differ exactly when a stale libakstdlib.so is on the loader
|
||||
* path, which is the failure these entry points exist to name.
|
||||
*
|
||||
* All three pointers are required; this library treats a NULL out-param as a
|
||||
* caller error rather than as "don't care" (so does aksl_free).
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_version(int *major, int *minor, int *patch);
|
||||
|
||||
/*
|
||||
* The loaded library's version as "MAJOR.MINOR.PATCH", and its soname. Neither
|
||||
* can fail -- they return pointers to string literals in the library's own
|
||||
* .rodata -- so they return the string directly rather than an error context,
|
||||
* the same exception akerr_name_for_status() makes for the same reason.
|
||||
*/
|
||||
const char *aksl_version_string(void);
|
||||
const char *aksl_version_soname(void);
|
||||
|
||||
/*
|
||||
* Compare the caller's compiled-in version against the loaded library's.
|
||||
* Compatibility is defined as "same soname", so pre-1.0 both major and minor
|
||||
* must match and patch is ignored; from 1.0 only major will matter. Raises
|
||||
* AKERR_VALUE naming both versions on a mismatch.
|
||||
*
|
||||
* Call it through AKSL_VERSION_CHECK() below rather than directly. The macro
|
||||
* expands at *your* call site, so it captures the AKSL_VERSION_* the caller was
|
||||
* built with; the function compares them against the values baked into the
|
||||
* library. Passing the numbers by hand defeats the entire mechanism.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_version_check(int major, int minor, int patch);
|
||||
|
||||
#define AKSL_VERSION_CHECK() \
|
||||
aksl_version_check(AKSL_VERSION_MAJOR, AKSL_VERSION_MINOR, AKSL_VERSION_PATCH)
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fopen(char *pathname, char *mode, FILE **fp);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
|
||||
@@ -16,7 +106,6 @@ 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_free(void *ptr);
|
||||
|
||||
|
||||
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_sprintf(int *count, char *restrict str, const char *restrict format, ...);
|
||||
@@ -28,4 +117,14 @@ 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_strhash_djb2(char *str, size_t len, uint32_t *hashval);
|
||||
|
||||
// Linked list functions
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_append(aksl_ListNode *list, aksl_ListNode *obj);
|
||||
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);
|
||||
|
||||
// Tree functions
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate(aksl_TreeNode *root, aksl_TreeNodeIterator iter, aksl_AllocFunc lalloc, aksl_FreeFunc lfree, uint8_t searchmode, void *data, aksl_ListNode *queue);
|
||||
|
||||
#endif
|
||||
|
||||
37
include/akstdlib_version.h.in
Normal file
37
include/akstdlib_version.h.in
Normal file
@@ -0,0 +1,37 @@
|
||||
#ifndef _AKSTDLIB_VERSION_H_
|
||||
#define _AKSTDLIB_VERSION_H_
|
||||
|
||||
/*
|
||||
* GENERATED FILE -- do not edit.
|
||||
*
|
||||
* Configured from include/akstdlib_version.h.in by CMake. The version itself
|
||||
* lives in exactly one place, the project() call in CMakeLists.txt, and flows
|
||||
* from there into these macros, the shared library's SOVERSION, the Version:
|
||||
* field in akstdlib.pc, and akstdlibConfigVersion.cmake. To bump the version,
|
||||
* edit project(); to change what the macros look like, edit this template.
|
||||
*/
|
||||
|
||||
#define AKSL_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
|
||||
#define AKSL_VERSION_MINOR @PROJECT_VERSION_MINOR@
|
||||
#define AKSL_VERSION_PATCH @PROJECT_VERSION_PATCH@
|
||||
#define AKSL_VERSION_STRING "@PROJECT_VERSION@"
|
||||
|
||||
/*
|
||||
* A single ordered integer, for `#if AKSL_VERSION_NUMBER >= ...` compares.
|
||||
* Computed rather than written out as a literal on purpose: a literal like
|
||||
* 000100 is octal in C, which would silently make version 0.1.0 compare as 64.
|
||||
* Each component gets two decimal digits, so this is correct up to x.99.99.
|
||||
*/
|
||||
#define AKSL_VERSION_NUMBER \
|
||||
((AKSL_VERSION_MAJOR * 10000) + (AKSL_VERSION_MINOR * 100) + AKSL_VERSION_PATCH)
|
||||
|
||||
/*
|
||||
* The soname of the library these headers describe -- "0.1" for libakstdlib.so.0.1.
|
||||
* This is the granularity at which the ABI is allowed to break, and it is what
|
||||
* aksl_version_check() compares: pre-1.0 a minor bump is a break, so the soname
|
||||
* carries MAJOR.MINOR. From 1.0 it becomes MAJOR alone, and the SOVERSION
|
||||
* expression in CMakeLists.txt and this macro change together.
|
||||
*/
|
||||
#define AKSL_VERSION_SONAME "@AKSL_SOVERSION@"
|
||||
|
||||
#endif // _AKSTDLIB_VERSION_H_
|
||||
469
scripts/coverage.py
Normal file
469
scripts/coverage.py
Normal file
@@ -0,0 +1,469 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Code coverage harness for libakstdlib.
|
||||
|
||||
Reports which lines, branches and functions of the library the CTest suite
|
||||
actually executed. Coverage answers "what did the tests touch"; the mutation
|
||||
harness in scripts/mutation_test.py answers the stronger question "would the
|
||||
tests notice if it broke". They are complementary: a line can be covered and
|
||||
still have no assertion behind it, so an uncovered line is definitely untested
|
||||
while a covered one is only maybe tested.
|
||||
|
||||
Requires a build configured with -DAKSL_COVERAGE=ON, which compiles the library
|
||||
and its tests with --coverage. That emits a .gcno next to every object at build
|
||||
time and a .gcda next to it at run time; this script feeds those to gcov and
|
||||
aggregates its JSON output.
|
||||
|
||||
There are no third-party dependencies (Python stdlib + gcc's own gcov), so this
|
||||
works anywhere the project already builds -- no lcov, gcovr or genhtml needed.
|
||||
|
||||
Usage:
|
||||
scripts/coverage.py --build build-coverage [options]
|
||||
|
||||
--build DIR build tree to read .gcda from (default: build-coverage)
|
||||
--source-root DIR repo root (default: parent of this script's dir)
|
||||
--gcov PROG gcov binary to use (default: $GCOV or gcov)
|
||||
--include PREFIX only report sources under this repo-relative prefix;
|
||||
repeatable. Default: src, include
|
||||
--exclude PREFIX drop sources under this repo-relative prefix;
|
||||
repeatable. Default: tests, deps, build
|
||||
--zero delete every .gcda in the build tree, then exit. Run
|
||||
before the suite so counters do not accumulate across
|
||||
runs.
|
||||
--run-tests run ctest in the build tree first (implies --zero)
|
||||
--threshold PCT exit non-zero if total line coverage < PCT (default 0)
|
||||
--branch-threshold PCT
|
||||
exit non-zero if total branch coverage < PCT
|
||||
--summary-only omit the per-file uncovered-line listing
|
||||
--output PATH also write the text report to PATH (CTest hides the
|
||||
output of a passing test, so the in-suite report lands
|
||||
in <build>/coverage-summary.txt this way)
|
||||
--cobertura PATH also write a Cobertura XML report (for CI publishers)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import xml.sax.saxutils as saxutils
|
||||
|
||||
DEFAULT_INCLUDE = ["src", "include"]
|
||||
DEFAULT_EXCLUDE = ["tests", "deps", "build"]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# gcov invocation and parsing
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
class FileCoverage:
|
||||
"""Accumulated coverage for one source file, keyed by line number.
|
||||
|
||||
A single source file can be represented in several .gcda files -- a header
|
||||
compiled into every test binary, or a .c file linked more than once -- so
|
||||
counts are summed across all of them rather than overwritten.
|
||||
"""
|
||||
|
||||
def __init__(self, path):
|
||||
self.path = path # repo-relative
|
||||
self.lines = {} # line number -> execution count
|
||||
self.branches = {} # line number -> list of branch counts
|
||||
self.functions = {} # function name -> execution count
|
||||
|
||||
def add_line(self, lineno, count):
|
||||
self.lines[lineno] = self.lines.get(lineno, 0) + count
|
||||
|
||||
def add_branches(self, lineno, counts):
|
||||
cur = self.branches.setdefault(lineno, [0] * len(counts))
|
||||
# Defensive: different translation units should agree on the branch
|
||||
# count for a line, but never let a mismatch raise.
|
||||
if len(cur) != len(counts):
|
||||
if len(counts) > len(cur):
|
||||
cur.extend([0] * (len(counts) - len(cur)))
|
||||
counts = counts + [0] * (len(cur) - len(counts))
|
||||
for i, c in enumerate(counts):
|
||||
cur[i] += c
|
||||
|
||||
def add_function(self, name, count):
|
||||
self.functions[name] = self.functions.get(name, 0) + count
|
||||
|
||||
# -- derived totals ---------------------------------------------------- #
|
||||
|
||||
@property
|
||||
def lines_total(self):
|
||||
return len(self.lines)
|
||||
|
||||
@property
|
||||
def lines_covered(self):
|
||||
return sum(1 for c in self.lines.values() if c > 0)
|
||||
|
||||
@property
|
||||
def branches_total(self):
|
||||
return sum(len(b) for b in self.branches.values())
|
||||
|
||||
@property
|
||||
def branches_covered(self):
|
||||
return sum(1 for b in self.branches.values() for c in b if c > 0)
|
||||
|
||||
@property
|
||||
def functions_total(self):
|
||||
return len(self.functions)
|
||||
|
||||
@property
|
||||
def functions_covered(self):
|
||||
return sum(1 for c in self.functions.values() if c > 0)
|
||||
|
||||
def uncovered_lines(self):
|
||||
return sorted(n for n, c in self.lines.items() if c == 0)
|
||||
|
||||
def uncovered_functions(self):
|
||||
return sorted(n for n, c in self.functions.items() if c == 0)
|
||||
|
||||
|
||||
def pct(covered, total):
|
||||
"""Coverage percentage. A file with nothing instrumented counts as 100%,
|
||||
matching gcov/lcov: there is nothing there to leave untested."""
|
||||
return 100.0 * covered / total if total else 100.0
|
||||
|
||||
|
||||
def find_gcda(build):
|
||||
return sorted(glob.glob(os.path.join(build, "**", "*.gcda"), recursive=True))
|
||||
|
||||
|
||||
def run_gcov(gcov, gcda):
|
||||
"""Run gcov on one .gcda and return the parsed JSON, or None on failure.
|
||||
|
||||
gcov resolves the matching .gcno relative to the .gcda, so it is invoked
|
||||
from the .gcda's directory with a bare filename.
|
||||
"""
|
||||
workdir = os.path.dirname(gcda) or "."
|
||||
cmd = [gcov, "--json-format", "--stdout", "--branch-probabilities",
|
||||
os.path.basename(gcda)]
|
||||
try:
|
||||
proc = subprocess.run(cmd, cwd=workdir, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
except OSError as exc:
|
||||
sys.stderr.write(f"warning: cannot run {gcov}: {exc}\n")
|
||||
return None
|
||||
if proc.returncode != 0:
|
||||
sys.stderr.write(f"warning: gcov failed on {gcda}:\n"
|
||||
f"{proc.stderr.decode(errors='replace')}")
|
||||
return None
|
||||
text = proc.stdout.decode(errors="replace").strip()
|
||||
if not text:
|
||||
return None
|
||||
try:
|
||||
return json.loads(text)
|
||||
except ValueError as exc:
|
||||
sys.stderr.write(f"warning: unparseable gcov JSON for {gcda}: {exc}\n")
|
||||
return None
|
||||
|
||||
|
||||
def rel_to_root(path, cwd, root):
|
||||
"""Resolve a path from gcov's JSON to a repo-relative path, or None if it
|
||||
falls outside the repo (libc headers, the toolchain, an out-of-tree dep)."""
|
||||
if not os.path.isabs(path):
|
||||
path = os.path.join(cwd, path)
|
||||
path = os.path.realpath(path)
|
||||
try:
|
||||
rel = os.path.relpath(path, root)
|
||||
except ValueError: # different drive (Windows)
|
||||
return None
|
||||
if rel.startswith(os.pardir):
|
||||
return None
|
||||
return rel.replace(os.sep, "/")
|
||||
|
||||
|
||||
def selected(rel, includes, excludes):
|
||||
"""True if a repo-relative path passes the include/exclude prefix filters.
|
||||
Excludes win, so `--include src --exclude src/generated` behaves sanely."""
|
||||
def under(prefix):
|
||||
return rel == prefix or rel.startswith(prefix.rstrip("/") + "/")
|
||||
if any(under(e) for e in excludes):
|
||||
return False
|
||||
if not includes:
|
||||
return True
|
||||
return any(under(i) for i in includes)
|
||||
|
||||
|
||||
def collect(build, root, gcov, includes, excludes):
|
||||
"""Aggregate coverage for every selected source file in the build tree."""
|
||||
files = {}
|
||||
gcda_files = find_gcda(build)
|
||||
for gcda in gcda_files:
|
||||
data = run_gcov(gcov, gcda)
|
||||
if not data:
|
||||
continue
|
||||
cwd = data.get("current_working_directory") or os.path.dirname(gcda)
|
||||
for entry in data.get("files", []):
|
||||
rel = rel_to_root(entry.get("file", ""), cwd, root)
|
||||
if rel is None or not selected(rel, includes, excludes):
|
||||
continue
|
||||
fc = files.setdefault(rel, FileCoverage(rel))
|
||||
for line in entry.get("lines", []):
|
||||
lineno = line.get("line_number")
|
||||
if lineno is None:
|
||||
continue
|
||||
fc.add_line(lineno, line.get("count", 0))
|
||||
branches = line.get("branches") or []
|
||||
if branches:
|
||||
fc.add_branches(lineno, [b.get("count", 0) for b in branches])
|
||||
for fn in entry.get("functions", []):
|
||||
name = fn.get("demangled_name") or fn.get("name")
|
||||
if name:
|
||||
fc.add_function(name, fn.get("execution_count", 0))
|
||||
return files, len(gcda_files)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Reporting
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def line_ranges(numbers):
|
||||
"""Compress [3,4,5,9,11,12] into ['3-5', '9', '11-12'] for readability."""
|
||||
out = []
|
||||
start = prev = None
|
||||
for n in numbers:
|
||||
if start is None:
|
||||
start = prev = n
|
||||
elif n == prev + 1:
|
||||
prev = n
|
||||
else:
|
||||
out.append(str(start) if start == prev else f"{start}-{prev}")
|
||||
start = prev = n
|
||||
if start is not None:
|
||||
out.append(str(start) if start == prev else f"{start}-{prev}")
|
||||
return out
|
||||
|
||||
|
||||
def report(files, summary_only):
|
||||
"""Render the text report. Returns (totals dict, report text)."""
|
||||
names = sorted(files)
|
||||
width = max([len(n) for n in names] + [len("TOTAL")])
|
||||
out = []
|
||||
|
||||
header = (f" {'file'.ljust(width)} {'lines':>15} {'branches':>15} "
|
||||
f"{'functions':>15}")
|
||||
out.append("=" * len(header))
|
||||
out.append("CODE COVERAGE SUMMARY")
|
||||
out.append("=" * len(header))
|
||||
out.append(header)
|
||||
out.append(" " + "-" * (len(header) - 2))
|
||||
|
||||
def row(name, lc, lt, bc, bt, fc_, ft):
|
||||
cell = lambda c, t: f"{pct(c, t):5.1f}% {c:4d}/{t:<4d}"
|
||||
out.append(f" {name.ljust(width)} {cell(lc, lt):>15} "
|
||||
f"{cell(bc, bt):>15} {cell(fc_, ft):>15}")
|
||||
|
||||
tot = [0, 0, 0, 0, 0, 0]
|
||||
for name in names:
|
||||
f = files[name]
|
||||
vals = (f.lines_covered, f.lines_total, f.branches_covered,
|
||||
f.branches_total, f.functions_covered, f.functions_total)
|
||||
row(name, *vals)
|
||||
tot = [t + v for t, v in zip(tot, vals)]
|
||||
|
||||
out.append(" " + "-" * (len(header) - 2))
|
||||
row("TOTAL", *tot)
|
||||
|
||||
if not summary_only:
|
||||
for name in names:
|
||||
f = files[name]
|
||||
gaps = f.uncovered_lines()
|
||||
missing_fns = f.uncovered_functions()
|
||||
if not gaps and not missing_fns:
|
||||
continue
|
||||
out.append(f"\n{name}: uncovered (each one is a missing test)")
|
||||
if missing_fns:
|
||||
out.append(" functions never called:")
|
||||
for fn in missing_fns:
|
||||
out.append(f" {fn}")
|
||||
if gaps:
|
||||
out.append(f" lines ({len(gaps)}): "
|
||||
+ ", ".join(line_ranges(gaps)))
|
||||
|
||||
text = "\n".join(out) + "\n"
|
||||
sys.stdout.write(text)
|
||||
return text, {
|
||||
"lines_covered": tot[0], "lines_total": tot[1],
|
||||
"branches_covered": tot[2], "branches_total": tot[3],
|
||||
"functions_covered": tot[4], "functions_total": tot[5],
|
||||
}
|
||||
|
||||
|
||||
def write_cobertura(path, files, root, totals):
|
||||
"""Emit a minimal Cobertura report. One <class> per source file, which is
|
||||
what CI coverage publishers expect for C."""
|
||||
esc = saxutils.quoteattr
|
||||
lr = pct(totals["lines_covered"], totals["lines_total"]) / 100.0
|
||||
br = pct(totals["branches_covered"], totals["branches_total"]) / 100.0
|
||||
out = ['<?xml version="1.0" encoding="UTF-8"?>',
|
||||
f'<coverage line-rate="{lr:.4f}" branch-rate="{br:.4f}" '
|
||||
f'lines-covered="{totals["lines_covered"]}" '
|
||||
f'lines-valid="{totals["lines_total"]}" '
|
||||
f'branches-covered="{totals["branches_covered"]}" '
|
||||
f'branches-valid="{totals["branches_total"]}" '
|
||||
'complexity="0" version="0" timestamp="0">',
|
||||
' <sources>', f' <source>{saxutils.escape(root)}</source>',
|
||||
' </sources>', ' <packages>',
|
||||
' <package name="akstdlib" '
|
||||
f'line-rate="{lr:.4f}" branch-rate="{br:.4f}" complexity="0">',
|
||||
' <classes>']
|
||||
for name in sorted(files):
|
||||
f = files[name]
|
||||
flr = pct(f.lines_covered, f.lines_total) / 100.0
|
||||
fbr = pct(f.branches_covered, f.branches_total) / 100.0
|
||||
cls = os.path.basename(name)
|
||||
out.append(f' <class name={esc(cls)} filename={esc(name)} '
|
||||
f'line-rate="{flr:.4f}" branch-rate="{fbr:.4f}" '
|
||||
'complexity="0">')
|
||||
out.append(' <methods/>')
|
||||
out.append(' <lines>')
|
||||
for lineno in sorted(f.lines):
|
||||
count = f.lines[lineno]
|
||||
brs = f.branches.get(lineno) or []
|
||||
if brs:
|
||||
taken = sum(1 for c in brs if c > 0)
|
||||
cond = (f' branch="true" condition-coverage='
|
||||
f'{esc(f"{pct(taken, len(brs)):.0f}% ({taken}/{len(brs)})")}')
|
||||
else:
|
||||
cond = ' branch="false"'
|
||||
out.append(f' <line number="{lineno}" '
|
||||
f'hits="{count}"{cond}/>')
|
||||
out.append(' </lines>')
|
||||
out.append(' </class>')
|
||||
out += [' </classes>', ' </package>', ' </packages>',
|
||||
'</coverage>']
|
||||
with open(path, "w") as fh:
|
||||
fh.write("\n".join(out) + "\n")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Driver
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def zero_counters(build):
|
||||
"""Delete accumulated .gcda. gcov counters are cumulative across runs, so
|
||||
without this a report mixes in every earlier suite run (and any test binary
|
||||
run by hand), which quietly overstates coverage."""
|
||||
removed = 0
|
||||
for gcda in find_gcda(build):
|
||||
try:
|
||||
os.remove(gcda)
|
||||
removed += 1
|
||||
except OSError as exc:
|
||||
sys.stderr.write(f"warning: cannot remove {gcda}: {exc}\n")
|
||||
return removed
|
||||
|
||||
|
||||
def main():
|
||||
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="Code coverage for libakstdlib")
|
||||
ap.add_argument("--build", default="build-coverage")
|
||||
ap.add_argument("--source-root", default=default_root)
|
||||
ap.add_argument("--gcov", default=os.environ.get("GCOV", "gcov"))
|
||||
ap.add_argument("--include", action="append", default=None)
|
||||
ap.add_argument("--exclude", action="append", default=None)
|
||||
ap.add_argument("--zero", action="store_true")
|
||||
ap.add_argument("--run-tests", action="store_true")
|
||||
ap.add_argument("--threshold", type=float, default=0.0)
|
||||
ap.add_argument("--branch-threshold", type=float, default=0.0)
|
||||
ap.add_argument("--summary-only", action="store_true")
|
||||
ap.add_argument("--output", default=None,
|
||||
help="also write the text report to this path")
|
||||
ap.add_argument("--cobertura", default=None)
|
||||
args = ap.parse_args()
|
||||
|
||||
root = os.path.realpath(args.source_root)
|
||||
build = os.path.abspath(args.build)
|
||||
includes = args.include if args.include is not None else DEFAULT_INCLUDE
|
||||
if args.exclude is not None:
|
||||
excludes = args.exclude
|
||||
else:
|
||||
# Excludes win over includes, so a *default* exclude must not silently
|
||||
# cancel an explicit --include: `--include tests` has to report tests/
|
||||
# even though tests/ is excluded by default.
|
||||
excludes = [e for e in DEFAULT_EXCLUDE
|
||||
if not any(selected(e, [i], []) for i in (args.include or []))]
|
||||
|
||||
if not os.path.isdir(build):
|
||||
sys.stderr.write(f"error: no such build directory: {build}\n"
|
||||
"Configure one with:\n"
|
||||
" cmake -S . -B build-coverage -DAKSL_COVERAGE=ON\n"
|
||||
" cmake --build build-coverage\n")
|
||||
return 2
|
||||
|
||||
if args.zero and not args.run_tests:
|
||||
n = zero_counters(build)
|
||||
print(f"Cleared {n} .gcda counter file(s) in {build}")
|
||||
return 0
|
||||
|
||||
if args.run_tests:
|
||||
n = zero_counters(build)
|
||||
print(f"Cleared {n} .gcda counter file(s) in {build}")
|
||||
print("Running ctest ...")
|
||||
rc = subprocess.call(["ctest", "--test-dir", build,
|
||||
"--output-on-failure"])
|
||||
if rc != 0:
|
||||
# Report anyway: partial coverage from a red suite is still useful,
|
||||
# and the ctest output above already shows what failed.
|
||||
sys.stderr.write(f"warning: ctest exited {rc}; "
|
||||
"reporting coverage from a failing suite\n")
|
||||
print()
|
||||
|
||||
files, n_gcda = collect(build, root, args.gcov, includes, excludes)
|
||||
if not n_gcda:
|
||||
sys.stderr.write(
|
||||
f"error: no .gcda files under {build}.\n"
|
||||
"Either the build was not configured with -DAKSL_COVERAGE=ON, or\n"
|
||||
"the test suite has not been run yet (try --run-tests).\n")
|
||||
return 2
|
||||
if not files:
|
||||
sys.stderr.write(
|
||||
f"error: {n_gcda} .gcda file(s) found, but none of the covered "
|
||||
"sources matched\nthe include/exclude filters "
|
||||
f"(include={includes}, exclude={excludes}).\n")
|
||||
return 2
|
||||
|
||||
text, totals = report(files, args.summary_only)
|
||||
|
||||
# CTest swallows the output of a passing test, so also drop the report on
|
||||
# disk: that is what makes the in-suite coverage_report entry useful without
|
||||
# having to re-run ctest under -V.
|
||||
if args.output:
|
||||
path = os.path.abspath(args.output)
|
||||
try:
|
||||
with open(path, "w") as fh:
|
||||
fh.write(text)
|
||||
print(f"\nReport written to: {path}")
|
||||
except OSError as exc:
|
||||
sys.stderr.write(f"warning: cannot write {path}: {exc}\n")
|
||||
|
||||
if args.cobertura:
|
||||
path = os.path.abspath(args.cobertura)
|
||||
write_cobertura(path, files, root, totals)
|
||||
print(f"\nCobertura report written to: {path}")
|
||||
|
||||
line_pct = pct(totals["lines_covered"], totals["lines_total"])
|
||||
branch_pct = pct(totals["branches_covered"], totals["branches_total"])
|
||||
failed = False
|
||||
if args.threshold > 0 and line_pct < args.threshold:
|
||||
print(f"\nFAIL: line coverage {line_pct:.1f}% < "
|
||||
f"threshold {args.threshold:.1f}%")
|
||||
failed = True
|
||||
if args.branch_threshold > 0 and branch_pct < args.branch_threshold:
|
||||
print(f"\nFAIL: branch coverage {branch_pct:.1f}% < "
|
||||
f"threshold {args.branch_threshold:.1f}%")
|
||||
failed = True
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
439
scripts/mutation_test.py
Executable file
439
scripts/mutation_test.py
Executable file
@@ -0,0 +1,439 @@
|
||||
#!/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):
|
||||
# "build*" covers build/, build-asan/ and build-coverage/: the harness
|
||||
# configures its own tree inside the copy, so copying those is pure IO --
|
||||
# and a coverage tree drags along every .gcno/.gcda as well.
|
||||
ignore = shutil.ignore_patterns("build*", ".git", "*.o", "*.so", "*~",
|
||||
"#*#", "*.iso", "*.png", "*.gcno", "*.gcda")
|
||||
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())
|
||||
225
src/stdlib.c
225
src/stdlib.c
@@ -4,6 +4,58 @@
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/*
|
||||
* Version of the library itself. These read the AKSL_VERSION_* macros as they
|
||||
* were when *this translation unit* was compiled, which is what makes them the
|
||||
* loaded library's version rather than the caller's: the caller's copy of the
|
||||
* same macros comes from whatever akstdlib_version.h it compiled against.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_version(int *major, int *minor, int *patch)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, major, AKERR_NULLPOINTER, "major=%p, minor=%p, patch=%p",
|
||||
(void *)major, (void *)minor, (void *)patch);
|
||||
FAIL_ZERO_RETURN(e, minor, AKERR_NULLPOINTER, "major=%p, minor=%p, patch=%p",
|
||||
(void *)major, (void *)minor, (void *)patch);
|
||||
FAIL_ZERO_RETURN(e, patch, AKERR_NULLPOINTER, "major=%p, minor=%p, patch=%p",
|
||||
(void *)major, (void *)minor, (void *)patch);
|
||||
*major = AKSL_VERSION_MAJOR;
|
||||
*minor = AKSL_VERSION_MINOR;
|
||||
*patch = AKSL_VERSION_PATCH;
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
const char *aksl_version_string(void)
|
||||
{
|
||||
return AKSL_VERSION_STRING;
|
||||
}
|
||||
|
||||
const char *aksl_version_soname(void)
|
||||
{
|
||||
return AKSL_VERSION_SONAME;
|
||||
}
|
||||
|
||||
/*
|
||||
* Compatibility is "same soname". Pre-1.0 that is MAJOR.MINOR, so both are
|
||||
* compared and patch is deliberately ignored; from 1.0 the minor comparison
|
||||
* comes out, in step with the SOVERSION expression in CMakeLists.txt. The
|
||||
* caller's numbers arrive as arguments because AKSL_VERSION_CHECK() expanded
|
||||
* them at the caller's site -- see the macro in akstdlib.h.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_version_check(int major, int minor, int patch)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
(void)patch;
|
||||
FAIL_NONZERO_RETURN(e,
|
||||
(major != AKSL_VERSION_MAJOR || minor != AKSL_VERSION_MINOR),
|
||||
AKERR_VALUE,
|
||||
"compiled against libakstdlib %d.%d.%d, loaded %s (soname %s)",
|
||||
major, minor, patch,
|
||||
AKSL_VERSION_STRING, AKSL_VERSION_SONAME);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_malloc(size_t size, void **dst)
|
||||
{
|
||||
@@ -98,6 +150,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_printf(int *count, const char *restrict
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, count, AKERR_NULLPOINTER, "count=%p, format=%p", (void *)count, (void *)format);
|
||||
FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "count=%p, format=%p", (void *)count, (void *)format);
|
||||
va_start(args, format);
|
||||
*count = vprintf(format, args);
|
||||
FAIL_NONZERO_RETURN(e, (*count == -1), errno, "Short write");
|
||||
SUCCEED_RETURN(e);
|
||||
@@ -111,6 +164,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fprintf(int *count, FILE *restrict strea
|
||||
FAIL_ZERO_RETURN(e, count, AKERR_NULLPOINTER, "count=%p, stream=%p, format=%p", (void *)count, (void *)stream, (void *)format);
|
||||
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "count=%p, stream=%p, format=%p", (void *)count, (void *)stream, (void *)format);
|
||||
FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "count=%p, stream=%p, format=%p", (void *)count, (void *)stream, (void *)format);
|
||||
va_start(args, format);
|
||||
*count = vfprintf(stream, format, args);
|
||||
FAIL_NONZERO_RETURN(e, (*count == -1), errno, "Short write");
|
||||
SUCCEED_RETURN(e);
|
||||
@@ -174,3 +228,174 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_realpath(const char *restrict path, char
|
||||
FAIL_ZERO_RETURN(e, result, errno, "path=%s, dest=%s", path, resolved_path);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2(char *str, size_t len, uint32_t *hashval)
|
||||
{
|
||||
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);
|
||||
FAIL_ZERO_RETURN(e, list, AKERR_NULLPOINTER, "list");
|
||||
FAIL_ZERO_RETURN(e, obj, AKERR_NULLPOINTER, "obj");
|
||||
aksl_ListNode *slow = list;
|
||||
aksl_ListNode *fast = list;
|
||||
aksl_ListNode *tail = list;
|
||||
while ( fast != NULL && fast->next != NULL ) {
|
||||
tail = slow;
|
||||
slow = slow->next;
|
||||
fast = fast->next->next;
|
||||
if ( fast == slow) {
|
||||
FAIL_RETURN(e, AKERR_CIRCULAR_REFERENCE, "%p", list);
|
||||
}
|
||||
}
|
||||
if ( fast != NULL ) {
|
||||
tail = fast;
|
||||
}
|
||||
tail->next = obj;
|
||||
obj->next = NULL;
|
||||
obj->prev = tail;
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_pop(aksl_ListNode *node)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
|
||||
if ( node->prev != NULL ) {
|
||||
node->prev->next = node->next;
|
||||
}
|
||||
if ( node->next != NULL ) {
|
||||
node->next->prev = node->prev;
|
||||
}
|
||||
node->next = NULL;
|
||||
node->prev = NULL;
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Iterates over a tree structure in breadth or depth first order, executing a callback function on each node.
|
||||
*
|
||||
* This function recursively calls itself to traverse a binary tree datastructure.
|
||||
*
|
||||
* @param[in] root The tree structure to search
|
||||
* @param[in] iter An aksl_TreeNodeIterator function which will be called for each node found
|
||||
* @param[in] lalloc An aksl_AllocFunc function which will be used to allocate aksl_ListNode elements for the search, or NULL to use the default allocator (aksl_malloc)
|
||||
* @param[in] lfree An aksl_FreeFunc function which will be used to free the elements procured with lalloc, or NULL to use the default free function (aksl_free)
|
||||
* @param[in] searchmode One of the AKSL_TREE_SEARCH_BFS* defines
|
||||
* @param[in] data Any user data that should be provided when the iterator is called
|
||||
* @param[in] queue The linked list node to use as the head of the queue. The caller should pass NULL here.
|
||||
*
|
||||
* @throws AKERR_NULLPOINTER On null pointer inputs
|
||||
* @return akerr_ErrorContext
|
||||
*/
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate(
|
||||
aksl_TreeNode *root,
|
||||
aksl_TreeNodeIterator iter,
|
||||
aksl_AllocFunc lalloc,
|
||||
aksl_FreeFunc lfree,
|
||||
uint8_t searchmode,
|
||||
void *data,
|
||||
aksl_ListNode *queue)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "root");
|
||||
FAIL_ZERO_RETURN(e, iter, AKERR_NULLPOINTER, "iter");
|
||||
if ( lalloc == NULL ) {
|
||||
lalloc = &aksl_malloc;
|
||||
}
|
||||
if ( lfree == NULL ) {
|
||||
lfree = &aksl_free;
|
||||
}
|
||||
ATTEMPT {
|
||||
switch ( searchmode ) {
|
||||
case AKSL_TREE_SEARCH_DFS_PREORDER:
|
||||
CATCH(e, iter(root, data));
|
||||
if ( root->left != NULL ) {
|
||||
PASS(e, aksl_tree_iterate(root->left, iter, lalloc, lfree, searchmode, data, NULL));
|
||||
}
|
||||
if ( root-> right != NULL ) {
|
||||
PASS(e, aksl_tree_iterate(root->right, iter, lalloc, lfree, searchmode, data, NULL));
|
||||
}
|
||||
break;
|
||||
case AKSL_TREE_SEARCH_DFS_POSTORDER:
|
||||
if ( root->left != NULL ) {
|
||||
PASS(e, aksl_tree_iterate(root->left, iter, lalloc, lfree, searchmode, data, NULL));
|
||||
}
|
||||
if ( root-> right != NULL ) {
|
||||
PASS(e, aksl_tree_iterate(root->right, iter, lalloc, lfree, searchmode, data, NULL));
|
||||
}
|
||||
CATCH(e, iter(root, data));
|
||||
break;
|
||||
case AKSL_TREE_SEARCH_DFS_INORDER:
|
||||
if ( root->left != NULL ) {
|
||||
PASS(e, aksl_tree_iterate(root->left, iter, lalloc, lfree, searchmode, data, NULL));
|
||||
}
|
||||
CATCH(e, iter(root, data));
|
||||
if ( root-> right != NULL ) {
|
||||
PASS(e, aksl_tree_iterate(root->right, iter, lalloc, lfree, searchmode, data, NULL));
|
||||
}
|
||||
break;
|
||||
case AKSL_TREE_SEARCH_BFS:
|
||||
case AKSL_TREE_SEARCH_BFS_RIGHT:
|
||||
FAIL_RETURN(e, AKERR_NOT_IMPLEMENTED, "Searchmode %d", searchmode);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Iterates over a linked list and execute a callback function on each node
|
||||
*
|
||||
* @param[in] list The linked list to iterate
|
||||
* @param[in] iter An aksl_ListNodeIterator function which will be called for each node found
|
||||
* @param[in] data Any user data that should be provided when the iterator is called
|
||||
*
|
||||
* @throws AKERR_NULLPOINTER on null pointer inputs
|
||||
* @throws AKERR_CIRCULAR_REFERENCE when the linked list contains a circular reference
|
||||
*
|
||||
* @return akerr_ErrorContext
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate(aksl_ListNode *list, aksl_ListNodeIterator iter, void *data)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, list, AKERR_NULLPOINTER, "list");
|
||||
FAIL_ZERO_RETURN(e, iter, AKERR_NULLPOINTER, "iter");
|
||||
aksl_ListNode *slow = list;
|
||||
aksl_ListNode *fast = list;
|
||||
while ( fast != NULL && fast->next != NULL ) {
|
||||
slow = slow->next;
|
||||
fast = fast->next->next;
|
||||
if ( fast == slow) {
|
||||
FAIL_RETURN(e, AKERR_CIRCULAR_REFERENCE, "%p", list);
|
||||
}
|
||||
}
|
||||
while ( slow != NULL ) {
|
||||
ATTEMPT {
|
||||
CATCH(e, iter(slow, data));
|
||||
slow = slow->next;
|
||||
} 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);
|
||||
}
|
||||
|
||||
341
tests/aksl_capture.h
Normal file
341
tests/aksl_capture.h
Normal file
@@ -0,0 +1,341 @@
|
||||
#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_STATUS_MSG_CONTAINS()
|
||||
* assert status first, then message content.
|
||||
* AKSL_CHECK_OK() assert a call returned no error context at all.
|
||||
* aksl_last_status/... the status, message and function name of the most
|
||||
* recent context taken by AKSL_CHECK_STATUS.
|
||||
* aksl_capture_install() swap in a capturing akerr_log_method so a test can
|
||||
* 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_temp_file() create an empty temp file for the stream, formatted
|
||||
* output and path tests to work on.
|
||||
* 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 <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.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;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Temp files */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
#define AKSL_TMP_MAX 256
|
||||
#define AKSL_TMP_TRACKED 32
|
||||
|
||||
static char aksl_tmp_paths[AKSL_TMP_TRACKED][AKSL_TMP_MAX];
|
||||
static int aksl_tmp_count = 0;
|
||||
|
||||
/*
|
||||
* Unlink every path aksl_temp_file() handed out. Registered with atexit, so the
|
||||
* files go away even when a test returns early on a failed assertion -- which is
|
||||
* the normal case for a known-failing test and for every mutant the mutation
|
||||
* harness builds. Paths a test already unlinked simply fail here, harmlessly.
|
||||
*/
|
||||
static void aksl_temp_cleanup(void)
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < aksl_tmp_count; i++ ) {
|
||||
unlink(aksl_tmp_paths[i]);
|
||||
}
|
||||
aksl_tmp_count = 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Create an empty temp file under $TMPDIR (or /tmp) and write its path into buf.
|
||||
* Returns 0 on success, non-zero on failure.
|
||||
*
|
||||
* mkstemp both names and creates the file, so a test that opens the path for
|
||||
* reading is never racing another process for the name. Tests should still
|
||||
* unlink what they create -- asserting on it catches a wrapper that removed or
|
||||
* renamed the file -- but aksl_temp_cleanup() is the backstop.
|
||||
*/
|
||||
static int __attribute__((unused)) aksl_temp_file(char *buf, size_t n)
|
||||
{
|
||||
const char *dir = getenv("TMPDIR");
|
||||
int fd = -1;
|
||||
|
||||
if ( dir == NULL || dir[0] == '\0' ) {
|
||||
dir = "/tmp";
|
||||
}
|
||||
if ( (size_t)snprintf(buf, n, "%s/aksl_test_XXXXXX", dir) >= n ) {
|
||||
return 1;
|
||||
}
|
||||
fd = mkstemp(buf);
|
||||
if ( fd < 0 ) {
|
||||
return 1;
|
||||
}
|
||||
close(fd);
|
||||
|
||||
if ( aksl_tmp_count < AKSL_TMP_TRACKED ) {
|
||||
if ( aksl_tmp_count == 0 && atexit(&aksl_temp_cleanup) != 0 ) {
|
||||
return 0; /* tracking is best-effort; the file itself is fine */
|
||||
}
|
||||
snprintf(aksl_tmp_paths[aksl_tmp_count], AKSL_TMP_MAX, "%s", buf);
|
||||
aksl_tmp_count++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* 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. Failure-path assertions should go through this so that no test
|
||||
* leaks a pool slot. Success-path assertions use AKSL_CHECK_OK, which also
|
||||
* verifies that no bogus status-0 context was returned.
|
||||
*/
|
||||
static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
|
||||
{
|
||||
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 )
|
||||
|
||||
/*
|
||||
* Success is represented by a NULL error context, not just status 0. This catches
|
||||
* wrappers that incorrectly raise an error using a stale errno value of 0.
|
||||
*/
|
||||
#define AKSL_CHECK_OK(__expr) \
|
||||
do { \
|
||||
akerr_ErrorContext *__e = (__expr); \
|
||||
if ( __e != NULL ) { \
|
||||
int __st = aksl_take(__e); \
|
||||
fprintf(stderr, \
|
||||
" CHECK FAILED: %s\n" \
|
||||
" returned error context with status %d (%s) \"%s\"\n" \
|
||||
" expected no error context\n" \
|
||||
" at %s:%d\n", \
|
||||
#__expr, \
|
||||
__st, akerr_name_for_status(__st, NULL), \
|
||||
aksl_last_message, \
|
||||
__FILE__, __LINE__); \
|
||||
return 1; \
|
||||
} \
|
||||
aksl_last_status = 0; \
|
||||
aksl_last_message[0] = '\0'; \
|
||||
aksl_last_function[0] = '\0'; \
|
||||
} while ( 0 )
|
||||
|
||||
#define AKSL_CHECK_CONTAINS(needle) \
|
||||
AKSL_CHECK(strstr(aksl_capture_buf, (needle)) != NULL)
|
||||
|
||||
#define AKSL_CHECK_NOT_CONTAINS(needle) \
|
||||
AKSL_CHECK(strstr(aksl_capture_buf, (needle)) == NULL)
|
||||
|
||||
/*
|
||||
* Message checks are secondary: the returned akerr status is the contract. Keep
|
||||
* the status and message assertions coupled so tests cannot pass on text alone.
|
||||
*/
|
||||
#define AKSL_CHECK_STATUS_MSG_CONTAINS(__expr, __expected, __needle) \
|
||||
do { \
|
||||
int __st = aksl_take(__expr); \
|
||||
if ( __st != (__expected) ) { \
|
||||
fprintf(stderr, \
|
||||
" CHECK FAILED: %s\n" \
|
||||
" got %d (%s) \"%s\"\n" \
|
||||
" expected %d (%s)\n" \
|
||||
" at %s:%d\n", \
|
||||
#__expr, \
|
||||
__st, akerr_name_for_status(__st, NULL), \
|
||||
aksl_last_message, \
|
||||
(__expected), \
|
||||
akerr_name_for_status((__expected), NULL), \
|
||||
__FILE__, __LINE__); \
|
||||
return 1; \
|
||||
} \
|
||||
if ( strstr(aksl_last_message, (__needle)) == NULL ) { \
|
||||
fprintf(stderr, \
|
||||
" CHECK FAILED: message from %s\n" \
|
||||
" got \"%s\"\n" \
|
||||
" expected substring \"%s\"\n" \
|
||||
" status %d (%s)\n" \
|
||||
" at %s:%d\n", \
|
||||
#__expr, \
|
||||
aksl_last_message, \
|
||||
(__needle), \
|
||||
__st, akerr_name_for_status(__st, NULL), \
|
||||
__FILE__, __LINE__); \
|
||||
return 1; \
|
||||
} \
|
||||
} while ( 0 )
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Test driver */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* 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
|
||||
128
tests/test_convert.c
Normal file
128
tests/test_convert.c
Normal file
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* String -> number wrappers: aksl_atoi / atol / atoll / atof.
|
||||
*
|
||||
* TODO.md section 1.4. This file covers the parts of the contract that are
|
||||
* stable today: the happy paths (including negatives and leading whitespace)
|
||||
* and the NULL guards.
|
||||
*
|
||||
* It deliberately says nothing about non-numeric input, trailing junk or
|
||||
* overflow. Those all return *success* today (TODO.md 2.1.5) and the correct
|
||||
* behaviour is asserted by tests/test_convert_strict.c, which is registered as
|
||||
* a known failure. Pinning the current lax behaviour here as well would mean
|
||||
* this file starts failing on the day the defect is fixed.
|
||||
*/
|
||||
|
||||
#include "aksl_capture.h"
|
||||
|
||||
static int test_atoi_converts_positive(void)
|
||||
{
|
||||
int out = -1;
|
||||
|
||||
AKSL_CHECK_OK(aksl_atoi("1234", &out));
|
||||
AKSL_CHECK(out == 1234);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_atoi_converts_negative(void)
|
||||
{
|
||||
int out = 0;
|
||||
|
||||
AKSL_CHECK_OK(aksl_atoi("-42", &out));
|
||||
AKSL_CHECK(out == -42);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_atoi_skips_leading_whitespace(void)
|
||||
{
|
||||
int out = 0;
|
||||
|
||||
AKSL_CHECK_OK(aksl_atoi(" \t 7", &out));
|
||||
AKSL_CHECK(out == 7);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_atoi_rejects_null_arguments(void)
|
||||
{
|
||||
int out = 0;
|
||||
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_atoi(NULL, &out),
|
||||
AKERR_NULLPOINTER, "nptr=");
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_atoi("1", NULL),
|
||||
AKERR_NULLPOINTER, "dest=");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_atol_converts_and_rejects_null(void)
|
||||
{
|
||||
long out = 0;
|
||||
|
||||
AKSL_CHECK_OK(aksl_atol("2147483648", &out));
|
||||
AKSL_CHECK(out == 2147483648L);
|
||||
AKSL_CHECK_OK(aksl_atol("-2147483648", &out));
|
||||
AKSL_CHECK(out == -2147483648L);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_atol(NULL, &out), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_atol("1", NULL), AKERR_NULLPOINTER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_atoll_converts_and_rejects_null(void)
|
||||
{
|
||||
long long out = 0;
|
||||
|
||||
AKSL_CHECK_OK(aksl_atoll("9007199254740993", &out));
|
||||
AKSL_CHECK(out == 9007199254740993LL);
|
||||
AKSL_CHECK_OK(aksl_atoll("-9007199254740993", &out));
|
||||
AKSL_CHECK(out == -9007199254740993LL);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_atoll(NULL, &out), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_atoll("1", NULL), AKERR_NULLPOINTER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_atof_converts_and_rejects_null(void)
|
||||
{
|
||||
double out = 0.0;
|
||||
|
||||
AKSL_CHECK_OK(aksl_atof("2.5", &out));
|
||||
AKSL_CHECK(out == 2.5);
|
||||
AKSL_CHECK_OK(aksl_atof(" -0.125", &out));
|
||||
AKSL_CHECK(out == -0.125);
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_atof(NULL, &out), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_atof("1", NULL), AKERR_NULLPOINTER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The wrappers hold no state: the same input converts the same way twice. */
|
||||
static int test_conversions_are_repeatable(void)
|
||||
{
|
||||
int first = 0;
|
||||
int second = 0;
|
||||
|
||||
AKSL_CHECK_OK(aksl_atoi("321", &first));
|
||||
AKSL_CHECK_OK(aksl_atoi("321", &second));
|
||||
AKSL_CHECK(first == second);
|
||||
AKSL_CHECK(first == 321);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int failures = 0;
|
||||
|
||||
akerr_init();
|
||||
|
||||
AKSL_RUN(failures, test_atoi_converts_positive);
|
||||
AKSL_RUN(failures, test_atoi_converts_negative);
|
||||
AKSL_RUN(failures, test_atoi_skips_leading_whitespace);
|
||||
AKSL_RUN(failures, test_atoi_rejects_null_arguments);
|
||||
|
||||
AKSL_RUN(failures, test_atol_converts_and_rejects_null);
|
||||
AKSL_RUN(failures, test_atoll_converts_and_rejects_null);
|
||||
AKSL_RUN(failures, test_atof_converts_and_rejects_null);
|
||||
|
||||
AKSL_RUN(failures, test_conversions_are_repeatable);
|
||||
|
||||
AKSL_REPORT(failures);
|
||||
}
|
||||
75
tests/test_convert_strict.c
Normal file
75
tests/test_convert_strict.c
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* KNOWN FAILING -- TODO.md section 2.1.5
|
||||
*
|
||||
* The ato* wrappers cannot report a conversion failure. atoi(3) and friends have
|
||||
* no error channel at all: "not a number" converts to 0 and an overflowing
|
||||
* literal converts to a wrapped value, and in both cases the wrapper hands back
|
||||
* success. A library whose entire purpose is turning silent libc failures into
|
||||
* error contexts should not be the one place a bad conversion passes unnoticed.
|
||||
*
|
||||
* This test asserts the contract the fix should provide -- reimplemented over
|
||||
* strtol/strtoll/strtod with errno cleared, an endptr check for "no digits
|
||||
* consumed" and "trailing junk", and a range check:
|
||||
*
|
||||
* no digits consumed / trailing junk -> AKERR_VALUE
|
||||
* value out of range -> ERANGE
|
||||
*
|
||||
* Registered in AKSL_KNOWN_FAILING_TESTS (WILL_FAIL). When the wrappers get a
|
||||
* real error channel, CTest reports this as unexpectedly passing -- move it into
|
||||
* AKSL_TESTS then, and fold the cases into tests/test_convert.c.
|
||||
*/
|
||||
|
||||
#include "aksl_capture.h"
|
||||
|
||||
#include <errno.h>
|
||||
|
||||
static int test_non_numeric_input_is_a_value_error(void)
|
||||
{
|
||||
int out = 0;
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_atoi("not a number", &out), AKERR_VALUE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_empty_input_is_a_value_error(void)
|
||||
{
|
||||
int out = 0;
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_atoi("", &out), AKERR_VALUE);
|
||||
AKSL_CHECK_STATUS(aksl_atoi(" ", &out), AKERR_VALUE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_trailing_junk_is_a_value_error(void)
|
||||
{
|
||||
int out = 0;
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_atoi("12abc", &out), AKERR_VALUE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_overflow_is_erange(void)
|
||||
{
|
||||
int out = 0;
|
||||
long lout = 0;
|
||||
long long llout = 0;
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_atoi("99999999999999999999", &out), ERANGE);
|
||||
AKSL_CHECK_STATUS(aksl_atol("99999999999999999999999999", &lout), ERANGE);
|
||||
AKSL_CHECK_STATUS(aksl_atoll("99999999999999999999999999", &llout), ERANGE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int failures = 0;
|
||||
|
||||
akerr_init();
|
||||
|
||||
AKSL_RUN(failures, test_non_numeric_input_is_a_value_error);
|
||||
AKSL_RUN(failures, test_empty_input_is_a_value_error);
|
||||
AKSL_RUN(failures, test_trailing_junk_is_a_value_error);
|
||||
AKSL_RUN(failures, test_overflow_is_erange);
|
||||
|
||||
AKSL_REPORT(failures);
|
||||
}
|
||||
224
tests/test_format.c
Normal file
224
tests/test_format.c
Normal file
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Formatted-output wrappers: aksl_printf / aksl_fprintf / aksl_sprintf.
|
||||
*
|
||||
* TODO.md section 1.3. Each happy path asserts both halves of the contract --
|
||||
* the byte count handed back through *count and the text that actually landed
|
||||
* somewhere -- and every pointer argument is checked for its NULL guard.
|
||||
*
|
||||
* Readback goes through plain libc rather than aksl_fread so that a failure here
|
||||
* points at the formatted-output wrapper under test and not at the stream
|
||||
* wrappers, which tests/test_stream.c covers.
|
||||
*
|
||||
* Not covered: the destination-overflow case, because aksl_sprintf wraps the
|
||||
* unbounded vsprintf and there is no bounded entry point to test yet
|
||||
* (TODO.md 2.2.4).
|
||||
*/
|
||||
|
||||
#include "aksl_capture.h"
|
||||
|
||||
#include <errno.h>
|
||||
|
||||
/* Read a whole file into buf and NUL-terminate. Returns bytes read, or -1. */
|
||||
static long read_file(const char *path, char *buf, size_t n)
|
||||
{
|
||||
FILE *fp = fopen(path, "r");
|
||||
size_t got = 0;
|
||||
|
||||
if ( fp == NULL ) {
|
||||
return -1;
|
||||
}
|
||||
got = fread(buf, 1, n - 1, fp);
|
||||
buf[got] = '\0';
|
||||
if ( ferror(fp) ) {
|
||||
fclose(fp);
|
||||
return -1;
|
||||
}
|
||||
fclose(fp);
|
||||
return (long)got;
|
||||
}
|
||||
|
||||
static int test_sprintf_writes_text_and_count(void)
|
||||
{
|
||||
char buf[64];
|
||||
int count = -1;
|
||||
|
||||
memset(buf, 0x00, sizeof(buf));
|
||||
AKSL_CHECK_OK(aksl_sprintf(&count, buf, "%s=%d", "x", 7));
|
||||
AKSL_CHECK(count == 3);
|
||||
AKSL_CHECK(strcmp(buf, "x=7") == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_sprintf_empty_format_writes_nothing(void)
|
||||
{
|
||||
char buf[8] = { 'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z' };
|
||||
int count = -1;
|
||||
|
||||
AKSL_CHECK_OK(aksl_sprintf(&count, buf, "%s", ""));
|
||||
AKSL_CHECK(count == 0);
|
||||
AKSL_CHECK(buf[0] == '\0');
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_sprintf_rejects_null_arguments(void)
|
||||
{
|
||||
char buf[8];
|
||||
int count = 0;
|
||||
|
||||
memset(buf, 0x00, sizeof(buf));
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_sprintf(NULL, buf, "x"),
|
||||
AKERR_NULLPOINTER, "count=");
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_sprintf(&count, NULL, "x"),
|
||||
AKERR_NULLPOINTER, "str=");
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_sprintf(&count, buf, NULL),
|
||||
AKERR_NULLPOINTER, "format=");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_fprintf_writes_to_stream(void)
|
||||
{
|
||||
char path[AKSL_TMP_MAX];
|
||||
char readback[64];
|
||||
FILE *fp = NULL;
|
||||
int count = -1;
|
||||
|
||||
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
|
||||
AKSL_CHECK_OK(aksl_fprintf(&count, fp, "%s %d", "value", 42));
|
||||
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||
|
||||
AKSL_CHECK(count == 8);
|
||||
AKSL_CHECK(read_file(path, readback, sizeof(readback)) == 8);
|
||||
AKSL_CHECK(strcmp(readback, "value 42") == 0);
|
||||
AKSL_CHECK(unlink(path) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* vfprintf on a stream opened "r" fails outright, so the wrapper reports the
|
||||
* errno it saw (EBADF on glibc). *count is left holding -1 in this case, which
|
||||
* TODO.md 1.3 flags as a contract gap -- the status is the assertion here.
|
||||
*/
|
||||
static int test_fprintf_to_read_only_stream_reports_errno(void)
|
||||
{
|
||||
char path[AKSL_TMP_MAX];
|
||||
FILE *fp = NULL;
|
||||
int count = 0;
|
||||
|
||||
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fprintf(&count, fp, "%d", 1),
|
||||
EBADF, "Short write");
|
||||
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||
AKSL_CHECK(unlink(path) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_fprintf_rejects_null_arguments(void)
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fprintf(NULL, stdout, "x"),
|
||||
AKERR_NULLPOINTER, "count=");
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fprintf(&count, NULL, "x"),
|
||||
AKERR_NULLPOINTER, "stream=");
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fprintf(&count, stdout, NULL),
|
||||
AKERR_NULLPOINTER, "format=");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* aksl_printf writes to stdout, so stdout is pointed at a temp file for the
|
||||
* duration of the call and then restored through a dup of the original
|
||||
* descriptor. Nothing between the freopen and the dup2 may return early: an
|
||||
* assertion there would leave stdout attached to the temp file for the rest of
|
||||
* the run, and the test report itself would vanish.
|
||||
*/
|
||||
static int test_printf_writes_to_stdout(void)
|
||||
{
|
||||
char path[AKSL_TMP_MAX];
|
||||
char readback[64];
|
||||
akerr_ErrorContext *err = NULL;
|
||||
int count = -1;
|
||||
int saved = -1;
|
||||
int restored = -1;
|
||||
|
||||
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||
|
||||
fflush(stdout);
|
||||
saved = dup(fileno(stdout));
|
||||
AKSL_CHECK(saved >= 0);
|
||||
if ( freopen(path, "w", stdout) == NULL ) {
|
||||
close(saved);
|
||||
AKSL_CHECK(0);
|
||||
}
|
||||
|
||||
err = aksl_printf(&count, "%s#%d", "out", 5);
|
||||
|
||||
fflush(stdout);
|
||||
restored = dup2(saved, fileno(stdout));
|
||||
close(saved);
|
||||
clearerr(stdout);
|
||||
|
||||
AKSL_CHECK(restored >= 0);
|
||||
AKSL_CHECK(aksl_take(err) == 0);
|
||||
AKSL_CHECK(count == 5);
|
||||
AKSL_CHECK(read_file(path, readback, sizeof(readback)) == 5);
|
||||
AKSL_CHECK(strcmp(readback, "out#5") == 0);
|
||||
AKSL_CHECK(unlink(path) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_printf_rejects_null_arguments(void)
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_printf(NULL, "x"),
|
||||
AKERR_NULLPOINTER, "count=");
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_printf(&count, NULL),
|
||||
AKERR_NULLPOINTER, "format=");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Regression cover for the missing va_end (TODO.md 2.1.4). Nothing here can
|
||||
* assert on register-save state directly; the point is to run the variadic
|
||||
* wrappers enough times, with enough arguments, that the sanitizer build has
|
||||
* something to trip over.
|
||||
*/
|
||||
static int test_variadic_wrappers_survive_repeated_calls(void)
|
||||
{
|
||||
char buf[128];
|
||||
int count = 0;
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < 512; i++ ) {
|
||||
AKSL_CHECK_OK(aksl_sprintf(&count, buf, "%d %s %ld %c %f",
|
||||
i, "iteration", (long)i, 'x', (double)i));
|
||||
AKSL_CHECK(count > 0);
|
||||
AKSL_CHECK((size_t)count == strlen(buf));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int failures = 0;
|
||||
|
||||
akerr_init();
|
||||
|
||||
AKSL_RUN(failures, test_sprintf_writes_text_and_count);
|
||||
AKSL_RUN(failures, test_sprintf_empty_format_writes_nothing);
|
||||
AKSL_RUN(failures, test_sprintf_rejects_null_arguments);
|
||||
|
||||
AKSL_RUN(failures, test_fprintf_writes_to_stream);
|
||||
AKSL_RUN(failures, test_fprintf_to_read_only_stream_reports_errno);
|
||||
AKSL_RUN(failures, test_fprintf_rejects_null_arguments);
|
||||
|
||||
AKSL_RUN(failures, test_printf_writes_to_stdout);
|
||||
AKSL_RUN(failures, test_printf_rejects_null_arguments);
|
||||
|
||||
AKSL_RUN(failures, test_variadic_wrappers_survive_repeated_calls);
|
||||
|
||||
AKSL_REPORT(failures);
|
||||
}
|
||||
320
tests/test_linkedlist.c
Normal file
320
tests/test_linkedlist.c
Normal file
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* 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_MSG_CONTAINS(
|
||||
aksl_list_iterate(&node, &record_visit, &log),
|
||||
AKERR_VALUE, "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);
|
||||
}
|
||||
59
tests/test_list_append_chain.c
Normal file
59
tests/test_list_append_chain.c
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* KNOWN FAILING -- TODO.md section 2.1.1
|
||||
*
|
||||
* aksl_list_append conflates Floyd cycle detection with finding the tail: the
|
||||
* `tail` cursor is assigned from `slow` *before* `slow` advances, so it tracks
|
||||
* the node behind the list midpoint rather than the last node. Appending to any
|
||||
* list of two or more nodes therefore overwrites an interior link and silently
|
||||
* drops every node after it.
|
||||
*
|
||||
* Appending n1..n4 to n0 produces the chain "n0 -> n4"; this test asserts the
|
||||
* correct "n0 -> n1 -> n2 -> n3 -> n4" and so fails until append is fixed.
|
||||
* It is registered in AKSL_KNOWN_FAILING_TESTS, which marks it WILL_FAIL; when
|
||||
* the fix lands, CTest will report it as unexpectedly passing, which is the
|
||||
* signal to move it into AKSL_TESTS.
|
||||
*/
|
||||
|
||||
#include "aksl_capture.h"
|
||||
|
||||
#define CHAIN_LEN 5
|
||||
|
||||
static int test_append_builds_full_chain(void)
|
||||
{
|
||||
aksl_ListNode node[CHAIN_LEN];
|
||||
aksl_ListNode *walk = NULL;
|
||||
int i = 0;
|
||||
|
||||
memset((void *)node, 0x00, sizeof(node));
|
||||
|
||||
for ( i = 1; i < CHAIN_LEN; i++ ) {
|
||||
AKSL_CHECK_OK(aksl_list_append(&node[0], &node[i]));
|
||||
}
|
||||
|
||||
/* Forward links, head to tail. */
|
||||
walk = &node[0];
|
||||
for ( i = 0; i < CHAIN_LEN; i++ ) {
|
||||
AKSL_CHECK(walk == &node[i]);
|
||||
walk = walk->next;
|
||||
}
|
||||
AKSL_CHECK(walk == NULL);
|
||||
|
||||
/* Back links, tail to head. */
|
||||
walk = &node[CHAIN_LEN - 1];
|
||||
for ( i = CHAIN_LEN - 1; i >= 0; i-- ) {
|
||||
AKSL_CHECK(walk == &node[i]);
|
||||
walk = walk->prev;
|
||||
}
|
||||
AKSL_CHECK(walk == NULL);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int failures = 0;
|
||||
|
||||
akerr_init();
|
||||
AKSL_RUN(failures, test_append_builds_full_chain);
|
||||
AKSL_REPORT(failures);
|
||||
}
|
||||
71
tests/test_list_iterate_head.c
Normal file
71
tests/test_list_iterate_head.c
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* KNOWN FAILING -- TODO.md section 2.1.2
|
||||
*
|
||||
* aksl_list_iterate runs a Floyd cycle check that leaves `slow` sitting at the
|
||||
* list midpoint, and then starts the visiting loop from `slow` instead of from
|
||||
* `list`. Every node before the midpoint -- including the head -- is never
|
||||
* passed to the callback.
|
||||
*
|
||||
* The list here is built by hand rather than with aksl_list_append so that this
|
||||
* test fails only for the iterate defect, not for the separate append defect in
|
||||
* TODO.md 2.1.1.
|
||||
*
|
||||
* Registered in AKSL_KNOWN_FAILING_TESTS (WILL_FAIL). When iterate is fixed,
|
||||
* CTest reports this as unexpectedly passing -- move it into AKSL_TESTS then.
|
||||
*/
|
||||
|
||||
#include "aksl_capture.h"
|
||||
|
||||
#define CHAIN_LEN 3
|
||||
|
||||
typedef struct VisitLog
|
||||
{
|
||||
int count;
|
||||
aksl_ListNode *seen[CHAIN_LEN];
|
||||
} VisitLog;
|
||||
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *record_visit(aksl_ListNode *node, void *data)
|
||||
{
|
||||
VisitLog *log = NULL;
|
||||
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
|
||||
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
|
||||
log = (VisitLog *)data;
|
||||
if ( log->count < CHAIN_LEN ) {
|
||||
log->seen[log->count] = node;
|
||||
}
|
||||
log->count += 1;
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
static int test_iterate_visits_every_node_from_the_head(void)
|
||||
{
|
||||
aksl_ListNode node[CHAIN_LEN];
|
||||
VisitLog log;
|
||||
int i = 0;
|
||||
|
||||
memset((void *)node, 0x00, sizeof(node));
|
||||
memset((void *)&log, 0x00, sizeof(log));
|
||||
|
||||
for ( i = 1; i < CHAIN_LEN; i++ ) {
|
||||
node[i - 1].next = &node[i];
|
||||
node[i].prev = &node[i - 1];
|
||||
}
|
||||
|
||||
AKSL_CHECK_OK(aksl_list_iterate(&node[0], &record_visit, &log));
|
||||
AKSL_CHECK(log.count == CHAIN_LEN);
|
||||
for ( i = 0; i < CHAIN_LEN; i++ ) {
|
||||
AKSL_CHECK(log.seen[i] == &node[i]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int failures = 0;
|
||||
|
||||
akerr_init();
|
||||
AKSL_RUN(failures, test_iterate_visits_every_node_from_the_head);
|
||||
AKSL_REPORT(failures);
|
||||
}
|
||||
150
tests/test_memory.c
Normal file
150
tests/test_memory.c
Normal file
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Memory-wrapper behaviour that is deterministic today.
|
||||
*
|
||||
* TODO.md section 1.1 also calls out malloc(0), malloc(SIZE_MAX), and
|
||||
* overlapping memcpy. Those are intentionally not pinned here yet: malloc(0)
|
||||
* is platform-dependent under the current wrapper, huge allocations need
|
||||
* sanitizer-safe handling, and overlapping memcpy is still an API-contract
|
||||
* decision.
|
||||
*/
|
||||
|
||||
#include "aksl_capture.h"
|
||||
|
||||
static int test_malloc_writes_nonnull_pointer(void)
|
||||
{
|
||||
void *ptr = NULL;
|
||||
|
||||
AKSL_CHECK_OK(aksl_malloc(32, &ptr));
|
||||
AKSL_CHECK(ptr != NULL);
|
||||
AKSL_CHECK_OK(aksl_free(ptr));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_malloc_rejects_null_destination(void)
|
||||
{
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_malloc(32, NULL),
|
||||
AKERR_NULLPOINTER, "NULL");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_free_rejects_null_pointer(void)
|
||||
{
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_free(NULL),
|
||||
AKERR_NULLPOINTER, "NULL");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_malloc_free_round_trip(void)
|
||||
{
|
||||
unsigned char *buf = NULL;
|
||||
size_t i = 0;
|
||||
|
||||
AKSL_CHECK_OK(aksl_malloc(64, (void **)&buf));
|
||||
AKSL_CHECK(buf != NULL);
|
||||
for ( i = 0; i < 64; i++ ) {
|
||||
buf[i] = (unsigned char)i;
|
||||
}
|
||||
for ( i = 0; i < 64; i++ ) {
|
||||
AKSL_CHECK(buf[i] == (unsigned char)i);
|
||||
}
|
||||
AKSL_CHECK_OK(aksl_free(buf));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_memset_fills_buffer(void)
|
||||
{
|
||||
unsigned char buf[8];
|
||||
size_t i = 0;
|
||||
|
||||
memset((void *)buf, 0x00, sizeof(buf));
|
||||
AKSL_CHECK_OK(aksl_memset(buf, 0x5a, sizeof(buf)));
|
||||
for ( i = 0; i < sizeof(buf); i++ ) {
|
||||
AKSL_CHECK(buf[i] == 0x5a);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_memset_zero_length_is_noop(void)
|
||||
{
|
||||
unsigned char buf[4] = { 1, 2, 3, 4 };
|
||||
|
||||
AKSL_CHECK_OK(aksl_memset(buf, 0xff, 0));
|
||||
AKSL_CHECK(buf[0] == 1);
|
||||
AKSL_CHECK(buf[1] == 2);
|
||||
AKSL_CHECK(buf[2] == 3);
|
||||
AKSL_CHECK(buf[3] == 4);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_memset_rejects_null_buffer(void)
|
||||
{
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_memset(NULL, 0x00, 4),
|
||||
AKERR_NULLPOINTER, "s=");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_memcpy_copies_bytes(void)
|
||||
{
|
||||
unsigned char src[6] = { 0, 1, 2, 3, 4, 5 };
|
||||
unsigned char dst[6] = { 9, 9, 9, 9, 9, 9 };
|
||||
size_t i = 0;
|
||||
|
||||
AKSL_CHECK_OK(aksl_memcpy(dst, src, sizeof(src)));
|
||||
for ( i = 0; i < sizeof(src); i++ ) {
|
||||
AKSL_CHECK(dst[i] == src[i]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_memcpy_zero_length_is_noop(void)
|
||||
{
|
||||
unsigned char src[3] = { 1, 2, 3 };
|
||||
unsigned char dst[3] = { 4, 5, 6 };
|
||||
|
||||
AKSL_CHECK_OK(aksl_memcpy(dst, src, 0));
|
||||
AKSL_CHECK(dst[0] == 4);
|
||||
AKSL_CHECK(dst[1] == 5);
|
||||
AKSL_CHECK(dst[2] == 6);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_memcpy_rejects_null_destination(void)
|
||||
{
|
||||
unsigned char src[1] = { 1 };
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_memcpy(NULL, src, sizeof(src)),
|
||||
AKERR_NULLPOINTER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_memcpy_rejects_null_source(void)
|
||||
{
|
||||
unsigned char dst[1] = { 0 };
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_memcpy(dst, NULL, sizeof(dst)),
|
||||
AKERR_NULLPOINTER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int failures = 0;
|
||||
|
||||
akerr_init();
|
||||
|
||||
AKSL_RUN(failures, test_malloc_writes_nonnull_pointer);
|
||||
AKSL_RUN(failures, test_malloc_rejects_null_destination);
|
||||
AKSL_RUN(failures, test_free_rejects_null_pointer);
|
||||
AKSL_RUN(failures, test_malloc_free_round_trip);
|
||||
|
||||
AKSL_RUN(failures, test_memset_fills_buffer);
|
||||
AKSL_RUN(failures, test_memset_zero_length_is_noop);
|
||||
AKSL_RUN(failures, test_memset_rejects_null_buffer);
|
||||
|
||||
AKSL_RUN(failures, test_memcpy_copies_bytes);
|
||||
AKSL_RUN(failures, test_memcpy_zero_length_is_noop);
|
||||
AKSL_RUN(failures, test_memcpy_rejects_null_destination);
|
||||
AKSL_RUN(failures, test_memcpy_rejects_null_source);
|
||||
|
||||
AKSL_REPORT(failures);
|
||||
}
|
||||
116
tests/test_path.c
Normal file
116
tests/test_path.c
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* aksl_realpath -- TODO.md section 1.5.
|
||||
*
|
||||
* The happy paths compare against realpath(3) itself rather than against a
|
||||
* hard-coded string, because $TMPDIR may itself be a symlink (/tmp -> /private/tmp
|
||||
* and friends) and the resolved answer is what the platform says it is.
|
||||
*
|
||||
* Every failure case here passes a *zeroed* resolved_path buffer. That is
|
||||
* deliberate: on failure the wrapper formats resolved_path with %s while
|
||||
* realpath(3) leaves the buffer unspecified (TODO.md 2.1.6), so a test that
|
||||
* passed an uninitialised buffer would be reading uninitialised memory in the
|
||||
* library's own error path. The uninitialised-buffer crash is the defect's own
|
||||
* test to write, not something these should trip over incidentally.
|
||||
*
|
||||
* Also not covered: resolved_path == NULL, which is unchecked today and leaks
|
||||
* the buffer realpath(3) allocates (2.1.6).
|
||||
*/
|
||||
|
||||
#include "aksl_capture.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
|
||||
static int test_resolves_an_existing_file(void)
|
||||
{
|
||||
char path[AKSL_TMP_MAX];
|
||||
char resolved[PATH_MAX];
|
||||
char expected[PATH_MAX];
|
||||
|
||||
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||
memset(resolved, 0x00, sizeof(resolved));
|
||||
AKSL_CHECK(realpath(path, expected) != NULL);
|
||||
|
||||
AKSL_CHECK_OK(aksl_realpath(path, resolved));
|
||||
AKSL_CHECK(strcmp(resolved, expected) == 0);
|
||||
AKSL_CHECK(resolved[0] == '/');
|
||||
AKSL_CHECK(unlink(path) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* A symlink resolves to its target, not to itself. */
|
||||
static int test_resolves_a_symlink_to_its_target(void)
|
||||
{
|
||||
char target[AKSL_TMP_MAX];
|
||||
char link[AKSL_TMP_MAX];
|
||||
char resolved[PATH_MAX];
|
||||
char expected[PATH_MAX];
|
||||
|
||||
AKSL_CHECK(aksl_temp_file(target, sizeof(target)) == 0);
|
||||
AKSL_CHECK(aksl_temp_file(link, sizeof(link)) == 0);
|
||||
/* mkstemp created the link path as a regular file; symlink needs it gone. */
|
||||
AKSL_CHECK(unlink(link) == 0);
|
||||
AKSL_CHECK(symlink(target, link) == 0);
|
||||
|
||||
memset(resolved, 0x00, sizeof(resolved));
|
||||
AKSL_CHECK(realpath(target, expected) != NULL);
|
||||
AKSL_CHECK_OK(aksl_realpath(link, resolved));
|
||||
AKSL_CHECK(strcmp(resolved, expected) == 0);
|
||||
|
||||
AKSL_CHECK(unlink(link) == 0);
|
||||
AKSL_CHECK(unlink(target) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_missing_path_reports_enoent(void)
|
||||
{
|
||||
char resolved[PATH_MAX];
|
||||
|
||||
memset(resolved, 0x00, sizeof(resolved));
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||
aksl_realpath("/nonexistent/aksl/path", resolved),
|
||||
ENOENT, "/nonexistent/aksl/path");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* A regular file used as a directory component is ENOTDIR, not ENOENT. */
|
||||
static int test_non_directory_component_reports_enotdir(void)
|
||||
{
|
||||
char path[AKSL_TMP_MAX];
|
||||
char child[AKSL_TMP_MAX + 8];
|
||||
char resolved[PATH_MAX];
|
||||
|
||||
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||
AKSL_CHECK((size_t)snprintf(child, sizeof(child), "%s/child", path)
|
||||
< sizeof(child));
|
||||
|
||||
memset(resolved, 0x00, sizeof(resolved));
|
||||
AKSL_CHECK_STATUS(aksl_realpath(child, resolved), ENOTDIR);
|
||||
AKSL_CHECK(unlink(path) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_rejects_null_path(void)
|
||||
{
|
||||
char resolved[PATH_MAX];
|
||||
|
||||
memset(resolved, 0x00, sizeof(resolved));
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath(NULL, resolved),
|
||||
AKERR_NULLPOINTER, "path=");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int failures = 0;
|
||||
|
||||
akerr_init();
|
||||
|
||||
AKSL_RUN(failures, test_resolves_an_existing_file);
|
||||
AKSL_RUN(failures, test_resolves_a_symlink_to_its_target);
|
||||
AKSL_RUN(failures, test_missing_path_reports_enoent);
|
||||
AKSL_RUN(failures, test_non_directory_component_reports_enotdir);
|
||||
AKSL_RUN(failures, test_rejects_null_path);
|
||||
|
||||
AKSL_REPORT(failures);
|
||||
}
|
||||
225
tests/test_status_registry.c
Normal file
225
tests/test_status_registry.c
Normal file
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* libakstdlib's side of the libakerror 1.0.0 status registry contract.
|
||||
*
|
||||
* See deps/libakerror/UPGRADING.md. That release made the status-name table
|
||||
* private, moved consumer status codes to a reserved band starting at
|
||||
* AKERR_FIRST_CONSUMER_STATUS, and made ownership of a range enforced rather
|
||||
* than advisory. None of it broke this library's build, because libakstdlib
|
||||
* defines no status codes of its own -- it raises libakerror's AKERR_* codes and
|
||||
* propagates the host's errno values, both of which live in libakerror's own
|
||||
* 0-255 band.
|
||||
*
|
||||
* That "defines none of its own" is now a contract worth pinning rather than an
|
||||
* accident, because it is what lets libakstdlib sit in a process alongside any
|
||||
* other libakerror consumer without a range negotiation. These tests assert it,
|
||||
* and assert that the registry the codes are looked up in is actually populated
|
||||
* -- a status whose name fails to register degrades to "Unknown Error" in every
|
||||
* later stack trace, which is a silent loss of debuggability rather than a
|
||||
* failure anyone would notice.
|
||||
*
|
||||
* If libakstdlib ever does define its own status codes, this file is where the
|
||||
* reservation gets asserted: replace test_reserves_no_consumer_range with one
|
||||
* that reserves the library's declared range and confirms its names registered.
|
||||
*/
|
||||
|
||||
#include "aksl_capture.h"
|
||||
|
||||
#include <errno.h>
|
||||
|
||||
/*
|
||||
* Every status libakstdlib raises, and where from. All of them belong to
|
||||
* libakerror or to the host's errno space; none is defined here.
|
||||
*/
|
||||
static const struct {
|
||||
int status;
|
||||
const char *symbol;
|
||||
const char *raised_by;
|
||||
} aksl_raised_statuses[] = {
|
||||
{ AKERR_NULLPOINTER, "AKERR_NULLPOINTER", "every NULL parameter guard" },
|
||||
{ AKERR_IO, "AKERR_IO", "aksl_fread / aksl_fwrite short count" },
|
||||
{ AKERR_EOF, "AKERR_EOF", "aksl_fread at end of stream" },
|
||||
{ AKERR_ITERATOR_BREAK, "AKERR_ITERATOR_BREAK", "aksl_list_iterate / aksl_tree_iterate" },
|
||||
{ AKERR_CIRCULAR_REFERENCE, "AKERR_CIRCULAR_REFERENCE", "aksl_list_append cycle guard" },
|
||||
{ AKERR_NOT_IMPLEMENTED, "AKERR_NOT_IMPLEMENTED", "unsupported tree search modes" },
|
||||
{ ENOENT, "ENOENT", "errno propagated by aksl_fopen" },
|
||||
};
|
||||
|
||||
#define AKSL_RAISED_STATUS_COUNT \
|
||||
((int)(sizeof(aksl_raised_statuses) / sizeof(aksl_raised_statuses[0])))
|
||||
|
||||
/*
|
||||
* Ranges this file claims out of the consumer band. A reservation is
|
||||
* process-global and there is no way to give one back, so each test function
|
||||
* gets its own slice and no two overlap.
|
||||
*
|
||||
* +0 .. +15 test_reserves_no_consumer_range
|
||||
* +16 .. +31 test_range_ownership_is_enforced
|
||||
* +32 .. +47 test_naming_a_foreign_status_is_refused
|
||||
*/
|
||||
#define AKSL_TEST_RANGE_FREE (AKERR_FIRST_CONSUMER_STATUS + 0)
|
||||
#define AKSL_TEST_RANGE_OWNED (AKERR_FIRST_CONSUMER_STATUS + 16)
|
||||
#define AKSL_TEST_RANGE_FOREIGN (AKERR_FIRST_CONSUMER_STATUS + 32)
|
||||
#define AKSL_TEST_RANGE_SIZE 16
|
||||
|
||||
#define AKSL_OWNER_A "aksl-test-a"
|
||||
#define AKSL_OWNER_B "aksl-test-b"
|
||||
|
||||
/*
|
||||
* The band boundary this library's "raises nothing of its own" claim rests on.
|
||||
* Asserted rather than assumed: if libakerror ever moves the boundary, the
|
||||
* status-band assertion below stops meaning what it says.
|
||||
*/
|
||||
static int test_consumer_band_starts_at_256(void)
|
||||
{
|
||||
AKSL_CHECK(AKERR_FIRST_CONSUMER_STATUS == 256);
|
||||
AKSL_CHECK(AKERR_RESERVED_STATUS_COUNT == 256);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Every status libakstdlib raises falls inside libakerror's reserved band, so it
|
||||
* cannot collide with a consumer that allocates from 256 up.
|
||||
*/
|
||||
static int test_raised_statuses_are_in_the_reserved_band(void)
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < AKSL_RAISED_STATUS_COUNT; i++ ) {
|
||||
if ( aksl_raised_statuses[i].status >= AKERR_FIRST_CONSUMER_STATUS ) {
|
||||
fprintf(stderr,
|
||||
" CHECK FAILED: %s (%d) is outside libakerror's reserved band\n"
|
||||
" raised by %s\n"
|
||||
" at %s:%d\n",
|
||||
aksl_raised_statuses[i].symbol,
|
||||
aksl_raised_statuses[i].status,
|
||||
aksl_raised_statuses[i].raised_by,
|
||||
__FILE__, __LINE__);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* The registry actually holds a name for each of them. This is the check that
|
||||
* catches a build whose name table is too small: registration failures are
|
||||
* reported at akerr_init() time, but the visible symptom afterwards is only that
|
||||
* stack traces read "Unknown Error", which no other test would notice.
|
||||
*/
|
||||
static int test_raised_statuses_have_registered_names(void)
|
||||
{
|
||||
int i = 0;
|
||||
char *name = NULL;
|
||||
|
||||
for ( i = 0; i < AKSL_RAISED_STATUS_COUNT; i++ ) {
|
||||
name = akerr_name_for_status(aksl_raised_statuses[i].status, NULL);
|
||||
if ( name == NULL || strcmp(name, "Unknown Error") == 0 ) {
|
||||
fprintf(stderr,
|
||||
" CHECK FAILED: %s (%d) has no registered name\n"
|
||||
" raised by %s\n"
|
||||
" reads back as \"%s\"\n"
|
||||
" at %s:%d\n",
|
||||
aksl_raised_statuses[i].symbol,
|
||||
aksl_raised_statuses[i].status,
|
||||
aksl_raised_statuses[i].raised_by,
|
||||
name == NULL ? "(NULL)" : name,
|
||||
__FILE__, __LINE__);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* libakstdlib reserves nothing in the consumer band, so an application is free
|
||||
* to allocate from AKERR_FIRST_CONSUMER_STATUS without coordinating with it.
|
||||
* The library is driven first so that anything it might do lazily has happened
|
||||
* before the range is claimed.
|
||||
*/
|
||||
static int test_reserves_no_consumer_range(void)
|
||||
{
|
||||
void *p = NULL;
|
||||
|
||||
AKSL_CHECK_OK(aksl_malloc(16, &p));
|
||||
AKSL_CHECK_OK(aksl_free(p));
|
||||
|
||||
AKSL_CHECK_OK(akerr_reserve_status_range(AKSL_TEST_RANGE_FREE,
|
||||
AKSL_TEST_RANGE_SIZE,
|
||||
AKSL_OWNER_A));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Ownership is enforced, which is what makes the assertion above meaningful --
|
||||
* a reservation that succeeded against an already-claimed range would prove
|
||||
* nothing. libakerror's own 0-255 band is refused for the same reason.
|
||||
*/
|
||||
static int test_range_ownership_is_enforced(void)
|
||||
{
|
||||
AKSL_CHECK_OK(akerr_reserve_status_range(AKSL_TEST_RANGE_OWNED,
|
||||
AKSL_TEST_RANGE_SIZE,
|
||||
AKSL_OWNER_A));
|
||||
|
||||
/* An identical reservation by the same owner is a documented no-op. */
|
||||
AKSL_CHECK_OK(akerr_reserve_status_range(AKSL_TEST_RANGE_OWNED,
|
||||
AKSL_TEST_RANGE_SIZE,
|
||||
AKSL_OWNER_A));
|
||||
|
||||
/* The same range under a different owner is not. */
|
||||
AKSL_CHECK_STATUS(akerr_reserve_status_range(AKSL_TEST_RANGE_OWNED,
|
||||
AKSL_TEST_RANGE_SIZE,
|
||||
AKSL_OWNER_B),
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
|
||||
/* Nor is any part of libakerror's own band. */
|
||||
AKSL_CHECK_STATUS(akerr_reserve_status_range(AKERR_NULLPOINTER, 1, AKSL_OWNER_B),
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Naming is enforced against the reservation too: a status nobody reserved and a
|
||||
* status somebody else reserved are both refused, with different codes.
|
||||
*/
|
||||
static int test_naming_a_foreign_status_is_refused(void)
|
||||
{
|
||||
AKSL_CHECK_STATUS(akerr_register_status_name(AKSL_OWNER_A,
|
||||
AKSL_TEST_RANGE_FOREIGN,
|
||||
"Unreserved"),
|
||||
AKERR_STATUS_NAME_UNRESERVED);
|
||||
|
||||
AKSL_CHECK_OK(akerr_reserve_status_range(AKSL_TEST_RANGE_FOREIGN,
|
||||
AKSL_TEST_RANGE_SIZE,
|
||||
AKSL_OWNER_A));
|
||||
|
||||
AKSL_CHECK_OK(akerr_register_status_name(AKSL_OWNER_A,
|
||||
AKSL_TEST_RANGE_FOREIGN,
|
||||
"Mine"));
|
||||
AKSL_CHECK(strcmp(akerr_name_for_status(AKSL_TEST_RANGE_FOREIGN, NULL),
|
||||
"Mine") == 0);
|
||||
|
||||
AKSL_CHECK_STATUS(akerr_register_status_name(AKSL_OWNER_B,
|
||||
AKSL_TEST_RANGE_FOREIGN,
|
||||
"Theirs"),
|
||||
AKERR_STATUS_NAME_FOREIGN);
|
||||
/* The refused registration left the owner's name in place. */
|
||||
AKSL_CHECK(strcmp(akerr_name_for_status(AKSL_TEST_RANGE_FOREIGN, NULL),
|
||||
"Mine") == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int failures = 0;
|
||||
|
||||
akerr_init();
|
||||
|
||||
AKSL_RUN(failures, test_consumer_band_starts_at_256);
|
||||
AKSL_RUN(failures, test_raised_statuses_are_in_the_reserved_band);
|
||||
AKSL_RUN(failures, test_raised_statuses_have_registered_names);
|
||||
AKSL_RUN(failures, test_reserves_no_consumer_range);
|
||||
AKSL_RUN(failures, test_range_ownership_is_enforced);
|
||||
AKSL_RUN(failures, test_naming_a_foreign_status_is_refused);
|
||||
|
||||
AKSL_REPORT(failures);
|
||||
}
|
||||
188
tests/test_stream.c
Normal file
188
tests/test_stream.c
Normal file
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Stream wrappers: aksl_fopen / aksl_fread / aksl_fwrite / aksl_fclose.
|
||||
*
|
||||
* TODO.md section 1.2. Covered here: the happy paths, the round trip, the NULL
|
||||
* guards that exist, and the two error statuses the wrappers can actually
|
||||
* produce today -- AKERR_EOF from a short read and AKERR_IO from a stream whose
|
||||
* error indicator is set.
|
||||
*
|
||||
* Not covered, because the behaviour is a documented gap rather than a
|
||||
* contract: aksl_fopen(NULL, ...) and aksl_fopen(path, NULL, ...) are unchecked
|
||||
* (2.2.2), aksl_fread/aksl_fwrite never check ptr and report a short transfer
|
||||
* that is neither EOF nor error as complete success (2.2.3).
|
||||
*
|
||||
* Temp files come from aksl_temp_file() and are unlinked by the test that made
|
||||
* them, so a failing test leaves nothing behind but the file it was mid-way
|
||||
* through.
|
||||
*/
|
||||
|
||||
#include "aksl_capture.h"
|
||||
|
||||
#include <errno.h>
|
||||
|
||||
static int test_fopen_writes_stream_pointer(void)
|
||||
{
|
||||
char path[AKSL_TMP_MAX];
|
||||
FILE *fp = NULL;
|
||||
|
||||
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
|
||||
AKSL_CHECK(fp != NULL);
|
||||
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||
AKSL_CHECK(unlink(path) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_fopen_reports_missing_path(void)
|
||||
{
|
||||
FILE *fp = NULL;
|
||||
|
||||
/* The pathname belongs in the message; the status is the errno fopen saw. */
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||
aksl_fopen("/nonexistent/aksl/stream", "r", &fp),
|
||||
ENOENT, "/nonexistent/aksl/stream");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_fopen_rejects_null_stream_out(void)
|
||||
{
|
||||
char path[AKSL_TMP_MAX];
|
||||
|
||||
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(path, "r", NULL),
|
||||
AKERR_NULLPOINTER, "NULL");
|
||||
AKSL_CHECK(unlink(path) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* fopen -> fwrite -> fclose -> fopen -> fread -> compare, all through the wrappers. */
|
||||
static int test_write_read_round_trip(void)
|
||||
{
|
||||
char path[AKSL_TMP_MAX];
|
||||
char payload[] = "libakstdlib round trip";
|
||||
char readback[sizeof(payload)];
|
||||
FILE *fp = NULL;
|
||||
|
||||
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||
|
||||
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
|
||||
AKSL_CHECK_OK(aksl_fwrite(payload, 1, sizeof(payload), fp));
|
||||
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||
|
||||
fp = NULL;
|
||||
memset(readback, 0x00, sizeof(readback));
|
||||
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
|
||||
AKSL_CHECK_OK(aksl_fread(readback, 1, sizeof(readback), fp));
|
||||
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||
|
||||
AKSL_CHECK(memcmp(payload, readback, sizeof(payload)) == 0);
|
||||
AKSL_CHECK(unlink(path) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Asking for more members than the file holds sets feof, which is AKERR_EOF. */
|
||||
static int test_fread_short_read_is_eof(void)
|
||||
{
|
||||
char path[AKSL_TMP_MAX];
|
||||
char buf[32];
|
||||
FILE *fp = NULL;
|
||||
|
||||
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||
|
||||
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
|
||||
AKSL_CHECK_OK(aksl_fwrite("abcd", 1, 4, fp));
|
||||
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||
|
||||
fp = NULL;
|
||||
memset(buf, 0x00, sizeof(buf));
|
||||
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), fp),
|
||||
AKERR_EOF, "EOF");
|
||||
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||
|
||||
/* The bytes that did arrive are still in the buffer. */
|
||||
AKSL_CHECK(memcmp(buf, "abcd", 4) == 0);
|
||||
AKSL_CHECK(unlink(path) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* A stream opened "w" has no read permission, so fread sets the error indicator
|
||||
* rather than the EOF one: AKERR_IO, not AKERR_EOF.
|
||||
*/
|
||||
static int test_fread_from_write_only_stream_is_io_error(void)
|
||||
{
|
||||
char path[AKSL_TMP_MAX];
|
||||
char buf[4];
|
||||
FILE *fp = NULL;
|
||||
|
||||
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
|
||||
memset(buf, 0x00, sizeof(buf));
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), fp),
|
||||
AKERR_IO, "Error reading file");
|
||||
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||
AKSL_CHECK(unlink(path) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_fread_rejects_null_stream(void)
|
||||
{
|
||||
char buf[4];
|
||||
|
||||
memset(buf, 0x00, sizeof(buf));
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), NULL),
|
||||
AKERR_NULLPOINTER, "NULL");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Mirror image of the fread case: a "r" stream cannot be written to. */
|
||||
static int test_fwrite_to_read_only_stream_is_io_error(void)
|
||||
{
|
||||
char path[AKSL_TMP_MAX];
|
||||
FILE *fp = NULL;
|
||||
|
||||
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
|
||||
AKSL_CHECK_STATUS(aksl_fwrite("xy", 1, 2, fp), AKERR_IO);
|
||||
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||
AKSL_CHECK(unlink(path) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_fwrite_rejects_null_stream(void)
|
||||
{
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fwrite("xy", 1, 2, NULL),
|
||||
AKERR_NULLPOINTER, "NULL");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_fclose_rejects_null_stream(void)
|
||||
{
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fclose(NULL),
|
||||
AKERR_NULLPOINTER, "NULL");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int failures = 0;
|
||||
|
||||
akerr_init();
|
||||
|
||||
AKSL_RUN(failures, test_fopen_writes_stream_pointer);
|
||||
AKSL_RUN(failures, test_fopen_reports_missing_path);
|
||||
AKSL_RUN(failures, test_fopen_rejects_null_stream_out);
|
||||
|
||||
AKSL_RUN(failures, test_write_read_round_trip);
|
||||
AKSL_RUN(failures, test_fread_short_read_is_eof);
|
||||
AKSL_RUN(failures, test_fread_from_write_only_stream_is_io_error);
|
||||
AKSL_RUN(failures, test_fread_rejects_null_stream);
|
||||
|
||||
AKSL_RUN(failures, test_fwrite_to_read_only_stream_is_io_error);
|
||||
AKSL_RUN(failures, test_fwrite_rejects_null_stream);
|
||||
|
||||
AKSL_RUN(failures, test_fclose_rejects_null_stream);
|
||||
|
||||
AKSL_REPORT(failures);
|
||||
}
|
||||
103
tests/test_strhash.c
Normal file
103
tests/test_strhash.c
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* aksl_strhash_djb2 -- TODO.md section 1.6.
|
||||
*
|
||||
* The expected values are the canonical djb2 ones: h = 5381, then
|
||||
* h = h * 33 + byte for each of len bytes, truncated to 32 bits. They were
|
||||
* computed independently of this implementation.
|
||||
*
|
||||
* Every vector here is 7-bit ASCII, where signed and unsigned char agree. The
|
||||
* high-bit case ("\xff\xfe") is the sign-extension defect in TODO.md 2.2.6 and
|
||||
* is left for a test that can be registered as a known failure.
|
||||
*/
|
||||
|
||||
#include "aksl_capture.h"
|
||||
|
||||
static int test_empty_string_is_the_djb2_seed(void)
|
||||
{
|
||||
uint32_t h = 0;
|
||||
|
||||
AKSL_CHECK_OK(aksl_strhash_djb2("", 0, &h));
|
||||
AKSL_CHECK(h == 5381);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* len drives the loop, so a zero length ignores the contents entirely. */
|
||||
static int test_zero_length_ignores_the_buffer(void)
|
||||
{
|
||||
char buf[] = "ignored";
|
||||
uint32_t h = 0;
|
||||
|
||||
AKSL_CHECK_OK(aksl_strhash_djb2(buf, 0, &h));
|
||||
AKSL_CHECK(h == 5381);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_known_answer_vectors(void)
|
||||
{
|
||||
char hello[] = "hello";
|
||||
char libname[] = "libakstdlib";
|
||||
uint32_t h = 0;
|
||||
|
||||
AKSL_CHECK_OK(aksl_strhash_djb2(hello, 5, &h));
|
||||
AKSL_CHECK(h == 261238937u);
|
||||
|
||||
AKSL_CHECK_OK(aksl_strhash_djb2(libname, 11, &h));
|
||||
AKSL_CHECK(h == 884285482u);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The function is length-driven, not NUL-driven: an embedded NUL is hashed. */
|
||||
static int test_embedded_nul_is_hashed(void)
|
||||
{
|
||||
char buf[3] = { 'a', '\0', 'b' };
|
||||
uint32_t whole = 0;
|
||||
uint32_t prefix = 0;
|
||||
|
||||
AKSL_CHECK_OK(aksl_strhash_djb2(buf, sizeof(buf), &whole));
|
||||
AKSL_CHECK(whole == 193482728u);
|
||||
|
||||
/* Stopping at the NUL would give the one-byte hash instead. */
|
||||
AKSL_CHECK_OK(aksl_strhash_djb2(buf, 1, &prefix));
|
||||
AKSL_CHECK(prefix != whole);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_hash_is_stable_across_calls(void)
|
||||
{
|
||||
char buf[] = "repeatable";
|
||||
uint32_t first = 0;
|
||||
uint32_t second = 0;
|
||||
|
||||
AKSL_CHECK_OK(aksl_strhash_djb2(buf, sizeof(buf) - 1, &first));
|
||||
AKSL_CHECK_OK(aksl_strhash_djb2(buf, sizeof(buf) - 1, &second));
|
||||
AKSL_CHECK(first == second);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_rejects_null_arguments(void)
|
||||
{
|
||||
char buf[] = "x";
|
||||
uint32_t h = 0;
|
||||
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strhash_djb2(NULL, 1, &h),
|
||||
AKERR_NULLPOINTER, "str");
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strhash_djb2(buf, 1, NULL),
|
||||
AKERR_NULLPOINTER, "hashval");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int failures = 0;
|
||||
|
||||
akerr_init();
|
||||
|
||||
AKSL_RUN(failures, test_empty_string_is_the_djb2_seed);
|
||||
AKSL_RUN(failures, test_zero_length_ignores_the_buffer);
|
||||
AKSL_RUN(failures, test_known_answer_vectors);
|
||||
AKSL_RUN(failures, test_embedded_nul_is_hashed);
|
||||
AKSL_RUN(failures, test_hash_is_stable_across_calls);
|
||||
AKSL_RUN(failures, test_rejects_null_arguments);
|
||||
|
||||
AKSL_REPORT(failures);
|
||||
}
|
||||
182
tests/test_tree.c
Normal file
182
tests/test_tree.c
Normal file
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
/*
|
||||
* Both breadth-first modes are declared in the header but not implemented, and
|
||||
* say so through AKERR_NOT_IMPLEMENTED rather than by silently visiting nothing.
|
||||
* TODO.md 2.2.10 tracks implementing them; until then this is the contract.
|
||||
*/
|
||||
static int bfs_reports_not_implemented(uint8_t searchmode)
|
||||
{
|
||||
aksl_TreeNode tree[MAX_LEAVES];
|
||||
TreeSearchParams parms;
|
||||
|
||||
build_tree(tree, &parms);
|
||||
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||
aksl_tree_iterate(&tree[0], &find_value, NULL, NULL, searchmode,
|
||||
&parms, NULL),
|
||||
AKERR_NOT_IMPLEMENTED, "Searchmode");
|
||||
AKSL_CHECK(parms.steps == 0);
|
||||
AKSL_CHECK(parms.node == NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_bfs_is_not_implemented(void)
|
||||
{
|
||||
return bfs_reports_not_implemented(AKSL_TREE_SEARCH_BFS);
|
||||
}
|
||||
|
||||
static int test_bfs_right_is_not_implemented(void)
|
||||
{
|
||||
return bfs_reports_not_implemented(AKSL_TREE_SEARCH_BFS_RIGHT);
|
||||
}
|
||||
|
||||
static int test_iterate_null_arguments(void)
|
||||
{
|
||||
aksl_TreeNode tree[MAX_LEAVES];
|
||||
TreeSearchParams parms;
|
||||
|
||||
build_tree(tree, &parms);
|
||||
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||
aksl_tree_iterate(NULL, &find_value, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER, &parms, NULL),
|
||||
AKERR_NULLPOINTER, "root");
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||
aksl_tree_iterate(&tree[0], NULL, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER, &parms, NULL),
|
||||
AKERR_NULLPOINTER, "iter");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* A callback error that is not AKERR_ITERATOR_BREAK reaches the caller. */
|
||||
static int test_iterate_propagates_callback_error(void)
|
||||
{
|
||||
aksl_TreeNode tree[MAX_LEAVES];
|
||||
TreeSearchParams parms;
|
||||
|
||||
build_tree(tree, &parms);
|
||||
/* find_value raises AKERR_NULLPOINTER when it is handed no data. */
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||
aksl_tree_iterate(&tree[0], &find_value, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER, NULL, NULL),
|
||||
AKERR_NULLPOINTER, "data");
|
||||
return 0;
|
||||
}
|
||||
|
||||
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_RUN(failures, test_bfs_is_not_implemented);
|
||||
AKSL_RUN(failures, test_bfs_right_is_not_implemented);
|
||||
AKSL_RUN(failures, test_iterate_null_arguments);
|
||||
AKSL_RUN(failures, test_iterate_propagates_callback_error);
|
||||
|
||||
AKSL_REPORT(failures);
|
||||
}
|
||||
89
tests/test_tree_iterate_break.c
Normal file
89
tests/test_tree_iterate_break.c
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* KNOWN FAILING -- TODO.md section 2.1.3
|
||||
*
|
||||
* AKERR_ITERATOR_BREAK does not abort a tree traversal. aksl_tree_iterate
|
||||
* recurses into itself, and the frame in which the callback raises the break
|
||||
* handles it in its own PROCESS/HANDLE(e, AKERR_ITERATOR_BREAK) block and
|
||||
* returns *success*. The parent frame's PASS therefore sees no error at all and
|
||||
* carries straight on into the sibling subtree, so the traversal runs to
|
||||
* completion.
|
||||
*
|
||||
* The 7-node tree below is walked pre-order (0, 1, 3, 4, 2, 5, 6) with the
|
||||
* callback breaking on tree[3], the third node visited. A working break stops
|
||||
* the walk at 3 visits; today the callback is invoked all 7 times.
|
||||
*
|
||||
* tests/test_tree.c cannot catch this: it hides its value in tree[6], which is
|
||||
* the last node visited in all three depth-first orders, so a break that never
|
||||
* fires is indistinguishable from one that fires on the final node.
|
||||
*
|
||||
* Registered in AKSL_KNOWN_FAILING_TESTS (WILL_FAIL). When the break semantics
|
||||
* are fixed, CTest reports this as unexpectedly passing -- move it into
|
||||
* AKSL_TESTS then.
|
||||
*/
|
||||
|
||||
#include "aksl_capture.h"
|
||||
|
||||
#define MAX_LEAVES 7
|
||||
|
||||
typedef struct BreakParams
|
||||
{
|
||||
aksl_TreeNode *stop_at;
|
||||
int visits;
|
||||
} BreakParams;
|
||||
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *break_at_node(aksl_TreeNode *node, void *data)
|
||||
{
|
||||
BreakParams *parms = NULL;
|
||||
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, node, AKERR_NULLPOINTER, "node");
|
||||
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
|
||||
parms = (BreakParams *)data;
|
||||
parms->visits += 1;
|
||||
if ( node == parms->stop_at ) {
|
||||
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop");
|
||||
}
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
static int test_break_aborts_the_whole_traversal(void)
|
||||
{
|
||||
aksl_TreeNode tree[MAX_LEAVES];
|
||||
BreakParams parms;
|
||||
|
||||
memset((void *)tree, 0x00, sizeof(tree));
|
||||
memset((void *)&parms, 0x00, sizeof(parms));
|
||||
|
||||
/*
|
||||
* TREE[0]
|
||||
* +--------^^---------+
|
||||
* | |
|
||||
* TREE[1] TREE[2]
|
||||
* +---^^---+ +---^^---+
|
||||
* | | | |
|
||||
*TREE[3] TREE[4] TREE[5] TREE[6]
|
||||
*/
|
||||
tree[0].left = &tree[1];
|
||||
tree[0].right = &tree[2];
|
||||
tree[1].left = &tree[3];
|
||||
tree[1].right = &tree[4];
|
||||
tree[2].left = &tree[5];
|
||||
tree[2].right = &tree[6];
|
||||
|
||||
parms.stop_at = &tree[3];
|
||||
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &break_at_node, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_PREORDER, &parms, NULL));
|
||||
/* Pre-order visits 0, 1, 3 -- the break on tree[3] must stop the walk there. */
|
||||
AKSL_CHECK(parms.visits == 3);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int failures = 0;
|
||||
|
||||
akerr_init();
|
||||
AKSL_RUN(failures, test_break_aborts_the_whole_traversal);
|
||||
AKSL_REPORT(failures);
|
||||
}
|
||||
177
tests/test_version.c
Normal file
177
tests/test_version.c
Normal file
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Version reporting -- TODO.md section 2.2.16.
|
||||
*
|
||||
* There are two versions in play and the whole point of this API is that they
|
||||
* are allowed to differ:
|
||||
*
|
||||
* the AKSL_VERSION_* macros what the caller was COMPILED against, baked
|
||||
* into the caller's object file from whatever
|
||||
* akstdlib_version.h was on its include path
|
||||
* aksl_version() and friends what is actually LOADED, baked into
|
||||
* libakstdlib.so when the library was built
|
||||
*
|
||||
* In this test they necessarily agree, because the test binary and the library
|
||||
* are built from one tree in one configure. So the assertions below split into
|
||||
* two kinds: the ones that check the two sides agree (which would catch a build
|
||||
* that compiled the library and its tests against different generated headers),
|
||||
* and the ones that drive aksl_version_check() with deliberately wrong numbers
|
||||
* to prove it actually refuses a mismatch rather than always returning success.
|
||||
* The second kind is what makes the first kind worth anything.
|
||||
*/
|
||||
|
||||
#include "aksl_capture.h"
|
||||
|
||||
static int test_version_string_matches_its_components(void)
|
||||
{
|
||||
char expected[64];
|
||||
|
||||
snprintf(expected, sizeof(expected), "%d.%d.%d",
|
||||
AKSL_VERSION_MAJOR, AKSL_VERSION_MINOR, AKSL_VERSION_PATCH);
|
||||
AKSL_CHECK(strcmp(AKSL_VERSION_STRING, expected) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* AKSL_VERSION_NUMBER must be ordinary decimal arithmetic. Written out as a
|
||||
* literal it would be tempting to spell 0.1.0 as 000100, which C reads as
|
||||
* octal 64 -- so this compares the compile-time macro against the same figure
|
||||
* computed at runtime from the loaded library's own components. A literal that
|
||||
* went octal fails here; the current computed form cannot.
|
||||
*/
|
||||
static int test_version_number_is_decimal_and_ordered(void)
|
||||
{
|
||||
int major = -1;
|
||||
int minor = -1;
|
||||
int patch = -1;
|
||||
|
||||
AKSL_CHECK_OK(aksl_version(&major, &minor, &patch));
|
||||
AKSL_CHECK(AKSL_VERSION_NUMBER == (major * 10000) + (minor * 100) + patch);
|
||||
|
||||
/* Usable in the preprocessor, which is the reason it exists at all. */
|
||||
#if AKSL_VERSION_NUMBER < 0
|
||||
AKSL_CHECK(0 && "AKSL_VERSION_NUMBER is negative");
|
||||
#endif
|
||||
AKSL_CHECK(AKSL_VERSION_NUMBER >= 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Header and shared library came out of the same configure. */
|
||||
static int test_loaded_version_matches_the_header(void)
|
||||
{
|
||||
int major = -1;
|
||||
int minor = -1;
|
||||
int patch = -1;
|
||||
|
||||
AKSL_CHECK_OK(aksl_version(&major, &minor, &patch));
|
||||
AKSL_CHECK(major == AKSL_VERSION_MAJOR);
|
||||
AKSL_CHECK(minor == AKSL_VERSION_MINOR);
|
||||
AKSL_CHECK(patch == AKSL_VERSION_PATCH);
|
||||
|
||||
AKSL_CHECK(strcmp(aksl_version_string(), AKSL_VERSION_STRING) == 0);
|
||||
AKSL_CHECK(strcmp(aksl_version_soname(), AKSL_VERSION_SONAME) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* The soname is the ABI-break granularity, and it mirrors the SOVERSION
|
||||
* expression in CMakeLists.txt: MAJOR.MINOR while major is 0, MAJOR alone after
|
||||
* that. If one of the two ever changes without the other, this fails.
|
||||
*/
|
||||
static int test_soname_matches_the_documented_rule(void)
|
||||
{
|
||||
char expected[64];
|
||||
|
||||
#if AKSL_VERSION_MAJOR == 0
|
||||
snprintf(expected, sizeof(expected), "%d.%d",
|
||||
AKSL_VERSION_MAJOR, AKSL_VERSION_MINOR);
|
||||
#else
|
||||
snprintf(expected, sizeof(expected), "%d", AKSL_VERSION_MAJOR);
|
||||
#endif
|
||||
AKSL_CHECK(strcmp(aksl_version_soname(), expected) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_version_check_accepts_a_matching_build(void)
|
||||
{
|
||||
AKSL_CHECK_OK(AKSL_VERSION_CHECK());
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* The refusals. Without these the success above proves nothing -- a
|
||||
* aksl_version_check() that returned NULL unconditionally would pass it.
|
||||
*/
|
||||
static int test_version_check_refuses_a_mismatch(void)
|
||||
{
|
||||
AKSL_CHECK_STATUS(aksl_version_check(AKSL_VERSION_MAJOR + 1,
|
||||
AKSL_VERSION_MINOR,
|
||||
AKSL_VERSION_PATCH),
|
||||
AKERR_VALUE);
|
||||
AKSL_CHECK_STATUS(aksl_version_check(AKSL_VERSION_MAJOR,
|
||||
AKSL_VERSION_MINOR + 1,
|
||||
AKSL_VERSION_PATCH),
|
||||
AKERR_VALUE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Patch is deliberately not part of the comparison: a patch bump is an ABI
|
||||
* promise, so a caller built against 0.1.0 must keep working against 0.1.7.
|
||||
*/
|
||||
static int test_version_check_ignores_the_patch_level(void)
|
||||
{
|
||||
AKSL_CHECK_OK(aksl_version_check(AKSL_VERSION_MAJOR,
|
||||
AKSL_VERSION_MINOR,
|
||||
AKSL_VERSION_PATCH + 9));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The error has to say which two versions disagree, or it is not actionable. */
|
||||
static int test_mismatch_message_names_both_versions(void)
|
||||
{
|
||||
char compiled[64];
|
||||
|
||||
snprintf(compiled, sizeof(compiled), "%d.%d.%d",
|
||||
AKSL_VERSION_MAJOR + 1, AKSL_VERSION_MINOR, AKSL_VERSION_PATCH);
|
||||
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_version_check(AKSL_VERSION_MAJOR + 1,
|
||||
AKSL_VERSION_MINOR,
|
||||
AKSL_VERSION_PATCH),
|
||||
AKERR_VALUE, compiled);
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_version_check(AKSL_VERSION_MAJOR + 1,
|
||||
AKSL_VERSION_MINOR,
|
||||
AKSL_VERSION_PATCH),
|
||||
AKERR_VALUE, AKSL_VERSION_STRING);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_rejects_null_arguments(void)
|
||||
{
|
||||
int major = 0;
|
||||
int minor = 0;
|
||||
int patch = 0;
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_version(NULL, &minor, &patch), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_version(&major, NULL, &patch), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_version(&major, &minor, NULL), AKERR_NULLPOINTER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int failures = 0;
|
||||
|
||||
akerr_init();
|
||||
|
||||
AKSL_RUN(failures, test_version_string_matches_its_components);
|
||||
AKSL_RUN(failures, test_version_number_is_decimal_and_ordered);
|
||||
AKSL_RUN(failures, test_loaded_version_matches_the_header);
|
||||
AKSL_RUN(failures, test_soname_matches_the_documented_rule);
|
||||
AKSL_RUN(failures, test_version_check_accepts_a_matching_build);
|
||||
AKSL_RUN(failures, test_version_check_refuses_a_mismatch);
|
||||
AKSL_RUN(failures, test_version_check_ignores_the_patch_level);
|
||||
AKSL_RUN(failures, test_mismatch_message_names_both_versions);
|
||||
AKSL_RUN(failures, test_rejects_null_arguments);
|
||||
|
||||
AKSL_REPORT(failures);
|
||||
}
|
||||
Reference in New Issue
Block a user