Commit Graph

6 Commits

Author SHA1 Message Date
9bf8bcd357 Fix two comments that misstate exit sites and NULL validity
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) Has been cancelled
Both found while auditing the pool for the cross-thread transfer guarantee.

AGENTS.md said the two exit() calls that bypass akerr_exit() are "both in
src/error.c". ENSURE_ERROR_READY's pool-exhaustion abort is in
include/akerror.tmpl.h. The count of abort sites is load-bearing -- it is
the argument against any API that could fail on pool exhaustion -- so
pointing at the wrong file makes it hard to check.

akerr_valid_error_address() returns 1 for NULL, which is right for VALID()
(NULL is how a function reports success) and a trap for anything reading it
as "is this a pool slot". Say so where the function is defined.

Comments only; no behavior change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:35:26 -04:00
5eaa956f50 Stop an unhandled error from exiting zero
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
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
756933c600 Record the mutation score and the concurrency mutants it misses
Some checks failed
libakerror CI Build / cmake_build (push) Successful in 2m48s
libakerror CI Build / coverage (push) Successful in 2m47s
libakerror CI Build / thread_sanitizer (push) Failing after 2m49s
libakerror CI Build / mutation_test (push) Successful in 38m22s
src/error.c now scores 81.2%: 238 of 293 mutants killed, 204 by a
failing test, 24 by failing to compile, and 10 by hanging the suite --
deleting akerr_mutex_init() or the akerr_initializing re-entry guard
deadlocks the first test, which is the right answer for a broken lock.

Lock deletions are the one survivor category where surviving does not
mean harmless, so measure it rather than assume: rebuilt, the surviving
"delete the pool lock" mutant fails tests/err_threads_pool.c in 4 of 10
runs and fails under scripts/thread_test.sh in 5 of 5, with no false
positive on the unmutated library. The property assertions alone are a
coin flip on a missing lock; the sanitizer run is what holds that line.
The harness builds mutants with default CMake options and so never sees
it -- TODO item 8.

Also warn that a sanitized test binary run by hand does not inherit the
halt_on_error CTest gives it, and will print a race and still exit 0.
That is how the 5-of-5 above first read as 2 of 5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:45:20 -04:00
be24f80022 Make the error pool and status registry thread safe
Every entry point may now be called from any thread. akerr_init() runs
exactly once however many threads race into it, the pool hands each slot
to exactly one thread, and reservations, registrations and lookups are
serialized against each other.

One recursive lock covers both tables (src/lock.h, private). Recursive
because raising an error re-enters the library -- FAIL needs a pool slot
and a status name -- and single because two locks would mean an ordering
to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN
macros are split into *_locked functions behind wrappers that take and
release the lock on one path; consumer callbacks are never called under
it.

This is an ABI break, hence 2.0.0 and SOVERSION 2:

- akerr_next_error() now returns a context that already holds its
  reference. Finding a free slot and claiming it has to be one operation,
  or two threads scanning at once are handed the same slot.
  ENSURE_ERROR_READY no longer increments.
- __akerr_last_ignored is thread-local, as is the last-ditch context used
  to report akerr_release_error(NULL).

The threading backend is chosen at configure time by AKERR_THREADS
(auto, pthread, none). auto fails the configure when it cannot find
POSIX threads rather than quietly building a library that reports itself
thread safe and is not. generrno.sh stamps the decision into the
generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree
with the library about it.

Tests: err_threads_init, err_threads_pool and err_threads_registry
assert exclusive slot ownership, exactly one winner for a contested
range, and every registered name readable back under contention.
AKERR_SANITIZE builds the library and the tests with any sanitizer;
scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs
it. Removing the pool lock makes both the sanitizer and the plain
assertions fail, so the tests are not vacuous.

Documented in README.md and UPGRADING.md, including what this does not
cover: renaming a status while another thread looks it up, and which of
two simultaneous unhandled errors sets the exit status.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:31:22 -04:00
9f0034a56e Add gcov code coverage to the test suite
scripts/coverage.py configures an instrumented build tree
(-DAKERR_COVERAGE=ON), runs the CTest suite in it, and reports merged
gcov line/branch/function coverage per library source. Like the mutation
harness it has no third-party dependencies and supports --threshold and
--junit; thresholds gate each file as well as the total so the generated
status-name table cannot mask a regression in src/error.c.

Only the library is instrumented. The public header's macros cannot be
measured this way -- GCC attributes an expanded macro to its call site,
so header logic would report as lines of the test that used it -- which
is what mutation testing against include/akerror.tmpl.h is for.

Coverage flags are applied per target rather than globally, so they do
not leak into the exported/installed target interface.

Current numbers for src/error.c are 94.0% line and 59.5% branch; the CI
gate is set to 90/50 to keep headroom, matching the convention used for
the mutation score threshold.

Tests run: ctest (23/23), cmake --build build --target coverage,
threshold gate verified failing at --threshold 99, cmake --install
checked for flag leakage, out-of-tree --build-dir checked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:05:30 -04:00
4ae1decde2 Expand error status test coverage
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m44s
libakerror CI Build / mutation_test (push) Successful in 7m46s
- Added explicit status validation across error handling tests.
- Added lifecycle and slot-leak checks to older tests.
- Improved unhandled-error propagation coverage.
- Added repository guidance and a test runner script.
- Verified all 23 CTest tests pass.

Co-Authored by Codex GPT 5.4
2026-07-29 17:12:52 -04:00