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:
2026-07-31 08:00:16 -04:00
parent 98a0a8562d
commit 125eeb2109
22 changed files with 4017 additions and 973 deletions

View File

@@ -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 \