2026-06-27 10:09:38 -04:00
# README

Document the test harnesses and add a pre-push hook
README.md was a build badge and nothing else. It now covers what the library
is, how to build it, and how to run each of the three harnesses locally: the
ctest suite, the AKSL_SANITIZE ASan/UBSan build, and mutation testing.
The section on reading ctest output matters most: two of the three test lists
are marked WILL_FAIL, so an all-green run 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.
.githooks/pre-push runs the two fast harnesses (default build + ctest, then
sanitizer build + ctest) before a push leaves the machine. It skips when there
is nothing to test (branch deletions, empty pushes), fails the push with the
captured output on any error, and builds under .git/aksl-prepush so it never
disturbs your own build/. The slow mutation gate is opt-in via
AKSL_HOOK_MUTATION=1, and git push --no-verify bypasses everything.
Enable with: git config core.hooksPath .githooks
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 11:51:28 -04:00
`libakstdlib` wraps C standard library functions so that they report failures
through [libakerror ](https://source.starfort.tech/andrew/libakerror )'s
`ATTEMPT { ... } HANDLE { ... }` error contexts instead of through return codes
and `errno` . It also provides a few data structures built on the same
convention (a doubly-linked list and a binary tree).
Every entry point returns `akerr_ErrorContext *` and is marked `AKERR_NOIGNORE` .
See `TODO.md` for the current state of the library: what is covered by tests,
which corner cases are still open, and which libc functions are not yet wrapped.
## Building
```sh
git submodule update --init --recursive # deps/libakerror
cmake -S . -B build
cmake --build build
cmake --install build
```
A top-level build compiles the vendored `deps/libakerror` . When `libakstdlib` is
consumed as a subproject, it uses whatever `akerror::akerror` target or installed
package the parent provides instead.
## Testing
Add code coverage to the CTest suite
New AKSL_COVERAGE option instruments the library and its tests with
--coverage -O0 and wires the report into the suite itself, so a plain
ctest --test-dir build-coverage both runs the tests and produces coverage.
Two CTest entries do the work, held in order by a CTest fixture rather
than by declaration order so they also hold under ctest -j:
coverage_reset (FIXTURES_SETUP) clears the .gcda counters before any test,
since gcov counts are cumulative and would otherwise fold in earlier runs;
coverage_report (FIXTURES_CLEANUP) aggregates gcov output afterwards.
AKSL_COVERAGE_THRESHOLD / AKSL_COVERAGE_BRANCH_THRESHOLD gate the report,
the same regression-ratchet idea as the mutation score. The `coverage`
target builds, runs and prints in one step.
scripts/coverage.py parses gcov's JSON output, aggregates line, branch and
function counts across translation units, and lists every uncovered line
and never-called function -- the actionable half, as with surviving
mutants. Python stdlib plus gcc's own gcov only: no lcov, gcovr or
genhtml. It also writes coverage-summary.txt (CTest hides the output of a
passing test) and a Cobertura coverage.xml for CI publishers.
Instrumentation is per target, so deps/libakerror stays out of the report.
The mutation harness now ignores build*/ and gcov artifacts when copying
the tree, so a coverage build does not slow it down.
Baseline on src/stdlib.c: 52.0% of lines, 23.6% of branches, 8 of 21
functions. The uncovered functions are the untested wrappers the mutation
survivors already point at (printf, ato*, stream, realpath, strhash).
Verified:
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON
cmake --build build-coverage --target coverage # 8/8, report printed
ctest --test-dir build-coverage -j8 # fixture order holds
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON -DAKSL_COVERAGE_THRESHOLD=60
ctest --test-dir build-coverage --output-on-failure # gate fails as expected
ctest --test-dir build --output-on-failure # 6/6, no .gcda emitted
ctest --test-dir build-asan --output-on-failure # 6/6
scripts/mutation_test.py --target src/stdlib.c --list # 173 mutants, unchanged
Totals match gcov itself: 51.98% of 202 lines, 23.60% of 661 branches.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 01:48:05 -04:00
There are four harnesses. The first three take seconds; the fourth takes about
Document the test harnesses and add a pre-push hook
README.md was a build badge and nothing else. It now covers what the library
is, how to build it, and how to run each of the three harnesses locally: the
ctest suite, the AKSL_SANITIZE ASan/UBSan build, and mutation testing.
The section on reading ctest output matters most: two of the three test lists
are marked WILL_FAIL, so an all-green run 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.
.githooks/pre-push runs the two fast harnesses (default build + ctest, then
sanitizer build + ctest) before a push leaves the machine. It skips when there
is nothing to test (branch deletions, empty pushes), fails the push with the
captured output on any error, and builds under .git/aksl-prepush so it never
disturbs your own build/. The slow mutation gate is opt-in via
AKSL_HOOK_MUTATION=1, and git push --no-verify bypasses everything.
Enable with: git config core.hooksPath .githooks
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 11:51:28 -04:00
half an hour.
### 1. The test suite
```sh
cmake -S . -B build
cmake --build build
ctest --test-dir build --output-on-failure
```
Tests live one per file in `tests/test_<name>.c` and share the helpers in
`tests/aksl_capture.h` — `AKSL_CHECK()` for plain assertions (unlike `assert()`
it survives `-DNDEBUG` ), `AKSL_CHECK_STATUS(call, expected)` to run a wrapper and
assert on the status it returns, and an `AKSL_RUN()` driver that additionally
fails any test which leaks a slot from libakerror's error pool.
To add a test, drop `tests/test_mything.c` in place and add `mything` to
`AKSL_TESTS` in `CMakeLists.txt` .
**Reading the results.** `CMakeLists.txt` splits tests into three lists, and two
of them invert the meaning of "Passed":
| List | Meaning |
|---|---|
| `AKSL_TESTS` | Ordinary tests. Must exit 0. |
| `AKSL_WILL_FAIL_TESTS` | Expected to abort by design — an unhandled error reaching `FINISH_NORETURN` , or a deliberate contract violation. Marked `WILL_FAIL` , so a non-zero exit is a pass. |
| `AKSL_KNOWN_FAILING_TESTS` | Assert the * correct * behaviour of a confirmed defect (see `TODO.md` §2.1). Also marked `WILL_FAIL` . |
So `ctest` reporting all green does **not ** mean the library is defect-free — it
means the known-good tests passed and the known-bad ones are still failing in the
documented way. When a defect is fixed, its test starts passing, CTest reports it
as failed with * unexpectedly passed * , and that is the cue to move it from
`AKSL_KNOWN_FAILING_TESTS` into `AKSL_TESTS` .
Every test is capped with a 30-second CTest `TIMEOUT` . The list and tree code is
full of loops whose termination hangs on a single condition, so a bug of that
shape hangs the suite rather than failing it.
### 2. Sanitizers
```sh
cmake -S . -B build-asan -DAKSL_SANITIZE=ON
cmake --build build-asan
ctest --test-dir build-asan --output-on-failure
```
Builds the library, the tests and the vendored libakerror with ASan + UBSan and
`-fno-sanitize-recover=all` . Several of the open items in `TODO.md` §2 only
misbehave under instrumentation — the uninitialised `%s` in `aksl_realpath` , the
unbounded `vsprintf` behind `aksl_sprintf` , the missing `va_end` in the `printf`
family — so new tests for those should be run this way.
Add code coverage to the CTest suite
New AKSL_COVERAGE option instruments the library and its tests with
--coverage -O0 and wires the report into the suite itself, so a plain
ctest --test-dir build-coverage both runs the tests and produces coverage.
Two CTest entries do the work, held in order by a CTest fixture rather
than by declaration order so they also hold under ctest -j:
coverage_reset (FIXTURES_SETUP) clears the .gcda counters before any test,
since gcov counts are cumulative and would otherwise fold in earlier runs;
coverage_report (FIXTURES_CLEANUP) aggregates gcov output afterwards.
AKSL_COVERAGE_THRESHOLD / AKSL_COVERAGE_BRANCH_THRESHOLD gate the report,
the same regression-ratchet idea as the mutation score. The `coverage`
target builds, runs and prints in one step.
scripts/coverage.py parses gcov's JSON output, aggregates line, branch and
function counts across translation units, and lists every uncovered line
and never-called function -- the actionable half, as with surviving
mutants. Python stdlib plus gcc's own gcov only: no lcov, gcovr or
genhtml. It also writes coverage-summary.txt (CTest hides the output of a
passing test) and a Cobertura coverage.xml for CI publishers.
Instrumentation is per target, so deps/libakerror stays out of the report.
The mutation harness now ignores build*/ and gcov artifacts when copying
the tree, so a coverage build does not slow it down.
Baseline on src/stdlib.c: 52.0% of lines, 23.6% of branches, 8 of 21
functions. The uncovered functions are the untested wrappers the mutation
survivors already point at (printf, ato*, stream, realpath, strhash).
Verified:
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON
cmake --build build-coverage --target coverage # 8/8, report printed
ctest --test-dir build-coverage -j8 # fixture order holds
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON -DAKSL_COVERAGE_THRESHOLD=60
ctest --test-dir build-coverage --output-on-failure # gate fails as expected
ctest --test-dir build --output-on-failure # 6/6, no .gcda emitted
ctest --test-dir build-asan --output-on-failure # 6/6
scripts/mutation_test.py --target src/stdlib.c --list # 173 mutants, unchanged
Totals match gcov itself: 51.98% of 202 lines, 23.60% of 661 branches.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 01:48:05 -04:00
### 3. Code coverage
```sh
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON
cmake --build build-coverage --target coverage
```
`-DAKSL_COVERAGE=ON` compiles the library and the tests with `--coverage -O0` ,
and wires the report into the suite itself, so a plain
`ctest --test-dir build-coverage` also produces it. Two extra CTest entries
appear, held in place by a CTest fixture rather than by declaration order, so
they work under `ctest -j` too:
| Test | When | Does |
|---|---|---|
| `coverage_reset` | before every other test | deletes the accumulated `.gcda` counters |
| `coverage_report` | after every other test | aggregates `gcov` output, prints the summary, applies the threshold gate |
The reset matters: gcov counters are cumulative, so without it each report would
fold in every earlier run and overstate coverage.
CTest hides the output of a passing test, so `coverage_report` also writes
`build-coverage/coverage-summary.txt` (the same text report) and
`build-coverage/coverage.xml` (Cobertura, for CI publishers). The `coverage`
target above prints the report to the terminal for you; otherwise read the file
or use `ctest --test-dir build-coverage -V -R coverage_report` .
The report lists per-file line, branch and function coverage, then every
uncovered line and every function the suite never called — that listing is the
actionable part, the same way surviving mutants are for the harness below.
Drive the script directly for anything narrower:
```sh
scripts/coverage.py --build build-coverage # report on disk counters
scripts/coverage.py --build build-coverage --summary-only # totals only
scripts/coverage.py --build build-coverage --include tests # coverage of the tests themselves
scripts/coverage.py --build build-coverage --run-tests # reset, run ctest, report
scripts/coverage.py --build build-coverage --threshold 50 --branch-threshold 25
```
It needs nothing but Python 3 and gcc's own `gcov` — no lcov, gcovr or genhtml.
To gate on coverage, set the threshold at configure time; `coverage_report` then
fails below it, and the same regression-ratchet logic applies as for the mutation
score:
```sh
cmake -S . -B build-coverage -DAKSL_COVERAGE=ON \
-DAKSL_COVERAGE_THRESHOLD=50 -DAKSL_COVERAGE_BRANCH_THRESHOLD=20
```
Two caveats. Coverage is measured at `-O0` , because the optimizer reorders lines
until per-line counts stop matching the source — so a coverage build is not the
build to profile. And gcov flushes its counters at normal process exit, which an
`AKSL_WILL_FAIL_TESTS` entry that aborts by design never reaches: such a test
contributes no coverage data at all, so lines only it reaches are reported as
uncovered.
### 4. Mutation testing
Document the test harnesses and add a pre-push hook
README.md was a build badge and nothing else. It now covers what the library
is, how to build it, and how to run each of the three harnesses locally: the
ctest suite, the AKSL_SANITIZE ASan/UBSan build, and mutation testing.
The section on reading ctest output matters most: two of the three test lists
are marked WILL_FAIL, so an all-green run 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.
.githooks/pre-push runs the two fast harnesses (default build + ctest, then
sanitizer build + ctest) before a push leaves the machine. It skips when there
is nothing to test (branch deletions, empty pushes), fails the push with the
captured output on any error, and builds under .git/aksl-prepush so it never
disturbs your own build/. The slow mutation gate is opt-in via
AKSL_HOOK_MUTATION=1, and git push --no-verify bypasses everything.
Enable with: git config core.hooksPath .githooks
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 11:51:28 -04:00
The suite tells you the library works. Mutation testing tells you the * suite *
works: it breaks the library in small ways, one at a time, and checks that the
tests notice.
```sh
cmake --build build --target mutation # src/stdlib.c + include/akstdlib.h
```
or drive the script directly for a faster or narrower run:
```sh
scripts/mutation_test.py --target src/stdlib.c # C source only
scripts/mutation_test.py --target src/stdlib.c --list # enumerate, build nothing
scripts/mutation_test.py --target src/stdlib.c --max-mutants 20
scripts/mutation_test.py --target src/stdlib.c --threshold 40
```
A mutant that makes the tests fail is * killed * (good); one the tests still pass
is a * survivor * , and names a missing test. The score is `killed / total` , and the
run prints every survivor with `file:line` and the exact edit. The harness never
touches your working tree — it copies the repo to a scratch directory and mutates
the copy.
CI runs the `src/stdlib.c` set with `--threshold 40` . That is a regression
ratchet rather than a quality bar: the current score is 46.8%, and the survivors
are concentrated in the wrappers that have no tests yet. Raise the threshold as
coverage lands.
## The pre-push hook
`.githooks/pre-push` runs the fast harnesses — the default build and the
sanitizer build, each followed by `ctest` — before letting a push out. Enable it
once per clone:
```sh
git config core.hooksPath .githooks
```
It only builds when there are commits to push (a branch deletion is a no-op), and
it builds under `.git/aksl-prepush` so it never disturbs your own `build/` .
```sh
AKSL_HOOK_MUTATION=1 git push # also run the mutation gate (slow)
git push --no-verify # skip the hook entirely
```
Other knobs: `AKSL_MUTATION_THRESHOLD` (default 40, keep it in step with
`.gitea/workflows/ci.yaml` ) and `AKSL_HOOK_BUILD_DIR` .