Files
libakerror/AGENTS.md
Andrew Kesterson 5eaa956f50
Some checks failed
libakerror CI Build / cmake_build (push) Successful in 2m47s
libakerror CI Build / coverage (push) Successful in 2m48s
libakerror CI Build / thread_sanitizer (push) Failing after 2m49s
libakerror CI Build / mutation_test (push) Successful in 39m15s
Stop an unhandled error from exiting zero
An unhandled error could kill the process and still report success. The
default handler ended in exit(errctx->status), and an exit status is one
byte wide: the kernel keeps the low 8 bits of the argument and discards
the rest. Consumer statuses start at AKERR_FIRST_CONSUMER_STATUS (256),
so the first status any consumer can reserve exited 0 and a shell saw a
clean run. Status 300 exited 44, an unrelated error's code.

There is no wider exit() to reach for. _exit(), _Exit(), quick_exit()
and the raw exit_group syscall all truncate identically, and even
waitid(), whose si_status is a full int, reports the truncated value --
the truncation happened before the parent looked.

akerr_exit() now owns that mapping and the default handler calls it: 0
exits 0, 1 through 255 exit the status, and anything else exits
AKERR_EXIT_STATUS_UNREPRESENTABLE (125) rather than a low byte that is
either a lie or a claim of success. Only values that were already being
delivered wrong behave differently. Call it instead of exit() anywhere
you leave the process on a status; it is declared AKERR_NORETURN.

akerr_exit(0) exits 0, because 0 is this library's success status. That
is not a hole in the rule: PROCESS opens with case 0, which marks a zero
status handled, so a successful context never reaches FINISH_NORETURN's
call to the handler at all.

tests/err_exit_status.c drives one table through akerr_exit() and
through the default handler in forked children and requires identical
exit codes, so the handler cannot grow a mapping of its own. With the
clamp removed it fails with "akerr_exit(256) exited 0, want 125". The
full-width status was already reaching the log and still does, which the
same test asserts against the captured stack trace.

2.0.1. No ABI break: the soname stays libakerror.so.2 and nothing that
already existed changed shape. akerr_exit() is a new exported symbol, so
a consumer that starts calling it needs 2.0.1 at link time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:25:37 -04:00

137 lines
6.4 KiB
Markdown

# Repository Guidelines
## Project Structure & Module Organization
This is a small C library built with CMake. Core implementation lives in
`src/error.c`. `src/lock.h` is private: it selects the threading backend
(pthread or none) and is not installed. The public header is generated at build
time from `include/akerror.tmpl.h` by `scripts/generrno.sh`, which also
generates `src/errno.c` under the build directory and stamps in whether the
build is thread safe. CMake package and pkg-config templates are in `cmake/` and
`akerror.pc.in`. Tests are one-file C programs in `tests/`; shared test helpers
live beside them, such as `tests/err_capture.h` and `tests/err_threads.h`.
## Build, Test, and Development Commands
Use an out-of-tree build:
```sh
cmake -S . -B build
cmake --build build
ctest --test-dir build --output-on-failure
```
`cmake -S . -B build` configures the build and generates `akerror.h`.
`cmake --build build` compiles `libakerror` and test executables. `ctest`
runs the registered unit tests. To emit CI-style results, use:
```sh
ctest --test-dir build --output-on-failure --output-junit "$(pwd)/ctest-junit.xml"
```
The suite includes thread tests. The run that proves there is no data race under
them is ThreadSanitizer:
```sh
scripts/thread_test.sh
```
which configures `build/tsan` with `-DAKERR_SANITIZE=thread`, builds the library
*and* the tests with it, and runs CTest with ASLR disabled (TSan aborts with an
"unexpected memory mapping" on kernels with `vm.mmap_rnd_bits` above 28).
`AKERR_SANITIZE` takes any sanitizer list, so `-DAKERR_SANITIZE=address,undefined`
works the same way. CTest gives each test `halt_on_error=1`, so a report fails
the test rather than being printed and passed over — running a sanitized test
binary by hand does **not** inherit that. Set it yourself
(`TSAN_OPTIONS=halt_on_error=1 ./build/tsan/test_err_threads_pool`) or the
binary can print a race and still exit 0.
Mutation testing is available through:
```sh
cmake --build build --target mutation
scripts/mutation_test.py --target src/error.c --threshold 65
```
Code coverage is available through:
```sh
cmake --build build --target coverage
scripts/coverage.py --threshold 90 --branch-threshold 50
```
`scripts/coverage.py` configures its own instrumented build tree (default
`build/coverage`, `-DAKERR_COVERAGE=ON`), runs the CTest suite there, and
reports gcov line/branch coverage per library source. Thresholds gate each
file as well as the total. Coverage measures the library's own sources only --
`src/error.c`, `src/lock.h` and the generated `src/errno.c`; the public header's
macros expand at their call sites, so mutation testing is what checks those.
To build without threads (no locking, no thread-local storage, thread tests not
registered), configure with `-DAKERR_THREADS=none`. `auto` is the default and
fails the configure rather than falling back.
## Coding Style & Naming Conventions
Use C99-compatible C and follow the surrounding style. Functions and types use
the `akerr_` prefix; macros and constants use `AKERR_` or all-caps macro names
such as `PREPARE_ERROR`. Keep generated-code changes in templates or generator
scripts, not in build outputs. Preserve concise comments for invariants,
macro constraints, and non-obvious error lifecycle behavior.
**Locking.** One recursive lock (`akerr_state_lock`, see `src/lock.h`) covers
both the error pool and the status registry. A function named `*_locked` is
called with that lock already held; a function without the suffix takes it.
Never take it in a body written with the `FAIL_*_RETURN` macros — those return
from the middle of the function and would skip the unlock. Split it instead: the
locked body does the work, and a thin wrapper takes the lock, calls it, and
releases it on the single return path. Do not call consumer code
(`akerr_log_method`, `akerr_handler_unhandled_error`) while holding it.
**Exiting.** Never call `exit()` with a status value. Call `akerr_exit()`, which
owns the one mapping from an akerr status to an exit code: an exit status is a
byte, and every consumer status starts at 256, so passing a status through
`exit()` silently truncates it — status 256 exits 0 and reports success. The
only exits that bypass it are the two that have no status to map, both in
`src/error.c`: `ENSURE_ERROR_READY`'s pool-exhaustion abort and the NULL-context
case in `akerr_default_handler_unhandled_error()`. This rule applies to test
programs too, except where the test's whole point is to observe the raw
truncation.
## Testing Guidelines
Add tests as `tests/err_<behavior>.c`. Register each new test in the
`AKERR_TESTS` list in `CMakeLists.txt`; tests that intentionally abort must
also be listed in `AKERR_WILL_FAIL_TESTS`. Prefer focused executable tests that
return zero on success and use existing helpers such as `AKERR_CHECK`. Run the
CTest suite before submitting changes, and run mutation testing and coverage
when changing core control-flow, reference counting, stack-trace, or handler
behavior.
Tests that drive the library from several threads go in the `AKERR_THREAD_SAFE`
branch of the `AKERR_TESTS` list — an `-DAKERR_THREADS=none` build has no
threading to test — and use `tests/err_threads.h`, which runs a body on
`AKERR_TEST_THREADS` threads that meet at a barrier first.
Count failures per thread with `AKERR_TCHECK` rather than returning early: a
thread that abandons its work leaves the others holding pool slots and turns one
failure into a cascade. Anything the test shares between its own threads must go
through `__atomic` builtins — a race in the test is still a race, and
ThreadSanitizer cannot tell you whose it is. The capturing logger in
`err_capture.h` is single-threaded (shared buffer, shared length); use
`akerr_thread_logger` instead. Run `scripts/thread_test.sh` when changing
anything that touches the pool, the registry, initialization, or the lock.
## Commit & Pull Request Guidelines
Recent commits use short, imperative, sentence-case subjects, for example
`Fix refcount leak and stack-trace buffer overflow`. Keep commits focused and
describe the observable behavior changed. Pull requests should include a brief
summary, tests run, and any compatibility impact for public macros, generated
headers, installation paths, or CMake/pkg-config consumers.
## Agent-Specific Instructions
Do not overwrite uncommitted user changes. Avoid editing generated files in
`build/`; update `include/akerror.tmpl.h`, `src/error.c`, CMake files, tests,
or scripts instead.