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>
- 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
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>