25 Commits

Author SHA1 Message Date
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
5ff87908e7 Use the library's own error idioms inside the library
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m48s
libakerror CI Build / coverage (push) Successful in 2m46s
libakerror CI Build / mutation_test (push) Successful in 16m9s
Four things in src/error.c did by hand what the macros already do, or
skipped checks the library would have caught for a consumer.

akerr_copy_string() returned void and validated only its capacity, while
writing through a caller-supplied pointer for a caller-supplied length.
It is now __akerr_copy_string() and raises: AKERR_NULLPOINTER for a NULL
destination or source, AKERR_VALUE for a capacity with no room for a
terminator. Both call sites PASS it, and the owner copy in
akerr_reserve_status_range() now gates the commit, so a failed copy
cannot leave a range claimed under an empty owner. It is exported under
the internal prefix rather than static so tests/err_copy_string.c can
drive those guards; nothing else can reach them.

__akerr_name_library_status() and the band reservation in akerr_init()
hand-rolled the log/handler/release sequence. Both now use
ATTEMPT/CATCH/PROCESS/FINISH_NORETURN. PASS does not fit: both sites are
void and have no caller to propagate to, so the terminal form of the same
idiom is the right one -- an unhandled failure prints its stack trace and
goes to akerr_handler_unhandled_error, which terminates, exactly as
before but without the bespoke plumbing. The legacy set path in
akerr_name_for_status() had the same shape and now handles its refusal
with HANDLE_DEFAULT, converting it to the "Unknown Error" sentinel.

Every remaining `if (x) { FAIL_RETURN }` in the registry is now
FAIL_ZERO_RETURN or FAIL_NONZERO_RETURN, and akerr_register_status_name()
checks both owner and name before passing either down --
akerr_store_status_name() reads a NULL owner as "caller did not identify
itself" for the legacy path, so a NULL arriving through the owned entry
point would have skipped the ownership check entirely.

New tests: err_copy_string (the guards above), err_library_status_fatal
(WILL_FAIL -- proves a refused library-status registration terminates).

Tests: ctest 31/31, mutation 80.7% (was 77.5%), line coverage 98.9%.
Branch coverage on src/error.c drops 64.5% -> 50.4%, just over its gate:
each FAIL_* site carries ~6 branch outcomes of error-construction
machinery that only run when that failure fires, and each PASS around a
call that cannot fail carries ~25, so added validation lowers the ratio
by construction. Recorded in TODO.md item 7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 21:53:33 -04:00
64da04e83b Raise errors from the status registry instead of returning codes
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m47s
libakerror CI Build / coverage (push) Successful in 2m46s
libakerror CI Build / mutation_test (push) Successful in 14m56s
akerr_reserve_status_range() and akerr_register_status_name() returned
private int enumerations, which was the one place in the library where a
failure was not an akerr_ErrorContext *. They now return one like
everything else: NULL on success, and on refusal an error whose status is
a real code in the library's reserved band, so it can be CATCH-ed,
HANDLE-d, PASS-ed, or left to propagate into a stack trace. Both are
marked AKERR_NOIGNORE, so discarding the result warns at compile time.

AKERR_STATUS_RANGE_OK and AKERR_STATUS_NAME_OK are gone; the remaining
seven codes move into the AKERR_* offset span and get registered names.
AKERR_LAST_LIBRARY_STATUS replaces AKERR_BADEXC as the top of that span
in the reserved-band static assert and the exhaustiveness sweep.

The refusal detail that used to go straight to akerr_log_method now
travels in the error message, so a caller that handles the error decides
whether it is reported. The two-argument akerr_name_for_status() set path
is the exception: it returns a name and cannot raise, so it logs and
releases. akerr_init() likewise has no caller to raise into, so failing
to reserve its own band or name its own codes is logged and fatal --
that can only happen on a misconfigured build, and continuing would
degrade every later stack trace to "Unknown Error".

Move the 1.0.0 upgrade notice out of README.md into UPGRADING.md and
rewrite its return-code tables in terms of the statuses now raised.

Tests: ctest 29/29, coverage 97.5% line / 64.5% branch, mutation 77.5%
(was 77.6%; the new survivors are the fatal init path, which needs a
library built with an undersized name table -- TODO item 7).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 20:58:47 -04:00
ba2430bfa1 Reduce TODO.md to outstanding work
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m46s
libakerror CI Build / coverage (push) Successful in 2m45s
libakerror CI Build / mutation_test (push) Successful in 15m10s
The status-code ownership notes described work that has landed. That record
belongs in the commit that made the change and in the comments around the code
it constrains, not in a file whose purpose is naming what is left.

Keeps the six open items and the two unrelated pre-existing issues, and
promotes the missing sanitizer run to the top: mutation testing found an
out-of-bounds probe whose failure mode was a silent BSS write, which no
assertion-based test was positioned to catch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:19:53 -04:00
f1283e21a3 Enforce status-code ownership and harden the name registry
Reservations were advisory bookkeeping: any component could name any status,
so the registry only detected declared-range overlap between components that
both opted in. Naming a status now requires a reservation.
akerr_register_status_name() checks that the range belongs to the caller, and
the legacy two-argument akerr_name_for_status() set path, which cannot
identify its caller, requires that some reservation covers the status. Every
refusal is logged and names the real owner, because a name that fails to
register degrades that code to "Unknown Error" in every later stack trace.

Fix a reservation made before the first PREPARE_ERROR being silently
discarded. akerr_init() clears the tables, so whichever component first
triggered it wiped an earlier reservation and the next component to claim the
same range was told it was free, producing exactly the undetected aliasing
the registry exists to prevent. Every registry entry point now calls
akerr_init(), which sets its guard before doing any work so those calls do
not recurse.

Replace the linear-scan name array with an open-addressed hash table, taking
lookup from O(n) to O(1) and raising usable capacity from 512 entries (366
free to consumers after errno registration) to 3072 (~2900 free). Both table
sizes are build-time overridable and applied PRIVATE: they live entirely in
src/error.c, so raising them cannot desynchronize a library from its
consumers the way AKERR_MAX_ERR_VALUE could. Exhausting either table is now
logged and returned to the caller rather than silently dropping the entry.
No dynamic allocation is introduced; both tables remain file-scope arrays,
and the library's undefined-symbol set gains only strcmp and strlen.

Register names for AKERR_EOF, AKERR_ITERATOR_BREAK and AKERR_NOT_IMPLEMENTED,
which had none and rendered as "Unknown Error" in every stack trace carrying
them. err_error_names.c now sweeps the whole AKERR_* offset span so a code
added without a name fails there instead of in production traces.

Add static assertions that the slot count is a power of two and that
AKERR_BADEXC stays inside the library's own 0-255 band, the latter guarding
against a host errno space large enough to push library codes into the range
consumers are told to allocate from.

Set a project version and soname (1.0.0 / libakerror.so.1) so a stale
installed library can no longer be silently paired with newer headers, and so
akerror.pc ships a real Version field instead of an empty one.

Mutation testing surfaced an out-of-bounds probe in the new table that the
suite did not catch: masking with SLOTS rather than SLOTS-1 indexes past the
array, and err_maxval.c asserted only that some names registered before the
table filled, which a collapsed probe sequence still satisfies. It now
requires a substantial entry count and reads every entry back by its own
distinct name.

Tests: 28/28 pass. Coverage 99.4% line / 86.8% branch. Mutation score for
src/error.c 74% -> 77.3%.

Compatibility: source and ABI break. AKERR_MAX_ERR_VALUE and the
__AKERR_ERROR_NAMES data symbol are gone, custom codes must move out of
0-255, and names must be registered against a reserved range. README.md
carries the migration steps.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:19:25 -04:00
11d21068df Add collision-safe status code registry
Replace the consumer-sized status-name array with private sparse storage and accept arbitrary integer status values. Add explicit range reservations with overlap diagnostics, reserve the library's 0-255 compatibility band, and harden pointer and string boundary handling.

Update regression coverage and document the required migration for custom status-code consumers.
2026-07-30 13:53:52 -04:00
0bb3a4d52c TODO.md
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m45s
libakerror CI Build / coverage (push) Successful in 2m45s
libakerror CI Build / mutation_test (push) Successful in 8m1s
2026-07-30 13:22:50 -04:00
539293cc1c Expand error release and handler test coverage
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m46s
libakerror CI Build / coverage (push) Successful in 2m45s
libakerror CI Build / mutation_test (push) Successful in 8m0s
2026-07-30 02:05:31 -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
0c0d81249f Avoid mutation target collisions when embedded
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m44s
libakerror CI Build / mutation_test (push) Successful in 7m38s
2026-07-29 18:01:58 -04:00
426efbb2d4 Add CLAUDE.md
Some checks failed
libakerror CI Build / cmake_build (push) Successful in 2m44s
libakerror CI Build / mutation_test (push) Has been cancelled
2026-07-29 17:42:56 -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
4212ff0b28 Fix format-string use of __FILE__/__func__ and name_for_status lower bound
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m48s
libakerror CI Build / mutation_test (push) Successful in 7m40s
Two hardening fixes flagged by the earlier review:

1. FAIL passed __FILE__ and __func__ directly as the snprintf format string.
   __FILE__ expands to a string literal that could contain a '%' (a build path
   under a directory with a percent sign), and __func__ is not a literal at all;
   either way snprintf would read nonexistent varargs. Pass them as "%s"
   arguments instead.

2. akerr_name_for_status guarded the upper bound but not the lower one, so a
   negative status indexed __AKERR_ERROR_NAMES[negative] -- an out-of-bounds
   read, or an out-of-bounds write when a name was supplied. Reject status < 0.

Regression tests err_format_string (uses #line to put a conversion specifier in
__FILE__) and err_name_bounds fail against the old code (verified) and pass now.
Full suite: 23/23, no warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 10:52:29 -04:00
de13b290d4 Fix refcount leak and stack-trace buffer overflow
Two memory-safety bugs in the macro core:

1. Refcount leak. ENSURE_ERROR_READY incremented refcount on every FAIL/SUCCEED
   rather than only when it acquired a fresh context from the pool. A function
   that FAILed a context more than once and then propagated arrived at its
   caller with refcount 2; the caller released once, leaking the slot. After
   AKERR_MAX_ARRAY_ERROR leaks the pool is exhausted and the library exit(1)s.
   Move the increment inside the acquisition branch.

2. Stack-trace overflow. Each appended frame passed the full buffer length to
   snprintf instead of the space remaining, and advanced the cursor by
   snprintf's would-be return value, so a trace that filled the buffer wrote
   past the end of stacktracebuf and ran the cursor out of bounds. Add
   AKERR_STACKTRACE_APPEND, which bounds the write to the remaining space and
   clamps the cursor advance.

Regression tests err_refcount_double_fail and err_stacktrace_bounds fail against
the old code (verified) and pass now. Full suite: 21/21, no warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 09:32:55 -04:00
3e24356f07 Document that CATCH/FAIL_*_BREAK must not be used inside a loop
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m47s
libakerror CI Build / mutation_test (push) Successful in 6m59s
These macros leave the ATTEMPT block with a C break, which only escapes the
innermost loop/switch. Nesting them in a loop inside an ATTEMPT lets the rest of
the block run with an error already pending. Document the correct patterns:
iterate with PASS / FAIL_*_RETURN (which return, not break), or move the loop
into a helper returning akerr_ErrorContext * and CATCH the single call. Also
note that merely extracting the loop into a function does not fix it if the
helper still wraps the loop in ATTEMPT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 08:49:29 -04:00
8a026d3006 Include passed tests in the JUnit report summary
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m48s
libakerror CI Build / mutation_test (push) Successful in 7m13s
The reporter warned "No annotations found ... configure 'include_passed' as
'true'" because with annotate_only the summary only listed failures. Set
include_passed: true on both reporter steps so the job summary table lists the
passing tests too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 21:15:13 -04:00
792e646957 Work around Gitea Checks API 404 in the JUnit reporter
Some checks failed
libakerror CI Build / cmake_build (push) Successful in 2m45s
libakerror CI Build / mutation_test (push) Has been cancelled
mikepenz/action-junit-report defaults to creating a check run via the Checks
API, which Gitea does not support -- the call 404s and the publish step fails
(mikepenz/action-junit-report#23). Set annotate_only: true on both reporter
steps to skip check creation, and detailed_summary: true so results still show
up in the job summary (which Gitea's runner does render).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 21:10:09 -04:00
43516c7e73 Emit JUnit XML from tests + mutation, consume it in CI
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 3m10s
libakerror CI Build / mutation_test (push) Successful in 6m58s
Produce machine-readable results and surface them in the Gitea pipeline:

- ctest: run with --output-junit to write ctest-junit.xml. The path must be
  absolute ("$(pwd)/...") because --output-junit otherwise resolves relative to
  the --test-dir build directory.
- mutation_test.py: new --junit FILE option writes a JUnit report where each
  mutant is a test case and a surviving mutant is a <failure> (so gaps show up
  as failing tests).
- .gitea/workflows/ci.yaml: both jobs generate their XML and feed it to
  mikepenz/action-junit-report with `if: always()`, so results publish even
  when a gate fails. Mutation publishing is display-only (fail_on_failure:
  false); the --threshold flag remains the gate.
- .gitignore: ignore the generated *-junit.xml artifacts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 20:51:28 -04:00
536a269aad Derive err_maxval's code set from the header, not a hardcoded list
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m41s
libakerror CI Build / mutation_test (push) Successful in 6m52s
The previous err_maxval hardcoded the list of AKERR_* codes, which silently
rots the moment a code is added. Instead, parse the generated akerror.h at
runtime: discover every "#define AKERR_<NAME> (AKERR_LAST_ERRNO_VALUE + N)",
take the highest offset actually defined, and assert AKERR_MAX_ERR_VALUE covers
it. A compile-time cross-check ties the parsed ceiling to the compiled macro so
the test can't pass by reading a stale header.

CMake injects the header path as AKERR_GENERATED_HEADER. Verified: the test
fails (max_err_value >= highest_code) when pointed at a +15 header while
AKERR_BADEXC is +17, and now also strengthens header mutation coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 17:26:32 -04:00
10f7203e8f Fix AKERR_MAX_ERR_VALUE to cover all AKERR_* codes
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m41s
libakerror CI Build / mutation_test (push) Successful in 6m54s
AKERR_MAX_ERR_VALUE was AKERR_LAST_ERRNO_VALUE + 15, but the highest defined
code, AKERR_BADEXC, is + 17 (AKERR_NOT_IMPLEMENTED is + 16). akerr_name_for_status
rejects any status above the max, so those codes could never have a registered
name and the AKERR_BADEXC registration in akerr_init was dead code -- a gap
found by mutation testing. Bump the max to + 17.

- err_maxval: new test asserting the reserved AKERR_* range exceeds the number
  of AKERR_* codes and that every code is individually indexable. Fails against
  the old + 15 value (verified), guarding against regression.
- err_error_names: now also checks AKERR_BADEXC's name, which the fix makes
  reachable.

Mutation score on src/error.c rises 71% -> 74%: the previously-dead BADEXC
registration and the name_for_status upper-bound check are now killable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 17:17:26 -04:00
43f46dca64 Add mutation testing to validate the test suite
Introduce a self-contained mutation testing harness that verifies the unit
tests actually catch bugs: it makes small deliberate breakages to the library
(flip comparisons, delete statements, swap true/false, etc.), rebuilds, and
runs the whole CTest suite against each mutant. Tests that still pass reveal a
gap; tests that fail "kill" the mutant.

- scripts/mutation_test.py: the engine (stdlib only, no LLVM/clang deps).
  Operators ROR/LCR/BCR/AOR/ICR/SDL over src/error.c and the macro header.
  Mutates a scratch copy, never the working tree. Supports --target, --list,
  --max-mutants sampling, --threshold gating, --timeout.
- CMakeLists.txt: 'mutation' custom target (cmake --build build --target mutation).
- .gitea/workflows/ci.yaml: gated mutation job on src/error.c (threshold 65%).
- tests/MUTATION.md: how to run, interpret survivors, and known equivalents.

Close the real gaps the harness found in src/error.c (score 53% -> 71%):
- err_error_names: the AKERR_* codes have their names registered by akerr_init
- err_release_clears: releasing a context wipes it before reuse
- err_pool_exhaust: akerr_next_error returns NULL when the pool is full and
  always hands back the lowest free slot

Also surfaced (documented, not fixed): AKERR_MAX_ERR_VALUE (+15) is below
AKERR_NOT_IMPLEMENTED (+16) and AKERR_BADEXC (+17), so those codes can never
have a name registered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 17:03:53 -04:00
e5f761662c Reformat new test files with Stroustrup style
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m38s
Apply emacs CC-mode "stroustrup" style (c-basic-offset 4, indent-tabs-mode t)
to the test files added in the previous commit, matching the existing house
style. Whitespace only; no behavioral change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 16:35:32 -04:00
4daa411f3f Expand test coverage for error-handling macros and error pool
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m42s
Add 11 CTest programs and a shared test helper covering gaps left by the
original four tests (which only exercised FAIL/CATCH/HANDLE and CLEANUP):

- err_capture.h: capturing akerr_log_method + NDEBUG-proof AKERR_CHECK so
  tests can assert on message/status/stacktrace content, not just exit codes
- err_success: clean nested return does not break/handle/leak
- err_pool_refcount: 100k raise->catch->handle cycles leak 0 pool slots
- err_handle_default / err_handle_group / err_handle_dispatch: handler routing
- err_pass / err_ignore / err_swallow: PASS, IGNORE, FINISH(e,false)
- err_break_variants: FAIL_*_BREAK and FAIL_*_RETURN
- err_errno: system errno name lookup + "Unknown Error" boundary
- err_custom_handler: override the unhandled-error hook, assert non-fatally

Register tests via a foreach loop in CMakeLists.txt. Ignore build/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 16:18:17 -04:00
16 changed files with 691 additions and 1283 deletions

View File

@@ -11,13 +11,6 @@ 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`.
Prose documentation lives in `docs/`, one file per topic — `architecture.md`,
`usage.md`, `status-codes.md`, `uncaught-errors.md`, `exit-status.md`,
`thread-safety.md`, `building.md`. `README.md` is deliberately short: summary,
installation, quickstart, and an index into `docs/`. Add new documentation to
the `docs/` file that owns the topic and link it from the README's index rather
than growing the README back.
## Build, Test, and Development Commands
Use an out-of-tree build:
@@ -99,9 +92,9 @@ releases it on the single return path. Do not call consumer code
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:
`ENSURE_ERROR_READY`'s pool-exhaustion abort in `include/akerror.tmpl.h`, and the
NULL-context case in `akerr_default_handler_unhandled_error()` in `src/error.c`. This rule applies to test
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.
@@ -133,31 +126,11 @@ anything that touches the pool, the registry, initialization, or the lock.
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, any compatibility impact for public macros, generated
headers, installation paths, or CMake/pkg-config consumers, and a link to the
issue they close.
summary, tests run, and any compatibility impact for public macros, generated
headers, installation paths, or CMake/pkg-config consumers.
## Agent-Specific Instructions
**Outstanding work goes in the issue tracker, not in a file.** Open an issue at
<https://source.starfort.tech/andrew/libakerror/issues> — `tea issues create
--repo andrew/libakerror` — naming the file and line, the functional
consequence, and what closing it would touch. Label it by kind and blast radius
and leave `status::grooming` on it until its scope and approach are settled.
**Do not add outstanding items to `TODO.md`**: that file is the record of why
the handler ladder is a major-version change, why a copied `akerr_ErrorContext`
is a trap, why validating more inputs lowers branch coverage, and why the
mutation score is a floor. A description of work still to do goes stale the
moment somebody does it.
**This library's defects are most often found by its consumers, so make filing
them cheap.** Three defects in this library — `IGNORE()` leaking a context, the
un-namespaced `coverage` target, and the missing `akerrorConfigVersion.cmake`
were written down in `libakstdlib`'s own notes and never here, so each was
worked around once per consumer and nobody saw the pattern. A consumer filing
upstream should cost them one issue instead of one workaround. `libakgl`,
`libakstdlib` and `akbasic` are all on the same forge.
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.

View File

@@ -234,7 +234,6 @@ if(AKERR_THREAD_SAFE)
err_threads_init
err_threads_pool
err_threads_registry
err_threads_handoff
)
endif()
@@ -263,14 +262,6 @@ foreach(_test IN LISTS AKERR_TESTS)
endif()
endforeach()
# HANDLE_GROUP deliberately enters the next case label. Keep that public macro
# compiling with the warning enabled, so a future macro edit cannot restore the
# warning for consumers which adopt -Wextra.
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(test_err_handle_group PRIVATE
-Werror=implicit-fallthrough)
endif()
set_tests_properties(
${AKERR_WILL_FAIL_TESTS}
PROPERTIES WILL_FAIL TRUE

646
README.md
View File

@@ -4,6 +4,21 @@ This library provides a TRY/CATCH style exception handling mechanism for C.
![build badge](https://source.starfort.tech/andrew/libakerror/actions/workflows/ci.yaml/badge.svg?branch=main)
## Upgrading
2.0.1 fixes an unhandled error killing the process and still reporting success:
the exit code was the status truncated to a byte, and every consumer status
starts at 256. Use `akerr_exit()` instead of `exit()` — see
[Exit status](#exit-status). No ABI break.
2.0.0 makes the library thread safe. That is an ABI break — `__akerr_last_ignored`
became thread-local storage and the pool now takes its own reference — so
everything built against a 1.x header must be rebuilt. 1.0.0 replaced the
consumer-sized status-name array with a private, ownership-enforced registry.
See [UPGRADING.md](UPGRADING.md) for all three, what was removed, how to migrate,
the capacity limits and how to raise them, and the thread-safety rules.
# Why?
There is nothing wrong with C as it is. This library does not claim to fix some problem with C.
@@ -12,6 +27,10 @@ Instead, this library implements a pragmatic and stylistic choice to assist the
Why? Because some programmers prefer to have the power of C with just a little bit of help in managing their errors.
# Library Architecture
## Philosophy of Use
This library has 6 guiding principles:
* Manually checking every possible return code for every possible meaning of that return code is tedious and prone to miss unpredicted failure cases
@@ -21,19 +40,171 @@ This library has 6 guiding principles:
* Manipulating the call stack directly is error prone and dangerous
* Declaring, capturing, and reacting to errors should be intuitive and no more difficult than managing return codes
# Documentation
## Lifecycle of an error in the AKError library
| Document | What it answers |
| -------- | --------------- |
| [docs/architecture.md](docs/architecture.md) | What an error context is, how one travels up the call stack, and what the macros build |
| [docs/usage.md](docs/usage.md) | The macro reference: `ATTEMPT`/`CLEANUP`/`PROCESS`/`FINISH`, `CATCH`, `FAIL_*`, `PASS`, `HANDLE`, `SUCCEED_RETURN` |
| [docs/status-codes.md](docs/status-codes.md) | Defining your own status codes, and reserving a range so two libraries cannot collide |
| [docs/uncaught-errors.md](docs/uncaught-errors.md) | `AKERR_NOIGNORE`, what a stack trace looks like, and how to read one |
| [docs/exit-status.md](docs/exit-status.md) | Why you call `akerr_exit()` and never `exit()`, and how to replace the unhandled-error handler |
| [docs/thread-safety.md](docs/thread-safety.md) | What thread safety here covers, what it does not, and how to hand an error to another thread |
| [docs/building.md](docs/building.md) | Configure options, the generated header, and building without stdlib |
| [UPGRADING.md](UPGRADING.md) | What changed in 1.0.0, 2.0.0 and 2.0.1, and how to migrate |
| [TODO.md](TODO.md) | The reasoning behind decisions and measurements. **Outstanding work is in [the issue tracker](https://source.starfort.tech/andrew/libakerror/issues).** |
TL;DR - `akerr_ErrorContext` objects are filled with error context information and bubbled up through nested control structures until they are handled or reach the top level, where an unhandled error halts program termination with a stack trace
1. At the point where an error occurs, an `akerr_ErrorContext` object is initialized and populated with information regarding the failure
2. The akerr_ErrorContext is returned from the scope where the error was detected
3. The akerr_ErrorContext enters a control structure provided by the AKError library through a series of macros that examine `akerr_ErrorContext` objects as they pass through
4. The control structure checks to see if the `akerr_ErrorContext` has an error set, and if so, if there are any handlers in the current control structure that can handle it
5. If the current control structure can handle the `akerr_ErrorContext`, it does so
6. If the current control structure can not handle the `akerr_ErrorContext`, then the current control structure's cleanup code (if any) is executed, and the `akerr_ErrorContext` object is passed out of the current control structure to the parent control structure
7. Steps 2-6 are repeated through as many control structures as are necessary to reach the first level of the control structure
8. When the first level of the control structure is reached, if the `akerr_ErrorContext` has an error set in it, then the stack trace information in the `akerr_ErrorContext` object is used to print a stack trace using the configured logging function, and program termination is halted
## What is in an Error Context
The Error Context object is a simple object which contains a few things:
* A numeric error code
* The name of the file in which the error occurred
* The name of the function in which the error occurred
* The line number in the file at which the error occurred
* A character buffer containing a message about the error in question
The structure also contains housekeeping information for the library which are of no specific interest to the user. See [include/akerror.h](include/akerror.h) for more details.
## What are the control structures
The library is structured around a series of macros that construct `switch` statements that perform logic against an `akerr_ErrorContext` which exists in the current scope and has been initialized. These macros must be assembled in a specific order to produce a syntactically correct `switch` statement which performs correct operations against the `akerr_ErrorContext` to attempt operations, detect failures, perform cleanup operations, handle errors, and then exit a given scope in a success or failure state.
## Functions and Return Codes
This library can catch errors from any function or expression that returns an integer value, or from functions that return `akerr_ErrorContext *`.
Any function which uses the `PREPARE_ERROR` macro should have a return type of `akerr_ErrorContext *`. The macros within this library, when they detect an unhandled error, will attempt to pass up the unhandled error to the context of the previous function in the call stack. This allows for errors to propagate up through the call stack in the same way as exceptions. (For example, if you use traditional C error handling in a call stack of `a() -> b() -> c()`, and `c()` fails because it runs out of memory, `b()` will likely detect that error and return some error to `a()`, but it may or may not return the context of what failed and why. With this, you get that context all the way up in `a()` without knowing anything about `c()`.
## Error codes
The library uses integer values to specify error codes inside of its context. These integer return codes are defined in `akerror.h` in the form of `AKERR_xxxxx` where `xxxxx` is the name of the error code in question. See `akerror.h` for a list of defined errors and their descriptions.
You can define additional error types as integer constants. Values 0 through 255
are reserved by libakerror (the host's errno values plus the `AKERR_*` codes);
consumers allocate codes starting at `AKERR_FIRST_CONSUMER_STATUS` (256). Status
names are stored sparsely, so any `int` is a legal status and no compile
definition is needed to use large values. Note that no consumer status can be a
process exit code — see [Exit status](#exit-status).
Every library that may coexist in one process must reserve its range during
initialization. `akerr_reserve_status_range()` and
`akerr_register_status_name()` report failure the way everything else in this
library does — they return `akerr_ErrorContext *`, and they are marked
`AKERR_NOIGNORE` — so a collision is an exception you can `CATCH`, `HANDLE`, or
`PASS` up out of your initialization:
```c
#define MYLIB_OWNER "my-library"
akerr_ErrorContext AKERR_NOIGNORE *mylib_init(void)
{
PREPARE_ERROR(errctx);
/* Another component owning part of the range propagates to our caller. */
PASS(errctx, akerr_reserve_status_range(256, 16, MYLIB_OWNER));
/* Then name each code, quoting the owner you reserved with. */
PASS(errctx, akerr_register_status_name(MYLIB_OWNER, 256,
"Some Error Code Description"));
SUCCEED_RETURN(errctx);
}
```
Reservations are process-local, fixed-capacity, and idempotent when the same
owner repeats the exact same range. They preserve compile-time integer constants
so `HANDLE` still works, since `case` labels require them.
Naming a status outside your reservation raises `AKERR_STATUS_NAME_FOREIGN`, and
naming one nobody reserved raises `AKERR_STATUS_NAME_UNRESERVED`; a colliding
reservation raises `AKERR_STATUS_RANGE_OVERLAP`, with a message naming the real
owner. See [UPGRADING.md](UPGRADING.md) for the full list of statuses these two
functions raise, the capacity limits and how to raise them, and thread-safety
rules.
# Thread safety
The library is thread safe as built by default. Every entry point may be called
from any thread at any time, including the first one: `akerr_init()` runs
exactly once no matter how many threads race into it.
What that covers:
* **The error pool.** Finding a free slot in `AKERR_ARRAY_ERROR` and taking its
reference is one operation under a lock, so two threads can never be handed
the same context. A context is then owned by the thread that raised it, all
the way through `CATCH`, `HANDLE`, and release.
* **The status registry.** Reservations and name registrations are serialized
against each other and against lookups. Two threads reserving the same range
cannot both win — exactly one gets `NULL` and the other gets
`AKERR_STATUS_RANGE_OVERLAP` naming the winner.
* **Per-thread state.** The context behind `IGNORE` (`__akerr_last_ignored`) and
the last-ditch context used to report `akerr_release_error(NULL)` are
thread-local, so one thread's ignored error is never another's.
What it does not cover, and cannot:
* **Sharing one error context between threads.** The library hands a context to
one thread; passing it to another is your synchronization to do.
* **`akerr_log_method` and `akerr_handler_unhandled_error`.** Set them during
startup, before you spawn threads. They are read on every error and the
library never writes them after initialization, so setting one while other
threads are raising errors is a race the library cannot mediate.
* **Renaming a status that other threads are looking up.**
`akerr_name_for_status(status, NULL)` returns a pointer into the registry,
valid for the life of the process; registering a *second* name for the same
status overwrites that buffer in place. Register names during initialization.
Registering a *new* status concurrently is fine.
* **Which unhandled error terminates the process.** An error that reaches
`FINISH_NORETURN` unhandled prints its stack trace and calls
`akerr_handler_unhandled_error`, which by default calls `akerr_exit()`. Each
thread's trace is whole — the buffer belongs to its context, and each line is
one call to `akerr_log_method` — but if two threads get there at the same
instant, both traces print and the exit status is whichever one won.
There is one lock, it is recursive, and it covers both the pool and the
registry. That means error construction is serialized across threads: raising an
error is the exceptional path, and correctness there is worth more than
throughput. A program that raises errors on its hot path will feel it.
`AKERR_THREAD_SAFE` in the generated header is `1` for a thread-safe build, so a
consumer can check what it linked against:
```c
#if AKERR_THREAD_SAFE
/* ... start worker threads ... */
#endif
```
## Building single threaded
The threading backend is chosen when libakerror is configured. `auto` (the
default) takes POSIX threads, and **fails the configure** if it cannot find
them rather than quietly producing a library that says it is thread safe and is
not. To mean it:
```sh
cmake -S . -B build -DAKERR_THREADS=none
```
That builds with no locking and no thread-local storage, stamps
`AKERR_THREAD_SAFE 0` into the header, and calling the library from more than
one thread is then undefined.
## Proving it
The thread tests (`tests/err_threads_*.c`) assert the properties above directly:
exclusive ownership of pool slots, exactly one winner for a contested range,
every registered name readable back. They run in the normal suite. The run that
proves the *absence* of a data race underneath them is ThreadSanitizer:
```sh
scripts/thread_test.sh
```
which configures `build/tsan` with `-DAKERR_SANITIZE=thread`, builds the library
and every test with it, and runs the suite. Under that build a sanitizer report
fails the test rather than being printed and passed over.
# Installation
@@ -43,11 +214,39 @@ cmake --build build
cmake --install build
```
The library depends on `stdlib` and on POSIX threads. Both are optional at the
cost of some functionality — see [docs/building.md](docs/building.md) for
`-DAKERR_USE_STDLIB=OFF` and
[docs/thread-safety.md](docs/thread-safety.md#building-single-threaded) for
`-DAKERR_THREADS=none`.
## Templating and autogenerated code
The build process relies upon `scripts/generrno.sh` which performs the following:
1. Executes `errno --list` and gathers up the output
1. Templates `include/akerror.tmpl.h` into `include/akerror.h` to set the `AKERR_LAST_ERRNO_VALUE` equal to the highest integer defined by `errno`
2. Generates `src/errno.c` which contains a function called by `akerr_init` which initializes all of the status names for the previously defined values of `errno`.
## Dependencies
This library depends upon `stdlib`, and upon POSIX threads unless it is built
with `-DAKERR_THREADS=none` (see "Thread safety" above). If you don't want to link against stdlib, you must modify the library code to include headers and link against a library that provides the following:
- `memset` function
- `strncpy` function
- `strlen` function
- `strcmp` function
- `sprintf` function
- `exit` function
- `bool` type
- `NULL` type
- `INT_MAX` constant
- `PATH_MAX` constant
... then you can compile it thusly:
```
cmake -S . -B build -DAKERR_USE_STDLIB=OFF
cmake --build build
cmake --install build
```
# Using the library
## Setting up your project
@@ -86,93 +285,368 @@ add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
target_link_libraries(YOUR_PROJECT PRIVATE akerror::akerror)
```
# Quickstart
A function that can fail returns an `akerr_ErrorContext *` instead of a value,
and moves its real output to a pointer parameter. `AKERR_NOIGNORE` makes the
compiler complain if a caller throws that return away.
## (Optional) Configuring the logging function
The default logging function (used for logging stack traces on failure) defaults to a wrapper that calls `fprintf(stderr, f, ...)`. If you want to override this behavior, then set the error handler to a function with a printf-style signature:
```
void my_logger(const char *fmt, ...)
{
/* ... do something */
}
/* set your custom error handler */
akerr_log_method = &my_logger;
/* proceed to use the library */
```
## Setting Up the Error Context
Before you can use any of these macros you must set up an error context inside of the current scope.
```c
#include <akerror.h>
#include <stdio.h>
/* Fails with a message; the caller finds out what and where. */
static akerr_ErrorContext AKERR_NOIGNORE *open_config(const char *path, FILE **dest)
{
PREPARE_ERROR(errctx);
```
FAIL_ZERO_RETURN(errctx, (path != NULL), AKERR_NULLPOINTER,
"no config path was given");
This will create a akerr_ErrorContext structure inside of the current scope named `errctx` and initialize it. This structure is used for all operations of the library within the current scope. Attempting to use the library in a given scope before calling this will result in compile-time errors.
*dest = fopen(path, "r");
FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKERR_IO,
"could not open %s", path);
## Attempting an Operation
SUCCEED_RETURN(errctx);
}
int main(int argc, char **argv)
{
FILE *config = NULL;
PREPARE_ERROR(errctx);
```c
ATTEMPT {
FAIL_ZERO_BREAK(errctx, (argc == 2), AKERR_VALUE,
"usage: %s <config>", argv[0]);
CATCH(errctx, open_config(argv[1], &config));
/* ... read the config ... */
// ... code
} CLEANUP {
/* Runs whether or not anything failed. */
if ( config != NULL ) {
fclose(config);
}
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_VALUE) {
/* A usage error is ours to handle, so handle it and carry on. */
} FINISH_NORETURN(errctx);
} FINISH(errctx, true)
```
return 0;
`ATTEMPT { ... }` is the block within which you will perform operations which may cause errors that need to be caught. See "Capturing errors", below.
`CLEANUP { ... }` is the block within which you will perform any code which MUST be executed REGARDLESS of whether or not errors were thrown. Closing open file handles, or releasing memory, for example.
`PROCESS(errctx) { ... }` is the block within which you will handle any errors that were caught inside of the `ATTEMPT` block. See "Handling Errors" below.
`FINISH(errctx, true)` terminates the attempt operation. The `FINISH` macro takes two arguments: the name of the akerr_ErrorContext, and a boolean regarding whether or not to pass unhandled errors up to the calling function. Unless you are inside of your `main()` method, this should be true. Inside of your `main()` method, call `FINISH_NORExbTURN(errctx)` instead.
# Capturing errors
Inside of an `ATTEMPT` block, any operation which could generate or represent an error should be wrapped in one of several macros.
## Capturing errors from functions which return akerr_ErrorContext *
For functions that return `akerr_ErrorContext *`, you should use the `CATCH` macro.
```c
ATTEMPT {
CATCH(errctx, errorGeneratingFunction())
} // ...
```
This will assign the return value of the function in question to the akerr_ErrorContext previously prepared in the current scope. If the function returns an akerr_ErrorContext that indicates any type of error, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
(One caveat: because this exit is implemented with a C `break`, `CATCH` must not be used inside a loop within the `ATTEMPT` block — see the section "Important: do not use CATCH or FAIL_*_BREAK inside a loop" below.)
## Setting errors from functions or expressions returning integer
For functions that return integer, such as logical comparisons or most standard library functions, use the `FAIL_ZERO_BREAK` and `FAIL_NONZERO_BREAK` macros. These macros allow you to capture an integer return code from an expression or function and set an error code in the current context based off that return.
Here is an example of checking for a NULL pointer
```c
ATTEMPT {
FAIL_ZERO_BREAK(errctx, (somePointer == NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
} // ...
```
Here is an example of checking for two strings that are not equal
```c
ATTEMPT {
FAIL_NONZERO_BREAK(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")
} // ...
```
When either of these two macros are used, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
## Important: do not use CATCH or FAIL_*_BREAK inside a loop
`CATCH`, `FAIL_ZERO_BREAK`, `FAIL_NONZERO_BREAK`, and `FAIL_BREAK` leave the `ATTEMPT` block by executing a C `break` statement. In C, `break` only exits the *innermost* enclosing `for`, `while`, `do`, or `switch`. Therefore **these macros must not be used inside a loop (or a nested `switch`) that is itself inside an `ATTEMPT` block.** If you do, the `break` escapes only the loop — not the `ATTEMPT` — and the rest of the `ATTEMPT` body then runs with an error already pending:
```c
ATTEMPT {
for ( int i = 0; i < n; i++ ) {
CATCH(errctx, process(items[i])); // WRONG: break exits the for loop, not the ATTEMPT
}
// ... this code still executes, with errctx already in an error state ...
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
```
Note that moving the loop into a helper function does **not** fix this on its own — if the helper still wraps the loop in an `ATTEMPT` and uses `CATCH`/`FAIL_*_BREAK` inside it, it has the exact same problem. The fix is to iterate with `return`-based macros, which are unaffected by loop nesting. Use one of the two patterns below.
**Pattern 1 — use `PASS` (or a `FAIL_*_RETURN` macro) inside the loop.** These exit the *enclosing function* with a `return` rather than a `break`, so loop nesting is irrelevant. Use this when the loop should stop and propagate on the first error:
```c
akerr_ErrorContext AKERR_NOIGNORE *process_all(Item *items, int n)
{
PREPARE_ERROR(errctx);
for ( int i = 0; i < n; i++ ) {
PASS(errctx, process(items[i])); // returns from process_all on the first error
}
SUCCEED_RETURN(errctx);
}
```
`ATTEMPT` is where work that can fail goes. `CLEANUP` always runs. `PROCESS`
opens the handler section, and each `HANDLE` claims one status. Anything no
`HANDLE` claims is still an error when it reaches `FINISH`: inside a function,
`FINISH(errctx, true)` returns it to your caller; at the top, as above,
`FINISH_NORETURN(errctx)` prints the stack trace and ends the process. You do
not need to call `akerr_init()` — every entry point does it for you.
**Pattern 2 — move the loop into a helper and `CATCH` the single call.** When you need a `CLEANUP` block or want to `HANDLE` the error locally, put the loop in its own `akerr_ErrorContext *`-returning function (written per Pattern 1) and `CATCH` that one call. The `CATCH` is then not inside a loop, so its `break` scopes to the `ATTEMPT` correctly:
Three things worth knowing before you write much more than that:
```c
ATTEMPT {
CATCH(errctx, process_all(items, n)); // a single CATCH, not looped
} CLEANUP {
// ... always runs ...
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_VALUE) {
// ... handle a failure from any iteration ...
} FINISH(errctx, true);
```
* [The full macro reference](docs/usage.md), including `PASS` for errors you
cannot do anything about, and `HANDLE_GROUP` for several statuses that fail
the same way.
* [Never use `CATCH` or a `FAIL_*_BREAK` macro inside a loop](docs/usage.md#important-do-not-use-catch-or-fail__break-inside-a-loop).
They leave the `ATTEMPT` with a C `break`, which only escapes the innermost
loop. This is the first thing that bites people.
* [Never call `exit()` with a status. Call `akerr_exit()`](docs/exit-status.md).
An exit status is a byte and every consumer status starts at 256, so `exit()`
truncates status 256 to 0 and reports success.
# Passing errors
# Thread safety
Sometimes you can't actually do anything about the errors that come out of a given method, but you want that error to be propagated back up the call chain, and to be properly reported. If this is your goal, you can avoid using a `ATTEMPT ... FINISH` block, and simply use the `PASS` macro.
The library is thread safe as built by default: every entry point may be called
from any thread at any time, and an error context may be handed from one thread
to another and released there. There is one recursive lock covering the pool and
the registry, so error construction is serialized — a program that raises errors
on its hot path will feel it. See [docs/thread-safety.md](docs/thread-safety.md)
for what that covers, what it cannot, and the handoff pattern.
```
PREPARE_ERROR(e);
PASS(e, some_method_that_may_fail());
SUCCEED_RETURN(e);
```
# Upgrading
This does the same thing as this, but with less code:
2.0.1 fixes an unhandled error killing the process and still reporting success:
the exit code was the status truncated to a byte, and every consumer status
starts at 256. Use `akerr_exit()` instead of `exit()` — see
[docs/exit-status.md](docs/exit-status.md). No ABI break.
```
PREPARE_ERROR(e);
ATTEMPT {
CATCH(e, some_method_that_may_fail());
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
```
# Handling errors
Inside of the `PROCESS { ... }` block, you must handle any errors that occurred during the `ATTEMPT { ... }` block. You do this with `HANDLE`, `HANDLE_GROUP`, and `HANDLE_DEFAULT`.
## Handling a specific error with HANDLE
In order to handle a specific error code, use the `HANDLE` macro.
```c
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
// Something is complaining about a null pointer error. Do something about it.
} // ...
```
## Handling a group of errors with HANDLE_GROUP
In order to handle a group of related errors that all require the same failure behavior, use `HANDLE` followed by `HANDLE_GROUP`. For example, to handle a scenario where an IO error, key error, and index error all need to be handled the same way:
```c
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_IO) {
} HANDLE_GROUP(errctx, AKERR_KEY) {
} HANDLE_GROUP(errctx, AKERR_INDEX) {
// error handling code goes here
}
```
This creates a fallthrough mechanism where all 3 errors get the same error handling code. Note that while the cases fall through, you can still (if desired) put some code specific to each error in that error's `HANDLE` or `HANDLE_GROUP` block; but this is not required, only the final handler needs to get any code.
The fallthrough behavior stops as soon as another `HANDLE` macro is encountered. For example, in this example, `AKERR_IO`, `AKERR_KEY` and `AKERR_INDEX` are all handled as a group, but `AKERR_RELATIONSHIP` is not.
```c
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_IO) {
} HANDLE_GROUP(errctx, AKERR_KEY) {
} HANDLE_GROUP(errctx, AKERR_INDEX) {
// This code handles 3 error cases
} HANDLE(errctx, AKERR_RELATIONSHIP) {
// This code handles 1 error case
}
```
# Returning success or failure from functions returning akerr_ErrorContext *
If at all possible, when using this library, your functiions should return `akerr_ErrorContext *`. When returning from such functions, you should use the `SUCCEED_RETURN` and `FAIL_RETURN` macros.
## SUCCEED_RETURN
This macro is used when your function has reached the end of its happy code path and is prepared to exit successfully. This sets the akerr_ErrorContext to a successful state and exits the function.
```c
PREPARE_ERROR(errctx);
ATTEMPT {
// ... stuff
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
```
## FAIL_RETURN
If the code path in the current function reaches a state wherein an error must be set and the function must return early, you can use `FAIL_RETURN` to accomplish this. Note that this should not be used inside of an `ATTEMPT { ... }` block; this immediately exits the function, preventing a `CLEANUP { ... }` block from executing. This can be safely used from inside of a `CLEANUP` or `PROCESS` block, or from anywhere within the function not inside of an `ATTEMPT { ... }` block.
The function allows you to provide printf-style variable arguments to provide a meaningful failure message.
```c
PREPARE_ERROR(errctx);
FAIL_RETURN(AKERR_BEHAVIOR, "Something went horribly wrong!")
```
## Conditionally failing and returning
In addition to `FAIL_RETURN` you can also test for zero or non-zero conditions, set an error, and return from the function immediately. Use the `FAIL_ZERO_RETURN` and `FAIL_NONZERO_RETURN` macros for this. These macros can be used anywhere that `FAIL_RETURN` can be used.
```c
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (somePointer == NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
```
```c
PREPARE_ERROR(errctx);
FAIL_NONZERO_RETURN(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")
```
# Uncaught errors
## Misbehaving methods
Any function which returns `akerr_ErrorContext *` and completes successfully MUST call `SUCCEED_RETURN(errctx)`. Failure to do this may result in an invalid `akerr_ErrorContext *` being returned, which will cause an `AKERR_BEHAVIOR` error to be triggered from your code.
## Ensuring that all error codes are captured
Any function which returns `akerr_ErrorContext *` should also be marked with `AKERROR_NOIGNORE`.
```c
akerr_ErrorContext AKERROR_NOIGNORE *f(...);
```
This will cause a compile-time error if the return value of such a function is not used. "Used" here means assigned to a variable - it does not necessarily mean that the value is checked. However assuming that such functions are called inside of `ATTEMPT { ... }` blocks, it is safe to assume that such returns will be caught with `CATCH(...)`; therefore this error is a generally effective safeguard against careless coding where errors are not checked.
Beware that `AKERROR_NOIGNORE` is not a failsafe - it implements the `warn_unused_result` mechanic. By design users may explicitly ignore an error code from a function marked with `warn_unused_result` by explicitly casting the return to `void`.
```c
#define AKERROR_NOIGNORE __attribute__((warn_unused_result))
```
## Stack Traces
Whenever an error is captured using the `FAIL_*` or `CATCH` methods, and is unhandled such that it manages to propagate all the way to the top of the caller stack without being managed, the last `FINISH` macro to touch the error will trigger a stack trace and kill the program.
Consider the `tests/err_trace.c` program which intentionally triggers this behavior. It produces output like this:
```
tests/err_trace.c:func2:7: 1 (Null Pointer Error) : This is a failure in func2
tests/err_trace.c:func2:10
tests/err_trace.c:func1:18: Detected error 0 from array (refcount 1)
tests/err_trace.c:func1:18
tests/err_trace.c:func1:21
tests/err_trace.c:main:30: Detected error 0 from array (refcount 1)
tests/err_trace.c:main:30
tests/err_trace.c:main:33: Unhandled Error 1 (Null Pointer Error): This is a failure in func2
```
From bottom to top, we have:
* The last line printed is the `FINISH` macro call that triggered the stacktrace.
* Above that, the `CATCH()` inside of `main()` which caught the exception from `func1()` but did not handle it
* Above that, a statement that the error was detected in the `CATCH()` statement at the same line
* Above that, the `FINISH()` macro in the `func1` method which detected the presence of an unhandled error and returned it up the calling stack
* Above that, the `CATCH()` macro in the `func1` method which caught the error coming out of `func2()`
* Above that, a statement that the error was detected in the `CATCH()` statement at the same line
* Above that, the `FINISH()` macro in `func2()` which detected an unhandled error and passed it out of the function
* Above that, a reference to the line where the `FAIL()` macro set the error code and provided the message which is printed here
## Exit status
**Never call `exit()` with an akerr status. Call `akerr_exit()`.**
```c
void akerr_exit(int status);
```
This applies everywhere you are leaving the process on account of a status, not
just in an unhandled-error handler: a CLI's top-level `HANDLE` block, an
initialization routine that cannot continue, a `main()` that ends by reporting
the status it finished with. One function owns the mapping so that a given
status produces the same exit code no matter which of your exits it left by.
The mapping exists because a process exit status is one byte wide. `exit()`
accepts an `int` and the kernel keeps the low 8 bits of it — `_exit()`,
`_Exit()`, `quick_exit()` and the raw `exit_group` syscall all behave
identically, and even `waitid()`, whose `si_status` is a full `int`, sees the
truncated value because the truncation happened before the parent looked. There
is no wider `exit()` to reach for.
That leaves 0 through 255 as the only statuses an exit code can carry, and
consumer statuses start at `AKERR_FIRST_CONSUMER_STATUS` (256) — so *no* consumer
status can be an exit code:
```
status exit code
0 0 (success)
1 .. 255 the status
negative, or > 255 AKERR_EXIT_STATUS_UNREPRESENTABLE (125)
```
Passing the low byte instead would have made status 256 exit 0 and report
success to the shell, and status 300 exit 44 — an unrelated error's code. 125 is
the conventional "the tool itself failed" status; 126, 127 and 128+*n* already
belong to the shell.
Status 0 exits 0, because 0 is this library's success status. That is not a hole
in the rule that an unhandled error never exits 0: `PROCESS` opens with `case
0`, which marks a zero status handled, so a successful context cannot reach
`FINISH_NORETURN`'s call to the handler at all.
Above 255, the exit code tells you the process died of an error, not which one.
125 is inside the library's reserved band and so is also some host's `errno`, and
every status above 255 collapses onto it. **The stack trace is what identifies
the error** — it is printed before the handler runs, carrying the status at full
width along with its registered name.
### Replacing the handler
After the trace is printed, `FINISH_NORETURN` calls
`akerr_handler_unhandled_error`. The default implementation,
`akerr_default_handler_unhandled_error()`, hands `errctx->status` to
`akerr_exit()` (a NULL context exits 1). Replace it if you need something else
to happen first — a core dump, a crash reporter, a flush — and finish by calling
`akerr_exit()` so the exit code still means what it means everywhere else:
```c
static void mylib_handler(akerr_ErrorContext *e)
{
mylib_flush_telemetry();
if ( e == NULL ) {
akerr_exit(AKERR_API);
}
akerr_exit(e->status);
}
akerr_handler_unhandled_error = &mylib_handler;
```
`akerr_exit()` is declared `AKERR_NORETURN`, so the compiler knows a handler
ending in one of those calls is complete rather than falling off the end.
Set the handler once, before you start any threads. `tests/err_custom_handler.c`
installs one that does not exit at all, which is how the test suite asserts on
unhandled errors without dying.
2.0.0 makes the library thread safe. That is an ABI break — `__akerr_last_ignored`
became thread-local storage and the pool now takes its own reference — so
everything built against a 1.x header must be rebuilt. 1.0.0 replaced the
consumer-sized status-name array with a private, ownership-enforced registry.
See [UPGRADING.md](UPGRADING.md) for all three, what was removed, how to migrate,
the capacity limits and how to raise them, and the thread-safety rules.

191
TODO.md
View File

@@ -1,103 +1,122 @@
# Record
# TODO
**Outstanding work is in the issue tracker, not in this file:**
<https://source.starfort.tech/andrew/libakerror/issues>
Working notes for `libakerror`. Outstanding items only.
Issues are labelled by kind and blast radius, and milestoned by what they can land
in: `2.0.x` for anything that breaks no ABI, `2.1.0` for additive surface, `3.0.0`
for the handler-ladder rewrite and the contract changes that go with it. Everything
filed carries `status::grooming` until it has been through grooming.
## 1. Only ThreadSanitizer is wired into CI, not ASan/UBSan
What stays here is the reasoning a tracker has no place for.
`AKERR_SANITIZE` builds the library and the tests with any sanitizer list, and
CI runs `-DAKERR_SANITIZE=thread` through `scripts/thread_test.sh`. Nothing runs
`address,undefined` yet, and that is the one that covers the original
motivation: mutation testing caught an out-of-bounds probe in the status-name
hash table that the suite could not, because the failure mode was a write into
adjacent BSS, which does not crash. Sharpening one test closed that instance;
ASan would catch the whole class regardless of how sharp the assertions are.
## Three defects were recorded only downstream
The machinery is in place — this is one more job in
`.gitea/workflows/ci.yaml` running
`cmake -S . -B build/asan -DAKERR_SANITIZE=address,undefined`. Left separate
because ASan and TSan cannot be combined in one build.
Found while moving this file and `libakstdlib`'s into their trackers. Each is a
defect **in this library**, each was written down in a *consumer's* TODO file, and
none had an entry here:
## 2. `HANDLE`-level status aliasing is still undetectable
| Issue | What it is | What it costs downstream |
|---|---|---|
| #14 | `IGNORE()` logs a context and never releases it | `libakstdlib`'s `aksl_tree_iterate` open-codes log-then-release by hand; every `IGNORE()` in `libakgl`'s `CLEANUP` blocks is a leaked slot |
| #15 | The `coverage` target is not namespaced when embedded, though `mutation` is | `libakstdlib` shadows `add_custom_target` to embed this library at all |
| #16 | No `akerrorConfigVersion.cmake` is installed | No consumer can ask `find_dependency(akerror)` for a version floor |
Two components can compile the same integer into a `case` label without ever
reserving a range or registering a name, and nothing sees it. Ownership
enforcement covers *naming*, which is the part the library mediates; the `case`
label never reaches it.
**That is the finding worth keeping, more than the three defects.** A library whose
consumers record its defects in their own files has no way to see them: each
consumer knows one, nobody sees the pattern, and the workaround gets written three
times. The tracker is now the place, and a consumer filing upstream costs them one
issue instead of one workaround.
Closing this needs the `if`/`else if` handler ladder — rewriting
`PROCESS`/`HANDLE`/`HANDLE_GROUP`/`HANDLE_DEFAULT`/`FINISH` so status matching
is not restricted to integer constant expressions. That would also allow
matching on ranges or predicates, and would let a handler resolve a code through
its owner. It touches the most load-bearing code in the library and every
consumer's error handling at once, so it wants its own change.
## Why the handler ladder is a `3.0.0` change and not a patch
Note it would *not* by itself fix the "don't use `CATCH` or `FAIL_*_BREAK`
inside a loop" hazard: that comes from exiting via `break`, not from `switch`.
`PROCESS`/`HANDLE`/`HANDLE_GROUP`/`HANDLE_DEFAULT`/`FINISH` compile to a `switch`,
which restricts status matching to integer constant expressions. That is what makes
`HANDLE`-level aliasing undetectable (#4): two components can compile the same
integer into a `case` label without reserving a range or registering a name, and
**ownership enforcement never sees it, because it covers naming and the `case` label
never reaches the library.**
## 3. No registry introspection
Rewriting it as an `if`/`else if` ladder would allow matching on ranges or
predicates and let a handler resolve a code through its owner. It also touches **the
most load-bearing code in the library and every consumer's error handling at once.**
There is no way to ask who owns a status, or to enumerate reservations. The
"coordinate ranges at the dependency-stack level" advice in UPGRADING.md
therefore has no tooling behind it.
One thing it would *not* fix, recorded so nobody expects it to: the "don't use
`CATCH` or `FAIL_*_BREAK` inside a loop" hazard comes from exiting via `break`, not
from `switch`.
A read-only accessor plus a dump through `akerr_log_method` would let a startup
self-check or a CI job print the whole map for a linked stack. Cheap, additive,
and the natural next step for multi-component adoption.
## Why a copied `akerr_ErrorContext` is a trap
## 4. No way to release a reservation
Recorded because the struct looks copyable and is not, and #11 exists only if a
consumer ever needs it.
A plugin host that `dlopen`s many distinct plugins over a process lifetime
accumulates ranges until the table fills. Reloading the *same* plugin is fine —
an identical repeat by the same owner is idempotent.
- **`stacktracebufptr` is self-referential.** `akerr_ErrorContext c = *src;` leaves
the copy's cursor pointing into the source's buffer, so the copy logs correctly
and then **corrupts a slot it does not own** the first time anything appends.
- **`arrayid` is restored after the wipe** in `akerr_release_error()`
(`src/error.c:395-398`), so a copied id makes the destination **impersonate the
source's slot for the life of the process.**
If it is ever built, the shape takes the destination as a parameter rather than
allocating it: an allocating copy could fail on pool exhaustion, and **reporting
that failure needs a pool slot**, so it would have to abort — a third `exit()` site
in a library that deliberately has two.
## Why validating more inputs lowers branch coverage
Branch coverage on `src/error.c` sits just above its 50% gate, and the gate is set
where it is on purpose.
Every `FAIL_*` site carries about **six** branch outcomes of error-construction
machinery (`ENSURE_ERROR_READY`, `AKERR_STACKTRACE_APPEND`) that only run when that
specific failure fires. **Every `PASS` site around a call that cannot fail carries
about twenty-five.**
So adding a defensive check lowers the ratio by construction. **Before adding one,
expect to add a test that drives it**, as `tests/err_copy_string.c` does.
## Why the mutation score is a floor
`scripts/mutation_test.py` configures each mutant with the default CMake options,
so **a mutant that only breaks under concurrency is judged by a suite running
without ThreadSanitizer.**
Measured: deleting the pool's `akerr_mutex_lock()` call survives the run, **even
though it is a real race.** Rebuilt and run directly, that mutant fails
`tests/err_threads_pool.c` in 4 of 10 runs, and fails under `scripts/thread_test.sh`
in 5 of 5.
**81.2% is therefore a floor for that category, not a verdict.** #10 is the
passthrough that would fix the measurement, and it belongs behind a flag: a
TSan-instrumented suite per mutant costs roughly 6s instead of 0.4s.
## Why a registered name is returned by pointer
## 5. Renaming a status is not safe against a concurrent lookup
`akerr_name_for_status(status, NULL)` returns a pointer into the registry rather
than a copy, **which is what makes it usable from inside `FAIL`** — it needs no
buffer and no error context of its own.
than a copy, which is what makes it usable from inside `FAIL` — it needs no
buffer and no error context of its own. Registering a *second* name for a status
that already has one (`tests/err_name_ownership.c` covers that it is allowed)
overwrites that buffer in place, so a thread reading the name at that moment can
see a torn string. Every other registry operation is serialized; this one cannot
be, because the reader is outside the lock by the time it reads the characters.
The cost is that renaming a status is not safe against a concurrent lookup (#7):
every other registry operation is serialized, and this one cannot be, because **the
reader is outside the lock by the time it reads the characters.** Documented in
`docs/thread-safety.md` and `UPGRADING.md` as "register names during
initialization".
Documented in README.md and UPGRADING.md as "register names during
initialization". Closing it properly means making a registered name immutable —
either refusing a rename outright (a behavior change, and
`tests/err_name_ownership.c` asserts the current contract), or copying names
into a bump-allocated arena and publishing the pointer with a release store, so
a rename allocates new storage instead of rewriting live storage. The arena is
the better answer; it costs a second capacity limit and its exhaustion path.
## 6. Deprecate the two-argument name-registration path
`akerr_name_for_status(status, name)` cannot identify its caller, so it can only
check that *some* reservation covers the status, not that the caller owns it. It
exists for migration. Once consumers have moved to
`akerr_register_status_name()`, make the set path a no-op or remove it and leave
`akerr_name_for_status()` as pure lookup.
## 7. `akerr_init()`'s own reservation failure is untested
`tests/err_library_status_fatal.c` covers the terminal path in
`__akerr_name_library_status()` by naming a status the library does not own. The
band reservation in `akerr_init()` has no such handle: it can only fail in a
build whose tables are too small for the library's own entries, and both sizes
are `PRIVATE` to the library target, so a test executable cannot set them.
Closing it means a second library target built with tiny tables plus a
`WILL_FAIL` test linked against it. Nothing in the CMake does that yet: the
sanitizer and coverage options vary the *flags* of the one library target, not
its compile definitions.
Related: branch coverage on `src/error.c` now sits just above its 50% gate.
Every `FAIL_*` site carries about six branch outcomes of error-construction
machinery (`ENSURE_ERROR_READY`, `AKERR_STACKTRACE_APPEND`) that only run when
that specific failure fires, and every `PASS` site around a call that cannot
fail carries about twenty-five. Validating more inputs therefore lowers the
ratio by construction. Before adding defensive checks, expect to add a test that
drives them, as `tests/err_copy_string.c` does.
## 8. Mutation testing judges concurrency mutants without a sanitizer
`scripts/mutation_test.py` configures each mutant build with the default CMake
options, so a mutant that only breaks under concurrency is judged by a suite
running without ThreadSanitizer. Deleting the pool's `akerr_mutex_lock()` call
survives the run even though it is a real race: rebuilt and run directly, that
mutant fails `tests/err_threads_pool.c` in 4 of 10 runs, and fails under
`scripts/thread_test.sh` in 5 of 5. So 81.2% is a floor for that category, not a
verdict.
Closing it means a `--cmake-arg` passthrough on the harness so the mutant build
can be configured with `-DAKERR_SANITIZE=thread`. The whole run then costs a
TSan-instrumented suite per mutant (roughly 6s instead of 0.4s), so it belongs
behind a flag rather than in the default target or in CI.
## Unrelated pre-existing issues
- The `AKERR_USE_STDLIB=OFF` build does not compile at all: `bool`, `PATH_MAX`
and `NULL` are used unconditionally but only included under the stdlib branch.
The README's dependency list states what a replacement must provide, but the
header still needs its includes untangled for that configuration to work.
- `CMakeLists.txt` sets `main_lib_dest` from `MY_LIBRARY_VERSION`, which is never
defined and never read. Dead line.

View File

@@ -33,8 +33,8 @@ top-level `HANDLE` block, an init routine that cannot continue — so one mappin
covers every exit. It is declared `AKERR_NORETURN`. If you were reading a
consumer status out of `$?`, you were never getting it: read the stack trace,
which carries the status at full width along with its registered name, or
install a handler that maps your own statuses into a byte. See
[docs/exit-status.md](docs/exit-status.md).
install a handler that maps your own statuses into a byte. See "Exit status" in
[README.md](README.md).
No ABI break. The soname stays `libakerror.so.2` and nothing you already call
changed shape. `akerr_exit()` is a new exported symbol, so a consumer that
@@ -95,21 +95,11 @@ Safe from any thread, with no coordination on your part:
* `akerr_name_for_status(status, NULL)` lookups, concurrently with each other
and with registrations of *other* statuses.
* `akerr_init()`, from any number of threads at once.
* **Handing a context to another thread, and releasing it there.** The reference
count is the only field the library reads across threads and it is only ever
touched under the pool lock, so `akerr_release_error()` does not care which
thread checked the slot out. Contexts live in process-global storage, not
thread-local, so one outlives the thread that raised it. What is still yours is
the handoff *itself*: it has to carry a happens-before edge, which any mutex,
condvar, `pthread_join` or acquire/release atomic gives you.
Still yours to coordinate:
* **Two threads in one context at once.** Ownership moves; it does not fork.
Hand a context over and stop touching it — the content is written with no
lock, so the handoff is what publishes it. See "Handing an error to another
thread" in docs/thread-safety.md for the pattern, and for why the queue has
to be bounded.
* **One error context is owned by one thread.** The library hands it to the
thread that raised it. Handing it to another thread is your synchronization.
* **`akerr_log_method` and `akerr_handler_unhandled_error`** are read on every
error and written by nobody but you. Set them during startup, before spawning.
* **Renaming a status while another thread looks it up.**
@@ -138,9 +128,9 @@ library from that thread.
## Proving it
`tests/err_threads_init.c`, `tests/err_threads_pool.c`,
`tests/err_threads_registry.c` and `tests/err_threads_handoff.c` assert the
properties directly and run in the normal suite. The run that proves there is no data race underneath them is
`tests/err_threads_init.c`, `tests/err_threads_pool.c` and
`tests/err_threads_registry.c` assert the properties directly and run in the
normal suite. The run that proves there is no data race underneath them is
ThreadSanitizer:
```sh

View File

@@ -1,48 +0,0 @@
# Library Architecture
## Philosophy of Use
This library has 6 guiding principles:
* Manually checking every possible return code for every possible meaning of that return code is tedious and prone to miss unpredicted failure cases
* Functions should return rich descriptive error contexts, not values
* Uncaught errors should cause program termination with a stacktrace
* Dynamic memory allocation is the source of many errors and should be avoided if possible
* Manipulating the call stack directly is error prone and dangerous
* Declaring, capturing, and reacting to errors should be intuitive and no more difficult than managing return codes
## Lifecycle of an error in the AKError library
TL;DR - `akerr_ErrorContext` objects are filled with error context information and bubbled up through nested control structures until they are handled or reach the top level, where an unhandled error halts program termination with a stack trace
1. At the point where an error occurs, an `akerr_ErrorContext` object is initialized and populated with information regarding the failure
2. The akerr_ErrorContext is returned from the scope where the error was detected
3. The akerr_ErrorContext enters a control structure provided by the AKError library through a series of macros that examine `akerr_ErrorContext` objects as they pass through
4. The control structure checks to see if the `akerr_ErrorContext` has an error set, and if so, if there are any handlers in the current control structure that can handle it
5. If the current control structure can handle the `akerr_ErrorContext`, it does so
6. If the current control structure can not handle the `akerr_ErrorContext`, then the current control structure's cleanup code (if any) is executed, and the `akerr_ErrorContext` object is passed out of the current control structure to the parent control structure
7. Steps 2-6 are repeated through as many control structures as are necessary to reach the first level of the control structure
8. When the first level of the control structure is reached, if the `akerr_ErrorContext` has an error set in it, then the stack trace information in the `akerr_ErrorContext` object is used to print a stack trace using the configured logging function, and program termination is halted
## What is in an Error Context
The Error Context object is a simple object which contains a few things:
* A numeric error code
* The name of the file in which the error occurred
* The name of the function in which the error occurred
* The line number in the file at which the error occurred
* A character buffer containing a message about the error in question
The structure also contains housekeeping information for the library which are of no specific interest to the user. See [include/akerror.tmpl.h](../include/akerror.tmpl.h) for more details.
## What are the control structures
The library is structured around a series of macros that construct `switch` statements that perform logic against an `akerr_ErrorContext` which exists in the current scope and has been initialized. These macros must be assembled in a specific order to produce a syntactically correct `switch` statement which performs correct operations against the `akerr_ErrorContext` to attempt operations, detect failures, perform cleanup operations, handle errors, and then exit a given scope in a success or failure state.
## Functions and Return Codes
This library can catch errors from any function or expression that returns an integer value, or from functions that return `akerr_ErrorContext *`.
Any function which uses the `PREPARE_ERROR` macro should have a return type of `akerr_ErrorContext *`. The macros within this library, when they detect an unhandled error, will attempt to pass up the unhandled error to the context of the previous function in the call stack. This allows for errors to propagate up through the call stack in the same way as exceptions. (For example, if you use traditional C error handling in a call stack of `a() -> b() -> c()`, and `c()` fails because it runs out of memory, `b()` will likely detect that error and return some error to `a()`, but it may or may not return the context of what failed and why. With this, you get that context all the way up in `a()` without knowing anything about `c()`.

View File

@@ -1,63 +0,0 @@
# Building libakerror
The ordinary build is an out-of-tree CMake build, described in
[the README](../README.md#installation). This file covers what the build
generates for you, what it links against, and the configure options that change
either of those.
## Configure options
| Option | Default | What it does |
| ------ | ------- | ------------ |
| `AKERR_THREADS` | `auto` | Threading backend: `auto`, `pthread`, or `none`. `auto` takes POSIX threads and **fails the configure** if it cannot find them. See [Building single threaded](thread-safety.md#building-single-threaded). |
| `AKERR_USE_STDLIB` | `ON` | Link against the C standard library. See [Dependencies](#dependencies) for what you must supply instead when this is `OFF`. |
| `AKERR_STATUS_NAME_SLOTS` | `4096` | Slots in the status-name table; 75% of it is usable. |
| `AKERR_MAX_RESERVED_STATUS_RANGES` | `64` | How many status ranges may be reserved in one process. |
| `AKERR_SANITIZE` | *(empty)* | Sanitizer list applied to the library and the tests, e.g. `thread` or `address,undefined`. |
| `AKERR_COVERAGE` | `OFF` | Instrument the library with gcov counters. |
The two capacity options are applied `PRIVATE`: the tables live entirely in
`src/error.c`, so raising them never changes anything a consumer can see. See
[UPGRADING.md](../UPGRADING.md) for what happens when you exhaust them.
## Templating and autogenerated code
The build process relies upon `scripts/generrno.sh` which performs the following:
1. Executes `errno --list` and gathers up the output
1. Templates `include/akerror.tmpl.h` into `include/akerror.h` to set the `AKERR_LAST_ERRNO_VALUE` equal to the highest integer defined by `errno`
2. Generates `src/errno.c` which contains a function called by `akerr_init` which initializes all of the status names for the previously defined values of `errno`.
Neither output is meant to be edited. Change the template or the generator.
## Dependencies
This library depends upon `stdlib`, and upon POSIX threads unless it is built
with `-DAKERR_THREADS=none` (see [Thread safety](thread-safety.md)). If you don't want to link against stdlib, you must modify the library code to include headers and link against a library that provides the following:
- `memset` function
- `strncpy` function
- `strlen` function
- `strcmp` function
- `sprintf` function
- `exit` function
- `bool` type
- `NULL` type
- `INT_MAX` constant
- `PATH_MAX` constant
... then you can compile it thusly:
```
cmake -S . -B build -DAKERR_USE_STDLIB=OFF
cmake --build build
cmake --install build
```
**Known defect:** that configuration does not currently compile. `bool`,
`PATH_MAX` and `NULL` are used unconditionally but only included under the
stdlib branch, so the header's includes need untangling before
`-DAKERR_USE_STDLIB=OFF` builds. The list above still states what a replacement
must provide. That configuration does not currently compile -- `bool`, `PATH_MAX` and `NULL`
are used unconditionally but included only under the stdlib branch. See
[issue #12](https://source.starfort.tech/andrew/libakerror/issues/12).

View File

@@ -1,76 +0,0 @@
# Exit status
**Never call `exit()` with an akerr status. Call `akerr_exit()`.**
```c
void akerr_exit(int status);
```
This applies everywhere you are leaving the process on account of a status, not
just in an unhandled-error handler: a CLI's top-level `HANDLE` block, an
initialization routine that cannot continue, a `main()` that ends by reporting
the status it finished with. One function owns the mapping so that a given
status produces the same exit code no matter which of your exits it left by.
The mapping exists because a process exit status is one byte wide. `exit()`
accepts an `int` and the kernel keeps the low 8 bits of it — `_exit()`,
`_Exit()`, `quick_exit()` and the raw `exit_group` syscall all behave
identically, and even `waitid()`, whose `si_status` is a full `int`, sees the
truncated value because the truncation happened before the parent looked. There
is no wider `exit()` to reach for.
That leaves 0 through 255 as the only statuses an exit code can carry, and
consumer statuses start at `AKERR_FIRST_CONSUMER_STATUS` (256) — so *no* consumer
status can be an exit code:
```
status exit code
0 0 (success)
1 .. 255 the status
negative, or > 255 AKERR_EXIT_STATUS_UNREPRESENTABLE (125)
```
Passing the low byte instead would have made status 256 exit 0 and report
success to the shell, and status 300 exit 44 — an unrelated error's code. 125 is
the conventional "the tool itself failed" status; 126, 127 and 128+*n* already
belong to the shell.
Status 0 exits 0, because 0 is this library's success status. That is not a hole
in the rule that an unhandled error never exits 0: `PROCESS` opens with `case
0`, which marks a zero status handled, so a successful context cannot reach
`FINISH_NORETURN`'s call to the handler at all.
Above 255, the exit code tells you the process died of an error, not which one.
125 is inside the library's reserved band and so is also some host's `errno`, and
every status above 255 collapses onto it. **The stack trace is what identifies
the error** — it is printed before the handler runs, carrying the status at full
width along with its registered name.
## Replacing the handler
After the trace is printed, `FINISH_NORETURN` calls
`akerr_handler_unhandled_error`. The default implementation,
`akerr_default_handler_unhandled_error()`, hands `errctx->status` to
`akerr_exit()` (a NULL context exits 1). Replace it if you need something else
to happen first — a core dump, a crash reporter, a flush — and finish by calling
`akerr_exit()` so the exit code still means what it means everywhere else:
```c
static void mylib_handler(akerr_ErrorContext *e)
{
mylib_flush_telemetry();
if ( e == NULL ) {
akerr_exit(AKERR_API);
}
akerr_exit(e->status);
}
akerr_handler_unhandled_error = &mylib_handler;
```
`akerr_exit()` is declared `AKERR_NORETURN`, so the compiler knows a handler
ending in one of those calls is complete rather than falling off the end.
Set the handler once, before you start any threads. `tests/err_custom_handler.c`
installs one that does not exit at all, which is how the test suite asserts on
unhandled errors without dying.

View File

@@ -1,46 +0,0 @@
# Error codes
The library uses integer values to specify error codes inside of its context. These integer return codes are defined in `akerror.h` in the form of `AKERR_xxxxx` where `xxxxx` is the name of the error code in question. See `akerror.h` for a list of defined errors and their descriptions.
You can define additional error types as integer constants. Values 0 through 255
are reserved by libakerror (the host's errno values plus the `AKERR_*` codes);
consumers allocate codes starting at `AKERR_FIRST_CONSUMER_STATUS` (256). Status
names are stored sparsely, so any `int` is a legal status and no compile
definition is needed to use large values. Note that no consumer status can be a
process exit code — see [Exit status](exit-status.md).
Every library that may coexist in one process must reserve its range during
initialization. `akerr_reserve_status_range()` and
`akerr_register_status_name()` report failure the way everything else in this
library does — they return `akerr_ErrorContext *`, and they are marked
`AKERR_NOIGNORE` — so a collision is an exception you can `CATCH`, `HANDLE`, or
`PASS` up out of your initialization:
```c
#define MYLIB_OWNER "my-library"
akerr_ErrorContext AKERR_NOIGNORE *mylib_init(void)
{
PREPARE_ERROR(errctx);
/* Another component owning part of the range propagates to our caller. */
PASS(errctx, akerr_reserve_status_range(256, 16, MYLIB_OWNER));
/* Then name each code, quoting the owner you reserved with. */
PASS(errctx, akerr_register_status_name(MYLIB_OWNER, 256,
"Some Error Code Description"));
SUCCEED_RETURN(errctx);
}
```
Reservations are process-local, fixed-capacity, and idempotent when the same
owner repeats the exact same range. They preserve compile-time integer constants
so `HANDLE` still works, since `case` labels require them.
Naming a status outside your reservation raises `AKERR_STATUS_NAME_FOREIGN`, and
naming one nobody reserved raises `AKERR_STATUS_NAME_UNRESERVED`; a colliding
reservation raises `AKERR_STATUS_RANGE_OVERLAP`, with a message naming the real
owner. See [UPGRADING.md](../UPGRADING.md) for the full list of statuses these two
functions raise, the capacity limits and how to raise them, and thread-safety
rules.

View File

@@ -1,225 +0,0 @@
# Thread safety
The library is thread safe as built by default. Every entry point may be called
from any thread at any time, including the first one: `akerr_init()` runs
exactly once no matter how many threads race into it.
What that covers:
* **The error pool.** Finding a free slot in `AKERR_ARRAY_ERROR` and taking its
reference is one operation under a lock, so two threads can never be handed
the same context. A context is then owned by exactly one thread at a time, all
the way through `CATCH`, `HANDLE`, and release.
* **The status registry.** Reservations and name registrations are serialized
against each other and against lookups. Two threads reserving the same range
cannot both win — exactly one gets `NULL` and the other gets
`AKERR_STATUS_RANGE_OVERLAP` naming the winner.
* **Per-thread state.** The context behind `IGNORE` (`__akerr_last_ignored`) and
the last-ditch context used to report `akerr_release_error(NULL)` are
thread-local, so one thread's ignored error is never another's.
* **Handing a context from one thread to another.** A context is not thread
state — it lives in `AKERR_ARRAY_ERROR`, which is process-global — so it
outlives the thread that raised it. The reference count is the only field the
library reads across threads, and it is only ever touched under the pool lock,
so `akerr_release_error()` does not care which thread checked the slot out.
Raise on a worker, queue it, let the worker exit; the collector still has a
whole error with a whole stack trace. See
[Handing an error to another thread](#handing-an-error-to-another-thread).
What it does not cover, and cannot:
* **Two threads inside one context at the same time.** A context has one owner,
and only one. `FAIL` rewrites the message, `HANDLE` rewinds the stack-trace
cursor, and none of that is locked — two threads in one context splice their
messages together and truncate each other's trace, without crashing. Handing a
context *from* one thread *to* another is a different thing, and is supported.
* **`akerr_log_method` and `akerr_handler_unhandled_error`.** Set them during
startup, before you spawn threads. They are read on every error and the
library never writes them after initialization, so setting one while other
threads are raising errors is a race the library cannot mediate.
* **Renaming a status that other threads are looking up.**
`akerr_name_for_status(status, NULL)` returns a pointer into the registry,
valid for the life of the process; registering a *second* name for the same
status overwrites that buffer in place. Register names during initialization.
Registering a *new* status concurrently is fine.
* **Which unhandled error terminates the process.** An error that reaches
`FINISH_NORETURN` unhandled prints its stack trace and calls
`akerr_handler_unhandled_error`, which by default calls `akerr_exit()`. Each
thread's trace is whole — the buffer belongs to its context, and each line is
one call to `akerr_log_method` — but if two threads get there at the same
instant, both traces print and the exit status is whichever one won.
There is one lock, it is recursive, and it covers both the pool and the
registry. That means error construction is serialized across threads: raising an
error is the exceptional path, and correctness there is worth more than
throughput. A program that raises errors on its hot path will feel it.
`AKERR_THREAD_SAFE` in the generated header is `1` for a thread-safe build, so a
consumer can check what it linked against:
```c
#if AKERR_THREAD_SAFE
/* ... start worker threads ... */
#endif
```
## Handing an error to another thread
**Transfer** an error context; never **share** one. One thread owns it at a time,
and ownership moves in a single step: the thread giving it up stops touching it
in the same act that makes it visible to the thread taking it over.
This works because a context is not thread state. It lives in
`AKERR_ARRAY_ERROR`, which is process-global, so a context outlives the thread
that raised it — a worker can raise an error, queue it, and exit, and the
collector still has a whole error with a whole stack trace. Nothing in the
library reads a context through any pointer but the one its current owner handed
in. The one field it reads across threads is the reference count, and that is
only ever touched under the pool lock, so `akerr_release_error()` does not care
which thread checked the slot out. Release it wherever it ended up.
**Always** hand it over through something that synchronizes: a mutex, a
condition variable, `pthread_join`, or an acquire/release atomic. The content of
a context is written with no lock at all — that is deliberate, error
construction should not pay for a lock it does not need — so the handoff itself
is what publishes those writes. Push the pointer through a relaxed atomic or a
plain global and the receiver can read a half-written message, on a machine you
did not test on.
**Never** touch a context after you have given it away. Not a `->status`, not a
log line. The receiver may be inside `HANDLE` rewinding the trace cursor, or
inside `akerr_release_error()` memsetting the slot.
**Release it exactly once.** A context released twice from a stale pointer takes
the refcount-zero branch a second time and wipes the slot again — which by then
holds somebody else's live error. Nothing crashes; a different thread's error
just quietly goes blank.
```c
/* Producer. Owns the context until queue_push() returns, and not after. */
static void report_unit_failure(int unit)
{
PREPARE_ERROR(e);
FAIL(e, MYLIB_UNIT_FAILED, "unit %d stopped responding", unit);
/* The last thing this thread does to it, so the trace records the crossing.
The buffer belongs to the context, so it travels with it. */
AKERR_STACKTRACE_APPEND(e, "%s:%s:%d: queued for the collector\n",
__FILE__, __func__, __LINE__);
queue_push(e); /* takes the queue mutex; `e` is not ours after this */
}
/* Collector. Owns it from the moment queue_pop() returns. */
static void collect_one(akerr_ErrorContext *e)
{
ATTEMPT {
} CLEANUP {
} PROCESS(e) {
} HANDLE(e, MYLIB_UNIT_FAILED) {
restart_unit(e);
} HANDLE_DEFAULT(e) {
LOG_ERROR_WITH_MESSAGE(e, "collector: unrecognized failure");
} FINISH_NORETURN(e);
}
static void *collector(void *unused)
{
akerr_ErrorContext *e;
(void)unused;
while ( (e = queue_pop()) != NULL ) {
collect_one(e);
}
return NULL;
}
```
Four things about the receiving side:
* **Declare a plain pointer, not `PREPARE_ERROR`.** That macro *declares* a
fresh context variable set to `NULL`; it cannot adopt one. For the same
reason, never `CATCH` into the variable holding a received context — `CATCH`
assigns over it, and the slot you were handed is gone.
* **In a `void` helper, use `FINISH_NORETURN`.** `FINISH_LOGIC` decides whether
to propagate at run time, so the compiler still *parses* its
`return __err_context` even when the second argument is the literal `false`.
`FINISH(e, false)` in a function returning void therefore draws
`warning: 'return' with a value, in function returning void` from gcc — a
constraint violation, and a build failure under `-Werror`. To propagate
inside the collector's own call stack, give the
helper an `akerr_ErrorContext *` return and use `FINISH(e, true)` as usual —
just never let an error propagate out of the thread body itself, whose
`void *` return nobody reads. `PASS` has the same problem for the same reason:
in a thread body it compiles, hands the pointer back as a `void *`, and leaks
the slot.
* **`FINISH_NORETURN(e)` on a received context still terminates the process.**
That is right — an unhandled error is unhandled wherever it was raised — but
it is now the collector's thread deciding the exit status, not the raiser's.
* **Give the handler blocks their own function.** `ATTEMPT` is a `switch`, and a
`break` inside one written directly in a loop leaves the `switch`, not the
loop. Same hazard as
[do not use CATCH or FAIL_*_BREAK inside a loop](usage.md#important-do-not-use-catch-or-fail__break-inside-a-loop).
**Always** bound the queue. A queued error is a checked-out pool slot, and there
are `AKERR_MAX_ARRAY_ERROR` (128) of them in the entire process. Size *queue
depth + producers with an error in flight* well under that. When the pool runs
dry the library logs and calls `exit(1)` from inside `FAIL` — there is no slot
left to raise the failure *from*, which is exactly why a collector that stops
draining takes the process with it.
### If you need to keep it as well as report it
There is no copy or retain call, on purpose: a context is a pool slot, and two
owners of one slot is the thing this whole section exists to prevent. So either
read out what you want to keep — `status` is an `int`, `message` and
`stacktracebuf` are ordinary NUL-terminated strings you can `snprintf` into your
own, much smaller, record — or raise two errors and hand one of them over.
**Never** copy an `akerr_ErrorContext` by assignment and keep the copy:
```c
akerr_ErrorContext snapshot = *failed; /* looks fine. is not. */
```
`stacktracebufptr` points into the context's *own* `stacktracebuf`, so after that
assignment `snapshot`'s cursor still points into `failed`'s buffer.
`LOG_ERROR(&snapshot)` reads the array and prints correctly, so it looks healthy
— and then the first `AKERR_STACKTRACE_APPEND(&snapshot, ...)` writes into a
pool slot you no longer own, arbitrarily far from the copy. `arrayid` has the
same shape: it is restored after the wipe, so a copied id makes the destination
impersonate the source's slot forever. And the copy is not a pool address, so
`akerr_valid_error_address()` rejects it, every `CATCH` on it becomes
`AKERR_BADEXC`, and releasing it memsets your own storage while the real slot
stays checked out for the life of the process.
## Building single threaded
The threading backend is chosen when libakerror is configured. `auto` (the
default) takes POSIX threads, and **fails the configure** if it cannot find
them rather than quietly producing a library that says it is thread safe and is
not. To mean it:
```sh
cmake -S . -B build -DAKERR_THREADS=none
```
That builds with no locking and no thread-local storage, stamps
`AKERR_THREAD_SAFE 0` into the header, and calling the library from more than
one thread is then undefined.
## Proving it
The thread tests (`tests/err_threads_*.c`) assert the properties above directly:
exclusive ownership of pool slots, exactly one winner for a contested range,
every registered name readable back, and — in `err_threads_handoff.c` — an error
raised on one thread arriving whole on another and released there. They run in
the normal suite. The run that
proves the *absence* of a data race underneath them is ThreadSanitizer:
```sh
scripts/thread_test.sh
```
which configures `build/tsan` with `-DAKERR_SANITIZE=thread`, builds the library
and every test with it, and runs the suite. Under that build a sanitizer report
fails the test rather than being printed and passed over.

View File

@@ -1,50 +0,0 @@
# Uncaught errors
## Misbehaving methods
Any function which returns `akerr_ErrorContext *` and completes successfully MUST call `SUCCEED_RETURN(errctx)`. Failure to do this may result in an invalid `akerr_ErrorContext *` being returned, which will cause an `AKERR_BEHAVIOR` error to be triggered from your code.
## Ensuring that all error codes are captured
Any function which returns `akerr_ErrorContext *` should also be marked with `AKERR_NOIGNORE`.
```c
akerr_ErrorContext AKERR_NOIGNORE *f(...);
```
This will cause a compile-time error if the return value of such a function is not used. "Used" here means assigned to a variable - it does not necessarily mean that the value is checked. However assuming that such functions are called inside of `ATTEMPT { ... }` blocks, it is safe to assume that such returns will be caught with `CATCH(...)`; therefore this error is a generally effective safeguard against careless coding where errors are not checked.
Beware that `AKERR_NOIGNORE` is not a failsafe - it implements the `warn_unused_result` mechanic. By design users may explicitly ignore an error code from a function marked with `warn_unused_result` by explicitly casting the return to `void`.
```c
#define AKERR_NOIGNORE __attribute__((warn_unused_result))
```
## Stack Traces
Whenever an error is captured using the `FAIL_*` or `CATCH` methods, and is unhandled such that it manages to propagate all the way to the top of the caller stack without being managed, the last `FINISH` macro to touch the error will trigger a stack trace and kill the program.
Consider the `tests/err_trace.c` program which intentionally triggers this behavior. It produces output like this:
```
tests/err_trace.c:func2:7: 1 (Null Pointer Error) : This is a failure in func2
tests/err_trace.c:func2:10
tests/err_trace.c:func1:18: Detected error 0 from array (refcount 1)
tests/err_trace.c:func1:18
tests/err_trace.c:func1:21
tests/err_trace.c:main:30: Detected error 0 from array (refcount 1)
tests/err_trace.c:main:30
tests/err_trace.c:main:33: Unhandled Error 1 (Null Pointer Error): This is a failure in func2
```
From bottom to top, we have:
* The last line printed is the `FINISH` macro call that triggered the stacktrace.
* Above that, the `CATCH()` inside of `main()` which caught the exception from `func1()` but did not handle it
* Above that, a statement that the error was detected in the `CATCH()` statement at the same line
* Above that, the `FINISH()` macro in the `func1` method which detected the presence of an unhandled error and returned it up the calling stack
* Above that, the `CATCH()` macro in the `func1` method which caught the error coming out of `func2()`
* Above that, a statement that the error was detected in the `CATCH()` statement at the same line
* Above that, the `FINISH()` macro in `func2()` which detected an unhandled error and passed it out of the function
* Above that, a reference to the line where the `FAIL()` macro set the error code and provided the message which is printed here

View File

@@ -1,238 +0,0 @@
# Using the library
## (Optional) Configuring the logging function
The default logging function (used for logging stack traces on failure) defaults to a wrapper that calls `fprintf(stderr, f, ...)`. If you want to override this behavior, then set the error handler to a function with a printf-style signature:
```
void my_logger(const char *fmt, ...)
{
/* ... do something */
}
/* set your custom error handler */
akerr_log_method = &my_logger;
/* proceed to use the library */
```
## Setting Up the Error Context
Before you can use any of these macros you must set up an error context inside of the current scope.
```c
PREPARE_ERROR(errctx);
```
This will create a akerr_ErrorContext structure inside of the current scope named `errctx` and initialize it. This structure is used for all operations of the library within the current scope. Attempting to use the library in a given scope before calling this will result in compile-time errors.
## Attempting an Operation
```c
ATTEMPT {
// ... code
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true)
```
`ATTEMPT { ... }` is the block within which you will perform operations which may cause errors that need to be caught. See "Capturing errors", below.
`CLEANUP { ... }` is the block within which you will perform any code which MUST be executed REGARDLESS of whether or not errors were thrown. Closing open file handles, or releasing memory, for example.
`PROCESS(errctx) { ... }` is the block within which you will handle any errors that were caught inside of the `ATTEMPT` block. See "Handling Errors" below.
`FINISH(errctx, true)` terminates the attempt operation. The `FINISH` macro takes two arguments: the name of the akerr_ErrorContext, and a boolean regarding whether or not to pass unhandled errors up to the calling function. Unless you are inside of your `main()` method, this should be true. Inside of your `main()` method, call `FINISH_NORETURN(errctx)` instead.
## Capturing errors
Inside of an `ATTEMPT` block, any operation which could generate or represent an error should be wrapped in one of several macros.
### Capturing errors from functions which return akerr_ErrorContext *
For functions that return `akerr_ErrorContext *`, you should use the `CATCH` macro.
```c
ATTEMPT {
CATCH(errctx, errorGeneratingFunction())
} // ...
```
This will assign the return value of the function in question to the akerr_ErrorContext previously prepared in the current scope. If the function returns an akerr_ErrorContext that indicates any type of error, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
(One caveat: because this exit is implemented with a C `break`, `CATCH` must not be used inside a loop within the `ATTEMPT` block — see the section "Important: do not use CATCH or FAIL_*_BREAK inside a loop" below.)
### Setting errors from functions or expressions returning integer
For functions that return integer, such as logical comparisons or most standard library functions, use the `FAIL_ZERO_BREAK` and `FAIL_NONZERO_BREAK` macros. These macros allow you to capture an integer return code from an expression or function and set an error code in the current context based off that return.
Here is an example of checking for a NULL pointer
```c
ATTEMPT {
FAIL_ZERO_BREAK(errctx, (somePointer != NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
} // ...
```
Here is an example of checking for two strings that are not equal
```c
ATTEMPT {
FAIL_NONZERO_BREAK(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")
} // ...
```
When either of these two macros are used, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
### Important: do not use CATCH or FAIL_*_BREAK inside a loop
`CATCH`, `FAIL_ZERO_BREAK`, `FAIL_NONZERO_BREAK`, and `FAIL_BREAK` leave the `ATTEMPT` block by executing a C `break` statement. In C, `break` only exits the *innermost* enclosing `for`, `while`, `do`, or `switch`. Therefore **these macros must not be used inside a loop (or a nested `switch`) that is itself inside an `ATTEMPT` block.** If you do, the `break` escapes only the loop — not the `ATTEMPT` — and the rest of the `ATTEMPT` body then runs with an error already pending:
```c
ATTEMPT {
for ( int i = 0; i < n; i++ ) {
CATCH(errctx, process(items[i])); // WRONG: break exits the for loop, not the ATTEMPT
}
// ... this code still executes, with errctx already in an error state ...
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
```
Note that moving the loop into a helper function does **not** fix this on its own — if the helper still wraps the loop in an `ATTEMPT` and uses `CATCH`/`FAIL_*_BREAK` inside it, it has the exact same problem. The fix is to iterate with `return`-based macros, which are unaffected by loop nesting. Use one of the two patterns below.
**Pattern 1 — use `PASS` (or a `FAIL_*_RETURN` macro) inside the loop.** These exit the *enclosing function* with a `return` rather than a `break`, so loop nesting is irrelevant. Use this when the loop should stop and propagate on the first error:
```c
akerr_ErrorContext AKERR_NOIGNORE *process_all(Item *items, int n)
{
PREPARE_ERROR(errctx);
for ( int i = 0; i < n; i++ ) {
PASS(errctx, process(items[i])); // returns from process_all on the first error
}
SUCCEED_RETURN(errctx);
}
```
**Pattern 2 — move the loop into a helper and `CATCH` the single call.** When you need a `CLEANUP` block or want to `HANDLE` the error locally, put the loop in its own `akerr_ErrorContext *`-returning function (written per Pattern 1) and `CATCH` that one call. The `CATCH` is then not inside a loop, so its `break` scopes to the `ATTEMPT` correctly:
```c
ATTEMPT {
CATCH(errctx, process_all(items, n)); // a single CATCH, not looped
} CLEANUP {
// ... always runs ...
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_VALUE) {
// ... handle a failure from any iteration ...
} FINISH(errctx, true);
```
## Passing errors
Sometimes you can't actually do anything about the errors that come out of a given method, but you want that error to be propagated back up the call chain, and to be properly reported. If this is your goal, you can avoid using a `ATTEMPT ... FINISH` block, and simply use the `PASS` macro.
```
PREPARE_ERROR(e);
PASS(e, some_method_that_may_fail());
SUCCEED_RETURN(e);
```
This does the same thing as this, but with less code:
```
PREPARE_ERROR(e);
ATTEMPT {
CATCH(e, some_method_that_may_fail());
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
```
## Handling errors
Inside of the `PROCESS { ... }` block, you must handle any errors that occurred during the `ATTEMPT { ... }` block. You do this with `HANDLE`, `HANDLE_GROUP`, and `HANDLE_DEFAULT`.
### Handling a specific error with HANDLE
In order to handle a specific error code, use the `HANDLE` macro.
```c
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
// Something is complaining about a null pointer error. Do something about it.
} // ...
```
### Handling a group of errors with HANDLE_GROUP
In order to handle a group of related errors that all require the same failure behavior, use `HANDLE` followed by `HANDLE_GROUP`. For example, to handle a scenario where an IO error, key error, and index error all need to be handled the same way:
```c
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_IO) {
} HANDLE_GROUP(errctx, AKERR_KEY) {
} HANDLE_GROUP(errctx, AKERR_INDEX) {
// error handling code goes here
}
```
This creates a fallthrough mechanism where all 3 errors get the same error handling code. Note that while the cases fall through, you can still (if desired) put some code specific to each error in that error's `HANDLE` or `HANDLE_GROUP` block; but this is not required, only the final handler needs to get any code.
The fallthrough behavior stops as soon as another `HANDLE` macro is encountered. For example, in this example, `AKERR_IO`, `AKERR_KEY` and `AKERR_INDEX` are all handled as a group, but `AKERR_RELATIONSHIP` is not.
```c
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_IO) {
} HANDLE_GROUP(errctx, AKERR_KEY) {
} HANDLE_GROUP(errctx, AKERR_INDEX) {
// This code handles 3 error cases
} HANDLE(errctx, AKERR_RELATIONSHIP) {
// This code handles 1 error case
}
```
## Returning success or failure from functions returning akerr_ErrorContext *
If at all possible, when using this library, your functions should return `akerr_ErrorContext *`. When returning from such functions, you should use the `SUCCEED_RETURN` and `FAIL_RETURN` macros.
### SUCCEED_RETURN
This macro is used when your function has reached the end of its happy code path and is prepared to exit successfully. This sets the akerr_ErrorContext to a successful state and exits the function.
```c
PREPARE_ERROR(errctx);
ATTEMPT {
// ... stuff
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
```
### FAIL_RETURN
If the code path in the current function reaches a state wherein an error must be set and the function must return early, you can use `FAIL_RETURN` to accomplish this. Note that this should not be used inside of an `ATTEMPT { ... }` block; this immediately exits the function, preventing a `CLEANUP { ... }` block from executing. This can be safely used from inside of a `CLEANUP` or `PROCESS` block, or from anywhere within the function not inside of an `ATTEMPT { ... }` block.
The function allows you to provide printf-style variable arguments to provide a meaningful failure message.
```c
PREPARE_ERROR(errctx);
FAIL_RETURN(AKERR_BEHAVIOR, "Something went horribly wrong!")
```
### Conditionally failing and returning
In addition to `FAIL_RETURN` you can also test for zero or non-zero conditions, set an error, and return from the function immediately. Use the `FAIL_ZERO_RETURN` and `FAIL_NONZERO_RETURN` macros for this. These macros can be used anywhere that `FAIL_RETURN` can be used.
```c
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (somePointer != NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
```
```c
PREPARE_ERROR(errctx);
FAIL_NONZERO_RETURN(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")
```

View File

@@ -21,8 +21,8 @@
*
* 1 The error pool and the status registry are mutex protected, and the
* per-thread state below is thread local. Every entry point may be called
* from any thread. See docs/thread-safety.md for what that does and does
* not cover.
* from any thread. See "Thread safety" in README.md for what that does and
* does not cover.
* 0 The library was built -DAKERR_THREADS=none for a single-threaded
* process: no locking, no thread-local storage, and calling it from more
* than one thread is undefined.
@@ -159,11 +159,6 @@ typedef struct
typedef void (*akerr_ErrorUnhandledErrorHandler)(akerr_ErrorContext *errctx);
typedef void (*akerr_ErrorLogFunction)(const char *f, ...);
/*
* The pool. Process-global, not thread-local: a context outlives the thread that
* raised it, which is what lets one be handed to another thread and released
* there.
*/
extern akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR];
/*
* Set these before starting threads. They are read on every error and written
@@ -179,17 +174,6 @@ extern akerr_ErrorLogFunction akerr_log_method;
*/
extern AKERR_THREAD_LOCAL akerr_ErrorContext *__akerr_last_ignored;
/*
* Drop one reference, returning NULL once the last one is gone so the caller can
* null its own pointer.
*
* This need not be the thread that checked the context out. The reference count
* is the only field the library reads across threads, and it is only ever
* touched under the pool lock, so a context handed to another thread is released
* there. Exactly once, though: releasing a stale pointer takes the
* refcount-zero branch a second time and wipes a slot that by then holds
* somebody else's live error.
*/
akerr_ErrorContext AKERR_NOIGNORE *akerr_release_error(akerr_ErrorContext *ptr);
/*
* Check a context out of the pool. The returned context already carries one
@@ -391,18 +375,6 @@ akerr_ErrorContext AKERR_NOIGNORE *__akerr_copy_string(char *destination, int ca
* Defines for the ATTEMPT/CATCH/CLEANUP/PROCESS/HANDLE/FINISH process
*/
/*
* HANDLE_GROUP deliberately enters the next case label. GCC and clang both
* understand this spelling in the C modes supported by libakerror. Other
* compilers keep the established control flow without receiving an attribute
* they do not implement.
*/
#if defined(__GNUC__) || defined(__clang__)
#define AKERR_FALLTHROUGH __attribute__((fallthrough));
#else
#define AKERR_FALLTHROUGH
#endif
#define ATTEMPT \
switch ( 0 ) { \
case 0: \
@@ -455,7 +427,6 @@ akerr_ErrorContext AKERR_NOIGNORE *__akerr_copy_string(char *destination, int ca
__err_context->handled = true;
#define HANDLE_GROUP(__err_context, __err_status) \
AKERR_FALLTHROUGH \
case __err_status: \
__err_context->stacktracebufptr = (char *)&__err_context->stacktracebuf; \
__err_context->handled = true;

View File

@@ -129,11 +129,6 @@ akerr_ErrorContext *__akerr_copy_string(char *destination, int capacity,
*
* Takes no lock: the addresses of the pool slots are fixed for the life of the
* process, and nothing here reads a slot's contents.
*
* NULL reads back as valid, because the caller is VALID() asking whether a
* function returned something it has no business returning, and NULL is how a
* function says it succeeded. Anything reading this as "is this a pool slot"
* must check for NULL itself.
*/
int akerr_valid_error_address(akerr_ErrorContext *ptr)
{

View File

@@ -94,8 +94,8 @@ Re-run after adding tests and confirm the score went up.
## Current status
`src/error.c` scores 81.4% — 245 of 301 mutants killed (211 by a failing test,
24 by failing to compile, 10 by hanging the suite), 56 surviving. The CI gate is
`src/error.c` scores 81.2% — 238 of 293 mutants killed (204 by a failing test,
24 by failing to compile, 10 by hanging the suite), 55 surviving. The CI gate is
set to 65% for headroom.
The ten timeout kills are all in the locking: deleting `akerr_mutex_init()` or
@@ -123,16 +123,10 @@ The remaining survivors are dominated by:
it never sees that. Deleting an `akerr_init()` call survives for a duller
reason: something else has always initialized the library by the time that
line runs.
* **The default logger** (`vfprintf`, `va_end`, and the `return` in the
no-stdlib branch): the other tests replace `akerr_log_method` with the
in-process capturing logger, so nothing observes what the default one writes
to a real stderr. Killing these needs a test that captures a child's stderr.
The *handler* internals next to them are no longer in this category:
`tests/err_unhandled_null.c` and `tests/err_exit_status.c` read a forked
child's exit code, which kills every mutant in `akerr_exit()` and in
`akerr_default_handler_unhandled_error()` — all twelve of them, including the
`status < 0``status < 1` variant that only a test asserting
`akerr_exit(0)` exits 0 can distinguish.
* **Default logger / handler internals** (`vfprintf`, `va_end`, the
`errctx == NULL` branch, `exit(1)`): killing these needs a subprocess-based
test that captures a child's stderr and exit code, rather than the in-process
capturing logger the other tests use.
* **Static assertions** (`akerr_assert_name_slots_pow2` and the occupancy cap
it guards): a mutated compile-time assertion that still compiles has no
runtime behavior to observe. Unkillable by construction — the assertion is
@@ -149,8 +143,9 @@ Findings surfaced by mutation testing:
* **Open:** the harness builds every mutant with the default CMake options, so a
mutant that only breaks under concurrency is judged by a suite running without
ThreadSanitizer. Mutating under `-DAKERR_SANITIZE=thread` would close that,
and needs a way to pass CMake options through to the mutant build. That is
issue #10; `TODO.md` records why the score is a floor rather than a verdict.
and needs a way to pass CMake options through to the mutant build. See
"Mutation testing judges concurrency mutants without a sanitizer" in
`TODO.md`.
* **Superseded:** status names now use a private sparse registry, so the old
public `AKERR_MAX_ERR_VALUE` ceiling and its consumer ABI mismatch no longer

View File

@@ -1,254 +0,0 @@
#include "akerror.h"
#include "err_capture.h"
#include "err_threads.h"
#include <string.h>
/*
* Handing an error context from one thread to another.
*
* A context is not thread state. It lives in AKERR_ARRAY_ERROR, which is
* process-global, and the only field of it the library reads across an
* ownership boundary is the reference count -- which is only ever touched under
* the pool lock. So a context can be raised on one thread, handed to another,
* and handled and released there, and akerr_release_error() does not care which
* thread checked the slot out. That is a property this library promises, and it
* is what makes the worker/collector shape usable at all.
*
* The other half of the promise is what it does *not* cover: two threads inside
* one context at once. Ownership moves, it does not fork. This test asserts the
* supported half; the unsupported half cannot be asserted without deliberately
* racing, which ThreadSanitizer would then correctly fail.
*
* The queue below is a plain mutex and two condition variables rather than the
* __atomic builtins the rest of these tests use, and that is deliberate: the
* mutex *is* the thing under test. Context content is written with no lock at
* all, so the handoff itself is what publishes those writes to the receiver.
*
* The claim is proved from four directions:
*
* 1. The context is still a live pool slot after it crosses, holding exactly
* the one reference it was checked out with.
* 2. Its message and its whole stack trace -- producer frame and all -- arrive
* intact, and in each producer's own order.
* 3. The slot is never recycled underneath the transfer: akerr_slot_owner[]
* still names the producer when the collector picks it up.
* 4. A context outlives the thread that raised it (see main()).
*/
#define ITERATIONS 500
#define AKERR_HANDOFF_DEPTH 32
/*
* The sizing rule docs/thread-safety.md gives, made executable. Every queued
* error is a checked-out pool slot, and so is every producer's error in flight.
* Outrun the pool and ENSURE_ERROR_READY exits the process from inside FAIL,
* with no slot left to raise the failure from.
*/
typedef char akerr_assert_handoff_fits_pool[
(AKERR_HANDOFF_DEPTH + AKERR_TEST_THREADS < AKERR_MAX_ARRAY_ERROR) ? 1 : -1];
static struct
{
pthread_mutex_t lock;
pthread_cond_t not_full;
pthread_cond_t not_empty;
akerr_ErrorContext *slot[AKERR_HANDOFF_DEPTH];
int head;
int count;
} queue;
/* Bounded on purpose: an unbounded queue of errors is an unbounded number of
* checked-out pool slots. Blocking the producer is the backpressure. */
static void queue_push(akerr_ErrorContext *errctx)
{
pthread_mutex_lock(&queue.lock);
while ( queue.count == AKERR_HANDOFF_DEPTH ) {
pthread_cond_wait(&queue.not_full, &queue.lock);
}
queue.slot[(queue.head + queue.count) % AKERR_HANDOFF_DEPTH] = errctx;
queue.count += 1;
pthread_cond_signal(&queue.not_empty);
pthread_mutex_unlock(&queue.lock);
}
static akerr_ErrorContext *queue_pop(void)
{
akerr_ErrorContext *errctx;
pthread_mutex_lock(&queue.lock);
while ( queue.count == 0 ) {
pthread_cond_wait(&queue.not_empty, &queue.lock);
}
errctx = queue.slot[queue.head];
queue.head = (queue.head + 1) % AKERR_HANDOFF_DEPTH;
queue.count -= 1;
pthread_cond_signal(&queue.not_full);
pthread_mutex_unlock(&queue.lock);
return errctx;
}
/*
* Raise an error and give it away. The stack-trace frame is appended before the
* push so the trace records the crossing, and it is the last thing this thread
* does to the context: after queue_push() returns, `e` belongs to the collector
* and reading even e->status here would be the unsupported half of the rule.
*/
static void produce_one(akerr_ThreadArg *arg, int seq)
{
PREPARE_ERROR(e);
FAIL(e, AKERR_VALUE, "thread %d seq %d", arg->id, seq);
AKERR_TCHECK(arg, akerr_slot_claim(e->arrayid, arg->id) == 0);
AKERR_STACKTRACE_APPEND(e, "queued by thread %d\n", arg->id);
queue_push(e);
}
/*
* One received error, handled and released on a thread that never called
* akerr_next_error(). That release is the whole claim.
*
* `seen` is the collector's own per-producer sequence counter. Collector-local
* means no atomics: keeping the producers in order is the queue's job, and
* checking it is this thread's.
*/
static void collect_one(akerr_ThreadArg *arg, akerr_ErrorContext *e, int *seen)
{
char expected[64];
int producer = 0;
int seq = 0;
AKERR_TCHECK(arg, akerr_valid_error_address(e) == 1);
/* It crossed holding exactly the reference it was checked out with. */
AKERR_TCHECK(arg, e->refcount == 1);
AKERR_TCHECK(arg, sscanf(e->message, "thread %d seq %d", &producer, &seq) == 2);
AKERR_TCHECK(arg, producer >= 2 && producer <= AKERR_TEST_THREADS);
if ( producer >= 2 && producer <= AKERR_TEST_THREADS ) {
AKERR_TCHECK(arg, seq == seen[producer]);
seen[producer] += 1;
}
/* The slot still belongs to the producer, so nothing recycled it while it
* was in flight. */
AKERR_TCHECK(arg, akerr_slot_holder(e->arrayid) == producer);
snprintf(expected, sizeof(expected), "thread %d seq %d", producer, seq);
/* Nothing to attempt -- the error is already in hand. The blocks are here
* because this is the assembly the macros require, and because a real
* collector reads exactly like this. */
ATTEMPT {
} CLEANUP {
} PROCESS(e) {
/* case 0: a handed-off error that arrives with no status means somebody
* wrote over the context after the producer let it go. */
int error_was_lost = 1;
AKERR_TCHECK(arg, error_was_lost == 0);
} HANDLE(e, AKERR_VALUE) {
/* HANDLE rewinds the cursor, but the bytes are still there: the whole
* trace crossed with the context, producer frame and handoff frame. */
AKERR_TCHECK(arg, strstr(e->stacktracebuf, expected) != NULL);
AKERR_TCHECK(arg, strstr(e->stacktracebuf, "queued by thread") != NULL);
/* Give the slot up before FINISH releases the context: the other order
* hands it back to the pool while this thread still claims it. */
akerr_slot_drop(e->arrayid);
} FINISH_NORETURN(e);
/* FINISH_NORETURN, not FINISH(e, false): FINISH_LOGIC decides whether to
* propagate at run time, so the compiler still parses its
* `return __err_context` and diagnoses it in a function returning void,
* whatever __pass_up says. An error this collector did not handle takes the
* process down from here, which is right -- but note it is now the
* collector's thread deciding the exit status. */
}
/*
* Drain exactly what the producers will send. A fixed count rather than a
* sentinel: a miscounted handoff should fail the test, not hang it.
*/
static void collect_all(akerr_ThreadArg *arg)
{
int seen[AKERR_TEST_THREADS + 1] = { 0 };
int total = (AKERR_TEST_THREADS - 1) * ITERATIONS;
for ( int i = 0; i < total; i++ ) {
collect_one(arg, queue_pop(), seen);
}
}
static void *handoff_body(void *raw)
{
akerr_ThreadArg *arg = raw;
pthread_barrier_wait(arg->barrier);
if ( arg->id == 1 ) {
collect_all(arg);
} else {
for ( int i = 0; i < ITERATIONS; i++ ) {
produce_one(arg, i);
}
}
return NULL;
}
/*
* Written by the raising thread, read by main() after pthread_join(). The join
* is the happens-before edge, which is the same thing the queue's mutex does
* above -- a plain global needs no atomics once something orders it.
*/
static akerr_ErrorContext *parked;
static void *raise_and_exit(void *unused)
{
PREPARE_ERROR(e);
(void)unused;
FAIL(e, AKERR_IO, "raised on a thread that exited");
parked = e;
return NULL;
}
int main(void)
{
pthread_t raiser;
int failures = 0;
akerr_log_method = &akerr_thread_logger;
akerr_init();
AKERR_CHECK(akerr_slots_in_use() == 0);
AKERR_CHECK(pthread_mutex_init(&queue.lock, NULL) == 0);
AKERR_CHECK(pthread_cond_init(&queue.not_full, NULL) == 0);
AKERR_CHECK(pthread_cond_init(&queue.not_empty, NULL) == 0);
failures = akerr_run_threads(&handoff_body);
AKERR_CHECK(failures == 0);
/* Every handed-off context was released by the thread that received it. */
AKERR_CHECK(akerr_slots_in_use() == 0);
/*
* A context outlives the thread that raised it: the pool is process-global,
* not thread-local storage. By the time these checks run, the thread that
* called FAIL() no longer exists.
*/
AKERR_CHECK(pthread_create(&raiser, NULL, &raise_and_exit, NULL) == 0);
AKERR_CHECK(pthread_join(raiser, NULL) == 0);
AKERR_CHECK(parked != NULL);
AKERR_CHECK(akerr_valid_error_address(parked) == 1);
AKERR_CHECK(parked->status == AKERR_IO);
AKERR_CHECK(parked->refcount == 1);
AKERR_CHECK(strstr(parked->stacktracebuf, "raised on a thread that exited") != NULL);
RELEASE_ERROR(parked);
AKERR_CHECK(parked == NULL);
AKERR_CHECK(akerr_slots_in_use() == 0);
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
AKERR_CHECK(akerr_slot_holder(i) == 0);
}
/* Nothing here reports through the log method: a handoff is not an error. */
AKERR_CHECK(akerr_thread_logs() == 0);
pthread_cond_destroy(&queue.not_empty);
pthread_cond_destroy(&queue.not_full);
pthread_mutex_destroy(&queue.lock);
fprintf(stderr, "err_threads_handoff ok (%d producers x %d errors)\n",
AKERR_TEST_THREADS - 1, ITERATIONS);
return 0;
}