2025-07-20 22:02:21 -04:00
|
|
|
cmake_minimum_required(VERSION 3.10)
|
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
|
|
|
# The status-code registry replaced the consumer-sized __AKERR_ERROR_NAMES array
|
|
|
|
|
# with private storage, which is a source and ABI break for anything built
|
|
|
|
|
# against an earlier header -- hence 1.0.0 and a SOVERSION, so a stale installed
|
|
|
|
|
# libakerror.so can no longer be silently paired with new headers.
|
|
|
|
|
project(akerror VERSION 1.0.0 LANGUAGES C)
|
2025-07-20 22:02:21 -04:00
|
|
|
|
2025-08-03 10:13:27 -04:00
|
|
|
include(GNUInstallDirs)
|
|
|
|
|
include(CMakePackageConfigHelpers)
|
2026-06-27 08:42:08 -04:00
|
|
|
include(CTest)
|
2025-08-03 10:13:27 -04:00
|
|
|
|
2026-01-10 10:20:35 -05:00
|
|
|
set(AKERR_USE_STDLIB 1 CACHE BOOL "Use the C standard library")
|
2026-07-30 01:48:07 -04:00
|
|
|
set(AKERR_COVERAGE 0 CACHE BOOL "Instrument the build with gcov coverage counters")
|
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
|
|
|
|
|
|
|
|
# Size of the private status-name hash table. Must be a power of two; usable
|
|
|
|
|
# capacity is 75% of it (src/error.c asserts both). The host's errno list
|
|
|
|
|
# consumes part of that at akerr_init() time, so the remainder is what all
|
|
|
|
|
# consumer libraries in the process share. These are applied PRIVATE on purpose:
|
|
|
|
|
# the table lives entirely in src/error.c, so raising them never changes
|
|
|
|
|
# anything a consumer can see. That is what makes them safe to tune, unlike the
|
|
|
|
|
# AKERR_MAX_ERR_VALUE they replaced.
|
|
|
|
|
set(AKERR_STATUS_NAME_SLOTS 4096 CACHE STRING
|
|
|
|
|
"Slots in the status-name table (power of two; 75% usable)")
|
|
|
|
|
set(AKERR_MAX_RESERVED_STATUS_RANGES 64 CACHE STRING
|
|
|
|
|
"Maximum number of status ranges that may be reserved")
|
2026-01-04 22:56:31 -05:00
|
|
|
set(akerror_install_cmakedir "${CMAKE_INSTALL_LIBDIR}/cmake/akerror")
|
2025-08-03 10:13:27 -04:00
|
|
|
|
2026-07-30 01:48:07 -04:00
|
|
|
# Coverage instrumentation. Applied per target (not globally) so it never leaks
|
|
|
|
|
# into the exported/installed target interface. Only the library is
|
|
|
|
|
# instrumented: the tests are the thing doing the covering, and the public
|
|
|
|
|
# header's macros cannot be measured this way at all -- GCC attributes an
|
|
|
|
|
# expanded macro to its call site, so header logic shows up as test-file lines.
|
|
|
|
|
# Coverage of those macros is what mutation testing (--target
|
|
|
|
|
# include/akerror.tmpl.h) is for.
|
|
|
|
|
if(AKERR_COVERAGE)
|
|
|
|
|
if(NOT CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
|
|
|
|
message(FATAL_ERROR
|
|
|
|
|
"AKERR_COVERAGE requires GCC or Clang, not ${CMAKE_C_COMPILER_ID}")
|
|
|
|
|
endif()
|
|
|
|
|
# -O0 keeps line counts attributable; no inlining or code motion.
|
|
|
|
|
set(AKERR_COVERAGE_FLAGS --coverage -O0 -g)
|
|
|
|
|
if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
|
|
|
|
|
# Record absolute source paths so gcov resolves sources built from the
|
|
|
|
|
# generated directory (GCC 8+; harmless to check).
|
|
|
|
|
include(CheckCCompilerFlag)
|
|
|
|
|
check_c_compiler_flag(-fprofile-abs-path AKERR_HAVE_PROFILE_ABS_PATH)
|
|
|
|
|
if(AKERR_HAVE_PROFILE_ABS_PATH)
|
|
|
|
|
list(APPEND AKERR_COVERAGE_FLAGS -fprofile-abs-path)
|
|
|
|
|
endif()
|
|
|
|
|
endif()
|
|
|
|
|
endif()
|
|
|
|
|
|
|
|
|
|
# Add coverage compile/link flags to one target, if coverage is enabled.
|
|
|
|
|
function(akerr_instrument_for_coverage _target)
|
|
|
|
|
if(AKERR_COVERAGE)
|
|
|
|
|
target_compile_options(${_target} PRIVATE ${AKERR_COVERAGE_FLAGS})
|
|
|
|
|
set_property(TARGET ${_target} APPEND_STRING
|
|
|
|
|
PROPERTY LINK_FLAGS " --coverage")
|
|
|
|
|
endif()
|
|
|
|
|
endfunction()
|
|
|
|
|
|
2026-05-12 16:44:06 -04:00
|
|
|
set(SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/scripts/generrno.sh)
|
|
|
|
|
set(INFILE ${CMAKE_CURRENT_SOURCE_DIR}/include/akerror.tmpl.h)
|
|
|
|
|
|
|
|
|
|
set(GENERATED_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated)
|
|
|
|
|
|
|
|
|
|
set(GENERATED_ERRNO_C ${GENERATED_DIR}/src/errno.c)
|
|
|
|
|
set(GENERATED_AKERROR_H ${GENERATED_DIR}/include/akerror.h)
|
|
|
|
|
|
2026-01-12 08:33:31 -05:00
|
|
|
add_custom_command(
|
2026-05-12 16:44:06 -04:00
|
|
|
OUTPUT ${GENERATED_ERRNO_C} ${GENERATED_AKERROR_H}
|
|
|
|
|
COMMAND ${CMAKE_COMMAND} -E make_directory ${GENERATED_DIR}
|
|
|
|
|
COMMAND /usr/bin/env bash
|
|
|
|
|
${SCRIPT}
|
|
|
|
|
${CMAKE_CURRENT_SOURCE_DIR}
|
|
|
|
|
${GENERATED_DIR}
|
2026-01-12 08:33:31 -05:00
|
|
|
DEPENDS ${SCRIPT} ${INFILE}
|
|
|
|
|
VERBATIM
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-12 16:42:33 -04:00
|
|
|
add_library(akerror SHARED
|
2026-01-12 08:33:31 -05:00
|
|
|
src/error.c
|
2026-05-12 16:44:06 -04:00
|
|
|
${GENERATED_ERRNO_C}
|
2025-07-20 22:02:21 -04:00
|
|
|
)
|
2026-05-12 16:44:06 -04:00
|
|
|
|
|
|
|
|
target_include_directories(akerror PUBLIC
|
2026-06-27 07:44:20 -04:00
|
|
|
$<BUILD_INTERFACE:${GENERATED_DIR}/include>
|
2026-05-12 16:44:06 -04:00
|
|
|
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
find_package(PkgConfig REQUIRED)
|
|
|
|
|
add_library(akerror::akerror ALIAS akerror)
|
2025-07-20 22:02:21 -04:00
|
|
|
|
2026-01-04 22:56:31 -05:00
|
|
|
target_compile_definitions(akerror
|
2026-01-10 10:20:35 -05:00
|
|
|
PUBLIC AKERR_USE_STDLIB=${AKERR_USE_STDLIB}
|
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
|
|
|
PRIVATE AKERR_STATUS_NAME_SLOTS=${AKERR_STATUS_NAME_SLOTS}
|
|
|
|
|
PRIVATE AKERR_MAX_RESERVED_STATUS_RANGES=${AKERR_MAX_RESERVED_STATUS_RANGES}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
set_target_properties(akerror PROPERTIES
|
|
|
|
|
VERSION ${PROJECT_VERSION}
|
|
|
|
|
SOVERSION ${PROJECT_VERSION_MAJOR}
|
2026-01-04 22:56:31 -05:00
|
|
|
)
|
|
|
|
|
|
2026-07-30 01:48:07 -04:00
|
|
|
akerr_instrument_for_coverage(akerror)
|
|
|
|
|
|
2026-07-27 16:18:17 -04:00
|
|
|
# Each test is one source file in tests/ built into test_<name> and registered
|
|
|
|
|
# as CTest <name>. Tests expected to abort (unhandled error / contract
|
|
|
|
|
# violation) go in AKERR_WILL_FAIL_TESTS; all others must exit 0.
|
|
|
|
|
set(AKERR_TESTS
|
|
|
|
|
err_catch
|
|
|
|
|
err_cleanup
|
|
|
|
|
err_trace
|
|
|
|
|
err_improper_closure
|
|
|
|
|
err_success
|
|
|
|
|
err_pool_refcount
|
|
|
|
|
err_handle_default
|
|
|
|
|
err_handle_group
|
|
|
|
|
err_handle_dispatch
|
|
|
|
|
err_pass
|
|
|
|
|
err_ignore
|
|
|
|
|
err_swallow
|
|
|
|
|
err_errno
|
|
|
|
|
err_break_variants
|
|
|
|
|
err_custom_handler
|
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
|
|
|
err_error_names
|
|
|
|
|
err_release_clears
|
|
|
|
|
err_pool_exhaust
|
2026-07-27 17:17:26 -04:00
|
|
|
err_maxval
|
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
|
|
|
err_name_ownership
|
|
|
|
|
err_registry_init_order
|
Raise errors from the status registry instead of returning codes
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
|
|
|
err_status_exception
|
Use the library's own error idioms inside the library
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
|
|
|
err_copy_string
|
|
|
|
|
err_library_status_fatal
|
2026-07-28 09:22:19 -04:00
|
|
|
err_refcount_double_fail
|
|
|
|
|
err_stacktrace_bounds
|
2026-07-28 10:52:29 -04:00
|
|
|
err_name_bounds
|
|
|
|
|
err_format_string
|
2026-07-30 02:00:54 -04:00
|
|
|
err_unhandled_null
|
|
|
|
|
err_release_null
|
|
|
|
|
err_release_refcount
|
2026-07-27 16:18:17 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
set(AKERR_WILL_FAIL_TESTS
|
|
|
|
|
err_trace
|
|
|
|
|
err_improper_closure
|
Use the library's own error idioms inside the library
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
|
|
|
err_library_status_fatal
|
2026-07-27 16:18:17 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
foreach(_test IN LISTS AKERR_TESTS)
|
|
|
|
|
add_executable(test_${_test} tests/${_test}.c)
|
|
|
|
|
target_include_directories(test_${_test} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests)
|
|
|
|
|
target_link_libraries(test_${_test} PRIVATE akerror)
|
|
|
|
|
add_test(NAME ${_test} COMMAND test_${_test})
|
|
|
|
|
endforeach()
|
2025-07-21 08:54:26 -04:00
|
|
|
|
2026-06-27 08:42:08 -04:00
|
|
|
set_tests_properties(
|
2026-07-27 16:18:17 -04:00
|
|
|
${AKERR_WILL_FAIL_TESTS}
|
2026-06-27 08:42:08 -04:00
|
|
|
PROPERTIES WILL_FAIL TRUE
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-30 01:48:07 -04:00
|
|
|
# Coverage and mutation testing are meta-checks on the test suite itself, and
|
|
|
|
|
# both rebuild and re-run the whole suite, so they are manual targets rather
|
|
|
|
|
# than CTest tests.
|
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
|
|
|
find_package(Python3 COMPONENTS Interpreter)
|
|
|
|
|
if(Python3_FOUND)
|
2026-07-30 01:48:07 -04:00
|
|
|
# Code coverage: which library lines/branches the CTest suite reaches.
|
|
|
|
|
# cmake --build build --target coverage
|
|
|
|
|
# The script configures and drives its own instrumented build tree (under
|
|
|
|
|
# ${CMAKE_BINARY_DIR}/coverage) so this build's binaries and its coverage
|
|
|
|
|
# counters can never be stale or half-instrumented. Reports via gcov.
|
|
|
|
|
add_custom_target(coverage
|
|
|
|
|
COMMAND ${Python3_EXECUTABLE}
|
|
|
|
|
${CMAKE_CURRENT_SOURCE_DIR}/scripts/coverage.py
|
|
|
|
|
--source-root ${CMAKE_CURRENT_SOURCE_DIR}
|
|
|
|
|
--build-dir ${CMAKE_CURRENT_BINARY_DIR}/coverage
|
|
|
|
|
--cmake ${CMAKE_COMMAND}
|
|
|
|
|
--ctest ${CMAKE_CTEST_COMMAND}
|
|
|
|
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
|
|
|
|
USES_TERMINAL
|
|
|
|
|
COMMENT "Running the test suite instrumented for coverage"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Mutation testing: break the library in small ways and confirm the test
|
|
|
|
|
# suite notices.
|
|
|
|
|
# cmake --build build --target mutation
|
|
|
|
|
# When embedded in another project, use a namespaced target to avoid
|
|
|
|
|
# collisions with mutation targets provided by sibling dependencies.
|
2026-07-29 17:59:22 -04:00
|
|
|
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
|
|
|
|
set(AKERR_MUTATION_TARGET mutation)
|
|
|
|
|
else()
|
|
|
|
|
set(AKERR_MUTATION_TARGET akerror_mutation)
|
|
|
|
|
endif()
|
|
|
|
|
add_custom_target(${AKERR_MUTATION_TARGET}
|
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
|
|
|
COMMAND ${Python3_EXECUTABLE}
|
|
|
|
|
${CMAKE_CURRENT_SOURCE_DIR}/scripts/mutation_test.py
|
|
|
|
|
--source-root ${CMAKE_CURRENT_SOURCE_DIR}
|
|
|
|
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
|
|
|
|
USES_TERMINAL
|
|
|
|
|
COMMENT "Running mutation tests (breaks the library, expects tests to fail)"
|
|
|
|
|
)
|
|
|
|
|
endif()
|
|
|
|
|
|
2025-07-20 22:02:21 -04:00
|
|
|
set(main_lib_dest "lib/my_library-${MY_LIBRARY_VERSION}")
|
2026-01-12 08:32:29 -05:00
|
|
|
install(TARGETS akerror
|
|
|
|
|
EXPORT akerrorTargets
|
|
|
|
|
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
|
|
|
|
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
|
|
|
|
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
|
|
|
|
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
|
|
|
|
|
)
|
|
|
|
|
|
2026-06-27 07:44:20 -04:00
|
|
|
install(FILES ${GENERATED_AKERROR_H} DESTINATION "include/")
|
2026-01-04 22:56:31 -05:00
|
|
|
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akerror.pc DESTINATION "lib/pkgconfig/")
|
2025-08-03 10:13:27 -04:00
|
|
|
|
2026-05-12 21:29:07 -04:00
|
|
|
install(EXPORT akerrorTargets
|
2026-01-04 22:56:31 -05:00
|
|
|
FILE akerrorTargets.cmake
|
|
|
|
|
NAMESPACE akerror::
|
|
|
|
|
DESTINATION ${akerror_install_cmakedir}
|
2025-08-03 10:13:27 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
configure_package_config_file(
|
2026-01-04 22:56:31 -05:00
|
|
|
cmake/akerror.cmake.in
|
|
|
|
|
"${CMAKE_CURRENT_BINARY_DIR}/akerrorConfig.cmake"
|
|
|
|
|
INSTALL_DESTINATION ${akerror_install_cmakedir}
|
2025-08-03 10:13:27 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
install(FILES
|
2026-01-04 22:56:31 -05:00
|
|
|
"${CMAKE_CURRENT_BINARY_DIR}/akerrorConfig.cmake"
|
|
|
|
|
DESTINATION ${akerror_install_cmakedir}
|
2025-08-03 10:13:27 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# pkgconfig
|
|
|
|
|
set(prefix ${CMAKE_INSTALL_PREFIX})
|
|
|
|
|
set(exec_prefix "\${prefix}")
|
|
|
|
|
set(libdir "\${exec_prefix}/lib")
|
|
|
|
|
set(includedir "\${prefix}/include")
|
2026-01-04 22:56:31 -05:00
|
|
|
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/akerror.pc.in ${CMAKE_CURRENT_BINARY_DIR}/akerror.pc @ONLY)
|