Compare commits
28 Commits
trees
...
f8425b8729
| Author | SHA1 | Date | |
|---|---|---|---|
|
f8425b8729
|
|||
|
cc5e7899bb
|
|||
|
3ad3994762
|
|||
|
7940276f87
|
|||
|
b104a07eb4
|
|||
|
125eeb2109
|
|||
|
98a0a8562d
|
|||
|
55eb0334c4
|
|||
|
fd71bcc67b
|
|||
|
95e5002512
|
|||
|
07c448508b
|
|||
|
a37ba3fb89
|
|||
|
437da2960b
|
|||
|
82c47ed773
|
|||
|
a87cbfb26d
|
|||
|
566004afd6
|
|||
|
11c04923f8
|
|||
|
01734f511b
|
|||
| 8003239116 | |||
|
80205d5c4f
|
|||
|
54fb44cd2b
|
|||
|
68009ea0e3
|
|||
|
3e296c3bff
|
|||
|
5793f6a178
|
|||
|
57929be1af
|
|||
|
3e3ea41dc8
|
|||
|
e4597aef7d
|
|||
|
06b2c8ad8a
|
183
.gitea/workflows/ci.yaml
Normal file
183
.gitea/workflows/ci.yaml
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
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
|
||||||
|
# This step used to clone and install libakerror@main. That was two
|
||||||
|
# different libakerrors depending on where you built: the install went to
|
||||||
|
# /usr/local, while the build below is top-level and so compiles
|
||||||
|
# deps/libakerror at the pinned commit via add_subdirectory -- the
|
||||||
|
# installed one was never actually linked against. TODO.md 2.3.
|
||||||
|
#
|
||||||
|
# The submodule wins. It is the version this repository pins, tests and
|
||||||
|
# ships against, and a CI that tests a different one is testing something
|
||||||
|
# nobody runs. It is installed here as well as compiled, because an
|
||||||
|
# installed libakstdlib is not usable without it: akstdlibConfig.cmake
|
||||||
|
# calls find_dependency(akerror), and the top-level build pulls the
|
||||||
|
# submodule in EXCLUDE_FROM_ALL so `cmake --install` on this project
|
||||||
|
# installs only this project.
|
||||||
|
- name: install the pinned libakerror
|
||||||
|
run: |
|
||||||
|
cmake -S deps/libakerror -B build-akerror
|
||||||
|
cmake --build build-akerror
|
||||||
|
sudo cmake --install build-akerror
|
||||||
|
- name: build and test
|
||||||
|
run: |
|
||||||
|
# -DAKSL_WERROR=ON here and not locally: a warning that stops the build
|
||||||
|
# mid-thought teaches people to turn warnings off, but a warning that
|
||||||
|
# reaches main is one nobody will ever look at again.
|
||||||
|
cmake -S . -B build -DAKSL_WERROR=ON
|
||||||
|
cmake --build build
|
||||||
|
sudo cmake --install build
|
||||||
|
ctest --test-dir build --output-on-failure
|
||||||
|
# Build something against what was just installed. The suite links the
|
||||||
|
# build tree, so it says nothing about whether the *installed* package is
|
||||||
|
# usable -- and the two have come apart before: find_dependency(akerror)
|
||||||
|
# resolving is a property of the install, not of the build.
|
||||||
|
- name: consume the installed package
|
||||||
|
run: |
|
||||||
|
sudo ldconfig
|
||||||
|
cmake -S tests/consumer -B build-consumer
|
||||||
|
cmake --build build-consumer
|
||||||
|
./build-consumer/consumer
|
||||||
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||||
|
|
||||||
|
# The sanitizer build is its own job rather than a step on the one above,
|
||||||
|
# because three of the defects fixed in 0.2.0 only ever misbehaved under
|
||||||
|
# instrumentation and the tests that pin them are worth failing on their own.
|
||||||
|
sanitizers:
|
||||||
|
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
|
||||||
|
- name: build and test under ASan + UBSan
|
||||||
|
run: |
|
||||||
|
cmake -S . -B build-asan -DAKSL_SANITIZE=ON -DAKSL_WERROR=ON
|
||||||
|
cmake --build build-asan
|
||||||
|
ctest --test-dir build-asan --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. Across all four sources the suite
|
||||||
|
# covers 99.5% of lines (1708/1716), 46.0% of branches and 100% of
|
||||||
|
# functions (154/154), 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.
|
||||||
|
#
|
||||||
|
# Eight lines are uncovered and every one is deliberate: four
|
||||||
|
# `HANDLE(e, AKERR_ITERATOR_BREAK)` macro artifacts, the size_t overflow
|
||||||
|
# guard in strbuf_reserve (reaching it needs a buffer near SIZE_MAX), and
|
||||||
|
# the short transfer with neither feof nor ferror set, which the standard
|
||||||
|
# permits and Linux never produces. README.md has the detail.
|
||||||
|
#
|
||||||
|
# Branch coverage sits far below line coverage because most branches are
|
||||||
|
# inside libakerror's FAIL_*/ATTEMPT/FINISH expansions -- pool exhaustion,
|
||||||
|
# stack-trace limits, akerr_valid_error_address failures -- which this
|
||||||
|
# library has no way to reach. 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. The gate stays at 40 for that reason and not for want of
|
||||||
|
# tests.
|
||||||
|
- 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.
|
||||||
|
#
|
||||||
|
# A sample rather than the whole set. The four sources generate 1701
|
||||||
|
# mutants and each one is a full rebuild and re-run of the suite, which is
|
||||||
|
# hours. --max-mutants samples by even index, not at random, so the same
|
||||||
|
# 260 run every time and the gate is reproducible -- and sampling all four
|
||||||
|
# files beats exhausting one of them, which is what this job used to do.
|
||||||
|
#
|
||||||
|
# The threshold is a ratchet, not a quality bar. The measured score is
|
||||||
|
# 72.3% (188/260). 65 leaves headroom for the runner and for the sample
|
||||||
|
# shifting as sources change, while still failing on a real regression --
|
||||||
|
# a test deleted, or new untested code added.
|
||||||
|
#
|
||||||
|
# It is well below the 89.6% this job reported at 0.1.0, and that is a
|
||||||
|
# change in denominator rather than in the tests: that figure covered one
|
||||||
|
# 561-line file, this one covers four totalling 1716 lines, most of the
|
||||||
|
# new surface being argument validation whose mutants are frequently
|
||||||
|
# equivalent. `errno = 0` deleted from a wrapper whose libc call always
|
||||||
|
# sets errno cannot be distinguished by any test that could be written.
|
||||||
|
# The survivors worth acting on are named in TODO.md; raise the gate as
|
||||||
|
# they become assertions.
|
||||||
|
- name: mutation testing
|
||||||
|
run: |
|
||||||
|
python3 scripts/mutation_test.py \
|
||||||
|
--target src/stdlib.c \
|
||||||
|
--target src/string.c \
|
||||||
|
--target src/stream.c \
|
||||||
|
--target src/collections.c \
|
||||||
|
--max-mutants 260 \
|
||||||
|
--junit mutation-junit.xml \
|
||||||
|
--threshold 65
|
||||||
|
# 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 }}."
|
||||||
123
.githooks/pre-push
Executable file
123
.githooks/pre-push
Executable file
@@ -0,0 +1,123 @@
|
|||||||
|
#!/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 (with -Werror, as CI has it)
|
||||||
|
# * ASan/UBSan build + ctest
|
||||||
|
# * doxygen -- fails on an undocumented public function
|
||||||
|
#
|
||||||
|
# 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:-65}"
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
# -DAKSL_WERROR=ON here and not in a plain local build: a warning that stops you
|
||||||
|
# mid-thought teaches people to turn warnings off, but a warning that reaches
|
||||||
|
# main is one nobody will look at again. This is the last point where it is
|
||||||
|
# still cheap to fix.
|
||||||
|
echo "pre-push: default build + ctest"
|
||||||
|
run cmake -S . -B "$builddir/default" -DAKSL_WERROR=ON
|
||||||
|
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 -DAKSL_WERROR=ON
|
||||||
|
run cmake --build "$builddir/asan"
|
||||||
|
run ctest --test-dir "$builddir/asan" --output-on-failure
|
||||||
|
|
||||||
|
# Doxygen runs with WARN_IF_UNDOCUMENTED and WARN_NO_PARAMDOC on and EXTRACT_ALL
|
||||||
|
# off, so the target fails when a public function, parameter or return value has
|
||||||
|
# nobody describing it. Skipped where doxygen is not installed rather than
|
||||||
|
# failing: it is a documentation check, not a build dependency.
|
||||||
|
if command -v doxygen > /dev/null 2>&1; then
|
||||||
|
echo "pre-push: doxygen"
|
||||||
|
run cmake --build "$builddir/default" --target docs
|
||||||
|
else
|
||||||
|
echo "pre-push: doxygen not installed, skipping the documentation check"
|
||||||
|
fi
|
||||||
|
|
||||||
|
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.
|
||||||
|
# Sampled, like CI: the full set is 1701 mutants and each is a rebuild plus
|
||||||
|
# a full test run. --max-mutants samples by even index rather than at
|
||||||
|
# random, so this is the same 260 CI gates on.
|
||||||
|
if ! python3 scripts/mutation_test.py \
|
||||||
|
--target src/stdlib.c \
|
||||||
|
--target src/string.c \
|
||||||
|
--target src/stream.c \
|
||||||
|
--target src/collections.c \
|
||||||
|
--max-mutants "${AKSL_HOOK_MUTANTS:-260}" \
|
||||||
|
--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
|
||||||
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Build trees. The names are conventional rather than enforced -- AGENTS.md uses
|
||||||
|
# build/, build-asan/ and build-coverage/ -- so anything build* is ignored.
|
||||||
|
build*/
|
||||||
|
|
||||||
|
# Profiling output. -pg is no longer in the default build (see CMakeLists.txt),
|
||||||
|
# but a deliberate profiling run still drops these wherever it was started.
|
||||||
|
gmon.out
|
||||||
|
*.gcda
|
||||||
|
*.gcno
|
||||||
|
*.gcov
|
||||||
|
|
||||||
|
# Test and report artifacts written into the source root by CI-style invocations.
|
||||||
|
ctest-junit.xml
|
||||||
|
mutation-junit.xml
|
||||||
|
coverage-summary.txt
|
||||||
|
coverage.xml
|
||||||
|
|
||||||
|
# Emacs. The backup files and the lock symlinks both; a dangling .#file symlink
|
||||||
|
# pointing at user@host:pid is not something anyone wants in a diff.
|
||||||
|
*~
|
||||||
|
\#*\#
|
||||||
|
.\#*
|
||||||
|
|
||||||
|
# Editor and OS noise
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
compile_commands.json
|
||||||
|
|
||||||
|
# Generated API documentation and its warning log. `cmake --build build --target
|
||||||
|
# docs` writes both; the log is the actionable part and is empty when clean.
|
||||||
|
doxygen/
|
||||||
|
doxygen-warnings.log
|
||||||
|
Doxyfile.generated
|
||||||
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
|
||||||
139
AGENTS.md
Normal file
139
AGENTS.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
## Project Structure & Module Organization
|
||||||
|
|
||||||
|
`libakstdlib` is a C shared library that wraps libc calls and data structures in
|
||||||
|
`libakerror` error contexts.
|
||||||
|
|
||||||
|
Public API declarations live in `include/akstdlib.h`, which is also where the
|
||||||
|
Doxygen documentation lives -- the header is the contract, the sources carry the
|
||||||
|
reasoning. The implementation is split by domain:
|
||||||
|
|
||||||
|
| File | Covers |
|
||||||
|
|---|---|
|
||||||
|
| `src/stdlib.c` | memory, formatted output, string-to-number, realpath, djb2, and the list/tree traversal entry points |
|
||||||
|
| `src/string.c` | the `string.h` surface |
|
||||||
|
| `src/stream.c` | `stdio.h` beyond open/read/write/close |
|
||||||
|
| `src/collections.c` | list and tree operations, hash map, string buffer, FNV-1a |
|
||||||
|
| `src/aksl_internal.h` | shared internals; not installed, not public |
|
||||||
|
|
||||||
|
Tests are one-file CTest executables under `tests/test_<name>.c`, with shared
|
||||||
|
helpers in `tests/aksl_capture.h`. `tests/negative/` holds sources that must
|
||||||
|
*fail* to compile, and `tests/consumer/` is a standalone project built against
|
||||||
|
the *installed* package rather than the build tree -- see the testing section
|
||||||
|
below. 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, and `cmake --build build --target docs` to regenerate the API
|
||||||
|
documentation -- that one fails if any public function, parameter or return value
|
||||||
|
is undocumented, so it is a check as well as a generator.
|
||||||
|
|
||||||
|
`.githooks/pre-push` runs the default build, the sanitizer build and the
|
||||||
|
documentation check before a push; enable it with
|
||||||
|
`git config core.hooksPath .githooks`. `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.
|
||||||
|
|
||||||
|
Four conventions hold across the whole library, and a new wrapper that breaks one
|
||||||
|
of them is wrong even if it compiles and passes:
|
||||||
|
|
||||||
|
- **A NULL out-param is a caller error**, not "don't care".
|
||||||
|
- **Finding nothing is success** -- searching functions write NULL or zero and
|
||||||
|
return NULL.
|
||||||
|
- **Truncation is a failure.** Take the destination's size, raise
|
||||||
|
AKERR_OUTOFBOUNDS, and write nothing rather than a prefix.
|
||||||
|
- **Clear `errno` before the wrapped call** and read it back through
|
||||||
|
`AKSL_ERRNO_OR`, so no error can carry status 0 -- which every downstream
|
||||||
|
`DETECT` reads as success.
|
||||||
|
|
||||||
|
Every new public function needs a Doxygen block on its **declaration** with
|
||||||
|
`@brief`, a `@param` per parameter, a `@throws` per status it can raise, and
|
||||||
|
`@return`. The `docs` target fails otherwise.
|
||||||
|
|
||||||
|
The build is `-Wall -Wextra` and CI adds `-Werror`. `-Wpedantic` is deliberately
|
||||||
|
off: libakerror's `FAIL_*` macros trip "ISO C99 requires at least one argument
|
||||||
|
for the ..." on their own expansion, not on anything at the call site.
|
||||||
|
|
||||||
|
## 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. Both of the
|
||||||
|
latter are currently empty -- all six confirmed defects are fixed -- but the
|
||||||
|
mechanism stays for the next one.
|
||||||
|
|
||||||
|
`tests/negative/` holds sources that must **fail** to compile. Each is an
|
||||||
|
`EXCLUDE_FROM_ALL` target built with `-Werror` and registered as a `WILL_FAIL`
|
||||||
|
CTest entry, so the test passes only when the compile fails. They cover the two
|
||||||
|
guarantees the compiler enforces and nothing else does: `AKERR_NOIGNORE` and the
|
||||||
|
format attributes. Drop either in a refactor and every ordinary test still
|
||||||
|
passes.
|
||||||
|
|
||||||
|
`tests/consumer/` is configured standalone against `CMAKE_PREFIX_PATH`, not as
|
||||||
|
part of this build, because being part of this build is what would let it pass
|
||||||
|
without testing anything. It covers the paths only an install has: the version
|
||||||
|
file, `find_dependency(akerror)` resolving, and the exported
|
||||||
|
`akstdlib::akstdlib` target. CI runs it after `cmake --install`.
|
||||||
|
|
||||||
|
Coverage is 99.5% of lines and 100% of functions across all four sources; CI
|
||||||
|
gates 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.
|
||||||
493
CMakeLists.txt
493
CMakeLists.txt
@@ -1,5 +1,138 @@
|
|||||||
cmake_minimum_required(VERSION 3.10)
|
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.2.0, and the minor bump is an ABI break on purpose. Fixing the confirmed
|
||||||
|
# defects in TODO.md 2.1 changed documented behaviour and, in five places,
|
||||||
|
# signatures:
|
||||||
|
#
|
||||||
|
# aksl_realpath takes the destination's length; aksl_realpath_alloc is new
|
||||||
|
# aksl_list_pop takes the head by reference
|
||||||
|
# aksl_tree_iterate lost its `queue` parameter
|
||||||
|
# aksl_fread/fwrite take a required transferred-count out-param
|
||||||
|
# aksl_sprintf is gone; aksl_snprintf replaces it
|
||||||
|
#
|
||||||
|
# plus the ato* family, which now reports a bad conversion rather than returning
|
||||||
|
# 0. Pre-1.0 the soname is MAJOR.MINOR, so 0.1 and 0.2 are different ABIs and a
|
||||||
|
# consumer built against 0.1 will not silently load this. See UPGRADING.md.
|
||||||
|
project(akstdlib VERSION 0.2.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
|
||||||
|
)
|
||||||
|
|
||||||
|
# This used to be three lines setting CMAKE_CXX_FLAGS and the linker flags to
|
||||||
|
# "-g -ggdb -pg". In a LANGUAGES C project the CXX variable reaches no compiler
|
||||||
|
# at all, so the debug flags never applied -- but -pg did reach the linkers, and
|
||||||
|
# every test binary that ran dropped a gmon.out in the working directory. Nobody
|
||||||
|
# was reading those profiles. CMAKE_BUILD_TYPE controls debug info now, which is
|
||||||
|
# what it is for; add -pg deliberately when you actually want to profile.
|
||||||
|
#
|
||||||
|
# Debug is the default for a bare `cmake -S . -B build` because this is a
|
||||||
|
# library under active development and a stripped -O3 build is not what anyone
|
||||||
|
# configuring it without an opinion is asking for.
|
||||||
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type" FORCE)
|
||||||
|
message(STATUS "CMAKE_BUILD_TYPE was unset; defaulting to Debug")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Warnings. The only warning in the tree when these went on was the unused
|
||||||
|
# `queue` parameter of aksl_tree_iterate, which TODO.md 2.2.8 had already
|
||||||
|
# recorded as a dead parameter -- so the cost of turning them on was one
|
||||||
|
# already-known defect, and the cost of leaving them off was every future one.
|
||||||
|
#
|
||||||
|
# -Wpedantic is deliberately not here. libakerror's FAIL_* macros take a message
|
||||||
|
# plus varargs, and a call with a bare message and no arguments trips "ISO C99
|
||||||
|
# requires at least one argument for the ...", which is a complaint about the
|
||||||
|
# macro's shape rather than about anything at this call site. Every FAIL_* in
|
||||||
|
# src/stdlib.c passes at least one argument regardless -- see the note in
|
||||||
|
# TODO.md 2.3 -- so the code is pedantic-clean; it is the expansion that is not.
|
||||||
|
if(CMAKE_C_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$")
|
||||||
|
add_compile_options(-Wall -Wextra)
|
||||||
|
# CI turns this on. Locally it is off, because a warning that stops the build
|
||||||
|
# while you are in the middle of something is a good way to teach people to
|
||||||
|
# turn warnings off.
|
||||||
|
option(AKSL_WERROR "Treat compiler warnings as errors" OFF)
|
||||||
|
if(AKSL_WERROR)
|
||||||
|
add_compile_options(-Werror)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# 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)
|
if(TARGET akerror::akerror)
|
||||||
message(STATUS "FOUND akerror::akerror")
|
message(STATUS "FOUND akerror::akerror")
|
||||||
@@ -11,9 +144,52 @@ include(CTest)
|
|||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
include(CMakePackageConfigHelpers)
|
include(CMakePackageConfigHelpers)
|
||||||
|
|
||||||
if(NOT TARGET akerror::akerror)
|
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||||
find_package(PkgConfig REQUIRED)
|
# libakerror registers its own CTest entries unconditionally, and because we
|
||||||
find_package(akerror REQUIRED)
|
# 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()
|
endif()
|
||||||
|
|
||||||
set(akstdlib_install_cmakedir "${CMAKE_INSTALL_LIBDIR}/cmake/akstdlib")
|
set(akstdlib_install_cmakedir "${CMAKE_INSTALL_LIBDIR}/cmake/akstdlib")
|
||||||
@@ -23,20 +199,42 @@ set(libdir "\${exec_prefix}/lib")
|
|||||||
set(includedir "\${prefix}/include")
|
set(includedir "\${prefix}/include")
|
||||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/akstdlib.pc.in ${CMAKE_CURRENT_BINARY_DIR}/akstdlib.pc @ONLY)
|
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/akstdlib.pc.in ${CMAKE_CURRENT_BINARY_DIR}/akstdlib.pc @ONLY)
|
||||||
|
|
||||||
|
# One translation unit per surface. src/stdlib.c was the whole library when it
|
||||||
|
# covered 16 libc calls; it is split by domain now that it does not, so that a
|
||||||
|
# file can still be read start to finish. src/aksl_internal.h carries what they
|
||||||
|
# share and is not installed.
|
||||||
add_library(akstdlib SHARED
|
add_library(akstdlib SHARED
|
||||||
src/stdlib.c
|
src/stdlib.c
|
||||||
|
src/string.c
|
||||||
|
src/stream.c
|
||||||
|
src/collections.c
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(akstdlib::akstdlib ALIAS akstdlib)
|
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
|
target_include_directories(akstdlib PUBLIC
|
||||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||||
|
$<BUILD_INTERFACE:${AKSL_GENERATED_INCLUDE_DIR}>
|
||||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/>
|
$<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)
|
target_link_libraries(akstdlib PUBLIC akerror::akerror)
|
||||||
|
|
||||||
|
aksl_target_coverage(akstdlib)
|
||||||
|
|
||||||
set(main_lib_dest "lib/my_library-${MY_LIBRARY_VERSION}")
|
set(main_lib_dest "lib/my_library-${MY_LIBRARY_VERSION}")
|
||||||
install(TARGETS akstdlib
|
install(TARGETS akstdlib
|
||||||
EXPORT akstdlibTargets
|
EXPORT akstdlibTargets
|
||||||
@@ -47,6 +245,7 @@ install(TARGETS akstdlib
|
|||||||
)
|
)
|
||||||
|
|
||||||
install(FILES "include/akstdlib.h" DESTINATION "include/")
|
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/")
|
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akstdlib.pc DESTINATION "lib/pkgconfig/")
|
||||||
|
|
||||||
|
|
||||||
@@ -62,9 +261,293 @@ configure_package_config_file(
|
|||||||
INSTALL_DESTINATION ${akstdlib_install_cmakedir}
|
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
|
install(FILES
|
||||||
"${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfig.cmake"
|
"${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfig.cmake"
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfigVersion.cmake"
|
||||||
DESTINATION ${akstdlib_install_cmakedir}
|
DESTINATION ${akstdlib_install_cmakedir}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Each test is one source file tests/test_<name>.c, built into the executable
|
||||||
|
# test_<name> and registered as the CTest test <name>. Shared helpers live in
|
||||||
|
# tests/aksl_capture.h.
|
||||||
|
#
|
||||||
|
# AKSL_TESTS must exit 0.
|
||||||
|
# AKSL_WILL_FAIL_TESTS expected to abort by design (an unhandled error
|
||||||
|
# reaching FINISH_NORETURN, a deliberate contract
|
||||||
|
# violation), so a non-zero exit is a pass.
|
||||||
|
# AKSL_KNOWN_FAILING_TESTS assert the *correct* behaviour of a confirmed
|
||||||
|
# defect from TODO.md section 2.1. They fail until
|
||||||
|
# the defect is fixed, and are marked WILL_FAIL so
|
||||||
|
# the suite stays green and the gap stays visible.
|
||||||
|
# When one is fixed CTest reports it as failed with
|
||||||
|
# "unexpectedly passed" -- that is the cue to move
|
||||||
|
# it up into AKSL_TESTS.
|
||||||
|
set(AKSL_TESTS
|
||||||
|
collections
|
||||||
|
convert
|
||||||
|
format
|
||||||
|
hashmap
|
||||||
|
linkedlist
|
||||||
|
memory
|
||||||
|
path
|
||||||
|
pool
|
||||||
|
status_registry
|
||||||
|
strbuf
|
||||||
|
stream
|
||||||
|
streamio
|
||||||
|
strhash
|
||||||
|
string
|
||||||
|
strto
|
||||||
|
tree
|
||||||
|
version
|
||||||
|
)
|
||||||
|
|
||||||
|
set(AKSL_WILL_FAIL_TESTS
|
||||||
|
)
|
||||||
|
|
||||||
|
# Empty, and that is the news. It held four entries -- convert_strict,
|
||||||
|
# list_append_chain, list_iterate_head and tree_iterate_break -- one for each
|
||||||
|
# confirmed defect in TODO.md 2.1. All four are fixed, so each of those files
|
||||||
|
# was folded back into the test for the thing it was testing (tests/
|
||||||
|
# test_convert.c, tests/test_linkedlist.c and tests/test_tree.c) where it now
|
||||||
|
# has to keep passing rather than merely keep failing visibly.
|
||||||
|
set(AKSL_KNOWN_FAILING_TESTS
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
# Negative compile tests -- TODO.md 1.3 and 1.9.
|
||||||
|
#
|
||||||
|
# Two properties of this library are enforced by the compiler and by nothing
|
||||||
|
# else: AKERR_NOIGNORE makes discarding a returned error context an error, and
|
||||||
|
# AKSL_PRINTF_FORMAT restores the format/argument checking a caller loses by
|
||||||
|
# going through a variadic wrapper. Both are attributes on declarations in
|
||||||
|
# include/akstdlib.h. If either is dropped in a refactor, every test still
|
||||||
|
# passes, the library still builds, and nothing looks wrong -- the guarantee just
|
||||||
|
# quietly stops existing.
|
||||||
|
#
|
||||||
|
# So each is asserted by a source file that must *fail* to compile. The target
|
||||||
|
# is EXCLUDE_FROM_ALL and built with -Werror; the CTest entry runs that build and
|
||||||
|
# is marked WILL_FAIL, so a successful compile fails the suite.
|
||||||
|
#
|
||||||
|
# Only where the compiler supports the attributes at all -- elsewhere
|
||||||
|
# AKSL_PRINTF_FORMAT expands to nothing and the file would compile correctly.
|
||||||
|
if(CMAKE_C_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$")
|
||||||
|
foreach(_neg noignore format_mismatch)
|
||||||
|
add_executable(negative_${_neg} EXCLUDE_FROM_ALL tests/negative/${_neg}.c)
|
||||||
|
target_link_libraries(negative_${_neg} PRIVATE akstdlib)
|
||||||
|
target_compile_options(negative_${_neg} PRIVATE -Werror)
|
||||||
|
add_test(NAME negative_${_neg}
|
||||||
|
COMMAND ${CMAKE_COMMAND} --build ${CMAKE_CURRENT_BINARY_DIR}
|
||||||
|
--target negative_${_neg})
|
||||||
|
set_tests_properties(negative_${_neg} PROPERTIES
|
||||||
|
WILL_FAIL TRUE
|
||||||
|
TIMEOUT 120
|
||||||
|
# These invoke the build system, so they must not run while another build
|
||||||
|
# of the same tree is in flight.
|
||||||
|
RUN_SERIAL TRUE)
|
||||||
|
endforeach()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# tests/test_memory.c asks for SIZE_MAX/2 bytes to check that a refused
|
||||||
|
# allocation reports ENOMEM and leaves *dst NULL. Plain malloc returns NULL and
|
||||||
|
# sets ENOMEM, which is the case under test; ASan's allocator instead treats a
|
||||||
|
# request that large as a bug in the caller and aborts the process before malloc
|
||||||
|
# ever returns. allocator_may_return_null=1 restores the libc behaviour for this
|
||||||
|
# one binary, so the sanitizer build tests the same contract as the normal one
|
||||||
|
# rather than skipping it.
|
||||||
|
if(AKSL_SANITIZE AND "memory" IN_LIST AKSL_TESTS)
|
||||||
|
set_tests_properties(memory PROPERTIES
|
||||||
|
ENVIRONMENT "ASAN_OPTIONS=allocator_may_return_null=1")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
# API documentation:
|
||||||
|
# cmake --build build --target docs
|
||||||
|
#
|
||||||
|
# The Doxyfile carries no PROJECT_NUMBER. The version lives in exactly one place
|
||||||
|
# -- the project() call at the top of this file -- and a version written into the
|
||||||
|
# Doxyfile as well would be a second place to forget. Doxygen reads its
|
||||||
|
# configuration from stdin when given "-", so the number is appended on the way
|
||||||
|
# past and the checked-in file stays version-free.
|
||||||
|
#
|
||||||
|
# WARN_IF_UNDOCUMENTED and WARN_NO_PARAMDOC are both on, and EXTRACT_ALL is off:
|
||||||
|
# an undocumented function is a warning rather than a silently empty page. The
|
||||||
|
# target fails if the log is non-empty, which is what makes "documented" a
|
||||||
|
# property the build checks rather than an impression.
|
||||||
|
find_program(AKSL_DOXYGEN doxygen)
|
||||||
|
if(AKSL_DOXYGEN)
|
||||||
|
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||||
|
set(AKSL_DOCS_TARGET docs)
|
||||||
|
else()
|
||||||
|
set(AKSL_DOCS_TARGET akstdlib_docs)
|
||||||
|
endif()
|
||||||
|
add_custom_target(${AKSL_DOCS_TARGET}
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E echo
|
||||||
|
"Generating API documentation for akstdlib ${PROJECT_VERSION}"
|
||||||
|
COMMAND ${CMAKE_COMMAND}
|
||||||
|
-DAKSL_DOXYGEN=${AKSL_DOXYGEN}
|
||||||
|
-DAKSL_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
-DAKSL_VERSION=${PROJECT_VERSION}
|
||||||
|
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunDoxygen.cmake
|
||||||
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
USES_TERMINAL
|
||||||
|
COMMENT "Running doxygen"
|
||||||
|
)
|
||||||
|
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
|
# pkgconfig
|
||||||
|
|||||||
490
README.md
Normal file
490
README.md
Normal file
@@ -0,0 +1,490 @@
|
|||||||
|
# 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 data structures built on the same convention.
|
||||||
|
|
||||||
|
Every entry point returns `akerr_ErrorContext *` and is marked `AKERR_NOIGNORE`.
|
||||||
|
See `TODO.md` for the current state of the library and `UPGRADING.md` if you are
|
||||||
|
coming from 0.1.0, which this release breaks.
|
||||||
|
|
||||||
|
## What it wraps
|
||||||
|
|
||||||
|
| Area | Source | Functions |
|
||||||
|
|---|---|---|
|
||||||
|
| Memory | `src/stdlib.c` | `malloc` `calloc` `realloc` `free` `freep` `memset` `memcpy` `memmove` `memcmp` `memchr` |
|
||||||
|
| Formatted output | `src/stdlib.c` | `printf` `fprintf` `snprintf` and their `v*` forms |
|
||||||
|
| String → number | `src/stdlib.c` | `strtol` `strtoll` `strtoul` `strtoull` `strtod` `strtof` `strtold`, and `atoi` `atol` `atoll` `atof` on top of them |
|
||||||
|
| Paths and hashing | `src/stdlib.c` | `realpath` `realpath_alloc` `strhash_djb2` `strhash_djb2_str` |
|
||||||
|
| Strings | `src/string.c` | `strlen` `strnlen` `strcpy` `strncpy` `strcat` `strncat` `strdup` `strndup` `strcmp` `strncmp` `strcasecmp` `strncasecmp` `strcoll` `strchr` `strrchr` `strstr` `strcasestr` `strpbrk` `strspn` `strcspn` `strtok_r` `strsep` `strerror` |
|
||||||
|
| Streams | `src/stream.c` | `fopen` `fread` `fwrite` `fclose` `fseek` `ftell` `rewind` `fseeko` `ftello` `fgetpos` `fsetpos` `fflush` `setvbuf` `fgetc` `fputc` `ungetc` `fgets` `fputs` `getline` `getdelim` `feof` `ferror` `clearerr` `fileno` `freopen` `fdopen` `tmpfile` `sscanf` `fscanf` `remove` `rename` `mkstemp` `mkdtemp` |
|
||||||
|
| Collections | `src/collections.c` | doubly-linked list (bare-node and tracked-container forms), binary search tree, breadth- and depth-first traversal, fixed-capacity hash map, growable string buffer, FNV-1a |
|
||||||
|
|
||||||
|
### Where it deviates from libc, and why
|
||||||
|
|
||||||
|
The whole point is to make silent failures loud, so several wrappers are
|
||||||
|
deliberately stricter than the function they are named for. These are the ones
|
||||||
|
that will surprise you:
|
||||||
|
|
||||||
|
| Wrapper | Deviation |
|
||||||
|
|---|---|
|
||||||
|
| `aksl_free(NULL)` | `AKERR_NULLPOINTER`. `free(NULL)` is legal and does nothing; in a codebase that routes every allocation through `aksl_malloc`, a pointer you believed was live turning out NULL means something upstream did not happen. |
|
||||||
|
| `aksl_malloc(0, &p)` | `AKERR_VALUE`. There is nothing useful to hand back, and `malloc(0)` returning NULL without setting `errno` is how an error with status `0` used to get raised. |
|
||||||
|
| `aksl_atoi` and friends | Report bad conversions. `atoi(3)` has no error channel at all: junk converts to `0` and overflow wraps. Base 10, whole string, `ERANGE` on overflow. |
|
||||||
|
| `aksl_strcpy` / `strncpy` / `strcat` / `strncat` | Take the destination's size, which the libc originals cannot be called safely without. Truncation is `AKERR_OUTOFBOUNDS` and writes nothing. `aksl_strncpy` always terminates and never NUL-pads. |
|
||||||
|
| `aksl_snprintf` | Truncation is `AKERR_OUTOFBOUNDS`, not a short success. There is no `aksl_sprintf`: an error-handling wrapper around an unbounded write is the sharp edge this library exists to remove. |
|
||||||
|
| `aksl_memcpy` | Overlapping ranges are `AKERR_VALUE` rather than undefined behaviour. Use `aksl_memmove`. |
|
||||||
|
| `aksl_fread` / `aksl_fwrite` | Require a transferred-count out-param, and report a short transfer with no stream error as `AKERR_IO` rather than as success. |
|
||||||
|
| `aksl_sscanf` / `aksl_fscanf` | Take the number of conversions you expect. Comparing `scanf(3)`'s return against that by hand at every call site is the check everyone eventually forgets. |
|
||||||
|
| `aksl_realpath` | Takes the destination's length and refuses anything below `PATH_MAX`, because `realpath(3)` cannot be bounded. |
|
||||||
|
| `aksl_list_pop` | Takes the head by reference, because popping the head has to move it. |
|
||||||
|
| Searching (`strchr`, `strstr`, `memchr`, `aksl_list_find`, `aksl_hashmap_get`, …) | Finding nothing is **success** with a NULL or zero result, not an error. Absent is an ordinary answer. |
|
||||||
|
| `aksl_strtok` | Does not exist. `strtok(3)` keeps its state in a hidden static; use `aksl_strtok_r` or `aksl_strsep`. |
|
||||||
|
|
||||||
|
### Thread safety
|
||||||
|
|
||||||
|
**This library is not thread-safe, and cannot be made so from here.**
|
||||||
|
|
||||||
|
libakerror hands out error contexts from `AKERR_ARRAY_ERROR`, a process-global
|
||||||
|
array with no locking, and every entry point in this library takes a slot from it
|
||||||
|
on any failure path. Two threads raising errors concurrently can be handed the
|
||||||
|
same slot. `errno` is thread-local so the wrapped calls themselves are fine; the
|
||||||
|
error reporting is not.
|
||||||
|
|
||||||
|
There is no TSan test here because there is nothing to verify — the answer is
|
||||||
|
known and it is "no". Fixing it means locking or thread-local storage in
|
||||||
|
libakerror's pool, which is that library's decision to make; `TODO.md` §1.9
|
||||||
|
records it. Until then: confine libakstdlib calls to one thread, or serialise
|
||||||
|
them yourself.
|
||||||
|
|
||||||
|
The wrappers add no state of their own beyond that. `aksl_strtok_r` and
|
||||||
|
`aksl_strsep` keep their state in the caller's `saveptr`, and no function here
|
||||||
|
uses a static buffer.
|
||||||
|
|
||||||
|
## 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.2.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.2.0 | From |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `AKSL_VERSION_MAJOR` / `_MINOR` / `_PATCH` | `0` / `2` / `0` | `include/akstdlib_version.h.in` |
|
||||||
|
| `AKSL_VERSION_STRING` | `"0.2.0"` | same |
|
||||||
|
| `AKSL_VERSION_NUMBER` | `200` | same |
|
||||||
|
| `AKSL_VERSION_SONAME` | `"0.2"` | same |
|
||||||
|
| shared library | `libakstdlib.so.0.2.0`, soname `libakstdlib.so.0.2` | `VERSION` / `SOVERSION` |
|
||||||
|
| `pkg-config --modversion akstdlib` | `0.2.0` | `akstdlib.pc` |
|
||||||
|
| `find_package(akstdlib 0.2)` | accepted; `0.1` 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. The 0.1 → 0.2 bump was itself an ABI break — fixing the
|
||||||
|
confirmed defects changed five signatures and the `ato*` contract, all of it
|
||||||
|
listed in `UPGRADING.md` — and the API is not being promised until the wishlist
|
||||||
|
in `TODO.md` §3 has settled. 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.2.0 keeps working
|
||||||
|
against 0.2.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.3.0 build in under the 0.2 filename, or a package that strips
|
||||||
|
versioning. Then the loader is happy and only the check notices:
|
||||||
|
|
||||||
|
```
|
||||||
|
compiled against : 0.2.0 (soname 0.2)
|
||||||
|
loaded : 0.3.0 (0.3.0)
|
||||||
|
MISMATCH DETECTED: compiled against libakstdlib 0.2.0, loaded 0.3.0 (soname 0.3)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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: `memory`, `format` (the `printf` family), `convert`
|
||||||
|
(the `ato*` family) and `strto` (the family underneath it), `stream`
|
||||||
|
(`fopen`/`fread`/`fwrite`/`fclose`) and `streamio` (everything else in
|
||||||
|
`src/stream.c`), `string`, `path` (`aksl_realpath`), `strhash`, `linkedlist`,
|
||||||
|
`tree`, `collections` (the list and tree additions), `hashmap`, `strbuf`,
|
||||||
|
`version`, `status_registry` (this library's side of the libakerror
|
||||||
|
status-registry contract — see "The libakerror version floor" above), and `pool`.
|
||||||
|
|
||||||
|
`pool` is the odd one out: it asserts two cross-cutting properties rather than
|
||||||
|
any function's behaviour. Every failure path is driven `AKERR_MAX_ARRAY_ERROR + 10`
|
||||||
|
times with the pool checked after each round, because a wrapper that raises an
|
||||||
|
error and forgets to release it does not fail visibly — it fails a hundred-odd
|
||||||
|
calls later in whatever unrelated code asks for a slot next. And every error is
|
||||||
|
checked to name the function and file it was actually raised from, which is what
|
||||||
|
catches a `FAIL` that migrates into a shared helper during a refactor: the status
|
||||||
|
stays right, the message stays right, and the origin quietly starts lying.
|
||||||
|
|
||||||
|
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`. |
|
||||||
|
|
||||||
|
**Both of those lists are currently empty**, which is the news: all six confirmed
|
||||||
|
defects in `TODO.md` §2.1 are fixed, and the four tests that used to sit in
|
||||||
|
`AKSL_KNOWN_FAILING_TESTS` are folded back into the tests for the things they
|
||||||
|
test, where they now have to keep passing rather than keep failing visibly. The
|
||||||
|
mechanism stays for the next one. When a defect is fixed its known-failing test
|
||||||
|
starts passing, CTest reports it as failed with *unexpectedly passed*, and that
|
||||||
|
is the cue to move it into `AKSL_TESTS`.
|
||||||
|
|
||||||
|
Two more entries, `negative_noignore` and `negative_format_mismatch`, are
|
||||||
|
compile-time assertions rather than programs. Each builds a source file under
|
||||||
|
`tests/negative/` with `-Werror` and is marked `WILL_FAIL`, so the test passes
|
||||||
|
only when the compile *fails*. They exist because `AKERR_NOIGNORE` and
|
||||||
|
`AKSL_PRINTF_FORMAT` are enforced by the compiler and by nothing else: drop
|
||||||
|
either attribute in a refactor and every test still passes, the library still
|
||||||
|
builds, and the guarantee just quietly stops existing.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### 1a. The installed package
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -S deps/libakerror -B build-akerror && cmake --build build-akerror
|
||||||
|
cmake --install build-akerror --prefix /some/prefix
|
||||||
|
cmake --install build --prefix /some/prefix
|
||||||
|
cmake -S tests/consumer -B build-consumer -DCMAKE_PREFIX_PATH=/some/prefix
|
||||||
|
cmake --build build-consumer && ./build-consumer/consumer
|
||||||
|
```
|
||||||
|
|
||||||
|
The suite links the build tree, so it says nothing about whether an *installed*
|
||||||
|
libakstdlib is usable. `tests/consumer/` is a standalone project that does the
|
||||||
|
things only an install exercises: `find_package(akstdlib 0.2)` against the
|
||||||
|
generated version file, `akstdlibConfig.cmake`'s `find_dependency(akerror)`, and
|
||||||
|
the exported `akstdlib::akstdlib` target. It touches one function from each of
|
||||||
|
the four sources, so a library installed with a source file missing from its link
|
||||||
|
line fails here rather than in whatever consumer finds it next.
|
||||||
|
|
||||||
|
Note that libakerror has to be installed too. A top-level build compiles the
|
||||||
|
vendored copy with `EXCLUDE_FROM_ALL`, so `cmake --install` on this project
|
||||||
|
installs only this project -- and an installed libakstdlib whose
|
||||||
|
`find_dependency(akerror)` cannot resolve is not usable. Install the submodule's
|
||||||
|
copy, not `libakerror@main`: that is the version this repository pins and tests
|
||||||
|
against, and it is what CI does.
|
||||||
|
|
||||||
|
### 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`. Three of the defects fixed in 0.2.0 only misbehaved
|
||||||
|
under instrumentation — the uninitialised `%s` in `aksl_realpath`'s error path,
|
||||||
|
the unbounded `vsprintf` behind the old `aksl_sprintf`, and the missing `va_end`
|
||||||
|
in the `printf` family — and the tests that pin them are written to be run this
|
||||||
|
way. `tests/test_path.c` deliberately passes an *uninitialised* buffer on every
|
||||||
|
failure path for exactly that reason.
|
||||||
|
|
||||||
|
One test needs help from the sanitizer to test the same thing the normal build
|
||||||
|
does: `tests/test_memory.c` asks for `SIZE_MAX / 2` bytes to check that a refused
|
||||||
|
allocation reports `ENOMEM` and leaves `*dst` NULL. Plain `malloc` returns NULL;
|
||||||
|
ASan treats a request that large as a bug in the caller and aborts before `malloc`
|
||||||
|
returns at all. `CMakeLists.txt` sets `ASAN_OPTIONS=allocator_may_return_null=1`
|
||||||
|
for that one binary so the contract under test stays the same in both builds.
|
||||||
|
|
||||||
|
### 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.** All four sources, with the whole suite:
|
||||||
|
|
||||||
|
| file | lines | branches | functions |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `src/collections.c` | 99.3% (601/605) | 43.5% | 100% (43/43) |
|
||||||
|
| `src/stdlib.c` | 99.4% (614/618) | 46.5% | 100% (55/55) |
|
||||||
|
| `src/stream.c` | 100% (282/282) | 43.4% | 100% (33/33) |
|
||||||
|
| `src/string.c` | 100% (211/211) | 53.8% | 100% (23/23) |
|
||||||
|
| **total** | **99.5% (1708/1716)** | **46.0%** | **100% (154/154)** |
|
||||||
|
|
||||||
|
so the 90/40 gate above is a ratchet with headroom rather than a target. Eight
|
||||||
|
lines are uncovered and each is uncovered on purpose:
|
||||||
|
|
||||||
|
- **Four `} HANDLE(e, AKERR_ITERATOR_BREAK) {`** lines. 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 context whose status is *zero* —
|
||||||
|
the pathological case §2.2.1 exists to remove. Left uncovered rather than
|
||||||
|
pinned by a test that would have to manufacture it.
|
||||||
|
- **Two in `strbuf_reserve`**, the `size_t` overflow guard on a doubling that
|
||||||
|
would wrap. Reaching it needs a buffer within a factor of two of `SIZE_MAX`,
|
||||||
|
which is not a test, it is a hang.
|
||||||
|
- **Two in `aksl_fread`/`aksl_fwrite`**, the short transfer with *neither* `feof`
|
||||||
|
nor `ferror` set. Every way of producing a short transfer on Linux sets one or
|
||||||
|
the other; the branch is there because the standard permits neither, not
|
||||||
|
because anything reaches it. `TODO.md` §1.2 records it as still open.
|
||||||
|
|
||||||
|
Branch coverage sits far below line coverage because most branches in these files
|
||||||
|
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. Every `FAIL_ZERO_RETURN` in the
|
||||||
|
tree contributes several branches that this library has no way to reach. The
|
||||||
|
libakerror 1.0.0 bump made that gap wider without changing a line here: the
|
||||||
|
branch denominator per call site grew, so identical tests scored lower. Chasing
|
||||||
|
the number would mean testing libakerror's macros, which is what libakerror's
|
||||||
|
mutation suite is for — macros expand at the call site, so coverage cannot see
|
||||||
|
them properly from either side.
|
||||||
|
|
||||||
|
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 a 260-mutant sample across all four sources with `--threshold 65`.
|
||||||
|
A sample, because 1701 mutants each needing a full rebuild and test run is
|
||||||
|
hours — and `--max-mutants` samples by *even index*, not at random, so the same
|
||||||
|
260 run every time and the gate is reproducible. Sampling all four files beats
|
||||||
|
exhausting one of them, which is what this job used to do.
|
||||||
|
|
||||||
|
**Where it stands: 72.3% (188/260 killed).** That is a ratchet with headroom,
|
||||||
|
not a target.
|
||||||
|
|
||||||
|
It is well below the 89.6% this project reported at 0.1.0, and the difference is
|
||||||
|
denominator rather than tests. That figure covered one 561-line file; this covers
|
||||||
|
four totalling 1716 lines, and most of the new surface is argument validation
|
||||||
|
whose mutants are frequently *equivalent* — a mutation that cannot change
|
||||||
|
observable behaviour, so no test could ever kill it. The clearest example:
|
||||||
|
|
||||||
|
```c
|
||||||
|
errno = 0; /* delete this line */
|
||||||
|
*dst = malloc(size);
|
||||||
|
FAIL_ZERO_RETURN(e, *dst, AKSL_ERRNO_OR(ENOMEM), "%zu bytes", size);
|
||||||
|
```
|
||||||
|
|
||||||
|
Deleting the `errno = 0` is undetectable, because `malloc` always sets `errno`
|
||||||
|
when it fails. The line is still right to have — it is what makes
|
||||||
|
`AKSL_ERRNO_OR`'s contract sound, and it matters for the calls that *don't* set
|
||||||
|
`errno` — but no test distinguishes the two versions. Twelve of the 72 survivors
|
||||||
|
are that line in twelve different wrappers.
|
||||||
|
|
||||||
|
The survivors do break down usefully:
|
||||||
|
|
||||||
|
| Survivors | What | Verdict |
|
||||||
|
|---|---|---|
|
||||||
|
| 12 | `errno = 0` deleted before a call that always sets `errno` | Equivalent. Not a missing test. |
|
||||||
|
| 14 | `FAIL_*` guards deleted or their constants shifted | Mixed — the constant shifts are undetectable where the test names the same constant symbolically; the deletions are real. |
|
||||||
|
| 7 | `SUCCEED_RETURN` deleted | The function falls off the end and returns whatever is in the return register, which is often NULL by luck. Needs an assertion on a side effect, not on the status. |
|
||||||
|
| 3 | `va_end` deleted | Undetectable on x86-64 SysV, where `va_end` is a no-op. Real UB, invisible here. |
|
||||||
|
| 3 | `FINISH(e, true)` → `FINISH(e, false)` | Real: an error swallowed instead of propagated. Worth a test. |
|
||||||
|
| 33 | the rest | Individually listed with `file:line` and the exact edit in the published report. |
|
||||||
|
|
||||||
|
Two of them were real gaps and are now fixed: the depth-first walk's `depth + 1`
|
||||||
|
on the *right* child (nothing had ever recursed right more than three deep, so a
|
||||||
|
right-leaning tree would have blown the stack the depth cap exists to protect),
|
||||||
|
and `aksl_tree_remove` on an empty tree, which without its guard dereferences
|
||||||
|
NULL. Both are in the suite now — which is what the harness is for.
|
||||||
|
|
||||||
|
## 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`.
|
||||||
357
TODO.md
Normal file
357
TODO.md
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
Working notes for `libakstdlib`. **Outstanding items only** — anything fixed comes
|
||||||
|
out of this file and goes into `UPGRADING.md`, the tests, or a comment beside the
|
||||||
|
code, whichever is the right place to be reminded of it.
|
||||||
|
|
||||||
|
Ordered by blast radius: what blocks other work first, what is merely wrong
|
||||||
|
second, what is missing last.
|
||||||
|
|
||||||
|
## Where the library stands
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Wrapped | 154 public functions across `src/stdlib.c`, `src/string.c`, `src/stream.c`, `src/collections.c` |
|
||||||
|
| Tests | 17 CTest binaries plus 2 negative-compile entries, green under the default and sanitizer builds |
|
||||||
|
| Line coverage | 99.5% (1708/1716) |
|
||||||
|
| Function coverage | 100% (154/154) |
|
||||||
|
| Doxygen | 100% of 154, gated — `cmake --build build --target docs` fails on an undocumented function, parameter or return |
|
||||||
|
| Mutation score | 72.3% (188/260 sampled from 1701), gated at 65 |
|
||||||
|
|
||||||
|
The six confirmed defects that used to head this file are fixed and
|
||||||
|
`AKSL_KNOWN_FAILING_TESTS` is empty. What they were, and what changed as a
|
||||||
|
result, is in `UPGRADING.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Blocked on libakerror
|
||||||
|
|
||||||
|
These are not fixable from inside this repository. Each one currently costs
|
||||||
|
something here, and the cost is what makes them worth carrying.
|
||||||
|
|
||||||
|
### 1.1 The error pool is a process-global array with no locking
|
||||||
|
|
||||||
|
**This is what makes the library single-threaded**, and it is the largest open
|
||||||
|
item by some distance.
|
||||||
|
|
||||||
|
`AKERR_ARRAY_ERROR` is a fixed array in `deps/libakerror/src/error.c`, handed out
|
||||||
|
by `akerr_next_error()` with no synchronisation of any kind. Every entry point in
|
||||||
|
this library takes a slot from it on any failure path, so two threads raising
|
||||||
|
errors concurrently can be handed the same slot and will corrupt each other's
|
||||||
|
message, status and stack trace.
|
||||||
|
|
||||||
|
**Consequence.** `README.md` says plainly that the library is not thread-safe.
|
||||||
|
That is honest, and it is also a hard ceiling: §4's `pthread_*` and socket
|
||||||
|
wrappers cannot be written until this is resolved, because a threading API
|
||||||
|
nobody can call from a thread is not an API.
|
||||||
|
|
||||||
|
**What closing it would touch.** libakerror's pool — either a mutex around
|
||||||
|
`akerr_next_error`/`akerr_release_error`, or thread-local slot arrays, which
|
||||||
|
would suit the bounded-preallocation style better and cost nothing on the
|
||||||
|
single-threaded path. Then a TSan job in `.gitea/workflows/ci.yaml` and a
|
||||||
|
concurrent smoke test here, and the warning in `README.md` comes out.
|
||||||
|
|
||||||
|
### 1.2 `IGNORE()` logs a context and never releases it
|
||||||
|
|
||||||
|
`deps/libakerror/include/akerror.tmpl.h:308`. The macro assigns the context to
|
||||||
|
`__akerr_last_ignored`, logs it, and stops. The slot is never returned to the
|
||||||
|
pool, so every `IGNORE()` on a failing call leaks one — and after
|
||||||
|
`AKERR_MAX_ARRAY_ERROR` of them the pool is exhausted and `ENSURE_ERROR_READY`
|
||||||
|
calls `exit(1)`.
|
||||||
|
|
||||||
|
**Consequence here.** `aksl_tree_iterate`'s `CLEANUP` block (`src/stdlib.c`)
|
||||||
|
cannot use `IGNORE()` to drop a queue-drain failure and open-codes the
|
||||||
|
log-then-release by hand instead, with a comment saying why. It is four lines
|
||||||
|
that should be one.
|
||||||
|
|
||||||
|
**What closing it would touch.** One `RELEASE_ERROR` in the macro; then delete
|
||||||
|
the workaround here.
|
||||||
|
|
||||||
|
### 1.3 libakerror does not namespace its `coverage` target when embedded
|
||||||
|
|
||||||
|
`deps/libakerror/CMakeLists.txt:172` versus `:189` — it namespaces `mutation` and
|
||||||
|
not `coverage`.
|
||||||
|
|
||||||
|
**Consequence here.** A `-DAKSL_COVERAGE=ON` top-level build fails to configure
|
||||||
|
at all: *"another target with the same name already exists"*. `CMakeLists.txt`
|
||||||
|
shadows `add_custom_target` for the duration of the `add_subdirectory()` call and
|
||||||
|
renames the dependency's to `akerror_coverage`.
|
||||||
|
|
||||||
|
**What closing it would touch.** Apply the same
|
||||||
|
`CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR` test upstream that the
|
||||||
|
`mutation` target already has; then delete the shadow here, which sits directly
|
||||||
|
above the `add_test` shadow and shares its comment.
|
||||||
|
|
||||||
|
### 1.4 libakerror installs no `akerrorConfigVersion.cmake`
|
||||||
|
|
||||||
|
**Consequence here.** `cmake/akstdlib.cmake.in` has to call
|
||||||
|
`find_dependency(akerror)` with no version, because a request for one would be
|
||||||
|
refused for want of a version file no matter what is installed. The 1.0.0 floor
|
||||||
|
therefore rests on `akstdlib.pc`'s `Requires:` and the `#error` guard in
|
||||||
|
`akstdlib.h`, neither of which covers a `find_package` consumer.
|
||||||
|
|
||||||
|
**What closing it would touch.** One `write_basic_package_version_file()` call
|
||||||
|
upstream — libakstdlib already does this correctly and can be copied — then add
|
||||||
|
the `1.0.0` floor to the `find_dependency` here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Known gaps in what is already wrapped
|
||||||
|
|
||||||
|
### 2.1 A short transfer with neither EOF nor a stream error is untested
|
||||||
|
|
||||||
|
`src/stdlib.c`, the `FAIL_RETURN(e, AKERR_IO, "short read: ...")` in `aksl_fread`
|
||||||
|
and its counterpart in `aksl_fwrite`. Two of the eight uncovered lines in the
|
||||||
|
whole library.
|
||||||
|
|
||||||
|
The standard permits a short transfer with neither indicator set, so the branch
|
||||||
|
is correct to have. Every way of actually producing one on Linux sets `feof` or
|
||||||
|
`ferror` first, so nothing in the suite reaches it.
|
||||||
|
|
||||||
|
**Consequence.** Low: the code is a handful of lines and reviewed, but it is the
|
||||||
|
only error path in the library that has never executed.
|
||||||
|
|
||||||
|
**What closing it would touch.** A `FILE *` over a custom stream — `fopencookie`
|
||||||
|
on glibc, `funopen` on the BSDs — whose read function returns a short count
|
||||||
|
without setting either flag. That is a platform-specific test helper in
|
||||||
|
`tests/aksl_capture.h` guarded on the platform, which is why it has not been
|
||||||
|
written yet rather than an oversight.
|
||||||
|
|
||||||
|
### 2.2 The string-buffer overflow guard is untestable
|
||||||
|
|
||||||
|
`src/collections.c`, the `capacity = needed` arm in `strbuf_reserve`. Reaching it
|
||||||
|
needs an `aksl_StrBuf` within a factor of two of `SIZE_MAX`, which is not a test,
|
||||||
|
it is a hang. The other two uncovered lines.
|
||||||
|
|
||||||
|
**Consequence.** None known. It is there because doubling a capacity is a
|
||||||
|
multiplication, and an unguarded one is how a growable buffer turns into a heap
|
||||||
|
overflow.
|
||||||
|
|
||||||
|
**What closing it would touch.** Nothing worth doing. Recorded so the coverage
|
||||||
|
listing does not read as an oversight.
|
||||||
|
|
||||||
|
### 2.3 `aksl_version_check()` ignores its `patch` argument
|
||||||
|
|
||||||
|
`src/stdlib.c`, the `(void)patch`. Correct for the current "same soname" rule —
|
||||||
|
patch level never breaks the ABI — but the parameter exists only so the error
|
||||||
|
message can name the caller's full version.
|
||||||
|
|
||||||
|
**Consequence.** None today. If a future compatibility rule needs the patch level
|
||||||
|
to participate, that is the line to change, and the `#if` in
|
||||||
|
`tests/test_version.c` is the test that encodes the rule.
|
||||||
|
|
||||||
|
### 2.4 Surviving mutants worth turning into assertions
|
||||||
|
|
||||||
|
The mutation harness samples 260 of 1701 mutants and kills 72.3% of them. Most of
|
||||||
|
the 72 survivors are equivalent mutants rather than missing tests — `README.md`
|
||||||
|
has the full breakdown — but three clusters are real work:
|
||||||
|
|
||||||
|
- **`FINISH(e, true)` → `FINISH(e, false)`, 3 survivors.** An error swallowed
|
||||||
|
instead of propagated out of an `ATTEMPT` block, and nothing notices. Each one
|
||||||
|
is a call whose failure path is exercised but whose *propagation* is not: the
|
||||||
|
test asserts the status the callee raised without checking it came from the
|
||||||
|
callee rather than being re-raised locally. `tests/test_pool.c`'s origin
|
||||||
|
assertions are the shape of the fix.
|
||||||
|
- **`SUCCEED_RETURN` deleted, 7 survivors.** The function falls off the end and
|
||||||
|
returns whatever is in the return register, which is NULL often enough to pass.
|
||||||
|
These need an assertion on the *side effect* — the buffer that was filled, the
|
||||||
|
node that was linked — rather than on the returned status.
|
||||||
|
- **`FAIL_*` guards deleted, ~6 of the 14 in that group.** Each is an argument
|
||||||
|
check nothing drives. The other 8 in the group are constant shifts that no test
|
||||||
|
can catch, because the test names the same constant symbolically and moves with
|
||||||
|
it.
|
||||||
|
|
||||||
|
Two survivors in this class were real and are fixed: the right child's
|
||||||
|
`depth + 1` in the depth-first walk, and `aksl_tree_remove` on an empty tree.
|
||||||
|
|
||||||
|
**What closing them would touch.** Only `tests/`. Raise the `--threshold` in
|
||||||
|
`.gitea/workflows/ci.yaml` and `.githooks/pre-push` in step, as a ratchet.
|
||||||
|
|
||||||
|
### 2.5 Four uncovered `HANDLE(e, AKERR_ITERATOR_BREAK)` lines
|
||||||
|
|
||||||
|
Macro artifacts rather than gaps. In libakerror that macro begins with the
|
||||||
|
`break;` belonging to `PROCESS`'s `case 0:` arm, reachable only when a callback
|
||||||
|
returns a non-NULL context whose status is *zero* — which is the pathological case
|
||||||
|
the errno-fallback work removed. Left uncovered deliberately rather than pinned
|
||||||
|
by a test that would have to manufacture it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Deliberate omissions
|
||||||
|
|
||||||
|
Recorded so nobody adds them thinking they were forgotten. Each is a decision,
|
||||||
|
and each can be revisited with an argument.
|
||||||
|
|
||||||
|
| Not wrapped | Why |
|
||||||
|
|---|---|
|
||||||
|
| `sprintf`, `vsprintf` | Cannot be bounded. An error-handling wrapper around an unbounded write is the sharp edge this library exists to remove. `aksl_snprintf` and `aksl_asprintf` cover both real uses. |
|
||||||
|
| `strtok` | Keeps its state in a hidden static, so two interleaved tokenisations corrupt each other silently and any use from a thread is a bug. `aksl_strtok_r` and `aksl_strsep` cover it. |
|
||||||
|
| `strcpy`, `strcat` as libc spells them | Cannot be called safely without the destination's size. The wrappers take it. |
|
||||||
|
| `setbuf` | Exactly `setvbuf(stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ)` and strictly less expressive. Wrapping it would add a second way to say one thing. |
|
||||||
|
| `perror` | Writes to stderr and consults a global. `aksl_strerror` is the akerror-native equivalent and knows this library's own statuses as well as errno's. |
|
||||||
|
| `strerror_r` | Two incompatible functions share that name and which one you get depends on feature-test macros a consumer cannot influence from inside this header. `aksl_strerror` is built on libakerror's registry instead. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Not yet wrapped
|
||||||
|
|
||||||
|
Ordered by how much a caller of this library would miss them. §3.1 and §3.6 of
|
||||||
|
the old numbering are done; what follows is what is left.
|
||||||
|
|
||||||
|
### 4.1 POSIX file and process API
|
||||||
|
|
||||||
|
The most likely next surface. Nothing here is blocked; it is simply not written.
|
||||||
|
|
||||||
|
**`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`
|
||||||
|
|
||||||
|
The short-read/short-write contract is the interesting part: `read(2)` returning
|
||||||
|
fewer bytes than asked for is *normal* on a pipe or a socket and a failure on a
|
||||||
|
regular file, so the wrapper needs the same transferred-count out-param
|
||||||
|
`aksl_fread` has, and callers need to be told which case they are in.
|
||||||
|
|
||||||
|
**`sys/stat.h`**
|
||||||
|
- [ ] `stat`, `fstat`, `lstat`, `fstatat`
|
||||||
|
- [ ] `statvfs`, `fstatvfs`
|
||||||
|
|
||||||
|
**`dirent.h`**
|
||||||
|
- [ ] `opendir`, `fdopendir`, `readdir`, `closedir`, `rewinddir`, `scandir`
|
||||||
|
|
||||||
|
`readdir(3)` returning NULL for both "end of directory" and "error, check errno"
|
||||||
|
is the same conflation `aksl_fgetc` and `aksl_fgets` already untangle, and should
|
||||||
|
be untangled the same way: AKERR_EOF for the end, the errno for the error.
|
||||||
|
|
||||||
|
**Process control**
|
||||||
|
- [ ] `fork`, the `exec*` family, `waitpid`, `wait`
|
||||||
|
- [ ] `posix_spawn`
|
||||||
|
- [ ] `system`, `popen`, `pclose`
|
||||||
|
- [ ] `getpid`, `getppid`, `getuid`, `geteuid`, `setuid`, `setgid`
|
||||||
|
- [ ] `atexit`, `exit`, `_exit`, `abort` — mostly to give akerror a shutdown hook
|
||||||
|
- [ ] `getenv`, `setenv`, `unsetenv`, `putenv`, `clearenv`
|
||||||
|
|
||||||
|
`fork` needs thinking about before it is wrapped: the error pool is inherited by
|
||||||
|
the child, and any context live at the moment of the fork exists twice
|
||||||
|
afterwards.
|
||||||
|
|
||||||
|
**`sys/mman.h`**
|
||||||
|
- [ ] `mmap`, `munmap`, `mprotect`, `msync`, `madvise`
|
||||||
|
|
||||||
|
### 4.2 Time
|
||||||
|
|
||||||
|
- [ ] `time`, `clock_gettime`, `clock_getres`, `gettimeofday`
|
||||||
|
- [ ] `localtime_r`, `gmtime_r`, `mktime`, `timegm`, `difftime`
|
||||||
|
- [ ] `strftime`, `strptime`
|
||||||
|
- [ ] `clock`, `times`
|
||||||
|
|
||||||
|
`strftime(3)` returns 0 for both "the output was empty" and "it did not fit",
|
||||||
|
which is the bounded-write ambiguity `aksl_snprintf` already resolves.
|
||||||
|
|
||||||
|
### 4.3 Sorting, searching and the rest of `stdlib.h`
|
||||||
|
|
||||||
|
- [ ] `qsort`, `qsort_r`, `bsearch` — a comparator that fails currently has
|
||||||
|
nowhere to put the error. An akerror-aware comparator signature, shaped
|
||||||
|
like the `aksl_TreeCompareFunc` the tree functions already take, would be a
|
||||||
|
genuine improvement over libc rather than a wrapper around it.
|
||||||
|
- [ ] `abs`, `labs`, `llabs`, `div`, `ldiv`, `lldiv` — note that
|
||||||
|
`abs(INT_MIN)` is undefined behaviour, which is exactly the kind of silent
|
||||||
|
trap worth surfacing.
|
||||||
|
- [ ] `rand`, `srand`, `random`, `srandom`, `getrandom`/`arc4random`
|
||||||
|
|
||||||
|
### 4.4 Larger surfaces
|
||||||
|
|
||||||
|
- [ ] **Sockets**: `socket`, `bind`, `listen`, `accept`, `connect`, the
|
||||||
|
`send`/`recv` families, `shutdown`, `setsockopt`/`getsockopt`,
|
||||||
|
`getaddrinfo`/`freeaddrinfo`/`gai_strerror`, `inet_ntop`/`inet_pton`.
|
||||||
|
`getaddrinfo` is the interesting one: it has its own error space, neither
|
||||||
|
errno nor akerror, which needs mapping into a registered status range.
|
||||||
|
- [ ] **Multiplexing**: `select`, `poll`, `ppoll`, `epoll_*`
|
||||||
|
- [ ] **Signals**: `sigaction`, `sigprocmask`, `sigemptyset`/`sigaddset`, `kill`,
|
||||||
|
`raise`, `signalfd`. A handler cannot raise an akerror context — the pool is
|
||||||
|
not async-signal-safe — so the wrapper covers installation and masking only,
|
||||||
|
and that limit should be documented rather than discovered.
|
||||||
|
- [ ] **Threads**: `pthread_create`/`join`/`detach`, `pthread_mutex_*`,
|
||||||
|
`pthread_cond_*`, `pthread_rwlock_*`, `sem_*`. **Blocked on §1.1.**
|
||||||
|
- [ ] **Dynamic loading**: `dlopen`, `dlsym`, `dlclose`, `dlerror`. `dlerror(3)`
|
||||||
|
is the same "returns a string and clears itself" trap as `strerror`.
|
||||||
|
- [ ] **Locale / wide chars**: `setlocale`, `mbstowcs`, `wcstombs`, `iconv_*`
|
||||||
|
- [ ] **Math**: the `math.h` functions that set `errno` or raise FP exceptions
|
||||||
|
(`sqrt`, `log`, `pow`, `acos`, …). Probably a dedicated `libakmath` rather
|
||||||
|
than more surface here — the failure model is `fetestexcept`, not errno, and
|
||||||
|
it does not resemble anything else in this library.
|
||||||
|
|
||||||
|
### 4.5 Data structures
|
||||||
|
|
||||||
|
The list and tree API is complete against what §3.6 asked for. What a second
|
||||||
|
consumer might want next:
|
||||||
|
|
||||||
|
- [ ] A dynamic array / vector, on the same "caller owns the storage" terms as
|
||||||
|
`aksl_HashMap`.
|
||||||
|
- [ ] A hash map keyed on something other than a string — the current one copies
|
||||||
|
keys into fixed slots, which is right for identifiers and wrong for
|
||||||
|
anything large or binary.
|
||||||
|
- [ ] Balanced insertion for the tree. It is a plain unbalanced BST and says so;
|
||||||
|
sorted input gives a degenerate chain that `AKSL_TREE_MAX_DEPTH` then
|
||||||
|
refuses. Bounded rather than dangerous, but a red-black or AVL variant is
|
||||||
|
the honest fix if anyone inserts sorted data in earnest.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 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 was the first consumer to exercise the
|
||||||
|
whole surface rather than a corner of it, and what it *could not* use is what
|
||||||
|
prioritised the work above.
|
||||||
|
|
||||||
|
**The number that started it.** Across `src/`, akbasic made **10 calls into this
|
||||||
|
library and 116 to raw libc** — a library whose value proposition is "turn silent
|
||||||
|
libc failures into error contexts", bypassed 92% of the time by the consumer most
|
||||||
|
committed to it.
|
||||||
|
|
||||||
|
| Raw libc it had to use | Count | Now available as |
|
||||||
|
|---|---|---|
|
||||||
|
| `strlen` | 37 | `aksl_strlen` |
|
||||||
|
| `snprintf` | 28 | `aksl_snprintf` |
|
||||||
|
| `strcmp` | 16 | `aksl_strcmp` |
|
||||||
|
| `memcpy` / `memset` | 16 | `aksl_memcpy` / `aksl_memset` |
|
||||||
|
| `strncpy` | 15 | `aksl_strncpy` |
|
||||||
|
| `strtoll` / `strtod` | 2 | `aksl_strtoll` / `aksl_strtod` |
|
||||||
|
| `fgets` | 2 | `aksl_fgets` |
|
||||||
|
| `strstr` | 1 | `aksl_strstr` |
|
||||||
|
|
||||||
|
**All four things the port had to write for itself now exist here.**
|
||||||
|
|
||||||
|
1. **A strict `strtoll`/`strtod` wrapper** (`akbasic/src/convert.c`, ~60 lines).
|
||||||
|
The `aksl_strto*` family is that, with the endptr/`errno`/range contract.
|
||||||
|
akbasic's own `TODO.md` §1.9 formally bans the `aksl_ato*` family because
|
||||||
|
routing four diagnosable errors through it would have turned them into wrong
|
||||||
|
answers — `VAL("garbage")` silently returning `0.0`. That ban can be lifted:
|
||||||
|
the `ato*` forms report failures now. **`src/convert.c` can be deleted.**
|
||||||
|
2. **A fixed-capacity string-keyed hash table** (`akbasic/src/symtab.c`, ~130
|
||||||
|
lines, needed three times over). `aksl_hashmap_*` is that table generalised,
|
||||||
|
with tombstones on delete, which the original did not have.
|
||||||
|
3. **The bounded-copy-with-truncation-as-error idiom, at ten sites.**
|
||||||
|
`aksl_strcpy` and `aksl_strncpy` are exactly that idiom.
|
||||||
|
4. **Uppercase folding for case-insensitive lookup, three times.**
|
||||||
|
`aksl_strcasecmp` and `aksl_strncasecmp`.
|
||||||
|
|
||||||
|
**And the four confirmed-with-impact items are closed.** §2.2.4's unbounded
|
||||||
|
`aksl_sprintf` is gone; §2.2.2's unchecked `aksl_fopen` arguments are checked, so
|
||||||
|
`akbasic_cmd_dload`'s hand-rolled validation and its comment pointing here can go;
|
||||||
|
§2.2.6's sign-extended djb2 reads bytes unsigned; and §2.1.4's missing `va_end` —
|
||||||
|
which akbasic's stdio text sink ran on every line of program output — is fixed.
|
||||||
|
|
||||||
|
**Still true, and still shaping the wishlist.** akbasic uses no allocator, no
|
||||||
|
lists and no trees, drawing everything from fixed pools by design. A consumer that
|
||||||
|
does allocate would weight §4.1's `open`/`read`/`write` far higher than this one
|
||||||
|
does. The next thing worth doing is porting akbasic onto this release and
|
||||||
|
counting the calls again.
|
||||||
178
UPGRADING.md
Normal file
178
UPGRADING.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# Upgrading
|
||||||
|
|
||||||
|
## 0.1.0 → 0.2.0
|
||||||
|
|
||||||
|
This is an ABI break and a source break. Pre-1.0 the soname is `MAJOR.MINOR`, so
|
||||||
|
`libakstdlib.so.0.1` and `libakstdlib.so.0.2` are different libraries as far as
|
||||||
|
the loader is concerned and a consumer built against 0.1 will not silently pick
|
||||||
|
this up. Rebuild against the new header.
|
||||||
|
|
||||||
|
`AKSL_VERSION_CHECK()` catches the pairing at runtime if a stale `.so` ever does
|
||||||
|
end up on the path.
|
||||||
|
|
||||||
|
Everything below comes out of `TODO.md` sections 2.1 and 2.2 — the confirmed
|
||||||
|
defects, all of which were reproduced against the 0.1.0 library before being
|
||||||
|
fixed.
|
||||||
|
|
||||||
|
### Behaviour changes with the same signature
|
||||||
|
|
||||||
|
**`aksl_atoi`, `aksl_atol`, `aksl_atoll`, `aksl_atof` report bad conversions.**
|
||||||
|
This is the one to look at first, because the compiler will not tell you about
|
||||||
|
it. They used to return success for anything: `aksl_atoi("not a number", &n)`
|
||||||
|
gave you `0`, and `aksl_atoi("99999999999999999999", &n)` gave you a wrapped
|
||||||
|
value. Now:
|
||||||
|
|
||||||
|
| input | before | after |
|
||||||
|
|---|---|---|
|
||||||
|
| `"42"` | success, 42 | success, 42 |
|
||||||
|
| `"not a number"` | **success, 0** | `AKERR_VALUE` |
|
||||||
|
| `""` or `" "` | **success, 0** | `AKERR_VALUE` |
|
||||||
|
| `"12abc"` | **success, 12** | `AKERR_VALUE` |
|
||||||
|
| `"0x10"` | **success, 0** | `AKERR_VALUE` (base 10 stops at the `x`) |
|
||||||
|
| `"99999999999999999999"` | **success, garbage** | `ERANGE` |
|
||||||
|
|
||||||
|
`*dest` is `0` on every failure path. If you were relying on the old silence —
|
||||||
|
treating an unparseable string as zero on purpose — that is now an error you
|
||||||
|
have to handle or deliberately ignore.
|
||||||
|
|
||||||
|
For `"0x10"` and friends, use `aksl_strtol(nptr, NULL, 0, &dest)`, which honours
|
||||||
|
the `0x` and `0` prefixes. The whole `aksl_strto*` family is new and is what the
|
||||||
|
`ato*` forms are built on.
|
||||||
|
|
||||||
|
**Errors no longer carry status `0`.** Any wrapper that reported `errno` could
|
||||||
|
previously raise an error whose status was whatever `errno` happened to hold,
|
||||||
|
including `0` — which every `DETECT` and `CATCH` downstream reads as *success*
|
||||||
|
while the context still holds a pool slot. `errno` is now cleared before each
|
||||||
|
wrapped call and read back through a fallback. If you were matching on a
|
||||||
|
specific status, check it is still the one you get; several calls that used to
|
||||||
|
report a flat `AKERR_IO` now report the real `errno` (a read from a write-only
|
||||||
|
stream is `EBADF`, not `AKERR_IO`).
|
||||||
|
|
||||||
|
**`aksl_malloc(0, &p)` is `AKERR_VALUE`.** It used to depend on whether the
|
||||||
|
platform's `malloc(0)` returned NULL.
|
||||||
|
|
||||||
|
**`aksl_memcpy` refuses overlapping ranges** with `AKERR_VALUE`. Use
|
||||||
|
`aksl_memmove`.
|
||||||
|
|
||||||
|
**`aksl_list_append` refuses a node already in the list** with `AKERR_VALUE`.
|
||||||
|
It used to relink it and orphan everything between its old position and the tail.
|
||||||
|
|
||||||
|
**`aksl_list_append` and `aksl_list_iterate` are correct now.** If you worked
|
||||||
|
around either defect — appending one node at a time and fixing up the links by
|
||||||
|
hand, or starting your own iteration from the head because the callback never
|
||||||
|
saw it — that workaround is now wrong. `append` truncated any list of two or
|
||||||
|
more nodes; `iterate` skipped everything before the list midpoint.
|
||||||
|
|
||||||
|
**`AKERR_ITERATOR_BREAK` stops a tree traversal.** It previously did not: the
|
||||||
|
walk ran to completion regardless. Code that raised it and relied on the
|
||||||
|
traversal continuing anyway (unlikely, but it was the behaviour) will now stop.
|
||||||
|
|
||||||
|
**Tree traversal is bounded.** A tree deeper than `AKSL_TREE_MAX_DEPTH` (256) is
|
||||||
|
`AKERR_OUTOFBOUNDS`, and a depth-first walk over a tree whose child points back
|
||||||
|
at an ancestor is `AKERR_CIRCULAR_REFERENCE`. Both used to run until the process
|
||||||
|
died.
|
||||||
|
|
||||||
|
**An unrecognised `searchmode` is `AKERR_VALUE`.** It used to return success
|
||||||
|
having visited nothing.
|
||||||
|
|
||||||
|
### Signature changes
|
||||||
|
|
||||||
|
**`aksl_sprintf` is gone.** It wrapped `vsprintf`, which cannot be bounded.
|
||||||
|
|
||||||
|
```c
|
||||||
|
/* before */
|
||||||
|
aksl_sprintf(&count, buf, "%s=%d", key, value);
|
||||||
|
/* after */
|
||||||
|
aksl_snprintf(&count, buf, sizeof(buf), "%s=%d", key, value);
|
||||||
|
```
|
||||||
|
|
||||||
|
Truncation is `AKERR_OUTOFBOUNDS` rather than a short success, and `*count` is
|
||||||
|
`0` on any failure rather than `vsprintf`'s `-1`.
|
||||||
|
|
||||||
|
**`aksl_realpath` takes the destination's length.**
|
||||||
|
|
||||||
|
```c
|
||||||
|
/* before */
|
||||||
|
char resolved[PATH_MAX];
|
||||||
|
aksl_realpath(path, resolved);
|
||||||
|
/* after */
|
||||||
|
char resolved[PATH_MAX];
|
||||||
|
aksl_realpath(path, resolved, sizeof(resolved));
|
||||||
|
```
|
||||||
|
|
||||||
|
`buflen` must be at least `PATH_MAX`; `realpath(3)` cannot be bounded below it,
|
||||||
|
so a shorter buffer is `AKERR_OUTOFBOUNDS` rather than an overflow. If you have
|
||||||
|
no `PATH_MAX`-sized buffer to hand, `aksl_realpath_alloc(path, &dest)` allocates
|
||||||
|
one and hands it over — release it with `aksl_free`.
|
||||||
|
|
||||||
|
**`aksl_fread` and `aksl_fwrite` take a transferred-count out-param.**
|
||||||
|
|
||||||
|
```c
|
||||||
|
/* before */
|
||||||
|
aksl_fread(buf, 1, sizeof(buf), fp);
|
||||||
|
/* after */
|
||||||
|
size_t got = 0;
|
||||||
|
aksl_fread(buf, 1, sizeof(buf), fp, &got);
|
||||||
|
```
|
||||||
|
|
||||||
|
It is required, not optional, because it is the only way a caller who gets
|
||||||
|
`AKERR_EOF` can find out how much data arrived. It is written on every path
|
||||||
|
including the failure ones. A short transfer with neither EOF nor a stream error
|
||||||
|
set is now `AKERR_IO` rather than success.
|
||||||
|
|
||||||
|
**`aksl_list_pop` takes the head by reference.**
|
||||||
|
|
||||||
|
```c
|
||||||
|
/* before */
|
||||||
|
aksl_list_pop(node);
|
||||||
|
/* after */
|
||||||
|
aksl_list_pop(&head, node); /* head is updated when node was the head */
|
||||||
|
```
|
||||||
|
|
||||||
|
Popping the head used to leave the caller's own head pointer aimed at a
|
||||||
|
now-detached node with no way to learn the new one.
|
||||||
|
|
||||||
|
**`aksl_tree_iterate` lost its `queue` parameter.**
|
||||||
|
|
||||||
|
```c
|
||||||
|
/* before */
|
||||||
|
aksl_tree_iterate(root, iter, NULL, NULL, mode, data, NULL);
|
||||||
|
/* after */
|
||||||
|
aksl_tree_iterate(root, iter, NULL, NULL, mode, data);
|
||||||
|
```
|
||||||
|
|
||||||
|
The doc comment told callers to pass `NULL`, which is a sign the queue belonged
|
||||||
|
in an internal helper. `lalloc` and `lfree` stay, and they are actually used now
|
||||||
|
— the breadth-first modes are implemented, where before they raised
|
||||||
|
`AKERR_NOT_IMPLEMENTED`.
|
||||||
|
|
||||||
|
**`aksl_strhash_djb2` takes `const char *`** and reads bytes as unsigned. Callers
|
||||||
|
no longer have to cast away constness. The hash value **changes** for any input
|
||||||
|
containing a byte ≥ 0x80: it now matches canonical djb2 instead of depending on
|
||||||
|
whether plain `char` is signed on the target. 7-bit ASCII keys are unaffected. If
|
||||||
|
you have persisted djb2 values across a restart, they will not match.
|
||||||
|
|
||||||
|
**`aksl_fopen` takes `const char *`** for both `pathname` and `mode`, and checks
|
||||||
|
them — `fopen(NULL, ...)` is undefined behaviour and used to go straight through.
|
||||||
|
|
||||||
|
**`aksl_memcpy` takes `const void *` for its source.**
|
||||||
|
|
||||||
|
### Additions
|
||||||
|
|
||||||
|
Nothing here breaks anything; see `README.md` for the full surface.
|
||||||
|
|
||||||
|
- `aksl_calloc`, `aksl_realloc`, `aksl_freep`, `aksl_memmove`, `aksl_memcmp`,
|
||||||
|
`aksl_memchr`
|
||||||
|
- `aksl_snprintf`, `aksl_vprintf`, `aksl_vfprintf`, `aksl_vsnprintf`
|
||||||
|
- `aksl_strtol`, `aksl_strtoll`, `aksl_strtoul`, `aksl_strtoull`, `aksl_strtod`,
|
||||||
|
`aksl_strtof`, `aksl_strtold`
|
||||||
|
- the whole of `src/string.c` and `src/stream.c`
|
||||||
|
- `aksl_list_prepend` / `insert_after` / `insert_before` / `length` / `find` /
|
||||||
|
`reverse` / `concat` / `free_all` / `iterate_reverse`, and the `aksl_List`
|
||||||
|
container so append is O(1)
|
||||||
|
- `aksl_tree_insert` / `find` / `remove` / `height` / `count` / `free_all`
|
||||||
|
- `aksl_hashmap_*`, `aksl_strbuf_*`, `aksl_strhash_fnv1a`
|
||||||
|
- `aksl_list_node_init` and `aksl_tree_node_init`, so a caller no longer has to
|
||||||
|
remember to `memset` a node before its first use
|
||||||
|
- the header is `extern "C"`-guarded and pulls in four standard headers rather
|
||||||
|
than six
|
||||||
@@ -6,5 +6,11 @@ includedir=${exec_prefix}/include
|
|||||||
Name: akstdlib
|
Name: akstdlib
|
||||||
Description: C stdlib with akerror sanity wrappers
|
Description: C stdlib with akerror sanity wrappers
|
||||||
Version: @PROJECT_VERSION@
|
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}/
|
Cflags: -I${includedir}/
|
||||||
Libs: -L${libdir} -lakstdlib
|
Libs: -L${libdir} -lakstdlib
|
||||||
49
cmake/RunDoxygen.cmake
Normal file
49
cmake/RunDoxygen.cmake
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Run doxygen with PROJECT_NUMBER supplied from CMake, and fail on any warning.
|
||||||
|
#
|
||||||
|
# Driven by the `docs` target in the top-level CMakeLists.txt. It is a separate
|
||||||
|
# script rather than a COMMAND because it has to do three things in sequence --
|
||||||
|
# feed the configuration in, then read the log back, then decide -- and a
|
||||||
|
# custom-target command list cannot branch on the result of an earlier command.
|
||||||
|
#
|
||||||
|
# The version comes in from CMake rather than living in the Doxyfile so that
|
||||||
|
# project() stays the single place a version number is written.
|
||||||
|
|
||||||
|
if(NOT AKSL_DOXYGEN OR NOT AKSL_SOURCE_DIR OR NOT AKSL_VERSION)
|
||||||
|
message(FATAL_ERROR "RunDoxygen.cmake needs AKSL_DOXYGEN, AKSL_SOURCE_DIR and AKSL_VERSION")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(_doxyfile "${AKSL_SOURCE_DIR}/Doxyfile")
|
||||||
|
set(_logfile "${AKSL_SOURCE_DIR}/doxygen-warnings.log")
|
||||||
|
file(READ "${_doxyfile}" _config)
|
||||||
|
# Later settings win in a Doxyfile, so appending is enough to override.
|
||||||
|
set(_config "${_config}\nPROJECT_NUMBER = ${AKSL_VERSION}\n")
|
||||||
|
|
||||||
|
set(_generated "${AKSL_SOURCE_DIR}/Doxyfile.generated")
|
||||||
|
file(WRITE "${_generated}" "${_config}")
|
||||||
|
|
||||||
|
execute_process(
|
||||||
|
COMMAND "${AKSL_DOXYGEN}" "${_generated}"
|
||||||
|
WORKING_DIRECTORY "${AKSL_SOURCE_DIR}"
|
||||||
|
RESULT_VARIABLE _result
|
||||||
|
OUTPUT_QUIET
|
||||||
|
)
|
||||||
|
file(REMOVE "${_generated}")
|
||||||
|
|
||||||
|
if(NOT _result EQUAL 0)
|
||||||
|
message(FATAL_ERROR "doxygen exited ${_result}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# WARN_IF_UNDOCUMENTED and WARN_NO_PARAMDOC are on, so anything in the log is a
|
||||||
|
# public function, parameter or return value nobody has described. Treat it the
|
||||||
|
# way an uncovered line or a surviving mutant is treated: as work, not as noise.
|
||||||
|
if(EXISTS "${_logfile}")
|
||||||
|
file(READ "${_logfile}" _warnings)
|
||||||
|
string(STRIP "${_warnings}" _warnings)
|
||||||
|
if(NOT _warnings STREQUAL "")
|
||||||
|
message("${_warnings}")
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"doxygen reported undocumented entities; see doxygen-warnings.log")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
message(STATUS "API documentation written to ${AKSL_SOURCE_DIR}/doxygen/html")
|
||||||
@@ -1,5 +1,17 @@
|
|||||||
# cmake/MyLibraryConfig.cmake.in
|
# cmake/akstdlib.cmake.in -- installed as akstdlibConfig.cmake
|
||||||
include(CMakeFindDependencyMacro) # If your library has dependencies
|
#
|
||||||
# find_dependency(AnotherDependency REQUIRED) # Example dependency
|
# 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")
|
include("${CMAKE_CURRENT_LIST_DIR}/akstdlibTargets.cmake")
|
||||||
|
|||||||
1
deps/libakerror
vendored
Submodule
1
deps/libakerror
vendored
Submodule
Submodule deps/libakerror added at 5ff87908e7
2247
include/akstdlib.h
2247
include/akstdlib.h
File diff suppressed because it is too large
Load Diff
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())
|
||||||
25
scripts/reindent.el
Normal file
25
scripts/reindent.el
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
;; Canonical reindent: Emacs cc-mode "stroustrup", c-basic-offset 4,
|
||||||
|
;; indent-tabs-mode on, tab-width 8. A correct file is a fixed point of this.
|
||||||
|
;;
|
||||||
|
;; inextern-lang is set to 0 because akstdlib.h wraps its whole body in
|
||||||
|
;; extern "C" { }, and cc-mode otherwise indents every declaration in the file
|
||||||
|
;; one level for it. That is a real cc-mode behaviour rather than a bug, but it
|
||||||
|
;; is not the house style and not what the file looked like before.
|
||||||
|
;;
|
||||||
|
;; AGENTS.md forbids introducing clang-format, and this is why the tool that
|
||||||
|
;; decides what "correct" means has to be the same engine the author's editor
|
||||||
|
;; runs: this offset is not something clang-format could be made to agree with.
|
||||||
|
(require 'cc-mode)
|
||||||
|
(setq-default indent-tabs-mode t)
|
||||||
|
(setq-default tab-width 8)
|
||||||
|
(setq c-default-style "stroustrup")
|
||||||
|
(dolist (file command-line-args-left)
|
||||||
|
(find-file file)
|
||||||
|
(c-mode)
|
||||||
|
(setq indent-tabs-mode t
|
||||||
|
tab-width 8
|
||||||
|
c-basic-offset 4)
|
||||||
|
(c-set-offset 'inextern-lang 0)
|
||||||
|
(indent-region (point-min) (point-max))
|
||||||
|
(when (buffer-modified-p) (save-buffer))
|
||||||
|
(kill-buffer))
|
||||||
25
src/aksl_internal.h
Normal file
25
src/aksl_internal.h
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#ifndef _AKSL_INTERNAL_H_
|
||||||
|
#define _AKSL_INTERNAL_H_
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Shared internals. Not installed, not part of the public API -- anything here
|
||||||
|
* is free to change without so much as a patch bump.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* errno as an akerror status, with a fallback for when it is not one.
|
||||||
|
*
|
||||||
|
* A libc call is allowed to fail without touching errno -- malloc(0) may return
|
||||||
|
* NULL and leave it alone -- and errno may equally be a leftover from some
|
||||||
|
* earlier, unrelated, *successful* call. Reporting it raw produces a FAIL whose
|
||||||
|
* status is 0, which every downstream DETECT and CATCH reads as success while
|
||||||
|
* the context still holds a pool slot: an error that is invisible and leaks at
|
||||||
|
* the same time. Every errno-sourced status in this library goes through here,
|
||||||
|
* and every wrapped call clears errno first so the value read back is its own.
|
||||||
|
* TODO.md 2.2.1.
|
||||||
|
*/
|
||||||
|
#define AKSL_ERRNO_OR(__fallback) (errno != 0 ? errno : (__fallback))
|
||||||
|
|
||||||
|
#endif // _AKSL_INTERNAL_H_
|
||||||
1130
src/collections.c
Normal file
1130
src/collections.c
Normal file
File diff suppressed because it is too large
Load Diff
1295
src/stdlib.c
1295
src/stdlib.c
File diff suppressed because it is too large
Load Diff
570
src/stream.c
Normal file
570
src/stream.c
Normal file
@@ -0,0 +1,570 @@
|
|||||||
|
/*
|
||||||
|
* stdio.h wrappers beyond fopen/fread/fwrite/fclose -- TODO.md section 3.1.
|
||||||
|
*
|
||||||
|
* Positioning, flushing, character and line I/O, stream state, formatted input,
|
||||||
|
* and the file-level operations that go with them.
|
||||||
|
*
|
||||||
|
* The recurring theme, and the reason most of these are worth wrapping at all,
|
||||||
|
* is that stdio reports failure through a return value that is easy to mistake
|
||||||
|
* for data. ftell(3) returns -1L, fgetc(3) returns EOF, fgets(3) returns NULL,
|
||||||
|
* and sscanf(3) returns a count that a caller has to compare against the number
|
||||||
|
* of conversions it wrote out by hand. Every one of those is a silent failure
|
||||||
|
* waiting for someone to forget the check once.
|
||||||
|
*
|
||||||
|
* Where a function has a genuine "nothing more to read" outcome -- fgets at the
|
||||||
|
* end of a file, getline at EOF -- that is AKERR_EOF rather than AKERR_IO, so a
|
||||||
|
* read loop can tell the end of its input from the failure of its input.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <akstdlib.h>
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "aksl_internal.h"
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Positioning */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_fseek(FILE *stream, long offset, int whence)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream);
|
||||||
|
errno = 0;
|
||||||
|
FAIL_NONZERO_RETURN(e, fseek(stream, offset, whence), AKSL_ERRNO_OR(AKERR_IO),
|
||||||
|
"fseek to offset %ld whence %d", offset, whence);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ftell(3) reports failure as -1L, which is also a perfectly ordinary thing for
|
||||||
|
* an arithmetic type to hold. Out through *dest, with the failure as a status.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_ftell(FILE *stream, long *dest)
|
||||||
|
{
|
||||||
|
long pos = 0;
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
|
||||||
|
*dest = 0;
|
||||||
|
errno = 0;
|
||||||
|
pos = ftell(stream);
|
||||||
|
FAIL_NONZERO_RETURN(e, (pos == -1L), AKSL_ERRNO_OR(AKERR_IO), "ftell failed");
|
||||||
|
*dest = pos;
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* rewind(3) is the one positioning call with no error return at all: it is
|
||||||
|
* fseek(stream, 0, SEEK_SET) with the result thrown away, and it clears the
|
||||||
|
* error indicator on the way past so even that evidence is gone. This is the
|
||||||
|
* fseek, so a rewind that cannot happen is reported rather than assumed.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_rewind(FILE *stream)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream);
|
||||||
|
errno = 0;
|
||||||
|
FAIL_NONZERO_RETURN(e, fseek(stream, 0L, SEEK_SET), AKSL_ERRNO_OR(AKERR_IO),
|
||||||
|
"rewind failed");
|
||||||
|
clearerr(stream);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_fseeko(FILE *stream, off_t offset, int whence)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream);
|
||||||
|
errno = 0;
|
||||||
|
FAIL_NONZERO_RETURN(e, fseeko(stream, offset, whence), AKSL_ERRNO_OR(AKERR_IO),
|
||||||
|
"fseeko to offset %lld whence %d", (long long)offset, whence);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_ftello(FILE *stream, off_t *dest)
|
||||||
|
{
|
||||||
|
off_t pos = 0;
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
|
||||||
|
*dest = 0;
|
||||||
|
errno = 0;
|
||||||
|
pos = ftello(stream);
|
||||||
|
FAIL_NONZERO_RETURN(e, (pos == (off_t)-1), AKSL_ERRNO_OR(AKERR_IO), "ftello failed");
|
||||||
|
*dest = pos;
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* fgetpos/fsetpos carry an opaque fpos_t rather than a byte offset, which is
|
||||||
|
* what makes them the right pair for a stream in a multibyte locale where a
|
||||||
|
* byte offset is not enough to restore the conversion state.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_fgetpos(FILE *stream, fpos_t *pos)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, pos=%p", (void *)stream, (void *)pos);
|
||||||
|
FAIL_ZERO_RETURN(e, pos, AKERR_NULLPOINTER, "stream=%p, pos=%p", (void *)stream, (void *)pos);
|
||||||
|
errno = 0;
|
||||||
|
FAIL_NONZERO_RETURN(e, fgetpos(stream, pos), AKSL_ERRNO_OR(AKERR_IO), "fgetpos failed");
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_fsetpos(FILE *stream, const fpos_t *pos)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, pos=%p", (void *)stream, (void *)pos);
|
||||||
|
FAIL_ZERO_RETURN(e, pos, AKERR_NULLPOINTER, "stream=%p, pos=%p", (void *)stream, (void *)pos);
|
||||||
|
errno = 0;
|
||||||
|
FAIL_NONZERO_RETURN(e, fsetpos(stream, pos), AKSL_ERRNO_OR(AKERR_IO), "fsetpos failed");
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Flushing and buffering */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* NULL is legal here and means "every output stream", which is fflush(3)'s own
|
||||||
|
* documented behaviour and genuinely useful before a fork or an abort -- so
|
||||||
|
* unlike almost everywhere else in this library, a NULL argument is not an
|
||||||
|
* error. It is also the only way to find out that buffered data could not be
|
||||||
|
* written before the process goes away.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_fflush(FILE *stream)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
errno = 0;
|
||||||
|
FAIL_NONZERO_RETURN(e, fflush(stream), AKSL_ERRNO_OR(AKERR_IO),
|
||||||
|
"fflush failed and the buffered data is lost");
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_setvbuf(FILE *stream, char *buf, int mode, size_t size)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream);
|
||||||
|
errno = 0;
|
||||||
|
FAIL_NONZERO_RETURN(e, setvbuf(stream, buf, mode, size), AKSL_ERRNO_OR(AKERR_VALUE),
|
||||||
|
"setvbuf mode %d size %zu", mode, size);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Character and line I/O */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* fgetc(3) folds three outcomes into one int: a byte, the end of the file, and
|
||||||
|
* a read error, the last two both spelled EOF. Split apart here -- *dest is the
|
||||||
|
* byte, AKERR_EOF is the end, and anything else is the error -- so a read loop
|
||||||
|
* can be written without the ferror/feof dance at the bottom of it.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_fgetc(FILE *stream, int *dest)
|
||||||
|
{
|
||||||
|
int c = 0;
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
|
||||||
|
*dest = 0;
|
||||||
|
errno = 0;
|
||||||
|
c = fgetc(stream);
|
||||||
|
if ( c == EOF ) {
|
||||||
|
FAIL_NONZERO_RETURN(e, ferror(stream), AKSL_ERRNO_OR(AKERR_IO), "fgetc failed");
|
||||||
|
FAIL_RETURN(e, AKERR_EOF, "end of stream");
|
||||||
|
}
|
||||||
|
*dest = c;
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_fputc(int c, FILE *stream)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream);
|
||||||
|
errno = 0;
|
||||||
|
FAIL_NONZERO_RETURN(e, (fputc(c, stream) == EOF), AKSL_ERRNO_OR(AKERR_IO),
|
||||||
|
"fputc of 0x%02x failed", (unsigned)c & 0xffu);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pushes one byte back so the next read returns it. One byte is all that is
|
||||||
|
* guaranteed; a second ungetc without a read in between may well fail. */
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_ungetc(int c, FILE *stream)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream);
|
||||||
|
errno = 0;
|
||||||
|
FAIL_NONZERO_RETURN(e, (ungetc(c, stream) == EOF), AKSL_ERRNO_OR(AKERR_IO),
|
||||||
|
"ungetc of 0x%02x failed", (unsigned)c & 0xffu);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* fgets(3) into a bounded buffer, with the length read reported and the three
|
||||||
|
* outcomes separated. A line too long for the buffer is *not* an error -- it is
|
||||||
|
* a short read that leaves the rest of the line in the stream, which is how
|
||||||
|
* fgets works and how a caller reading fixed-size chunks wants it -- but
|
||||||
|
* *len_out lets the caller notice, because a full buffer with no trailing
|
||||||
|
* newline is exactly that case.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_fgets(char *s, size_t size, FILE *stream, size_t *len_out)
|
||||||
|
{
|
||||||
|
char *result = NULL;
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, len_out, AKERR_NULLPOINTER, "s=%p, stream=%p, len_out=%p",
|
||||||
|
(void *)s, (void *)stream, (void *)len_out);
|
||||||
|
*len_out = 0;
|
||||||
|
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, stream=%p, len_out=%p",
|
||||||
|
(void *)s, (void *)stream, (void *)len_out);
|
||||||
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "s=%p, stream=%p, len_out=%p",
|
||||||
|
(void *)s, (void *)stream, (void *)len_out);
|
||||||
|
/* fgets takes an int, and a size that does not fit one is a caller error. */
|
||||||
|
FAIL_NONZERO_RETURN(e, (size == 0 || size > (size_t)INT_MAX), AKERR_VALUE,
|
||||||
|
"size %zu is outside the range fgets accepts", size);
|
||||||
|
s[0] = '\0';
|
||||||
|
errno = 0;
|
||||||
|
result = fgets(s, (int)size, stream);
|
||||||
|
if ( result == NULL ) {
|
||||||
|
FAIL_NONZERO_RETURN(e, ferror(stream), AKSL_ERRNO_OR(AKERR_IO), "fgets failed");
|
||||||
|
FAIL_RETURN(e, AKERR_EOF, "end of stream");
|
||||||
|
}
|
||||||
|
*len_out = strlen(s);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_fputs(const char *s, FILE *stream)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, stream=%p", (void *)s, (void *)stream);
|
||||||
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "s=%p, stream=%p", (void *)s, (void *)stream);
|
||||||
|
errno = 0;
|
||||||
|
FAIL_NONZERO_RETURN(e, (fputs(s, stream) == EOF), AKSL_ERRNO_OR(AKERR_IO), "fputs failed");
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* getline(3) grows *lineptr as needed, so the caller starts with
|
||||||
|
* `char *line = NULL; size_t cap = 0;` and releases the buffer with aksl_free
|
||||||
|
* once the loop is done -- not once per line. *len_out is the length read,
|
||||||
|
* which is what tells an embedded NUL from the end of the line; strlen cannot.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_getline(char **lineptr, size_t *n, FILE *stream, size_t *len_out)
|
||||||
|
{
|
||||||
|
ssize_t got = 0;
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, len_out, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
|
||||||
|
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
|
||||||
|
*len_out = 0;
|
||||||
|
FAIL_ZERO_RETURN(e, lineptr, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
|
||||||
|
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
|
||||||
|
FAIL_ZERO_RETURN(e, n, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
|
||||||
|
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
|
||||||
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
|
||||||
|
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
|
||||||
|
errno = 0;
|
||||||
|
got = getline(lineptr, n, stream);
|
||||||
|
if ( got < 0 ) {
|
||||||
|
FAIL_NONZERO_RETURN(e, ferror(stream), AKSL_ERRNO_OR(AKERR_IO), "getline failed");
|
||||||
|
FAIL_RETURN(e, AKERR_EOF, "end of stream");
|
||||||
|
}
|
||||||
|
*len_out = (size_t)got;
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_getdelim(char **lineptr, size_t *n, int delim,
|
||||||
|
FILE *stream, size_t *len_out)
|
||||||
|
{
|
||||||
|
ssize_t got = 0;
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, len_out, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
|
||||||
|
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
|
||||||
|
*len_out = 0;
|
||||||
|
FAIL_ZERO_RETURN(e, lineptr, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
|
||||||
|
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
|
||||||
|
FAIL_ZERO_RETURN(e, n, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
|
||||||
|
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
|
||||||
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
|
||||||
|
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
|
||||||
|
errno = 0;
|
||||||
|
got = getdelim(lineptr, n, delim, stream);
|
||||||
|
if ( got < 0 ) {
|
||||||
|
FAIL_NONZERO_RETURN(e, ferror(stream), AKSL_ERRNO_OR(AKERR_IO), "getdelim failed");
|
||||||
|
FAIL_RETURN(e, AKERR_EOF, "end of stream");
|
||||||
|
}
|
||||||
|
*len_out = (size_t)got;
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Stream state */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* feof and ferror answer through an out-param, exposed so that a caller never
|
||||||
|
* has to reach into a FILE * itself. They are thin, and that is the point: the
|
||||||
|
* NULL check is the whole value.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_feof(FILE *stream, int *dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
|
||||||
|
*dest = feof(stream);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_ferror(FILE *stream, int *dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
|
||||||
|
*dest = ferror(stream);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_clearerr(FILE *stream)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream);
|
||||||
|
clearerr(stream);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_fileno(FILE *stream, int *dest)
|
||||||
|
{
|
||||||
|
int fd = -1;
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
|
||||||
|
*dest = -1;
|
||||||
|
errno = 0;
|
||||||
|
fd = fileno(stream);
|
||||||
|
FAIL_NONZERO_RETURN(e, (fd < 0), AKSL_ERRNO_OR(EBADF), "stream has no descriptor");
|
||||||
|
*dest = fd;
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Opening by other means */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_freopen(const char *pathname, const char *mode,
|
||||||
|
FILE *stream, FILE **fp)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "pathname=%p, mode=%p, stream=%p, fp=%p",
|
||||||
|
(void *)pathname, (void *)mode, (void *)stream, (void *)fp);
|
||||||
|
*fp = NULL;
|
||||||
|
FAIL_ZERO_RETURN(e, mode, AKERR_NULLPOINTER, "pathname=%p, mode=%p, stream=%p, fp=%p",
|
||||||
|
(void *)pathname, (void *)mode, (void *)stream, (void *)fp);
|
||||||
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "pathname=%p, mode=%p, stream=%p, fp=%p",
|
||||||
|
(void *)pathname, (void *)mode, (void *)stream, (void *)fp);
|
||||||
|
/*
|
||||||
|
* pathname NULL is legal and means "reopen the same file with a new mode",
|
||||||
|
* which is the one thing freopen can do that fopen cannot -- so it is not
|
||||||
|
* checked, unlike aksl_fopen's.
|
||||||
|
*/
|
||||||
|
errno = 0;
|
||||||
|
*fp = freopen(pathname, mode, stream);
|
||||||
|
FAIL_ZERO_RETURN(e, *fp, AKSL_ERRNO_OR(AKERR_IO), "%s", pathname != NULL ? pathname : "(same file)");
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_fdopen(int fd, const char *mode, FILE **fp)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "mode=%p, fp=%p", (void *)mode, (void *)fp);
|
||||||
|
*fp = NULL;
|
||||||
|
FAIL_ZERO_RETURN(e, mode, AKERR_NULLPOINTER, "mode=%p, fp=%p", (void *)mode, (void *)fp);
|
||||||
|
FAIL_NONZERO_RETURN(e, (fd < 0), AKERR_VALUE, "fd=%d is not a descriptor", fd);
|
||||||
|
errno = 0;
|
||||||
|
*fp = fdopen(fd, mode);
|
||||||
|
FAIL_ZERO_RETURN(e, *fp, AKSL_ERRNO_OR(AKERR_IO), "fd=%d mode=%s", fd, mode);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_tmpfile(FILE **fp)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "fp=%p", (void *)fp);
|
||||||
|
*fp = NULL;
|
||||||
|
errno = 0;
|
||||||
|
*fp = tmpfile();
|
||||||
|
FAIL_ZERO_RETURN(e, *fp, AKSL_ERRNO_OR(AKERR_IO), "could not create a temporary file");
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Formatted input */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The scanf family's return value is the number of conversions that succeeded,
|
||||||
|
* which the caller is expected to compare against the number it wrote in the
|
||||||
|
* format string -- by hand, from memory, at every call site. Getting that wrong
|
||||||
|
* leaves the unassigned arguments holding whatever they held before, which for
|
||||||
|
* the usual uninitialised local is anything at all.
|
||||||
|
*
|
||||||
|
* So `expected` is an argument: say how many conversions must succeed, and
|
||||||
|
* anything less is AKERR_VALUE with both counts in the message. Pass 0 to opt
|
||||||
|
* out and read the count from *assigned yourself.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_vsscanf(const char *str, const char *format,
|
||||||
|
int expected, int *assigned, va_list args)
|
||||||
|
{
|
||||||
|
int got = 0;
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, assigned, AKERR_NULLPOINTER, "str=%p, format=%p, assigned=%p",
|
||||||
|
(void *)str, (void *)format, (void *)assigned);
|
||||||
|
*assigned = 0;
|
||||||
|
FAIL_ZERO_RETURN(e, str, AKERR_NULLPOINTER, "str=%p, format=%p, assigned=%p",
|
||||||
|
(void *)str, (void *)format, (void *)assigned);
|
||||||
|
FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "str=%p, format=%p, assigned=%p",
|
||||||
|
(void *)str, (void *)format, (void *)assigned);
|
||||||
|
errno = 0;
|
||||||
|
got = vsscanf(str, format, args);
|
||||||
|
FAIL_NONZERO_RETURN(e, (got == EOF), AKSL_ERRNO_OR(AKERR_VALUE),
|
||||||
|
"no conversions were possible on \"%s\"", str);
|
||||||
|
*assigned = got;
|
||||||
|
FAIL_NONZERO_RETURN(e, (expected > 0 && got < expected), AKERR_VALUE,
|
||||||
|
"%d of %d conversions succeeded on \"%s\"", got, expected, str);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_sscanf(const char *str, const char *format,
|
||||||
|
int expected, int *assigned, ...)
|
||||||
|
{
|
||||||
|
va_list args;
|
||||||
|
akerr_ErrorContext *raised = NULL;
|
||||||
|
|
||||||
|
va_start(args, assigned);
|
||||||
|
raised = aksl_vsscanf(str, format, expected, assigned, args);
|
||||||
|
va_end(args);
|
||||||
|
return raised;
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_vfscanf(FILE *stream, const char *format,
|
||||||
|
int expected, int *assigned, va_list args)
|
||||||
|
{
|
||||||
|
int got = 0;
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, assigned, AKERR_NULLPOINTER, "stream=%p, format=%p, assigned=%p",
|
||||||
|
(void *)stream, (void *)format, (void *)assigned);
|
||||||
|
*assigned = 0;
|
||||||
|
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, format=%p, assigned=%p",
|
||||||
|
(void *)stream, (void *)format, (void *)assigned);
|
||||||
|
FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "stream=%p, format=%p, assigned=%p",
|
||||||
|
(void *)stream, (void *)format, (void *)assigned);
|
||||||
|
errno = 0;
|
||||||
|
got = vfscanf(stream, format, args);
|
||||||
|
if ( got == EOF ) {
|
||||||
|
FAIL_NONZERO_RETURN(e, ferror(stream), AKSL_ERRNO_OR(AKERR_IO), "fscanf failed");
|
||||||
|
FAIL_RETURN(e, AKERR_EOF, "end of stream before any conversion");
|
||||||
|
}
|
||||||
|
*assigned = got;
|
||||||
|
FAIL_NONZERO_RETURN(e, (expected > 0 && got < expected), AKERR_VALUE,
|
||||||
|
"%d of %d conversions succeeded", got, expected);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_fscanf(FILE *stream, const char *format,
|
||||||
|
int expected, int *assigned, ...)
|
||||||
|
{
|
||||||
|
va_list args;
|
||||||
|
akerr_ErrorContext *raised = NULL;
|
||||||
|
|
||||||
|
va_start(args, assigned);
|
||||||
|
raised = aksl_vfscanf(stream, format, expected, assigned, args);
|
||||||
|
va_end(args);
|
||||||
|
return raised;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* stdin, for symmetry with aksl_printf writing to stdout. */
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_scanf(const char *format,
|
||||||
|
int expected, int *assigned, ...)
|
||||||
|
{
|
||||||
|
va_list args;
|
||||||
|
akerr_ErrorContext *raised = NULL;
|
||||||
|
|
||||||
|
va_start(args, assigned);
|
||||||
|
raised = aksl_vfscanf(stdin, format, expected, assigned, args);
|
||||||
|
va_end(args);
|
||||||
|
return raised;
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_vscanf(const char *format, int expected,
|
||||||
|
int *assigned, va_list args)
|
||||||
|
{
|
||||||
|
return aksl_vfscanf(stdin, format, expected, assigned, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Files */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_remove(const char *pathname)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, pathname, AKERR_NULLPOINTER, "pathname=%p", (void *)pathname);
|
||||||
|
errno = 0;
|
||||||
|
FAIL_NONZERO_RETURN(e, remove(pathname), AKSL_ERRNO_OR(AKERR_IO), "%s", pathname);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_rename(const char *oldpath, const char *newpath)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, oldpath, AKERR_NULLPOINTER, "oldpath=%p, newpath=%p",
|
||||||
|
(void *)oldpath, (void *)newpath);
|
||||||
|
FAIL_ZERO_RETURN(e, newpath, AKERR_NULLPOINTER, "oldpath=%p, newpath=%p",
|
||||||
|
(void *)oldpath, (void *)newpath);
|
||||||
|
errno = 0;
|
||||||
|
FAIL_NONZERO_RETURN(e, rename(oldpath, newpath), AKSL_ERRNO_OR(AKERR_IO),
|
||||||
|
"%s -> %s", oldpath, newpath);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* mkstemp and mkdtemp both rewrite the template in place, so the template must
|
||||||
|
* be a writable buffer ending in exactly six X characters -- a string literal
|
||||||
|
* is a segfault, and that is checked here rather than left to the kernel. The
|
||||||
|
* caller owns the resulting file or directory, including removing it.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_mkstemp(char *template_, int *fd)
|
||||||
|
{
|
||||||
|
size_t len = 0;
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, fd, AKERR_NULLPOINTER, "template=%p, fd=%p", (void *)template_, (void *)fd);
|
||||||
|
*fd = -1;
|
||||||
|
FAIL_ZERO_RETURN(e, template_, AKERR_NULLPOINTER, "template=%p, fd=%p",
|
||||||
|
(void *)template_, (void *)fd);
|
||||||
|
len = strlen(template_);
|
||||||
|
FAIL_NONZERO_RETURN(e, (len < 6 || strcmp(template_ + len - 6, "XXXXXX") != 0),
|
||||||
|
AKERR_VALUE,
|
||||||
|
"template \"%s\" must end in six literal X characters", template_);
|
||||||
|
errno = 0;
|
||||||
|
*fd = mkstemp(template_);
|
||||||
|
FAIL_NONZERO_RETURN(e, (*fd < 0), AKSL_ERRNO_OR(AKERR_IO), "template \"%s\"", template_);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_mkdtemp(char *template_)
|
||||||
|
{
|
||||||
|
size_t len = 0;
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, template_, AKERR_NULLPOINTER, "template=%p", (void *)template_);
|
||||||
|
len = strlen(template_);
|
||||||
|
FAIL_NONZERO_RETURN(e, (len < 6 || strcmp(template_ + len - 6, "XXXXXX") != 0),
|
||||||
|
AKERR_VALUE,
|
||||||
|
"template \"%s\" must end in six literal X characters", template_);
|
||||||
|
errno = 0;
|
||||||
|
FAIL_ZERO_RETURN(e, mkdtemp(template_), AKSL_ERRNO_OR(AKERR_IO),
|
||||||
|
"template \"%s\"", template_);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
486
src/string.c
Normal file
486
src/string.c
Normal file
@@ -0,0 +1,486 @@
|
|||||||
|
/*
|
||||||
|
* string.h wrappers -- TODO.md section 3.1.
|
||||||
|
*
|
||||||
|
* This is the section akbasic needed most and could not have: across its source
|
||||||
|
* it calls strlen 37 times, strcmp 16, strncpy 15 and strstr once, every one of
|
||||||
|
* them raw because there was nothing here to call instead. Ten of those sites
|
||||||
|
* are the same idiom written out by hand -- a length check, then strncpy, then
|
||||||
|
* an explicit NUL -- which is exactly the "truncation reported as an error
|
||||||
|
* rather than silently accepted" that TODO.md 3.1 asks for.
|
||||||
|
*
|
||||||
|
* Two conventions run through the whole file.
|
||||||
|
*
|
||||||
|
* The copying functions take the size of the destination, and the ones named
|
||||||
|
* after libc functions that do not take one take it anyway. strcpy(3) and
|
||||||
|
* strcat(3) cannot be called safely without knowing how much room the
|
||||||
|
* destination has, and a wrapper that accepts the same arguments as strcpy
|
||||||
|
* would be an error-handling wrapper around a buffer overflow. Truncation is
|
||||||
|
* AKERR_OUTOFBOUNDS: it is the failure these functions exist to report, and
|
||||||
|
* nothing is written to the destination when it happens, so a caller who
|
||||||
|
* ignores the status does not get a half-copied string either. aksl_strncpy
|
||||||
|
* also always terminates, which strncpy(3) famously does not.
|
||||||
|
*
|
||||||
|
* The searching functions answer through an out-param and treat "not found" as
|
||||||
|
* a successful answer of NULL rather than as an error -- absent is an ordinary
|
||||||
|
* answer to "where is this", and a library that raised on it would have every
|
||||||
|
* caller handling a non-error as an error.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <akstdlib.h>
|
||||||
|
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <strings.h>
|
||||||
|
|
||||||
|
#include "aksl_internal.h"
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Length */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strlen(const char *s, size_t *dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||||
|
*dest = strlen(s);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* strnlen(3) stops at maxlen whether or not it found a terminator, so a result
|
||||||
|
* equal to maxlen means "at least this long" rather than "this long". That
|
||||||
|
* ambiguity is the reason to use it -- it is how you measure a buffer that may
|
||||||
|
* not be terminated -- so it is reported rather than hidden: *dest is the
|
||||||
|
* length and the call succeeds either way.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strnlen(const char *s, size_t maxlen, size_t *dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||||
|
*dest = strnlen(s, maxlen);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Copying and concatenation */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Bounded copy. dstsize is the whole destination buffer, terminator included,
|
||||||
|
* so `char buf[64]` pairs with `sizeof(buf)`. A source that does not fit is
|
||||||
|
* AKERR_OUTOFBOUNDS and the destination is left as an empty string rather than
|
||||||
|
* as a truncated one -- a caller who ignores the error gets nothing, which is
|
||||||
|
* far easier to notice than a plausible-looking prefix.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcpy(char *dst, size_t dstsize, const char *src)
|
||||||
|
{
|
||||||
|
size_t len = 0;
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
|
||||||
|
FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
|
||||||
|
FAIL_ZERO_RETURN(e, dstsize, AKERR_VALUE, "dstsize=0 leaves no room for a terminator");
|
||||||
|
dst[0] = '\0';
|
||||||
|
len = strlen(src);
|
||||||
|
FAIL_NONZERO_RETURN(e, (len >= dstsize), AKERR_OUTOFBOUNDS,
|
||||||
|
"source is %zu bytes and the destination holds %zu including the terminator",
|
||||||
|
len, dstsize);
|
||||||
|
memcpy(dst, src, len + 1);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* At most n bytes of src, and always terminated.
|
||||||
|
*
|
||||||
|
* strncpy(3) does two surprising things that this does not: it leaves the
|
||||||
|
* destination unterminated when the source is at least n bytes long, and it
|
||||||
|
* pads the remainder with NULs when the source is shorter, which turns a short
|
||||||
|
* copy into a full-length write. Here n bounds how much of the source is
|
||||||
|
* considered, dstsize bounds the write, and the result is always a C string.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strncpy(char *dst, size_t dstsize, const char *src, size_t n)
|
||||||
|
{
|
||||||
|
size_t len = 0;
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
|
||||||
|
FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
|
||||||
|
FAIL_ZERO_RETURN(e, dstsize, AKERR_VALUE, "dstsize=0 leaves no room for a terminator");
|
||||||
|
dst[0] = '\0';
|
||||||
|
len = strnlen(src, n);
|
||||||
|
FAIL_NONZERO_RETURN(e, (len >= dstsize), AKERR_OUTOFBOUNDS,
|
||||||
|
"%zu bytes to copy and the destination holds %zu including the terminator",
|
||||||
|
len, dstsize);
|
||||||
|
memcpy(dst, src, len);
|
||||||
|
dst[len] = '\0';
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Bounded append. dstsize is again the whole buffer, so the room actually
|
||||||
|
* available is dstsize minus what is already in there. The destination must
|
||||||
|
* already be a terminated string within dstsize; if it is not, that is
|
||||||
|
* AKERR_VALUE rather than a walk off the end looking for a NUL that is not
|
||||||
|
* there.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcat(char *dst, size_t dstsize, const char *src)
|
||||||
|
{
|
||||||
|
size_t used = 0;
|
||||||
|
size_t len = 0;
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
|
||||||
|
FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
|
||||||
|
FAIL_ZERO_RETURN(e, dstsize, AKERR_VALUE, "dstsize=0 leaves no room for a terminator");
|
||||||
|
used = strnlen(dst, dstsize);
|
||||||
|
FAIL_NONZERO_RETURN(e, (used == dstsize), AKERR_VALUE,
|
||||||
|
"destination is not terminated within its %zu bytes", dstsize);
|
||||||
|
len = strlen(src);
|
||||||
|
FAIL_NONZERO_RETURN(e, (used + len >= dstsize), AKERR_OUTOFBOUNDS,
|
||||||
|
"%zu bytes in use plus %zu to append exceeds the %zu-byte destination",
|
||||||
|
used, len, dstsize);
|
||||||
|
memcpy(dst + used, src, len + 1);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strncat(char *dst, size_t dstsize, const char *src, size_t n)
|
||||||
|
{
|
||||||
|
size_t used = 0;
|
||||||
|
size_t len = 0;
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
|
||||||
|
FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
|
||||||
|
FAIL_ZERO_RETURN(e, dstsize, AKERR_VALUE, "dstsize=0 leaves no room for a terminator");
|
||||||
|
used = strnlen(dst, dstsize);
|
||||||
|
FAIL_NONZERO_RETURN(e, (used == dstsize), AKERR_VALUE,
|
||||||
|
"destination is not terminated within its %zu bytes", dstsize);
|
||||||
|
len = strnlen(src, n);
|
||||||
|
FAIL_NONZERO_RETURN(e, (used + len >= dstsize), AKERR_OUTOFBOUNDS,
|
||||||
|
"%zu bytes in use plus %zu to append exceeds the %zu-byte destination",
|
||||||
|
used, len, dstsize);
|
||||||
|
memcpy(dst + used, src, len);
|
||||||
|
dst[used + len] = '\0';
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Duplication */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* *dest is the caller's, to release with aksl_free or aksl_freep. */
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strdup(const char *s, char **dest)
|
||||||
|
{
|
||||||
|
char *copy = NULL;
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||||
|
*dest = NULL;
|
||||||
|
errno = 0;
|
||||||
|
copy = strdup(s);
|
||||||
|
FAIL_ZERO_RETURN(e, copy, AKSL_ERRNO_OR(ENOMEM), "%zu bytes", strlen(s) + 1);
|
||||||
|
*dest = copy;
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strndup(const char *s, size_t n, char **dest)
|
||||||
|
{
|
||||||
|
char *copy = NULL;
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||||
|
*dest = NULL;
|
||||||
|
errno = 0;
|
||||||
|
copy = strndup(s, n);
|
||||||
|
FAIL_ZERO_RETURN(e, copy, AKSL_ERRNO_OR(ENOMEM), "%zu bytes", n + 1);
|
||||||
|
*dest = copy;
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Comparison */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The comparison result goes through *dest because the return value is spoken
|
||||||
|
* for by the error context. The sign is the usual one: negative, zero or
|
||||||
|
* positive as a sorts before, equal to, or after b.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcmp(const char *a, const char *b, int *dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||||
|
*dest = strcmp(a, b);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strncmp(const char *a, const char *b, size_t n, int *dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||||
|
*dest = strncmp(a, b, n);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Case-insensitive comparison. akbasic folds case by hand at three sites --
|
||||||
|
* twice with an open-coded loop and once through <ctype.h> -- because BASIC
|
||||||
|
* verb and function names are case-insensitive while variable names are not.
|
||||||
|
* All three of those are this call.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcasecmp(const char *a, const char *b, int *dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||||
|
*dest = strcasecmp(a, b);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strncasecmp(const char *a, const char *b, size_t n, int *dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||||
|
*dest = strncasecmp(a, b, n);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Locale-aware collation. strcoll(3) has no error return of its own, so a
|
||||||
|
* malformed multibyte sequence in the current locale shows up only as errno
|
||||||
|
* being set -- which is why errno is cleared first and consulted after.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcoll(const char *a, const char *b, int *dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||||
|
errno = 0;
|
||||||
|
*dest = strcoll(a, b);
|
||||||
|
FAIL_NONZERO_RETURN(e, errno, errno, "strcoll failed in the current locale");
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Searching */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Every function in this section writes NULL to *dest and succeeds when there
|
||||||
|
* is nothing to find. "Absent" is an answer, not a failure; raising on it would
|
||||||
|
* make every caller handle a non-error as an error, and the pool slot that
|
||||||
|
* error consumed would be pure waste.
|
||||||
|
*
|
||||||
|
* *dest points into the caller's own string, so it lives exactly as long as the
|
||||||
|
* string does and must not be freed.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strchr(const char *s, int c, char **dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||||
|
*dest = strchr(s, c);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strrchr(const char *s, int c, char **dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||||
|
*dest = strrchr(s, c);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strstr(const char *haystack, const char *needle, char **dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, haystack, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p",
|
||||||
|
(void *)haystack, (void *)needle, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, needle, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p",
|
||||||
|
(void *)haystack, (void *)needle, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p",
|
||||||
|
(void *)haystack, (void *)needle, (void *)dest);
|
||||||
|
*dest = strstr(haystack, needle);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Case-insensitive search. strcasestr(3) is a GNU extension that also exists on
|
||||||
|
* the BSDs and musl, but it is not in any standard, so it is open-coded here
|
||||||
|
* rather than depending on _GNU_SOURCE reaching every consumer's build. The
|
||||||
|
* naive scan is the same complexity as glibc's fallback and this is not the hot
|
||||||
|
* path in anything.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcasestr(const char *haystack, const char *needle, char **dest)
|
||||||
|
{
|
||||||
|
size_t hlen = 0;
|
||||||
|
size_t nlen = 0;
|
||||||
|
size_t i = 0;
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, haystack, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p",
|
||||||
|
(void *)haystack, (void *)needle, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, needle, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p",
|
||||||
|
(void *)haystack, (void *)needle, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p",
|
||||||
|
(void *)haystack, (void *)needle, (void *)dest);
|
||||||
|
*dest = NULL;
|
||||||
|
nlen = strlen(needle);
|
||||||
|
/* An empty needle matches at the start, as strstr(3) has it. */
|
||||||
|
if ( nlen == 0 ) {
|
||||||
|
*dest = (char *)haystack;
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
hlen = strlen(haystack);
|
||||||
|
if ( nlen <= hlen ) {
|
||||||
|
for ( i = 0; i <= hlen - nlen; i++ ) {
|
||||||
|
if ( strncasecmp(haystack + i, needle, nlen) == 0 ) {
|
||||||
|
*dest = (char *)(haystack + i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strpbrk(const char *s, const char *accept, char **dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p",
|
||||||
|
(void *)s, (void *)accept, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, accept, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p",
|
||||||
|
(void *)s, (void *)accept, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p",
|
||||||
|
(void *)s, (void *)accept, (void *)dest);
|
||||||
|
*dest = strpbrk(s, accept);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strspn(const char *s, const char *accept, size_t *dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p",
|
||||||
|
(void *)s, (void *)accept, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, accept, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p",
|
||||||
|
(void *)s, (void *)accept, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p",
|
||||||
|
(void *)s, (void *)accept, (void *)dest);
|
||||||
|
*dest = strspn(s, accept);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcspn(const char *s, const char *reject, size_t *dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, reject=%p, dest=%p",
|
||||||
|
(void *)s, (void *)reject, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, reject, AKERR_NULLPOINTER, "s=%p, reject=%p, dest=%p",
|
||||||
|
(void *)s, (void *)reject, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, reject=%p, dest=%p",
|
||||||
|
(void *)s, (void *)reject, (void *)dest);
|
||||||
|
*dest = strcspn(s, reject);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Tokenising */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The reentrant tokeniser only. strtok(3) keeps its state in a hidden static,
|
||||||
|
* which makes two interleaved tokenisations silently corrupt each other and
|
||||||
|
* makes any use from a thread a bug, so it is not wrapped here at all -- see
|
||||||
|
* the note on threads in README.md.
|
||||||
|
*
|
||||||
|
* Running out of tokens is success with *dest NULL, for the same reason a failed
|
||||||
|
* search is: it is how the loop ends, not a fault.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strtok_r(char *str, const char *delim, char **saveptr, char **dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, delim, AKERR_NULLPOINTER, "delim=%p, saveptr=%p, dest=%p",
|
||||||
|
(void *)delim, (void *)saveptr, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, saveptr, AKERR_NULLPOINTER, "delim=%p, saveptr=%p, dest=%p",
|
||||||
|
(void *)delim, (void *)saveptr, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "delim=%p, saveptr=%p, dest=%p",
|
||||||
|
(void *)delim, (void *)saveptr, (void *)dest);
|
||||||
|
*dest = strtok_r(str, delim, saveptr);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* strsep(3) differs from strtok_r in returning empty tokens between adjacent
|
||||||
|
* delimiters, which is what you want for parsing "a::b" as three fields rather
|
||||||
|
* than two. It advances *stringp itself and sets it to NULL when done.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strsep(char **stringp, const char *delim, char **dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, stringp, AKERR_NULLPOINTER, "stringp=%p, delim=%p, dest=%p",
|
||||||
|
(void *)stringp, (void *)delim, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, delim, AKERR_NULLPOINTER, "stringp=%p, delim=%p, dest=%p",
|
||||||
|
(void *)stringp, (void *)delim, (void *)dest);
|
||||||
|
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stringp=%p, delim=%p, dest=%p",
|
||||||
|
(void *)stringp, (void *)delim, (void *)dest);
|
||||||
|
*dest = strsep(stringp, delim);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* errno messages */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The message for a status, into the caller's own buffer.
|
||||||
|
*
|
||||||
|
* Deliberately not built on strerror_r(3). There are two incompatible functions
|
||||||
|
* by that name -- the XSI one returns int, the GNU one returns char * and may
|
||||||
|
* not touch the buffer at all -- and which one a translation unit gets depends
|
||||||
|
* on feature-test macros that a consumer of this library cannot influence from
|
||||||
|
* in here. strerror(3) itself is not thread-safe, so that is no way out either.
|
||||||
|
*
|
||||||
|
* libakerror already carries the full errno name table (its generrno.sh builds
|
||||||
|
* one at configure time), and its registry also knows the names of this
|
||||||
|
* library's own non-errno statuses -- AKERR_NULLPOINTER, AKERR_ITERATOR_BREAK
|
||||||
|
* and the rest -- which strerror_r could never name. So the lookup goes there,
|
||||||
|
* and a status neither table recognises comes back as its own number rather
|
||||||
|
* than as the bare string "Unknown Error", which tells the reader nothing.
|
||||||
|
*
|
||||||
|
* The message is truncated to nothing rather than to a prefix if it does not
|
||||||
|
* fit, on the same principle as aksl_strcpy above.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *aksl_strerror(int status, char *buf, size_t buflen)
|
||||||
|
{
|
||||||
|
const char *name = NULL;
|
||||||
|
int needed = 0;
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, buf, AKERR_NULLPOINTER, "buf=%p", (void *)buf);
|
||||||
|
FAIL_ZERO_RETURN(e, buflen, AKERR_VALUE, "buflen=0 leaves no room for a terminator");
|
||||||
|
buf[0] = '\0';
|
||||||
|
name = akerr_name_for_status(status, NULL);
|
||||||
|
/*
|
||||||
|
* akerr_name_for_status never returns NULL; it returns this exact string
|
||||||
|
* for anything it does not have a name for.
|
||||||
|
*/
|
||||||
|
if ( name == NULL || strcmp(name, "Unknown Error") == 0 ) {
|
||||||
|
needed = snprintf(buf, buflen, "Unknown status %d", status);
|
||||||
|
} else {
|
||||||
|
needed = snprintf(buf, buflen, "%s", name);
|
||||||
|
}
|
||||||
|
FAIL_NONZERO_RETURN(e, (needed < 0), AKERR_IO, "could not format status %d", status);
|
||||||
|
if ( (size_t)needed >= buflen ) {
|
||||||
|
buf[0] = '\0';
|
||||||
|
FAIL_RETURN(e, AKERR_OUTOFBOUNDS,
|
||||||
|
"the message for status %d needs %d bytes and the buffer holds %zu",
|
||||||
|
status, needed + 1, buflen);
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
4
test.sh
Normal file
4
test.sh
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
cmake -S . -B build
|
||||||
|
cmake --build build
|
||||||
|
ctest --test-dir build --output-on-failure --output-junit "$(pwd)/ctest-junit.xml"
|
||||||
|
python3 scripts/mutation_test.py --target src/error.c --junit mutation-junit.xml --threshold 65
|
||||||
356
tests/aksl_capture.h
Normal file
356
tests/aksl_capture.h
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
#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];
|
||||||
|
/*
|
||||||
|
* The source location the error was raised from. Captured so that
|
||||||
|
* tests/test_pool.c can assert an error came from the wrapper it names rather
|
||||||
|
* than from somewhere further down -- an error that reports the wrong origin is
|
||||||
|
* worse than useless when the only debugging you get is a log file after the
|
||||||
|
* fact, which is the stated reason this library exists.
|
||||||
|
*/
|
||||||
|
static char aksl_last_file[AKERR_MAX_ERROR_FNAME_LENGTH];
|
||||||
|
static int aksl_last_line = 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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';
|
||||||
|
aksl_last_file[0] = '\0';
|
||||||
|
aksl_last_line = 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);
|
||||||
|
snprintf(aksl_last_file, sizeof(aksl_last_file), "%s", e->fname);
|
||||||
|
aksl_last_line = e->lineno;
|
||||||
|
/*
|
||||||
|
* 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'; \
|
||||||
|
aksl_last_file[0] = '\0'; \
|
||||||
|
aksl_last_line = 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
|
||||||
21
tests/consumer/CMakeLists.txt
Normal file
21
tests/consumer/CMakeLists.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# A consumer of the *installed* package, not of the build tree.
|
||||||
|
#
|
||||||
|
# The test suite links the build tree, so it says nothing about whether an
|
||||||
|
# installed libakstdlib is usable -- and the two have come apart before. This is
|
||||||
|
# the smallest thing that exercises the paths only an install has: the version
|
||||||
|
# file (find_package with a version request), akstdlibConfig.cmake's
|
||||||
|
# find_dependency(akerror), and the exported akstdlib::akstdlib target.
|
||||||
|
#
|
||||||
|
# Configured standalone against CMAKE_PREFIX_PATH rather than being part of the
|
||||||
|
# main build, because being part of the main build is exactly what would let it
|
||||||
|
# pass without testing anything.
|
||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
project(akstdlib_consumer LANGUAGES C)
|
||||||
|
|
||||||
|
# A version request, so akstdlibConfigVersion.cmake has to be present and has to
|
||||||
|
# agree. Deliberately MAJOR.MINOR rather than the full version: that is the
|
||||||
|
# compatibility promise the soname makes, so it is the one worth asserting.
|
||||||
|
find_package(akstdlib 0.2 REQUIRED)
|
||||||
|
|
||||||
|
add_executable(consumer main.c)
|
||||||
|
target_link_libraries(consumer PRIVATE akstdlib::akstdlib)
|
||||||
73
tests/consumer/main.c
Normal file
73
tests/consumer/main.c
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* Smoke test for the installed package. See CMakeLists.txt beside this file.
|
||||||
|
*
|
||||||
|
* Deliberately touches one function from each translation unit -- the version
|
||||||
|
* entry points, a string buffer, a formatted append and a print -- so that a
|
||||||
|
* library installed with a source file missing from its link line fails here
|
||||||
|
* rather than in whatever consumer discovers it next.
|
||||||
|
*
|
||||||
|
* Returns a distinct status per step, so a CI failure names the step.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <akstdlib.h>
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
akerr_ErrorContext *e = NULL;
|
||||||
|
aksl_StrBuf buf;
|
||||||
|
const char *text = NULL;
|
||||||
|
size_t len = 0;
|
||||||
|
uint32_t hash = 0;
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
/* The header and the .so have to agree, which is the whole point of the
|
||||||
|
* version machinery: the soname catches this first in normal use, and this
|
||||||
|
* catches a hand-install that bypassed it. */
|
||||||
|
e = AKSL_VERSION_CHECK();
|
||||||
|
if ( e != NULL ) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* src/collections.c */
|
||||||
|
e = aksl_strbuf_init(&buf, 0);
|
||||||
|
if ( e != NULL ) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
e = aksl_strbuf_appendf(&buf, "libakstdlib %s, soname %s",
|
||||||
|
aksl_version_string(), aksl_version_soname());
|
||||||
|
if ( e != NULL ) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
e = aksl_strbuf_cstr(&buf, &text);
|
||||||
|
if ( e != NULL ) {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* src/string.c */
|
||||||
|
e = aksl_strlen(text, &len);
|
||||||
|
if ( e != NULL ) {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* src/stdlib.c */
|
||||||
|
e = aksl_strhash_djb2(text, len, &hash);
|
||||||
|
if ( e != NULL ) {
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
e = aksl_printf(&count, "%s (%zu bytes, djb2 %u)\n", text, len, hash);
|
||||||
|
if ( e != NULL ) {
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* src/stream.c */
|
||||||
|
e = aksl_fflush(NULL);
|
||||||
|
if ( e != NULL ) {
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
e = aksl_strbuf_free(&buf);
|
||||||
|
if ( e != NULL ) {
|
||||||
|
return 9;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
26
tests/negative/format_mismatch.c
Normal file
26
tests/negative/format_mismatch.c
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* NEGATIVE COMPILE TEST -- TODO.md sections 1.3 and 2.2.5.
|
||||||
|
*
|
||||||
|
* This file must NOT compile. It is built by the CTest entry
|
||||||
|
* `negative_format_mismatch` with -Werror, and that test is marked WILL_FAIL.
|
||||||
|
*
|
||||||
|
* What it asserts: the format attributes on the variadic wrappers are attached
|
||||||
|
* and effective. Going through a wrapper is exactly how a caller loses the
|
||||||
|
* compile-time format checking it would have had calling printf(3) directly --
|
||||||
|
* printf("%d", "str") is caught and an unattributed wrapper's equivalent is not.
|
||||||
|
* AKSL_PRINTF_FORMAT is what restores it, and this is what proves it is there.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <akstdlib.h>
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
char buf[64];
|
||||||
|
int count = 0;
|
||||||
|
akerr_ErrorContext *raised = NULL;
|
||||||
|
|
||||||
|
/* %d against a string. This is the line that must not build. */
|
||||||
|
raised = aksl_snprintf(&count, buf, sizeof(buf), "%d", "not an int");
|
||||||
|
|
||||||
|
return raised == NULL ? 0 : 1;
|
||||||
|
}
|
||||||
26
tests/negative/noignore.c
Normal file
26
tests/negative/noignore.c
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* NEGATIVE COMPILE TEST -- TODO.md section 1.9.
|
||||||
|
*
|
||||||
|
* This file must NOT compile. It is built by the CTest entry `negative_noignore`
|
||||||
|
* with -Werror, and that test is marked WILL_FAIL, so a successful build is a
|
||||||
|
* test failure.
|
||||||
|
*
|
||||||
|
* What it asserts: AKERR_NOIGNORE is actually attached and actually effective.
|
||||||
|
* Every entry point in this library returns an error context the caller must
|
||||||
|
* look at, and the whole design rests on discarding one being hard rather than
|
||||||
|
* merely inadvisable. AKERR_NOIGNORE expands to warn_unused_result, which does
|
||||||
|
* nothing at all if it is dropped from a declaration during a refactor -- and
|
||||||
|
* nothing about the resulting library would look wrong.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <akstdlib.h>
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
void *ptr = NULL;
|
||||||
|
|
||||||
|
/* Discarding the returned context. This is the line that must not build. */
|
||||||
|
aksl_malloc(32, &ptr);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
1139
tests/test_collections.c
Normal file
1139
tests/test_collections.c
Normal file
File diff suppressed because it is too large
Load Diff
324
tests/test_convert.c
Normal file
324
tests/test_convert.c
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
/*
|
||||||
|
* String -> number wrappers: aksl_atoi / atol / atoll / atof.
|
||||||
|
*
|
||||||
|
* TODO.md section 1.4, complete. The cases that used to live in
|
||||||
|
* tests/test_convert_strict.c -- registered as a known failure because the ato*
|
||||||
|
* family had no error channel at all (2.1.5) -- are folded back in here now that
|
||||||
|
* they pass: non-numeric input, empty input, trailing junk and overflow are all
|
||||||
|
* errors rather than silent wrong answers.
|
||||||
|
*
|
||||||
|
* The strto* family these are built on is tested in tests/test_strto.c.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <limits.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The whole reason 2.1.5 mattered. atoi("not a number") returned success and 0,
|
||||||
|
* so a caller could not tell a parse failure from a legitimate zero -- and
|
||||||
|
* akbasic had to write ~60 lines of its own strtoll/strtod wrapper to avoid
|
||||||
|
* turning four diagnosable BASIC errors into four wrong answers.
|
||||||
|
*/
|
||||||
|
static int test_non_numeric_input_is_a_value_error(void)
|
||||||
|
{
|
||||||
|
int out = 99;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_atoi("not a number", &out),
|
||||||
|
AKERR_VALUE, "no digits");
|
||||||
|
/* *dest is 0 on every failure path, never left holding a partial answer. */
|
||||||
|
AKSL_CHECK(out == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_empty_input_is_a_value_error(void)
|
||||||
|
{
|
||||||
|
int out = 99;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_atoi("", &out), AKERR_VALUE);
|
||||||
|
AKSL_CHECK(out == 0);
|
||||||
|
AKSL_CHECK_STATUS(aksl_atoi(" ", &out), AKERR_VALUE);
|
||||||
|
AKSL_CHECK(out == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_trailing_junk_is_a_value_error(void)
|
||||||
|
{
|
||||||
|
int out = 99;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_atoi("12abc", &out),
|
||||||
|
AKERR_VALUE, "trailing junk");
|
||||||
|
AKSL_CHECK(out == 0);
|
||||||
|
/* Trailing whitespace counts as junk too: the whole string must be a number. */
|
||||||
|
AKSL_CHECK_STATUS(aksl_atoi("12 ", &out), AKERR_VALUE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* "0x10" through the ato* forms is base 10, so parsing stops at the 'x' and the
|
||||||
|
* rest is trailing junk. TODO.md 1.4 left this open; the answer is that the
|
||||||
|
* prefix-honouring parse is aksl_strtol(nptr, NULL, 0, &dest), and tests/
|
||||||
|
* test_strto.c holds that half.
|
||||||
|
*/
|
||||||
|
static int test_hex_literal_is_rejected_by_the_base_10_forms(void)
|
||||||
|
{
|
||||||
|
int out = 99;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_atoi("0x10", &out), AKERR_VALUE);
|
||||||
|
AKSL_CHECK(out == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_overflow_is_erange(void)
|
||||||
|
{
|
||||||
|
int out = 99;
|
||||||
|
long lout = 99;
|
||||||
|
long long llout = 99;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_atoi("99999999999999999999", &out),
|
||||||
|
ERANGE, "out of range");
|
||||||
|
AKSL_CHECK(out == 0);
|
||||||
|
AKSL_CHECK_STATUS(aksl_atol("99999999999999999999999999", &lout), ERANGE);
|
||||||
|
AKSL_CHECK(lout == 0);
|
||||||
|
AKSL_CHECK_STATUS(aksl_atoll("99999999999999999999999999", &llout), ERANGE);
|
||||||
|
AKSL_CHECK(llout == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* aksl_atoi narrows from long to int, so a value that fits a long and not an int
|
||||||
|
* is ERANGE from the wrapper rather than from strtol. On a platform where long
|
||||||
|
* and int are the same width there is nothing to narrow and strtol has already
|
||||||
|
* caught it, so the case is skipped rather than asserted into a false pass.
|
||||||
|
*/
|
||||||
|
static int test_atoi_narrows_to_int_range(void)
|
||||||
|
{
|
||||||
|
#if LONG_MAX > INT_MAX
|
||||||
|
int out = 99;
|
||||||
|
char buf[64];
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "%ld", (long)INT_MAX + 1);
|
||||||
|
AKSL_CHECK_STATUS(aksl_atoi(buf, &out), ERANGE);
|
||||||
|
AKSL_CHECK(out == 0);
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "%ld", (long)INT_MIN - 1);
|
||||||
|
AKSL_CHECK_STATUS(aksl_atoi(buf, &out), ERANGE);
|
||||||
|
AKSL_CHECK(out == 0);
|
||||||
|
#else
|
||||||
|
fprintf(stderr, " (skipped: long and int are the same width here)\n");
|
||||||
|
#endif
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TODO.md 1.4: the type boundaries round-trip exactly rather than nearly. */
|
||||||
|
static int test_type_boundaries_round_trip(void)
|
||||||
|
{
|
||||||
|
char buf[64];
|
||||||
|
int iout = 0;
|
||||||
|
long lout = 0;
|
||||||
|
long long llout = 0;
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "%d", INT_MAX);
|
||||||
|
AKSL_CHECK_OK(aksl_atoi(buf, &iout));
|
||||||
|
AKSL_CHECK(iout == INT_MAX);
|
||||||
|
snprintf(buf, sizeof(buf), "%d", INT_MIN);
|
||||||
|
AKSL_CHECK_OK(aksl_atoi(buf, &iout));
|
||||||
|
AKSL_CHECK(iout == INT_MIN);
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "%ld", LONG_MAX);
|
||||||
|
AKSL_CHECK_OK(aksl_atol(buf, &lout));
|
||||||
|
AKSL_CHECK(lout == LONG_MAX);
|
||||||
|
snprintf(buf, sizeof(buf), "%ld", LONG_MIN);
|
||||||
|
AKSL_CHECK_OK(aksl_atol(buf, &lout));
|
||||||
|
AKSL_CHECK(lout == LONG_MIN);
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "%lld", LLONG_MAX);
|
||||||
|
AKSL_CHECK_OK(aksl_atoll(buf, &llout));
|
||||||
|
AKSL_CHECK(llout == LLONG_MAX);
|
||||||
|
snprintf(buf, sizeof(buf), "%lld", LLONG_MIN);
|
||||||
|
AKSL_CHECK_OK(aksl_atoll(buf, &llout));
|
||||||
|
AKSL_CHECK(llout == LLONG_MIN);
|
||||||
|
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);
|
||||||
|
AKSL_CHECK_STATUS(aksl_atol("nope", &out), AKERR_VALUE);
|
||||||
|
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);
|
||||||
|
AKSL_CHECK_STATUS(aksl_atoll("nope", &out), AKERR_VALUE);
|
||||||
|
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_OK(aksl_atof("1e3", &out));
|
||||||
|
AKSL_CHECK(out == 1000.0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_atof(NULL, &out), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_atof("1", NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_atof("nope", &out), AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_atof("1.5xyz", &out), AKERR_VALUE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO.md 1.4 left "inf" and "nan" open. They are accepted, because strtod(3)
|
||||||
|
* accepts them and because they are exact round-trips rather than approximations
|
||||||
|
* of something else -- a caller who did not want them has a domain check to make
|
||||||
|
* that this library cannot make for it.
|
||||||
|
*/
|
||||||
|
static int test_atof_accepts_infinity_and_nan(void)
|
||||||
|
{
|
||||||
|
double out = 0.0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_atof("inf", &out));
|
||||||
|
AKSL_CHECK(out > 0.0 && (out * 2.0) == out);
|
||||||
|
AKSL_CHECK_OK(aksl_atof("-INFINITY", &out));
|
||||||
|
AKSL_CHECK(out < 0.0 && (out * 2.0) == out);
|
||||||
|
AKSL_CHECK_OK(aksl_atof("nan", &out));
|
||||||
|
/* The only value that is not equal to itself. */
|
||||||
|
AKSL_CHECK(out != out);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overflow and underflow both come back as ERANGE, which is strtod's only signal. */
|
||||||
|
static int test_atof_range_errors(void)
|
||||||
|
{
|
||||||
|
double out = 1.0;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_atof("1e400", &out), ERANGE);
|
||||||
|
AKSL_CHECK(out == 0.0);
|
||||||
|
AKSL_CHECK_STATUS(aksl_atof("1e-400", &out), ERANGE);
|
||||||
|
AKSL_CHECK(out == 0.0);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* errno is cleared before each wrapped call, so a failure left over from an
|
||||||
|
* earlier unrelated call cannot be reported as this one's. Set errno to
|
||||||
|
* something conspicuous first and check that a successful conversion still
|
||||||
|
* succeeds -- the old code could report a stale errno as its own status.
|
||||||
|
*/
|
||||||
|
static int test_stale_errno_does_not_leak_into_the_result(void)
|
||||||
|
{
|
||||||
|
int out = 0;
|
||||||
|
|
||||||
|
errno = ERANGE;
|
||||||
|
AKSL_CHECK_OK(aksl_atoi("5", &out));
|
||||||
|
AKSL_CHECK(out == 5);
|
||||||
|
|
||||||
|
errno = ENOMEM;
|
||||||
|
AKSL_CHECK_STATUS(aksl_atoi("junk", &out), AKERR_VALUE);
|
||||||
|
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_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_hex_literal_is_rejected_by_the_base_10_forms);
|
||||||
|
AKSL_RUN(failures, test_overflow_is_erange);
|
||||||
|
AKSL_RUN(failures, test_atoi_narrows_to_int_range);
|
||||||
|
AKSL_RUN(failures, test_type_boundaries_round_trip);
|
||||||
|
|
||||||
|
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_atof_accepts_infinity_and_nan);
|
||||||
|
AKSL_RUN(failures, test_atof_range_errors);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_conversions_are_repeatable);
|
||||||
|
AKSL_RUN(failures, test_stale_errno_does_not_leak_into_the_result);
|
||||||
|
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
351
tests/test_format.c
Normal file
351
tests/test_format.c
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
/*
|
||||||
|
* Formatted-output wrappers: aksl_printf / aksl_fprintf / aksl_snprintf and
|
||||||
|
* their va_list forms.
|
||||||
|
*
|
||||||
|
* TODO.md section 1.3, now complete. 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.
|
||||||
|
*
|
||||||
|
* aksl_sprintf is gone (TODO.md 2.2.4) and aksl_snprintf takes its place, so the
|
||||||
|
* destination-overflow case that could not previously be written is here: it is
|
||||||
|
* AKERR_OUTOFBOUNDS, not the short success snprintf(3) would have reported.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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_snprintf_writes_text_and_count(void)
|
||||||
|
{
|
||||||
|
char buf[64];
|
||||||
|
int count = -1;
|
||||||
|
|
||||||
|
memset(buf, 0x00, sizeof(buf));
|
||||||
|
AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%s=%d", "x", 7));
|
||||||
|
AKSL_CHECK(count == 3);
|
||||||
|
AKSL_CHECK(strcmp(buf, "x=7") == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_snprintf_empty_format_writes_nothing(void)
|
||||||
|
{
|
||||||
|
char buf[8] = { 'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z' };
|
||||||
|
int count = -1;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%s", ""));
|
||||||
|
AKSL_CHECK(count == 0);
|
||||||
|
AKSL_CHECK(buf[0] == '\0');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The case that could not be written while the wrapper was aksl_sprintf: output
|
||||||
|
* longer than the destination. snprintf(3) would truncate, NUL-terminate and
|
||||||
|
* report the length it *would* have written, leaving the caller to notice; here
|
||||||
|
* it is an error, and *count is 0 rather than the would-have-been length.
|
||||||
|
*/
|
||||||
|
static int test_snprintf_truncation_is_an_error(void)
|
||||||
|
{
|
||||||
|
char buf[8];
|
||||||
|
int count = -1;
|
||||||
|
|
||||||
|
memset(buf, 0x00, sizeof(buf));
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||||
|
aksl_snprintf(&count, buf, sizeof(buf), "%s", "far too long for eight bytes"),
|
||||||
|
AKERR_OUTOFBOUNDS, "truncated");
|
||||||
|
AKSL_CHECK(count == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Exactly filling the buffer is not truncation; one byte more is. */
|
||||||
|
static int test_snprintf_boundary_is_exact(void)
|
||||||
|
{
|
||||||
|
char buf[8];
|
||||||
|
int count = -1;
|
||||||
|
|
||||||
|
/* 7 characters plus the NUL is exactly sizeof(buf). */
|
||||||
|
AKSL_CHECK_OK(aksl_snprintf(&count, buf, sizeof(buf), "%s", "1234567"));
|
||||||
|
AKSL_CHECK(count == 7);
|
||||||
|
AKSL_CHECK(strcmp(buf, "1234567") == 0);
|
||||||
|
|
||||||
|
count = -1;
|
||||||
|
AKSL_CHECK_STATUS(aksl_snprintf(&count, buf, sizeof(buf), "%s", "12345678"),
|
||||||
|
AKERR_OUTOFBOUNDS);
|
||||||
|
AKSL_CHECK(count == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_snprintf_rejects_null_arguments_and_zero_size(void)
|
||||||
|
{
|
||||||
|
char buf[8];
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
memset(buf, 0x00, sizeof(buf));
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(NULL, buf, sizeof(buf), "x"),
|
||||||
|
AKERR_NULLPOINTER, "count=");
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(&count, NULL, 8, "x"),
|
||||||
|
AKERR_NULLPOINTER, "str=");
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(&count, buf, sizeof(buf), NULL),
|
||||||
|
AKERR_NULLPOINTER, "format=");
|
||||||
|
/* size 0 leaves no room even for the terminator, so there is nothing to do. */
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_snprintf(&count, buf, 0, "x"),
|
||||||
|
AKERR_VALUE, "size=0");
|
||||||
|
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 0 afterwards, not vfprintf's -1:
|
||||||
|
* TODO.md 1.3 recorded the negative count as a contract gap, and this is the
|
||||||
|
* assertion that closes it.
|
||||||
|
*/
|
||||||
|
static int test_fprintf_to_read_only_stream_reports_errno(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
int count = 99;
|
||||||
|
|
||||||
|
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(count == 0);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The allocating form. No fixed buffer means no truncation case, which is what
|
||||||
|
* makes it the right answer when the length is not knowable in advance and the
|
||||||
|
* result is too short-lived to want a whole aksl_StrBuf.
|
||||||
|
*/
|
||||||
|
static int test_asprintf_allocates_to_fit(void)
|
||||||
|
{
|
||||||
|
char *out = NULL;
|
||||||
|
int count = -1;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_asprintf(&count, &out, "%s=%d", "key", 42));
|
||||||
|
AKSL_CHECK(out != NULL);
|
||||||
|
AKSL_CHECK(strcmp(out, "key=42") == 0);
|
||||||
|
AKSL_CHECK(count == 6);
|
||||||
|
AKSL_CHECK_OK(aksl_freep((void **)&out));
|
||||||
|
|
||||||
|
/* Far longer than any buffer a fixed-size wrapper would have used. */
|
||||||
|
AKSL_CHECK_OK(aksl_asprintf(&count, &out, "%01000d", 7));
|
||||||
|
AKSL_CHECK(count == 1000);
|
||||||
|
AKSL_CHECK(strlen(out) == 1000);
|
||||||
|
AKSL_CHECK_OK(aksl_freep((void **)&out));
|
||||||
|
|
||||||
|
/* An empty result is a valid empty string, not NULL. */
|
||||||
|
AKSL_CHECK_OK(aksl_asprintf(&count, &out, "%s", ""));
|
||||||
|
AKSL_CHECK(count == 0);
|
||||||
|
AKSL_CHECK(out != NULL && out[0] == '\0');
|
||||||
|
AKSL_CHECK_OK(aksl_freep((void **)&out));
|
||||||
|
|
||||||
|
/* Repeatedly, so a leak shows up under the sanitizer build. */
|
||||||
|
for ( i = 0; i < 256; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_asprintf(&count, &out, "iteration %d of %d", i, 256));
|
||||||
|
AKSL_CHECK((size_t)count == strlen(out));
|
||||||
|
AKSL_CHECK_OK(aksl_freep((void **)&out));
|
||||||
|
}
|
||||||
|
|
||||||
|
out = (char *)0x1;
|
||||||
|
AKSL_CHECK_STATUS(aksl_asprintf(NULL, &out, "x"), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_asprintf(&count, NULL, "x"), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_asprintf(&count, &out, NULL), AKERR_NULLPOINTER);
|
||||||
|
/* Cleared before the format is even looked at. */
|
||||||
|
AKSL_CHECK(out == NULL);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The va_list forms are what the variadic ones are built on, and TODO.md 3.1
|
||||||
|
* wanted them exposed so consumers can write their own variadic wrappers. This
|
||||||
|
* is a consumer doing exactly that.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext AKERR_NOIGNORE *consumer_wrapper(int *count, char *buf, size_t n,
|
||||||
|
const char *fmt, ...)
|
||||||
|
{
|
||||||
|
va_list args;
|
||||||
|
akerr_ErrorContext *raised = NULL;
|
||||||
|
|
||||||
|
va_start(args, fmt);
|
||||||
|
raised = aksl_vsnprintf(count, buf, n, fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
return raised;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_va_list_forms_are_usable_from_outside(void)
|
||||||
|
{
|
||||||
|
char buf[32];
|
||||||
|
int count = -1;
|
||||||
|
|
||||||
|
memset(buf, 0x00, sizeof(buf));
|
||||||
|
AKSL_CHECK_OK(consumer_wrapper(&count, buf, sizeof(buf), "%s/%d", "via", 3));
|
||||||
|
AKSL_CHECK(count == 5);
|
||||||
|
AKSL_CHECK(strcmp(buf, "via/3") == 0);
|
||||||
|
|
||||||
|
/* The error contract survives the extra layer intact. */
|
||||||
|
AKSL_CHECK_STATUS(consumer_wrapper(&count, buf, 4, "%s", "too long"),
|
||||||
|
AKERR_OUTOFBOUNDS);
|
||||||
|
AKSL_CHECK(count == 0);
|
||||||
|
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_snprintf(&count, buf, sizeof(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_snprintf_writes_text_and_count);
|
||||||
|
AKSL_RUN(failures, test_snprintf_empty_format_writes_nothing);
|
||||||
|
AKSL_RUN(failures, test_snprintf_truncation_is_an_error);
|
||||||
|
AKSL_RUN(failures, test_snprintf_boundary_is_exact);
|
||||||
|
AKSL_RUN(failures, test_snprintf_rejects_null_arguments_and_zero_size);
|
||||||
|
|
||||||
|
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_asprintf_allocates_to_fit);
|
||||||
|
AKSL_RUN(failures, test_va_list_forms_are_usable_from_outside);
|
||||||
|
AKSL_RUN(failures, test_variadic_wrappers_survive_repeated_calls);
|
||||||
|
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
417
tests/test_hashmap.c
Normal file
417
tests/test_hashmap.c
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
/*
|
||||||
|
* The fixed-capacity hash map and FNV-1a -- src/collections.c, TODO.md 3.6.
|
||||||
|
*
|
||||||
|
* "The single most obviously-missing data structure in the library", by the
|
||||||
|
* TODO's own account: akbasic needed one three times over -- variables,
|
||||||
|
* functions and labels -- and wrote ~130 lines of open-addressed table to get it.
|
||||||
|
*
|
||||||
|
* The behaviour worth testing hardest is the tombstone. Deleting a key that
|
||||||
|
* something else probed past has to leave a marker rather than an empty slot,
|
||||||
|
* because an empty slot ends a probe chain and would make the later key
|
||||||
|
* unreachable while it is still sitting there.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
#define CAP 16
|
||||||
|
|
||||||
|
typedef struct SeenLog
|
||||||
|
{
|
||||||
|
int count;
|
||||||
|
char keys[CAP][AKSL_HASHMAP_MAX_KEY];
|
||||||
|
int break_at;
|
||||||
|
} SeenLog;
|
||||||
|
|
||||||
|
static akerr_ErrorContext AKERR_NOIGNORE *record_entry(const char *key, void *value, void *data)
|
||||||
|
{
|
||||||
|
SeenLog *log = NULL;
|
||||||
|
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
FAIL_ZERO_RETURN(e, key, AKERR_NULLPOINTER, "key");
|
||||||
|
FAIL_ZERO_RETURN(e, data, AKERR_NULLPOINTER, "data");
|
||||||
|
(void)value;
|
||||||
|
log = (SeenLog *)data;
|
||||||
|
if ( log->count < CAP ) {
|
||||||
|
snprintf(log->keys[log->count], AKSL_HASHMAP_MAX_KEY, "%s", key);
|
||||||
|
}
|
||||||
|
log->count += 1;
|
||||||
|
if ( log->break_at == log->count - 1 ) {
|
||||||
|
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop");
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* FNV-1a */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The reference values are the canonical 32-bit FNV-1a ones: h = 2166136261,
|
||||||
|
* then h = (h XOR byte) * 16777619 for each byte. "a" and "foobar" are the
|
||||||
|
* vectors from the FNV reference material.
|
||||||
|
*/
|
||||||
|
static int test_fnv1a_known_answers(void)
|
||||||
|
{
|
||||||
|
uint32_t h = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strhash_fnv1a("", 0, &h));
|
||||||
|
AKSL_CHECK(h == 2166136261u);
|
||||||
|
AKSL_CHECK_OK(aksl_strhash_fnv1a("a", 1, &h));
|
||||||
|
AKSL_CHECK(h == 0xe40c292cu);
|
||||||
|
AKSL_CHECK_OK(aksl_strhash_fnv1a("foobar", 6, &h));
|
||||||
|
AKSL_CHECK(h == 0xbf9cf968u);
|
||||||
|
|
||||||
|
/* The NUL-terminated form agrees with the length-driven one. */
|
||||||
|
AKSL_CHECK_OK(aksl_strhash_fnv1a_str("foobar", &h));
|
||||||
|
AKSL_CHECK(h == 0xbf9cf968u);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strhash_fnv1a(NULL, 0, &h), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strhash_fnv1a("a", 1, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strhash_fnv1a_str(NULL, &h), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strhash_fnv1a_str("a", NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* High bytes are unsigned here for the same reason they are in djb2. */
|
||||||
|
static int test_fnv1a_high_bit_bytes_are_unsigned(void)
|
||||||
|
{
|
||||||
|
char buf[2] = { (char)0xff, (char)0xfe };
|
||||||
|
uint32_t h = 0;
|
||||||
|
uint32_t expected = 2166136261u;
|
||||||
|
|
||||||
|
expected = (expected ^ 0xffu) * 16777619u;
|
||||||
|
expected = (expected ^ 0xfeu) * 16777619u;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strhash_fnv1a(buf, sizeof(buf), &h));
|
||||||
|
AKSL_CHECK(h == expected);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Put and get */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_put_and_get(void)
|
||||||
|
{
|
||||||
|
aksl_HashEntry slots[CAP];
|
||||||
|
aksl_HashMap map;
|
||||||
|
int a = 1;
|
||||||
|
int b = 2;
|
||||||
|
void *value = NULL;
|
||||||
|
int found = 99;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
|
||||||
|
AKSL_CHECK(map.count == 0);
|
||||||
|
AKSL_CHECK(map.capacity == CAP);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, "alpha", &a));
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, "beta", &b));
|
||||||
|
AKSL_CHECK(map.count == 2);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_get(&map, "alpha", &value, &found));
|
||||||
|
AKSL_CHECK(found == 1);
|
||||||
|
AKSL_CHECK(value == &a);
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_get(&map, "beta", &value, &found));
|
||||||
|
AKSL_CHECK(found == 1);
|
||||||
|
AKSL_CHECK(value == &b);
|
||||||
|
|
||||||
|
/* A key that is not there is found = 0 and success, not an error: looking
|
||||||
|
* something up and not finding it is the ordinary case for a symbol table. */
|
||||||
|
value = (void *)0x1;
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_get(&map, "gamma", &value, &found));
|
||||||
|
AKSL_CHECK(found == 0);
|
||||||
|
AKSL_CHECK(value == (void *)0x1); /* untouched when the key is absent */
|
||||||
|
|
||||||
|
/* NULL value out-param is allowed: sometimes you only want to know if it is there. */
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_get(&map, "alpha", NULL, &found));
|
||||||
|
AKSL_CHECK(found == 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Putting an existing key replaces the value rather than adding a second entry. */
|
||||||
|
static int test_put_replaces_an_existing_key(void)
|
||||||
|
{
|
||||||
|
aksl_HashEntry slots[CAP];
|
||||||
|
aksl_HashMap map;
|
||||||
|
int a = 1;
|
||||||
|
int b = 2;
|
||||||
|
void *value = NULL;
|
||||||
|
int found = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, "key", &a));
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, "key", &b));
|
||||||
|
AKSL_CHECK(map.count == 1);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_get(&map, "key", &value, &found));
|
||||||
|
AKSL_CHECK(found == 1);
|
||||||
|
AKSL_CHECK(value == &b);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The map copies keys into its slots, so it owns them and cannot outlive them. */
|
||||||
|
static int test_the_map_owns_its_keys(void)
|
||||||
|
{
|
||||||
|
aksl_HashEntry slots[CAP];
|
||||||
|
aksl_HashMap map;
|
||||||
|
char key[16] = "transient";
|
||||||
|
int a = 1;
|
||||||
|
int found = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, key, &a));
|
||||||
|
|
||||||
|
/* Scribble over the caller's copy of the key. */
|
||||||
|
memset(key, 'Z', sizeof(key) - 1);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_get(&map, "transient", NULL, &found));
|
||||||
|
AKSL_CHECK(found == 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Bounds */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Full is an error naming the capacity, not a silent resize. A table that
|
||||||
|
* reallocates is a table whose entry pointers move underneath anything holding
|
||||||
|
* one; this one has a worst case you can state.
|
||||||
|
*/
|
||||||
|
static int test_a_full_map_refuses_rather_than_resizing(void)
|
||||||
|
{
|
||||||
|
aksl_HashEntry slots[4];
|
||||||
|
aksl_HashMap map;
|
||||||
|
char key[16];
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
|
||||||
|
for ( i = 0; i < 4; i++ ) {
|
||||||
|
snprintf(key, sizeof(key), "k%d", i);
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL));
|
||||||
|
}
|
||||||
|
AKSL_CHECK(map.count == 4);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_hashmap_put(&map, "overflow", NULL),
|
||||||
|
AKERR_OUTOFBOUNDS, "does not resize");
|
||||||
|
AKSL_CHECK(map.count == 4);
|
||||||
|
|
||||||
|
/* Everything already in there is still reachable and still correct. */
|
||||||
|
for ( i = 0; i < 4; i++ ) {
|
||||||
|
int found = 0;
|
||||||
|
snprintf(key, sizeof(key), "k%d", i);
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_get(&map, key, NULL, &found));
|
||||||
|
AKSL_CHECK(found == 1);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A key too long for a slot is refused rather than truncated. Truncation would
|
||||||
|
* silently collide two different keys that share a prefix, which is the worst
|
||||||
|
* possible failure for a symbol table -- a lookup that returns the wrong thing.
|
||||||
|
*/
|
||||||
|
static int test_an_oversized_key_is_refused(void)
|
||||||
|
{
|
||||||
|
aksl_HashEntry slots[CAP];
|
||||||
|
aksl_HashMap map;
|
||||||
|
char longkey[AKSL_HASHMAP_MAX_KEY + 8];
|
||||||
|
int found = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
|
||||||
|
memset(longkey, 'k', sizeof(longkey) - 1);
|
||||||
|
longkey[sizeof(longkey) - 1] = '\0';
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_hashmap_put(&map, longkey, NULL),
|
||||||
|
AKERR_OUTOFBOUNDS, "AKSL_HASHMAP_MAX_KEY");
|
||||||
|
AKSL_CHECK_STATUS(aksl_hashmap_get(&map, longkey, NULL, &found), AKERR_OUTOFBOUNDS);
|
||||||
|
AKSL_CHECK_STATUS(aksl_hashmap_remove(&map, longkey, NULL), AKERR_OUTOFBOUNDS);
|
||||||
|
|
||||||
|
/* One byte short of the limit is fine: it is a limit, not an off-by-one. */
|
||||||
|
longkey[AKSL_HASHMAP_MAX_KEY - 1] = '\0';
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, longkey, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_get(&map, longkey, NULL, &found));
|
||||||
|
AKSL_CHECK(found == 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Removal and tombstones */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_remove(void)
|
||||||
|
{
|
||||||
|
aksl_HashEntry slots[CAP];
|
||||||
|
aksl_HashMap map;
|
||||||
|
int a = 1;
|
||||||
|
int found = 99;
|
||||||
|
int removed = 99;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, "alpha", &a));
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_remove(&map, "alpha", &removed));
|
||||||
|
AKSL_CHECK(removed == 1);
|
||||||
|
AKSL_CHECK(map.count == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_get(&map, "alpha", NULL, &found));
|
||||||
|
AKSL_CHECK(found == 0);
|
||||||
|
|
||||||
|
/* Removing what is not there is success with removed = 0. */
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_remove(&map, "alpha", &removed));
|
||||||
|
AKSL_CHECK(removed == 0);
|
||||||
|
/* The out-param is optional. */
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_remove(&map, "nothing", NULL));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The tombstone case, which is the one an open-addressed table gets wrong.
|
||||||
|
*
|
||||||
|
* A tiny capacity forces collisions, so several keys share a probe chain.
|
||||||
|
* Deleting one from the middle of that chain must not cut the keys behind it
|
||||||
|
* loose: if the slot were simply emptied, the probe for a later key would stop
|
||||||
|
* there and report the key missing while it was still sitting in the table.
|
||||||
|
*/
|
||||||
|
static int test_removal_does_not_break_a_probe_chain(void)
|
||||||
|
{
|
||||||
|
aksl_HashEntry slots[4];
|
||||||
|
aksl_HashMap map;
|
||||||
|
char key[16];
|
||||||
|
int found = 0;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
|
||||||
|
for ( i = 0; i < 4; i++ ) {
|
||||||
|
snprintf(key, sizeof(key), "k%d", i);
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Take out each key in turn and confirm every remaining one is still found. */
|
||||||
|
for ( i = 0; i < 4; i++ ) {
|
||||||
|
int j = 0;
|
||||||
|
snprintf(key, sizeof(key), "k%d", i);
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_remove(&map, key, NULL));
|
||||||
|
for ( j = i + 1; j < 4; j++ ) {
|
||||||
|
snprintf(key, sizeof(key), "k%d", j);
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_get(&map, key, NULL, &found));
|
||||||
|
AKSL_CHECK(found == 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AKSL_CHECK(map.count == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A tombstone is reusable, so a table churned through repeatedly does not fill up. */
|
||||||
|
static int test_tombstones_are_reused(void)
|
||||||
|
{
|
||||||
|
aksl_HashEntry slots[4];
|
||||||
|
aksl_HashMap map;
|
||||||
|
char key[16];
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
|
||||||
|
/* Far more insert/remove cycles than there are slots. */
|
||||||
|
for ( i = 0; i < 64; i++ ) {
|
||||||
|
snprintf(key, sizeof(key), "cycle%d", i);
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_remove(&map, key, NULL));
|
||||||
|
}
|
||||||
|
AKSL_CHECK(map.count == 0);
|
||||||
|
/* Still usable afterwards. */
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, "final", NULL));
|
||||||
|
AKSL_CHECK(map.count == 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Iteration */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_iterate_visits_every_live_entry(void)
|
||||||
|
{
|
||||||
|
aksl_HashEntry slots[CAP];
|
||||||
|
aksl_HashMap map;
|
||||||
|
SeenLog log;
|
||||||
|
char key[16];
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
|
||||||
|
for ( i = 0; i < 5; i++ ) {
|
||||||
|
snprintf(key, sizeof(key), "k%d", i);
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_put(&map, key, NULL));
|
||||||
|
}
|
||||||
|
/* One removed, so there is a tombstone for the walk to skip. */
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_remove(&map, "k2", NULL));
|
||||||
|
|
||||||
|
memset((void *)&log, 0x00, sizeof(log));
|
||||||
|
log.break_at = -1;
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_iterate(&map, &record_entry, &log));
|
||||||
|
AKSL_CHECK(log.count == 4);
|
||||||
|
|
||||||
|
/* The break stops it, as everywhere else in this library. */
|
||||||
|
memset((void *)&log, 0x00, sizeof(log));
|
||||||
|
log.break_at = 1;
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_iterate(&map, &record_entry, &log));
|
||||||
|
AKSL_CHECK(log.count == 2);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_rejects_null_and_uninitialised(void)
|
||||||
|
{
|
||||||
|
aksl_HashEntry slots[CAP];
|
||||||
|
aksl_HashMap map;
|
||||||
|
aksl_HashMap empty;
|
||||||
|
SeenLog log;
|
||||||
|
int found = 0;
|
||||||
|
|
||||||
|
memset((void *)&empty, 0x00, sizeof(empty));
|
||||||
|
memset((void *)&log, 0x00, sizeof(log));
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, CAP));
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_hashmap_init(NULL, slots, CAP), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_hashmap_init(&map, NULL, CAP), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_hashmap_init(&map, slots, 0), AKERR_VALUE);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_hashmap_put(NULL, "k", NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_hashmap_put(&map, NULL, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_hashmap_get(NULL, "k", NULL, &found), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_hashmap_get(&map, NULL, NULL, &found), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_hashmap_get(&map, "k", NULL, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_hashmap_remove(NULL, "k", NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_hashmap_remove(&map, NULL, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_hashmap_iterate(NULL, &record_entry, &log), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_hashmap_iterate(&map, NULL, &log), AKERR_NULLPOINTER);
|
||||||
|
|
||||||
|
/* A map that was never initialised is caught rather than dereferenced. */
|
||||||
|
AKSL_CHECK_STATUS(aksl_hashmap_put(&empty, "k", NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_hashmap_get(&empty, "k", NULL, &found), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_hashmap_remove(&empty, "k", NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_hashmap_iterate(&empty, &record_entry, &log), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
akerr_init();
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_fnv1a_known_answers);
|
||||||
|
AKSL_RUN(failures, test_fnv1a_high_bit_bytes_are_unsigned);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_put_and_get);
|
||||||
|
AKSL_RUN(failures, test_put_replaces_an_existing_key);
|
||||||
|
AKSL_RUN(failures, test_the_map_owns_its_keys);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_a_full_map_refuses_rather_than_resizing);
|
||||||
|
AKSL_RUN(failures, test_an_oversized_key_is_refused);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_remove);
|
||||||
|
AKSL_RUN(failures, test_removal_does_not_break_a_probe_chain);
|
||||||
|
AKSL_RUN(failures, test_tombstones_are_reused);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_iterate_visits_every_live_entry);
|
||||||
|
AKSL_RUN(failures, test_rejects_null_and_uninitialised);
|
||||||
|
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
643
tests/test_linkedlist.c
Normal file
643
tests/test_linkedlist.c
Normal file
@@ -0,0 +1,643 @@
|
|||||||
|
/*
|
||||||
|
* Linked list -- TODO.md section 1.7, complete.
|
||||||
|
*
|
||||||
|
* The two confirmed list defects are fixed, so the tests that used to live in
|
||||||
|
* tests/test_list_append_chain.c and tests/test_list_iterate_head.c are folded
|
||||||
|
* back in here: aksl_list_append builds the whole chain rather than truncating
|
||||||
|
* it at the midpoint (2.1.1), and aksl_list_iterate starts at the head rather
|
||||||
|
* than at whatever node Floyd's slow pointer happened to stop on (2.1.2).
|
||||||
|
*
|
||||||
|
* The chain assertions build their lists with aksl_list_append now, which is the
|
||||||
|
* point -- they could not, while append was the thing under suspicion.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
#define MAX_VISITS 16
|
||||||
|
#define CHAIN_LEN 5
|
||||||
|
|
||||||
|
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_node_init */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO.md 2.2.14: every caller used to have to remember to memset a node before
|
||||||
|
* its first use, and a stack node that skipped it walked straight into garbage.
|
||||||
|
*/
|
||||||
|
static int test_node_init_zeroes_the_links(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode node;
|
||||||
|
int payload = 7;
|
||||||
|
|
||||||
|
memset((void *)&node, 0xff, sizeof(node));
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&node, &payload));
|
||||||
|
AKSL_CHECK(node.next == NULL);
|
||||||
|
AKSL_CHECK(node.prev == NULL);
|
||||||
|
AKSL_CHECK(node.data == (void *)&payload);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
|
||||||
|
AKSL_CHECK(node.data == NULL);
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_node_init(NULL, NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* aksl_list_append */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_append_single_node(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode head;
|
||||||
|
aksl_ListNode tail;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&head, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&tail, NULL));
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The defect that made this library's list unusable: `tail` was assigned from
|
||||||
|
* Floyd's `slow` cursor *before* slow advanced, so it tracked the node behind
|
||||||
|
* the midpoint rather than the last node. Appending n1..n4 to n0 produced the
|
||||||
|
* chain "n0 -> n4" and silently dropped n1, n2 and n3. TODO.md 2.1.1.
|
||||||
|
*/
|
||||||
|
static int test_append_builds_the_whole_chain(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode node[CHAIN_LEN];
|
||||||
|
aksl_ListNode *walk = NULL;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < CHAIN_LEN; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&node[i], NULL));
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* append points obj->prev at the real tail and terminates the chain at obj. */
|
||||||
|
static int test_append_links_prev_to_the_real_tail(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode node[4];
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < 4; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&node[i], NULL));
|
||||||
|
}
|
||||||
|
for ( i = 1; i < 4; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_list_append(&node[0], &node[i]));
|
||||||
|
AKSL_CHECK(node[i].prev == &node[i - 1]);
|
||||||
|
AKSL_CHECK(node[i].next == NULL);
|
||||||
|
AKSL_CHECK(node[i - 1].next == &node[i]);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_append_null_arguments(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode node;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&head, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
|
||||||
|
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;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&a, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&b, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
|
||||||
|
a.next = &b;
|
||||||
|
b.prev = &a;
|
||||||
|
b.next = &a;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_append(&a, &node), AKERR_CIRCULAR_REFERENCE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A cycle that does not include the head: a -> b -> c -> b. */
|
||||||
|
static int test_append_detects_cycle_below_the_head(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode a;
|
||||||
|
aksl_ListNode b;
|
||||||
|
aksl_ListNode c;
|
||||||
|
aksl_ListNode node;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&a, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&b, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&c, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
|
||||||
|
a.next = &b;
|
||||||
|
b.prev = &a;
|
||||||
|
b.next = &c;
|
||||||
|
c.prev = &b;
|
||||||
|
c.next = &b;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_append(&a, &node), AKERR_CIRCULAR_REFERENCE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO.md 1.7 asked for the aliasing contract to be defined. It is refusal:
|
||||||
|
* relinking a node that is already in the list would orphan everything between
|
||||||
|
* its old position and the tail, so the tail walk -- which happens anyway --
|
||||||
|
* doubles as the check.
|
||||||
|
*/
|
||||||
|
static int test_append_refuses_a_node_already_in_the_list(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode node[3];
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < 3; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&node[i], NULL));
|
||||||
|
}
|
||||||
|
AKSL_CHECK_OK(aksl_list_append(&node[0], &node[1]));
|
||||||
|
AKSL_CHECK_OK(aksl_list_append(&node[0], &node[2]));
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_list_append(&node[0], &node[1]),
|
||||||
|
AKERR_VALUE, "already in this list");
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_list_append(&node[0], &node[0]),
|
||||||
|
AKERR_VALUE, "already the head");
|
||||||
|
/* The list is untouched by the refusal. */
|
||||||
|
AKSL_CHECK(node[0].next == &node[1]);
|
||||||
|
AKSL_CHECK(node[1].next == &node[2]);
|
||||||
|
AKSL_CHECK(node[2].next == NULL);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* aksl_list_iterate */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_iterate_null_arguments(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode node;
|
||||||
|
VisitLog log;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
|
||||||
|
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;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Every node, exactly once, in order, starting at the head. The cycle check
|
||||||
|
* leaves Floyd's `slow` cursor at the list midpoint, and the visiting loop used
|
||||||
|
* to start from there -- so the whole first half of the list, head included, was
|
||||||
|
* never passed to the callback at all. TODO.md 2.1.2.
|
||||||
|
*/
|
||||||
|
static int test_iterate_visits_every_node_from_the_head(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode node[CHAIN_LEN];
|
||||||
|
VisitLog log;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < CHAIN_LEN; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&node[i], NULL));
|
||||||
|
}
|
||||||
|
for ( i = 1; i < CHAIN_LEN; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_list_append(&node[0], &node[i]));
|
||||||
|
}
|
||||||
|
visitlog_init(&log);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* An even-length list too: Floyd's midpoint lands differently, the answer does not. */
|
||||||
|
static int test_iterate_visits_every_node_of_an_even_list(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode node[4];
|
||||||
|
VisitLog log;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < 4; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&node[i], NULL));
|
||||||
|
}
|
||||||
|
for ( i = 1; i < 4; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_list_append(&node[0], &node[i]));
|
||||||
|
}
|
||||||
|
visitlog_init(&log);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_iterate(&node[0], &record_visit, &log));
|
||||||
|
AKSL_CHECK(log.count == 4);
|
||||||
|
for ( i = 0; i < 4; i++ ) {
|
||||||
|
AKSL_CHECK(log.seen[i] == &node[i]);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_iterate_detects_self_cycle(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode head;
|
||||||
|
VisitLog log;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&head, NULL));
|
||||||
|
head.next = &head;
|
||||||
|
visitlog_init(&log);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_iterate(&head, &record_visit, &log),
|
||||||
|
AKERR_CIRCULAR_REFERENCE);
|
||||||
|
AKSL_CHECK(log.count == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_iterate_detects_two_node_cycle(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode a;
|
||||||
|
aksl_ListNode b;
|
||||||
|
VisitLog log;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&a, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&b, NULL));
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tail pointing back into the middle: a -> b -> c -> b. */
|
||||||
|
static int test_iterate_detects_tail_to_middle_cycle(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode a;
|
||||||
|
aksl_ListNode b;
|
||||||
|
aksl_ListNode c;
|
||||||
|
VisitLog log;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&a, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&b, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&c, NULL));
|
||||||
|
a.next = &b;
|
||||||
|
b.prev = &a;
|
||||||
|
b.next = &c;
|
||||||
|
c.prev = &b;
|
||||||
|
c.next = &b;
|
||||||
|
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;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
|
||||||
|
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;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
|
||||||
|
visitlog_init(&log);
|
||||||
|
log.break_at = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_iterate(&node, &record_visit, &log));
|
||||||
|
AKSL_CHECK(log.count == 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* And the count is what proves it stopped early rather than merely finishing.
|
||||||
|
* A break on the second of five nodes must leave three nodes unvisited.
|
||||||
|
*/
|
||||||
|
static int test_iterate_break_stops_at_that_node(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode node[CHAIN_LEN];
|
||||||
|
VisitLog log;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < CHAIN_LEN; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&node[i], NULL));
|
||||||
|
}
|
||||||
|
for ( i = 1; i < CHAIN_LEN; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_list_append(&node[0], &node[i]));
|
||||||
|
}
|
||||||
|
visitlog_init(&log);
|
||||||
|
log.break_at = 1;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_iterate(&node[0], &record_visit, &log));
|
||||||
|
AKSL_CHECK(log.count == 2);
|
||||||
|
AKSL_CHECK(log.seen[0] == &node[0]);
|
||||||
|
AKSL_CHECK(log.seen[1] == &node[1]);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* aksl_list_pop */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_pop_middle_node(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode a;
|
||||||
|
aksl_ListNode b;
|
||||||
|
aksl_ListNode c;
|
||||||
|
aksl_ListNode *head = &a;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&a, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&b, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&c, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_list_append(&a, &b));
|
||||||
|
AKSL_CHECK_OK(aksl_list_append(&a, &c));
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_pop(&head, &b));
|
||||||
|
AKSL_CHECK(head == &a);
|
||||||
|
AKSL_CHECK(a.next == &c);
|
||||||
|
AKSL_CHECK(c.prev == &a);
|
||||||
|
AKSL_CHECK(b.next == NULL);
|
||||||
|
AKSL_CHECK(b.prev == NULL);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO.md 2.2.12: popping the head used to leave the caller's own head pointer
|
||||||
|
* aimed at a node that was no longer in the list, with no way to learn the new
|
||||||
|
* one. That is what the head out-param is for, and this is the assertion.
|
||||||
|
*/
|
||||||
|
static int test_pop_head_node_moves_the_head(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode a;
|
||||||
|
aksl_ListNode b;
|
||||||
|
aksl_ListNode *head = &a;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&a, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&b, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_list_append(&a, &b));
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_pop(&head, &a));
|
||||||
|
AKSL_CHECK(head == &b);
|
||||||
|
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;
|
||||||
|
aksl_ListNode *head = &a;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&a, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&b, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_list_append(&a, &b));
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_pop(&head, &b));
|
||||||
|
AKSL_CHECK(head == &a);
|
||||||
|
AKSL_CHECK(a.next == NULL);
|
||||||
|
AKSL_CHECK(a.prev == NULL);
|
||||||
|
AKSL_CHECK(b.next == NULL);
|
||||||
|
AKSL_CHECK(b.prev == NULL);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Popping the only node empties the list, and the head becomes NULL. */
|
||||||
|
static int test_pop_only_node_empties_the_list(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode a;
|
||||||
|
aksl_ListNode *head = &a;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&a, NULL));
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_pop(&head, &a));
|
||||||
|
AKSL_CHECK(head == NULL);
|
||||||
|
AKSL_CHECK(a.next == NULL);
|
||||||
|
AKSL_CHECK(a.prev == NULL);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_pop_null_arguments(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode a;
|
||||||
|
aksl_ListNode *head = &a;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&a, NULL));
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_pop(NULL, &a), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_pop(&head, NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The list is still walkable after a pop, and the popped node is gone from it. */
|
||||||
|
static int test_pop_then_iterate(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode node[4];
|
||||||
|
aksl_ListNode *head = &node[0];
|
||||||
|
VisitLog log;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < 4; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&node[i], NULL));
|
||||||
|
}
|
||||||
|
for ( i = 1; i < 4; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_list_append(&node[0], &node[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_pop(&head, &node[2]));
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_OK(aksl_list_iterate(head, &record_visit, &log));
|
||||||
|
AKSL_CHECK(log.count == 3);
|
||||||
|
AKSL_CHECK(log.seen[0] == &node[0]);
|
||||||
|
AKSL_CHECK(log.seen[1] == &node[1]);
|
||||||
|
AKSL_CHECK(log.seen[2] == &node[3]);
|
||||||
|
|
||||||
|
/* And again, this time taking the head out. */
|
||||||
|
AKSL_CHECK_OK(aksl_list_pop(&head, &node[0]));
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_OK(aksl_list_iterate(head, &record_visit, &log));
|
||||||
|
AKSL_CHECK(log.count == 2);
|
||||||
|
AKSL_CHECK(log.seen[0] == &node[1]);
|
||||||
|
AKSL_CHECK(log.seen[1] == &node[3]);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO.md 1.7's pool-accounting case. AKSL_RUN already asserts that each test
|
||||||
|
* leaves the pool as it found it; this one drives enough failures in a row to
|
||||||
|
* exhaust the pool several times over, which is where a wrapper that raises an
|
||||||
|
* error and forgets to release it shows up as an outright exhaustion rather than
|
||||||
|
* as a slow leak.
|
||||||
|
*/
|
||||||
|
static int test_error_pool_survives_a_long_run_of_failures(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode a;
|
||||||
|
aksl_ListNode b;
|
||||||
|
aksl_ListNode *head = &a;
|
||||||
|
VisitLog log;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&a, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&b, NULL));
|
||||||
|
a.next = &a;
|
||||||
|
|
||||||
|
for ( i = 0; i < AKERR_MAX_ARRAY_ERROR + 10; i++ ) {
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_append(&a, &b), AKERR_CIRCULAR_REFERENCE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_append(NULL, &b), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_pop(&head, NULL), AKERR_NULLPOINTER);
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_iterate(&a, &record_visit, &log),
|
||||||
|
AKERR_CIRCULAR_REFERENCE);
|
||||||
|
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
akerr_init();
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_node_init_zeroes_the_links);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_append_single_node);
|
||||||
|
AKSL_RUN(failures, test_append_builds_the_whole_chain);
|
||||||
|
AKSL_RUN(failures, test_append_links_prev_to_the_real_tail);
|
||||||
|
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_append_detects_cycle_below_the_head);
|
||||||
|
AKSL_RUN(failures, test_append_refuses_a_node_already_in_the_list);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_iterate_null_arguments);
|
||||||
|
AKSL_RUN(failures, test_iterate_single_node);
|
||||||
|
AKSL_RUN(failures, test_iterate_visits_every_node_from_the_head);
|
||||||
|
AKSL_RUN(failures, test_iterate_visits_every_node_of_an_even_list);
|
||||||
|
AKSL_RUN(failures, test_iterate_detects_self_cycle);
|
||||||
|
AKSL_RUN(failures, test_iterate_detects_two_node_cycle);
|
||||||
|
AKSL_RUN(failures, test_iterate_detects_tail_to_middle_cycle);
|
||||||
|
AKSL_RUN(failures, test_iterate_propagates_callback_error);
|
||||||
|
AKSL_RUN(failures, test_iterate_break_is_not_an_error);
|
||||||
|
AKSL_RUN(failures, test_iterate_break_stops_at_that_node);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_pop_middle_node);
|
||||||
|
AKSL_RUN(failures, test_pop_head_node_moves_the_head);
|
||||||
|
AKSL_RUN(failures, test_pop_tail_node);
|
||||||
|
AKSL_RUN(failures, test_pop_only_node_empties_the_list);
|
||||||
|
AKSL_RUN(failures, test_pop_null_arguments);
|
||||||
|
AKSL_RUN(failures, test_pop_then_iterate);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_error_pool_survives_a_long_run_of_failures);
|
||||||
|
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
428
tests/test_memory.c
Normal file
428
tests/test_memory.c
Normal file
@@ -0,0 +1,428 @@
|
|||||||
|
/*
|
||||||
|
* Memory wrappers -- TODO.md section 1.1, now complete, plus the additions from
|
||||||
|
* section 3.1.
|
||||||
|
*
|
||||||
|
* The three cases 1.1 left open are all pinned here: malloc(0) is AKERR_VALUE
|
||||||
|
* rather than whatever errno happened to hold when the platform's malloc(0)
|
||||||
|
* returned NULL; an allocation the system cannot satisfy reports ENOMEM and
|
||||||
|
* leaves *dst NULL rather than garbage; and overlapping memcpy is refused with
|
||||||
|
* AKERR_VALUE rather than left as undefined behaviour that usually looks fine.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdint.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, "dst=");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO.md 1.1 asked for this contract to be pinned down. malloc(0) is allowed to
|
||||||
|
* return either a unique pointer or NULL, and a NULL there is not a failure and
|
||||||
|
* need not set errno -- so the old wrapper could raise an error whose status was
|
||||||
|
* 0, which every DETECT downstream reads as success while the context holds a
|
||||||
|
* pool slot. A zero-size request is refused on its own terms instead.
|
||||||
|
*/
|
||||||
|
static int test_malloc_rejects_zero_size(void)
|
||||||
|
{
|
||||||
|
void *ptr = (void *)0x1;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_malloc(0, &ptr), AKERR_VALUE, "zero-size");
|
||||||
|
AKSL_CHECK(ptr == NULL);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* An allocation no system can satisfy. SIZE_MAX/2 rather than SIZE_MAX because
|
||||||
|
* some allocators reject the latter as an obvious overflow before ever
|
||||||
|
* consulting the system, and the case worth testing is the one where the request
|
||||||
|
* is plausible and the answer is still no.
|
||||||
|
*/
|
||||||
|
static int test_malloc_reports_out_of_memory(void)
|
||||||
|
{
|
||||||
|
void *ptr = (void *)0x1;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_malloc(SIZE_MAX / 2, &ptr), ENOMEM);
|
||||||
|
/* Never garbage on the failure path. */
|
||||||
|
AKSL_CHECK(ptr == NULL);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_calloc_zeroes_and_guards(void)
|
||||||
|
{
|
||||||
|
unsigned char *buf = NULL;
|
||||||
|
void *ptr = (void *)0x1;
|
||||||
|
size_t i = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_calloc(16, sizeof(unsigned char), (void **)&buf));
|
||||||
|
AKSL_CHECK(buf != NULL);
|
||||||
|
for ( i = 0; i < 16; i++ ) {
|
||||||
|
AKSL_CHECK(buf[i] == 0);
|
||||||
|
}
|
||||||
|
AKSL_CHECK_OK(aksl_free(buf));
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_calloc(16, 1, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_calloc(0, 1, &ptr), AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_calloc(1, 0, &ptr), AKERR_VALUE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* realloc(3)'s trap: on failure it returns NULL and the original block is *still
|
||||||
|
* valid*, so `p = realloc(p, n)` leaks it. The wrapper takes the pointer by
|
||||||
|
* reference and leaves it untouched on failure, so the caller still holds a
|
||||||
|
* pointer it can free -- which is what this asserts by freeing it.
|
||||||
|
*/
|
||||||
|
static int test_realloc_grows_and_preserves_the_old_block_on_failure(void)
|
||||||
|
{
|
||||||
|
unsigned char *buf = NULL;
|
||||||
|
void *ptr = NULL;
|
||||||
|
size_t i = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_malloc(8, (void **)&buf));
|
||||||
|
for ( i = 0; i < 8; i++ ) {
|
||||||
|
buf[i] = (unsigned char)i;
|
||||||
|
}
|
||||||
|
|
||||||
|
ptr = buf;
|
||||||
|
AKSL_CHECK_OK(aksl_realloc(&ptr, 64));
|
||||||
|
buf = (unsigned char *)ptr;
|
||||||
|
AKSL_CHECK(buf != NULL);
|
||||||
|
/* The contents that fit are carried over. */
|
||||||
|
for ( i = 0; i < 8; i++ ) {
|
||||||
|
AKSL_CHECK(buf[i] == (unsigned char)i);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A request that cannot be met leaves ptr aimed at the original block. */
|
||||||
|
AKSL_CHECK_STATUS(aksl_realloc(&ptr, SIZE_MAX / 2), ENOMEM);
|
||||||
|
AKSL_CHECK(ptr == (void *)buf);
|
||||||
|
for ( i = 0; i < 8; i++ ) {
|
||||||
|
AKSL_CHECK(buf[i] == (unsigned char)i);
|
||||||
|
}
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_free(ptr));
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_realloc(NULL, 8), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* reallocarray's whole reason for existing: `realloc(p, n * size)` is a heap
|
||||||
|
* overflow waiting for an n large enough to wrap, and the multiplication is the
|
||||||
|
* place a caller does not think to look.
|
||||||
|
*/
|
||||||
|
static int test_reallocarray_checks_the_multiplication(void)
|
||||||
|
{
|
||||||
|
void *ptr = NULL;
|
||||||
|
unsigned char *buf = NULL;
|
||||||
|
size_t i = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_malloc(4, &ptr));
|
||||||
|
AKSL_CHECK_OK(aksl_reallocarray(&ptr, 16, 8));
|
||||||
|
buf = (unsigned char *)ptr;
|
||||||
|
for ( i = 0; i < 128; i++ ) {
|
||||||
|
buf[i] = (unsigned char)i;
|
||||||
|
}
|
||||||
|
AKSL_CHECK_OK(aksl_free(ptr));
|
||||||
|
|
||||||
|
ptr = NULL;
|
||||||
|
/* SIZE_MAX/4 members of 8 bytes wraps; without the check this would be a
|
||||||
|
* plausible-looking small allocation followed by a very large write. */
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_reallocarray(&ptr, SIZE_MAX / 4, 8),
|
||||||
|
AKERR_OUTOFBOUNDS, "overflows size_t");
|
||||||
|
AKSL_CHECK(ptr == NULL);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_reallocarray(NULL, 1, 1), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_reallocarray(&ptr, 0, 1), AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_reallocarray(&ptr, 1, 0), AKERR_VALUE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* aligned_alloc(3) requires size to be a multiple of the alignment and the
|
||||||
|
* alignment to be a power of two. Violating either is undefined behaviour that
|
||||||
|
* usually just returns NULL, so both are checked and reported as what they are.
|
||||||
|
*/
|
||||||
|
static int test_aligned_alloc_checks_its_preconditions(void)
|
||||||
|
{
|
||||||
|
void *ptr = (void *)0x1;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_aligned_alloc(64, 128, &ptr));
|
||||||
|
AKSL_CHECK(ptr != NULL);
|
||||||
|
AKSL_CHECK(((uintptr_t)ptr % 64) == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_free(ptr));
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_aligned_alloc(24, 48, &ptr),
|
||||||
|
AKERR_VALUE, "not a power of two");
|
||||||
|
AKSL_CHECK(ptr == NULL);
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_aligned_alloc(64, 100, &ptr),
|
||||||
|
AKERR_VALUE, "not a multiple of alignment");
|
||||||
|
AKSL_CHECK_STATUS(aksl_aligned_alloc(0, 64, &ptr), AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_aligned_alloc(64, 0, &ptr), AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_aligned_alloc(64, 128, NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* posix_memalign(3) breaks the errno convention: it returns the error number and
|
||||||
|
* leaves errno alone. The wrapper reports it as a status like everything else.
|
||||||
|
*/
|
||||||
|
static int test_posix_memalign(void)
|
||||||
|
{
|
||||||
|
void *ptr = (void *)0x1;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_posix_memalign(&ptr, sizeof(void *) * 2, 100));
|
||||||
|
AKSL_CHECK(ptr != NULL);
|
||||||
|
AKSL_CHECK(((uintptr_t)ptr % (sizeof(void *) * 2)) == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_free(ptr));
|
||||||
|
|
||||||
|
/* Not a multiple of sizeof(void *), which posix_memalign refuses. */
|
||||||
|
AKSL_CHECK_STATUS(aksl_posix_memalign(&ptr, 3, 100), EINVAL);
|
||||||
|
AKSL_CHECK(ptr == NULL);
|
||||||
|
AKSL_CHECK_STATUS(aksl_posix_memalign(NULL, 16, 100), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_posix_memalign(&ptr, 16, 0), AKERR_VALUE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_free_rejects_null_pointer(void)
|
||||||
|
{
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_free(NULL),
|
||||||
|
AKERR_NULLPOINTER, "NULL");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* aksl_freep clears the caller's pointer, so a second free is caught. */
|
||||||
|
static int test_freep_clears_the_pointer(void)
|
||||||
|
{
|
||||||
|
void *ptr = NULL;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_malloc(16, &ptr));
|
||||||
|
AKSL_CHECK(ptr != NULL);
|
||||||
|
AKSL_CHECK_OK(aksl_freep(&ptr));
|
||||||
|
AKSL_CHECK(ptr == NULL);
|
||||||
|
/* The second attempt is now an error instead of a double free. */
|
||||||
|
AKSL_CHECK_STATUS(aksl_freep(&ptr), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_freep(NULL), AKERR_NULLPOINTER);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO.md 1.1's open question, decided: overlap is AKERR_VALUE. memcpy(3) calls
|
||||||
|
* it undefined behaviour, which in practice means "works until the day a
|
||||||
|
* compiler version or a length changes and it does not". Callers who mean to
|
||||||
|
* overlap want aksl_memmove, and the message says so.
|
||||||
|
*/
|
||||||
|
static int test_memcpy_rejects_overlapping_ranges(void)
|
||||||
|
{
|
||||||
|
unsigned char buf[8] = { 0, 1, 2, 3, 4, 5, 6, 7 };
|
||||||
|
|
||||||
|
/* Destination inside the source range. */
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_memcpy(&buf[2], &buf[0], 4),
|
||||||
|
AKERR_VALUE, "aksl_memmove");
|
||||||
|
/* Source inside the destination range. */
|
||||||
|
AKSL_CHECK_STATUS(aksl_memcpy(&buf[0], &buf[2], 4), AKERR_VALUE);
|
||||||
|
/* Adjacent but not overlapping is fine. */
|
||||||
|
AKSL_CHECK_OK(aksl_memcpy(&buf[4], &buf[0], 4));
|
||||||
|
AKSL_CHECK(buf[4] == 0 && buf[5] == 1 && buf[6] == 2 && buf[7] == 3);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_memmove_handles_overlap(void)
|
||||||
|
{
|
||||||
|
unsigned char buf[8] = { 0, 1, 2, 3, 4, 5, 6, 7 };
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_memmove(&buf[2], &buf[0], 4));
|
||||||
|
AKSL_CHECK(buf[2] == 0 && buf[3] == 1 && buf[4] == 2 && buf[5] == 3);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_memmove(NULL, buf, 1), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_memmove(buf, NULL, 1), AKERR_NULLPOINTER);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* memcmp and memchr answer through an out-param, because the return value is
|
||||||
|
* already spoken for by the error context. A memchr that finds nothing is not an
|
||||||
|
* error: "absent" is an ordinary answer to that question, so *dest is NULL and
|
||||||
|
* the call succeeds.
|
||||||
|
*/
|
||||||
|
static int test_memcmp_reports_ordering(void)
|
||||||
|
{
|
||||||
|
unsigned char a[4] = { 1, 2, 3, 4 };
|
||||||
|
unsigned char b[4] = { 1, 2, 3, 4 };
|
||||||
|
unsigned char c[4] = { 1, 2, 9, 4 };
|
||||||
|
int result = 99;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_memcmp(a, b, sizeof(a), &result));
|
||||||
|
AKSL_CHECK(result == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_memcmp(a, c, sizeof(a), &result));
|
||||||
|
AKSL_CHECK(result < 0);
|
||||||
|
AKSL_CHECK_OK(aksl_memcmp(c, a, sizeof(a), &result));
|
||||||
|
AKSL_CHECK(result > 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_memcmp(NULL, b, 4, &result), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_memcmp(a, NULL, 4, &result), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_memcmp(a, b, 4, NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_memchr_finds_or_reports_absence(void)
|
||||||
|
{
|
||||||
|
unsigned char buf[5] = { 'a', 'b', 'c', 'd', 'e' };
|
||||||
|
void *found = NULL;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_memchr(buf, 'c', sizeof(buf), &found));
|
||||||
|
AKSL_CHECK(found == (void *)&buf[2]);
|
||||||
|
|
||||||
|
/* Not found is a successful answer of "nowhere". */
|
||||||
|
AKSL_CHECK_OK(aksl_memchr(buf, 'z', sizeof(buf), &found));
|
||||||
|
AKSL_CHECK(found == NULL);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_memchr(NULL, 'a', 1, &found), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_memchr(buf, 'a', 1, NULL), 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_malloc_rejects_zero_size);
|
||||||
|
AKSL_RUN(failures, test_malloc_reports_out_of_memory);
|
||||||
|
AKSL_RUN(failures, test_malloc_free_round_trip);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_calloc_zeroes_and_guards);
|
||||||
|
AKSL_RUN(failures, test_reallocarray_checks_the_multiplication);
|
||||||
|
AKSL_RUN(failures, test_aligned_alloc_checks_its_preconditions);
|
||||||
|
AKSL_RUN(failures, test_posix_memalign);
|
||||||
|
AKSL_RUN(failures, test_realloc_grows_and_preserves_the_old_block_on_failure);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_free_rejects_null_pointer);
|
||||||
|
AKSL_RUN(failures, test_freep_clears_the_pointer);
|
||||||
|
|
||||||
|
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_overlapping_ranges);
|
||||||
|
AKSL_RUN(failures, test_memcpy_rejects_null_destination);
|
||||||
|
AKSL_RUN(failures, test_memcpy_rejects_null_source);
|
||||||
|
AKSL_RUN(failures, test_memmove_handles_overlap);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_memcmp_reports_ordering);
|
||||||
|
AKSL_RUN(failures, test_memchr_finds_or_reports_absence);
|
||||||
|
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
199
tests/test_path.c
Normal file
199
tests/test_path.c
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
/*
|
||||||
|
* aksl_realpath and aksl_realpath_alloc -- TODO.md section 1.5, now complete.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* The failure cases now pass an *uninitialised* resolved_path on purpose. That
|
||||||
|
* used to be the crash case (TODO.md 2.1.6): the wrapper's own error path
|
||||||
|
* formatted the buffer with %s while realpath(3) leaves its contents
|
||||||
|
* unspecified on failure, so the library read uninitialised memory while
|
||||||
|
* reporting an error. The message names only the input path now, and this test
|
||||||
|
* is what holds that -- it is meant to be run under the sanitizer build, where a
|
||||||
|
* regression is an immediate abort rather than a silent read of stack garbage.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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, sizeof(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, sizeof(resolved)));
|
||||||
|
AKSL_CHECK(strcmp(resolved, expected) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK(unlink(link) == 0);
|
||||||
|
AKSL_CHECK(unlink(target) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The failure path with a buffer nobody has written to. Uninitialised on
|
||||||
|
* purpose: see the header comment. Under ASan/MSan this is the test that fails
|
||||||
|
* if the error path ever starts reading resolved_path again.
|
||||||
|
*/
|
||||||
|
static int test_missing_path_reports_enoent(void)
|
||||||
|
{
|
||||||
|
char resolved[PATH_MAX];
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||||
|
aksl_realpath("/nonexistent/aksl/path", resolved, sizeof(resolved)),
|
||||||
|
ENOENT, "/nonexistent/aksl/path");
|
||||||
|
/* The message must name the path and must not quote the buffer back. */
|
||||||
|
AKSL_CHECK(strstr(aksl_last_message, "resolved") == NULL);
|
||||||
|
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));
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_realpath(child, resolved, sizeof(resolved)), ENOTDIR);
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Two symlinks pointing at each other: the kernel gives up with ELOOP. */
|
||||||
|
static int test_symlink_loop_reports_eloop(void)
|
||||||
|
{
|
||||||
|
char a[AKSL_TMP_MAX];
|
||||||
|
char b[AKSL_TMP_MAX];
|
||||||
|
char resolved[PATH_MAX];
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(a, sizeof(a)) == 0);
|
||||||
|
AKSL_CHECK(aksl_temp_file(b, sizeof(b)) == 0);
|
||||||
|
AKSL_CHECK(unlink(a) == 0);
|
||||||
|
AKSL_CHECK(unlink(b) == 0);
|
||||||
|
AKSL_CHECK(symlink(a, b) == 0);
|
||||||
|
AKSL_CHECK(symlink(b, a) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_realpath(a, resolved, sizeof(resolved)), ELOOP);
|
||||||
|
|
||||||
|
AKSL_CHECK(unlink(a) == 0);
|
||||||
|
AKSL_CHECK(unlink(b) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_rejects_null_arguments(void)
|
||||||
|
{
|
||||||
|
char resolved[PATH_MAX];
|
||||||
|
|
||||||
|
memset(resolved, 0x00, sizeof(resolved));
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath(NULL, resolved, sizeof(resolved)),
|
||||||
|
AKERR_NULLPOINTER, "path=");
|
||||||
|
/*
|
||||||
|
* TODO.md 2.1.6: this used to be unchecked, and realpath(path, NULL)
|
||||||
|
* allocated a buffer that the wrapper then discarded and leaked.
|
||||||
|
*/
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath("/tmp", NULL, PATH_MAX),
|
||||||
|
AKERR_NULLPOINTER, "resolved_path=");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* realpath(3) cannot be told how much room it has, so the only safe answer to an
|
||||||
|
* undersized buffer is to refuse before calling it. A caller who gets this back
|
||||||
|
* has a bug that would otherwise have been a stack smash.
|
||||||
|
*/
|
||||||
|
static int test_rejects_a_buffer_below_path_max(void)
|
||||||
|
{
|
||||||
|
char small[16];
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath("/tmp", small, sizeof(small)),
|
||||||
|
AKERR_OUTOFBOUNDS, "PATH_MAX");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_alloc_form_resolves_and_hands_over_the_buffer(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
char expected[PATH_MAX];
|
||||||
|
char *resolved = NULL;
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
AKSL_CHECK(realpath(path, expected) != NULL);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_realpath_alloc(path, &resolved));
|
||||||
|
AKSL_CHECK(resolved != NULL);
|
||||||
|
AKSL_CHECK(strcmp(resolved, expected) == 0);
|
||||||
|
/* The buffer is the caller's; releasing it through the library closes the
|
||||||
|
* leak that the old NULL-destination path opened. */
|
||||||
|
AKSL_CHECK_OK(aksl_free(resolved));
|
||||||
|
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_alloc_form_reports_failure_and_writes_no_pointer(void)
|
||||||
|
{
|
||||||
|
char *resolved = (char *)0x1;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||||
|
aksl_realpath_alloc("/nonexistent/aksl/path", &resolved),
|
||||||
|
ENOENT, "/nonexistent/aksl/path");
|
||||||
|
/* Cleared before the call, so a failure cannot leave a stale pointer. */
|
||||||
|
AKSL_CHECK(resolved == NULL);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_realpath_alloc(NULL, &resolved), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_realpath_alloc("/tmp", NULL), AKERR_NULLPOINTER);
|
||||||
|
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_symlink_loop_reports_eloop);
|
||||||
|
AKSL_RUN(failures, test_rejects_null_arguments);
|
||||||
|
AKSL_RUN(failures, test_rejects_a_buffer_below_path_max);
|
||||||
|
AKSL_RUN(failures, test_alloc_form_resolves_and_hands_over_the_buffer);
|
||||||
|
AKSL_RUN(failures, test_alloc_form_reports_failure_and_writes_no_pointer);
|
||||||
|
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
410
tests/test_pool.c
Normal file
410
tests/test_pool.c
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
/*
|
||||||
|
* Cross-cutting properties every wrapper has to have -- TODO.md section 1.9.
|
||||||
|
*
|
||||||
|
* Two of them, and neither is about what any individual function computes:
|
||||||
|
*
|
||||||
|
* Error-pool accounting. libakerror hands out error contexts from a fixed
|
||||||
|
* process-global array of AKERR_MAX_ARRAY_ERROR slots. A wrapper that raises
|
||||||
|
* an error and forgets to release it does not fail visibly -- it fails
|
||||||
|
* AKERR_MAX_ARRAY_ERROR calls later, in whatever unrelated code happens to ask
|
||||||
|
* for a slot next, by which time the leak is nowhere near the symptom. So
|
||||||
|
* every failure path here is driven AKERR_MAX_ARRAY_ERROR + 10 times, which is
|
||||||
|
* past exhaustion several times over, with the pool checked after each round.
|
||||||
|
*
|
||||||
|
* Stack-trace content. An error that reports the wrong origin is worse than
|
||||||
|
* useless when the only debugging you get is a log file after the fact, which
|
||||||
|
* is the stated reason this library exists at all. Each assertion here names
|
||||||
|
* the function the error must say it came from and the source file it must
|
||||||
|
* name, so a FAIL that migrates into a helper is caught.
|
||||||
|
*
|
||||||
|
* AKSL_RUN already checks the pool after every test in every file. This one
|
||||||
|
* checks it inside the loop as well, because a leak of one slot per call needs
|
||||||
|
* more than one call to show up.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <limits.h>
|
||||||
|
|
||||||
|
/* Rounds enough to exhaust the pool several times over. */
|
||||||
|
#define ROUNDS (AKERR_MAX_ARRAY_ERROR + 10)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Assert the last error came from `func` in a file whose name ends in `file`.
|
||||||
|
* The recorded fname is whatever __FILE__ expanded to at the FAIL site, which is
|
||||||
|
* an absolute path under CMake, so this matches on the tail rather than the
|
||||||
|
* whole thing.
|
||||||
|
*/
|
||||||
|
static int came_from(const char *func, const char *file)
|
||||||
|
{
|
||||||
|
size_t flen = strlen(file);
|
||||||
|
size_t rlen = strlen(aksl_last_file);
|
||||||
|
|
||||||
|
if ( strcmp(aksl_last_function, func) != 0 ) {
|
||||||
|
fprintf(stderr, " CHECK FAILED: error says it came from \"%s\", expected \"%s\"\n",
|
||||||
|
aksl_last_function, func);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if ( rlen < flen || strcmp(aksl_last_file + (rlen - flen), file) != 0 ) {
|
||||||
|
fprintf(stderr, " CHECK FAILED: error names file \"%s\", expected one ending \"%s\"\n",
|
||||||
|
aksl_last_file, file);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if ( aksl_last_line <= 0 ) {
|
||||||
|
fprintf(stderr, " CHECK FAILED: error records line %d\n", aksl_last_line);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Pool accounting */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_memory_wrappers_do_not_leak_slots(void)
|
||||||
|
{
|
||||||
|
void *ptr = NULL;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < ROUNDS; i++ ) {
|
||||||
|
AKSL_CHECK_STATUS(aksl_malloc(32, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_malloc(0, &ptr), AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_calloc(0, 1, &ptr), AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_realloc(NULL, 8), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_free(NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_freep(NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_memset(NULL, 0, 1), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_memcpy(NULL, "a", 1), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_memmove(NULL, "a", 1), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_memcmp(NULL, "a", 1, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_memchr(NULL, 'a', 1, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_stream_wrappers_do_not_leak_slots(void)
|
||||||
|
{
|
||||||
|
FILE *fp = NULL;
|
||||||
|
size_t moved = 0;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < ROUNDS; i++ ) {
|
||||||
|
AKSL_CHECK_STATUS(aksl_fopen(NULL, "r", &fp), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fopen("/nonexistent/aksl/x", "r", &fp), ENOENT);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fread(NULL, 1, 1, stdin, &moved), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fwrite(NULL, 1, 1, stdout, &moved), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fclose(NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fseek(NULL, 0, SEEK_SET), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_ftell(NULL, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fgetc(NULL, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fputs(NULL, stdout), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_remove("/nonexistent/aksl/x"), ENOENT);
|
||||||
|
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_format_wrappers_do_not_leak_slots(void)
|
||||||
|
{
|
||||||
|
char buf[16];
|
||||||
|
int count = 0;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < ROUNDS; i++ ) {
|
||||||
|
AKSL_CHECK_STATUS(aksl_printf(NULL, "x"), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fprintf(NULL, stdout, "x"), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_snprintf(NULL, buf, sizeof(buf), "x"), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_snprintf(&count, buf, 4, "%s", "far too long"),
|
||||||
|
AKERR_OUTOFBOUNDS);
|
||||||
|
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_conversion_wrappers_do_not_leak_slots(void)
|
||||||
|
{
|
||||||
|
int iout = 0;
|
||||||
|
long lout = 0;
|
||||||
|
unsigned long ulout = 0;
|
||||||
|
double dout = 0.0;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < ROUNDS; i++ ) {
|
||||||
|
AKSL_CHECK_STATUS(aksl_atoi(NULL, &iout), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_atoi("junk", &iout), AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_atoi("99999999999999999999", &iout), ERANGE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtol("junk", NULL, 10, &lout), AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtoul("-1", NULL, 10, &ulout), AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtod("junk", NULL, &dout), AKERR_VALUE);
|
||||||
|
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_string_wrappers_do_not_leak_slots(void)
|
||||||
|
{
|
||||||
|
char buf[8];
|
||||||
|
char *at = NULL;
|
||||||
|
size_t n = 0;
|
||||||
|
int r = 0;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < ROUNDS; i++ ) {
|
||||||
|
AKSL_CHECK_STATUS(aksl_strlen(NULL, &n), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcpy(buf, sizeof(buf), "far too long for eight"),
|
||||||
|
AKERR_OUTOFBOUNDS);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcat(buf, 0, "x"), AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strdup(NULL, &at), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcmp(NULL, "b", &r), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strchr(NULL, 'a', &at), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strerror(0, buf, 0), AKERR_VALUE);
|
||||||
|
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_collection_wrappers_do_not_leak_slots(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode node;
|
||||||
|
aksl_ListNode *head = NULL;
|
||||||
|
aksl_List list;
|
||||||
|
aksl_TreeNode tree;
|
||||||
|
aksl_HashMap map;
|
||||||
|
aksl_HashEntry slots[4];
|
||||||
|
aksl_StrBuf strbuf;
|
||||||
|
size_t n = 0;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_list_init(&list));
|
||||||
|
AKSL_CHECK_OK(aksl_tree_node_init(&tree, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
|
||||||
|
memset((void *)&strbuf, 0x00, sizeof(strbuf));
|
||||||
|
|
||||||
|
for ( i = 0; i < ROUNDS; i++ ) {
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_append(NULL, &node), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_pop(&head, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_prepend(NULL, &node), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_length(&node, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_remove(&list, &node), AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree, NULL, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_PREORDER, NULL),
|
||||||
|
AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_tree_count(&tree, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_hashmap_get(&map, "absent", NULL, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strbuf_append(&strbuf, "x"), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strhash_djb2(NULL, 0, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||||
|
}
|
||||||
|
(void)n;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A traversal that fails part-way through has the most opportunities to leak: an
|
||||||
|
* error is raised inside a callback, propagated up through several frames, and
|
||||||
|
* handled or not at the top. Drive that repeatedly too.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext AKERR_NOIGNORE *always_fails(aksl_TreeNode *node, void *data)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
(void)node;
|
||||||
|
(void)data;
|
||||||
|
FAIL_RETURN(e, AKERR_VALUE, "callback always fails");
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext AKERR_NOIGNORE *always_breaks(aksl_TreeNode *node, void *data)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
(void)node;
|
||||||
|
(void)data;
|
||||||
|
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "callback always breaks");
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_traversal_failures_do_not_leak_slots(void)
|
||||||
|
{
|
||||||
|
aksl_TreeNode tree[3];
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < 3; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_tree_node_init(&tree[i], NULL));
|
||||||
|
}
|
||||||
|
tree[0].left = &tree[1];
|
||||||
|
tree[0].right = &tree[2];
|
||||||
|
|
||||||
|
for ( i = 0; i < ROUNDS; i++ ) {
|
||||||
|
/* Propagated out of the recursion. */
|
||||||
|
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], &always_fails, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_PREORDER, NULL),
|
||||||
|
AKERR_VALUE);
|
||||||
|
/* Swallowed at the top -- the released-not-returned path. */
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &always_breaks, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_PREORDER, NULL));
|
||||||
|
/* And through the breadth-first walk, which also drains a queue. */
|
||||||
|
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], &always_fails, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_BFS, NULL),
|
||||||
|
AKERR_VALUE);
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &always_breaks, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_BFS, NULL));
|
||||||
|
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Stack-trace content */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Each of these asserts that the error names the function that raised it and
|
||||||
|
* the file that function lives in. This is what catches a FAIL that gets moved
|
||||||
|
* into a shared helper during a refactor: the status stays right, the message
|
||||||
|
* stays right, and the origin quietly starts pointing at the wrong place.
|
||||||
|
*/
|
||||||
|
static int test_errors_name_their_origin_in_stdlib(void)
|
||||||
|
{
|
||||||
|
void *ptr = NULL;
|
||||||
|
int count = 0;
|
||||||
|
char resolved[PATH_MAX];
|
||||||
|
uint32_t h = 0;
|
||||||
|
aksl_ListNode node;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_malloc(32, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(came_from("aksl_malloc", "src/stdlib.c") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_free(NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(came_from("aksl_free", "src/stdlib.c") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_memcpy(NULL, "a", 1), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(came_from("aksl_memcpy", "src/stdlib.c") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_fopen(NULL, "r", (FILE **)&ptr), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(came_from("aksl_fopen", "src/stdlib.c") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_fclose(NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(came_from("aksl_fclose", "src/stdlib.c") == 0);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* aksl_printf forwards to aksl_vprintf, so the error legitimately comes
|
||||||
|
* from the latter -- the variadic form is a three-line wrapper with no FAIL
|
||||||
|
* of its own. Asserting the real origin rather than the one a reader might
|
||||||
|
* expect is the point of recording it.
|
||||||
|
*/
|
||||||
|
AKSL_CHECK_STATUS(aksl_printf(NULL, "x"), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(came_from("aksl_vprintf", "src/stdlib.c") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_snprintf(&count, NULL, 8, "x"), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(came_from("aksl_vsnprintf", "src/stdlib.c") == 0);
|
||||||
|
|
||||||
|
/* Likewise, the ato* forms are calls into the strto* ones. */
|
||||||
|
AKSL_CHECK_STATUS(aksl_atol("junk", (long *)&ptr), AKERR_VALUE);
|
||||||
|
AKSL_CHECK(came_from("aksl_strtol", "src/stdlib.c") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_realpath(NULL, resolved, sizeof(resolved)), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(came_from("aksl_realpath", "src/stdlib.c") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strhash_djb2(NULL, 0, &h), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(came_from("aksl_strhash_djb2", "src/stdlib.c") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_append(NULL, &node), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(came_from("aksl_list_append", "src/stdlib.c") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_tree_iterate(NULL, NULL, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_PREORDER, NULL),
|
||||||
|
AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(came_from("aksl_tree_iterate", "src/stdlib.c") == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_errors_name_their_origin_in_string_and_stream(void)
|
||||||
|
{
|
||||||
|
char buf[8];
|
||||||
|
size_t n = 0;
|
||||||
|
long pos = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strlen(NULL, &n), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(came_from("aksl_strlen", "src/string.c") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcpy(buf, sizeof(buf), "far too long for eight"),
|
||||||
|
AKERR_OUTOFBOUNDS);
|
||||||
|
AKSL_CHECK(came_from("aksl_strcpy", "src/string.c") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strdup(NULL, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(came_from("aksl_strdup", "src/string.c") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strerror(0, buf, 0), AKERR_VALUE);
|
||||||
|
AKSL_CHECK(came_from("aksl_strerror", "src/string.c") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_fseek(NULL, 0, SEEK_SET), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(came_from("aksl_fseek", "src/stream.c") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_ftell(NULL, &pos), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(came_from("aksl_ftell", "src/stream.c") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_getline(NULL, NULL, NULL, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(came_from("aksl_getline", "src/stream.c") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_mkdtemp("no-placeholder"), AKERR_VALUE);
|
||||||
|
AKSL_CHECK(came_from("aksl_mkdtemp", "src/stream.c") == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_errors_name_their_origin_in_collections(void)
|
||||||
|
{
|
||||||
|
aksl_ListNode node;
|
||||||
|
aksl_List list;
|
||||||
|
aksl_HashMap map;
|
||||||
|
aksl_HashEntry slots[4];
|
||||||
|
aksl_StrBuf strbuf;
|
||||||
|
char longkey[AKSL_HASHMAP_MAX_KEY + 4];
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_list_node_init(&node, NULL));
|
||||||
|
AKSL_CHECK_OK(aksl_list_init(&list));
|
||||||
|
AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4));
|
||||||
|
memset((void *)&strbuf, 0x00, sizeof(strbuf));
|
||||||
|
memset(longkey, 'k', sizeof(longkey) - 1);
|
||||||
|
longkey[sizeof(longkey) - 1] = '\0';
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_prepend(NULL, &node), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(came_from("aksl_list_prepend", "src/collections.c") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_list_remove(&list, &node), AKERR_VALUE);
|
||||||
|
AKSL_CHECK(came_from("aksl_list_remove", "src/collections.c") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_tree_insert(NULL, NULL, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(came_from("aksl_tree_insert", "src/collections.c") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_hashmap_put(&map, longkey, NULL), AKERR_OUTOFBOUNDS);
|
||||||
|
AKSL_CHECK(came_from("aksl_hashmap_put", "src/collections.c") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strhash_fnv1a(NULL, 0, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(came_from("aksl_strhash_fnv1a", "src/collections.c") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strbuf_reset(&strbuf), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(came_from("aksl_strbuf_reset", "src/collections.c") == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
akerr_init();
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_memory_wrappers_do_not_leak_slots);
|
||||||
|
AKSL_RUN(failures, test_stream_wrappers_do_not_leak_slots);
|
||||||
|
AKSL_RUN(failures, test_format_wrappers_do_not_leak_slots);
|
||||||
|
AKSL_RUN(failures, test_conversion_wrappers_do_not_leak_slots);
|
||||||
|
AKSL_RUN(failures, test_string_wrappers_do_not_leak_slots);
|
||||||
|
AKSL_RUN(failures, test_collection_wrappers_do_not_leak_slots);
|
||||||
|
AKSL_RUN(failures, test_traversal_failures_do_not_leak_slots);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_errors_name_their_origin_in_stdlib);
|
||||||
|
AKSL_RUN(failures, test_errors_name_their_origin_in_string_and_stream);
|
||||||
|
AKSL_RUN(failures, test_errors_name_their_origin_in_collections);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
258
tests/test_strbuf.c
Normal file
258
tests/test_strbuf.c
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
/*
|
||||||
|
* The growable string buffer -- src/collections.c, TODO.md 3.6.
|
||||||
|
*
|
||||||
|
* The bounded formatting wrappers are the right answer when the destination is
|
||||||
|
* a fixed buffer and no answer at all when the length is not known in advance.
|
||||||
|
* This is that answer, and the properties worth holding onto are that it always
|
||||||
|
* grows enough, always stays NUL-terminated, and always says so when it cannot.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
static int test_init_and_free(void)
|
||||||
|
{
|
||||||
|
aksl_StrBuf buf;
|
||||||
|
const char *s = NULL;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
|
||||||
|
AKSL_CHECK(buf.data != NULL);
|
||||||
|
AKSL_CHECK(buf.length == 0);
|
||||||
|
/* A zero request is raised to the minimum rather than refused. */
|
||||||
|
AKSL_CHECK(buf.capacity >= 32);
|
||||||
|
/* Valid as a C string immediately, with no finalise step. */
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
|
||||||
|
AKSL_CHECK(strcmp(s, "") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
|
||||||
|
AKSL_CHECK(buf.data == NULL);
|
||||||
|
AKSL_CHECK(buf.capacity == 0);
|
||||||
|
/* Freeing twice is an error rather than a double free, as aksl_freep arranges. */
|
||||||
|
AKSL_CHECK_STATUS(aksl_strbuf_free(&buf), AKERR_NULLPOINTER);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strbuf_init(NULL, 16), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strbuf_free(NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_append_concatenates(void)
|
||||||
|
{
|
||||||
|
aksl_StrBuf buf;
|
||||||
|
const char *s = NULL;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "hello"));
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_append_char(&buf, ' '));
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "world"));
|
||||||
|
AKSL_CHECK(buf.length == 11);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
|
||||||
|
AKSL_CHECK(strcmp(s, "hello world") == 0);
|
||||||
|
|
||||||
|
/* Appending nothing is a no-op, not an error. */
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_append(&buf, ""));
|
||||||
|
AKSL_CHECK(buf.length == 11);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Growth is the whole point. Appending far past the initial capacity has to
|
||||||
|
* reallocate, and everything written before the move has to survive it.
|
||||||
|
*/
|
||||||
|
static int test_growth_preserves_the_contents(void)
|
||||||
|
{
|
||||||
|
aksl_StrBuf buf;
|
||||||
|
const char *s = NULL;
|
||||||
|
size_t initial = 0;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 32));
|
||||||
|
initial = buf.capacity;
|
||||||
|
|
||||||
|
for ( i = 0; i < 1000; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "0123456789"));
|
||||||
|
}
|
||||||
|
AKSL_CHECK(buf.length == 10000);
|
||||||
|
AKSL_CHECK(buf.capacity > initial);
|
||||||
|
AKSL_CHECK(buf.capacity >= buf.length + 1);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
|
||||||
|
AKSL_CHECK(strlen(s) == 10000);
|
||||||
|
/* Both ends, so a botched realloc shows up wherever it happened. */
|
||||||
|
AKSL_CHECK(strncmp(s, "0123456789", 10) == 0);
|
||||||
|
AKSL_CHECK(strcmp(s + 9990, "0123456789") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bytes rather than a string, so an embedded NUL can be appended deliberately. */
|
||||||
|
static int test_append_bytes_keeps_embedded_nuls(void)
|
||||||
|
{
|
||||||
|
aksl_StrBuf buf;
|
||||||
|
const char *s = NULL;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_append_bytes(&buf, "ab\0cd", 5));
|
||||||
|
/* length counts all five; the C string view stops at the first NUL. */
|
||||||
|
AKSL_CHECK(buf.length == 5);
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
|
||||||
|
AKSL_CHECK(strlen(s) == 2);
|
||||||
|
AKSL_CHECK(memcmp(s, "ab\0cd", 5) == 0);
|
||||||
|
/* And the terminator is still there past the end. */
|
||||||
|
AKSL_CHECK(s[5] == '\0');
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Formatted append. vsnprintf is measured first and then written, so a long
|
||||||
|
* result grows the buffer rather than truncating -- which is the difference
|
||||||
|
* between this and aksl_snprintf into a fixed array.
|
||||||
|
*/
|
||||||
|
static int test_appendf_formats_and_grows(void)
|
||||||
|
{
|
||||||
|
aksl_StrBuf buf;
|
||||||
|
const char *s = NULL;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 8));
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_appendf(&buf, "%s=%d", "x", 7));
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
|
||||||
|
AKSL_CHECK(strcmp(s, "x=7") == 0);
|
||||||
|
|
||||||
|
/* Far longer than the 8 bytes it started with. */
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_appendf(&buf, " %s", "and a good deal more text than that"));
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
|
||||||
|
AKSL_CHECK(strcmp(s, "x=7 and a good deal more text than that") == 0);
|
||||||
|
|
||||||
|
/* Repeated formatted appends, to exercise the measure/write pair often. */
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_reset(&buf));
|
||||||
|
for ( i = 0; i < 200; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_appendf(&buf, "[%03d]", i));
|
||||||
|
}
|
||||||
|
AKSL_CHECK(buf.length == 1000);
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
|
||||||
|
AKSL_CHECK(strncmp(s, "[000][001][002]", 15) == 0);
|
||||||
|
AKSL_CHECK(strcmp(s + 995, "[199]") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* An empty format produces nothing and is not an error. */
|
||||||
|
static int test_appendf_with_no_output(void)
|
||||||
|
{
|
||||||
|
aksl_StrBuf buf;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_appendf(&buf, "%s", ""));
|
||||||
|
AKSL_CHECK(buf.length == 0);
|
||||||
|
AKSL_CHECK(buf.data[0] == '\0');
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* reset empties without releasing, so the capacity is reused. */
|
||||||
|
static int test_reset_keeps_the_capacity(void)
|
||||||
|
{
|
||||||
|
aksl_StrBuf buf;
|
||||||
|
const char *s = NULL;
|
||||||
|
size_t grown = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "something reasonably long to force a grow"));
|
||||||
|
grown = buf.capacity;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_reset(&buf));
|
||||||
|
AKSL_CHECK(buf.length == 0);
|
||||||
|
AKSL_CHECK(buf.capacity == grown);
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
|
||||||
|
AKSL_CHECK(strcmp(s, "") == 0);
|
||||||
|
|
||||||
|
/* And it still works afterwards. */
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "again"));
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
|
||||||
|
AKSL_CHECK(strcmp(s, "again") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Every entry point checks that the buffer was initialised, so a zeroed
|
||||||
|
* aksl_StrBuf on the stack is an error rather than a NULL dereference.
|
||||||
|
*/
|
||||||
|
static int test_rejects_null_and_uninitialised(void)
|
||||||
|
{
|
||||||
|
aksl_StrBuf buf;
|
||||||
|
aksl_StrBuf zeroed;
|
||||||
|
const char *s = NULL;
|
||||||
|
|
||||||
|
memset((void *)&zeroed, 0x00, sizeof(zeroed));
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strbuf_append(NULL, "x"), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strbuf_append(&buf, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strbuf_append_bytes(NULL, "x", 1), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strbuf_append_bytes(&buf, NULL, 1), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strbuf_append_char(NULL, 'x'), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strbuf_appendf(NULL, "x"), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strbuf_appendf(&buf, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strbuf_reset(NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strbuf_cstr(NULL, &s), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strbuf_cstr(&buf, NULL), AKERR_NULLPOINTER);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strbuf_append(&zeroed, "x"), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strbuf_appendf(&zeroed, "x"), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strbuf_reset(&zeroed), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strbuf_cstr(&zeroed, &s), AKERR_NULLPOINTER);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The reason this type exists, written the way a caller would: build a report of
|
||||||
|
* unknown length out of pieces, without anyone having to size a buffer first.
|
||||||
|
*/
|
||||||
|
static int test_builds_a_report(void)
|
||||||
|
{
|
||||||
|
aksl_StrBuf buf;
|
||||||
|
const char *s = NULL;
|
||||||
|
static const char *names[3] = { "alpha", "beta", "gamma" };
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_init(&buf, 0));
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_append(&buf, "items:"));
|
||||||
|
for ( i = 0; i < 3; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_appendf(&buf, " %s(%d)", names[i], i));
|
||||||
|
}
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_cstr(&buf, &s));
|
||||||
|
AKSL_CHECK(strcmp(s, "items: alpha(0) beta(1) gamma(2)") == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_strbuf_free(&buf));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
akerr_init();
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_init_and_free);
|
||||||
|
AKSL_RUN(failures, test_append_concatenates);
|
||||||
|
AKSL_RUN(failures, test_growth_preserves_the_contents);
|
||||||
|
AKSL_RUN(failures, test_append_bytes_keeps_embedded_nuls);
|
||||||
|
AKSL_RUN(failures, test_appendf_formats_and_grows);
|
||||||
|
AKSL_RUN(failures, test_appendf_with_no_output);
|
||||||
|
AKSL_RUN(failures, test_reset_keeps_the_capacity);
|
||||||
|
AKSL_RUN(failures, test_rejects_null_and_uninitialised);
|
||||||
|
AKSL_RUN(failures, test_builds_a_report);
|
||||||
|
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
330
tests/test_stream.c
Normal file
330
tests/test_stream.c
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
/*
|
||||||
|
* Stream wrappers: aksl_fopen / aksl_fread / aksl_fwrite / aksl_fclose.
|
||||||
|
*
|
||||||
|
* TODO.md section 1.2, now complete. The happy paths, the round trip, every
|
||||||
|
* NULL guard, both stream-error statuses, the transferred-member count that
|
||||||
|
* aksl_fread and aksl_fwrite report through nmemb_out, and the three cases that
|
||||||
|
* needed a hostile file to produce: a mode-denied path (EACCES), a full device
|
||||||
|
* (/dev/full) for the short write, and a stream whose buffered flush fails at
|
||||||
|
* fclose time.
|
||||||
|
*
|
||||||
|
* The /dev/full tests are skipped where the device is absent -- it is a Linux
|
||||||
|
* thing -- rather than failing, and say so on stderr so a skip cannot be
|
||||||
|
* mistaken for a pass.
|
||||||
|
*
|
||||||
|
* 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>
|
||||||
|
#include <sys/stat.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");
|
||||||
|
/* *fp is cleared before the call, so a failed open cannot leave garbage. */
|
||||||
|
AKSL_CHECK(fp == NULL);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A file the caller cannot open for reading is EACCES rather than ENOENT.
|
||||||
|
* Skipped under a user who bypasses the permission bits -- root, or anything
|
||||||
|
* holding CAP_DAC_OVERRIDE -- where chmod 000 simply does not deny anything.
|
||||||
|
*/
|
||||||
|
static int test_fopen_reports_permission_denied(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
AKSL_CHECK(chmod(path, 0000) == 0);
|
||||||
|
|
||||||
|
if ( geteuid() == 0 ) {
|
||||||
|
fprintf(stderr, " (skipped: running as root, chmod 000 denies nothing)\n");
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(path, "r", &fp), EACCES, path);
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_fopen_rejects_null_arguments(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(path, "r", NULL),
|
||||||
|
AKERR_NULLPOINTER, "fp=");
|
||||||
|
/* TODO.md 2.2.2: both of these used to go straight through to fopen(3). */
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(NULL, "r", &fp),
|
||||||
|
AKERR_NULLPOINTER, "pathname=");
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(path, NULL, &fp),
|
||||||
|
AKERR_NULLPOINTER, "mode=");
|
||||||
|
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)];
|
||||||
|
size_t moved = 0;
|
||||||
|
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, &moved));
|
||||||
|
AKSL_CHECK(moved == sizeof(payload));
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
|
||||||
|
fp = NULL;
|
||||||
|
moved = 0;
|
||||||
|
memset(readback, 0x00, sizeof(readback));
|
||||||
|
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
|
||||||
|
AKSL_CHECK_OK(aksl_fread(readback, 1, sizeof(readback), fp, &moved));
|
||||||
|
AKSL_CHECK(moved == sizeof(readback));
|
||||||
|
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 --
|
||||||
|
* and nmemb_out says how many did arrive, which is the entire reason for
|
||||||
|
* distinguishing EOF from an error rather than reporting both as AKERR_IO.
|
||||||
|
*/
|
||||||
|
static int test_fread_short_read_is_eof_and_reports_the_count(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
char buf[32];
|
||||||
|
size_t moved = 0;
|
||||||
|
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, &moved));
|
||||||
|
AKSL_CHECK(moved == 4);
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
|
||||||
|
fp = NULL;
|
||||||
|
moved = 99;
|
||||||
|
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, &moved),
|
||||||
|
AKERR_EOF, "EOF");
|
||||||
|
/* TODO.md 2.2.3: this is what the caller could not previously find out. */
|
||||||
|
AKSL_CHECK(moved == 4);
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
|
||||||
|
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 -- and the status is the errno the C library actually
|
||||||
|
* saw, EBADF, rather than a flat AKERR_IO.
|
||||||
|
*
|
||||||
|
* That is a change from the old wrapper, which read ferror() and reported
|
||||||
|
* AKERR_IO without ever consulting errno. Routing it through AKSL_ERRNO_OR
|
||||||
|
* (TODO.md 2.2.1) keeps AKERR_IO as the fallback for the case where the stream
|
||||||
|
* is in error and errno says nothing, and hands back the real reason otherwise.
|
||||||
|
*/
|
||||||
|
static int test_fread_from_write_only_stream_reports_errno(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
char buf[4];
|
||||||
|
size_t moved = 99;
|
||||||
|
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, &moved),
|
||||||
|
EBADF, "of 4 members");
|
||||||
|
AKSL_CHECK(moved == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_fread_rejects_null_arguments(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
char buf[4];
|
||||||
|
size_t moved = 0;
|
||||||
|
FILE *fp = NULL;
|
||||||
|
|
||||||
|
memset(buf, 0x00, sizeof(buf));
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), NULL, &moved),
|
||||||
|
AKERR_NULLPOINTER, "fp=");
|
||||||
|
/* TODO.md 2.2.3: ptr was never checked in either direction. */
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(NULL, 1, sizeof(buf), fp, &moved),
|
||||||
|
AKERR_NULLPOINTER, "ptr=");
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), fp, NULL),
|
||||||
|
AKERR_NULLPOINTER, "nmemb_out=");
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mirror image of the fread case: a "r" stream cannot be written to. */
|
||||||
|
static int test_fwrite_to_read_only_stream_reports_errno(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
size_t moved = 99;
|
||||||
|
FILE *fp = NULL;
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fwrite("xy", 1, 2, fp, &moved),
|
||||||
|
EBADF, "wrote 0 of 2 members");
|
||||||
|
AKSL_CHECK(moved == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_fwrite_rejects_null_arguments(void)
|
||||||
|
{
|
||||||
|
size_t moved = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fwrite("xy", 1, 2, NULL, &moved),
|
||||||
|
AKERR_NULLPOINTER, "fp=");
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fwrite(NULL, 1, 2, stdout, &moved),
|
||||||
|
AKERR_NULLPOINTER, "ptr=");
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fwrite("xy", 1, 2, stdout, NULL),
|
||||||
|
AKERR_NULLPOINTER, "nmemb_out=");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* /dev/full accepts an open for writing and then fails every write with ENOSPC.
|
||||||
|
* It is the only convenient way to reach a genuinely short write on a stream
|
||||||
|
* that is otherwise perfectly healthy.
|
||||||
|
*/
|
||||||
|
static int test_fwrite_to_full_device_reports_enospc(void)
|
||||||
|
{
|
||||||
|
char payload[8192];
|
||||||
|
size_t moved = 99;
|
||||||
|
FILE *fp = NULL;
|
||||||
|
|
||||||
|
if ( access("/dev/full", W_OK) != 0 ) {
|
||||||
|
fprintf(stderr, " (skipped: no writable /dev/full on this platform)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(payload, 'x', sizeof(payload));
|
||||||
|
AKSL_CHECK_OK(aksl_fopen("/dev/full", "w", &fp));
|
||||||
|
/*
|
||||||
|
* Larger than any plausible stdio buffer, so the write reaches the device
|
||||||
|
* inside fwrite rather than being deferred to fclose -- the deferred case is
|
||||||
|
* the next test.
|
||||||
|
*/
|
||||||
|
AKSL_CHECK_STATUS(aksl_fwrite(payload, 1, sizeof(payload), fp, &moved), ENOSPC);
|
||||||
|
AKSL_CHECK(moved < sizeof(payload));
|
||||||
|
/*
|
||||||
|
* fclose succeeds here, and that is not a contradiction: the write already
|
||||||
|
* reached the device and failed, so by close time there is nothing left
|
||||||
|
* buffered to fail on. The deferred case -- where the write fits in the
|
||||||
|
* stdio buffer and only fclose finds out -- is the next test. Take the
|
||||||
|
* context however it comes back rather than asserting either way, since
|
||||||
|
* whether the error indicator survives to fclose is a stdio implementation
|
||||||
|
* detail and not part of this library's contract.
|
||||||
|
*/
|
||||||
|
(void)aksl_take(aksl_fclose(fp));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The write that fits in the stdio buffer succeeds, and the failure surfaces
|
||||||
|
* only when fclose flushes it. A caller who checks fwrite and ignores fclose
|
||||||
|
* loses the data silently, which is why aksl_fclose reports errno rather than
|
||||||
|
* just a non-zero return.
|
||||||
|
*/
|
||||||
|
static int test_fclose_surfaces_a_failed_flush(void)
|
||||||
|
{
|
||||||
|
size_t moved = 0;
|
||||||
|
FILE *fp = NULL;
|
||||||
|
|
||||||
|
if ( access("/dev/full", W_OK) != 0 ) {
|
||||||
|
fprintf(stderr, " (skipped: no writable /dev/full on this platform)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_fopen("/dev/full", "w", &fp));
|
||||||
|
AKSL_CHECK_OK(aksl_fwrite("small", 1, 5, fp, &moved));
|
||||||
|
AKSL_CHECK(moved == 5);
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fclose(fp), ENOSPC, "buffered data is lost");
|
||||||
|
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_reports_permission_denied);
|
||||||
|
AKSL_RUN(failures, test_fopen_rejects_null_arguments);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_write_read_round_trip);
|
||||||
|
AKSL_RUN(failures, test_fread_short_read_is_eof_and_reports_the_count);
|
||||||
|
AKSL_RUN(failures, test_fread_from_write_only_stream_reports_errno);
|
||||||
|
AKSL_RUN(failures, test_fread_rejects_null_arguments);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_fwrite_to_read_only_stream_reports_errno);
|
||||||
|
AKSL_RUN(failures, test_fwrite_rejects_null_arguments);
|
||||||
|
AKSL_RUN(failures, test_fwrite_to_full_device_reports_enospc);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_fclose_surfaces_a_failed_flush);
|
||||||
|
AKSL_RUN(failures, test_fclose_rejects_null_stream);
|
||||||
|
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
710
tests/test_streamio.c
Normal file
710
tests/test_streamio.c
Normal file
@@ -0,0 +1,710 @@
|
|||||||
|
/*
|
||||||
|
* Stream wrappers beyond open/read/write/close -- src/stream.c, TODO.md 3.1.
|
||||||
|
*
|
||||||
|
* tests/test_stream.c covers fopen/fread/fwrite/fclose. This one covers
|
||||||
|
* positioning, flushing, character and line I/O, stream state, and formatted
|
||||||
|
* input.
|
||||||
|
*
|
||||||
|
* The recurring assertion is the one the section exists for: where stdio folds
|
||||||
|
* "no more data" and "something broke" into a single sentinel -- EOF from
|
||||||
|
* fgetc, NULL from fgets, -1L from ftell -- the wrapper separates them, so
|
||||||
|
* AKERR_EOF ends a read loop and anything else is a fault.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
|
||||||
|
/* A temp file containing exactly `text`, opened for reading. */
|
||||||
|
static int open_with_text(const char *text, char *path, size_t pathlen, FILE **fp)
|
||||||
|
{
|
||||||
|
FILE *out = NULL;
|
||||||
|
|
||||||
|
if ( aksl_temp_file(path, pathlen) != 0 ) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
out = fopen(path, "w");
|
||||||
|
if ( out == NULL ) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if ( text[0] != '\0' && fputs(text, out) == EOF ) {
|
||||||
|
fclose(out);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if ( fclose(out) != 0 ) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
*fp = fopen(path, "r");
|
||||||
|
return (*fp == NULL) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Positioning */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_seek_and_tell(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
long pos = -1;
|
||||||
|
int c = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK(open_with_text("0123456789", path, sizeof(path), &fp) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_ftell(fp, &pos));
|
||||||
|
AKSL_CHECK(pos == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_fseek(fp, 4, SEEK_SET));
|
||||||
|
AKSL_CHECK_OK(aksl_ftell(fp, &pos));
|
||||||
|
AKSL_CHECK(pos == 4);
|
||||||
|
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
|
||||||
|
AKSL_CHECK(c == '4');
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_fseek(fp, -1, SEEK_END));
|
||||||
|
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
|
||||||
|
AKSL_CHECK(c == '9');
|
||||||
|
|
||||||
|
/* rewind is the fseek whose failure rewind(3) throws away. */
|
||||||
|
AKSL_CHECK_OK(aksl_rewind(fp));
|
||||||
|
AKSL_CHECK_OK(aksl_ftell(fp, &pos));
|
||||||
|
AKSL_CHECK(pos == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_seeko_and_tello(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
off_t pos = -1;
|
||||||
|
|
||||||
|
AKSL_CHECK(open_with_text("0123456789", path, sizeof(path), &fp) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_fseeko(fp, 6, SEEK_SET));
|
||||||
|
AKSL_CHECK_OK(aksl_ftello(fp, &pos));
|
||||||
|
AKSL_CHECK(pos == 6);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_getpos_and_setpos(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
fpos_t saved;
|
||||||
|
int c = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK(open_with_text("abcdef", path, sizeof(path), &fp) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_fseek(fp, 2, SEEK_SET));
|
||||||
|
AKSL_CHECK_OK(aksl_fgetpos(fp, &saved));
|
||||||
|
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
|
||||||
|
AKSL_CHECK(c == 'c');
|
||||||
|
AKSL_CHECK_OK(aksl_fsetpos(fp, &saved));
|
||||||
|
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
|
||||||
|
AKSL_CHECK(c == 'c');
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_positioning_rejects_null(void)
|
||||||
|
{
|
||||||
|
long pos = 0;
|
||||||
|
off_t opos = 0;
|
||||||
|
fpos_t fpos;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_fseek(NULL, 0, SEEK_SET), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_ftell(NULL, &pos), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_ftell(stdin, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_rewind(NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fseeko(NULL, 0, SEEK_SET), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_ftello(NULL, &opos), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_ftello(stdin, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fgetpos(NULL, &fpos), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fgetpos(stdin, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fsetpos(NULL, &fpos), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fsetpos(stdin, NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Seeking a pipe is ESPIPE, which is the failure rewind(3) would have hidden. */
|
||||||
|
static int test_seek_on_a_pipe_reports_espipe(void)
|
||||||
|
{
|
||||||
|
FILE *fp = popen("echo hello", "r");
|
||||||
|
long pos = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK(fp != NULL);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fseek(fp, 0, SEEK_SET), ESPIPE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_ftell(fp, &pos), ESPIPE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_rewind(fp), ESPIPE);
|
||||||
|
pclose(fp);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Character and line I/O */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* fgetc(3) returns EOF for both the end of the file and a read error. Split
|
||||||
|
* apart, a read loop needs no ferror/feof dance at the bottom of it.
|
||||||
|
*/
|
||||||
|
static int test_fgetc_separates_eof_from_error(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
int c = 0;
|
||||||
|
int seen = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK(open_with_text("ab", path, sizeof(path), &fp) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
|
||||||
|
AKSL_CHECK(c == 'a');
|
||||||
|
seen++;
|
||||||
|
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
|
||||||
|
AKSL_CHECK(c == 'b');
|
||||||
|
seen++;
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fgetc(fp, &c), AKERR_EOF, "end of stream");
|
||||||
|
AKSL_CHECK(seen == 2);
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
|
||||||
|
/* A stream with no read permission is the error half of the same sentinel. */
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
|
||||||
|
AKSL_CHECK_STATUS(aksl_fgetc(fp, &c), EBADF);
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_fgetc(NULL, &c), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fgetc(stdin, NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_fputc_and_ungetc(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
int c = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
|
||||||
|
AKSL_CHECK_OK(aksl_fputc('x', fp));
|
||||||
|
AKSL_CHECK_OK(aksl_fputc('y', fp));
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
|
||||||
|
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
|
||||||
|
AKSL_CHECK(c == 'x');
|
||||||
|
/* Put it back and read it again. */
|
||||||
|
AKSL_CHECK_OK(aksl_ungetc(c, fp));
|
||||||
|
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
|
||||||
|
AKSL_CHECK(c == 'x');
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_fputc('x', NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_ungetc('x', NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_fgets_and_fputs(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
char line[64];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
size_t len = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
|
||||||
|
AKSL_CHECK_OK(aksl_fputs("first\n", fp));
|
||||||
|
AKSL_CHECK_OK(aksl_fputs("second\n", fp));
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
|
||||||
|
AKSL_CHECK_OK(aksl_fgets(line, sizeof(line), fp, &len));
|
||||||
|
AKSL_CHECK(strcmp(line, "first\n") == 0);
|
||||||
|
AKSL_CHECK(len == 6);
|
||||||
|
AKSL_CHECK_OK(aksl_fgets(line, sizeof(line), fp, &len));
|
||||||
|
AKSL_CHECK(strcmp(line, "second\n") == 0);
|
||||||
|
/* The end of the input is AKERR_EOF, which is how the loop terminates. */
|
||||||
|
AKSL_CHECK_STATUS(aksl_fgets(line, sizeof(line), fp, &len), AKERR_EOF);
|
||||||
|
AKSL_CHECK(len == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A line longer than the buffer is a short read, not an error -- that is what
|
||||||
|
* fgets does and what a caller reading fixed chunks wants. The reported length
|
||||||
|
* is how the caller notices: a full buffer with no newline is exactly this case.
|
||||||
|
*/
|
||||||
|
static int test_fgets_reports_a_long_line_as_a_short_read(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
char line[4];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
size_t len = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK(open_with_text("abcdefgh\n", path, sizeof(path), &fp) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_fgets(line, sizeof(line), fp, &len));
|
||||||
|
AKSL_CHECK(len == 3);
|
||||||
|
AKSL_CHECK(strcmp(line, "abc") == 0);
|
||||||
|
AKSL_CHECK(line[len - 1] != '\n');
|
||||||
|
/* The rest of the line is still there. */
|
||||||
|
AKSL_CHECK_OK(aksl_fgets(line, sizeof(line), fp, &len));
|
||||||
|
AKSL_CHECK(strcmp(line, "def") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_fgets(NULL, 4, stdin, &len), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fgets(line, 4, NULL, &len), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fgets(line, 4, stdin, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fgets(line, 0, stdin, &len), AKERR_VALUE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* getline grows the buffer, so the caller starts with NULL/0 and frees once at
|
||||||
|
* the end -- not once per line. The reported length is what distinguishes an
|
||||||
|
* embedded NUL from the end of the line; strlen cannot.
|
||||||
|
*/
|
||||||
|
static int test_getline_grows_its_buffer(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
char *line = NULL;
|
||||||
|
size_t cap = 0;
|
||||||
|
size_t len = 0;
|
||||||
|
FILE *fp = NULL;
|
||||||
|
int lines = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK(open_with_text("short\na much longer line than the first one\n",
|
||||||
|
path, sizeof(path), &fp) == 0);
|
||||||
|
|
||||||
|
while ( 1 ) {
|
||||||
|
akerr_ErrorContext *raised = aksl_getline(&line, &cap, fp, &len);
|
||||||
|
int status = aksl_take(raised);
|
||||||
|
if ( status == AKERR_EOF ) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
AKSL_CHECK(status == 0);
|
||||||
|
AKSL_CHECK(len > 0);
|
||||||
|
AKSL_CHECK(line != NULL);
|
||||||
|
lines++;
|
||||||
|
}
|
||||||
|
AKSL_CHECK(lines == 2);
|
||||||
|
/* One buffer for the whole file, grown as needed. */
|
||||||
|
AKSL_CHECK(cap >= 42);
|
||||||
|
AKSL_CHECK_OK(aksl_freep((void **)&line));
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_getdelim_splits_on_any_byte(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
char *field = NULL;
|
||||||
|
size_t cap = 0;
|
||||||
|
size_t len = 0;
|
||||||
|
FILE *fp = NULL;
|
||||||
|
int fields = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK(open_with_text("a:b:c", path, sizeof(path), &fp) == 0);
|
||||||
|
|
||||||
|
while ( 1 ) {
|
||||||
|
akerr_ErrorContext *raised = aksl_getdelim(&field, &cap, ':', fp, &len);
|
||||||
|
int status = aksl_take(raised);
|
||||||
|
if ( status == AKERR_EOF ) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
AKSL_CHECK(status == 0);
|
||||||
|
fields++;
|
||||||
|
}
|
||||||
|
AKSL_CHECK(fields == 3);
|
||||||
|
AKSL_CHECK_OK(aksl_freep((void **)&field));
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_getline(NULL, &cap, stdin, &len), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_getline(&field, NULL, stdin, &len), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_getline(&field, &cap, NULL, &len), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_getline(&field, &cap, stdin, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_getdelim(NULL, &cap, ':', stdin, &len), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_getdelim(&field, &cap, ':', stdin, NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Stream state */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_stream_state(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
int flag = 99;
|
||||||
|
int fd = -1;
|
||||||
|
int c = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK(open_with_text("a", path, sizeof(path), &fp) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_feof(fp, &flag));
|
||||||
|
AKSL_CHECK(flag == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_ferror(fp, &flag));
|
||||||
|
AKSL_CHECK(flag == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_fileno(fp, &fd));
|
||||||
|
AKSL_CHECK(fd >= 0);
|
||||||
|
|
||||||
|
/* Read past the end to set the EOF indicator, then clear it. */
|
||||||
|
AKSL_CHECK_OK(aksl_fgetc(fp, &c));
|
||||||
|
AKSL_CHECK_STATUS(aksl_fgetc(fp, &c), AKERR_EOF);
|
||||||
|
AKSL_CHECK_OK(aksl_feof(fp, &flag));
|
||||||
|
AKSL_CHECK(flag != 0);
|
||||||
|
AKSL_CHECK_OK(aksl_clearerr(fp));
|
||||||
|
AKSL_CHECK_OK(aksl_feof(fp, &flag));
|
||||||
|
AKSL_CHECK(flag == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_feof(NULL, &flag), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_feof(stdin, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_ferror(NULL, &flag), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_ferror(stdin, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_clearerr(NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fileno(NULL, &fd), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fileno(stdin, NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Flushing, buffering and opening */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_fflush_and_setvbuf(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
char buffer[BUFSIZ];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
|
||||||
|
AKSL_CHECK_OK(aksl_setvbuf(fp, buffer, _IOFBF, sizeof(buffer)));
|
||||||
|
AKSL_CHECK_OK(aksl_fputs("buffered", fp));
|
||||||
|
AKSL_CHECK_OK(aksl_fflush(fp));
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
|
||||||
|
/* NULL means "every output stream" and is not an error, unlike everywhere else. */
|
||||||
|
AKSL_CHECK_OK(aksl_fflush(NULL));
|
||||||
|
AKSL_CHECK_STATUS(aksl_setvbuf(NULL, NULL, _IONBF, 0), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_tmpfile_fdopen_and_freopen(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
FILE *reopened = NULL;
|
||||||
|
int fd = -1;
|
||||||
|
size_t moved = 0;
|
||||||
|
|
||||||
|
/* tmpfile: no name, gone when it is closed. */
|
||||||
|
AKSL_CHECK_OK(aksl_tmpfile(&fp));
|
||||||
|
AKSL_CHECK(fp != NULL);
|
||||||
|
AKSL_CHECK_OK(aksl_fwrite("x", 1, 1, fp, &moved));
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
|
||||||
|
/* fdopen: wrap a descriptor this test owns. */
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
fd = open(path, O_RDONLY);
|
||||||
|
AKSL_CHECK(fd >= 0);
|
||||||
|
AKSL_CHECK_OK(aksl_fdopen(fd, "r", &fp));
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp)); /* closes fd too */
|
||||||
|
|
||||||
|
/* freopen: point an existing stream at a different file. */
|
||||||
|
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
|
||||||
|
AKSL_CHECK_OK(aksl_freopen(path, "r", fp, &reopened));
|
||||||
|
AKSL_CHECK(reopened != NULL);
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(reopened));
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_tmpfile(NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fdopen(-1, "r", &fp), AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fdopen(0, NULL, &fp), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fdopen(0, "r", NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_freopen("/tmp", "r", NULL, &fp), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_freopen("/tmp", NULL, stdin, &fp), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_freopen("/tmp", "r", stdin, NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Formatted input */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The reason the scanf wrappers take an expected count: scanf(3) returns how
|
||||||
|
* many conversions succeeded, and comparing that against the number written in
|
||||||
|
* the format string is a check every caller has to do by hand and eventually
|
||||||
|
* forgets. Forgetting leaves the unassigned arguments holding whatever they
|
||||||
|
* held before.
|
||||||
|
*/
|
||||||
|
static int test_sscanf_enforces_the_expected_count(void)
|
||||||
|
{
|
||||||
|
int a = 0;
|
||||||
|
int b = 0;
|
||||||
|
int assigned = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_sscanf("12 34", "%d %d", 2, &assigned, &a, &b));
|
||||||
|
AKSL_CHECK(assigned == 2);
|
||||||
|
AKSL_CHECK(a == 12 && b == 34);
|
||||||
|
|
||||||
|
/* One conversion short: an error rather than a stale second variable. */
|
||||||
|
a = 0;
|
||||||
|
b = 999;
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_sscanf("12 xy", "%d %d", 2, &assigned, &a, &b),
|
||||||
|
AKERR_VALUE, "1 of 2 conversions");
|
||||||
|
AKSL_CHECK(assigned == 1);
|
||||||
|
AKSL_CHECK(b == 999);
|
||||||
|
|
||||||
|
/* expected 0 opts out and hands the count back for the caller to judge. */
|
||||||
|
AKSL_CHECK_OK(aksl_sscanf("12 xy", "%d %d", 0, &assigned, &a, &b));
|
||||||
|
AKSL_CHECK(assigned == 1);
|
||||||
|
|
||||||
|
/* Nothing matched at all. */
|
||||||
|
AKSL_CHECK_STATUS(aksl_sscanf("", "%d", 1, &assigned, &a), AKERR_VALUE);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_sscanf(NULL, "%d", 1, &assigned, &a), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_sscanf("1", NULL, 1, &assigned), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_sscanf("1", "%d", 1, NULL, &a), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_fscanf_reads_from_a_stream(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
int a = 0;
|
||||||
|
int b = 0;
|
||||||
|
int assigned = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK(open_with_text("7 8\n", path, sizeof(path), &fp) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_fscanf(fp, "%d %d", 2, &assigned, &a, &b));
|
||||||
|
AKSL_CHECK(assigned == 2);
|
||||||
|
AKSL_CHECK(a == 7 && b == 8);
|
||||||
|
|
||||||
|
/* Nothing left: the end of the stream, not a conversion failure. */
|
||||||
|
AKSL_CHECK_STATUS(aksl_fscanf(fp, "%d", 1, &assigned, &a), AKERR_EOF);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_fscanf(NULL, "%d", 1, &assigned, &a), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fscanf(stdin, NULL, 1, &assigned), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_fscanf(stdin, "%d", 1, NULL, &a), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* aksl_scanf reads stdin, so stdin is pointed at a temp file for the duration of
|
||||||
|
* the call and restored through a dup of the original descriptor -- the same
|
||||||
|
* dance tests/test_format.c does for aksl_printf and stdout, and for the same
|
||||||
|
* reason: nothing between the freopen and the dup2 may return early.
|
||||||
|
*/
|
||||||
|
static int test_scanf_reads_stdin(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
akerr_ErrorContext *err = NULL;
|
||||||
|
int a = 0;
|
||||||
|
int b = 0;
|
||||||
|
int assigned = 0;
|
||||||
|
int saved = -1;
|
||||||
|
int restored = -1;
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
fp = fopen(path, "w");
|
||||||
|
AKSL_CHECK(fp != NULL);
|
||||||
|
AKSL_CHECK(fputs("11 22\n", fp) != EOF);
|
||||||
|
AKSL_CHECK(fclose(fp) == 0);
|
||||||
|
|
||||||
|
saved = dup(fileno(stdin));
|
||||||
|
AKSL_CHECK(saved >= 0);
|
||||||
|
if ( freopen(path, "r", stdin) == NULL ) {
|
||||||
|
close(saved);
|
||||||
|
AKSL_CHECK(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
err = aksl_scanf("%d %d", 2, &assigned, &a, &b);
|
||||||
|
|
||||||
|
restored = dup2(saved, fileno(stdin));
|
||||||
|
close(saved);
|
||||||
|
clearerr(stdin);
|
||||||
|
|
||||||
|
AKSL_CHECK(restored >= 0);
|
||||||
|
AKSL_CHECK(aksl_take(err) == 0);
|
||||||
|
AKSL_CHECK(assigned == 2);
|
||||||
|
AKSL_CHECK(a == 11 && b == 22);
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_scanf(NULL, 1, &assigned), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_scanf("%d", 1, NULL, &a), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The va_list form, used the way a consumer building its own wrapper would. */
|
||||||
|
static akerr_ErrorContext AKERR_NOIGNORE *consumer_scan(const char *fmt, int expected,
|
||||||
|
int *assigned, ...)
|
||||||
|
{
|
||||||
|
va_list args;
|
||||||
|
akerr_ErrorContext *raised = NULL;
|
||||||
|
|
||||||
|
va_start(args, assigned);
|
||||||
|
raised = aksl_vscanf(fmt, expected, assigned, args);
|
||||||
|
va_end(args);
|
||||||
|
return raised;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_vscanf_reads_stdin(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
FILE *fp = NULL;
|
||||||
|
akerr_ErrorContext *err = NULL;
|
||||||
|
int a = 0;
|
||||||
|
int assigned = 0;
|
||||||
|
int saved = -1;
|
||||||
|
int restored = -1;
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
fp = fopen(path, "w");
|
||||||
|
AKSL_CHECK(fp != NULL);
|
||||||
|
AKSL_CHECK(fputs("33\n", fp) != EOF);
|
||||||
|
AKSL_CHECK(fclose(fp) == 0);
|
||||||
|
|
||||||
|
saved = dup(fileno(stdin));
|
||||||
|
AKSL_CHECK(saved >= 0);
|
||||||
|
if ( freopen(path, "r", stdin) == NULL ) {
|
||||||
|
close(saved);
|
||||||
|
AKSL_CHECK(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
err = consumer_scan("%d", 1, &assigned, &a);
|
||||||
|
|
||||||
|
restored = dup2(saved, fileno(stdin));
|
||||||
|
close(saved);
|
||||||
|
clearerr(stdin);
|
||||||
|
|
||||||
|
AKSL_CHECK(restored >= 0);
|
||||||
|
AKSL_CHECK(aksl_take(err) == 0);
|
||||||
|
AKSL_CHECK(assigned == 1);
|
||||||
|
AKSL_CHECK(a == 33);
|
||||||
|
AKSL_CHECK(unlink(path) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Files */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_remove_and_rename(void)
|
||||||
|
{
|
||||||
|
char path[AKSL_TMP_MAX];
|
||||||
|
char moved[AKSL_TMP_MAX + 8];
|
||||||
|
|
||||||
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
||||||
|
AKSL_CHECK((size_t)snprintf(moved, sizeof(moved), "%s.moved", path) < sizeof(moved));
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_rename(path, moved));
|
||||||
|
AKSL_CHECK(access(path, F_OK) != 0);
|
||||||
|
AKSL_CHECK(access(moved, F_OK) == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_remove(moved));
|
||||||
|
AKSL_CHECK(access(moved, F_OK) != 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_remove("/nonexistent/aksl/file"),
|
||||||
|
ENOENT, "/nonexistent/aksl/file");
|
||||||
|
AKSL_CHECK_STATUS(aksl_remove(NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_rename(NULL, "b"), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_rename("a", NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* mkstemp and mkdtemp rewrite the template in place, so a template that is not
|
||||||
|
* a writable buffer of the right shape is a crash waiting to happen. The shape
|
||||||
|
* is checked here rather than left to the kernel.
|
||||||
|
*/
|
||||||
|
static int test_mkstemp_and_mkdtemp(void)
|
||||||
|
{
|
||||||
|
char file_template[] = "/tmp/aksl_streamio_XXXXXX";
|
||||||
|
char dir_template[] = "/tmp/aksl_streamio_dir_XXXXXX";
|
||||||
|
char bad[] = "/tmp/aksl_no_placeholder";
|
||||||
|
int fd = -1;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_mkstemp(file_template, &fd));
|
||||||
|
AKSL_CHECK(fd >= 0);
|
||||||
|
AKSL_CHECK(strcmp(file_template, "/tmp/aksl_streamio_XXXXXX") != 0);
|
||||||
|
AKSL_CHECK(close(fd) == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_remove(file_template));
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_mkdtemp(dir_template));
|
||||||
|
AKSL_CHECK(access(dir_template, F_OK) == 0);
|
||||||
|
AKSL_CHECK(rmdir(dir_template) == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_mkstemp(bad, &fd), AKERR_VALUE, "six literal X");
|
||||||
|
AKSL_CHECK(fd == -1);
|
||||||
|
AKSL_CHECK_STATUS(aksl_mkdtemp(bad), AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_mkstemp(NULL, &fd), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_mkstemp(file_template, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_mkdtemp(NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
akerr_init();
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_seek_and_tell);
|
||||||
|
AKSL_RUN(failures, test_seeko_and_tello);
|
||||||
|
AKSL_RUN(failures, test_getpos_and_setpos);
|
||||||
|
AKSL_RUN(failures, test_positioning_rejects_null);
|
||||||
|
AKSL_RUN(failures, test_seek_on_a_pipe_reports_espipe);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_fgetc_separates_eof_from_error);
|
||||||
|
AKSL_RUN(failures, test_fputc_and_ungetc);
|
||||||
|
AKSL_RUN(failures, test_fgets_and_fputs);
|
||||||
|
AKSL_RUN(failures, test_fgets_reports_a_long_line_as_a_short_read);
|
||||||
|
AKSL_RUN(failures, test_getline_grows_its_buffer);
|
||||||
|
AKSL_RUN(failures, test_getdelim_splits_on_any_byte);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_stream_state);
|
||||||
|
AKSL_RUN(failures, test_fflush_and_setvbuf);
|
||||||
|
AKSL_RUN(failures, test_tmpfile_fdopen_and_freopen);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_sscanf_enforces_the_expected_count);
|
||||||
|
AKSL_RUN(failures, test_fscanf_reads_from_a_stream);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_scanf_reads_stdin);
|
||||||
|
AKSL_RUN(failures, test_vscanf_reads_stdin);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_remove_and_rename);
|
||||||
|
AKSL_RUN(failures, test_mkstemp_and_mkdtemp);
|
||||||
|
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
148
tests/test_strhash.c
Normal file
148
tests/test_strhash.c
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* Most vectors here are 7-bit ASCII, where signed and unsigned char agree and
|
||||||
|
* the test therefore says nothing either way about byte signedness. The high-bit
|
||||||
|
* vector is the one that pins TODO.md 2.2.6 down.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO.md 2.2.6, pinned. The cursor is an unsigned char * now, so bytes at or
|
||||||
|
* above 0x80 contribute their unsigned value. Iterating a plain char * on x86 or
|
||||||
|
* ARM Linux made them negative, giving 5859874 here instead of 5868578 -- a hash
|
||||||
|
* that disagreed with canonical djb2 and, worse, disagreed with itself across
|
||||||
|
* platforms depending on whether char happened to be signed.
|
||||||
|
*
|
||||||
|
* Benign for the 7-bit identifiers akbasic hashes; quietly wrong for the first
|
||||||
|
* caller to key a table on a filename or a UTF-8 string literal.
|
||||||
|
*/
|
||||||
|
static int test_high_bit_bytes_are_unsigned(void)
|
||||||
|
{
|
||||||
|
char buf[2] = { (char)0xff, (char)0xfe };
|
||||||
|
uint32_t h = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strhash_djb2(buf, sizeof(buf), &h));
|
||||||
|
AKSL_CHECK(h == 5868578u);
|
||||||
|
/* The sign-extended answer, spelled out so a regression names itself. */
|
||||||
|
AKSL_CHECK(h != 5859874u);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The NUL-terminated convenience form (TODO.md 3.6) agrees with the length one. */
|
||||||
|
static int test_str_form_matches_the_length_form(void)
|
||||||
|
{
|
||||||
|
const char *s = "libakstdlib";
|
||||||
|
uint32_t from_str = 0;
|
||||||
|
uint32_t from_len = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strhash_djb2_str(s, &from_str));
|
||||||
|
AKSL_CHECK_OK(aksl_strhash_djb2(s, strlen(s), &from_len));
|
||||||
|
AKSL_CHECK(from_str == from_len);
|
||||||
|
AKSL_CHECK(from_str == 884285482u);
|
||||||
|
|
||||||
|
/* The empty string is the seed, not an error. */
|
||||||
|
AKSL_CHECK_OK(aksl_strhash_djb2_str("", &from_str));
|
||||||
|
AKSL_CHECK(from_str == 5381);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strhash_djb2_str(NULL, &from_str), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strhash_djb2_str("x", NULL), AKERR_NULLPOINTER);
|
||||||
|
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_high_bit_bytes_are_unsigned);
|
||||||
|
AKSL_RUN(failures, test_str_form_matches_the_length_form);
|
||||||
|
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);
|
||||||
|
}
|
||||||
448
tests/test_string.c
Normal file
448
tests/test_string.c
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
/*
|
||||||
|
* String wrappers -- src/string.c, TODO.md section 3.1.
|
||||||
|
*
|
||||||
|
* The two contracts worth testing hardest are the ones that differ from libc:
|
||||||
|
* every copying function takes the destination size and treats truncation as an
|
||||||
|
* error that writes nothing, and every searching function treats "not found" as
|
||||||
|
* a successful answer of NULL.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Length */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_strlen_and_strnlen(void)
|
||||||
|
{
|
||||||
|
size_t n = 99;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strlen("hello", &n));
|
||||||
|
AKSL_CHECK(n == 5);
|
||||||
|
AKSL_CHECK_OK(aksl_strlen("", &n));
|
||||||
|
AKSL_CHECK(n == 0);
|
||||||
|
|
||||||
|
/* strnlen stops at maxlen whether or not it found a terminator. */
|
||||||
|
AKSL_CHECK_OK(aksl_strnlen("hello", 3, &n));
|
||||||
|
AKSL_CHECK(n == 3);
|
||||||
|
AKSL_CHECK_OK(aksl_strnlen("hello", 99, &n));
|
||||||
|
AKSL_CHECK(n == 5);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strlen(NULL, &n), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strlen("x", NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strnlen(NULL, 1, &n), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strnlen("x", 1, NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Copying */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_strcpy_copies_within_the_buffer(void)
|
||||||
|
{
|
||||||
|
char buf[8];
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strcpy(buf, sizeof(buf), "abc"));
|
||||||
|
AKSL_CHECK(strcmp(buf, "abc") == 0);
|
||||||
|
|
||||||
|
/* Exactly filling the buffer, terminator included, is not truncation. */
|
||||||
|
AKSL_CHECK_OK(aksl_strcpy(buf, sizeof(buf), "1234567"));
|
||||||
|
AKSL_CHECK(strcmp(buf, "1234567") == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The whole point of taking dstsize. akbasic writes this check by hand at ten
|
||||||
|
* sites: a length test, then strncpy, then an explicit NUL.
|
||||||
|
*/
|
||||||
|
static int test_strcpy_truncation_is_an_error_and_writes_nothing(void)
|
||||||
|
{
|
||||||
|
char buf[8] = "previous";
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strcpy(buf, sizeof(buf), "12345678"),
|
||||||
|
AKERR_OUTOFBOUNDS, "destination holds");
|
||||||
|
/*
|
||||||
|
* Empty, not a truncated prefix. A caller who ignores the status gets
|
||||||
|
* nothing, which is far easier to notice than a plausible-looking "1234567".
|
||||||
|
*/
|
||||||
|
AKSL_CHECK(buf[0] == '\0');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* strncpy(3) leaves the destination unterminated when the source is at least n
|
||||||
|
* bytes, and NUL-pads the whole remainder when it is shorter. This does neither.
|
||||||
|
*/
|
||||||
|
static int test_strncpy_always_terminates_and_never_pads(void)
|
||||||
|
{
|
||||||
|
char buf[16];
|
||||||
|
|
||||||
|
memset(buf, 'Z', sizeof(buf));
|
||||||
|
AKSL_CHECK_OK(aksl_strncpy(buf, sizeof(buf), "abcdef", 3));
|
||||||
|
AKSL_CHECK(strcmp(buf, "abc") == 0);
|
||||||
|
/* Not padded: everything past the terminator is untouched. */
|
||||||
|
AKSL_CHECK(buf[4] == 'Z');
|
||||||
|
|
||||||
|
/* n larger than the source just copies the source. */
|
||||||
|
AKSL_CHECK_OK(aksl_strncpy(buf, sizeof(buf), "abc", 99));
|
||||||
|
AKSL_CHECK(strcmp(buf, "abc") == 0);
|
||||||
|
|
||||||
|
/* n bytes that do not fit the destination is still an error. */
|
||||||
|
AKSL_CHECK_STATUS(aksl_strncpy(buf, 4, "abcdef", 6), AKERR_OUTOFBOUNDS);
|
||||||
|
AKSL_CHECK(buf[0] == '\0');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_strcat_appends_within_the_buffer(void)
|
||||||
|
{
|
||||||
|
char buf[16] = "ab";
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strcat(buf, sizeof(buf), "cd"));
|
||||||
|
AKSL_CHECK(strcmp(buf, "abcd") == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_strcat(buf, sizeof(buf), ""));
|
||||||
|
AKSL_CHECK(strcmp(buf, "abcd") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strcat(buf, sizeof(buf), "0123456789ab"),
|
||||||
|
AKERR_OUTOFBOUNDS, "in use plus");
|
||||||
|
/* The existing contents survive a refused append. */
|
||||||
|
AKSL_CHECK(strcmp(buf, "abcd") == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* An unterminated destination is refused rather than walked off the end of. */
|
||||||
|
static int test_strcat_refuses_an_unterminated_destination(void)
|
||||||
|
{
|
||||||
|
char buf[4];
|
||||||
|
|
||||||
|
memset(buf, 'x', sizeof(buf));
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strcat(buf, sizeof(buf), "y"),
|
||||||
|
AKERR_VALUE, "not terminated");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_strncat_appends_at_most_n(void)
|
||||||
|
{
|
||||||
|
char buf[16] = "ab";
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strncat(buf, sizeof(buf), "cdef", 2));
|
||||||
|
AKSL_CHECK(strcmp(buf, "abcd") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strncat(buf, 6, "xyz", 3), AKERR_OUTOFBOUNDS);
|
||||||
|
AKSL_CHECK(strcmp(buf, "abcd") == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_copying_rejects_null_and_zero_size(void)
|
||||||
|
{
|
||||||
|
char buf[8] = "";
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcpy(NULL, 8, "x"), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcpy(buf, sizeof(buf), NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcpy(buf, 0, "x"), AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strncpy(NULL, 8, "x", 1), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strncpy(buf, sizeof(buf), NULL, 1), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strncpy(buf, 0, "x", 1), AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcat(NULL, 8, "x"), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcat(buf, sizeof(buf), NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcat(buf, 0, "x"), AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strncat(NULL, 8, "x", 1), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strncat(buf, sizeof(buf), NULL, 1), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strncat(buf, 0, "x", 1), AKERR_VALUE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Duplication */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_strdup_and_strndup(void)
|
||||||
|
{
|
||||||
|
char *copy = NULL;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strdup("duplicate me", ©));
|
||||||
|
AKSL_CHECK(copy != NULL);
|
||||||
|
AKSL_CHECK(strcmp(copy, "duplicate me") == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_freep((void **)©));
|
||||||
|
AKSL_CHECK(copy == NULL);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strndup("duplicate me", 9, ©));
|
||||||
|
AKSL_CHECK(strcmp(copy, "duplicate") == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_freep((void **)©));
|
||||||
|
|
||||||
|
/* n past the end of the string just copies the string. */
|
||||||
|
AKSL_CHECK_OK(aksl_strndup("ab", 99, ©));
|
||||||
|
AKSL_CHECK(strcmp(copy, "ab") == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_freep((void **)©));
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strdup(NULL, ©), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strdup("x", NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strndup(NULL, 1, ©), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strndup("x", 1, NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Comparison */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_comparisons(void)
|
||||||
|
{
|
||||||
|
int r = 99;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strcmp("abc", "abc", &r));
|
||||||
|
AKSL_CHECK(r == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_strcmp("abc", "abd", &r));
|
||||||
|
AKSL_CHECK(r < 0);
|
||||||
|
AKSL_CHECK_OK(aksl_strcmp("abd", "abc", &r));
|
||||||
|
AKSL_CHECK(r > 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strncmp("abcXX", "abcYY", 3, &r));
|
||||||
|
AKSL_CHECK(r == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_strncmp("abcXX", "abcYY", 4, &r));
|
||||||
|
AKSL_CHECK(r != 0);
|
||||||
|
|
||||||
|
/* The three case-folding sites akbasic writes out by hand. */
|
||||||
|
AKSL_CHECK_OK(aksl_strcasecmp("PRINT", "print", &r));
|
||||||
|
AKSL_CHECK(r == 0);
|
||||||
|
AKSL_CHECK_OK(aksl_strcasecmp("PRINT", "input", &r));
|
||||||
|
AKSL_CHECK(r != 0);
|
||||||
|
AKSL_CHECK_OK(aksl_strncasecmp("PRINTXX", "printYY", 5, &r));
|
||||||
|
AKSL_CHECK(r == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strcoll("abc", "abc", &r));
|
||||||
|
AKSL_CHECK(r == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcmp(NULL, "b", &r), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcmp("a", NULL, &r), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcmp("a", "b", NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strncmp(NULL, "b", 1, &r), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strncmp("a", NULL, 1, &r), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strncmp("a", "b", 1, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcasecmp(NULL, "b", &r), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcasecmp("a", NULL, &r), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcasecmp("a", "b", NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strncasecmp(NULL, "b", 1, &r), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strncasecmp("a", NULL, 1, &r), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strncasecmp("a", "b", 1, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcoll(NULL, "b", &r), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcoll("a", NULL, &r), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcoll("a", "b", NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Searching */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Every one of these treats absence as an answer. A library that raised on it
|
||||||
|
* would have every caller catching a non-error, and burning a pool slot to do
|
||||||
|
* so.
|
||||||
|
*/
|
||||||
|
static int test_searching_reports_absence_as_success(void)
|
||||||
|
{
|
||||||
|
const char *s = "the quick brown fox";
|
||||||
|
char *at = (char *)0x1;
|
||||||
|
size_t n = 99;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strchr(s, 'q', &at));
|
||||||
|
AKSL_CHECK(at == s + 4);
|
||||||
|
AKSL_CHECK_OK(aksl_strchr(s, 'Z', &at));
|
||||||
|
AKSL_CHECK(at == NULL);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strrchr(s, 'o', &at));
|
||||||
|
AKSL_CHECK(at == s + 17);
|
||||||
|
AKSL_CHECK_OK(aksl_strrchr(s, 'Z', &at));
|
||||||
|
AKSL_CHECK(at == NULL);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strstr(s, "brown", &at));
|
||||||
|
AKSL_CHECK(at == s + 10);
|
||||||
|
AKSL_CHECK_OK(aksl_strstr(s, "purple", &at));
|
||||||
|
AKSL_CHECK(at == NULL);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strpbrk(s, "xq", &at));
|
||||||
|
AKSL_CHECK(at == s + 4);
|
||||||
|
AKSL_CHECK_OK(aksl_strpbrk(s, "ZY", &at));
|
||||||
|
AKSL_CHECK(at == NULL);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strspn("aaabbb", "a", &n));
|
||||||
|
AKSL_CHECK(n == 3);
|
||||||
|
AKSL_CHECK_OK(aksl_strcspn("aaabbb", "b", &n));
|
||||||
|
AKSL_CHECK(n == 3);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The open-coded case-insensitive search, including its edge cases. */
|
||||||
|
static int test_strcasestr(void)
|
||||||
|
{
|
||||||
|
const char *s = "The Quick Brown Fox";
|
||||||
|
char *at = (char *)0x1;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strcasestr(s, "quick", &at));
|
||||||
|
AKSL_CHECK(at == s + 4);
|
||||||
|
AKSL_CHECK_OK(aksl_strcasestr(s, "QUICK", &at));
|
||||||
|
AKSL_CHECK(at == s + 4);
|
||||||
|
AKSL_CHECK_OK(aksl_strcasestr(s, "purple", &at));
|
||||||
|
AKSL_CHECK(at == NULL);
|
||||||
|
|
||||||
|
/* An empty needle matches at the start, as strstr(3) has it. */
|
||||||
|
AKSL_CHECK_OK(aksl_strcasestr(s, "", &at));
|
||||||
|
AKSL_CHECK(at == s);
|
||||||
|
|
||||||
|
/* A needle longer than the haystack cannot match, and must not read past. */
|
||||||
|
AKSL_CHECK_OK(aksl_strcasestr("ab", "abcdef", &at));
|
||||||
|
AKSL_CHECK(at == NULL);
|
||||||
|
|
||||||
|
/* A match right at the end. */
|
||||||
|
AKSL_CHECK_OK(aksl_strcasestr(s, "fox", &at));
|
||||||
|
AKSL_CHECK(at == s + 16);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_searching_rejects_null_arguments(void)
|
||||||
|
{
|
||||||
|
char *at = NULL;
|
||||||
|
size_t n = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strchr(NULL, 'a', &at), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strchr("a", 'a', NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strrchr(NULL, 'a', &at), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strrchr("a", 'a', NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strstr(NULL, "a", &at), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strstr("a", NULL, &at), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strstr("a", "a", NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcasestr(NULL, "a", &at), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcasestr("a", NULL, &at), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcasestr("a", "a", NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strpbrk(NULL, "a", &at), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strpbrk("a", NULL, &at), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strpbrk("a", "a", NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strspn(NULL, "a", &n), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strspn("a", NULL, &n), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strspn("a", "a", NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcspn(NULL, "a", &n), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcspn("a", NULL, &n), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strcspn("a", "a", NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Tokenising */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_strtok_r_walks_the_tokens(void)
|
||||||
|
{
|
||||||
|
char input[] = "one,two,,three";
|
||||||
|
char *save = NULL;
|
||||||
|
char *tok = NULL;
|
||||||
|
int seen = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strtok_r(input, ",", &save, &tok));
|
||||||
|
AKSL_CHECK(tok != NULL && strcmp(tok, "one") == 0);
|
||||||
|
seen++;
|
||||||
|
while ( 1 ) {
|
||||||
|
AKSL_CHECK_OK(aksl_strtok_r(NULL, ",", &save, &tok));
|
||||||
|
if ( tok == NULL ) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
seen++;
|
||||||
|
}
|
||||||
|
/* strtok_r collapses the empty field between the two commas. */
|
||||||
|
AKSL_CHECK(seen == 3);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtok_r(NULL, NULL, &save, &tok), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtok_r(NULL, ",", NULL, &tok), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtok_r(NULL, ",", &save, NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* strsep keeps the empty fields, which is why it exists alongside strtok_r. */
|
||||||
|
static int test_strsep_keeps_empty_fields(void)
|
||||||
|
{
|
||||||
|
char input[] = "one,two,,three";
|
||||||
|
char *cursor = input;
|
||||||
|
char *tok = NULL;
|
||||||
|
int seen = 0;
|
||||||
|
|
||||||
|
while ( cursor != NULL ) {
|
||||||
|
AKSL_CHECK_OK(aksl_strsep(&cursor, ",", &tok));
|
||||||
|
if ( tok == NULL ) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
seen++;
|
||||||
|
}
|
||||||
|
AKSL_CHECK(seen == 4);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strsep(NULL, ",", &tok), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strsep(&cursor, NULL, &tok), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strsep(&cursor, ",", NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Status messages */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* aksl_strerror knows this library's own statuses as well as errno values,
|
||||||
|
* which is the reason it is not a strerror_r wrapper: strerror_r could never
|
||||||
|
* name AKERR_NULLPOINTER.
|
||||||
|
*/
|
||||||
|
static int test_strerror_names_both_kinds_of_status(void)
|
||||||
|
{
|
||||||
|
char buf[128];
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strerror(ENOENT, buf, sizeof(buf)));
|
||||||
|
AKSL_CHECK(buf[0] != '\0');
|
||||||
|
AKSL_CHECK(strcmp(buf, "Unknown status 2") != 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strerror(AKERR_NULLPOINTER, buf, sizeof(buf)));
|
||||||
|
AKSL_CHECK(buf[0] != '\0');
|
||||||
|
|
||||||
|
/* A status nothing has a name for is rendered as its number. */
|
||||||
|
AKSL_CHECK_OK(aksl_strerror(-98765, buf, sizeof(buf)));
|
||||||
|
AKSL_CHECK(strcmp(buf, "Unknown status -98765") == 0);
|
||||||
|
|
||||||
|
/* Too small a buffer is an error, and leaves nothing rather than a prefix. */
|
||||||
|
AKSL_CHECK_STATUS(aksl_strerror(ENOENT, buf, 2), AKERR_OUTOFBOUNDS);
|
||||||
|
AKSL_CHECK(buf[0] == '\0');
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strerror(ENOENT, NULL, 16), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strerror(ENOENT, buf, 0), AKERR_VALUE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
akerr_init();
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_strlen_and_strnlen);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_strcpy_copies_within_the_buffer);
|
||||||
|
AKSL_RUN(failures, test_strcpy_truncation_is_an_error_and_writes_nothing);
|
||||||
|
AKSL_RUN(failures, test_strncpy_always_terminates_and_never_pads);
|
||||||
|
AKSL_RUN(failures, test_strcat_appends_within_the_buffer);
|
||||||
|
AKSL_RUN(failures, test_strcat_refuses_an_unterminated_destination);
|
||||||
|
AKSL_RUN(failures, test_strncat_appends_at_most_n);
|
||||||
|
AKSL_RUN(failures, test_copying_rejects_null_and_zero_size);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_strdup_and_strndup);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_comparisons);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_searching_reports_absence_as_success);
|
||||||
|
AKSL_RUN(failures, test_strcasestr);
|
||||||
|
AKSL_RUN(failures, test_searching_rejects_null_arguments);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_strtok_r_walks_the_tokens);
|
||||||
|
AKSL_RUN(failures, test_strsep_keeps_empty_fields);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_strerror_names_both_kinds_of_status);
|
||||||
|
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
340
tests/test_strto.c
Normal file
340
tests/test_strto.c
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
/*
|
||||||
|
* The strto* family -- TODO.md section 3.1.
|
||||||
|
*
|
||||||
|
* These are the real implementation behind the ato* wrappers and the thing
|
||||||
|
* akbasic had to hand-write for itself (its src/convert.c, ~60 lines) because
|
||||||
|
* the library would not do it. What they add over the ato* forms is a base and
|
||||||
|
* an endptr: pass a non-NULL endptr to parse a number off the front of a longer
|
||||||
|
* string and find out where it stopped, and the trailing-junk check goes away
|
||||||
|
* because the caller has taken responsibility for what follows.
|
||||||
|
*
|
||||||
|
* tests/test_convert.c covers what the two families share; this file is the part
|
||||||
|
* that is specific to having a base and an endptr.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <limits.h>
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Base handling */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_base_is_honoured(void)
|
||||||
|
{
|
||||||
|
long out = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strtol("ff", NULL, 16, &out));
|
||||||
|
AKSL_CHECK(out == 255);
|
||||||
|
AKSL_CHECK_OK(aksl_strtol("1010", NULL, 2, &out));
|
||||||
|
AKSL_CHECK(out == 10);
|
||||||
|
AKSL_CHECK_OK(aksl_strtol("777", NULL, 8, &out));
|
||||||
|
AKSL_CHECK(out == 511);
|
||||||
|
AKSL_CHECK_OK(aksl_strtol("z", NULL, 36, &out));
|
||||||
|
AKSL_CHECK(out == 35);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base 0 auto-detects the 0x and 0 prefixes, which is the counterpart to
|
||||||
|
* tests/test_convert.c's test that aksl_atoi refuses "0x10". */
|
||||||
|
static int test_base_zero_detects_the_prefix(void)
|
||||||
|
{
|
||||||
|
long out = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strtol("0x10", NULL, 0, &out));
|
||||||
|
AKSL_CHECK(out == 16);
|
||||||
|
AKSL_CHECK_OK(aksl_strtol("010", NULL, 0, &out));
|
||||||
|
AKSL_CHECK(out == 8);
|
||||||
|
AKSL_CHECK_OK(aksl_strtol("10", NULL, 0, &out));
|
||||||
|
AKSL_CHECK(out == 10);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A digit that does not belong to the base is where the number ends. */
|
||||||
|
static int test_digits_outside_the_base_end_the_number(void)
|
||||||
|
{
|
||||||
|
long out = 0;
|
||||||
|
char *end = NULL;
|
||||||
|
|
||||||
|
/* "9" is not an octal digit, so with a NULL endptr it is trailing junk. */
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtol("129", NULL, 8, &out), AKERR_VALUE);
|
||||||
|
/* With an endptr the caller gets the partial parse and the stopping point. */
|
||||||
|
AKSL_CHECK_OK(aksl_strtol("129", &end, 8, &out));
|
||||||
|
AKSL_CHECK(out == 10); /* 012 octal */
|
||||||
|
AKSL_CHECK(end != NULL && *end == '9');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* An invalid base is EINVAL from strtol(3) rather than a silent answer. */
|
||||||
|
static int test_invalid_base_is_rejected(void)
|
||||||
|
{
|
||||||
|
long out = 99;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtol("10", NULL, 37, &out), EINVAL);
|
||||||
|
AKSL_CHECK(out == 0);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtol("10", NULL, 1, &out), EINVAL);
|
||||||
|
AKSL_CHECK(out == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* endptr */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* With an endptr, trailing text is the caller's business and not an error. This
|
||||||
|
* is the shape a tokenizer wants: parse a number, carry on from where it ended.
|
||||||
|
*/
|
||||||
|
static int test_endptr_reports_where_parsing_stopped(void)
|
||||||
|
{
|
||||||
|
const char *input = "42 rest of the line";
|
||||||
|
char *end = NULL;
|
||||||
|
long out = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strtol(input, &end, 10, &out));
|
||||||
|
AKSL_CHECK(out == 42);
|
||||||
|
AKSL_CHECK(end == input + 2);
|
||||||
|
AKSL_CHECK(strcmp(end, " rest of the line") == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Consuming nothing is still an error, endptr or not: there was no number. */
|
||||||
|
static int test_no_digits_is_an_error_even_with_an_endptr(void)
|
||||||
|
{
|
||||||
|
char *end = (char *)0x1;
|
||||||
|
long out = 99;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strtol("nope", &end, 10, &out),
|
||||||
|
AKERR_VALUE, "no digits");
|
||||||
|
AKSL_CHECK(out == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Every form writes through endptr, not just the two the other tests happen to
|
||||||
|
* use. Each type has its own function body, so "strtol handles endptr" says
|
||||||
|
* nothing whatsoever about strtoull.
|
||||||
|
*/
|
||||||
|
static int test_every_form_writes_the_endptr(void)
|
||||||
|
{
|
||||||
|
const char *input = "42rest";
|
||||||
|
char *end = NULL;
|
||||||
|
long lout = 0;
|
||||||
|
long long llout = 0;
|
||||||
|
unsigned long ulout = 0;
|
||||||
|
unsigned long long ullout = 0;
|
||||||
|
double dout = 0.0;
|
||||||
|
float fout = 0.0f;
|
||||||
|
long double ldout = 0.0L;
|
||||||
|
|
||||||
|
end = NULL;
|
||||||
|
AKSL_CHECK_OK(aksl_strtol(input, &end, 10, &lout));
|
||||||
|
AKSL_CHECK(lout == 42 && strcmp(end, "rest") == 0);
|
||||||
|
|
||||||
|
end = NULL;
|
||||||
|
AKSL_CHECK_OK(aksl_strtoll(input, &end, 10, &llout));
|
||||||
|
AKSL_CHECK(llout == 42 && strcmp(end, "rest") == 0);
|
||||||
|
|
||||||
|
end = NULL;
|
||||||
|
AKSL_CHECK_OK(aksl_strtoul(input, &end, 10, &ulout));
|
||||||
|
AKSL_CHECK(ulout == 42 && strcmp(end, "rest") == 0);
|
||||||
|
|
||||||
|
end = NULL;
|
||||||
|
AKSL_CHECK_OK(aksl_strtoull(input, &end, 10, &ullout));
|
||||||
|
AKSL_CHECK(ullout == 42 && strcmp(end, "rest") == 0);
|
||||||
|
|
||||||
|
end = NULL;
|
||||||
|
AKSL_CHECK_OK(aksl_strtod(input, &end, &dout));
|
||||||
|
AKSL_CHECK(dout == 42.0 && strcmp(end, "rest") == 0);
|
||||||
|
|
||||||
|
end = NULL;
|
||||||
|
AKSL_CHECK_OK(aksl_strtof(input, &end, &fout));
|
||||||
|
AKSL_CHECK(fout == 42.0f && strcmp(end, "rest") == 0);
|
||||||
|
|
||||||
|
end = NULL;
|
||||||
|
AKSL_CHECK_OK(aksl_strtold(input, &end, &ldout));
|
||||||
|
AKSL_CHECK(ldout == 42.0L && strcmp(end, "rest") == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Successive calls chained through endptr walk a whole list of numbers. */
|
||||||
|
static int test_endptr_chains_across_a_list(void)
|
||||||
|
{
|
||||||
|
const char *input = "1,2,3";
|
||||||
|
char *cursor = (char *)input;
|
||||||
|
long out = 0;
|
||||||
|
long total = 0;
|
||||||
|
int seen = 0;
|
||||||
|
|
||||||
|
while ( *cursor != '\0' ) {
|
||||||
|
AKSL_CHECK_OK(aksl_strtol(cursor, &cursor, 10, &out));
|
||||||
|
total += out;
|
||||||
|
seen += 1;
|
||||||
|
if ( *cursor == ',' ) {
|
||||||
|
cursor++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AKSL_CHECK(seen == 3);
|
||||||
|
AKSL_CHECK(total == 6);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Ranges and signs */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_range_errors_per_type(void)
|
||||||
|
{
|
||||||
|
long lout = 99;
|
||||||
|
long long llout = 99;
|
||||||
|
unsigned long ulout = 99;
|
||||||
|
unsigned long long ullout = 99;
|
||||||
|
char buf[64];
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "%lld9", (long long)LLONG_MAX);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtol(buf, NULL, 10, &lout), ERANGE);
|
||||||
|
AKSL_CHECK(lout == 0);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtoll(buf, NULL, 10, &llout), ERANGE);
|
||||||
|
AKSL_CHECK(llout == 0);
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "%llu9", (unsigned long long)ULLONG_MAX);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtoul(buf, NULL, 10, &ulout), ERANGE);
|
||||||
|
AKSL_CHECK(ulout == 0);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtoull(buf, NULL, 10, &ullout), ERANGE);
|
||||||
|
AKSL_CHECK(ullout == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* strtoul(3) and strtoull(3) accept a leading '-' and hand back the negation
|
||||||
|
* wrapped into the unsigned range, so "-1" parses as ULONG_MAX and reports
|
||||||
|
* nothing at all. That is exactly the silent failure this library exists to
|
||||||
|
* surface, so the sign is refused before the call.
|
||||||
|
*/
|
||||||
|
static int test_unsigned_forms_refuse_a_negative(void)
|
||||||
|
{
|
||||||
|
unsigned long ulout = 99;
|
||||||
|
unsigned long long ullout = 99;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strtoul("-1", NULL, 10, &ulout),
|
||||||
|
AKERR_VALUE, "negative");
|
||||||
|
AKSL_CHECK(ulout == 0);
|
||||||
|
/* Including behind leading whitespace, which strtoul skips. */
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtoul(" -1", NULL, 10, &ulout), AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtoull("-1", NULL, 10, &ullout), AKERR_VALUE);
|
||||||
|
AKSL_CHECK(ullout == 0);
|
||||||
|
|
||||||
|
/* A leading '+' is fine: it is a sign, not a negation. */
|
||||||
|
AKSL_CHECK_OK(aksl_strtoul("+7", NULL, 10, &ulout));
|
||||||
|
AKSL_CHECK(ulout == 7);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_unsigned_boundaries_round_trip(void)
|
||||||
|
{
|
||||||
|
unsigned long ulout = 0;
|
||||||
|
unsigned long long ullout = 0;
|
||||||
|
char buf[64];
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "%lu", ULONG_MAX);
|
||||||
|
AKSL_CHECK_OK(aksl_strtoul(buf, NULL, 10, &ulout));
|
||||||
|
AKSL_CHECK(ulout == ULONG_MAX);
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "%llu", ULLONG_MAX);
|
||||||
|
AKSL_CHECK_OK(aksl_strtoull(buf, NULL, 10, &ullout));
|
||||||
|
AKSL_CHECK(ullout == ULLONG_MAX);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Floating point */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_floating_point_forms(void)
|
||||||
|
{
|
||||||
|
double dout = 0.0;
|
||||||
|
float fout = 0.0f;
|
||||||
|
long double ldout = 0.0L;
|
||||||
|
char *end = NULL;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strtod("2.5", NULL, &dout));
|
||||||
|
AKSL_CHECK(dout == 2.5);
|
||||||
|
AKSL_CHECK_OK(aksl_strtof("2.5", NULL, &fout));
|
||||||
|
AKSL_CHECK(fout == 2.5f);
|
||||||
|
AKSL_CHECK_OK(aksl_strtold("2.5", NULL, &ldout));
|
||||||
|
AKSL_CHECK(ldout == 2.5L);
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_strtod("2.5rest", &end, &dout));
|
||||||
|
AKSL_CHECK(dout == 2.5);
|
||||||
|
AKSL_CHECK(strcmp(end, "rest") == 0);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtod("2.5rest", NULL, &dout), AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtof("nope", NULL, &fout), AKERR_VALUE);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtold("nope", NULL, &ldout), AKERR_VALUE);
|
||||||
|
|
||||||
|
/* float has a much smaller range than double, and says so. */
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtof("1e300", NULL, &fout), ERANGE);
|
||||||
|
AKSL_CHECK(fout == 0.0f);
|
||||||
|
AKSL_CHECK_OK(aksl_strtod("1e300", NULL, &dout));
|
||||||
|
AKSL_CHECK(dout > 0.0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Argument validation */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_every_form_rejects_null_arguments(void)
|
||||||
|
{
|
||||||
|
long lout = 0;
|
||||||
|
long long llout = 0;
|
||||||
|
unsigned long ulout = 0;
|
||||||
|
unsigned long long ullout = 0;
|
||||||
|
double dout = 0.0;
|
||||||
|
float fout = 0.0f;
|
||||||
|
long double ldout = 0.0L;
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtol(NULL, NULL, 10, &lout), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtol("1", NULL, 10, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtoll(NULL, NULL, 10, &llout), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtoll("1", NULL, 10, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtoul(NULL, NULL, 10, &ulout), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtoul("1", NULL, 10, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtoull(NULL, NULL, 10, &ullout), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtoull("1", NULL, 10, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtod(NULL, NULL, &dout), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtod("1", NULL, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtof(NULL, NULL, &fout), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtof("1", NULL, NULL), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtold(NULL, NULL, &ldout), AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_strtold("1", NULL, NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
akerr_init();
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_base_is_honoured);
|
||||||
|
AKSL_RUN(failures, test_base_zero_detects_the_prefix);
|
||||||
|
AKSL_RUN(failures, test_digits_outside_the_base_end_the_number);
|
||||||
|
AKSL_RUN(failures, test_invalid_base_is_rejected);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_endptr_reports_where_parsing_stopped);
|
||||||
|
AKSL_RUN(failures, test_every_form_writes_the_endptr);
|
||||||
|
AKSL_RUN(failures, test_no_digits_is_an_error_even_with_an_endptr);
|
||||||
|
AKSL_RUN(failures, test_endptr_chains_across_a_list);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_range_errors_per_type);
|
||||||
|
AKSL_RUN(failures, test_unsigned_forms_refuse_a_negative);
|
||||||
|
AKSL_RUN(failures, test_unsigned_boundaries_round_trip);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_floating_point_forms);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_every_form_rejects_null_arguments);
|
||||||
|
|
||||||
|
AKSL_REPORT(failures);
|
||||||
|
}
|
||||||
638
tests/test_tree.c
Normal file
638
tests/test_tree.c
Normal file
@@ -0,0 +1,638 @@
|
|||||||
|
/*
|
||||||
|
* Tree traversal -- TODO.md section 1.8, complete.
|
||||||
|
*
|
||||||
|
* The old version of this file counted steps, which cannot tell the three
|
||||||
|
* depth-first orders apart because all three visit all seven nodes -- and could
|
||||||
|
* not prove that AKERR_ITERATOR_BREAK stopped anything either, because it hid
|
||||||
|
* its target in tree[6], the last node visited in every depth-first order. Every
|
||||||
|
* traversal here records the node pointers it was handed, in order, and compares
|
||||||
|
* against the expected sequence.
|
||||||
|
*
|
||||||
|
* The 7-node tree used throughout:
|
||||||
|
*
|
||||||
|
* TREE[0]
|
||||||
|
* +--------^^---------+
|
||||||
|
* | |
|
||||||
|
* TREE[1] TREE[2]
|
||||||
|
* +---^^---+ +---^^---+
|
||||||
|
* | | | |
|
||||||
|
* TREE[3] TREE[4] TREE[5] TREE[6]
|
||||||
|
*
|
||||||
|
* pre-order 0 1 3 4 2 5 6
|
||||||
|
* in-order 3 1 4 0 5 2 6
|
||||||
|
* post-order 3 4 1 5 6 2 0
|
||||||
|
* BFS 0 1 2 3 4 5 6
|
||||||
|
* BFS_RIGHT 0 2 1 6 5 4 3
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aksl_capture.h"
|
||||||
|
|
||||||
|
#define MAX_LEAVES 7
|
||||||
|
#define MAX_VISITS 32
|
||||||
|
|
||||||
|
typedef struct VisitLog
|
||||||
|
{
|
||||||
|
int count;
|
||||||
|
aksl_TreeNode *seen[MAX_VISITS];
|
||||||
|
aksl_TreeNode *break_at; /* node to raise ITERATOR_BREAK on, or NULL */
|
||||||
|
aksl_TreeNode *fail_at; /* node to raise AKERR_VALUE on, or NULL */
|
||||||
|
} VisitLog;
|
||||||
|
|
||||||
|
static void visitlog_init(VisitLog *log)
|
||||||
|
{
|
||||||
|
memset((void *)log, 0x00, sizeof(VisitLog));
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext AKERR_NOIGNORE *record_visit(aksl_TreeNode *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 < MAX_VISITS ) {
|
||||||
|
log->seen[log->count] = node;
|
||||||
|
}
|
||||||
|
log->count += 1;
|
||||||
|
if ( log->fail_at == node ) {
|
||||||
|
FAIL_RETURN(e, AKERR_VALUE, "iterator failed at node %p", (void *)node);
|
||||||
|
}
|
||||||
|
if ( log->break_at == node ) {
|
||||||
|
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop at node %p", (void *)node);
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void build_tree(aksl_TreeNode *tree)
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < MAX_LEAVES; i++ ) {
|
||||||
|
memset((void *)&tree[i], 0x00, sizeof(aksl_TreeNode));
|
||||||
|
}
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Assert that the visit log matches `expected`, which is a list of tree indices. */
|
||||||
|
static int check_order(VisitLog *log, aksl_TreeNode *tree,
|
||||||
|
const int *expected, int n)
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
if ( log->count != n ) {
|
||||||
|
fprintf(stderr, " CHECK FAILED: visited %d nodes, expected %d\n",
|
||||||
|
log->count, n);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
for ( i = 0; i < n; i++ ) {
|
||||||
|
if ( log->seen[i] != &tree[expected[i]] ) {
|
||||||
|
fprintf(stderr, " CHECK FAILED: visit %d was node %ld, expected %d\n",
|
||||||
|
i, (long)(log->seen[i] - tree), expected[i]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* aksl_tree_node_init */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_node_init_zeroes_the_links(void)
|
||||||
|
{
|
||||||
|
aksl_TreeNode node;
|
||||||
|
int payload = 3;
|
||||||
|
|
||||||
|
memset((void *)&node, 0xff, sizeof(node));
|
||||||
|
AKSL_CHECK_OK(aksl_tree_node_init(&node, &payload));
|
||||||
|
AKSL_CHECK(node.parent == NULL);
|
||||||
|
AKSL_CHECK(node.left == NULL);
|
||||||
|
AKSL_CHECK(node.right == NULL);
|
||||||
|
AKSL_CHECK(node.leaf == (void *)&payload);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_tree_node_init(NULL, NULL), AKERR_NULLPOINTER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Traversal orders */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_preorder_visits_root_left_right(void)
|
||||||
|
{
|
||||||
|
aksl_TreeNode tree[MAX_LEAVES];
|
||||||
|
VisitLog log;
|
||||||
|
static const int expected[MAX_LEAVES] = { 0, 1, 3, 4, 2, 5, 6 };
|
||||||
|
|
||||||
|
build_tree(tree);
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_PREORDER, &log));
|
||||||
|
AKSL_CHECK(check_order(&log, tree, expected, MAX_LEAVES) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_inorder_visits_left_root_right(void)
|
||||||
|
{
|
||||||
|
aksl_TreeNode tree[MAX_LEAVES];
|
||||||
|
VisitLog log;
|
||||||
|
static const int expected[MAX_LEAVES] = { 3, 1, 4, 0, 5, 2, 6 };
|
||||||
|
|
||||||
|
build_tree(tree);
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_INORDER, &log));
|
||||||
|
AKSL_CHECK(check_order(&log, tree, expected, MAX_LEAVES) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_postorder_visits_left_right_root(void)
|
||||||
|
{
|
||||||
|
aksl_TreeNode tree[MAX_LEAVES];
|
||||||
|
VisitLog log;
|
||||||
|
static const int expected[MAX_LEAVES] = { 3, 4, 1, 5, 6, 2, 0 };
|
||||||
|
|
||||||
|
build_tree(tree);
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_POSTORDER, &log));
|
||||||
|
AKSL_CHECK(check_order(&log, tree, expected, MAX_LEAVES) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AKSL_TREE_SEARCH_DFS is documented as an alias for the pre-order mode. */
|
||||||
|
static int test_dfs_is_an_alias_for_preorder(void)
|
||||||
|
{
|
||||||
|
aksl_TreeNode tree[MAX_LEAVES];
|
||||||
|
VisitLog log;
|
||||||
|
static const int expected[MAX_LEAVES] = { 0, 1, 3, 4, 2, 5, 6 };
|
||||||
|
|
||||||
|
build_tree(tree);
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS, &log));
|
||||||
|
AKSL_CHECK(check_order(&log, tree, expected, MAX_LEAVES) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* BFS was AKERR_NOT_IMPLEMENTED, and the lalloc/lfree parameters that existed to
|
||||||
|
* serve it were defaulted and then never called -- TODO.md 2.2.8 and 2.2.10.
|
||||||
|
* Both modes work now, and the allocator test below proves the queue is real.
|
||||||
|
*/
|
||||||
|
static int test_bfs_visits_level_by_level(void)
|
||||||
|
{
|
||||||
|
aksl_TreeNode tree[MAX_LEAVES];
|
||||||
|
VisitLog log;
|
||||||
|
static const int expected[MAX_LEAVES] = { 0, 1, 2, 3, 4, 5, 6 };
|
||||||
|
|
||||||
|
build_tree(tree);
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_BFS, &log));
|
||||||
|
AKSL_CHECK(check_order(&log, tree, expected, MAX_LEAVES) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_bfs_right_visits_right_child_first(void)
|
||||||
|
{
|
||||||
|
aksl_TreeNode tree[MAX_LEAVES];
|
||||||
|
VisitLog log;
|
||||||
|
static const int expected[MAX_LEAVES] = { 0, 2, 1, 6, 5, 4, 3 };
|
||||||
|
|
||||||
|
build_tree(tree);
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_BFS_RIGHT, &log));
|
||||||
|
AKSL_CHECK(check_order(&log, tree, expected, MAX_LEAVES) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AKSL_TREE_SEARCH_VISIT: this node and no further. */
|
||||||
|
static int test_visit_mode_does_not_descend(void)
|
||||||
|
{
|
||||||
|
aksl_TreeNode tree[MAX_LEAVES];
|
||||||
|
VisitLog log;
|
||||||
|
static const int expected[1] = { 0 };
|
||||||
|
|
||||||
|
build_tree(tree);
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_VISIT, &log));
|
||||||
|
AKSL_CHECK(check_order(&log, tree, expected, 1) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Custom allocator */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int counting_alloc_calls = 0;
|
||||||
|
static int counting_free_calls = 0;
|
||||||
|
|
||||||
|
static akerr_ErrorContext AKERR_NOIGNORE *counting_alloc(size_t size, void **dest)
|
||||||
|
{
|
||||||
|
counting_alloc_calls += 1;
|
||||||
|
return aksl_malloc(size, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext AKERR_NOIGNORE *counting_free(void *ptr)
|
||||||
|
{
|
||||||
|
counting_free_calls += 1;
|
||||||
|
return aksl_free(ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO.md 1.8: "Custom lalloc/lfree are actually invoked -- currently they are
|
||||||
|
* stored and never called". They are called now, once per node enqueued, and
|
||||||
|
* every allocation is released. The depth-first modes allocate nothing at all,
|
||||||
|
* which is the other half of the contract.
|
||||||
|
*/
|
||||||
|
static int test_custom_allocator_is_used_and_balanced(void)
|
||||||
|
{
|
||||||
|
aksl_TreeNode tree[MAX_LEAVES];
|
||||||
|
VisitLog log;
|
||||||
|
|
||||||
|
build_tree(tree);
|
||||||
|
visitlog_init(&log);
|
||||||
|
counting_alloc_calls = 0;
|
||||||
|
counting_free_calls = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit,
|
||||||
|
&counting_alloc, &counting_free,
|
||||||
|
AKSL_TREE_SEARCH_BFS, &log));
|
||||||
|
AKSL_CHECK(log.count == MAX_LEAVES);
|
||||||
|
/* One queue entry per node visited, and every one of them released. */
|
||||||
|
AKSL_CHECK(counting_alloc_calls == MAX_LEAVES);
|
||||||
|
AKSL_CHECK(counting_free_calls == MAX_LEAVES);
|
||||||
|
|
||||||
|
/* Depth-first needs no queue, so it must not touch the allocator. */
|
||||||
|
counting_alloc_calls = 0;
|
||||||
|
counting_free_calls = 0;
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit,
|
||||||
|
&counting_alloc, &counting_free,
|
||||||
|
AKSL_TREE_SEARCH_DFS_PREORDER, &log));
|
||||||
|
AKSL_CHECK(counting_alloc_calls == 0);
|
||||||
|
AKSL_CHECK(counting_free_calls == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A break part-way through a BFS still drains the queue it had already built. */
|
||||||
|
static int test_break_during_bfs_releases_the_queue(void)
|
||||||
|
{
|
||||||
|
aksl_TreeNode tree[MAX_LEAVES];
|
||||||
|
VisitLog log;
|
||||||
|
|
||||||
|
build_tree(tree);
|
||||||
|
visitlog_init(&log);
|
||||||
|
log.break_at = &tree[1];
|
||||||
|
counting_alloc_calls = 0;
|
||||||
|
counting_free_calls = 0;
|
||||||
|
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit,
|
||||||
|
&counting_alloc, &counting_free,
|
||||||
|
AKSL_TREE_SEARCH_BFS, &log));
|
||||||
|
/* 0 and 1 visited; the walk stops at 1 with 2 still sitting in the queue. */
|
||||||
|
AKSL_CHECK(log.count == 2);
|
||||||
|
AKSL_CHECK(counting_alloc_calls > 0);
|
||||||
|
AKSL_CHECK(counting_alloc_calls == counting_free_calls);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Degenerate shapes */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_single_node_tree_visits_once_in_every_order(void)
|
||||||
|
{
|
||||||
|
aksl_TreeNode node;
|
||||||
|
VisitLog log;
|
||||||
|
static const uint8_t modes[] = {
|
||||||
|
AKSL_TREE_SEARCH_DFS_PREORDER,
|
||||||
|
AKSL_TREE_SEARCH_DFS_INORDER,
|
||||||
|
AKSL_TREE_SEARCH_DFS_POSTORDER,
|
||||||
|
AKSL_TREE_SEARCH_BFS,
|
||||||
|
AKSL_TREE_SEARCH_BFS_RIGHT,
|
||||||
|
AKSL_TREE_SEARCH_VISIT,
|
||||||
|
};
|
||||||
|
size_t i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < sizeof(modes) / sizeof(modes[0]); i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_tree_node_init(&node, NULL));
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&node, &record_visit, NULL, NULL,
|
||||||
|
modes[i], &log));
|
||||||
|
AKSL_CHECK(log.count == 1);
|
||||||
|
AKSL_CHECK(log.seen[0] == &node);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Left-only and right-only chains of three nodes, no branching anywhere. */
|
||||||
|
static int test_degenerate_chains(void)
|
||||||
|
{
|
||||||
|
aksl_TreeNode chain[3];
|
||||||
|
VisitLog log;
|
||||||
|
static const int forward[3] = { 0, 1, 2 };
|
||||||
|
static const int backward[3] = { 2, 1, 0 };
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
/* Left-only: 0 -> 1 -> 2 */
|
||||||
|
for ( i = 0; i < 3; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_tree_node_init(&chain[i], NULL));
|
||||||
|
}
|
||||||
|
chain[0].left = &chain[1];
|
||||||
|
chain[1].left = &chain[2];
|
||||||
|
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_PREORDER, &log));
|
||||||
|
AKSL_CHECK(check_order(&log, chain, forward, 3) == 0);
|
||||||
|
|
||||||
|
/* In-order down a left chain arrives at the deepest node first. */
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_INORDER, &log));
|
||||||
|
AKSL_CHECK(check_order(&log, chain, backward, 3) == 0);
|
||||||
|
|
||||||
|
/* Right-only: the same shape, mirrored. */
|
||||||
|
for ( i = 0; i < 3; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_tree_node_init(&chain[i], NULL));
|
||||||
|
}
|
||||||
|
chain[0].right = &chain[1];
|
||||||
|
chain[1].right = &chain[2];
|
||||||
|
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_INORDER, &log));
|
||||||
|
AKSL_CHECK(check_order(&log, chain, forward, 3) == 0);
|
||||||
|
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_BFS, &log));
|
||||||
|
AKSL_CHECK(check_order(&log, chain, forward, 3) == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO.md 1.8 / 2.2.7: a chain deeper than the recursion can take. It used to
|
||||||
|
* overflow the stack; it is AKERR_OUTOFBOUNDS now, and the message names the
|
||||||
|
* documented limit. Built one node past the cap so the failure is the cap itself
|
||||||
|
* and not some incidental shortfall. `static` because AKSL_TREE_MAX_DEPTH nodes
|
||||||
|
* on the stack of a test function is not the point of the test.
|
||||||
|
*/
|
||||||
|
static int test_tree_deeper_than_the_cap_is_out_of_bounds(void)
|
||||||
|
{
|
||||||
|
static aksl_TreeNode chain[AKSL_TREE_MAX_DEPTH + 2];
|
||||||
|
VisitLog log;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < AKSL_TREE_MAX_DEPTH + 2; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_tree_node_init(&chain[i], NULL));
|
||||||
|
}
|
||||||
|
for ( i = 0; i < AKSL_TREE_MAX_DEPTH + 1; i++ ) {
|
||||||
|
chain[i].left = &chain[i + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||||
|
aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_PREORDER, &log),
|
||||||
|
AKERR_OUTOFBOUNDS, "AKSL_TREE_MAX_DEPTH");
|
||||||
|
|
||||||
|
/* Exactly at the cap is fine -- a limit, not an off-by-one. */
|
||||||
|
chain[AKSL_TREE_MAX_DEPTH - 1].left = NULL;
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_PREORDER, &log));
|
||||||
|
AKSL_CHECK(log.count == AKSL_TREE_MAX_DEPTH);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* And again leaning right. The recursion counts depth separately for each
|
||||||
|
* child, so a chain that only ever goes left says nothing about whether the
|
||||||
|
* right-hand descent counts at all -- mutation testing found exactly that:
|
||||||
|
* changing the right child's `depth + 1` to `depth + 0` survived the whole
|
||||||
|
* suite, because nothing here had ever recursed right more than three deep.
|
||||||
|
*/
|
||||||
|
for ( i = 0; i < AKSL_TREE_MAX_DEPTH + 2; i++ ) {
|
||||||
|
AKSL_CHECK_OK(aksl_tree_node_init(&chain[i], NULL));
|
||||||
|
}
|
||||||
|
for ( i = 0; i < AKSL_TREE_MAX_DEPTH + 1; i++ ) {
|
||||||
|
chain[i].right = &chain[i + 1];
|
||||||
|
}
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_STATUS(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_PREORDER, &log),
|
||||||
|
AKERR_OUTOFBOUNDS);
|
||||||
|
|
||||||
|
/* Both orders that descend right, so in-order and post-order count too. */
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_STATUS(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_INORDER, &log),
|
||||||
|
AKERR_OUTOFBOUNDS);
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_STATUS(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_POSTORDER, &log),
|
||||||
|
AKERR_OUTOFBOUNDS);
|
||||||
|
|
||||||
|
/* And breadth-first, whose depth is carried on the queue entry instead. */
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_STATUS(aksl_tree_iterate(&chain[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_BFS, &log),
|
||||||
|
AKERR_OUTOFBOUNDS);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A child pointing back at an ancestor. This used to recurse until the process
|
||||||
|
* died. The depth-first walk carries the ancestor chain and recognises the node
|
||||||
|
* as its own forebear; the breadth-first walk has no ancestor chain to compare
|
||||||
|
* against, so the same tree comes back as AKERR_OUTOFBOUNDS through the depth
|
||||||
|
* cap instead -- a different status for the same shape, which the header says
|
||||||
|
* out loud rather than leaving to be discovered.
|
||||||
|
*/
|
||||||
|
static int test_cyclic_tree_is_caught(void)
|
||||||
|
{
|
||||||
|
aksl_TreeNode tree[MAX_LEAVES];
|
||||||
|
VisitLog log;
|
||||||
|
|
||||||
|
build_tree(tree);
|
||||||
|
tree[3].left = &tree[0]; /* back to the root */
|
||||||
|
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||||
|
aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_PREORDER, &log),
|
||||||
|
AKERR_CIRCULAR_REFERENCE, "own ancestor");
|
||||||
|
|
||||||
|
visitlog_init(&log);
|
||||||
|
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_BFS, &log),
|
||||||
|
AKERR_OUTOFBOUNDS);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Callback control flow */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The defect that made ITERATOR_BREAK useless on a tree: the recursive frame
|
||||||
|
* that raised the break handled it in its own PROCESS/HANDLE block and returned
|
||||||
|
* success, so the parent frame's PASS saw nothing wrong and carried straight on
|
||||||
|
* into the sibling subtree. All seven nodes were visited no matter where the
|
||||||
|
* break was raised. TODO.md 2.1.3.
|
||||||
|
*
|
||||||
|
* One case per order, each breaking on a node that is *not* last in that order --
|
||||||
|
* which is precisely what the old test could not do, because it hid its target
|
||||||
|
* in tree[6], the final node in all three depth-first walks.
|
||||||
|
*/
|
||||||
|
static int test_break_aborts_the_whole_traversal(void)
|
||||||
|
{
|
||||||
|
aksl_TreeNode tree[MAX_LEAVES];
|
||||||
|
VisitLog log;
|
||||||
|
|
||||||
|
build_tree(tree);
|
||||||
|
|
||||||
|
/* Pre-order 0 1 3 ... : breaking at 3 is the third visit. */
|
||||||
|
visitlog_init(&log);
|
||||||
|
log.break_at = &tree[3];
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_PREORDER, &log));
|
||||||
|
AKSL_CHECK(log.count == 3);
|
||||||
|
|
||||||
|
/* In-order 3 1 4 ... : breaking at 4 is the third visit. */
|
||||||
|
visitlog_init(&log);
|
||||||
|
log.break_at = &tree[4];
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_INORDER, &log));
|
||||||
|
AKSL_CHECK(log.count == 3);
|
||||||
|
|
||||||
|
/* Post-order 3 4 1 5 ... : breaking at 5 is the fourth visit. */
|
||||||
|
visitlog_init(&log);
|
||||||
|
log.break_at = &tree[5];
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_POSTORDER, &log));
|
||||||
|
AKSL_CHECK(log.count == 4);
|
||||||
|
|
||||||
|
/* BFS 0 1 2 3 ... : breaking at 2 is the third visit. */
|
||||||
|
visitlog_init(&log);
|
||||||
|
log.break_at = &tree[2];
|
||||||
|
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_BFS, &log));
|
||||||
|
AKSL_CHECK(log.count == 3);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Any other status propagates out with its message and its own status intact. */
|
||||||
|
static int test_callback_error_propagates(void)
|
||||||
|
{
|
||||||
|
aksl_TreeNode tree[MAX_LEAVES];
|
||||||
|
VisitLog log;
|
||||||
|
|
||||||
|
build_tree(tree);
|
||||||
|
visitlog_init(&log);
|
||||||
|
log.fail_at = &tree[3];
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||||
|
aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_PREORDER, &log),
|
||||||
|
AKERR_VALUE, "iterator failed at node");
|
||||||
|
AKSL_CHECK(log.count == 3);
|
||||||
|
|
||||||
|
/* And out of a BFS, where the queue has to be drained on the way past. */
|
||||||
|
visitlog_init(&log);
|
||||||
|
log.fail_at = &tree[2];
|
||||||
|
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_BFS, &log),
|
||||||
|
AKERR_VALUE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
/* Argument validation */
|
||||||
|
/* ---------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int test_null_arguments(void)
|
||||||
|
{
|
||||||
|
aksl_TreeNode tree[MAX_LEAVES];
|
||||||
|
VisitLog log;
|
||||||
|
|
||||||
|
build_tree(tree);
|
||||||
|
visitlog_init(&log);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS(aksl_tree_iterate(NULL, &record_visit, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_PREORDER, &log),
|
||||||
|
AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], NULL, NULL, NULL,
|
||||||
|
AKSL_TREE_SEARCH_DFS_PREORDER, &log),
|
||||||
|
AKERR_NULLPOINTER);
|
||||||
|
AKSL_CHECK(log.count == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO.md 2.2.9: the switch had no default, so an unrecognised mode -- and
|
||||||
|
* AKSL_TREE_SEARCH_VISIT, which the header documented but nothing implemented --
|
||||||
|
* fell straight through to SUCCEED_RETURN having visited nothing at all. A
|
||||||
|
* traversal that silently did not happen, reported as success.
|
||||||
|
*/
|
||||||
|
static int test_unknown_searchmode_is_a_value_error(void)
|
||||||
|
{
|
||||||
|
aksl_TreeNode tree[MAX_LEAVES];
|
||||||
|
VisitLog log;
|
||||||
|
|
||||||
|
build_tree(tree);
|
||||||
|
visitlog_init(&log);
|
||||||
|
|
||||||
|
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
||||||
|
aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL, 99, &log),
|
||||||
|
AKERR_VALUE, "unknown searchmode");
|
||||||
|
AKSL_CHECK(log.count == 0);
|
||||||
|
|
||||||
|
/* 6 is one past the last defined mode, and just as unacceptable. */
|
||||||
|
AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], &record_visit, NULL, NULL, 6, &log),
|
||||||
|
AKERR_VALUE);
|
||||||
|
AKSL_CHECK(log.count == 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
akerr_init();
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_node_init_zeroes_the_links);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_preorder_visits_root_left_right);
|
||||||
|
AKSL_RUN(failures, test_inorder_visits_left_root_right);
|
||||||
|
AKSL_RUN(failures, test_postorder_visits_left_right_root);
|
||||||
|
AKSL_RUN(failures, test_dfs_is_an_alias_for_preorder);
|
||||||
|
AKSL_RUN(failures, test_bfs_visits_level_by_level);
|
||||||
|
AKSL_RUN(failures, test_bfs_right_visits_right_child_first);
|
||||||
|
AKSL_RUN(failures, test_visit_mode_does_not_descend);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_custom_allocator_is_used_and_balanced);
|
||||||
|
AKSL_RUN(failures, test_break_during_bfs_releases_the_queue);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_single_node_tree_visits_once_in_every_order);
|
||||||
|
AKSL_RUN(failures, test_degenerate_chains);
|
||||||
|
AKSL_RUN(failures, test_tree_deeper_than_the_cap_is_out_of_bounds);
|
||||||
|
AKSL_RUN(failures, test_cyclic_tree_is_caught);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_break_aborts_the_whole_traversal);
|
||||||
|
AKSL_RUN(failures, test_callback_error_propagates);
|
||||||
|
|
||||||
|
AKSL_RUN(failures, test_null_arguments);
|
||||||
|
AKSL_RUN(failures, test_unknown_searchmode_is_a_value_error);
|
||||||
|
|
||||||
|
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