Version at 0.2.0: complete the wishlist, document it, gate the docs
Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that
file to hold outstanding items only.
The API break gets a minor bump, because pre-1.0 the soname carries
MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five
signatures changed and the ato* contract with them; UPGRADING.md is new
and lists every one, with the before/after for the cases the compiler
cannot warn about.
Section 3.1 is finished: reallocarray with the multiplication checked,
aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf.
Four functions on that list are deliberately absent rather than missing
-- sprintf, strtok, setbuf and perror -- and TODO.md now says which and
why, so nobody adds them thinking they were forgotten.
Section 1.9, the cross-cutting tests:
tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR
+ 10 times and checks the pool after each round,
because a wrapper that leaks a slot fails a
hundred calls later in unrelated code. It also
asserts that each error names the function and
file it was raised from, which is what catches a
FAIL that migrates into a helper during a
refactor: status right, message right, origin
quietly lying.
tests/negative/ two sources that must FAIL to compile, built with
-Werror and registered WILL_FAIL. AKERR_NOIGNORE
and the format attributes are enforced by the
compiler and by nothing else; drop either and
every ordinary test still passes.
Thread safety is answered rather than tested: the library is not
thread-safe and cannot be made so from here, because libakerror's error
pool is an unlocked process-global array. README.md says so plainly and
TODO.md carries it as the item blocking any future pthread wrappers.
Doxygen is configured and gated. All 147 public functions have @brief,
a @param each, @throws per status and @return; EXTRACT_ALL is off and
WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an
undocumented entity. It ran to 0 warnings. The Doxyfile carries no
version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from
project(), so that stays the one place a version is written.
CI now builds against the submodule it pins instead of also installing
libakerror@main and never linking it, adds -Werror, and gains a
sanitizer job. The pre-push hook matches, and runs the docs check too.
Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The
eight uncovered lines are each uncovered on purpose and TODO.md says
which and why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,20 +17,48 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y cmake gcc moreutils
|
||||
# Depends on libakerror@main
|
||||
git clone https://source.starfort.tech/andrew/libakerror.git
|
||||
cd libakerror
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
cmake --install build
|
||||
# This step used to clone and install libakerror@main as well. 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. `cmake --install` below still installs both, so the
|
||||
# find_package and pkg-config paths are exercised.
|
||||
- name: build and test
|
||||
run: |
|
||||
cmake -S . -B build
|
||||
# -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
|
||||
- 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:
|
||||
@@ -45,28 +73,27 @@ jobs:
|
||||
# scripts/coverage.py needs nothing but python3 and gcc's own gcov, so
|
||||
# there is no lcov/gcovr to install here.
|
||||
#
|
||||
# The gate is a ratchet, not a target: src/stdlib.c is at 99.1% of lines
|
||||
# and 45.1% of branches, so 90/40 fails on a real regression (a test
|
||||
# deleted, or new untested code added) without tripping over rounding.
|
||||
# The report is printed either way -- the uncovered lines it lists are the
|
||||
# The gate is a ratchet, not a target. Across all four sources the suite
|
||||
# covers 99.5% of lines (1643/1651), 46.0% of branches and 100% of
|
||||
# functions (147/147), so 90/40 fails on a real regression -- a test
|
||||
# deleted, or new untested code added -- without tripping over rounding.
|
||||
# The report is printed either way; the uncovered lines it lists are the
|
||||
# missing tests.
|
||||
#
|
||||
# Branch coverage is back over 45 (the aksl_version_* functions are fully
|
||||
# covered, which lifted it from 44.3%), but the gate stays at 40: 0.1
|
||||
# points of headroom is not a ratchet, it is a coin flip on the next
|
||||
# rounding change.
|
||||
# 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.
|
||||
#
|
||||
# The branch gate was 45 against 51.0% until the libakerror 1.0.0 bump.
|
||||
# Nothing about this library's tests changed: line coverage held at
|
||||
# 99.0% (200/202) and function coverage at 100% (21/21), but the branch
|
||||
# denominator went from 661 to 1087 because the 1.0.0 PREPARE_ERROR /
|
||||
# FAIL_* macros expand to more branches at every call site in
|
||||
# src/stdlib.c, and most of the added branches are not reachable from the
|
||||
# way this library calls them. 337/661 became 481/1087 -- 144 more
|
||||
# branches covered, 426 more branches counted. Chasing them here would be
|
||||
# testing libakerror's macros, which is libakerror's mutation suite's job
|
||||
# (macros expand at the call site, so coverage cannot see them properly
|
||||
# from either side). Re-ratcheted rather than papered over.
|
||||
# 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 \
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
#
|
||||
# Runs by default, a few seconds all in:
|
||||
#
|
||||
# * default build + ctest
|
||||
# * 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):
|
||||
@@ -72,22 +73,40 @@ run() {
|
||||
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"
|
||||
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
|
||||
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.
|
||||
if ! python3 scripts/mutation_test.py \
|
||||
--target src/stdlib.c \
|
||||
--target src/string.c \
|
||||
--target src/stream.c \
|
||||
--target src/collections.c \
|
||||
--threshold "$MUTATION_THRESHOLD"; then
|
||||
echo >&2
|
||||
echo "pre-push: mutation score below ${MUTATION_THRESHOLD}%. Push aborted." >&2
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -25,3 +25,9 @@ coverage.xml
|
||||
*.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
|
||||
|
||||
75
AGENTS.md
75
AGENTS.md
@@ -2,14 +2,27 @@
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
`libakstdlib` is a C shared library that wraps libc calls and small data
|
||||
structures in `libakerror` error contexts. Public API declarations live in
|
||||
`include/akstdlib.h`; implementation lives in `src/stdlib.c`. Tests are
|
||||
one-file CTest executables under `tests/test_<name>.c`, with shared test helpers
|
||||
in `tests/aksl_capture.h`. CMake package templates are in `cmake/` and
|
||||
`akstdlib.pc.in`. The vendored dependency is `deps/libakerror`; update it as a
|
||||
submodule rather than editing generated files under `build/`, `build-asan/` or
|
||||
`build-coverage/`.
|
||||
`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 -- 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
|
||||
|
||||
@@ -40,8 +53,15 @@ cmake --build build-coverage --target coverage
|
||||
```
|
||||
|
||||
Run `cmake --build build --target mutation` only when you need the slower
|
||||
mutation harness. `rebuild.sh` installs to `/home/andrew/local` and removes the
|
||||
local build directory, so treat it as a local convenience script.
|
||||
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
|
||||
|
||||
@@ -51,16 +71,45 @@ 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.
|
||||
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.
|
||||
|
||||
`src/stdlib.c` is at 99.1% line coverage and CI gates it at 90 (line) / 40
|
||||
(branch), so new code needs tests in the same commit. Run
|
||||
`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.
|
||||
|
||||
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 —
|
||||
|
||||
@@ -4,10 +4,20 @@ cmake_minimum_required(VERSION 3.10)
|
||||
# shared library's VERSION/SOVERSION, the Version: field in akstdlib.pc, and
|
||||
# akstdlibConfigVersion.cmake. Nothing else should spell a version number.
|
||||
#
|
||||
# 0.x on purpose: TODO.md section 2.1 still records four confirmed defects whose
|
||||
# fixes change documented behaviour (aksl_atoi's contract, aksl_realpath's
|
||||
# signature), so this library is not promising a stable API yet.
|
||||
project(akstdlib VERSION 0.1.0 LANGUAGES C)
|
||||
# 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
|
||||
@@ -302,6 +312,7 @@ set(AKSL_TESTS
|
||||
linkedlist
|
||||
memory
|
||||
path
|
||||
pool
|
||||
status_registry
|
||||
strbuf
|
||||
stream
|
||||
@@ -339,6 +350,39 @@ foreach(_test IN LISTS AKSL_TESTS AKSL_WILL_FAIL_TESTS AKSL_KNOWN_FAILING_TESTS)
|
||||
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
|
||||
@@ -445,6 +489,40 @@ elseif(AKSL_COVERAGE)
|
||||
"(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
|
||||
|
||||
58
Doxyfile
58
Doxyfile
@@ -42,9 +42,7 @@ DOXYFILE_ENCODING = UTF-8
|
||||
# title of most generated pages and in a few other places.
|
||||
# The default value is: My Project.
|
||||
|
||||
PROJECT_NAME = "My Project"
|
||||
|
||||
# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
|
||||
PROJECT_NAME = "libakstdlib"
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
@@ -54,9 +52,7 @@ PROJECT_NUMBER =
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
# quick idea about the purpose of the project. Keep the description short.
|
||||
|
||||
PROJECT_BRIEF =
|
||||
|
||||
# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
|
||||
PROJECT_BRIEF = "libc wrappers that report failures through libakerror error contexts"
|
||||
# in the documentation. The maximum height of the logo should not exceed 55
|
||||
# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
|
||||
# the logo to the output directory.
|
||||
@@ -68,9 +64,7 @@ PROJECT_LOGO =
|
||||
# entered, it will be relative to the location where doxygen was started. If
|
||||
# left blank the current directory will be used.
|
||||
|
||||
OUTPUT_DIRECTORY =
|
||||
|
||||
# If the CREATE_SUBDIRS tag is set to YES then doxygen will create up to 4096
|
||||
OUTPUT_DIRECTORY = doxygen
|
||||
# sub-directories (in 2 levels) under the output directory of each output format
|
||||
# and will distribute the generated files over these directories. Enabling this
|
||||
# option can be useful when feeding doxygen a huge amount of source files, where
|
||||
@@ -157,9 +151,7 @@ ABBREVIATE_BRIEF = "The $name class" \
|
||||
# description.
|
||||
# The default value is: NO.
|
||||
|
||||
ALWAYS_DETAILED_SEC = NO
|
||||
|
||||
# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
|
||||
ALWAYS_DETAILED_SEC = YES
|
||||
# inherited members of a class in the documentation of that class as if those
|
||||
# members were ordinary class members. Constructors, destructors and assignment
|
||||
# operators of the base classes will not be shown.
|
||||
@@ -209,9 +201,7 @@ SHORT_NAMES = NO
|
||||
# description.)
|
||||
# The default value is: NO.
|
||||
|
||||
JAVADOC_AUTOBRIEF = NO
|
||||
|
||||
# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line
|
||||
JAVADOC_AUTOBRIEF = YES
|
||||
# such as
|
||||
# /***************
|
||||
# as being the beginning of a Javadoc-style comment "banner". If set to NO, the
|
||||
@@ -291,9 +281,7 @@ ALIASES =
|
||||
# members will be omitted, etc.
|
||||
# The default value is: NO.
|
||||
|
||||
OPTIMIZE_OUTPUT_FOR_C = NO
|
||||
|
||||
# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
|
||||
OPTIMIZE_OUTPUT_FOR_C = YES
|
||||
# Python sources only. Doxygen will then generate output that is more tailored
|
||||
# for that language. For instance, namespaces will be presented as packages,
|
||||
# qualified scopes will look different, etc.
|
||||
@@ -519,8 +507,6 @@ TIMESTAMP = NO
|
||||
# The default value is: NO.
|
||||
|
||||
EXTRACT_ALL = NO
|
||||
|
||||
# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
|
||||
# be included in the documentation.
|
||||
# The default value is: NO.
|
||||
|
||||
@@ -543,8 +529,6 @@ EXTRACT_PACKAGE = NO
|
||||
# The default value is: NO.
|
||||
|
||||
EXTRACT_STATIC = NO
|
||||
|
||||
# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
|
||||
# locally in source files will be included in the documentation. If set to NO,
|
||||
# only classes defined in header files are included. Does not have any effect
|
||||
# for Java sources.
|
||||
@@ -681,9 +665,7 @@ INLINE_INFO = YES
|
||||
# name. If set to NO, the members will appear in declaration order.
|
||||
# The default value is: YES.
|
||||
|
||||
SORT_MEMBER_DOCS = YES
|
||||
|
||||
# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
|
||||
SORT_MEMBER_DOCS = NO
|
||||
# descriptions of file, namespace and class members alphabetically by member
|
||||
# name. If set to NO, the members will appear in declaration order. Note that
|
||||
# this will also influence the order of the classes in the class list.
|
||||
@@ -853,8 +835,6 @@ WARNINGS = YES
|
||||
# The default value is: YES.
|
||||
|
||||
WARN_IF_UNDOCUMENTED = YES
|
||||
|
||||
# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
|
||||
# potential errors in the documentation, such as documenting some parameters in
|
||||
# a documented function twice, or documenting parameters that don't exist or
|
||||
# using markup commands wrongly.
|
||||
@@ -877,9 +857,7 @@ WARN_IF_INCOMPLETE_DOC = YES
|
||||
# WARN_IF_INCOMPLETE_DOC
|
||||
# The default value is: NO.
|
||||
|
||||
WARN_NO_PARAMDOC = NO
|
||||
|
||||
# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, doxygen will warn about
|
||||
WARN_NO_PARAMDOC = YES
|
||||
# undocumented enumeration values. If set to NO, doxygen will accept
|
||||
# undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag
|
||||
# will automatically be disabled.
|
||||
@@ -931,9 +909,7 @@ WARN_LINE_FORMAT = "at line $line of file $file"
|
||||
# specified the warning and error messages are written to standard output
|
||||
# (stdout).
|
||||
|
||||
WARN_LOGFILE =
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
WARN_LOGFILE = doxygen-warnings.log
|
||||
# Configuration options related to the input files
|
||||
#---------------------------------------------------------------------------
|
||||
|
||||
@@ -943,9 +919,7 @@ WARN_LOGFILE =
|
||||
# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
|
||||
# Note: If this tag is empty the current directory is searched.
|
||||
|
||||
INPUT =
|
||||
|
||||
# This tag can be used to specify the character encoding of the source files
|
||||
INPUT = include src
|
||||
# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
|
||||
# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
|
||||
# documentation (see:
|
||||
@@ -1038,18 +1012,14 @@ FILE_PATTERNS = *.c \
|
||||
# be searched for input files as well.
|
||||
# The default value is: NO.
|
||||
|
||||
RECURSIVE = YES
|
||||
|
||||
# The EXCLUDE tag can be used to specify files and/or directories that should be
|
||||
RECURSIVE = NO
|
||||
# excluded from the INPUT source files. This way you can easily exclude a
|
||||
# subdirectory from a directory tree whose root is specified with the INPUT tag.
|
||||
#
|
||||
# Note that relative paths are relative to the directory from which doxygen is
|
||||
# run.
|
||||
|
||||
EXCLUDE =
|
||||
|
||||
# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
|
||||
EXCLUDE = src/aksl_internal.h
|
||||
# directories that are symbolic links (a Unix file system feature) are excluded
|
||||
# from the input.
|
||||
# The default value is: NO.
|
||||
@@ -1968,9 +1938,7 @@ EXTRA_SEARCH_MAPPINGS =
|
||||
# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.
|
||||
# The default value is: YES.
|
||||
|
||||
GENERATE_LATEX = YES
|
||||
|
||||
# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a
|
||||
GENERATE_LATEX = NO
|
||||
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
|
||||
# it.
|
||||
# The default directory is: latex.
|
||||
|
||||
205
README.md
205
README.md
@@ -5,12 +5,64 @@
|
||||
`libakstdlib` wraps C standard library functions so that they report failures
|
||||
through [libakerror](https://source.starfort.tech/andrew/libakerror)'s
|
||||
`ATTEMPT { ... } HANDLE { ... }` error contexts instead of through return codes
|
||||
and `errno`. It also provides a few data structures built on the same
|
||||
convention (a doubly-linked list and a binary tree).
|
||||
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: what is covered by tests,
|
||||
which corner cases are still open, and which libc functions are not yet wrapped.
|
||||
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
|
||||
|
||||
@@ -27,27 +79,29 @@ package the parent provides instead.
|
||||
|
||||
### This library's own version
|
||||
|
||||
`libakstdlib` is at **0.1.0**. The version lives in exactly one place — the
|
||||
`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.1.0 | From |
|
||||
| Artifact | Value at 0.2.0 | From |
|
||||
| --- | --- | --- |
|
||||
| `AKSL_VERSION_MAJOR` / `_MINOR` / `_PATCH` | `0` / `1` / `0` | `include/akstdlib_version.h.in` |
|
||||
| `AKSL_VERSION_STRING` | `"0.1.0"` | same |
|
||||
| `AKSL_VERSION_NUMBER` | `100` | same |
|
||||
| `AKSL_VERSION_SONAME` | `"0.1"` | same |
|
||||
| shared library | `libakstdlib.so.0.1.0`, soname `libakstdlib.so.0.1` | `VERSION` / `SOVERSION` |
|
||||
| `pkg-config --modversion akstdlib` | `0.1.0` | `akstdlib.pc` |
|
||||
| `find_package(akstdlib 0.1)` | accepted; `0.2` and `1.0` refused | `akstdlibConfigVersion.cmake` |
|
||||
| `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. `TODO.md` §2.1 still records four confirmed defects
|
||||
whose fixes change documented behaviour, so the API is not being promised yet.
|
||||
While the major version is `0`, **the soname carries `MAJOR.MINOR`**: 0.1 and 0.2
|
||||
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
|
||||
@@ -75,18 +129,18 @@ function that compares against the values baked into the library. Calling
|
||||
`aksl_version_check()` with hand-written numbers defeats the whole mechanism.
|
||||
|
||||
Compatibility is defined as "same soname", so pre-1.0 both major and minor must
|
||||
match and the patch level is ignored — a caller built against 0.1.0 keeps working
|
||||
against 0.1.7, which is exactly the promise the shared soname makes.
|
||||
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.2.0 build in under the 0.1 filename, or a package that strips
|
||||
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.1.0 (soname 0.1)
|
||||
loaded : 0.2.0 (0.2.0)
|
||||
MISMATCH DETECTED: compiled against libakstdlib 0.1.0, loaded 0.2.0 (soname 0.2)
|
||||
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
|
||||
@@ -144,11 +198,22 @@ assert on the status it returns, `aksl_temp_file()` for tests that need a real
|
||||
file to work on, and an `AKSL_RUN()` driver that additionally fails any test which
|
||||
leaks a slot from libakerror's error pool.
|
||||
|
||||
One file per area of the API: `convert` (the `ato*` family), `format` (the
|
||||
`printf` family), `stream` (`fopen`/`fread`/`fwrite`/`fclose`), `path`
|
||||
(`aksl_realpath`), `strhash`, `memory`, `linkedlist`, `tree`, and
|
||||
`status_registry` (this library's side of the libakerror status-registry
|
||||
contract — see "The libakerror version floor" above).
|
||||
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`.
|
||||
@@ -162,11 +227,21 @@ of them invert the meaning of "Passed":
|
||||
| `AKSL_WILL_FAIL_TESTS` | Expected to abort by design — an unhandled error reaching `FINISH_NORETURN`, or a deliberate contract violation. Marked `WILL_FAIL`, so a non-zero exit is a pass. |
|
||||
| `AKSL_KNOWN_FAILING_TESTS` | Assert the *correct* behaviour of a confirmed defect (see `TODO.md` §2.1). Also marked `WILL_FAIL`. |
|
||||
|
||||
So `ctest` reporting all green does **not** mean the library is defect-free — it
|
||||
means the known-good tests passed and the known-bad ones are still failing in the
|
||||
documented way. When a defect is fixed, its test starts passing, CTest reports it
|
||||
as failed with *unexpectedly passed*, and that is the cue to move it from
|
||||
`AKSL_KNOWN_FAILING_TESTS` into `AKSL_TESTS`.
|
||||
**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
|
||||
@@ -181,10 +256,19 @@ ctest --test-dir build-asan --output-on-failure
|
||||
```
|
||||
|
||||
Builds the library, the tests and the vendored libakerror with ASan + UBSan and
|
||||
`-fno-sanitize-recover=all`. Several of the open items in `TODO.md` §2 only
|
||||
misbehave under instrumentation — the uninitialised `%s` in `aksl_realpath`, the
|
||||
unbounded `vsprintf` behind `aksl_sprintf`, the missing `va_end` in the `printf`
|
||||
family — so new tests for those should be run this way.
|
||||
`-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
|
||||
|
||||
@@ -246,25 +330,42 @@ cmake -S . -B build-coverage -DAKSL_COVERAGE=ON \
|
||||
-DAKSL_COVERAGE_THRESHOLD=90 -DAKSL_COVERAGE_BRANCH_THRESHOLD=40
|
||||
```
|
||||
|
||||
**Where it stands.** `src/stdlib.c` is at **99.1% of lines (217/219)**, **45.1% of
|
||||
branches (519/1151)** and **25/25 functions**, so 90/40 above is a ratchet with
|
||||
headroom rather than a target. The two uncovered lines are both
|
||||
`} HANDLE(e, AKERR_ITERATOR_BREAK) {` — in libakerror that macro begins with the
|
||||
`break;` belonging to `PROCESS`'s `case 0:` arm, which is only reachable when a
|
||||
callback returns a non-NULL error context whose status is *zero*. That is the
|
||||
pathological case §2.2.1 of `TODO.md` exists to remove, so it is left uncovered
|
||||
deliberately rather than pinned by a test.
|
||||
**Where it stands.** All four sources, with the whole suite:
|
||||
|
||||
Branch coverage sits far below line coverage because most branches in this file
|
||||
| file | lines | branches | functions |
|
||||
|---|---|---|---|
|
||||
| `src/collections.c` | 99.3% (601/605) | 43.5% | 100% (43/43) |
|
||||
| `src/stdlib.c` | 99.3% (557/561) | 46.5% | 100% (50/50) |
|
||||
| `src/stream.c` | 100% (274/274) | 43.4% | 100% (31/31) |
|
||||
| `src/string.c` | 100% (211/211) | 53.8% | 100% (23/23) |
|
||||
| **total** | **99.5% (1643/1651)** | **46.0%** | **100% (147/147)** |
|
||||
|
||||
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. The libakerror 1.0.0 bump made
|
||||
that gap wider without changing a line of this library: the branch denominator
|
||||
went from 661 to 1087 as those macros grew, so the same tests that scored 51.0%
|
||||
(337/661) dropped to 44.3% (481/1087). The gate moved 45 → 40 to match; line and
|
||||
function coverage did not move at all. Adding the `aksl_version_*` family and its
|
||||
tests brought the figure back to 45.1% (519/1151), but the gate stays at 40 —
|
||||
0.1 points of headroom is not a ratchet.
|
||||
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
|
||||
|
||||
838
TODO.md
838
TODO.md
@@ -1,555 +1,178 @@
|
||||
# TODO
|
||||
|
||||
Working notes for `libakstdlib` — the akerror-wrapped libc surface.
|
||||
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.
|
||||
|
||||
Scope of the current library (`include/akstdlib.h`, `src/stdlib.c`): 16 libc wrappers
|
||||
(`fopen`, `fread`, `fwrite`, `fclose`, `malloc`, `free`, `memset`, `memcpy`, `printf`,
|
||||
`fprintf`, `sprintf`, `atoi`, `atol`, `atoll`, `atof`, `realpath`), one hash helper
|
||||
(`aksl_strhash_djb2`), a doubly-linked list (`append`/`pop`/`iterate`) and a binary tree
|
||||
iterator (`aksl_tree_iterate`).
|
||||
Ordered by blast radius: what blocks other work first, what is merely wrong
|
||||
second, what is missing last.
|
||||
|
||||
Items marked **[CONFIRMED]** were reproduced by building the library and running a probe
|
||||
program against it, not just read off the source.
|
||||
## Where the library stands
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Wrapped | 147 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% (1643/1651) |
|
||||
| Function coverage | 100% (147/147) |
|
||||
| Doxygen | 100%, gated — `cmake --build build --target docs` fails on an undocumented function, parameter or return |
|
||||
|
||||
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. Unit tests that should be written
|
||||
## 1. Blocked on libakerror
|
||||
|
||||
### 1.0 Test-harness prerequisites — **DONE** (branch `feature/test-harness`)
|
||||
These are not fixable from inside this repository. Each one currently costs
|
||||
something here, and the cost is what makes them worth carrying.
|
||||
|
||||
`ctest` is green: 5/5, no *Not Run* entries. Everything in §1.1–1.9 can now be written
|
||||
against `tests/aksl_capture.h` and added to the `AKSL_TESTS` list.
|
||||
### 1.1 The error pool is a process-global array with no locking
|
||||
|
||||
- [x] **The current tests cannot fail.** `tests/test_linkedlist.c` asserted nothing at all;
|
||||
it printed node names and returned `0` unconditionally, so any of the list bugs in
|
||||
§2.1 would pass it. Rewritten as 15 assertion-based cases covering the append, iterate
|
||||
and pop behaviour that is correct today.
|
||||
- [x] **`tests/test_tree.c` was red.** `ctest` reported `tree (Failed)`, exit 136, because
|
||||
`parms.steps` was never reset between the three searches, so the second assertion
|
||||
compared an accumulated `14` against `7`. Each search now builds its own tree and
|
||||
params. The file records that its step counts do not actually distinguish the three
|
||||
traversal orders — real order assertions remain §1.8.
|
||||
- [x] **Adopt libakerror's test conventions.** `tests/aksl_capture.h` provides `AKSL_CHECK()`
|
||||
(an `NDEBUG`-proof assert), `AKSL_CHECK_STATUS()`/`AKSL_CHECK_OK()` which run an
|
||||
akerror-returning call, assert on its status and release the context,
|
||||
`AKSL_CHECK_MSG_CONTAINS()`, a capturing `akerr_log_method`, `aksl_slots_in_use()` for
|
||||
pool-leak checks, and an `AKSL_RUN()` driver that additionally fails any test which
|
||||
leaks an error-pool slot.
|
||||
- [x] **One test source per behaviour, registered with CTest via a list variable.**
|
||||
`tests/test_<name>.c` → executable `test_<name>` → CTest test `<name>`, driven by
|
||||
`AKSL_TESTS` in `CMakeLists.txt`.
|
||||
- [x] **Add a `WILL_FAIL` category.** Two lists: `AKSL_WILL_FAIL_TESTS` for tests that abort
|
||||
by design (empty for now) and `AKSL_KNOWN_FAILING_TESTS` for the confirmed defects in
|
||||
§2.1 — see the note at the head of §2.1.
|
||||
- [x] **Fix the four permanently-failing submodule tests.** `deps/libakerror` is added
|
||||
`EXCLUDE_FROM_ALL`, so akerror's `add_test` entries registered but its test binaries
|
||||
were never built, and `ctest` reported `err_catch`, `err_cleanup`, `err_trace`,
|
||||
`err_improper_closure` as *Not Run* → failed on every run. Neither the pinned commit
|
||||
nor upstream `main` guards that registration, CMake has no way to un-register a test,
|
||||
and `set_tests_properties` cannot reach across directory scopes — so `add_test` and
|
||||
`set_tests_properties` are shadowed for the duration of the `add_subdirectory` call.
|
||||
The dependency has its own CI; this suite now contains only this project's tests.
|
||||
- [x] **Wire in a memory-error build.** `cmake -S . -B build-asan -DAKSL_SANITIZE=ON` adds
|
||||
`-fsanitize=address,undefined -fno-sanitize-recover=all` to the library, the tests and
|
||||
the vendored libakerror. Currently clean, and it is the intended way to test the items
|
||||
in §2 that only misbehave under instrumentation (uninitialised `%s` in
|
||||
`aksl_realpath`, unbounded `vsprintf`, missing `va_end`).
|
||||
- [x] **Port `scripts/mutation_test.py` from libakerror.** Retargeted at `src/stdlib.c` and
|
||||
`include/akstdlib.h` (178 mutants) and exposed as
|
||||
`cmake --build build --target mutation`. CI runs the narrower `src/stdlib.c` set (173
|
||||
mutants) as its own `mutation_test` job with `--threshold 80` and a JUnit report.
|
||||
**Current score: 89.6%** — 155 killed, 18 survived. It was 46.8% (81 killed, 92
|
||||
survived) before §1.2–1.6 were written; the remaining survivors are:
|
||||
**This is what makes the library single-threaded**, and it is the largest open
|
||||
item by some distance.
|
||||
|
||||
| surviving mutants | where | why |
|
||||
|---|---|---|
|
||||
| 5 | deleted statements whose absence nothing observes (`free(ptr)`, `obj->next = NULL`, three `SUCCEED_RETURN`s) | needs an assertion on the side effect, not on the status |
|
||||
| 4 | `aksl_list_append`'s cycle/tail walk | the function is broken here (§2.1.1); its known-failing test cannot pin the internals yet |
|
||||
| 4 | `lalloc`/`lfree` defaulting in `aksl_tree_iterate` | dead parameters (§2.2.8) — defaulted, then never called |
|
||||
| 3 | `*count == -1` in the `printf` family | `*count` on the error path is itself a contract gap (§1.3) |
|
||||
| 2 | `feof` in `aksl_fwrite`, `break` after the BFS `FAIL_RETURN` | unreachable in practice |
|
||||
`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.
|
||||
|
||||
The threshold is a regression ratchet, not a quality bar; raise it as those survivors
|
||||
become assertions. Each one is a concrete missing test — `mutation-junit.xml` lists
|
||||
them with `file:line` and the exact edit.
|
||||
- [x] **Cap every test with a CTest `TIMEOUT`.** Found while measuring the above: a mutant
|
||||
that deletes `fast = fast->next->next` turns `aksl_list_iterate` into an infinite
|
||||
loop, so `ctest` waited forever, the mutation harness killed `ctest` at its own
|
||||
120 s timeout, and the orphaned test binary kept spinning at 100% CPU while holding
|
||||
the pipe open — the run wedged and had to be killed by hand. `TIMEOUT 30` on every
|
||||
test makes `ctest` reap its own child, so those mutants are now killed in 30 s and
|
||||
leave nothing behind. It also guards the real suite against the same class of bug.
|
||||
- [x] **CI could not have run any of this.** `actions/checkout@v4` was not fetching
|
||||
submodules, so `add_subdirectory(deps/libakerror)` had nothing to descend into and the
|
||||
configure step failed before ever reaching the tests. Added `submodules: recursive`,
|
||||
and swapped `cmake --build build --target test` for `ctest --output-on-failure` so a
|
||||
failure is diagnosable from the job log. The deeper problem — CI installing
|
||||
`libakerror@main` while the build actually compiles the pinned submodule — is §2.3.
|
||||
**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.
|
||||
|
||||
### 1.1 Memory wrappers
|
||||
**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.
|
||||
|
||||
- [x] `aksl_malloc` — happy path returns non-NULL and writes through `dst`.
|
||||
- [x] `aksl_malloc(n, NULL)` → `AKERR_NULLPOINTER`, message content asserted.
|
||||
- [ ] `aksl_malloc(0, &p)` — pin down the contract. Today the result depends on whether the
|
||||
platform's `malloc(0)` returns NULL; if it does, the wrapper reports `errno`, which may
|
||||
be `0`. See §2.2.1.
|
||||
- [ ] `aksl_malloc(SIZE_MAX, &p)` → allocation failure surfaces `ENOMEM`, and `*dst` is left
|
||||
NULL rather than garbage.
|
||||
- [x] `aksl_free(NULL)` → `AKERR_NULLPOINTER` (assert the documented behaviour, since
|
||||
`free(NULL)` is legal C and callers will be surprised).
|
||||
- [x] `aksl_free` happy path, and a malloc→free round trip under ASan.
|
||||
- [x] `aksl_memset` — happy path fills the buffer; `n == 0` is a no-op; `s == NULL` →
|
||||
`AKERR_NULLPOINTER`.
|
||||
- [x] `aksl_memcpy` — happy path copies; `d == NULL` and `s == NULL` each →
|
||||
`AKERR_NULLPOINTER`; `n == 0` with valid pointers succeeds.
|
||||
- [ ] `aksl_memcpy` with overlapping regions — decide and test whether this is rejected
|
||||
(`AKERR_VALUE`) or documented as UB like `memcpy`.
|
||||
### 1.2 `IGNORE()` logs a context and never releases it
|
||||
|
||||
### 1.2 File I/O
|
||||
`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)`.
|
||||
|
||||
Covered by `tests/test_stream.c`.
|
||||
**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.
|
||||
|
||||
- [x] `aksl_fopen` happy path on a temp file; `*fp` is written.
|
||||
- [x] `aksl_fopen("/nonexistent/path", "r", &fp)` → `ENOENT` propagated as the status, with
|
||||
the pathname in the message.
|
||||
- [ ] `aksl_fopen` on a mode-denied path (e.g. `/proc/1/mem`, or a `chmod 000` temp file) →
|
||||
`EACCES`.
|
||||
- [x] `aksl_fopen(path, mode, NULL)` → `AKERR_NULLPOINTER`.
|
||||
- [ ] `aksl_fopen(NULL, "r", &fp)` and `aksl_fopen(path, NULL, &fp)` → currently unchecked,
|
||||
see §2.2.2. Test once the guards exist.
|
||||
- [x] `aksl_fread` full read; short read at EOF → `AKERR_EOF`; read from a write-only stream
|
||||
→ `AKERR_IO`; `fp == NULL` → `AKERR_NULLPOINTER`. `ptr == NULL` is still unchecked and
|
||||
untested (see §2.2.3).
|
||||
- [ ] `aksl_fread` **partial read that is neither EOF nor error** — assert whatever the fixed
|
||||
contract is; today this silently returns success and the caller cannot tell how many
|
||||
members were read.
|
||||
- [x] `aksl_fwrite` happy path; write to a read-only stream → `AKERR_IO`; `fp == NULL` →
|
||||
`AKERR_NULLPOINTER`. The full-device (`/dev/full`) case is still open.
|
||||
- [x] `aksl_fclose` happy path; `NULL` → `AKERR_NULLPOINTER`. Double-close is still
|
||||
undecided, so it is neither caught nor tested.
|
||||
- [ ] `aksl_fclose` on a stream whose buffered flush fails (`/dev/full`) → non-zero `fclose`
|
||||
surfaces `errno`.
|
||||
- [x] Round-trip test: `fopen` → `fwrite` → `fclose` → `fopen` → `fread` → compare bytes.
|
||||
**What closing it would touch.** One `RELEASE_ERROR` in the macro; then delete
|
||||
the workaround here.
|
||||
|
||||
### 1.3 Formatted output
|
||||
### 1.3 libakerror does not namespace its `coverage` target when embedded
|
||||
|
||||
Covered by `tests/test_format.c`.
|
||||
`deps/libakerror/CMakeLists.txt:172` versus `:189` — it namespaces `mutation` and
|
||||
not `coverage`.
|
||||
|
||||
- [x] `aksl_printf` / `aksl_fprintf` / `aksl_sprintf` happy paths, asserting both the byte
|
||||
count written through `count` and the produced text. (`aksl_printf` is checked by
|
||||
pointing `stdout` at a temp file for the duration of the call.)
|
||||
- [x] Each of the three with every pointer argument NULL in turn → `AKERR_NULLPOINTER`.
|
||||
- [x] `aksl_fprintf` to a read-only stream → the `errno` it saw (`EBADF`) with "Short write"
|
||||
in the message. `*count` is still left holding `-1`; the test asserts the status only,
|
||||
so it will not have to change when that is fixed.
|
||||
- [ ] `aksl_sprintf` with a format that overflows the destination — currently unbounded
|
||||
(§2.2.4). Test the `aksl_snprintf` replacement once it exists.
|
||||
- [x] Regression test for the missing `va_end` (§2.1.4) — 512 variadic calls in a loop, which
|
||||
the sanitizer build also runs.
|
||||
- [ ] Format-string/arg mismatch is caught at compile time once
|
||||
`__attribute__((format(printf, ...)))` is added (§2.2.5) — a negative compile test.
|
||||
**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`.
|
||||
|
||||
### 1.4 String → number
|
||||
**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.
|
||||
|
||||
Happy paths and NULL guards are covered by `tests/test_convert.c`; the strict-conversion
|
||||
contract is asserted by `tests/test_convert_strict.c`, which is registered as a known
|
||||
failure until §2.1.5 is fixed.
|
||||
### 1.4 libakerror installs no `akerrorConfigVersion.cmake`
|
||||
|
||||
- [x] `aksl_atoi` / `atol` / `atoll` / `atof` happy paths, including negative values and
|
||||
leading whitespace.
|
||||
- [x] NULL `nptr` and NULL `dest` for each → `AKERR_NULLPOINTER`.
|
||||
- [x] **Non-numeric input** (`"not a number"`) — **[CONFIRMED]** currently returns *success*
|
||||
with `*dest == 0`. `AKERR_VALUE` is asserted by the known-failing test.
|
||||
- [x] **Overflow** (`"99999999999999999999"`) — **[CONFIRMED]** currently returns success with
|
||||
a garbage value (`-1` on this box). `ERANGE` is asserted by the known-failing test.
|
||||
- [x] Empty string, `" "` and `"12abc"` (trailing junk) → `AKERR_VALUE`, in the known-failing
|
||||
test. `"0x10"` and `"inf"`/`"nan"` for `atof` are still open.
|
||||
- [ ] `LONG_MIN`/`LONG_MAX`/`LLONG_MIN`/`LLONG_MAX` boundary strings round-trip exactly.
|
||||
**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.
|
||||
|
||||
### 1.5 `aksl_realpath`
|
||||
|
||||
Covered by `tests/test_path.c`. Every failure case there passes a zeroed `resolved_path`,
|
||||
because the wrapper's own error path formats that buffer with `%s` (§2.1.6).
|
||||
|
||||
- [x] Happy path on an existing file and on a symlink chain; result matches `realpath(3)`.
|
||||
- [x] Non-existent path → `ENOENT`.
|
||||
- [x] A path component that is not a directory → `ENOTDIR`.
|
||||
- [ ] Symlink loop → `ELOOP`.
|
||||
- [x] `path == NULL` → `AKERR_NULLPOINTER`.
|
||||
- [ ] `resolved_path == NULL` — currently unchecked and leaks (§2.1.6). Test once fixed.
|
||||
- [ ] A failure case where `resolved_path` is an *uninitialised* buffer — this is the crash
|
||||
case in §2.1.6; run it under ASan/MSan.
|
||||
|
||||
### 1.6 `aksl_strhash_djb2`
|
||||
|
||||
Covered by `tests/test_strhash.c`.
|
||||
|
||||
- [x] Known-answer vectors: `djb2("")` == 5381; a handful of fixed strings with their
|
||||
pre-computed 32-bit values.
|
||||
- [x] `len == 0` returns 5381 regardless of `str` contents.
|
||||
- [x] NULL `str` and NULL `hashval` → `AKERR_NULLPOINTER`.
|
||||
- [ ] **High-bit bytes** (`"\xff\xfe"`) — pins down the sign-extension bug in §2.2.6; the
|
||||
expected value must be the `unsigned char` one (5868578; the wrapper returns the
|
||||
sign-extended 5859874 today). Every vector in the test file is 7-bit ASCII precisely so
|
||||
that it says nothing about this case either way.
|
||||
- [x] Embedded NUL bytes are hashed (the function is length-driven, not NUL-driven).
|
||||
- [x] Same input → same output across two calls (no hidden state).
|
||||
|
||||
### 1.7 Linked list
|
||||
|
||||
- [ ] **`aksl_list_append` builds a correct chain of N nodes** — **[CONFIRMED BROKEN]**, see
|
||||
§2.1.1. Appending `n1..n4` to `n0` yields the chain `n0 -> n4`; `n1`, `n2`, `n3` are
|
||||
silently dropped. This is the single most important test to add.
|
||||
- [ ] `append` sets `obj->prev` to the real tail and `obj->next` to NULL.
|
||||
- [ ] `append` onto an empty (single, zeroed) node.
|
||||
- [ ] `append` NULL list / NULL obj → `AKERR_NULLPOINTER`.
|
||||
- [ ] `append` onto a list containing a cycle → `AKERR_CIRCULAR_REFERENCE`, for a self-loop,
|
||||
a 2-node cycle, and a cycle that does not include the head.
|
||||
- [ ] `append` a node that is already in the list (aliasing) — define and test the contract.
|
||||
- [ ] **`aksl_list_iterate` visits every node exactly once, starting at the head** —
|
||||
**[CONFIRMED BROKEN]**, see §2.1.2: it starts iterating from the *midpoint* left behind
|
||||
by the cycle detector, so the head is never visited. Assert both the visit count and
|
||||
the visit order.
|
||||
- [ ] `iterate` over a single-node list visits exactly one node.
|
||||
- [ ] `iterate` NULL list / NULL iter → `AKERR_NULLPOINTER`.
|
||||
- [ ] `iterate` over a cyclic list → `AKERR_CIRCULAR_REFERENCE` (self-loop, 2-node,
|
||||
tail-to-middle).
|
||||
- [ ] `iterate` where the callback raises `AKERR_ITERATOR_BREAK` → iteration stops at that
|
||||
node, the wrapper returns success, and the visit count proves the early exit.
|
||||
- [ ] `iterate` where the callback raises some *other* error → that error propagates out
|
||||
unchanged, with the callback's message intact.
|
||||
- [ ] `aksl_list_pop` on a middle node relinks `prev`/`next` correctly and clears the popped
|
||||
node's pointers.
|
||||
- [ ] `pop` on the head node, on the tail node, and on a single-node list.
|
||||
- [ ] `pop(NULL)` → `AKERR_NULLPOINTER`.
|
||||
- [ ] `pop` then `iterate` — the list is still traversable and the popped node is gone.
|
||||
- [ ] Pool-accounting test: after a long sequence of list operations including failures,
|
||||
`akerr_slots_in_use() == 0`.
|
||||
|
||||
### 1.8 Tree
|
||||
|
||||
- [ ] Visit **order** assertions, not just counts, for `DFS_PREORDER`, `DFS_INORDER` and
|
||||
`DFS_POSTORDER` over the 7-node tree — record the visited node pointers into an array
|
||||
and compare against the expected sequence. The current test only counts steps, which
|
||||
cannot distinguish the three orders (all three visit 7 nodes).
|
||||
- [ ] **`AKERR_ITERATOR_BREAK` aborts the whole traversal** — **[CONFIRMED BROKEN]**, see
|
||||
§2.1.3. The pre-order case exists as `tests/test_tree_iterate_break.c`: pre-order
|
||||
visits 0, 1, 3, so breaking at `tree[3]` must stop the walk at **3** visits, and today
|
||||
it runs on to all 7. Add the equivalent for in-order (3, 1, 4, 0, 5, 2, 6 → breaking at
|
||||
`tree[4]` gives 3) and post-order (3, 4, 1, 5, 6, 2, 0 → breaking at `tree[5]` gives 4).
|
||||
- [x] `AKSL_TREE_SEARCH_BFS` and `AKSL_TREE_SEARCH_BFS_RIGHT` → `AKERR_NOT_IMPLEMENTED`
|
||||
today, asserted in `tests/test_tree.c` along with the callback never being called;
|
||||
replace with real order assertions once implemented (§3 / §2.2.9).
|
||||
- [x] `aksl_tree_iterate` NULL `root` / NULL `iter` → `AKERR_NULLPOINTER`, and a callback error
|
||||
that is *not* `AKERR_ITERATOR_BREAK` propagates out to the caller.
|
||||
- [ ] **Unknown `searchmode`** (e.g. `99`) — **[CONFIRMED]** currently returns *success*
|
||||
having visited nothing. Should be `AKERR_VALUE`.
|
||||
- [ ] **`AKSL_TREE_SEARCH_VISIT`** (defined in the header, `5`) — **[CONFIRMED]** falls into
|
||||
the same silent-success hole. Either implement it or reject it.
|
||||
- [ ] `root == NULL` / `iter == NULL` → `AKERR_NULLPOINTER`.
|
||||
- [ ] Single-node tree (no children) visits exactly once, in all three orders.
|
||||
- [ ] Left-only and right-only degenerate chains.
|
||||
- [ ] Callback raising a non-`ITERATOR_BREAK` error propagates out of the recursion with the
|
||||
original status and message.
|
||||
- [ ] Custom `lalloc`/`lfree` are actually invoked — **currently they are stored and never
|
||||
called** (§2.2.8), so this test will fail until BFS lands.
|
||||
- [ ] Deep/degenerate tree (e.g. 100k-node left chain) — documents the recursion-depth limit
|
||||
(§2.2.7).
|
||||
- [ ] A tree containing a cycle (child pointing back at an ancestor) — currently infinite
|
||||
recursion; test for `AKERR_CIRCULAR_REFERENCE` once guarded.
|
||||
|
||||
### 1.9 Cross-cutting
|
||||
|
||||
- [ ] **Error-pool accounting**: for *every* wrapper, a test that drives the failure path
|
||||
`AKERR_MAX_ARRAY_ERROR + 10` times and asserts the pool does not leak
|
||||
(`akerr_slots_in_use() == 0` after each handled error).
|
||||
- [ ] **Stack-trace content**: assert that the file/function/line recorded by each wrapper's
|
||||
`FAIL` points at `src/stdlib.c` and the right function name.
|
||||
- [ ] **`AKERR_NOIGNORE` is effective**: a negative compile test where a wrapper's return is
|
||||
discarded and `-Werror=unused-result` fires.
|
||||
- [ ] **Thread safety**: libakerror's `AKERR_ARRAY_ERROR` is a process-global array with no
|
||||
locking. If `libakstdlib` is meant to be callable from threads, add a concurrent
|
||||
smoke test under TSan — and if it is not, say so in the README.
|
||||
**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. Corner cases in existing functionality that should be resolved
|
||||
## 2. Known gaps in what is already wrapped
|
||||
|
||||
### 2.1 Confirmed defects (reproduced against the built library)
|
||||
### 2.1 A short transfer with neither EOF nor a stream error is untested
|
||||
|
||||
Four of these now have a failing test apiece, registered in `AKSL_KNOWN_FAILING_TESTS`
|
||||
and therefore marked `WILL_FAIL` so the suite stays green while the gap stays visible:
|
||||
`tests/test_list_append_chain.c`, `tests/test_list_iterate_head.c`,
|
||||
`tests/test_tree_iterate_break.c` and `tests/test_convert_strict.c`. When one of these
|
||||
defects is fixed, CTest reports that test as failed with *unexpectedly passed* — that is the
|
||||
cue to move it into `AKSL_TESTS`.
|
||||
`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.
|
||||
|
||||
1. **`aksl_list_append` does not find the tail — it silently truncates the list.**
|
||||
`src/stdlib.c:194`. The function conflates Floyd cycle detection with tail-finding:
|
||||
`tail` is set to `slow` *before* `slow` advances, so it tracks the node *behind the
|
||||
midpoint*, not the tail. Appending `n1`, `n2`, `n3`, `n4` to `n0` produces the chain
|
||||
`n0 -> n4`; `n1`–`n3` are unlinked and lost. The fix is to keep the cycle check but walk
|
||||
a separate cursor to the real tail (`while (tail->next) tail = tail->next;`), or run
|
||||
Floyd first and then walk to the end.
|
||||
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.
|
||||
|
||||
2. **`aksl_list_iterate` skips the first half of the list.** `src/stdlib.c:324`. After the
|
||||
cycle-detection loop, `slow` is left at the list midpoint, and the visiting loop then
|
||||
starts from `slow` instead of from `list`. The head node is never passed to the callback.
|
||||
Fix: iterate from `list`, not from `slow`.
|
||||
**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.
|
||||
|
||||
3. **`AKERR_ITERATOR_BREAK` does not stop a tree traversal.** `src/stdlib.c:251`. The
|
||||
recursive frame in which the callback raises the break handles it in its own
|
||||
`PROCESS`/`HANDLE(e, AKERR_ITERATOR_BREAK)` block and returns *success*; the parent's
|
||||
`PASS` therefore sees no error and continues on to the sibling subtree. Breaking at
|
||||
`tree[3]` in pre-order still visits all 7 nodes. Fix by splitting the recursion: an
|
||||
internal helper that propagates `AKERR_ITERATOR_BREAK` unhandled, and a public entry point
|
||||
that swallows it exactly once at the top. `tests/test_tree.c` currently passes only
|
||||
because the search target is the last node in all three orders.
|
||||
**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.
|
||||
|
||||
4. **`va_end` is never called.** `src/stdlib.c:96`, `:109`, `:122` each call `va_start` with
|
||||
no matching `va_end` on any path — happy or error. This is undefined behaviour per the C
|
||||
standard and leaks register-save state on some ABIs.
|
||||
### 2.2 The string-buffer overflow guard is untestable
|
||||
|
||||
5. **`aksl_atoi`/`atol`/`atoll`/`atof` cannot report a conversion failure.**
|
||||
`src/stdlib.c:135`–`169`. `atoi("not a number")` returns success with `0`;
|
||||
`atoi("99999999999999999999")` returns success with a wrapped value. Since the entire
|
||||
point of this library is turning silent libc failures into error contexts, these should be
|
||||
reimplemented over `strtol`/`strtoll`/`strtod` with `errno = 0` before the call, an
|
||||
`endptr` check for "no digits consumed" and "trailing junk", and a range check —
|
||||
raising `AKERR_VALUE` and `ERANGE` respectively. Keep the `atoi`-compatible names but
|
||||
document the stricter contract, or add `aksl_strtol`-family wrappers alongside.
|
||||
`tests/test_convert_strict.c` asserts that contract and is registered as a known failure.
|
||||
`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.
|
||||
|
||||
6. **`aksl_realpath` mishandles `resolved_path`.** `src/stdlib.c:171`.
|
||||
- `resolved_path` is never NULL-checked. `realpath(path, NULL)` is valid and mallocs a
|
||||
buffer, but the wrapper discards `result`, so the caller gets nothing and the buffer
|
||||
leaks.
|
||||
- On failure the message formats `resolved_path` with `%s` while `realpath` leaves it
|
||||
unspecified — for the normal caller who passed an uninitialised stack buffer, the error
|
||||
path itself reads uninitialised memory and can crash.
|
||||
- There is no way to express the buffer's size, so callers must know to supply `PATH_MAX`
|
||||
bytes. Consider `aksl_realpath(const char *path, char *buf, size_t buflen)`, or an
|
||||
allocating variant that returns the malloc'd pointer through an out-param.
|
||||
**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.
|
||||
|
||||
### 2.2 Latent issues and API contract gaps
|
||||
**What closing it would touch.** Nothing worth doing. Recorded so the coverage
|
||||
listing does not read as an oversight.
|
||||
|
||||
1. **`errno` is used as an error status where it may be stale.** `aksl_malloc` reports
|
||||
`errno` when `malloc` returns NULL, but `malloc(0)` may legitimately return NULL without
|
||||
setting `errno`, producing a `FAIL` with `status == 0` — an error context that every
|
||||
`DETECT`/`CATCH` will read as *success* while still holding a pool slot. Guard every
|
||||
`errno`-sourced status with a fallback (`errno ? errno : AKERR_IO`) and set `errno = 0`
|
||||
before the call being wrapped.
|
||||
### 2.3 `aksl_version_check()` ignores its `patch` argument
|
||||
|
||||
2. **`aksl_fopen` does not validate `pathname` or `mode`.** `fopen(NULL, ...)` is UB. Add
|
||||
`AKERR_NULLPOINTER` guards.
|
||||
`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.
|
||||
|
||||
3. **`aksl_fread`/`aksl_fwrite` lose the transfer count and hide short transfers.**
|
||||
- `ptr` is never NULL-checked in either function.
|
||||
- Neither has an out-param for the number of members actually transferred, so a caller who
|
||||
gets `AKERR_EOF` cannot tell how much data arrived. Add `size_t *nmemb_out`.
|
||||
- If `nmemr != nmemb` but neither `feof` nor `ferror` is set, both functions fall through
|
||||
to `SUCCEED_RETURN` — a short transfer reported as complete success.
|
||||
- `aksl_fwrite`'s error message reads `"Error reading file"` (`src/stdlib.c:83`), and
|
||||
checking `feof()` on a write path is meaningless.
|
||||
**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.
|
||||
|
||||
4. **`aksl_sprintf` wraps `vsprintf`, which is unbounded.** There is no way for a caller to
|
||||
bound the destination. Add `aksl_snprintf`/`aksl_vsnprintf` and consider deprecating the
|
||||
`sprintf` wrapper, or reject it outright — an "error-handling" wrapper around an
|
||||
unbounded write is a sharp edge the library exists to remove.
|
||||
### 2.4 Four uncovered `HANDLE(e, AKERR_ITERATOR_BREAK)` lines
|
||||
|
||||
5. **No `format` attribute on the variadic wrappers.** Adding
|
||||
`__attribute__((format(printf, 2, 3)))` (and the equivalents) restores the compile-time
|
||||
format/argument checking that callers lose by going through the wrapper.
|
||||
|
||||
6. **`aksl_strhash_djb2` sign-extends bytes ≥ 0x80.** `src/stdlib.c:181` iterates a
|
||||
`char *`, which is signed on x86/ARM Linux, so high bytes contribute a sign-extended
|
||||
negative value and the hash differs from the canonical djb2 — and differs across
|
||||
platforms where `char` is unsigned. Iterate a `const unsigned char *`. Also take
|
||||
`const char *` in the signature so callers need not cast away constness.
|
||||
|
||||
7. **`aksl_tree_iterate` recursion is unbounded and cycle-blind.** A deep or degenerate tree
|
||||
overflows the stack, and a child pointer that loops back to an ancestor recurses forever.
|
||||
Add a depth limit (raising `AKERR_OUTOFBOUNDS`) and/or a visited check raising
|
||||
`AKERR_CIRCULAR_REFERENCE`, consistent with what the list functions already do.
|
||||
|
||||
8. **`lalloc`, `lfree` and `queue` are dead parameters.** `lalloc`/`lfree` are defaulted and
|
||||
then never called; `queue` is entirely unused (it is the only `-Wunused-parameter` warning
|
||||
in the file). The doc comment even tells the caller to "pass NULL here", which is a sign
|
||||
the queue belongs in an internal helper rather than the public signature. Either
|
||||
implement BFS so they are used, or drop them from the public API until it is.
|
||||
|
||||
9. **Unknown `searchmode` values return success.** The `switch` in `aksl_tree_iterate` has no
|
||||
`default:`; `searchmode == 99` and the header-defined `AKSL_TREE_SEARCH_VISIT` (5) both
|
||||
fall straight through to `SUCCEED_RETURN` having visited nothing. Add
|
||||
`default: FAIL_RETURN(e, AKERR_VALUE, ...)`.
|
||||
|
||||
10. **`AKSL_TREE_SEARCH_BFS`/`BFS_RIGHT` are unimplemented** (`AKERR_NOT_IMPLEMENTED`), and
|
||||
`AKSL_TREE_SEARCH_VISIT` is declared in the header with a documented meaning but no
|
||||
implementation anywhere.
|
||||
|
||||
11. **`aksl_memset`'s and `aksl_memcpy`'s inner checks are dead code.** `memset` returns `s`
|
||||
and `memcpy` returns `d`, both unconditionally; neither can fail. The
|
||||
`FAIL_ZERO_RETURN(e, memset(...), errno, ...)` and `(memcpy(...) == d)` checks can never
|
||||
fire and only obscure the intent. Also, `memcpy` with overlapping ranges is UB — either
|
||||
document that or dispatch to `memmove`.
|
||||
|
||||
12. **`aksl_list_pop` cannot tell the caller the new head.** Popping the head leaves the
|
||||
caller's head pointer dangling at a now-detached node. Add an out-param for the new head,
|
||||
or a `aksl_list_pop(aksl_ListNode **head, aksl_ListNode *node)` form.
|
||||
|
||||
13. **`aksl_free` does not clear the caller's pointer**, so double-free remains easy. Consider
|
||||
an `aksl_freep(void **ptr)` that frees and NULLs.
|
||||
|
||||
14. **No initialisers for the public structs.** Every caller must remember to `memset` an
|
||||
`aksl_ListNode`/`aksl_TreeNode` to zero before use (both existing tests do). Add
|
||||
`aksl_list_node_init`/`aksl_tree_node_init` or `AKSL_LIST_NODE_INIT` macros.
|
||||
|
||||
15. **`aksl_TreeNode.parent` is declared but never set or read** by any library function.
|
||||
|
||||
16. **Header hygiene**: no `extern "C" { }` guard for C++ consumers; `akstdlib.h` pulls in
|
||||
`stdio.h`/`stdlib.h`/`string.h`/`stdint.h` into every consumer's namespace. The version
|
||||
macros this item also asked for are **done**: `AKSL_VERSION_MAJOR`/`_MINOR`/`_PATCH`/
|
||||
`_STRING`/`_NUMBER`/`_SONAME` are generated into `akstdlib_version.h` from
|
||||
`project(akstdlib VERSION …)`, with `aksl_version()`/`aksl_version_string()`/
|
||||
`aksl_version_soname()` and `AKSL_VERSION_CHECK()` reporting the *loaded* library so a
|
||||
header/`.so` mismatch is nameable. See `tests/test_version.c` and `README.md`.
|
||||
|
||||
17. **Doxygen coverage is 2 functions out of 24.** A `Doxyfile` exists but only
|
||||
`aksl_tree_iterate` and `aksl_list_iterate` have doc comments. Every public function
|
||||
needs `@param`/`@throws`/`@return`, especially the ones whose contract deviates from libc
|
||||
(`aksl_free(NULL)` is an error; `aksl_atoi` will become strict). The four
|
||||
`aksl_version_*` entry points have prose comments in the header but no Doxygen tags.
|
||||
|
||||
### 2.3 Build, CI and repository
|
||||
|
||||
- [ ] **`CMakeLists.txt:4-6` sets `CMAKE_CXX_FLAGS` in a C-only project**, so `-g -ggdb -pg`
|
||||
never reach the C compiler. Use `CMAKE_C_FLAGS` — or better, drop `-pg` from the
|
||||
default build entirely (it is what produces the stray `gmon.out` sitting untracked in
|
||||
the repo root) and let `CMAKE_BUILD_TYPE` control debug info.
|
||||
- [ ] **No warning flags.** Add `-Wall -Wextra` (and ideally `-Werror` in CI). The only
|
||||
current warning is the unused `queue` parameter, so the cost of turning them on is low.
|
||||
- [ ] `src/stdlib.c` triggers **"ISO C99 requires at least one argument for the `...`"** on
|
||||
roughly 20 lines under `-Wpedantic`, because `FAIL_*` is called with a bare message and
|
||||
no varargs. Either always pass an argument, or add a zero-arg-safe form in libakerror.
|
||||
- [x] **The `deps/libakerror` submodule was pinned 22 commits behind `main`** (pinned at
|
||||
`4fad0ce "Add gitea workflow"`). Bumped to `5ff8790`, which is libakerror **1.0.0** —
|
||||
the release that made the status-name table private, moved consumer status codes to a
|
||||
band starting at `AKERR_FIRST_CONSUMER_STATUS` (256), made range ownership enforced,
|
||||
and gave the library an soname. See `deps/libakerror/UPGRADING.md`. The bump also
|
||||
brings the refcount-leak fix, the stack-trace buffer-overflow fix and the
|
||||
format-string fix. Nothing in this library's sources used the removed
|
||||
`AKERR_MAX_ERR_VALUE`, `__AKERR_ERROR_NAMES`, `AKERR_STATUS_RANGE_OK` or
|
||||
`AKERR_STATUS_NAME_OK`, so the source change was confined to the build, the packaging
|
||||
and one compile-time guard. What it did move is recorded in the three items below.
|
||||
- [ ] **libakerror does not namespace its `coverage` target when embedded**, only its
|
||||
`mutation` target (`deps/libakerror/CMakeLists.txt:172` vs `:189`). A
|
||||
`-DAKSL_COVERAGE=ON` top-level build therefore failed to configure at all —
|
||||
*"another target with the same name already exists"* — the moment the dependency
|
||||
gained a coverage target. Worked around in `CMakeLists.txt` by shadowing
|
||||
`add_custom_target` for the duration of the `add_subdirectory()` call and renaming the
|
||||
dependency's to `akerror_coverage`, alongside the existing `add_test` shadow. **Fix
|
||||
upstream and delete the workaround**: libakerror should apply the same
|
||||
`CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR` test to `coverage` that it
|
||||
already applies to `mutation`.
|
||||
- [ ] **Branch coverage of `src/stdlib.c` fell from 51.0% to 44.3% on the 1.0.0 bump**, with
|
||||
no change to this library's sources or tests. Line coverage held at 99.0% (200/202)
|
||||
and function coverage at 100% (21/21); the branch *denominator* went from 661 to 1087
|
||||
because the 1.0.0 `PREPARE_ERROR`/`FAIL_*` macros expand to more branches at every
|
||||
call site. 337/661 became 481/1087 — 144 more branches covered, 426 more counted. The
|
||||
CI branch gate was re-ratcheted 45 → 40 to match. Most of the added branches are
|
||||
unreachable from the way this library calls the macros, so raising the number here
|
||||
would mean testing libakerror's macros rather than this library; that belongs to
|
||||
libakerror's mutation suite, since macros expand at the call site and coverage cannot
|
||||
see them properly from either side. Revisit if the gap ever hides a real regression.
|
||||
- [x] **`project()` declared no `VERSION`**, so `@PROJECT_VERSION@` expanded to nothing, the
|
||||
installed `akstdlib.pc` shipped an empty `Version:` field, and `libakstdlib.so` carried
|
||||
no soname. Now `project(akstdlib VERSION 0.1.0)`, with `SOVERSION` `0.1`
|
||||
(`MAJOR.MINOR` while major is 0, `MAJOR` from 1.0 — the `if()` in `CMakeLists.txt` and
|
||||
the matching `#if` in `tests/test_version.c` encode the rule and are tested against
|
||||
each other), an `akstdlibConfigVersion.cmake` at `SameMinorVersion`, and version macros
|
||||
generated into `akstdlib_version.h`. Verified: `find_package(akstdlib 0.1)` is accepted
|
||||
while `0.2` and `1.0` are refused, and a 0.2.0 build dropped in under the 0.1 filename
|
||||
is caught by `AKSL_VERSION_CHECK()`.
|
||||
- [ ] **libakerror still installs no `akerrorConfigVersion.cmake`**, so
|
||||
`cmake/akstdlib.cmake.in` has to call `find_dependency(akerror)` with no version — a
|
||||
request for one would be refused for want of a version file regardless of what is
|
||||
installed. libakstdlib now does this correctly and libakerror does not; fix it there
|
||||
with the same `write_basic_package_version_file()` call, then add the `1.0.0` floor to
|
||||
the `find_dependency` here. Until then the floor rests on `akstdlib.pc`'s `Requires:`
|
||||
and the `#error` guard in `akstdlib.h`.
|
||||
- [ ] **`aksl_version_check()` ignores its `patch` argument** (`src/stdlib.c`, the `(void)patch`),
|
||||
which is correct for the current "same soname" rule but means the parameter exists only
|
||||
so the error message can name the caller's full version. If a future rule needs patch
|
||||
to participate, that is the line to change.
|
||||
- [ ] **CI does not build against the submodule it pins.** `.gitea/workflows/ci.yaml` clones
|
||||
`libakerror@main` and installs it, while the build it then runs is top-level and so
|
||||
compiles `deps/libakerror` at the pinned commit — two different libakerror versions
|
||||
depending on where you build, and the installed one is never actually linked. Pick
|
||||
one. (The submodule is at least *present* in CI now: §1.0 added
|
||||
`submodules: recursive` to the checkout, without which configure failed outright.)
|
||||
- [x] **`ctest` was red** (`tree` failing plus four *Not Run* submodule tests) — fixed in
|
||||
§1.0. The build badge in `README.md` means something again.
|
||||
- [ ] **Untracked build litter** in the repo root: `build/`, `gmon.out`,
|
||||
`CMakeLists.txt-akstdlib`, and a dozen `*~` editor backups. Add a `.gitignore`
|
||||
(libakerror has one; this repo does not).
|
||||
- [ ] **`README.md` is a build badge and nothing else.** It needs at minimum: what the
|
||||
library is, the akerror wrapper contract, the list of wrapped functions, and the
|
||||
deviations from libc semantics.
|
||||
- [ ] **Indentation is inconsistent** — `src/stdlib.c` mixes hard tabs and 4-space indents
|
||||
within the same functions. libakerror standardised on Stroustrup style
|
||||
(commit `e5f7616`); apply the same here, ideally with a checked-in `.clang-format`.
|
||||
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. libc functions not yet wrapped
|
||||
## 3. Deliberate omissions
|
||||
|
||||
Ordered roughly by how much a caller of this library would miss them. Each entry means "add
|
||||
an `aksl_`-prefixed wrapper that turns the documented failure modes into an
|
||||
`akerr_ErrorContext`".
|
||||
Recorded so nobody adds them thinking they were forgotten. Each is a decision,
|
||||
and each can be revisited with an argument.
|
||||
|
||||
### 3.1 High priority — gaps in areas the library already covers
|
||||
| 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. |
|
||||
|
||||
**Memory (`stdlib.h`, `string.h`)**
|
||||
- [ ] `calloc`, `realloc`, `reallocarray` — `realloc`'s "returns NULL and the old pointer is
|
||||
still valid" trap is exactly what this library should be hiding.
|
||||
- [ ] `aligned_alloc`, `posix_memalign`
|
||||
- [ ] `memmove`, `memcmp`, `memchr`
|
||||
---
|
||||
|
||||
**Bounded string formatting (`stdio.h`)**
|
||||
- [ ] `snprintf`, `vsnprintf` — needed to make §2.2.4 fixable.
|
||||
- [ ] `vprintf`, `vfprintf`, `vsprintf` — the `va_list` forms, so consumers can build their
|
||||
own variadic wrappers on top.
|
||||
- [ ] `asprintf` / `vasprintf` (GNU) as an allocating alternative.
|
||||
## 4. Not yet wrapped
|
||||
|
||||
**String → number (`stdlib.h`)**
|
||||
- [ ] `strtol`, `strtoll`, `strtoul`, `strtoull`, `strtod`, `strtof`, `strtold` — the correct
|
||||
foundation for §2.1.5.
|
||||
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.
|
||||
|
||||
**Strings (`string.h`)**
|
||||
- [ ] `strlen`, `strnlen`
|
||||
- [ ] `strcpy`, `strncpy`, `strcat`, `strncat` — with truncation reported as an error rather
|
||||
than silently accepted, which is the whole value proposition here.
|
||||
- [ ] `strdup`, `strndup`
|
||||
- [ ] `strcmp`, `strncmp`, `strcasecmp`, `strncasecmp`, `strcoll`
|
||||
- [ ] `strchr`, `strrchr`, `strstr`, `strcasestr`, `strpbrk`, `strspn`, `strcspn`
|
||||
- [ ] `strtok_r`, `strsep`
|
||||
- [ ] `strerror_r`
|
||||
### 4.1 POSIX file and process API
|
||||
|
||||
**Stream I/O (`stdio.h`)**
|
||||
- [ ] `fseek`, `ftell`, `rewind`, `fgetpos`, `fsetpos`, `fseeko`, `ftello`
|
||||
- [ ] `fflush`
|
||||
- [ ] `fgets`, `fputs`, `fgetc`/`getc`/`getchar`, `fputc`/`putc`/`putchar`, `ungetc`
|
||||
- [ ] `getline`, `getdelim`
|
||||
- [ ] `freopen`, `fdopen`, `fileno`
|
||||
- [ ] `setvbuf`, `setbuf`
|
||||
- [ ] `clearerr`, `feof`, `ferror` — thin, but worth exposing so callers never touch raw
|
||||
`FILE *` internals.
|
||||
- [ ] `sscanf`, `fscanf`, `scanf` — the return-value semantics (items matched vs `EOF`) are a
|
||||
classic silent-failure source.
|
||||
- [ ] `remove`, `rename`, `tmpfile`, `mkstemp`, `mkdtemp`
|
||||
- [ ] `perror` — or an akerror-native equivalent.
|
||||
|
||||
### 3.2 POSIX file and process API (likely the next major surface)
|
||||
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`
|
||||
@@ -563,153 +186,144 @@ an `aksl_`-prefixed wrapper that turns the documented failure modes into an
|
||||
- [ ] `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`, `readdir_r`, `closedir`, `rewinddir`, `scandir`
|
||||
- [ ] `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`, `execve` / `execvp` / `execl` family, `waitpid`, `wait`
|
||||
- [ ] `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 hook on shutdown.
|
||||
- [ ] `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`
|
||||
|
||||
### 3.3 Time
|
||||
### 4.2 Time
|
||||
|
||||
- [ ] `time`, `clock_gettime`, `clock_getres`, `gettimeofday`
|
||||
- [ ] `localtime_r`, `gmtime_r`, `mktime`, `timegm`, `difftime`
|
||||
- [ ] `strftime`, `strptime`
|
||||
- [ ] `clock`, `times`
|
||||
|
||||
### 3.4 Sorting, searching and misc `stdlib.h`
|
||||
`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.
|
||||
|
||||
- [ ] `qsort`, `qsort_r`, `bsearch` — comparator errors currently have nowhere to go; an
|
||||
akerror-aware comparator signature would be a genuine improvement.
|
||||
- [ ] `abs`, `labs`, `llabs`, `div`, `ldiv`, `lldiv`
|
||||
### 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`
|
||||
|
||||
### 3.5 Lower priority / larger projects
|
||||
### 4.4 Larger surfaces
|
||||
|
||||
- [ ] **Sockets**: `socket`, `bind`, `listen`, `accept`, `connect`, `send`/`sendto`/`sendmsg`,
|
||||
`recv`/`recvfrom`/`recvmsg`, `shutdown`, `setsockopt`/`getsockopt`, `getaddrinfo`/
|
||||
`freeaddrinfo`/`gai_strerror`, `inet_ntop`/`inet_pton`
|
||||
- [ ] **Multiplexing**: `select`, `poll`, `ppoll`, `epoll_create1`/`epoll_ctl`/`epoll_wait`
|
||||
- [ ] **Signals**: `sigaction`, `sigprocmask`, `sigemptyset`/`sigaddset`, `kill`, `raise`,
|
||||
`signalfd`
|
||||
- [ ] **Threads**: `pthread_create`/`join`/`detach`, `pthread_mutex_*`, `pthread_cond_*`,
|
||||
`pthread_rwlock_*`, `sem_*` — blocked on resolving the thread-safety question in §1.9
|
||||
(libakerror's error pool is an unlocked process-global array).
|
||||
- [ ] **Dynamic loading**: `dlopen`, `dlsym`, `dlclose`, `dlerror`
|
||||
- [ ] **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`/raise FP exceptions (`sqrt`, `log`,
|
||||
`pow`, `acos`, …) — probably better served by a dedicated `libakmath`.
|
||||
- [ ] **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.
|
||||
|
||||
### 3.6 Non-libc additions the current data structures imply
|
||||
### 4.5 Data structures
|
||||
|
||||
Not libc wrappers, but the existing list/tree API is visibly incomplete:
|
||||
The list and tree API is complete against what §3.6 asked for. What a second
|
||||
consumer might want next:
|
||||
|
||||
- [ ] List: `prepend`, `insert_after`/`insert_before`, `length`, `find`, `reverse`,
|
||||
`concat`, `free_all`, reverse iteration, and a head/tail-tracking container type so
|
||||
`append` is O(1) instead of O(n).
|
||||
- [ ] Tree: `insert`, `remove`, `find`, `height`, `count`, `free_all`, and the BFS traversal
|
||||
the `lalloc`/`lfree`/`queue` parameters were designed for.
|
||||
- [ ] Hash map built on `aksl_strhash_djb2`, plus additional hashes (FNV-1a, xxHash) and a
|
||||
NUL-terminated `aksl_strhash_djb2_str` convenience form.
|
||||
- [ ] Growable buffer / string-builder type, to make the `snprintf` and `strcat` wrappers
|
||||
pleasant to use.
|
||||
- [ ] 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.
|
||||
|
||||
## 4. Evidence from the first full consumer
|
||||
---
|
||||
|
||||
`akbasic` (`source.starfort.tech/andrew/akbasic`) is a ~6,300-line C interpreter built on this
|
||||
library and `libakerror`. It is the first consumer to exercise the whole surface rather than a
|
||||
corner of it, so what it *could not* use is worth recording: it prioritizes the wishlist in §3
|
||||
by what a real port actually reaches for, and it confirms which §2 entries have teeth.
|
||||
## 5. Evidence from the first full consumer
|
||||
|
||||
**The headline number.** Across `src/`, akbasic makes **10 calls into this library** and
|
||||
**116 calls to raw libc**:
|
||||
`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.
|
||||
|
||||
| `aksl_*` used | Count |
|
||||
|---|---|
|
||||
| `aksl_fopen` / `aksl_fclose` | 6 |
|
||||
| `aksl_fprintf` | 2 |
|
||||
| `aksl_fwrite` | 1 |
|
||||
| `aksl_strhash_djb2` | 1 |
|
||||
**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 instead | Count | Wanted from §3 |
|
||||
| Raw libc it had to use | Count | Now available as |
|
||||
|---|---|---|
|
||||
| `snprintf` | 28 | §3.1 bounded string formatting |
|
||||
| `strlen` | 37 | §3.1 strings |
|
||||
| `strcmp` | 16 | §3.1 strings |
|
||||
| `strncpy` | 15 | §3.1 strings |
|
||||
| `memcpy` / `memset` | 16 | wrapped, but see §2.2.11 |
|
||||
| `strtoll` / `strtod` | 2 | §3.1 string → number |
|
||||
| `fgets` | 2 | §3.1 stream I/O |
|
||||
| `strstr` | 1 | §3.1 strings |
|
||||
| `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` |
|
||||
|
||||
A library whose value proposition is "turn silent libc failures into error contexts" is being
|
||||
bypassed 92% of the time by the consumer most committed to it. Every one of those calls is a
|
||||
place where a failure goes unreported.
|
||||
**All four things the port had to write for itself now exist here.**
|
||||
|
||||
### 4.1 What the port had to write for itself
|
||||
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`.
|
||||
|
||||
1. **A strict `strtoll`/`strtod` wrapper** (`akbasic/src/convert.c`, ~60 lines). This is §2.1.5
|
||||
biting, and it is not cosmetic: the Go reference this port reproduces checks `strconv`'s
|
||||
error at four sites and turns each into a BASIC error. Routing them through `aksl_atoi`
|
||||
would have converted four *diagnosable errors* into *wrong answers* — `VAL("garbage")`
|
||||
silently returning `0.0` rather than raising, and a malformed line number becoming line 0.
|
||||
akbasic's own `TODO.md` §1.9 formally bans the `aksl_ato*` family for this reason. **The
|
||||
fix wanted is exactly §3.1's `strtol` family plus the endptr/`errno`/range contract §2.1.5
|
||||
describes**; when it lands, `src/convert.c` gets deleted.
|
||||
**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.
|
||||
|
||||
2. **A fixed-capacity string-keyed hash table** (`akbasic/src/symtab.c`, ~130 lines). §3.6
|
||||
already calls for a "hash map built on `aksl_strhash_djb2`". This is that, three times over:
|
||||
the interpreter needs one for variables, one for functions and one for labels per scope. It
|
||||
is open-addressed with linear probing over a caller-supplied slot count, refuses rather than
|
||||
resizes when full, and is the single most obviously-missing data structure in the library.
|
||||
Worth lifting more or less verbatim if you want a starting point.
|
||||
|
||||
3. **The bounded-copy-with-truncation-as-error idiom, ten times.** `strncpy` followed by an
|
||||
explicit NUL and a preceding length check appears at ten sites across six files. §3.1 lists
|
||||
`strncpy` "with truncation reported as an error rather than silently accepted, which is the
|
||||
whole value proposition here" — agreed, and the repetition is the evidence.
|
||||
|
||||
4. **Uppercase folding for case-insensitive lookup, three times.** Verb and function names are
|
||||
case-insensitive in BASIC while variable names are not, so the port folds case before every
|
||||
keyword lookup: by hand at `src/environment.c:197` and `src/parser_commands.c:113`, and
|
||||
through `<ctype.h>`'s `toupper` in `src/verbs.c`. §3.1's `strcasecmp`/`strncasecmp` would
|
||||
remove all three.
|
||||
|
||||
### 4.2 Confirmed with impact
|
||||
|
||||
- **§2.2.4 (`aksl_sprintf` is unbounded)** — akbasic never calls it, deliberately. Its 28
|
||||
`snprintf` calls all write into fixed buffers whose size is the whole point, and there is no
|
||||
`aksl_snprintf` to route them through. This is the single highest-value item in §3.1 for a
|
||||
consumer of this shape.
|
||||
- **§2.2.2 (`aksl_fopen` does not validate `pathname`/`mode`)** — real, and reached from user
|
||||
input. akbasic's `DLOAD`/`DSAVE` take a filename from a BASIC string expression, so an empty
|
||||
or unevaluated one is reachable from a running program; `akbasic_cmd_dload` validates the
|
||||
name itself before handing it over, with a comment pointing here.
|
||||
- **§2.2.6 (`aksl_strhash_djb2` sign-extends high bytes)** — benign for akbasic, which only
|
||||
ever hashes 7-bit ASCII identifiers, and noted in its §1.3 as a hazard for anyone who later
|
||||
keys a table on a filename or a string literal. Still worth fixing; the wrong answer is
|
||||
platform-dependent, which is the bad kind of benign.
|
||||
- **§2.1.4 (`va_end` is never called)** — akbasic's stdio text sink is built on `aksl_fprintf`
|
||||
and therefore runs this UB on every line of program output. Nothing has misbehaved on
|
||||
x86-64 glibc, which is exactly what makes it worth fixing before something does.
|
||||
|
||||
### 4.3 Not needed, and why
|
||||
|
||||
Recorded so the wishlist does not get prioritized purely by this consumer's shape. akbasic
|
||||
uses **no** allocator (§3.1 memory), **no** lists or trees (§3.6), and **no** `aksl_realpath`:
|
||||
it draws everything from fixed pools by design, and the `ak*` no-dynamic-allocation rule means
|
||||
`calloc`/`realloc` would go unused here even if they existed. A consumer that does allocate
|
||||
would weight §3.1's memory section far higher. The list and tree defects in §2.1.1–3 were
|
||||
avoided rather than survived — akbasic's symbol tables are open-addressed precisely because
|
||||
`aksl_list_append` truncates.
|
||||
**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
|
||||
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")
|
||||
2113
include/akstdlib.h
2113
include/akstdlib.h
File diff suppressed because it is too large
Load Diff
169
src/stdlib.c
169
src/stdlib.c
@@ -116,6 +116,69 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_realloc(void **ptr, size_t size)
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/*
|
||||
* reallocarray(3): a resize whose element count and element size are multiplied
|
||||
* *with an overflow check*. `realloc(p, n * size)` is a heap overflow waiting for
|
||||
* an n large enough to wrap, and the multiplication is exactly the place a
|
||||
* caller does not think to look. Not every platform has it, so the check is done
|
||||
* here rather than delegated.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_reallocarray(void **ptr, size_t nmemb, size_t size)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, ptr, AKERR_NULLPOINTER, "ptr=%p", (void *)ptr);
|
||||
FAIL_ZERO_RETURN(e, nmemb, AKERR_VALUE, "zero-size realloc (nmemb=0)");
|
||||
FAIL_ZERO_RETURN(e, size, AKERR_VALUE, "zero-size realloc (size=0)");
|
||||
FAIL_NONZERO_RETURN(e, (nmemb > (size_t)-1 / size), AKERR_OUTOFBOUNDS,
|
||||
"%zu x %zu overflows size_t", nmemb, size);
|
||||
return aksl_realloc(ptr, nmemb * size);
|
||||
}
|
||||
|
||||
/*
|
||||
* Aligned allocation. aligned_alloc(3) requires size to be a multiple of the
|
||||
* alignment and the alignment to be a power of two supported by the
|
||||
* implementation; violating either is undefined behaviour that usually just
|
||||
* returns NULL, so both are checked here and reported as what they are.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_aligned_alloc(size_t alignment, size_t size, void **dst)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p", (void *)dst);
|
||||
*dst = NULL;
|
||||
FAIL_ZERO_RETURN(e, alignment, AKERR_VALUE, "alignment=0");
|
||||
FAIL_ZERO_RETURN(e, size, AKERR_VALUE, "zero-size allocation");
|
||||
FAIL_NONZERO_RETURN(e, ((alignment & (alignment - 1)) != 0), AKERR_VALUE,
|
||||
"alignment %zu is not a power of two", alignment);
|
||||
FAIL_NONZERO_RETURN(e, (size % alignment != 0), AKERR_VALUE,
|
||||
"size %zu is not a multiple of alignment %zu", size, alignment);
|
||||
errno = 0;
|
||||
*dst = aligned_alloc(alignment, size);
|
||||
FAIL_ZERO_RETURN(e, *dst, AKSL_ERRNO_OR(ENOMEM),
|
||||
"%zu bytes aligned to %zu", size, alignment);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/*
|
||||
* posix_memalign(3) is the older interface, and the one that does not require
|
||||
* size to be a multiple of the alignment. It also breaks the errno convention --
|
||||
* it *returns* the error number and leaves errno alone -- which is precisely the
|
||||
* kind of local irregularity a caller mis-handles once and never notices.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_posix_memalign(void **dst, size_t alignment, size_t size)
|
||||
{
|
||||
int failed = 0;
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p", (void *)dst);
|
||||
*dst = NULL;
|
||||
FAIL_ZERO_RETURN(e, size, AKERR_VALUE, "zero-size allocation");
|
||||
failed = posix_memalign(dst, alignment, size);
|
||||
if ( failed != 0 ) {
|
||||
*dst = NULL;
|
||||
FAIL_RETURN(e, failed, "%zu bytes aligned to %zu", size, alignment);
|
||||
}
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/*
|
||||
* free(NULL) is legal C and does nothing. This treats it as an error anyway,
|
||||
* deliberately and against libc: in a codebase that routes every allocation
|
||||
@@ -454,6 +517,68 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_snprintf(int *count, char *restrict str,
|
||||
return raised;
|
||||
}
|
||||
|
||||
/*
|
||||
* The allocating form: no buffer, no size, no truncation to worry about. Where
|
||||
* aksl_snprintf is right for a fixed destination, this is right when the length
|
||||
* is not knowable in advance and the result is short-lived enough not to want a
|
||||
* whole aksl_StrBuf.
|
||||
*
|
||||
* asprintf(3) is a GNU extension, so it is built here on the two-pass vsnprintf
|
||||
* measure-then-write rather than called, which keeps this off _GNU_SOURCE. *dest
|
||||
* is the caller's to release with aksl_free.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_vasprintf(int *count, char **dest, const char *restrict format, va_list args)
|
||||
{
|
||||
va_list measure;
|
||||
int needed = 0;
|
||||
char *buf = NULL;
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, count, AKERR_NULLPOINTER, "count=%p, dest=%p, format=%p",
|
||||
(void *)count, (void *)dest, (void *)format);
|
||||
*count = 0;
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "count=%p, dest=%p, format=%p",
|
||||
(void *)count, (void *)dest, (void *)format);
|
||||
*dest = NULL;
|
||||
FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "count=%p, dest=%p, format=%p",
|
||||
(void *)count, (void *)dest, (void *)format);
|
||||
va_copy(measure, args);
|
||||
needed = vsnprintf(NULL, 0, format, measure);
|
||||
va_end(measure);
|
||||
FAIL_NONZERO_RETURN(e, (needed < 0), AKSL_ERRNO_OR(AKERR_IO), "could not format the arguments");
|
||||
ATTEMPT {
|
||||
CATCH(e, aksl_malloc((size_t)needed + 1, (void **)&buf));
|
||||
/*
|
||||
* Cannot truncate -- the buffer was sized from this same format and
|
||||
* arguments -- but the return is checked anyway, because "cannot happen"
|
||||
* is a claim about the line above rather than about vsnprintf.
|
||||
*/
|
||||
FAIL_NONZERO_BREAK(e, (vsnprintf(buf, (size_t)needed + 1, format, args) != needed),
|
||||
AKERR_IO, "formatted output changed length between passes");
|
||||
*dest = buf;
|
||||
*count = needed;
|
||||
} CLEANUP {
|
||||
if ( *dest == NULL && buf != NULL ) {
|
||||
akerr_ErrorContext *released = aksl_free(buf);
|
||||
if ( released != NULL ) {
|
||||
released = akerr_release_error(released);
|
||||
}
|
||||
}
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_asprintf(int *count, char **dest, const char *restrict format, ...)
|
||||
{
|
||||
va_list args;
|
||||
akerr_ErrorContext *raised = NULL;
|
||||
|
||||
va_start(args, format);
|
||||
raised = aksl_vasprintf(count, dest, format, args);
|
||||
va_end(args);
|
||||
return raised;
|
||||
}
|
||||
|
||||
/*
|
||||
* String to number.
|
||||
*
|
||||
@@ -893,6 +1018,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_node_init(aksl_TreeNode *node, void
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/** @cond INTERNAL */
|
||||
/*
|
||||
* One entry in the breadth-first traversal queue. Internal, and deliberately not
|
||||
* aksl_ListNode: the walk needs to carry each node's depth alongside it, which is
|
||||
@@ -905,6 +1031,7 @@ typedef struct TreeQueueEntry {
|
||||
aksl_TreeNode *node;
|
||||
int depth;
|
||||
} TreeQueueEntry;
|
||||
/** @endcond */
|
||||
|
||||
/* Append one tree node to the tail of the traversal queue. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *tree_queue_push(
|
||||
@@ -1101,31 +1228,10 @@ static akerr_ErrorContext AKERR_NOIGNORE *tree_dfs_walk(
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Iterate a binary tree in breadth-first or depth-first order, calling a callback on each node.
|
||||
*
|
||||
* The callback may stop the walk early by raising AKERR_ITERATOR_BREAK; this
|
||||
* function absorbs that one status and returns success, so an early exit is not
|
||||
* an error to the caller. Any other status the callback raises propagates out
|
||||
* unchanged, with its message and stack trace intact.
|
||||
*
|
||||
* Traversal is bounded. A tree deeper than AKSL_TREE_MAX_DEPTH raises
|
||||
* AKERR_OUTOFBOUNDS rather than overflowing the stack, and a depth-first walk
|
||||
* over a tree whose child points back at one of its own ancestors raises
|
||||
* AKERR_CIRCULAR_REFERENCE rather than recursing forever.
|
||||
*
|
||||
* @param[in] root The root of the tree to walk
|
||||
* @param[in] iter An aksl_TreeNodeIterator called once for each node visited
|
||||
* @param[in] lalloc An aksl_AllocFunc used to allocate the internal traversal queue, or NULL for aksl_malloc. Breadth-first modes only; the depth-first modes allocate nothing.
|
||||
* @param[in] lfree An aksl_FreeFunc used to release what lalloc returned, or NULL for aksl_free
|
||||
* @param[in] searchmode One of the AKSL_TREE_SEARCH_* defines
|
||||
* @param[in] data Caller data passed through to every invocation of iter
|
||||
*
|
||||
* @throws AKERR_NULLPOINTER On NULL root or iter
|
||||
* @throws AKERR_VALUE On an unrecognised searchmode
|
||||
* @throws AKERR_OUTOFBOUNDS When the tree is deeper than AKSL_TREE_MAX_DEPTH
|
||||
* @throws AKERR_CIRCULAR_REFERENCE When a depth-first walk reaches a node that is its own ancestor
|
||||
* @return akerr_ErrorContext
|
||||
/*
|
||||
* Public entry point for every tree walk. The contract -- every status, every
|
||||
* argument -- is documented on the declaration in include/akstdlib.h; what is
|
||||
* here is why the code looks the way it does.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate(
|
||||
aksl_TreeNode *root,
|
||||
@@ -1198,17 +1304,8 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate(
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Iterates over a linked list and execute a callback function on each node
|
||||
*
|
||||
* @param[in] list The linked list to iterate
|
||||
* @param[in] iter An aksl_ListNodeIterator function which will be called for each node found
|
||||
* @param[in] data Any user data that should be provided when the iterator is called
|
||||
*
|
||||
* @throws AKERR_NULLPOINTER on null pointer inputs
|
||||
* @throws AKERR_CIRCULAR_REFERENCE when the linked list contains a circular reference
|
||||
*
|
||||
* @return akerr_ErrorContext
|
||||
/*
|
||||
* Contract documented on the declaration in include/akstdlib.h.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate(aksl_ListNode *list, aksl_ListNodeIterator iter, void *data)
|
||||
{
|
||||
|
||||
19
src/stream.c
19
src/stream.c
@@ -485,6 +485,25 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fscanf(FILE *stream, const char *format,
|
||||
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 */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
@@ -170,6 +170,15 @@ 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
|
||||
@@ -184,6 +193,8 @@ static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
|
||||
|
||||
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;
|
||||
@@ -191,6 +202,8 @@ static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
|
||||
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.
|
||||
@@ -260,6 +273,8 @@ static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
|
||||
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) \
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -149,6 +149,29 @@ static int test_insert_after_and_before(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Inserting before a node that is *not* the head, which relinks the node in
|
||||
* front of it as well -- a different path from inserting before the head, where
|
||||
* there is no such node.
|
||||
*/
|
||||
static int test_insert_before_a_middle_node(void)
|
||||
{
|
||||
aksl_ListNode node[3];
|
||||
aksl_ListNode fresh;
|
||||
aksl_ListNode *head = &node[0];
|
||||
|
||||
build_chain(node, 3);
|
||||
memset((void *)&fresh, 0x00, sizeof(fresh));
|
||||
|
||||
AKSL_CHECK_OK(aksl_list_insert_before(&head, &node[2], &fresh));
|
||||
AKSL_CHECK(head == &node[0]); /* unchanged, unlike the head case */
|
||||
AKSL_CHECK(node[1].next == &fresh);
|
||||
AKSL_CHECK(fresh.prev == &node[1]);
|
||||
AKSL_CHECK(fresh.next == &node[2]);
|
||||
AKSL_CHECK(node[2].prev == &fresh);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Inspection */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
@@ -177,6 +200,7 @@ static int test_find_returns_the_first_match_or_null(void)
|
||||
{
|
||||
aksl_ListNode node[N];
|
||||
aksl_ListNode *found = (aksl_ListNode *)0x1;
|
||||
aksl_ListNode *cyclic = NULL;
|
||||
int payload = 42;
|
||||
int absent = 0;
|
||||
|
||||
@@ -199,6 +223,19 @@ static int test_find_returns_the_first_match_or_null(void)
|
||||
aksl_list_find(&node[0], &failing_predicate, NULL, &found),
|
||||
AKERR_VALUE, "predicate refused");
|
||||
|
||||
/*
|
||||
* A cyclic list is refused before the walk starts rather than searched
|
||||
* forever. Every whole-list function shares one bounded tail walk, so this
|
||||
* covers the bound for find, reverse, concat and free_all alike.
|
||||
*/
|
||||
node[N - 1].next = &node[0];
|
||||
cyclic = &node[0];
|
||||
AKSL_CHECK_STATUS(aksl_list_find(&node[0], &match_data, &payload, &found),
|
||||
AKERR_CIRCULAR_REFERENCE);
|
||||
AKSL_CHECK_STATUS(aksl_list_reverse(&cyclic), AKERR_CIRCULAR_REFERENCE);
|
||||
AKSL_CHECK_STATUS(aksl_list_concat(&node[0], &node[2]), AKERR_CIRCULAR_REFERENCE);
|
||||
node[N - 1].next = NULL;
|
||||
|
||||
AKSL_CHECK_STATUS(aksl_list_find(&node[0], NULL, NULL, &found), AKERR_NULLPOINTER);
|
||||
AKSL_CHECK_STATUS(aksl_list_find(&node[0], &match_data, NULL, NULL), AKERR_NULLPOINTER);
|
||||
return 0;
|
||||
@@ -336,6 +373,257 @@ static int test_free_all_releases_every_node(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* A free function that fails on one node in the middle.
|
||||
*
|
||||
* Both *_free_all functions go out of their way to keep the first error and
|
||||
* carry on rather than returning immediately, because abandoning the walk would
|
||||
* leak everything after the node that failed -- turning one bad free into a leak
|
||||
* of the whole remaining structure. This is the test that says so.
|
||||
*/
|
||||
/* Defined with the tree tests below; declared here because the tree free-all
|
||||
* case belongs beside the list one rather than beside its comparator. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *compare_ints(void *a, void *b, int *dest);
|
||||
|
||||
static int failing_free_countdown = 0;
|
||||
static int failing_free_calls = 0;
|
||||
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *free_that_fails_once(void *ptr)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
failing_free_calls += 1;
|
||||
if ( failing_free_calls == failing_free_countdown ) {
|
||||
/* Refuse, but still release the memory: the point is the error path,
|
||||
* not a deliberate leak for the sanitizer to find. */
|
||||
free(ptr);
|
||||
FAIL_RETURN(e, AKERR_VALUE, "free refused on call %d", failing_free_calls);
|
||||
}
|
||||
free(ptr);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/*
|
||||
* And one that refuses every time, so more than one error is raised during a
|
||||
* single walk. Only the first is kept and handed back; the rest have to be
|
||||
* released, or a walk over n nodes with a broken free would consume n pool slots
|
||||
* and exhaust the pool -- the failure mode the whole pool-accounting section of
|
||||
* TODO.md 1.9 exists to catch.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *free_that_always_fails(void *ptr)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
failing_free_calls += 1;
|
||||
free(ptr);
|
||||
FAIL_RETURN(e, AKERR_VALUE, "free refused on call %d", failing_free_calls);
|
||||
}
|
||||
|
||||
/*
|
||||
* And one that refuses from a given call onwards, which is what it takes to fail
|
||||
* repeatedly *inside* a drain: the dequeues that come first have to succeed, or
|
||||
* the walk stops with a real error before it ever reaches the break.
|
||||
*/
|
||||
static int failing_free_from = 0;
|
||||
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *free_that_fails_from(void *ptr)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
failing_free_calls += 1;
|
||||
free(ptr);
|
||||
if ( failing_free_calls >= failing_free_from ) {
|
||||
FAIL_RETURN(e, AKERR_VALUE, "free refused on call %d", failing_free_calls);
|
||||
}
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
static int test_free_all_releases_the_errors_it_does_not_return(void)
|
||||
{
|
||||
aksl_ListNode *head = NULL;
|
||||
aksl_ListNode *node = NULL;
|
||||
aksl_TreeNode *root = NULL;
|
||||
aksl_TreeNode *tnode = NULL;
|
||||
static int values[N] = { 5, 3, 8, 1, 9 };
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < N; i++ ) {
|
||||
AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_ListNode), (void **)&node));
|
||||
AKSL_CHECK_OK(aksl_list_node_init(node, NULL));
|
||||
if ( head == NULL ) {
|
||||
head = node;
|
||||
} else {
|
||||
AKSL_CHECK_OK(aksl_list_append(head, node));
|
||||
}
|
||||
}
|
||||
|
||||
failing_free_calls = 0;
|
||||
/* The first of N errors comes back; the other N-1 must not leak. */
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_list_free_all(&head, &free_that_always_fails),
|
||||
AKERR_VALUE, "free refused on call 1");
|
||||
AKSL_CHECK(failing_free_calls == N);
|
||||
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||
|
||||
for ( i = 0; i < N; i++ ) {
|
||||
AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_TreeNode), (void **)&tnode));
|
||||
AKSL_CHECK_OK(aksl_tree_node_init(tnode, &values[i]));
|
||||
AKSL_CHECK_OK(aksl_tree_insert(&root, tnode, &compare_ints));
|
||||
}
|
||||
|
||||
failing_free_calls = 0;
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_tree_free_all(&root, &free_that_always_fails),
|
||||
AKERR_VALUE, "free refused on call 1");
|
||||
AKSL_CHECK(failing_free_calls == N);
|
||||
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_list_free_all_reports_the_first_failure_but_frees_everything(void)
|
||||
{
|
||||
aksl_ListNode *head = NULL;
|
||||
aksl_ListNode *node = NULL;
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < N; i++ ) {
|
||||
AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_ListNode), (void **)&node));
|
||||
AKSL_CHECK_OK(aksl_list_node_init(node, NULL));
|
||||
if ( head == NULL ) {
|
||||
head = node;
|
||||
} else {
|
||||
AKSL_CHECK_OK(aksl_list_append(head, node));
|
||||
}
|
||||
}
|
||||
|
||||
failing_free_calls = 0;
|
||||
failing_free_countdown = 2; /* fail on the second of five */
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_list_free_all(&head, &free_that_fails_once),
|
||||
AKERR_VALUE, "free refused on call 2");
|
||||
/* Every node was still visited: the walk did not stop at the failure. */
|
||||
AKSL_CHECK(failing_free_calls == N);
|
||||
AKSL_CHECK(head == NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_tree_free_all_reports_the_first_failure_but_frees_everything(void)
|
||||
{
|
||||
static int values[5] = { 5, 3, 8, 1, 9 };
|
||||
aksl_TreeNode *root = NULL;
|
||||
aksl_TreeNode *node = NULL;
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < 5; i++ ) {
|
||||
AKSL_CHECK_OK(aksl_malloc(sizeof(aksl_TreeNode), (void **)&node));
|
||||
AKSL_CHECK_OK(aksl_tree_node_init(node, &values[i]));
|
||||
AKSL_CHECK_OK(aksl_tree_insert(&root, node, &compare_ints));
|
||||
}
|
||||
|
||||
failing_free_calls = 0;
|
||||
failing_free_countdown = 2;
|
||||
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_tree_free_all(&root, &free_that_fails_once),
|
||||
AKERR_VALUE, "free refused on call 2");
|
||||
AKSL_CHECK(failing_free_calls == 5);
|
||||
AKSL_CHECK(root == NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* The same idea for the breadth-first traversal queue: a failing lfree during
|
||||
* the drain must not stop the drain, and must not mask the error that got the
|
||||
* walk there in the first place. The traversal itself still succeeds -- the
|
||||
* drain failure is logged and dropped, because losing the caller's real error to
|
||||
* report a bookkeeping one would be the worse trade.
|
||||
*/
|
||||
static aksl_TreeNode *break_on_node = NULL;
|
||||
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *break_at(aksl_TreeNode *node, void *data)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
(void)data;
|
||||
if ( node == break_on_node ) {
|
||||
FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "stop here");
|
||||
}
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *plain_alloc(size_t size, void **dest)
|
||||
{
|
||||
return aksl_malloc(size, dest);
|
||||
}
|
||||
|
||||
static int test_bfs_queue_drain_survives_a_failing_free(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];
|
||||
|
||||
/*
|
||||
* The lfree calls, in order:
|
||||
* 1 the root's queue entry, released as it is dequeued
|
||||
* 2 the left child's entry, likewise
|
||||
* -- the callback breaks on the left child, leaving the right child queued
|
||||
* 3 the right child's entry, released by the drain in CLEANUP
|
||||
*
|
||||
* Failing on call 3 is therefore a failure inside the drain. The traversal
|
||||
* still succeeds: the break is not an error, and losing that answer in order
|
||||
* to report a bookkeeping failure would be the worse trade -- so the drain
|
||||
* error is logged and dropped, which is what this asserts.
|
||||
*/
|
||||
break_on_node = &tree[1];
|
||||
failing_free_calls = 0;
|
||||
failing_free_countdown = 3;
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &break_at,
|
||||
&plain_alloc, &free_that_fails_once,
|
||||
AKSL_TREE_SEARCH_BFS, NULL));
|
||||
AKSL_CHECK(failing_free_calls == 3);
|
||||
|
||||
/* And the pool is intact afterwards: the dropped context was released. */
|
||||
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* More than one failure inside a single drain, which is the case where the drain
|
||||
* has to release the errors it is not keeping. A seven-node tree broken on the
|
||||
* third visit leaves two entries queued:
|
||||
*
|
||||
* dequeue 0 (free 1), enqueue 1 and 2
|
||||
* dequeue 1 (free 2), enqueue 3 and 4 queue: 2 3 4
|
||||
* dequeue 2 (free 3), callback breaks queue: 3 4
|
||||
* drain frees 3 and 4 (free 4, free 5)
|
||||
*
|
||||
* With a free that refuses every time, the drain raises twice and must return at
|
||||
* most one context to be dropped -- keeping both would leak a pool slot per
|
||||
* queued node, which over a large tree exhausts the pool outright.
|
||||
*/
|
||||
static int test_bfs_queue_drain_releases_repeated_failures(void)
|
||||
{
|
||||
aksl_TreeNode tree[7];
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < 7; i++ ) {
|
||||
AKSL_CHECK_OK(aksl_tree_node_init(&tree[i], NULL));
|
||||
}
|
||||
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];
|
||||
|
||||
break_on_node = &tree[2];
|
||||
failing_free_calls = 0;
|
||||
failing_free_from = 4; /* the first drain call */
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &break_at,
|
||||
&plain_alloc, &free_that_fails_from,
|
||||
AKSL_TREE_SEARCH_BFS, NULL));
|
||||
/* Three dequeues plus two drained entries. */
|
||||
AKSL_CHECK(failing_free_calls == 5);
|
||||
AKSL_CHECK(aksl_slots_in_use() == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* The tracked container */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
@@ -619,6 +907,99 @@ static int test_tree_remove_all_three_cases(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* The mirror images of the cases above: a node that is its parent's *right*
|
||||
* child, and a node whose only child is on the left. Both take different
|
||||
* branches through tree_replace, and neither is reached by the test above.
|
||||
*/
|
||||
static int test_tree_remove_mirrored_shapes(void)
|
||||
{
|
||||
static int values[4] = { 5, 8, 7, 6 };
|
||||
aksl_TreeNode node[4];
|
||||
aksl_TreeNode *root = NULL;
|
||||
aksl_TreeNode *found = NULL;
|
||||
int missing = 8;
|
||||
size_t count = 0;
|
||||
int i = 0;
|
||||
|
||||
/*
|
||||
* 5 node[0]
|
||||
* \
|
||||
* 8 node[1], a right child
|
||||
* /
|
||||
* 7 node[2], whose only child is on the left
|
||||
* /
|
||||
* 6 node[3]
|
||||
*/
|
||||
for ( i = 0; i < 4; i++ ) {
|
||||
AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &values[i]));
|
||||
AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints));
|
||||
}
|
||||
AKSL_CHECK(node[0].right == &node[1]);
|
||||
AKSL_CHECK(node[1].left == &node[2]);
|
||||
AKSL_CHECK(node[2].left == &node[3]);
|
||||
|
||||
/* A right child with a single left child underneath it. */
|
||||
AKSL_CHECK_OK(aksl_tree_remove(&root, &node[1]));
|
||||
AKSL_CHECK_OK(aksl_tree_count(root, &count));
|
||||
AKSL_CHECK(count == 3);
|
||||
AKSL_CHECK_OK(aksl_tree_find(root, &missing, &compare_ints, &found));
|
||||
AKSL_CHECK(found == NULL);
|
||||
/* 7 took its place as the root's right child. */
|
||||
AKSL_CHECK(node[0].right == &node[2]);
|
||||
AKSL_CHECK(node[2].parent == &node[0]);
|
||||
|
||||
/* And now a node whose only child is on the left. */
|
||||
AKSL_CHECK_OK(aksl_tree_remove(&root, &node[2]));
|
||||
AKSL_CHECK_OK(aksl_tree_count(root, &count));
|
||||
AKSL_CHECK(count == 2);
|
||||
AKSL_CHECK(node[0].right == &node[3]);
|
||||
AKSL_CHECK(node[3].parent == &node[0]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* The two-children case where the successor is not the removed node's own right
|
||||
* child, so the successor has to be lifted out of its position first. The other
|
||||
* removal test happens to hit the adjacent-successor path.
|
||||
*/
|
||||
static int test_tree_remove_with_a_distant_successor(void)
|
||||
{
|
||||
static int values[5] = { 5, 3, 9, 7, 8 };
|
||||
aksl_TreeNode node[5];
|
||||
aksl_TreeNode *root = NULL;
|
||||
OrderLog log;
|
||||
int i = 0;
|
||||
|
||||
/*
|
||||
* 5 node[0]
|
||||
* / \
|
||||
* 3 9 node[1], node[2]
|
||||
* /
|
||||
* 7 node[3] -- the in-order successor of 5, two levels down
|
||||
* \
|
||||
* 8 node[4]
|
||||
*/
|
||||
for ( i = 0; i < 5; i++ ) {
|
||||
AKSL_CHECK_OK(aksl_tree_node_init(&node[i], &values[i]));
|
||||
AKSL_CHECK_OK(aksl_tree_insert(&root, &node[i], &compare_ints));
|
||||
}
|
||||
|
||||
AKSL_CHECK_OK(aksl_tree_remove(&root, &node[0]));
|
||||
/* 7 becomes the root, and its own right child (8) is not lost. */
|
||||
AKSL_CHECK(root == &node[3]);
|
||||
AKSL_CHECK(root->parent == NULL);
|
||||
|
||||
memset((void *)&log, 0x00, sizeof(log));
|
||||
AKSL_CHECK_OK(aksl_tree_iterate(root, &record_leaf, NULL, NULL,
|
||||
AKSL_TREE_SEARCH_DFS_INORDER, &log));
|
||||
AKSL_CHECK(log.count == 4);
|
||||
for ( i = 1; i < log.count; i++ ) {
|
||||
AKSL_CHECK(log.seen[i - 1] < log.seen[i]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Removing the last node empties the tree rather than leaving a dangling root. */
|
||||
static int test_tree_remove_the_only_node(void)
|
||||
{
|
||||
@@ -707,12 +1088,18 @@ int main(void)
|
||||
|
||||
AKSL_RUN(failures, test_prepend_moves_the_head);
|
||||
AKSL_RUN(failures, test_insert_after_and_before);
|
||||
AKSL_RUN(failures, test_insert_before_a_middle_node);
|
||||
AKSL_RUN(failures, test_length_counts_and_refuses_cycles);
|
||||
AKSL_RUN(failures, test_find_returns_the_first_match_or_null);
|
||||
AKSL_RUN(failures, test_reverse_flips_both_directions);
|
||||
AKSL_RUN(failures, test_concat_joins_two_lists);
|
||||
AKSL_RUN(failures, test_iterate_reverse_walks_back_to_the_head);
|
||||
AKSL_RUN(failures, test_free_all_releases_every_node);
|
||||
AKSL_RUN(failures, test_free_all_releases_the_errors_it_does_not_return);
|
||||
AKSL_RUN(failures, test_list_free_all_reports_the_first_failure_but_frees_everything);
|
||||
AKSL_RUN(failures, test_tree_free_all_reports_the_first_failure_but_frees_everything);
|
||||
AKSL_RUN(failures, test_bfs_queue_drain_survives_a_failing_free);
|
||||
AKSL_RUN(failures, test_bfs_queue_drain_releases_repeated_failures);
|
||||
|
||||
AKSL_RUN(failures, test_container_push_and_unshift);
|
||||
AKSL_RUN(failures, test_container_remove_maintains_the_endpoints);
|
||||
@@ -722,6 +1109,8 @@ int main(void)
|
||||
AKSL_RUN(failures, test_tree_insert_sets_the_parent_links);
|
||||
AKSL_RUN(failures, test_tree_find);
|
||||
AKSL_RUN(failures, test_tree_remove_all_three_cases);
|
||||
AKSL_RUN(failures, test_tree_remove_mirrored_shapes);
|
||||
AKSL_RUN(failures, test_tree_remove_with_a_distant_successor);
|
||||
AKSL_RUN(failures, test_tree_remove_the_only_node);
|
||||
AKSL_RUN(failures, test_tree_height_and_count);
|
||||
AKSL_RUN(failures, test_tree_free_all);
|
||||
|
||||
@@ -224,6 +224,51 @@ static int test_printf_rejects_null_arguments(void)
|
||||
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
|
||||
@@ -298,6 +343,7 @@ int main(void)
|
||||
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);
|
||||
|
||||
|
||||
@@ -121,6 +121,84 @@ static int test_realloc_grows_and_preserves_the_old_block_on_failure(void)
|
||||
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),
|
||||
@@ -324,6 +402,9 @@ int main(void)
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -520,6 +520,53 @@ static int test_fscanf_reads_from_a_stream(void)
|
||||
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;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Files */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
@@ -603,6 +650,8 @@ int main(void)
|
||||
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_remove_and_rename);
|
||||
AKSL_RUN(failures, test_mkstemp_and_mkdtemp);
|
||||
|
||||
|
||||
@@ -111,6 +111,53 @@ static int test_no_digits_is_an_error_even_with_an_endptr(void)
|
||||
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)
|
||||
{
|
||||
@@ -277,6 +324,7 @@ int main(void)
|
||||
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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user