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