Upgrade to libakerror 1.0.0

Bump deps/libakerror 22 commits to 5ff8790 (1.0.0), which makes the
status-name table private, moves consumer status codes to a band starting
at AKERR_FIRST_CONSUMER_STATUS, enforces range ownership rather than
treating it as advisory, and gives the library an soname. See
deps/libakerror/UPGRADING.md.

src/stdlib.c needed no changes. This library defines no status codes of its
own -- it raises libakerror's AKERR_* codes and propagates errno, both
inside libakerror's reserved 0-255 band -- and it never referenced
AKERR_MAX_ERR_VALUE, __AKERR_ERROR_NAMES, AKERR_STATUS_RANGE_OK or
AKERR_STATUS_NAME_OK. What moved was everything around the code:

A -DAKSL_COVERAGE=ON build stopped configuring at all. libakerror
namespaces its `mutation` target when embedded but not its `coverage`
target, so it collided with ours. Shadow add_custom_target for the duration
of the add_subdirectory() call and rename the dependency's to
akerror_coverage, alongside the existing add_test shadow. Fix upstream and
delete the workaround; recorded in TODO.md.

Pin the 1.0.0 floor three ways, since no single one covers every
consumption path: an #error in akstdlib.h feature-testing
AKERR_FIRST_CONSUMER_STATUS, because libakerror publishes no version macro;
Requires: akerror >= 1.0.0 in akstdlib.pc, which also gets consumers
-lakerror transitively; and find_dependency(akerror) in
akstdlibConfig.cmake. The last was already broken before this bump -- the
template still carried its MyLibraryConfig placeholder with the dependency
commented out, so any external find_package(akstdlib) failed with a bare
"akerror::akerror not found" out of the generated targets file.

Branch coverage of src/stdlib.c fell from 51.0% to 44.3% with no source or
test change: the 1.0.0 PREPARE_ERROR/FAIL_* macros expand to more branches
at every call site, so 337/661 became 481/1087 -- 144 more branches covered,
426 more counted. Line coverage held at 99.0% (200/202) and function
coverage at 100% (21/21). Re-ratchet the CI branch gate 45 -> 40 rather than
chase branches that belong to libakerror's own suite.

tests/test_status_registry.c pins the contract that made the status-code
migration a no-op: libakstdlib reserves no consumer range, so an application
may allocate from AKERR_FIRST_CONSUMER_STATUS without coordinating with it,
and every status this library raises is inside the reserved band with a name
actually registered -- an unnamed one degrades to "Unknown Error" in every
later stack trace, which nothing else would notice. It exercises the new
ownership enforcement too, so the "reserves nothing" assertion cannot pass
vacuously.

ctest 13/13, ASan+UBSan 13/13, coverage 15/15 at 90/40, mutation 89.6%
(155/173, unchanged). Also verified out of tree: the #error fires as the
first diagnostic against a stale akerror.h, pkg-config refuses akerror
0.9.0, and an external find_package(akstdlib) consumer builds and runs
against a temp-prefix install.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 22:20:54 -04:00
parent a37ba3fb89
commit 07c448508b
10 changed files with 393 additions and 18 deletions

View File

@@ -25,6 +25,41 @@ A top-level build compiles the vendored `deps/libakerror`. When `libakstdlib` is
consumed as a subproject, it uses whatever `akerror::akerror` target or installed
package the parent provides instead.
### The libakerror version floor
**libakerror 1.0.0 or newer is required.** That release made the status-name
table private, moved consumer status codes into a band starting at
`AKERR_FIRST_CONSUMER_STATUS` (256), made range ownership enforced rather than
advisory, and gave the library an soname — see
`deps/libakerror/UPGRADING.md`. It is a source *and* ABI break, so pairing this
header with an older `akerror.h` is not a compile problem you can work around;
the pairing is simply invalid.
Three things enforce the floor, because no single one covers every way the
library gets consumed:
| Mechanism | Where | Catches |
| --- | --- | --- |
| `#error` on a missing `AKERR_FIRST_CONSUMER_STATUS` | `include/akstdlib.h` | a stale `akerror.h` earlier on the include path, at the first diagnostic rather than as a pile of errors inside `src/stdlib.c` |
| `Requires: akerror >= 1.0.0` | `akstdlib.pc.in` | a pkg-config consumer, which also now gets `-lakerror` transitively |
| `find_dependency(akerror)` | `cmake/akstdlib.cmake.in` | a `find_package(akstdlib)` consumer, which previously failed with a bare *"akerror::akerror not found"* out of the generated targets file |
The header guard feature-tests rather than version-tests because libakerror
publishes no version macro; `AKERR_FIRST_CONSUMER_STATUS` is the symbol 1.0.0
introduced, so its absence is what "older than 1.0.0" actually looks like. The
CMake path requests no version for the same kind of reason: libakerror installs
no `akerrorConfigVersion.cmake`, so `find_dependency(akerror 1.0.0)` would be
refused for want of a version file no matter which akerror is installed.
**libakstdlib defines no status codes of its own.** It raises libakerror's
`AKERR_*` codes and propagates the host's `errno` values, all of which live in
libakerror's reserved `0``255` band, so it reserves no range and an application
is free to allocate from `AKERR_FIRST_CONSUMER_STATUS` without coordinating with
it. `tests/test_status_registry.c` pins that, along with the requirement that
every status this library raises actually has a name registered — an unnamed one
degrades to `"Unknown Error"` in every later stack trace, which nothing else
would notice.
## Testing
There are four harnesses. The first three take seconds; the fourth takes about
@@ -47,7 +82,9 @@ 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` and `tree`.
(`aksl_realpath`), `strhash`, `memory`, `linkedlist`, `tree`, and
`status_registry` (this library's side of the libakerror status-registry
contract — see "The libakerror version floor" above).
To add a test, drop `tests/test_mything.c` in place and add `mything` to
`AKSL_TESTS` in `CMakeLists.txt`.
@@ -106,6 +143,14 @@ they work under `ctest -j` too:
The reset matters: gcov counters are cumulative, so without it each report would
fold in every earlier run and overstate coverage.
`coverage` here is this project's target. libakerror ships a `coverage` target of
its own and, unlike its `mutation` target, does not namespace it when embedded,
so a top-level `-DAKSL_COVERAGE=ON` build would collide on the name and fail to
configure at all. `CMakeLists.txt` renames the dependency's to `akerror_coverage`
on the way past — it drives its own instrumented build tree, so
`cmake --build build-coverage --target akerror_coverage` still works. The
workaround goes away when libakerror namespaces it upstream; see `TODO.md` §2.3.
CTest hides the output of a passing test, so `coverage_report` also writes
`build-coverage/coverage-summary.txt` (the same text report) and
`build-coverage/coverage.xml` (Cobertura, for CI publishers). The `coverage`
@@ -123,7 +168,7 @@ scripts/coverage.py --build build-coverage # report on disk
scripts/coverage.py --build build-coverage --summary-only # totals only
scripts/coverage.py --build build-coverage --include tests # coverage of the tests themselves
scripts/coverage.py --build build-coverage --run-tests # reset, run ctest, report
scripts/coverage.py --build build-coverage --threshold 90 --branch-threshold 45
scripts/coverage.py --build build-coverage --threshold 90 --branch-threshold 40
```
It needs nothing but Python 3 and gcc's own `gcov` — no lcov, gcovr or genhtml.
@@ -134,12 +179,12 @@ score:
```sh
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON \
-DAKSL_COVERAGE_THRESHOLD=90 -DAKSL_COVERAGE_BRANCH_THRESHOLD=45
-DAKSL_COVERAGE_THRESHOLD=90 -DAKSL_COVERAGE_BRANCH_THRESHOLD=40
```
**Where it stands.** `src/stdlib.c` is at **99.0% of lines (200/202)**, **51.0% of
branches** and **21/21 functions**, so 90/45 above is a ratchet with headroom
rather than a target. The two uncovered lines are both
**Where it stands.** `src/stdlib.c` is at **99.0% of lines (200/202)**, **44.3% of
branches (481/1087)** and **21/21 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
@@ -149,7 +194,11 @@ deliberately rather than pinned by a test.
Branch coverage sits far below line coverage because most branches in this file
are inside the `FAIL_*`/`ATTEMPT`/`FINISH` macro expansions — pool exhaustion,
stack-trace buffer limits, `akerr_valid_error_address` failures — and belong to
libakerror's own suite rather than to this one.
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) now score 44.3% (481/1087). The gate moved 45 → 40 to match; line and
function coverage did not move at all.
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