Commit Graph

19 Commits

Author SHA1 Message Date
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
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
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
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
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
0c29f5d69f Fix handling of CATCH() or FAIL() macros around functions that should return an (akerr_ErrorContext *) but return an invalid pointer (to something not in our exception array) 2026-05-06 12:36:56 -04:00
c526bb1ba3 Cleanup the code and unified everything (except the macros) under the akerr_ namespace 2026-01-10 10:20:35 -05:00
cf0f2bbeac Make stdlib optional. Remove dependency on SDL3. Update README. 2026-01-05 21:18:44 -05:00
0521ebaf7d Better support for user-defined error codes and messages 2025-08-02 15:07:08 -04:00
a3c9d815e5 Improve README, add example tests 2025-07-21 07:19:41 -04:00