Compare commits
1 Commits
status-cod
...
new_readme
| Author | SHA1 | Date | |
|---|---|---|---|
| 5aeea26645 |
@@ -1,99 +0,0 @@
|
|||||||
name: libakerror CI Build
|
|
||||||
run-name: ${{ gitea.actor }} libakerror test
|
|
||||||
on: [push]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
cmake_build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: echo "Triggered by ${{ gitea.event_name }} from ${{ gitea.repository }}@${{ gitea.ref }}. Building on ${{ runner.os }}."
|
|
||||||
- name: Check out repository code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update -y
|
|
||||||
sudo apt-get install -y cmake gcc moreutils
|
|
||||||
- name: build and install
|
|
||||||
run: |
|
|
||||||
mkdir installdir
|
|
||||||
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=installdir
|
|
||||||
cmake --build build
|
|
||||||
cmake --install build
|
|
||||||
# --output-junit resolves relative to the test dir, so give an absolute
|
|
||||||
# path to land the report in the workspace root for the reporter below.
|
|
||||||
- name: test (JUnit)
|
|
||||||
run: ctest --test-dir build --output-on-failure --output-junit "$(pwd)/ctest-junit.xml"
|
|
||||||
# annotate_only: true skips creating a check run via the Checks API, which
|
|
||||||
# Gitea does not support and 404s on (mikepenz/action-junit-report#23).
|
|
||||||
# Results surface via the job summary instead.
|
|
||||||
- name: publish test results
|
|
||||||
if: always()
|
|
||||||
uses: mikepenz/action-junit-report@v4
|
|
||||||
with:
|
|
||||||
report_paths: 'ctest-junit.xml'
|
|
||||||
annotate_only: true
|
|
||||||
detailed_summary: true
|
|
||||||
include_passed: true
|
|
||||||
fail_on_failure: 'true'
|
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
|
||||||
|
|
||||||
coverage:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out repository code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update -y
|
|
||||||
sudo apt-get install -y cmake gcc moreutils python3
|
|
||||||
# Run the suite against a gcov-instrumented build and gate on coverage of
|
|
||||||
# the library sources. Thresholds keep headroom below the current numbers
|
|
||||||
# (src/error.c: ~94% line, ~60% branch) and apply per file as well as to
|
|
||||||
# the total, so the generated status-name table cannot mask a regression.
|
|
||||||
- name: coverage
|
|
||||||
run: |
|
|
||||||
python3 scripts/coverage.py --junit coverage-junit.xml --threshold 90 --branch-threshold 50
|
|
||||||
# Publish even when the threshold gate fails, so gaps are visible.
|
|
||||||
# Display-only (fail_on_failure: false); the --threshold above is the gate.
|
|
||||||
# annotate_only avoids the Checks API 404 on Gitea (see note above).
|
|
||||||
- name: publish coverage results
|
|
||||||
if: always()
|
|
||||||
uses: mikepenz/action-junit-report@v4
|
|
||||||
with:
|
|
||||||
report_paths: 'coverage-junit.xml'
|
|
||||||
annotate_only: true
|
|
||||||
detailed_summary: true
|
|
||||||
include_passed: true
|
|
||||||
fail_on_failure: 'false'
|
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
|
||||||
|
|
||||||
mutation_test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out repository code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update -y
|
|
||||||
sudo apt-get install -y cmake gcc moreutils python3
|
|
||||||
# Verify the tests actually catch bugs: break the library many ways and
|
|
||||||
# confirm the suite fails. Gated on src/error.c (fast, deterministic).
|
|
||||||
# The threshold keeps headroom below the current score for equivalent
|
|
||||||
# mutants; see tests/MUTATION.md. Run the full default target locally for
|
|
||||||
# deeper (slower) coverage including the macro header.
|
|
||||||
- name: mutation testing
|
|
||||||
run: |
|
|
||||||
python3 scripts/mutation_test.py --target src/error.c --junit mutation-junit.xml --threshold 65
|
|
||||||
# Publish even when the threshold gate fails, so survivors are visible.
|
|
||||||
# Display-only (fail_on_failure: false); the --threshold above is the gate.
|
|
||||||
# annotate_only avoids the Checks API 404 on Gitea (see note above).
|
|
||||||
- name: publish mutation results
|
|
||||||
if: always()
|
|
||||||
uses: mikepenz/action-junit-report@v4
|
|
||||||
with:
|
|
||||||
report_paths: 'mutation-junit.xml'
|
|
||||||
annotate_only: true
|
|
||||||
detailed_summary: true
|
|
||||||
include_passed: true
|
|
||||||
fail_on_failure: 'false'
|
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
include/akerror.h
|
|
||||||
build/
|
|
||||||
*-junit.xml
|
|
||||||
81
AGENTS.md
81
AGENTS.md
@@ -1,81 +0,0 @@
|
|||||||
# Repository Guidelines
|
|
||||||
|
|
||||||
## Project Structure & Module Organization
|
|
||||||
|
|
||||||
This is a small C library built with CMake. Core implementation lives in
|
|
||||||
`src/error.c`. The public header is generated at build time from
|
|
||||||
`include/akerror.tmpl.h` by `scripts/generrno.sh`, which also generates
|
|
||||||
`src/errno.c` under the build directory. 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`.
|
|
||||||
|
|
||||||
## Build, Test, and Development Commands
|
|
||||||
|
|
||||||
Use an out-of-tree build:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cmake -S . -B build
|
|
||||||
cmake --build build
|
|
||||||
ctest --test-dir build --output-on-failure
|
|
||||||
```
|
|
||||||
|
|
||||||
`cmake -S . -B build` configures the build and generates `akerror.h`.
|
|
||||||
`cmake --build build` compiles `libakerror` and test executables. `ctest`
|
|
||||||
runs the registered unit tests. To emit CI-style results, use:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
ctest --test-dir build --output-on-failure --output-junit "$(pwd)/ctest-junit.xml"
|
|
||||||
```
|
|
||||||
|
|
||||||
Mutation testing is available through:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cmake --build build --target mutation
|
|
||||||
scripts/mutation_test.py --target src/error.c --threshold 65
|
|
||||||
```
|
|
||||||
|
|
||||||
Code coverage is available through:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cmake --build build --target coverage
|
|
||||||
scripts/coverage.py --threshold 90 --branch-threshold 50
|
|
||||||
```
|
|
||||||
|
|
||||||
`scripts/coverage.py` configures its own instrumented build tree (default
|
|
||||||
`build/coverage`, `-DAKERR_COVERAGE=ON`), runs the CTest suite there, and
|
|
||||||
reports gcov line/branch coverage per library source. Thresholds gate each
|
|
||||||
file as well as the total. Coverage measures `src/error.c` and the generated
|
|
||||||
`src/errno.c` only; the public header's macros expand at their call sites, so
|
|
||||||
mutation testing is what checks those.
|
|
||||||
|
|
||||||
## Coding Style & Naming Conventions
|
|
||||||
|
|
||||||
Use C99-compatible C and follow the surrounding style. Functions and types use
|
|
||||||
the `akerr_` prefix; macros and constants use `AKERR_` or all-caps macro names
|
|
||||||
such as `PREPARE_ERROR`. Keep generated-code changes in templates or generator
|
|
||||||
scripts, not in build outputs. Preserve concise comments for invariants,
|
|
||||||
macro constraints, and non-obvious error lifecycle behavior.
|
|
||||||
|
|
||||||
## Testing Guidelines
|
|
||||||
|
|
||||||
Add tests as `tests/err_<behavior>.c`. Register each new test in the
|
|
||||||
`AKERR_TESTS` list in `CMakeLists.txt`; tests that intentionally abort must
|
|
||||||
also be listed in `AKERR_WILL_FAIL_TESTS`. Prefer focused executable tests that
|
|
||||||
return zero on success and use existing helpers such as `AKERR_CHECK`. Run the
|
|
||||||
CTest suite before submitting changes, and run mutation testing and coverage
|
|
||||||
when changing core control-flow, reference counting, stack-trace, or handler
|
|
||||||
behavior.
|
|
||||||
|
|
||||||
## Commit & Pull Request Guidelines
|
|
||||||
|
|
||||||
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, and any compatibility impact for public macros, generated
|
|
||||||
headers, installation paths, or CMake/pkg-config consumers.
|
|
||||||
|
|
||||||
## Agent-Specific Instructions
|
|
||||||
|
|
||||||
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.
|
|
||||||
247
CMakeLists.txt
247
CMakeLists.txt
@@ -1,233 +1,60 @@
|
|||||||
cmake_minimum_required(VERSION 3.10)
|
cmake_minimum_required(VERSION 3.10)
|
||||||
# The status-code registry replaced the consumer-sized __AKERR_ERROR_NAMES array
|
project(sdlerror LANGUAGES C)
|
||||||
# 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)
|
|
||||||
|
|
||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
include(CMakePackageConfigHelpers)
|
include(CMakePackageConfigHelpers)
|
||||||
include(CTest)
|
|
||||||
|
|
||||||
set(AKERR_USE_STDLIB 1 CACHE BOOL "Use the C standard library")
|
set(sdlerror_install_cmakedir "${CMAKE_INSTALL_LIBDIR}/cmake/sdlerror")
|
||||||
set(AKERR_COVERAGE 0 CACHE BOOL "Instrument the build with gcov coverage counters")
|
|
||||||
|
|
||||||
# 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")
|
|
||||||
set(akerror_install_cmakedir "${CMAKE_INSTALL_LIBDIR}/cmake/akerror")
|
|
||||||
|
|
||||||
# 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()
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
add_custom_command(
|
|
||||||
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}
|
|
||||||
DEPENDS ${SCRIPT} ${INFILE}
|
|
||||||
VERBATIM
|
|
||||||
)
|
|
||||||
|
|
||||||
add_library(akerror SHARED
|
|
||||||
src/error.c
|
|
||||||
${GENERATED_ERRNO_C}
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(akerror PUBLIC
|
|
||||||
$<BUILD_INTERFACE:${GENERATED_DIR}/include>
|
|
||||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
|
|
||||||
)
|
|
||||||
|
|
||||||
find_package(PkgConfig REQUIRED)
|
find_package(PkgConfig REQUIRED)
|
||||||
add_library(akerror::akerror ALIAS akerror)
|
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3-static)
|
||||||
|
|
||||||
target_compile_definitions(akerror
|
# Check for SDL3 using pkg-config
|
||||||
PUBLIC AKERR_USE_STDLIB=${AKERR_USE_STDLIB}
|
pkg_check_modules(SDL3 REQUIRED sdl3)
|
||||||
PRIVATE AKERR_STATUS_NAME_SLOTS=${AKERR_STATUS_NAME_SLOTS}
|
|
||||||
PRIVATE AKERR_MAX_RESERVED_STATUS_RANGES=${AKERR_MAX_RESERVED_STATUS_RANGES}
|
# Add include directories
|
||||||
|
include_directories(${SDL3_INCLUDE_DIRS})
|
||||||
|
add_library(sdlerror SHARED
|
||||||
|
src/error.c
|
||||||
)
|
)
|
||||||
|
|
||||||
set_target_properties(akerror PROPERTIES
|
add_executable(test_err_catch tests/err_catch.c)
|
||||||
VERSION ${PROJECT_VERSION}
|
add_executable(test_err_cleanup tests/err_cleanup.c)
|
||||||
SOVERSION ${PROJECT_VERSION_MAJOR}
|
add_executable(test_err_trace tests/err_trace.c)
|
||||||
|
add_test(NAME err_catch COMMAND test_err_catch)
|
||||||
|
add_test(NAME err_cleanup COMMAND test_err_cleanup)
|
||||||
|
add_test(NAME err_trace COMMAND test_err_trace)
|
||||||
|
|
||||||
|
# Specify include directories for the library's headers (if applicable)
|
||||||
|
target_include_directories(sdlerror PUBLIC
|
||||||
|
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||||
|
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/>
|
||||||
)
|
)
|
||||||
|
target_link_libraries(test_err_catch PRIVATE sdlerror SDL3::SDL3)
|
||||||
akerr_instrument_for_coverage(akerror)
|
target_link_libraries(test_err_cleanup PRIVATE sdlerror SDL3::SDL3)
|
||||||
|
target_link_libraries(test_err_trace PRIVATE sdlerror SDL3::SDL3)
|
||||||
# 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
|
|
||||||
err_error_names
|
|
||||||
err_release_clears
|
|
||||||
err_pool_exhaust
|
|
||||||
err_maxval
|
|
||||||
err_name_ownership
|
|
||||||
err_registry_init_order
|
|
||||||
err_status_exception
|
|
||||||
err_copy_string
|
|
||||||
err_library_status_fatal
|
|
||||||
err_refcount_double_fail
|
|
||||||
err_stacktrace_bounds
|
|
||||||
err_name_bounds
|
|
||||||
err_format_string
|
|
||||||
err_unhandled_null
|
|
||||||
err_release_null
|
|
||||||
err_release_refcount
|
|
||||||
)
|
|
||||||
|
|
||||||
set(AKERR_WILL_FAIL_TESTS
|
|
||||||
err_trace
|
|
||||||
err_improper_closure
|
|
||||||
err_library_status_fatal
|
|
||||||
)
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
set_tests_properties(
|
|
||||||
${AKERR_WILL_FAIL_TESTS}
|
|
||||||
PROPERTIES WILL_FAIL TRUE
|
|
||||||
)
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
find_package(Python3 COMPONENTS Interpreter)
|
|
||||||
if(Python3_FOUND)
|
|
||||||
# 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.
|
|
||||||
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}
|
|
||||||
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()
|
|
||||||
|
|
||||||
set(main_lib_dest "lib/my_library-${MY_LIBRARY_VERSION}")
|
set(main_lib_dest "lib/my_library-${MY_LIBRARY_VERSION}")
|
||||||
install(TARGETS akerror
|
install(TARGETS sdlerror EXPORT sdlerror DESTINATION "lib/")
|
||||||
EXPORT akerrorTargets
|
install(FILES "include/sdlerror.h" DESTINATION "include/")
|
||||||
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/sdlerror.pc DESTINATION "lib/pkgconfig/")
|
||||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
|
||||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
|
||||||
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
|
|
||||||
)
|
|
||||||
|
|
||||||
install(FILES ${GENERATED_AKERROR_H} DESTINATION "include/")
|
|
||||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akerror.pc DESTINATION "lib/pkgconfig/")
|
|
||||||
|
|
||||||
install(EXPORT akerrorTargets
|
install(EXPORT sdlerror
|
||||||
FILE akerrorTargets.cmake
|
FILE sdlerrorTargets.cmake
|
||||||
NAMESPACE akerror::
|
NAMESPACE sdlerror::
|
||||||
DESTINATION ${akerror_install_cmakedir}
|
DESTINATION ${sdlerror_install_cmakedir}
|
||||||
)
|
)
|
||||||
|
|
||||||
configure_package_config_file(
|
configure_package_config_file(
|
||||||
cmake/akerror.cmake.in
|
cmake/sdlerror.cmake.in
|
||||||
"${CMAKE_CURRENT_BINARY_DIR}/akerrorConfig.cmake"
|
"${CMAKE_CURRENT_BINARY_DIR}/sdlerrorConfig.cmake"
|
||||||
INSTALL_DESTINATION ${akerror_install_cmakedir}
|
INSTALL_DESTINATION ${sdlerror_install_cmakedir}
|
||||||
)
|
)
|
||||||
|
|
||||||
install(FILES
|
install(FILES
|
||||||
"${CMAKE_CURRENT_BINARY_DIR}/akerrorConfig.cmake"
|
"${CMAKE_CURRENT_BINARY_DIR}/sdlerrorConfig.cmake"
|
||||||
DESTINATION ${akerror_install_cmakedir}
|
DESTINATION ${sdlerror_install_cmakedir}
|
||||||
)
|
)
|
||||||
|
|
||||||
# pkgconfig
|
# pkgconfig
|
||||||
@@ -235,4 +62,4 @@ set(prefix ${CMAKE_INSTALL_PREFIX})
|
|||||||
set(exec_prefix "\${prefix}")
|
set(exec_prefix "\${prefix}")
|
||||||
set(libdir "\${exec_prefix}/lib")
|
set(libdir "\${exec_prefix}/lib")
|
||||||
set(includedir "\${prefix}/include")
|
set(includedir "\${prefix}/include")
|
||||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/akerror.pc.in ${CMAKE_CURRENT_BINARY_DIR}/akerror.pc @ONLY)
|
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/sdlerror.pc.in ${CMAKE_CURRENT_BINARY_DIR}/sdlerror.pc @ONLY)
|
||||||
|
|||||||
605
README.md
605
README.md
@@ -1,482 +1,291 @@
|
|||||||
# Summary
|
# libsdlerror
|
||||||
|
|
||||||
This library provides a TRY/CATCH style exception handling mechanism for C.
|
**Explicit, disciplined error handling for C — built on SDL3.**
|
||||||
|
|
||||||

|
`libsdlerror` is a small library that helps C programmers write correct, readable, and maintainable error-handling code without pretending that C is something it is not.
|
||||||
|
|
||||||
## Upgrading from a pre-1.0.0 release
|
It does not add exceptions to C.
|
||||||
|
It does not hide control flow.
|
||||||
|
It does not allocate memory at runtime.
|
||||||
|
|
||||||
1.0.0 replaced the consumer-sized status-name array with a private, ownership-
|
If you want magic, look elsewhere.
|
||||||
enforced registry. That is a source and ABI break: see
|
|
||||||
[UPGRADING.md](UPGRADING.md) for what was removed, how to migrate, the capacity
|
|
||||||
limits and how to raise them, and the thread-safety rules.
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Why?
|
## Why This Library Exists
|
||||||
|
|
||||||
There is nothing wrong with C as it is. This library does not claim to fix some problem with C.
|
Error handling in C is not hard — but it is tedious, repetitive, and easy to get wrong.
|
||||||
|
|
||||||
Instead, this library implements a pragmatic and stylistic choice to assist the programmer in better handling errors in their programs. Vanilla C provides everything you need to do this out of the box, but this library makes it easier to avoid pointing certain guns at your foot, and when you do, it provides better context with those errors to help you more quickly recover.
|
The usual pattern:
|
||||||
|
|
||||||
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
|
|
||||||
* 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.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.
|
|
||||||
|
|
||||||
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
|
```c
|
||||||
#define MYLIB_OWNER "my-library"
|
if (foo() < 0) {
|
||||||
|
fprintf(stderr, "%s\n", SDL_GetError());
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *mylib_init(void)
|
cleanup();
|
||||||
{
|
return -1;
|
||||||
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
|
works, until it doesn’t. As programs grow:
|
||||||
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
|
- Error checks get duplicated
|
||||||
naming one nobody reserved raises `AKERR_STATUS_NAME_UNRESERVED`; a colliding
|
- Cleanup paths multiply
|
||||||
reservation raises `AKERR_STATUS_RANGE_OVERLAP`, with a message naming the real
|
- Context disappears
|
||||||
owner. See [UPGRADING.md](UPGRADING.md) for the full list of statuses these two
|
- Control flow becomes fragmented
|
||||||
functions raise, the capacity limits and how to raise them, and thread-safety
|
|
||||||
rules.
|
|
||||||
|
|
||||||
|
SDL compounds this by storing errors in a single thread-local string, forcing programmers to either handle errors immediately or lose information.
|
||||||
|
|
||||||
# Installation
|
`libsdlerror` exists to impose structure and discipline on this process — without compromising the explicitness that makes C reliable.
|
||||||
|
|
||||||
```bash
|
---
|
||||||
|
|
||||||
|
## What This Library Is — and Is Not
|
||||||
|
|
||||||
|
This library is deliberately conservative.
|
||||||
|
|
||||||
|
### 🚫 No `setjmp`, No `longjmp`
|
||||||
|
|
||||||
|
Many C error libraries attempt to simulate exceptions using `setjmp` / `longjmp`.
|
||||||
|
|
||||||
|
That approach is rejected here.
|
||||||
|
|
||||||
|
Non-local jumps:
|
||||||
|
|
||||||
|
- Bypass normal control flow
|
||||||
|
- Skip cleanup invisibly
|
||||||
|
- Break assumptions about stack lifetime
|
||||||
|
- Make reasoning about correctness harder, not easier
|
||||||
|
|
||||||
|
If control flow moves in `libsdlerror`, it does so because the source code says so.
|
||||||
|
|
||||||
|
Nothing jumps. Nothing unwinds itself. Nothing “just happens.”
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🚫 No Runtime Memory Allocation — Ever
|
||||||
|
|
||||||
|
`libsdlerror` never performs dynamic memory allocation at runtime.
|
||||||
|
|
||||||
|
It does not call:
|
||||||
|
|
||||||
|
- `malloc`
|
||||||
|
- `calloc`
|
||||||
|
- `realloc`
|
||||||
|
- `free`
|
||||||
|
|
||||||
|
There is no allocator dependency and no runtime allocation failure mode.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📌 Explicit Storage Model
|
||||||
|
|
||||||
|
All memory used by `libsdlerror` falls into one of two categories:
|
||||||
|
|
||||||
|
- **Automatic storage (stack)**
|
||||||
|
Caller-owned error contexts and local state
|
||||||
|
|
||||||
|
- **Static storage duration**
|
||||||
|
Fixed-size internal tables allocated at program load time
|
||||||
|
|
||||||
|
There is no heap allocation and no runtime resizing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ C Means C
|
||||||
|
|
||||||
|
- No compiler extensions
|
||||||
|
- No undefined behavior
|
||||||
|
- No optimizer tricks
|
||||||
|
|
||||||
|
If it compiles as C, it works as C.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ Designed for SDL, Not Against It
|
||||||
|
|
||||||
|
SDL already has an error system. It is simple, global, and fragile.
|
||||||
|
|
||||||
|
`libsdlerror` does not replace it. It captures SDL error state at the right moment, preserves context, and makes it usable without forcing error checks everywhere.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Six Guiding Principles
|
||||||
|
|
||||||
|
This library is opinionated, and intentionally so.
|
||||||
|
|
||||||
|
1. **Control flow must be explicit**
|
||||||
|
If execution moves, the source code must show why.
|
||||||
|
|
||||||
|
2. **No `setjmp` / `longjmp`**
|
||||||
|
Errors must not bypass the stack.
|
||||||
|
|
||||||
|
3. **No runtime memory allocation**
|
||||||
|
The library relies only on stack and static storage.
|
||||||
|
|
||||||
|
4. **The caller owns all state**
|
||||||
|
No hidden globals exposed to the user.
|
||||||
|
|
||||||
|
5. **Cleanup must be deterministic**
|
||||||
|
Cleanup always runs, exactly once.
|
||||||
|
|
||||||
|
6. **Errors are values, not side effects**
|
||||||
|
They are captured, inspected, propagated, or handled deliberately.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Build and Install
|
||||||
|
|
||||||
|
```sh
|
||||||
cmake -S . -B build
|
cmake -S . -B build
|
||||||
cmake --build build
|
cmake --build build
|
||||||
cmake --install build
|
cmake --install build
|
||||||
```
|
```
|
||||||
|
|
||||||
## Templating and autogenerated code
|
### Compile Your Program
|
||||||
|
|
||||||
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`. 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
|
|
||||||
|
|
||||||
Include it
|
|
||||||
|
|
||||||
```c
|
|
||||||
#include <akerror.h>
|
|
||||||
```
|
|
||||||
|
|
||||||
Link the library directly, or
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cc -lakerror
|
gcc `pkg-config --cflags sdlerror` main.c \
|
||||||
|
`pkg-config --libs sdlerror` \
|
||||||
|
-o app
|
||||||
```
|
```
|
||||||
|
|
||||||
Using pkg-config, or
|
### Minimal Example
|
||||||
|
|
||||||
```sh
|
|
||||||
pkg-config akerror --cflags
|
|
||||||
pkg-config akerror --ldflags
|
|
||||||
```
|
|
||||||
|
|
||||||
Using cmake:
|
|
||||||
|
|
||||||
```cmake
|
|
||||||
find_package(akerror REQUIRED)
|
|
||||||
pkg_check_modules(akerror REQUIRED akerror)
|
|
||||||
target_link_libraries(YOUR_TARGET PRIVATE akerror::akerror)
|
|
||||||
```
|
|
||||||
|
|
||||||
Using this project as a submodule with cmake:
|
|
||||||
|
|
||||||
```cmake
|
|
||||||
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
|
|
||||||
|
|
||||||
target_link_libraries(YOUR_PROJECT PRIVATE akerror::akerror)
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## (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
|
```c
|
||||||
PREPARE_ERROR(errctx);
|
#include <sdlerror.h>
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
int main(void) {
|
||||||
|
PREPARE_ERROR(err);
|
||||||
|
|
||||||
## Attempting an Operation
|
ATTEMPT {
|
||||||
|
CATCH(err, SDL_Init(SDL_INIT_VIDEO));
|
||||||
```c
|
CATCH(err, do_something_that_can_fail());
|
||||||
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_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 {
|
||||||
} CLEANUP {
|
SDL_Quit();
|
||||||
} 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);
|
PROCESS(err) {
|
||||||
|
fprintf(stderr, "Error: %s\n", ERROR_MESSAGE(err));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
FINISH(err, true);
|
||||||
|
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**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:
|
---
|
||||||
|
|
||||||
|
## User-Configurable Error Codes
|
||||||
|
|
||||||
|
While most aspects of `libsdlerror` are intentionally fixed, error codes themselves are user-defined.
|
||||||
|
|
||||||
|
### Defining `ERR_*` Codes
|
||||||
|
|
||||||
```c
|
```c
|
||||||
ATTEMPT {
|
#define ERR_OK 0
|
||||||
CATCH(errctx, process_all(items, n)); // a single CATCH, not looped
|
#define ERR_SDL 1
|
||||||
} CLEANUP {
|
#define ERR_IO 2
|
||||||
// ... always runs ...
|
#define ERR_CONFIG 3
|
||||||
} PROCESS(errctx) {
|
|
||||||
} HANDLE(errctx, AKERR_VALUE) {
|
|
||||||
// ... handle a failure from any iteration ...
|
|
||||||
} FINISH(errctx, true);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# Passing errors
|
These are simple integer status values.
|
||||||
|
|
||||||
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.
|
---
|
||||||
|
|
||||||
```
|
### `MAX_ERR_VALUE`
|
||||||
PREPARE_ERROR(e);
|
|
||||||
PASS(e, some_method_that_may_fail());
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
```
|
|
||||||
|
|
||||||
This does the same thing as this, but with less code:
|
`MAX_ERR_VALUE` must be greater than or equal to the highest `ERR_*` value you define.
|
||||||
|
|
||||||
```
|
It determines the size of internal static tables used to map error codes to names.
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(e, some_method_that_may_fail());
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} FINISH(e, true);
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
```
|
|
||||||
|
|
||||||
# Handling errors
|
If you add new error codes, you must update `MAX_ERR_VALUE` and recompile.
|
||||||
|
|
||||||
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
|
### Naming Errors: `error_name_for_status`
|
||||||
|
|
||||||
In order to handle a specific error code, use the `HANDLE` macro.
|
|
||||||
|
|
||||||
```c
|
```c
|
||||||
} PROCESS(errctx) {
|
error_name_for_status(ERR_IO, "I/O error");
|
||||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
error_name_for_status(ERR_CONFIG, "Configuration error");
|
||||||
// Something is complaining about a null pointer error. Do something about it.
|
|
||||||
} // ...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Handling a group of errors with HANDLE_GROUP
|
Names are copied into fixed-size static buffers and persist for the lifetime of the program.
|
||||||
|
|
||||||
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:
|
No allocation occurs.
|
||||||
|
|
||||||
```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.
|
## Appendix: Memory Model
|
||||||
|
|
||||||
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.
|
### Storage Duration Overview
|
||||||
|
|
||||||
```c
|
`libsdlerror` uses only automatic (stack) storage and static storage duration.
|
||||||
} 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 *
|
No heap allocation occurs at runtime.
|
||||||
|
|
||||||
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
|
### Compile-Time Limits (`MAX_ERR*`)
|
||||||
|
|
||||||
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.
|
The `MAX_ERR*` macros in `sdlerror.h` define fixed upper bounds for:
|
||||||
|
|
||||||
```c
|
- Error message length
|
||||||
PREPARE_ERROR(errctx);
|
- Error name length
|
||||||
ATTEMPT {
|
- Stacktrace buffer size
|
||||||
// ... stuff
|
- Number of error contexts
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} FINISH(errctx, true);
|
|
||||||
SUCCEED_RETURN(errctx);
|
|
||||||
```
|
|
||||||
|
|
||||||
## FAIL_RETURN
|
These values are not user-tunable at runtime.
|
||||||
|
|
||||||
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.
|
If you need different limits, you are expected to edit the header and recompile.
|
||||||
|
|
||||||
The function allows you to provide printf-style variable arguments to provide a meaningful failure message.
|
---
|
||||||
|
|
||||||
```c
|
### Static Memory Usage (x86_64)
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
FAIL_RETURN(AKERR_BEHAVIOR, "Something went horribly wrong!")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Conditionally failing and returning
|
On a typical x86_64 system:
|
||||||
|
|
||||||
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.
|
- Error name table: ~1 KB
|
||||||
|
- Static error pool: ~450 KB
|
||||||
|
- Miscellaneous globals: a few KB
|
||||||
|
|
||||||
```c
|
**Total static footprint: ~470 KB**
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
FAIL_ZERO_RETURN(errctx, (somePointer == NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
|
|
||||||
```
|
|
||||||
|
|
||||||
```c
|
All of this memory is allocated at program load time.
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
FAIL_NONZERO_RETURN(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")
|
|
||||||
```
|
|
||||||
|
|
||||||
# Uncaught errors
|
---
|
||||||
|
|
||||||
## Misbehaving methods
|
## FAQ
|
||||||
|
|
||||||
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.
|
### Why Use Static Globals Instead of the Heap?
|
||||||
|
|
||||||
## Ensuring that all error codes are captured
|
Because error handling infrastructure must not itself fail.
|
||||||
|
|
||||||
Any function which returns `akerr_ErrorContext *` should also be marked with `AKERROR_NOIGNORE`.
|
Heap allocation introduces failure modes, ordering problems, and unpredictability.
|
||||||
|
|
||||||
```c
|
Static storage avoids all of this.
|
||||||
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.
|
The globals used by `libsdlerror` are fixed-size, initialized at load time, and free of allocator dependencies.
|
||||||
|
|
||||||
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
|
### Why Not Allocate Per-Error Structures Dynamically?
|
||||||
#define AKERROR_NOIGNORE __attribute__((warn_unused_result))
|
|
||||||
```
|
|
||||||
|
|
||||||
## Stack Traces
|
Because error handling must work when memory is tight, during early startup, and during shutdown.
|
||||||
|
|
||||||
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.
|
Dynamic allocation undermines all three.
|
||||||
|
|
||||||
Consider the `tests/err_trace.c` program which intentionally triggers this behavior. It produces output like this:
|
---
|
||||||
|
|
||||||
```
|
## Summary
|
||||||
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:
|
`libsdlerror` is deliberately conservative.
|
||||||
|
|
||||||
* The last line printed is the `FINISH` macro call that triggered the stacktrace.
|
No jumps.
|
||||||
* Above that, the `CATCH()` inside of `main()` which caught the exception from `func1()` but did not handle it
|
No heap allocation.
|
||||||
* Above that, a statement that the error was detected in the `CATCH()` statement at the same line
|
No hidden control flow.
|
||||||
* 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()`
|
Just explicit, disciplined error handling — the way C has always worked when used correctly.
|
||||||
* 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
|
|
||||||
|
|
||||||
|
|||||||
93
TODO.md
93
TODO.md
@@ -1,93 +0,0 @@
|
|||||||
# TODO
|
|
||||||
|
|
||||||
Working notes for `libakerror`. Outstanding items only.
|
|
||||||
|
|
||||||
## 1. The test suite has no sanitizer run
|
|
||||||
|
|
||||||
Mutation testing caught an out-of-bounds probe in the status-name hash table
|
|
||||||
that the suite could not: the failure mode was a write into adjacent BSS, which
|
|
||||||
does not crash, so every test still passed. Sharpening one test closed that
|
|
||||||
instance, but ASan would have caught the whole class directly and independently
|
|
||||||
of how sharp the assertions are.
|
|
||||||
|
|
||||||
Add a `-fsanitize=address,undefined` build to `.gitea/workflows/ci.yaml`, or a
|
|
||||||
CMake option alongside `AKERR_COVERAGE`. This is the highest-value item here: it
|
|
||||||
covers the whole library, not just the registry, and the library's fixed pools
|
|
||||||
and manual buffer arithmetic are exactly what it is good at.
|
|
||||||
|
|
||||||
## 2. `HANDLE`-level status aliasing is still undetectable
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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`.
|
|
||||||
|
|
||||||
## 3. No registry introspection
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## 4. No way to release a reservation
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## 5. The registry is not thread safe
|
|
||||||
|
|
||||||
Global mutable state, no locking, and `akerr_status_name_count++` is not atomic.
|
|
||||||
Currently documented as an initialization-time-only API rather than enforced. If
|
|
||||||
components start initializing on separate threads this needs either a lock or a
|
|
||||||
documented once-per-process init barrier.
|
|
||||||
|
|
||||||
## 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 — worth doing when the CMake gains a
|
|
||||||
sanitizer variant (item 1), since that adds the same machinery.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
208
UPGRADING.md
208
UPGRADING.md
@@ -1,208 +0,0 @@
|
|||||||
# Upgrade notice: custom status codes (1.0.0)
|
|
||||||
|
|
||||||
Version 1.0.0 replaces the consumer-sized status-name array with a private
|
|
||||||
registry, and makes status-code ownership explicit and enforced. This is a
|
|
||||||
source and ABI break. The library now carries a version and an soname
|
|
||||||
(`libakerror.so.1`), so a stale installed library can no longer be silently
|
|
||||||
paired with a newer header — but anything built against a pre-1.0.0 header must
|
|
||||||
be rebuilt.
|
|
||||||
|
|
||||||
What was removed:
|
|
||||||
|
|
||||||
* `AKERR_MAX_ERR_VALUE` — the registry is sparse and accepts any `int`, so
|
|
||||||
consumers no longer size it. Delete every compile definition and source
|
|
||||||
reference. A stale `-DAKERR_MAX_ERR_VALUE=...` is now harmless but useless.
|
|
||||||
* `__AKERR_ERROR_NAMES` — the name table is private to the library. Code that
|
|
||||||
touched this data symbol was using an undocumented interface; use
|
|
||||||
`akerr_name_for_status()`.
|
|
||||||
* `AKERR_STATUS_RANGE_OK` and `AKERR_STATUS_NAME_OK` — the registry functions no
|
|
||||||
longer return an `int`. Success is a `NULL` `akerr_ErrorContext *`, like every
|
|
||||||
other function in the library. See "The registry raises errors" below.
|
|
||||||
|
|
||||||
To migrate:
|
|
||||||
|
|
||||||
1. Rebuild libakerror and every dependent library against the new header.
|
|
||||||
2. Move custom status codes out of the reserved `0`–`255` band. Use fixed
|
|
||||||
integer constants beginning at `AKERR_FIRST_CONSUMER_STATUS` (256) rather
|
|
||||||
than offsets from `AKERR_LAST_ERRNO_VALUE`, so a libc that grows an errno
|
|
||||||
cannot move your codes.
|
|
||||||
3. Assign a distinct range to every library that may coexist in one process, and
|
|
||||||
coordinate those ranges at the application or dependency-stack level.
|
|
||||||
4. Reserve the complete range with `akerr_reserve_status_range()` during
|
|
||||||
initialization, and treat the error it returns like any other error: `CATCH`
|
|
||||||
it, `PASS` it, or let it propagate out of your init function.
|
|
||||||
5. Register names with `akerr_register_status_name()`, passing the same owner
|
|
||||||
string you reserved with. **You can no longer name a status you have not
|
|
||||||
reserved** — see "Ownership is enforced" below.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
```c
|
|
||||||
enum {
|
|
||||||
MYLIB_ERR_BASE = AKERR_FIRST_CONSUMER_STATUS, /* 256 */
|
|
||||||
MYLIB_ERR_PARSE = MYLIB_ERR_BASE,
|
|
||||||
MYLIB_ERR_STORAGE,
|
|
||||||
MYLIB_ERR_LIMIT
|
|
||||||
};
|
|
||||||
|
|
||||||
#define MYLIB_OWNER "mylib"
|
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *mylib_init(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
|
|
||||||
/* Any collision propagates out of mylib_init() to the caller. */
|
|
||||||
PASS(errctx, akerr_reserve_status_range(MYLIB_ERR_BASE,
|
|
||||||
MYLIB_ERR_LIMIT - MYLIB_ERR_BASE,
|
|
||||||
MYLIB_OWNER));
|
|
||||||
PASS(errctx, akerr_register_status_name(MYLIB_OWNER, MYLIB_ERR_PARSE,
|
|
||||||
"Parse Error"));
|
|
||||||
PASS(errctx, akerr_register_status_name(MYLIB_OWNER, MYLIB_ERR_STORAGE,
|
|
||||||
"Storage Error"));
|
|
||||||
SUCCEED_RETURN(errctx);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
If your `init` cannot return an `akerr_ErrorContext *` — a C API with an `int`
|
|
||||||
return, say — catch the error and convert it. Close with `FINISH_NORETURN`
|
|
||||||
rather than `FINISH`: nothing can propagate out of a function that does not
|
|
||||||
return an error context, and `FINISH(errctx, false)` in an `int`-returning
|
|
||||||
function compiles the (dead) propagation branch anyway, which warns.
|
|
||||||
|
|
||||||
```c
|
|
||||||
int mylib_init(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
int rc = MYLIB_INIT_OK;
|
|
||||||
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(errctx, akerr_reserve_status_range(MYLIB_ERR_BASE,
|
|
||||||
MYLIB_ERR_LIMIT - MYLIB_ERR_BASE,
|
|
||||||
MYLIB_OWNER));
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} HANDLE_DEFAULT(errctx) {
|
|
||||||
LOG_ERROR_WITH_MESSAGE(errctx, "mylib could not claim its status range");
|
|
||||||
rc = MYLIB_INIT_FAILED; /* another component owns part of the range */
|
|
||||||
} FINISH_NORETURN(errctx);
|
|
||||||
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
You do not need to call `akerr_init()` first. Every registry entry point calls
|
|
||||||
it for you, so a library that reserves its range before anything else in the
|
|
||||||
process has touched libakerror keeps that reservation. (Before 1.0.0 this was
|
|
||||||
silently destructive: `akerr_init()` cleared the tables, so a reservation made
|
|
||||||
too early was discarded and the next component to claim the same range was told
|
|
||||||
it was free.)
|
|
||||||
|
|
||||||
## Ownership is enforced
|
|
||||||
|
|
||||||
Reserving a range is no longer advisory bookkeeping. A status may only be named
|
|
||||||
from inside a reservation:
|
|
||||||
|
|
||||||
* `akerr_register_status_name(owner, status, name)` requires that `status` fall
|
|
||||||
in a range reserved by `owner`. Naming another component's status fails with
|
|
||||||
`AKERR_STATUS_NAME_FOREIGN`, and naming a status nobody reserved fails with
|
|
||||||
`AKERR_STATUS_NAME_UNRESERVED`.
|
|
||||||
* The two-argument `akerr_name_for_status(status, name)` set path still works,
|
|
||||||
but it cannot identify its caller, so it can only require that *some*
|
|
||||||
reservation covers the status. Prefer the owned form: it is the one that
|
|
||||||
catches a component writing into a range that is not its own.
|
|
||||||
|
|
||||||
Every refusal is reported, because a name that fails to register degrades that
|
|
||||||
code to `"Unknown Error"` in every later stack trace.
|
|
||||||
|
|
||||||
This detects *name* collisions. It cannot detect two components compiling the
|
|
||||||
same integer into a `HANDLE` label without ever registering a name, so every
|
|
||||||
co-resident library should still reserve its range.
|
|
||||||
|
|
||||||
## The registry raises errors
|
|
||||||
|
|
||||||
`akerr_reserve_status_range()` and `akerr_register_status_name()` return
|
|
||||||
`akerr_ErrorContext *`, exactly like the rest of the library. They are marked
|
|
||||||
`AKERR_NOIGNORE`, so discarding the result is a compile-time warning, and an
|
|
||||||
error you catch but do not handle propagates out of your init function instead
|
|
||||||
of leaving you with a range you do not actually own.
|
|
||||||
|
|
||||||
```c
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(errctx, akerr_reserve_status_range(256, 16, MYLIB_OWNER));
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} HANDLE(errctx, AKERR_STATUS_RANGE_OVERLAP) {
|
|
||||||
/* Another component owns part of it -- the message names which. */
|
|
||||||
} FINISH(errctx, true);
|
|
||||||
```
|
|
||||||
|
|
||||||
`akerr_reserve_status_range()` raises:
|
|
||||||
|
|
||||||
| Status | Meaning |
|
|
||||||
| --- | --- |
|
|
||||||
| — (returns `NULL`) | Range reserved, or an identical range was already reserved by the same owner |
|
|
||||||
| `AKERR_STATUS_RANGE_OVERLAP` | Part of the range is owned by someone else (the message names the owner) |
|
|
||||||
| `AKERR_STATUS_RANGE_FULL` | No reservation slots remain |
|
|
||||||
| `AKERR_STATUS_RANGE_INVALID` | `count < 1`, NULL/empty/over-long owner, or the range overflows `int` |
|
|
||||||
|
|
||||||
`akerr_register_status_name()` raises:
|
|
||||||
|
|
||||||
| Status | Meaning |
|
|
||||||
| --- | --- |
|
|
||||||
| — (returns `NULL`) | Name registered |
|
|
||||||
| `AKERR_STATUS_NAME_UNRESERVED` | No reservation contains this status |
|
|
||||||
| `AKERR_STATUS_NAME_FOREIGN` | The status is in a range owned by someone else |
|
|
||||||
| `AKERR_STATUS_NAME_FULL` | The name registry is full |
|
|
||||||
| `AKERR_STATUS_NAME_INVALID` | NULL/empty/over-long owner, or a NULL name |
|
|
||||||
|
|
||||||
These are ordinary status codes inside the library's reserved band, so they can
|
|
||||||
be matched with `HANDLE`, grouped with `HANDLE_GROUP`, and they print with a
|
|
||||||
name and a stack trace when they go unhandled.
|
|
||||||
|
|
||||||
`akerr_name_for_status(status, name)` is the one exception: it returns a name
|
|
||||||
rather than an error context, so it cannot raise. A refused registration through
|
|
||||||
that path is reported through `akerr_log_method` and reads back as
|
|
||||||
`"Unknown Error"`.
|
|
||||||
|
|
||||||
Repeating an identical reservation for the same owner is a no-op. A *subset* or
|
|
||||||
*superset* of your own range is not — it raises `AKERR_STATUS_RANGE_OVERLAP`.
|
|
||||||
Reserve the whole range in one call.
|
|
||||||
|
|
||||||
The library holds itself to the same rule at startup: if `akerr_init()` cannot
|
|
||||||
reserve its own `0`–`255` band or name one of its own codes, it logs the failure
|
|
||||||
and terminates the program. That can only happen in a misconfigured build (a
|
|
||||||
name table too small for the library's own entries), and the alternative is a
|
|
||||||
process whose stack traces silently read `"Unknown Error"` for built-in codes.
|
|
||||||
|
|
||||||
## Capacity
|
|
||||||
|
|
||||||
Both tables are fixed size, allocated in BSS, and never grow:
|
|
||||||
|
|
||||||
| Limit | Default | Build-time override |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Status names | 3072 usable (4096 slots, 75% load) | `-DAKERR_STATUS_NAME_SLOTS=<power of two>` |
|
|
||||||
| Reserved ranges | 64 | `-DAKERR_MAX_RESERVED_STATUS_RANGES=<n>` |
|
|
||||||
|
|
||||||
`akerr_init()` consumes one name slot per host errno value plus one per
|
|
||||||
`AKERR_*` code — around 150 on glibc, leaving roughly 2900 for all consumers in
|
|
||||||
the process to share. That figure varies with the host libc, so treat it as
|
|
||||||
approximate rather than a budget to fill.
|
|
||||||
|
|
||||||
Both overrides are CMake cache variables and apply `PRIVATE` to the library
|
|
||||||
target. The tables live entirely inside `src/error.c`, so raising them changes
|
|
||||||
nothing a consumer can observe — unlike the `AKERR_MAX_ERR_VALUE` they replaced,
|
|
||||||
where a mismatch between the library and its consumers corrupted memory. Set
|
|
||||||
them when configuring libakerror itself:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cmake -S . -B build -DAKERR_STATUS_NAME_SLOTS=16384
|
|
||||||
```
|
|
||||||
|
|
||||||
Exhausting either table raises an error to the caller; it is never silent.
|
|
||||||
|
|
||||||
## Thread safety
|
|
||||||
|
|
||||||
The registry is process-global mutable state with no locking. Reserve ranges and
|
|
||||||
register names during single-threaded initialization, before spawning threads.
|
|
||||||
Lookups (`akerr_name_for_status(status, NULL)`) are safe to call concurrently
|
|
||||||
once registration has finished.
|
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
include(CMakeFindDependencyMacro) # If your library has dependencies
|
include(CMakeFindDependencyMacro) # If your library has dependencies
|
||||||
# find_dependency(AnotherDependency REQUIRED) # Example dependency
|
# find_dependency(AnotherDependency REQUIRED) # Example dependency
|
||||||
|
|
||||||
include("${CMAKE_CURRENT_LIST_DIR}/akerrorTargets.cmake")
|
include("${CMAKE_CURRENT_LIST_DIR}/sdlerror.cmake")
|
||||||
@@ -1,364 +0,0 @@
|
|||||||
#ifndef _AKERR_H_
|
|
||||||
#define _AKERR_H_
|
|
||||||
|
|
||||||
#if (defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1) || (!defined(AKERR_USE_STDLIB))
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <limits.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// FIXME: This is huge now. It used to be 1000 bytes, then I wanted to report errors
|
|
||||||
// related to filesystem paths, which made it grow beyond PATH_MAX, then I started
|
|
||||||
// reporting messages including 2 file paths (PATH_MAX * 2), so now to make the compiler warnings
|
|
||||||
// shut up, it's enormous (PATH_MAX*3).
|
|
||||||
#define AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH 12384
|
|
||||||
|
|
||||||
#define AKERR_MAX_ERROR_NAME_LENGTH 64
|
|
||||||
#define AKERR_MAX_ERROR_FNAME_LENGTH PATH_MAX
|
|
||||||
#define AKERR_MAX_ERROR_FUNCTION_LENGTH 128
|
|
||||||
#define AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH (AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH + AKERR_MAX_ERROR_NAME_LENGTH + AKERR_MAX_ERROR_FNAME_LENGTH + AKERR_MAX_ERROR_FUNCTION_LENGTH + 16)
|
|
||||||
|
|
||||||
#define AKERR_LAST_ERRNO_VALUE AKERR_LAST_ERRNO_VALUE_SED
|
|
||||||
|
|
||||||
#define AKERR_NULLPOINTER (AKERR_LAST_ERRNO_VALUE + 1) /** A pointer had a NULL value where such was not permissible */
|
|
||||||
#define AKERR_OUTOFBOUNDS (AKERR_LAST_ERRNO_VALUE + 2) /** Attempt to access a datastructure outside of bounds */
|
|
||||||
#define AKERR_API (AKERR_LAST_ERRNO_VALUE + 3) /** An otherwise unspecified API contract has been violated */
|
|
||||||
#define AKERR_ATTRIBUTE (AKERR_LAST_ERRNO_VALUE + 4) /** Relates to accessing of attributes on objects */
|
|
||||||
#define AKERR_TYPE (AKERR_LAST_ERRNO_VALUE + 5) /** An object had the incorrect type */
|
|
||||||
#define AKERR_KEY (AKERR_LAST_ERRNO_VALUE + 6) /** A key was either invalid for or not present in a map */
|
|
||||||
#define AKERR_INDEX (AKERR_LAST_ERRNO_VALUE + 8) /** An error occurred when attempting to index an indexable datastructure (other than out of bounds) */
|
|
||||||
#define AKERR_FORMAT (AKERR_LAST_ERRNO_VALUE + 9) /** An error occurred in the formatting of an object (usually a string) */
|
|
||||||
#define AKERR_IO (AKERR_LAST_ERRNO_VALUE + 10) /** An unspecified IO error occurred. */
|
|
||||||
#define AKERR_VALUE (AKERR_LAST_ERRNO_VALUE + 11) /** A provided value was invalid */
|
|
||||||
#define AKERR_RELATIONSHIP (AKERR_LAST_ERRNO_VALUE + 12) /** An error occurred in establishing, maintaining or severing a relationship between two objects */
|
|
||||||
#define AKERR_EOF (AKERR_LAST_ERRNO_VALUE + 13) /** The end of a stream or file has been encountered */
|
|
||||||
#define AKERR_CIRCULAR_REFERENCE (AKERR_LAST_ERRNO_VALUE + 14) /** Indicates that a circular reference has been found in a linked list */
|
|
||||||
#define AKERR_ITERATOR_BREAK (AKERR_LAST_ERRNO_VALUE + 15) /** Used to prematurely end an iteration cycle (such as when searching a graph and the desired node has been found) */
|
|
||||||
#define AKERR_NOT_IMPLEMENTED (AKERR_LAST_ERRNO_VALUE + 16) /** A method was called that is defined but not currently implemented */
|
|
||||||
#define AKERR_BADEXC (AKERR_LAST_ERRNO_VALUE + 17) /** The libakerr library was given an akerr_ErrorContext to parse that did not come from AKERR_ARRAY_ERROR (likely an uninitialized pointer) */
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Registry failures. These are ordinary status codes, not a private return
|
|
||||||
* enumeration: akerr_reserve_status_range() and akerr_register_status_name()
|
|
||||||
* return akerr_ErrorContext * like everything else in this library, so a refused
|
|
||||||
* reservation can be CATCH-ed, HANDLE-d, PASS-ed, or left unhandled to produce a
|
|
||||||
* stack trace and stop the program. Success returns NULL.
|
|
||||||
*/
|
|
||||||
#define AKERR_STATUS_RANGE_OVERLAP (AKERR_LAST_ERRNO_VALUE + 18) /** Some part of the range is already owned by someone else */
|
|
||||||
#define AKERR_STATUS_RANGE_FULL (AKERR_LAST_ERRNO_VALUE + 19) /** No reservation slots remain (see AKERR_MAX_RESERVED_STATUS_RANGES) */
|
|
||||||
#define AKERR_STATUS_RANGE_INVALID (AKERR_LAST_ERRNO_VALUE + 20) /** Bad count, bad owner string, or the range overflows int */
|
|
||||||
#define AKERR_STATUS_NAME_UNRESERVED (AKERR_LAST_ERRNO_VALUE + 21) /** No owner has reserved a range containing this status */
|
|
||||||
#define AKERR_STATUS_NAME_FOREIGN (AKERR_LAST_ERRNO_VALUE + 22) /** The status lies in a range reserved by a different owner */
|
|
||||||
#define AKERR_STATUS_NAME_FULL (AKERR_LAST_ERRNO_VALUE + 23) /** The name registry is full (raise AKERR_STATUS_NAME_SLOTS) */
|
|
||||||
#define AKERR_STATUS_NAME_INVALID (AKERR_LAST_ERRNO_VALUE + 24) /** NULL/empty/over-long owner, or a NULL name */
|
|
||||||
|
|
||||||
/* The last status the library defines for itself. Everything from
|
|
||||||
* AKERR_LAST_ERRNO_VALUE + 1 through here must have a registered name. */
|
|
||||||
#define AKERR_LAST_LIBRARY_STATUS AKERR_STATUS_NAME_INVALID
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Status values 0 through 255 are reserved by libakerror at akerr_init() time:
|
|
||||||
* the host's errno values plus the AKERR_* codes above. Consumers allocate from
|
|
||||||
* 256 upwards. Reserving any part of this band fails with
|
|
||||||
* AKERR_STATUS_RANGE_OVERLAP naming AKERR_LIBRARY_OWNER.
|
|
||||||
*/
|
|
||||||
#define AKERR_LIBRARY_OWNER "libakerror"
|
|
||||||
#define AKERR_RESERVED_STATUS_COUNT 256
|
|
||||||
#define AKERR_FIRST_CONSUMER_STATUS AKERR_RESERVED_STATUS_COUNT
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The library reserves status values 0 through 255 for itself (see akerr_init),
|
|
||||||
* which must contain every AKERR_* code above. AKERR_LAST_ERRNO_VALUE is
|
|
||||||
* derived from the host's errno list at build time, so on a platform with an
|
|
||||||
* unusually large errno space these codes could escape the band and collide
|
|
||||||
* with consumer codes allocated at 256. Fail the build instead.
|
|
||||||
*/
|
|
||||||
typedef char akerr_assert_codes_within_reserved_band[(AKERR_LAST_LIBRARY_STATUS < 256) ? 1 : -1];
|
|
||||||
|
|
||||||
#define AKERR_MAX_ARRAY_ERROR 128
|
|
||||||
|
|
||||||
|
|
||||||
typedef struct
|
|
||||||
{
|
|
||||||
char message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH];
|
|
||||||
int arrayid;
|
|
||||||
int status;
|
|
||||||
bool handled;
|
|
||||||
int refcount;
|
|
||||||
char fname[AKERR_MAX_ERROR_FNAME_LENGTH];
|
|
||||||
char function[AKERR_MAX_ERROR_FNAME_LENGTH];
|
|
||||||
int lineno;
|
|
||||||
bool reported;
|
|
||||||
char stacktracebuf[AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH];
|
|
||||||
char *stacktracebufptr;
|
|
||||||
} akerr_ErrorContext;
|
|
||||||
|
|
||||||
#define AKERR_NOIGNORE __attribute__((warn_unused_result))
|
|
||||||
|
|
||||||
typedef void (*akerr_ErrorUnhandledErrorHandler)(akerr_ErrorContext *errctx);
|
|
||||||
typedef void (*akerr_ErrorLogFunction)(const char *f, ...);
|
|
||||||
|
|
||||||
extern akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR];
|
|
||||||
extern akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
|
|
||||||
extern akerr_ErrorLogFunction akerr_log_method;
|
|
||||||
extern akerr_ErrorContext *__akerr_last_ignored;
|
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_release_error(akerr_ErrorContext *ptr);
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_next_error();
|
|
||||||
/*
|
|
||||||
* Look up (name == NULL) or register (name != NULL) the display name for a
|
|
||||||
* status. Registration succeeds only if some owner has reserved a range
|
|
||||||
* containing `status`; prefer akerr_register_status_name(), which also checks
|
|
||||||
* that the range belongs to you and raises an error saying why a registration
|
|
||||||
* was refused. This entry point cannot return an error context, so a refusal
|
|
||||||
* here is reported through akerr_log_method and reads back as "Unknown Error".
|
|
||||||
* Never returns NULL -- an unregistered status reads back as "Unknown Error".
|
|
||||||
*/
|
|
||||||
char *akerr_name_for_status(int status, char *name);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Register a display name for a status you own. `owner` must match the owner
|
|
||||||
* string passed to akerr_reserve_status_range() for the range containing
|
|
||||||
* `status`. Returns NULL on success, or an error context whose status is one of
|
|
||||||
* the AKERR_STATUS_NAME_* codes above -- CATCH it, HANDLE it, or let it
|
|
||||||
* propagate.
|
|
||||||
*/
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_register_status_name(const char *owner, int status, const char *name);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Claim `count` status values starting at `first_status` for `owner`. Repeating
|
|
||||||
* an identical reservation for the same owner is a no-op; any other collision is
|
|
||||||
* refused. Returns NULL on success, or an error context whose status is one of
|
|
||||||
* the AKERR_STATUS_RANGE_* codes above. Treat any error as an initialization
|
|
||||||
* failure: either handle it or let it propagate out of your init function.
|
|
||||||
*/
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_reserve_status_range(int first_status, int count, const char *owner);
|
|
||||||
void akerr_init();
|
|
||||||
void akerr_default_handler_unhandled_error(akerr_ErrorContext *ptr);
|
|
||||||
void akerr_default_logger(const char *f, ...);
|
|
||||||
int akerr_valid_error_address(akerr_ErrorContext *ptr);
|
|
||||||
/* defined in src/errno.c which is built dynamically at build time from system errno definitions */
|
|
||||||
void akerr_init_errno(void);
|
|
||||||
/*
|
|
||||||
* Internal. Names a status in the library's own reserved band on behalf of
|
|
||||||
* akerr_init() and the generated errno table, which have no caller to raise
|
|
||||||
* into: a failure here is logged and terminates the program. Not part of the
|
|
||||||
* consumer API -- use akerr_register_status_name(), which raises instead.
|
|
||||||
*/
|
|
||||||
void __akerr_name_library_status(int status, const char *name);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Internal. Bounded string copy into a fixed buffer, always NUL-terminated.
|
|
||||||
* Raises AKERR_NULLPOINTER for a NULL destination or source and AKERR_VALUE for
|
|
||||||
* a capacity that leaves no room for a terminator. Exported so the library's
|
|
||||||
* own tests can drive those guards; not part of the consumer API.
|
|
||||||
*/
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *__akerr_copy_string(char *destination, int capacity,
|
|
||||||
const char *source);
|
|
||||||
|
|
||||||
#define LOG_ERROR_WITH_MESSAGE(__err_context, __err_message) \
|
|
||||||
akerr_log_method("%s%s:%s:%d: %s %d (%s): %s", (char *)&__err_context->stacktracebuf, (char *)__FILE__, (char *)__func__, __LINE__, __err_message, __err_context->status, akerr_name_for_status(__err_context->status, NULL), __err_context->message); \
|
|
||||||
|
|
||||||
#define LOG_ERROR(__err_context) \
|
|
||||||
LOG_ERROR_WITH_MESSAGE(__err_context, "");
|
|
||||||
|
|
||||||
#define RELEASE_ERROR(__err_context) \
|
|
||||||
if ( __err_context != NULL ) { \
|
|
||||||
__err_context = akerr_release_error(__err_context); \
|
|
||||||
}
|
|
||||||
|
|
||||||
#define PREPARE_ERROR(__err_context) \
|
|
||||||
akerr_init(); \
|
|
||||||
akerr_ErrorContext __attribute__ ((unused)) *__err_context = NULL;
|
|
||||||
|
|
||||||
#define ENSURE_ERROR_READY(__err_context) \
|
|
||||||
if ( __err_context == NULL ) { \
|
|
||||||
__err_context = akerr_next_error(); \
|
|
||||||
if ( __err_context == NULL ) { \
|
|
||||||
akerr_log_method("%s:%s:%d: Unable to pull an error context from the array!", __FILE__, (char *)__func__, __LINE__); \
|
|
||||||
exit(1); \
|
|
||||||
} \
|
|
||||||
__err_context->refcount += 1; \
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Append a formatted line to the error's stack-trace buffer, bounded by the
|
|
||||||
* space that remains so a deep propagation chain cannot write past the end of
|
|
||||||
* stacktracebuf. snprintf reports the length it *would* have written, which on
|
|
||||||
* truncation exceeds what it actually wrote, so the cursor advance is clamped
|
|
||||||
* to the remaining space.
|
|
||||||
*/
|
|
||||||
#define AKERR_STACKTRACE_APPEND(__err_context, ...) \
|
|
||||||
do { \
|
|
||||||
char *__akerr_stb = (char *)__err_context->stacktracebuf; \
|
|
||||||
size_t __akerr_used = (size_t)(__err_context->stacktracebufptr - __akerr_stb); \
|
|
||||||
if ( __akerr_used < AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH ) { \
|
|
||||||
size_t __akerr_rem = AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH - __akerr_used; \
|
|
||||||
int __akerr_n = snprintf(__err_context->stacktracebufptr, __akerr_rem, __VA_ARGS__); \
|
|
||||||
if ( __akerr_n < 0 ) { \
|
|
||||||
__akerr_n = 0; \
|
|
||||||
} \
|
|
||||||
__err_context->stacktracebufptr += ((size_t)__akerr_n < __akerr_rem) \
|
|
||||||
? (size_t)__akerr_n : (__akerr_rem - 1); \
|
|
||||||
} \
|
|
||||||
} while ( 0 )
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Failure and success methods for functions that return akerr_ErrorContext *
|
|
||||||
*/
|
|
||||||
|
|
||||||
#define FAIL_ZERO_RETURN(__err_context, __x, __err, __message, ...) \
|
|
||||||
if ( __x == 0 ) { \
|
|
||||||
FAIL(__err_context, __err, __message, ##__VA_ARGS__); \
|
|
||||||
return __err_context; \
|
|
||||||
}
|
|
||||||
|
|
||||||
#define FAIL_NONZERO_RETURN(__err_context, __x, __err, __message, ...) \
|
|
||||||
if ( __x != 0 ) { \
|
|
||||||
FAIL(__err_context, __err, __message, ##__VA_ARGS__); \
|
|
||||||
return __err_context; \
|
|
||||||
}
|
|
||||||
|
|
||||||
#define FAIL_RETURN(__err_context, __err, __message, ...) \
|
|
||||||
FAIL(__err_context, __err, __message, ##__VA_ARGS__); \
|
|
||||||
return __err_context;
|
|
||||||
|
|
||||||
#define SUCCEED_RETURN(__err_context) \
|
|
||||||
RELEASE_ERROR(__err_context); \
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Failure and success methods for use inside of ATTEMPT() blocks
|
|
||||||
*/
|
|
||||||
|
|
||||||
#define FAIL_ZERO_BREAK(__err_context, __x, __err, __message, ...) \
|
|
||||||
if ( __x == 0 ) { \
|
|
||||||
FAIL(__err_context, __err, __message, ##__VA_ARGS__); \
|
|
||||||
break; \
|
|
||||||
}
|
|
||||||
|
|
||||||
#define FAIL_NONZERO_BREAK(__err_context, __x, __err, __message, ...) \
|
|
||||||
if ( __x != 0 ) { \
|
|
||||||
FAIL(__err_context, __err, __message, ##__VA_ARGS__); \
|
|
||||||
break; \
|
|
||||||
}
|
|
||||||
|
|
||||||
#define FAIL_BREAK(__err_context, __err_, __message, ...) \
|
|
||||||
FAIL(__err_context, __err_, __message, ##__VA_ARGS__); \
|
|
||||||
break;
|
|
||||||
|
|
||||||
#define SUCCEED_BREAK(__err_context) \
|
|
||||||
SUCCEED(__err_context); \
|
|
||||||
break;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* General failure and success methods
|
|
||||||
*/
|
|
||||||
|
|
||||||
#define FAIL(__err_context, __err, __message, ...) \
|
|
||||||
ENSURE_ERROR_READY(__err_context); \
|
|
||||||
__err_context->status = __err; \
|
|
||||||
snprintf((char *)__err_context->fname, AKERR_MAX_ERROR_FNAME_LENGTH, "%s", __FILE__); \
|
|
||||||
snprintf((char *)__err_context->function, AKERR_MAX_ERROR_FUNCTION_LENGTH, "%s", __func__); \
|
|
||||||
__err_context->lineno = __LINE__; \
|
|
||||||
snprintf((char *)__err_context->message, AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH, __message, ## __VA_ARGS__); \
|
|
||||||
AKERR_STACKTRACE_APPEND(__err_context, "%s:%s:%d: %d (%s) : %s\n", (char *)__err_context->fname, (char *)__err_context->function, __err_context->lineno, __err_context->status, akerr_name_for_status(__err_context->status, NULL), (__err_context->message == NULL ? "" : __err_context->message));
|
|
||||||
|
|
||||||
|
|
||||||
#define SUCCEED(__err_context) \
|
|
||||||
ENSURE_ERROR_READY(__err_context); \
|
|
||||||
__err_context->status = 0;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Defines for the ATTEMPT/CATCH/CLEANUP/PROCESS/HANDLE/FINISH process
|
|
||||||
*/
|
|
||||||
|
|
||||||
#define ATTEMPT \
|
|
||||||
switch ( 0 ) { \
|
|
||||||
case 0: \
|
|
||||||
|
|
||||||
#define VALID(__err_context, __stmt) \
|
|
||||||
__stmt; \
|
|
||||||
if ( akerr_valid_error_address(__err_context) == 0 ) { \
|
|
||||||
__err_context = NULL; \
|
|
||||||
FAIL(__err_context, AKERR_BADEXC, "Received (akerr_ErrorContext *) from an invalid memory region. (Did the method finish without calling SUCCEED_RETURN?)"); \
|
|
||||||
}
|
|
||||||
|
|
||||||
#define DETECT(__err_context, __stmt) \
|
|
||||||
VALID(__err_context, __stmt); \
|
|
||||||
if ( __err_context != NULL ) { \
|
|
||||||
if ( __err_context->status != 0 ) { \
|
|
||||||
AKERR_STACKTRACE_APPEND(__err_context, "%s:%s:%d\n", (char *)__FILE__, (char *)__func__, __LINE__); \
|
|
||||||
break; \
|
|
||||||
} \
|
|
||||||
}
|
|
||||||
|
|
||||||
#define CATCH(__err_context, __stmt) \
|
|
||||||
DETECT(__err_context, __err_context = __stmt);
|
|
||||||
|
|
||||||
#define PASS(__err_context, __stmt) \
|
|
||||||
switch ( 0 ) { \
|
|
||||||
case 0: \
|
|
||||||
DETECT(__err_context, __err_context = __stmt); \
|
|
||||||
} \
|
|
||||||
FINISH_LOGIC(__err_context, true);
|
|
||||||
|
|
||||||
#define IGNORE(__stmt) \
|
|
||||||
__akerr_last_ignored = __stmt; \
|
|
||||||
if ( __akerr_last_ignored != NULL ) { \
|
|
||||||
LOG_ERROR_WITH_MESSAGE(__akerr_last_ignored, "** IGNORED ERROR **"); \
|
|
||||||
}
|
|
||||||
|
|
||||||
#define CLEANUP \
|
|
||||||
};
|
|
||||||
|
|
||||||
#define PROCESS(__err_context) \
|
|
||||||
if ( __err_context != NULL ) { \
|
|
||||||
switch ( __err_context->status ) { \
|
|
||||||
case 0: \
|
|
||||||
__err_context->handled = true;
|
|
||||||
|
|
||||||
#define HANDLE(__err_context, __err_status) \
|
|
||||||
break; \
|
|
||||||
case __err_status: \
|
|
||||||
__err_context->stacktracebufptr = (char *)&__err_context->stacktracebuf; \
|
|
||||||
__err_context->handled = true;
|
|
||||||
|
|
||||||
#define HANDLE_GROUP(__err_context, __err_status) \
|
|
||||||
case __err_status: \
|
|
||||||
__err_context->stacktracebufptr = (char *)&__err_context->stacktracebuf; \
|
|
||||||
__err_context->handled = true;
|
|
||||||
|
|
||||||
#define HANDLE_DEFAULT(__err_context) \
|
|
||||||
break; \
|
|
||||||
default: \
|
|
||||||
__err_context->stacktracebufptr = (char *)&__err_context->stacktracebuf; \
|
|
||||||
__err_context->handled = true;
|
|
||||||
|
|
||||||
#define FINISH_LOGIC(__err_context, __pass_up) \
|
|
||||||
if ( __err_context != NULL ) { \
|
|
||||||
if ( __err_context->handled == false && __pass_up == true ) { \
|
|
||||||
return __err_context; \
|
|
||||||
} \
|
|
||||||
} \
|
|
||||||
|
|
||||||
#define FINISH(__err_context, __pass_up) \
|
|
||||||
}; \
|
|
||||||
}; \
|
|
||||||
FINISH_LOGIC(__err_context, __pass_up) \
|
|
||||||
RELEASE_ERROR(__err_context);
|
|
||||||
|
|
||||||
#define FINISH_NORETURN(__err_context) \
|
|
||||||
}; \
|
|
||||||
}; \
|
|
||||||
if ( __err_context != NULL ) { \
|
|
||||||
if ( __err_context->handled == false ) { \
|
|
||||||
LOG_ERROR_WITH_MESSAGE(__err_context, "Unhandled Error"); \
|
|
||||||
akerr_handler_unhandled_error(__err_context); \
|
|
||||||
} \
|
|
||||||
} \
|
|
||||||
RELEASE_ERROR(__err_context);
|
|
||||||
|
|
||||||
#endif // _AKERR_H_
|
|
||||||
243
include/sdlerror.h
Normal file
243
include/sdlerror.h
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
#ifndef _ERROR_H_
|
||||||
|
#define _ERROR_H_
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#define MAX_ERROR_CONTEXT_STRING_LENGTH 1024
|
||||||
|
#define MAX_ERROR_NAME_LENGTH 64
|
||||||
|
#define MAX_ERROR_FNAME_LENGTH 256
|
||||||
|
#define MAX_ERROR_FUNCTION_LENGTH 128
|
||||||
|
#define MAX_ERROR_STACKTRACE_BUF_LENGTH 2048
|
||||||
|
|
||||||
|
#define ERR_NULLPOINTER 1
|
||||||
|
#define ERR_OUTOFBOUNDS 2
|
||||||
|
#define ERR_SDL 3
|
||||||
|
#define ERR_ATTRIBUTE 4
|
||||||
|
#define ERR_TYPE 5
|
||||||
|
#define ERR_KEY 6
|
||||||
|
#define ERR_HEAP 7
|
||||||
|
#define ERR_INDEX 8
|
||||||
|
#define ERR_FORMAT 9
|
||||||
|
#define ERR_IO 10
|
||||||
|
#define ERR_REGISTRY 11
|
||||||
|
#define ERR_VALUE 12
|
||||||
|
#define ERR_BEHAVIOR 13
|
||||||
|
#define ERR_RELATIONSHIP 14
|
||||||
|
|
||||||
|
#ifndef MAX_ERR_VALUE
|
||||||
|
#define MAX_ERR_VALUE 14
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extern char __ERROR_NAMES[MAX_ERR_VALUE+1][MAX_ERROR_NAME_LENGTH];
|
||||||
|
|
||||||
|
#define MAX_HEAP_ERROR 128
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
char message[MAX_ERROR_CONTEXT_STRING_LENGTH];
|
||||||
|
int heapid;
|
||||||
|
int status;
|
||||||
|
bool handled;
|
||||||
|
int refcount;
|
||||||
|
char fname[MAX_ERROR_FNAME_LENGTH];
|
||||||
|
char function[MAX_ERROR_FNAME_LENGTH];
|
||||||
|
int lineno;
|
||||||
|
bool reported;
|
||||||
|
char stacktracebuf[MAX_ERROR_STACKTRACE_BUF_LENGTH];
|
||||||
|
char *stacktracebufptr;
|
||||||
|
} ErrorContext;
|
||||||
|
|
||||||
|
#define ERROR_NOIGNORE __attribute__((warn_unused_result))
|
||||||
|
|
||||||
|
typedef void (*ErrorUnhandledErrorHandler)(ErrorContext *errctx);
|
||||||
|
|
||||||
|
extern ErrorContext HEAP_ERROR[MAX_HEAP_ERROR];
|
||||||
|
extern ErrorUnhandledErrorHandler error_handler_unhandled_error;
|
||||||
|
extern ErrorContext *__error_last_ignored;
|
||||||
|
|
||||||
|
ErrorContext ERROR_NOIGNORE *heap_release_error(ErrorContext *ptr);
|
||||||
|
ErrorContext ERROR_NOIGNORE *heap_next_error();
|
||||||
|
char *error_name_for_status(int status, char *name);
|
||||||
|
void error_init();
|
||||||
|
void error_default_handler_unhandled_error(ErrorContext *ptr);
|
||||||
|
|
||||||
|
#define LOG_ERROR_WITH_MESSAGE(__err_context, __err_message) \
|
||||||
|
SDL_Log("%s%s:%s:%d: %s %d (%s): %s", (char *)&__err_context->stacktracebuf, (char *)__FILE__, (char *)__func__, __LINE__, __err_message, __err_context->status, error_name_for_status(__err_context->status, NULL), __err_context->message); \
|
||||||
|
|
||||||
|
#define LOG_ERROR(__err_context) \
|
||||||
|
LOG_ERROR_WITH_MESSAGE(__err_context, "");
|
||||||
|
|
||||||
|
#define RELEASE_ERROR(__err_context) \
|
||||||
|
if ( __err_context != NULL ) { \
|
||||||
|
__err_context = heap_release_error(__err_context); \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define PREPARE_ERROR(__err_context) \
|
||||||
|
error_init(); \
|
||||||
|
ErrorContext __attribute__ ((unused)) *__err_context = NULL;
|
||||||
|
|
||||||
|
#define ENSURE_ERROR_READY(__err_context) \
|
||||||
|
if ( __err_context == NULL ) { \
|
||||||
|
__err_context = heap_next_error(); \
|
||||||
|
if ( __err_context == NULL ) { \
|
||||||
|
SDL_Log("%s:%s:%d: Unable to pull an ErrorContext from the heap!", __FILE__, (char *)__func__, __LINE__); \
|
||||||
|
exit(1); \
|
||||||
|
} \
|
||||||
|
} \
|
||||||
|
__err_context->refcount += 1;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Failure and success methods for functions that return ErrorContext *
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define FAIL_ZERO_RETURN(__err_context, __x, __err, __message, ...) \
|
||||||
|
if ( __x == 0 ) { \
|
||||||
|
FAIL(__err_context, __err, __message, ##__VA_ARGS__); \
|
||||||
|
return __err_context; \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define FAIL_NONZERO_RETURN(__err_context, __x, __err, __message, ...) \
|
||||||
|
if ( __x != 0 ) { \
|
||||||
|
FAIL(__err_context, __err, __message, ##__VA_ARGS__); \
|
||||||
|
return __err_context; \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define FAIL_RETURN(__err_context, __err, __message, ...) \
|
||||||
|
FAIL(__err_context, __err, __message, ##__VA_ARGS__); \
|
||||||
|
return __err_context;
|
||||||
|
|
||||||
|
#define SUCCEED_RETURN(__err_context) \
|
||||||
|
RELEASE_ERROR(__err_context); \
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Failure and success methods for use inside of ATTEMPT() blocks
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define FAIL_ZERO_BREAK(__err_context, __x, __err, __message, ...) \
|
||||||
|
if ( __x == 0 ) { \
|
||||||
|
FAIL(__err_context, __err, __message, ##__VA_ARGS__); \
|
||||||
|
break; \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define FAIL_NONZERO_BREAK(__err_context, __x, __err, __message, ...) \
|
||||||
|
if ( __x != 0 ) { \
|
||||||
|
FAIL(__err_context, __err, __message, ##__VA_ARGS__); \
|
||||||
|
break; \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define FAIL_BREAK(__err_context, __err_, __message, ...) \
|
||||||
|
FAIL(__err_context, __err_, __message, ##__VA_ARGS__); \
|
||||||
|
break;
|
||||||
|
|
||||||
|
#define SUCCEED_BREAK(__err_context) \
|
||||||
|
SUCCEED(__err_context); \
|
||||||
|
break;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* General failure and success methods
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define FAIL(__err_context, __err, __message, ...) \
|
||||||
|
ENSURE_ERROR_READY(__err_context); \
|
||||||
|
__err_context->status = __err; \
|
||||||
|
snprintf((char *)__err_context->fname, MAX_ERROR_FNAME_LENGTH, __FILE__); \
|
||||||
|
snprintf((char *)__err_context->function, MAX_ERROR_FUNCTION_LENGTH, __func__); \
|
||||||
|
__err_context->lineno = __LINE__; \
|
||||||
|
snprintf((char *)__err_context->message, MAX_ERROR_CONTEXT_STRING_LENGTH, __message, ## __VA_ARGS__); \
|
||||||
|
__err_context->stacktracebufptr += sprintf(__err_context->stacktracebufptr, "%s:%s:%d: %d (%s) : %s\n", (char *)__err_context->fname, (char *)__err_context->function, __err_context->lineno, __err_context->status, error_name_for_status(__err_context->status, NULL), __err_context->message);
|
||||||
|
|
||||||
|
|
||||||
|
#define SUCCEED(__err_context) \
|
||||||
|
ENSURE_ERROR_READY(__err_context); \
|
||||||
|
__err_context->status = 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Defines for the ATTEMPT/CATCH/CLEANUP/PROCESS/HANDLE/FINISH process
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define ATTEMPT \
|
||||||
|
switch ( 0 ) { \
|
||||||
|
case 0: \
|
||||||
|
|
||||||
|
#define DETECT(__err_context, __stmt) \
|
||||||
|
__stmt; \
|
||||||
|
if ( __err_context != NULL ) { \
|
||||||
|
__err_context->stacktracebufptr += sprintf(__err_context->stacktracebufptr, "%s:%s:%d: Detected error %d from heap (refcount %d)\n", (char *)__FILE__, (char *)__func__, __LINE__, __err_context->heapid, __err_context->refcount); \
|
||||||
|
if ( __err_context->status != 0 ) { \
|
||||||
|
__err_context->stacktracebufptr += sprintf(__err_context->stacktracebufptr, "%s:%s:%d\n", (char *)__FILE__, (char *)__func__, __LINE__); \
|
||||||
|
break; \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CATCH(__err_context, __stmt) \
|
||||||
|
DETECT(__err_context, __err_context = __stmt);
|
||||||
|
|
||||||
|
#define IGNORE(__stmt) \
|
||||||
|
__error_last_ignored = __stmt; \
|
||||||
|
if ( __error_last_ignored != NULL ) { \
|
||||||
|
LOG_ERROR_WITH_MESSAGE(__error_last_ignored, "** IGNORED ERROR **"); \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CLEANUP \
|
||||||
|
};
|
||||||
|
|
||||||
|
#define PROCESS(__err_context) \
|
||||||
|
if ( __err_context != NULL ) { \
|
||||||
|
switch ( __err_context->status ) { \
|
||||||
|
case 0: \
|
||||||
|
__err_context->handled = true;
|
||||||
|
|
||||||
|
#define HANDLE(__err_context, __err_status) \
|
||||||
|
break; \
|
||||||
|
case __err_status: \
|
||||||
|
__err_context->stacktracebufptr = (char *)&__err_context->stacktracebuf; \
|
||||||
|
__err_context->handled = true;
|
||||||
|
|
||||||
|
#define HANDLE_GROUP(__err_context, __err_status) \
|
||||||
|
case __err_status: \
|
||||||
|
__err_context->stacktracebufptr = (char *)&__err_context->stacktracebuf; \
|
||||||
|
__err_context->handled = true;
|
||||||
|
|
||||||
|
#define HANDLE_DEFAULT(__err_context) \
|
||||||
|
break; \
|
||||||
|
default: \
|
||||||
|
__err_context->stacktracebufptr = (char *)&__err_context->stacktracebuf; \
|
||||||
|
__err_context->handled = true;
|
||||||
|
|
||||||
|
#define FINISH(__err_context, __pass_up) \
|
||||||
|
}; \
|
||||||
|
}; \
|
||||||
|
if ( __err_context != NULL ) { \
|
||||||
|
if ( __err_context->handled == false && __pass_up == true ) { \
|
||||||
|
__err_context->stacktracebufptr += sprintf(__err_context->stacktracebufptr, "%s:%s:%d\n", (char *)__FILE__, (char *)__func__, __LINE__); \
|
||||||
|
return __err_context; \
|
||||||
|
} \
|
||||||
|
} \
|
||||||
|
RELEASE_ERROR(__err_context);
|
||||||
|
|
||||||
|
#define FINISH_NORETURN(__err_context) \
|
||||||
|
}; \
|
||||||
|
}; \
|
||||||
|
if ( __err_context != NULL ) { \
|
||||||
|
if ( __err_context->handled == false ) { \
|
||||||
|
LOG_ERROR_WITH_MESSAGE(__err_context, "Unhandled Error"); \
|
||||||
|
error_handler_unhandled_error(__err_context); \
|
||||||
|
} \
|
||||||
|
} \
|
||||||
|
RELEASE_ERROR(__err_context);
|
||||||
|
|
||||||
|
#define CATCH_AND_RETURN(__err_context, __stmt) \
|
||||||
|
ATTEMPT { \
|
||||||
|
CATCH(__err_context, __stmt); \
|
||||||
|
} CLEANUP { \
|
||||||
|
} PROCESS(__err_context) { \
|
||||||
|
} FINISH(__err_context, true);
|
||||||
|
|
||||||
|
|
||||||
|
#endif // _ERROR_H_
|
||||||
@@ -1,407 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Code coverage harness for libakerror.
|
|
||||||
|
|
||||||
Coverage measures which parts of the library the CTest suite actually executes.
|
|
||||||
It is the complement to mutation testing (scripts/mutation_test.py): coverage
|
|
||||||
finds code the tests never reach, mutation testing finds code the tests reach
|
|
||||||
but do not really check.
|
|
||||||
|
|
||||||
What is measured is the library's own translation units: src/error.c and the
|
|
||||||
generated src/errno.c. The macros in the public header are deliberately not
|
|
||||||
measured -- GCC attributes an expanded macro to its call site, so header logic
|
|
||||||
would be reported as lines of the test that used it. Use mutation testing
|
|
||||||
(--target include/akerror.tmpl.h) to check how well those macros are tested.
|
|
||||||
|
|
||||||
This harness has no third-party dependencies -- just gcov, which ships with the
|
|
||||||
compiler, plus the project's normal cmake/ctest toolchain. By default it
|
|
||||||
configures its own instrumented build directory so the ordinary build tree is
|
|
||||||
left alone and coverage counters can never be stale.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
scripts/coverage.py [options]
|
|
||||||
|
|
||||||
--source-root DIR repo root (default: parent of this script's dir)
|
|
||||||
--build-dir DIR instrumented build dir (default: <root>/build/coverage)
|
|
||||||
--no-configure reuse the build dir as-is (do not cmake/build)
|
|
||||||
--no-run report existing counters (do not reset and run ctest)
|
|
||||||
--threshold PCT exit non-zero if line coverage < PCT (default: 0 = off)
|
|
||||||
--branch-threshold P exit non-zero if branch coverage < P (default: 0 = off)
|
|
||||||
--junit PATH write a JUnit XML report to this path
|
|
||||||
--max-uncovered N uncovered lines to list per file (default: 40, 0 = all)
|
|
||||||
--exclude SUBSTR skip reported paths containing SUBSTR; repeatable
|
|
||||||
--gcov PROG gcov program (default: $GCOV or "gcov")
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import collections
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
|
||||||
# Running the instrumented suite
|
|
||||||
# --------------------------------------------------------------------------- #
|
|
||||||
|
|
||||||
def configure_and_build(cmake, root, build, jobs):
|
|
||||||
"""Configure an instrumented build dir and build everything in it."""
|
|
||||||
cmds = [
|
|
||||||
[cmake, "-S", root, "-B", build, "-DAKERR_COVERAGE=ON"],
|
|
||||||
[cmake, "--build", build] + (["--parallel", str(jobs)] if jobs else []),
|
|
||||||
]
|
|
||||||
for cmd in cmds:
|
|
||||||
print("+ " + " ".join(cmd))
|
|
||||||
if subprocess.call(cmd) != 0:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def reset_counters(build):
|
|
||||||
"""Delete accumulated .gcda files so each run reports one suite run."""
|
|
||||||
removed = 0
|
|
||||||
for dirpath, _dirs, files in os.walk(build):
|
|
||||||
for name in files:
|
|
||||||
if name.endswith(".gcda"):
|
|
||||||
os.unlink(os.path.join(dirpath, name))
|
|
||||||
removed += 1
|
|
||||||
if removed:
|
|
||||||
print(f"Reset {removed} coverage data file(s).")
|
|
||||||
|
|
||||||
|
|
||||||
def run_ctest(build, ctest):
|
|
||||||
"""Run the suite. Returns True if every test passed.
|
|
||||||
|
|
||||||
Coverage counters are flushed at exit(), and the tests that are expected to
|
|
||||||
fail exit via the library's unhandled-error handler (exit(), not abort()),
|
|
||||||
so WILL_FAIL tests still contribute their counters.
|
|
||||||
"""
|
|
||||||
cmd = [ctest, "--output-on-failure"]
|
|
||||||
print("+ " + " ".join(cmd) + f" (in {build})")
|
|
||||||
return subprocess.call(cmd, cwd=build) == 0
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
|
||||||
# Collecting gcov data
|
|
||||||
# --------------------------------------------------------------------------- #
|
|
||||||
|
|
||||||
class FileCov:
|
|
||||||
"""Merged coverage for one source file, across every object that built it.
|
|
||||||
|
|
||||||
A file compiled into more than one object -- or a header included by several
|
|
||||||
translation units -- is reported once, with counts summed. Branches are
|
|
||||||
merged by (line, index within line), so differing expansions of the same
|
|
||||||
line contribute the union of their branches.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.lines = collections.Counter() # lineno -> execution count
|
|
||||||
self.branches = collections.Counter() # (lineno, idx) -> taken count
|
|
||||||
self.funcs = collections.Counter() # (name, start_line) -> count
|
|
||||||
|
|
||||||
def merge(self, entry):
|
|
||||||
for ln in entry.get("lines", []):
|
|
||||||
no = ln["line_number"]
|
|
||||||
self.lines[no] += ln.get("count", 0)
|
|
||||||
for idx, br in enumerate(ln.get("branches", [])):
|
|
||||||
if br.get("throw"):
|
|
||||||
continue
|
|
||||||
self.branches[(no, idx)] += br.get("count", 0)
|
|
||||||
for fn in entry.get("functions", []):
|
|
||||||
key = (fn.get("name", "?"), fn.get("start_line", 0))
|
|
||||||
self.funcs[key] += fn.get("execution_count", 0)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _ratio(counter):
|
|
||||||
total = len(counter)
|
|
||||||
hit = sum(1 for v in counter.values() if v > 0)
|
|
||||||
return hit, total
|
|
||||||
|
|
||||||
def line_stats(self):
|
|
||||||
return self._ratio(self.lines)
|
|
||||||
|
|
||||||
def branch_stats(self):
|
|
||||||
return self._ratio(self.branches)
|
|
||||||
|
|
||||||
def func_stats(self):
|
|
||||||
return self._ratio(self.funcs)
|
|
||||||
|
|
||||||
def uncovered_lines(self):
|
|
||||||
return sorted(no for no, count in self.lines.items() if count == 0)
|
|
||||||
|
|
||||||
|
|
||||||
def find_notes(build):
|
|
||||||
"""Every .gcno in the build tree: one per instrumented translation unit."""
|
|
||||||
notes = []
|
|
||||||
for dirpath, _dirs, files in os.walk(build):
|
|
||||||
for name in files:
|
|
||||||
if name.endswith(".gcno"):
|
|
||||||
notes.append(os.path.join(dirpath, name))
|
|
||||||
return sorted(notes)
|
|
||||||
|
|
||||||
|
|
||||||
def gcov_json(gcov, note, cwd):
|
|
||||||
"""Run gcov on one .gcno and return its parsed JSON, or None on failure.
|
|
||||||
|
|
||||||
--stdout keeps gcov from littering .gcov files in the build tree. A .gcno
|
|
||||||
with no matching .gcda still reports, with all counts zero, which is the
|
|
||||||
correct answer for a translation unit no test executed.
|
|
||||||
"""
|
|
||||||
cmd = [gcov, "--branch-probabilities", "--json-format", "--stdout", note]
|
|
||||||
proc = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE)
|
|
||||||
out, err = proc.communicate()
|
|
||||||
if proc.returncode != 0:
|
|
||||||
sys.stderr.write(f"warning: {' '.join(cmd)} failed:\n"
|
|
||||||
f"{err.decode(errors='replace')}")
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
return json.loads(out.decode(errors="replace"))
|
|
||||||
except ValueError as exc:
|
|
||||||
sys.stderr.write(f"warning: unparseable gcov output for {note}: {exc}\n")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def display_path(path, build, root):
|
|
||||||
"""Label a source: build-relative for generated files, else repo-relative.
|
|
||||||
|
|
||||||
Returns None for anything outside both trees (system headers, toolchain
|
|
||||||
internals). Generated sources are labelled relative to the build dir so the
|
|
||||||
report and its thresholds do not change with the build dir's location.
|
|
||||||
"""
|
|
||||||
for base in (build, root):
|
|
||||||
if path.startswith(base + os.sep):
|
|
||||||
return os.path.relpath(path, base)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def collect(gcov, build, root, excludes):
|
|
||||||
"""Merge gcov data for every instrumented unit into {display path: FileCov}."""
|
|
||||||
covs = {}
|
|
||||||
for note in find_notes(build):
|
|
||||||
data = gcov_json(gcov, note, build)
|
|
||||||
if data is None:
|
|
||||||
continue
|
|
||||||
# Paths in the report are relative to the directory the unit was
|
|
||||||
# compiled in, which gcov records in the notes file.
|
|
||||||
compile_dir = data.get("current_working_directory") or build
|
|
||||||
for entry in data.get("files", []):
|
|
||||||
path = entry.get("file", "")
|
|
||||||
if not os.path.isabs(path):
|
|
||||||
path = os.path.join(compile_dir, path)
|
|
||||||
display = display_path(os.path.realpath(path), build, root)
|
|
||||||
if display is None:
|
|
||||||
continue # system headers, toolchain internals
|
|
||||||
if any(x in display for x in excludes):
|
|
||||||
continue
|
|
||||||
covs.setdefault(display, FileCov()).merge(entry)
|
|
||||||
return covs
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
|
||||||
# Reporting
|
|
||||||
# --------------------------------------------------------------------------- #
|
|
||||||
|
|
||||||
def pct(hit, total):
|
|
||||||
return 100.0 * hit / total if total else 100.0
|
|
||||||
|
|
||||||
|
|
||||||
def fmt_ratio(hit, total):
|
|
||||||
if not total:
|
|
||||||
return f"{'-':>9} -"
|
|
||||||
return f"{hit:>4}/{total:<4} {pct(hit, total):5.1f}%"
|
|
||||||
|
|
||||||
|
|
||||||
def compress(numbers):
|
|
||||||
"""[1,2,3,7,9,10] -> '1-3, 7, 9-10' for readable uncovered-line lists."""
|
|
||||||
out, start, prev = [], None, None
|
|
||||||
for n in numbers:
|
|
||||||
if start is None:
|
|
||||||
start = prev = n
|
|
||||||
elif n == prev + 1:
|
|
||||||
prev = n
|
|
||||||
else:
|
|
||||||
out.append(f"{start}" if start == prev else f"{start}-{prev}")
|
|
||||||
start = prev = n
|
|
||||||
if start is not None:
|
|
||||||
out.append(f"{start}" if start == prev else f"{start}-{prev}")
|
|
||||||
return ", ".join(out)
|
|
||||||
|
|
||||||
|
|
||||||
def report(covs, max_uncovered):
|
|
||||||
"""Print the per-file table and uncovered detail; return overall percentages."""
|
|
||||||
width = max([len(p) for p in covs] + [len("TOTAL")])
|
|
||||||
print("\n" + "=" * 72)
|
|
||||||
print("CODE COVERAGE SUMMARY")
|
|
||||||
print("=" * 72)
|
|
||||||
print(f" {'FILE':<{width}} {'LINES':^15} {'BRANCHES':^15} FUNCS")
|
|
||||||
|
|
||||||
totals = [0, 0, 0, 0, 0, 0] # lines hit/total, branches hit/total, funcs
|
|
||||||
for path in sorted(covs):
|
|
||||||
cov = covs[path]
|
|
||||||
lh, lt = cov.line_stats()
|
|
||||||
bh, bt = cov.branch_stats()
|
|
||||||
fh, ft = cov.func_stats()
|
|
||||||
for i, v in enumerate((lh, lt, bh, bt, fh, ft)):
|
|
||||||
totals[i] += v
|
|
||||||
print(f" {path:<{width}} {fmt_ratio(lh, lt)} {fmt_ratio(bh, bt)} "
|
|
||||||
f"{fh}/{ft}")
|
|
||||||
|
|
||||||
lh, lt, bh, bt, fh, ft = totals
|
|
||||||
print(f" {'-' * width} {'-' * 15} {'-' * 15} -----")
|
|
||||||
print(f" {'TOTAL':<{width}} {fmt_ratio(lh, lt)} {fmt_ratio(bh, bt)} "
|
|
||||||
f"{fh}/{ft}")
|
|
||||||
|
|
||||||
for path in sorted(covs):
|
|
||||||
missing = covs[path].uncovered_lines()
|
|
||||||
if not missing:
|
|
||||||
continue
|
|
||||||
shown = missing if not max_uncovered else missing[:max_uncovered]
|
|
||||||
more = "" if len(shown) == len(missing) else \
|
|
||||||
f" ... (+{len(missing) - len(shown)} more)"
|
|
||||||
print(f"\n uncovered in {path} ({len(missing)} line(s)):")
|
|
||||||
print(f" {compress(shown)}{more}")
|
|
||||||
|
|
||||||
return pct(lh, lt), pct(bh, bt)
|
|
||||||
|
|
||||||
|
|
||||||
def _xml_escape(text):
|
|
||||||
return (str(text).replace("&", "&").replace("<", "<")
|
|
||||||
.replace(">", ">").replace('"', """))
|
|
||||||
|
|
||||||
|
|
||||||
def write_junit(path, covs, line_threshold, branch_threshold):
|
|
||||||
"""One <testcase> per file per metric; below-threshold is a <failure>."""
|
|
||||||
cases = []
|
|
||||||
for f in sorted(covs):
|
|
||||||
cov = covs[f]
|
|
||||||
cases.append((f, "lines", cov.line_stats(), line_threshold))
|
|
||||||
cases.append((f, "branches", cov.branch_stats(), branch_threshold))
|
|
||||||
for metric, thr, stats in (("lines", line_threshold,
|
|
||||||
[c.line_stats() for c in covs.values()]),
|
|
||||||
("branches", branch_threshold,
|
|
||||||
[c.branch_stats() for c in covs.values()])):
|
|
||||||
cases.append(("TOTAL", metric,
|
|
||||||
(sum(h for h, _t in stats), sum(t for _h, t in stats)),
|
|
||||||
thr))
|
|
||||||
|
|
||||||
fails = sum(1 for _f, _m, (h, t), thr in cases
|
|
||||||
if t and thr > 0 and pct(h, t) < thr)
|
|
||||||
out = ['<?xml version="1.0" encoding="UTF-8"?>',
|
|
||||||
f'<testsuites name="coverage" tests="{len(cases)}" '
|
|
||||||
f'failures="{fails}">',
|
|
||||||
f' <testsuite name="coverage" tests="{len(cases)}" '
|
|
||||||
f'failures="{fails}">']
|
|
||||||
for f, metric, (hit, total), thr in cases:
|
|
||||||
name = _xml_escape(f"{f} {metric}")
|
|
||||||
detail = _xml_escape(f"{hit}/{total} ({pct(hit, total):.1f}%)"
|
|
||||||
if total else "no data")
|
|
||||||
out.append(f' <testcase name="{name}" '
|
|
||||||
f'classname="coverage.{_xml_escape(metric)}" time="0">')
|
|
||||||
if total and thr > 0 and pct(hit, total) < thr:
|
|
||||||
out.append(f' <failure message="{metric} coverage '
|
|
||||||
f'{pct(hit, total):.1f}% < threshold {thr:.1f}%">'
|
|
||||||
f'{detail}</failure>')
|
|
||||||
else:
|
|
||||||
out.append(f' <system-out>{detail}</system-out>')
|
|
||||||
out.append(' </testcase>')
|
|
||||||
out.append(' </testsuite>')
|
|
||||||
out.append('</testsuites>')
|
|
||||||
with open(path, "w") as fh:
|
|
||||||
fh.write("\n".join(out) + "\n")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Line-buffer stdout so progress is visible live under CI / the cmake target.
|
|
||||||
try:
|
|
||||||
sys.stdout.reconfigure(line_buffering=True)
|
|
||||||
except (AttributeError, ValueError):
|
|
||||||
pass
|
|
||||||
here = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
default_root = os.path.dirname(here)
|
|
||||||
|
|
||||||
ap = argparse.ArgumentParser(description="Code coverage for libakerror")
|
|
||||||
ap.add_argument("--source-root", default=default_root)
|
|
||||||
ap.add_argument("--build-dir", default=None)
|
|
||||||
ap.add_argument("--configure", dest="configure", action="store_true",
|
|
||||||
default=True)
|
|
||||||
ap.add_argument("--no-configure", dest="configure", action="store_false")
|
|
||||||
ap.add_argument("--run", dest="run", action="store_true", default=True)
|
|
||||||
ap.add_argument("--no-run", dest="run", action="store_false")
|
|
||||||
ap.add_argument("--threshold", type=float, default=0.0,
|
|
||||||
help="fail if line coverage is below this percentage")
|
|
||||||
ap.add_argument("--branch-threshold", type=float, default=0.0,
|
|
||||||
help="fail if branch coverage is below this percentage")
|
|
||||||
ap.add_argument("--junit", default=None,
|
|
||||||
help="write a JUnit XML report to this path")
|
|
||||||
ap.add_argument("--max-uncovered", type=int, default=40)
|
|
||||||
ap.add_argument("--exclude", action="append", default=None,
|
|
||||||
help="skip reported paths containing this substring")
|
|
||||||
ap.add_argument("--gcov", default=os.environ.get("GCOV", "gcov"))
|
|
||||||
ap.add_argument("--cmake", default=os.environ.get("CMAKE", "cmake"))
|
|
||||||
ap.add_argument("--ctest", default=os.environ.get("CTEST", "ctest"))
|
|
||||||
ap.add_argument("-j", "--jobs", type=int, default=0)
|
|
||||||
args = ap.parse_args()
|
|
||||||
|
|
||||||
root = os.path.realpath(args.source_root)
|
|
||||||
build = os.path.realpath(args.build_dir or os.path.join(root, "build",
|
|
||||||
"coverage"))
|
|
||||||
# The tests exercise the library; their own source is not what we measure.
|
|
||||||
excludes = args.exclude if args.exclude is not None else ["tests/"]
|
|
||||||
|
|
||||||
if args.configure:
|
|
||||||
if not configure_and_build(args.cmake, root, build, args.jobs):
|
|
||||||
sys.stderr.write("\nInstrumented build FAILED; aborting.\n")
|
|
||||||
return 2
|
|
||||||
elif not os.path.isdir(build):
|
|
||||||
sys.stderr.write(f"No build dir at {build} (drop --no-configure).\n")
|
|
||||||
return 2
|
|
||||||
|
|
||||||
tests_ok = True
|
|
||||||
if args.run:
|
|
||||||
reset_counters(build)
|
|
||||||
tests_ok = run_ctest(build, args.ctest)
|
|
||||||
if not tests_ok:
|
|
||||||
sys.stderr.write("\nwarning: some tests FAILED; coverage below is "
|
|
||||||
"still reported, but the run is not green.\n")
|
|
||||||
|
|
||||||
covs = collect(args.gcov, build, root, excludes)
|
|
||||||
if not covs:
|
|
||||||
sys.stderr.write("No coverage data found. Was the build instrumented "
|
|
||||||
"(-DAKERR_COVERAGE=ON) and the suite run?\n")
|
|
||||||
return 2
|
|
||||||
|
|
||||||
line_pct, branch_pct = report(covs, args.max_uncovered)
|
|
||||||
|
|
||||||
if args.junit:
|
|
||||||
junit_path = os.path.abspath(args.junit)
|
|
||||||
write_junit(junit_path, covs, args.threshold, args.branch_threshold)
|
|
||||||
print(f"\nJUnit report written to: {junit_path}")
|
|
||||||
|
|
||||||
rc = 0
|
|
||||||
if not tests_ok:
|
|
||||||
print("\nFAIL: the CTest suite did not pass.")
|
|
||||||
rc = 1
|
|
||||||
|
|
||||||
# Thresholds gate every file as well as the total: a small, well-covered
|
|
||||||
# file (the generated status-name table) must not mask a regression in a
|
|
||||||
# bigger one. Files with no branches at all are not branch-gated.
|
|
||||||
checks = [("total", "line", line_pct, args.threshold),
|
|
||||||
("total", "branch", branch_pct, args.branch_threshold)]
|
|
||||||
for path in sorted(covs):
|
|
||||||
lh, lt = covs[path].line_stats()
|
|
||||||
bh, bt = covs[path].branch_stats()
|
|
||||||
checks.append((path, "line", pct(lh, lt), args.threshold))
|
|
||||||
if bt:
|
|
||||||
checks.append((path, "branch", pct(bh, bt), args.branch_threshold))
|
|
||||||
for path, metric, value, threshold in checks:
|
|
||||||
if threshold > 0 and value < threshold:
|
|
||||||
print(f"\nFAIL: {path} {metric} coverage {value:.1f}% < threshold "
|
|
||||||
f"{threshold:.1f}%")
|
|
||||||
rc = 1
|
|
||||||
return rc
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
srcdir=$1
|
|
||||||
outdir=$2
|
|
||||||
|
|
||||||
mkdir -p ${outdir}/src
|
|
||||||
mkdir -p ${outdir}/include
|
|
||||||
rm -f ${outdir}/src/errno.c
|
|
||||||
echo "#include <akerror.h>" >> ${outdir}/src/errno.c
|
|
||||||
echo "#include <errno.h>" >> ${outdir}/src/errno.c
|
|
||||||
cat >> ${outdir}/src/errno.c <<'EOF'
|
|
||||||
|
|
||||||
/*
|
|
||||||
* These names belong to the library's own reserved band, and this runs from
|
|
||||||
* akerr_init(), which has no caller to raise into -- so it goes through
|
|
||||||
* __akerr_name_library_status(), which reports a refusal and terminates rather
|
|
||||||
* than leaving every later stack trace to print "Unknown Error" for an errno.
|
|
||||||
* Keeping the branch in src/error.c also keeps this generated file free of
|
|
||||||
* control flow no test can reach.
|
|
||||||
*/
|
|
||||||
EOF
|
|
||||||
echo "void akerr_init_errno(void) {" >> ${outdir}/src/errno.c
|
|
||||||
maxval=$(errno --list | cut -d ' ' -f 2 | sort -g | tail -n 1)
|
|
||||||
errno --list | while read LINE; do
|
|
||||||
define=$(echo "$LINE" | cut -d ' ' -f 1);
|
|
||||||
value=$(echo "$LINE" | cut -d ' ' -f 2);
|
|
||||||
desc=$(echo "$LINE" | cut -d ' ' -f 3-);
|
|
||||||
echo " __akerr_name_library_status(${define}, \"${desc}\");" >> ${outdir}/src/errno.c ;
|
|
||||||
done;
|
|
||||||
echo "}" >> ${outdir}/src/errno.c
|
|
||||||
sed "s/#define AKERR_LAST_ERRNO_VALUE .*/#define AKERR_LAST_ERRNO_VALUE ${maxval}/" ${srcdir}/include/akerror.tmpl.h > ${outdir}/include/akerror.h
|
|
||||||
@@ -1,436 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Mutation testing harness for libakerror.
|
|
||||||
|
|
||||||
Mutation testing measures how good the test suite is at catching bugs. It works
|
|
||||||
by making many small, deliberate breakages ("mutants") to the library source --
|
|
||||||
flipping a comparison, deleting a statement, swapping true/false -- and then
|
|
||||||
running the whole CTest suite against each one. If the tests fail, the mutant is
|
|
||||||
"killed" (good: the tests noticed the bug). If the tests still pass, the mutant
|
|
||||||
"survived" (bad: a real bug of that shape would slip through unnoticed).
|
|
||||||
|
|
||||||
The mutation score is killed / (killed + survived). Surviving mutants are printed
|
|
||||||
with file:line and the exact change so they can be turned into new test cases.
|
|
||||||
|
|
||||||
This harness has no third-party dependencies (Python stdlib + the project's
|
|
||||||
normal cmake/ctest toolchain). It never mutates the real working tree: it copies
|
|
||||||
the repo to a scratch directory and mutates there.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
scripts/mutation_test.py [options]
|
|
||||||
|
|
||||||
--source-root DIR repo root to copy (default: parent of this script's dir)
|
|
||||||
--target FILE source file to mutate, relative to root; repeatable.
|
|
||||||
Default: src/error.c and include/akerror.tmpl.h
|
|
||||||
--work DIR scratch dir for the mutated copy (default: a temp dir)
|
|
||||||
--timeout SECONDS per-suite ctest timeout (default: 120)
|
|
||||||
--threshold PCT exit non-zero if mutation score < PCT (default: 0 = off)
|
|
||||||
--list only list the mutants that would be run, then exit
|
|
||||||
--keep keep the scratch working copy on exit (for debugging)
|
|
||||||
-j N (reserved) currently runs sequentially
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
|
||||||
# Mutation operators
|
|
||||||
#
|
|
||||||
# Each operator yields zero or more (start, end, replacement) edits for a single
|
|
||||||
# line of source. The driver applies exactly one edit per mutant so every mutant
|
|
||||||
# differs from the original by one localized change.
|
|
||||||
# --------------------------------------------------------------------------- #
|
|
||||||
|
|
||||||
# Relational operator replacement: map each operator to the alternatives that
|
|
||||||
# meaningfully change behaviour (not merely the strict negation).
|
|
||||||
_REL = {
|
|
||||||
"==": ["!="],
|
|
||||||
"!=": ["=="],
|
|
||||||
"<=": ["<", "=="],
|
|
||||||
">=": [">", "=="],
|
|
||||||
"<": ["<=", ">"],
|
|
||||||
">": [">=", "<"],
|
|
||||||
}
|
|
||||||
# Match a relational operator that is NOT part of ->, <<, >>, =>, <=, >=, ==, !=
|
|
||||||
# unless we intend it. We tokenize the two-char operators first, then single.
|
|
||||||
_REL_TWO = re.compile(r"(==|!=|<=|>=)")
|
|
||||||
_REL_ONE = re.compile(r"(?<![-<>=!+])([<>])(?![=<>])")
|
|
||||||
|
|
||||||
_LOGICAL = {"&&": "||", "||": "&&"}
|
|
||||||
_LOG_RE = re.compile(r"(&&|\|\|)")
|
|
||||||
|
|
||||||
_BOOL = {"true": "false", "false": "true"}
|
|
||||||
_BOOL_RE = re.compile(r"\b(true|false)\b")
|
|
||||||
|
|
||||||
# Arithmetic / compound-assignment on whitespace-delimited operands only, to
|
|
||||||
# avoid touching ++, --, ->, unary signs, or pointer/format punctuation.
|
|
||||||
_ARITH_RE = re.compile(r"(?<=\s)([+\-])(?=\s)")
|
|
||||||
_ARITH = {"+": "-", "-": "+"}
|
|
||||||
_COMPOUND_RE = re.compile(r"(\+=|-=)")
|
|
||||||
_COMPOUND = {"+=": "-=", "-=": "+="}
|
|
||||||
|
|
||||||
# Integer literal replacement: 0 <-> 1 (word-bounded, not inside identifiers or
|
|
||||||
# larger numbers, not a float).
|
|
||||||
_INT_RE = re.compile(r"(?<![\w.])([01])(?![\w.])")
|
|
||||||
_INT = {"0": "1", "1": "0"}
|
|
||||||
|
|
||||||
|
|
||||||
def _op_edits(line):
|
|
||||||
"""Yield (tag, start, end, replacement) for every candidate mutation."""
|
|
||||||
# Relational (two-char first so we don't split them with the one-char pass)
|
|
||||||
for m in _REL_TWO.finditer(line):
|
|
||||||
for alt in _REL[m.group(1)]:
|
|
||||||
yield ("ROR", m.start(1), m.end(1), alt)
|
|
||||||
for m in _REL_ONE.finditer(line):
|
|
||||||
for alt in _REL[m.group(1)]:
|
|
||||||
yield ("ROR", m.start(1), m.end(1), alt)
|
|
||||||
for m in _LOG_RE.finditer(line):
|
|
||||||
yield ("LCR", m.start(1), m.end(1), _LOGICAL[m.group(1)])
|
|
||||||
for m in _BOOL_RE.finditer(line):
|
|
||||||
yield ("BCR", m.start(1), m.end(1), _BOOL[m.group(1)])
|
|
||||||
for m in _COMPOUND_RE.finditer(line):
|
|
||||||
yield ("AOR", m.start(1), m.end(1), _COMPOUND[m.group(1)])
|
|
||||||
for m in _ARITH_RE.finditer(line):
|
|
||||||
yield ("AOR", m.start(1), m.end(1), _ARITH[m.group(1)])
|
|
||||||
for m in _INT_RE.finditer(line):
|
|
||||||
yield ("ICR", m.start(1), m.end(1), _INT[m.group(1)])
|
|
||||||
|
|
||||||
|
|
||||||
# Statement-deletion: neutralize a whole statement. We only delete statements
|
|
||||||
# that are safe to drop without guaranteeing a compile error, so a surviving
|
|
||||||
# deletion is a genuine test gap rather than compiler noise.
|
|
||||||
_STMT_DELETABLE = re.compile(
|
|
||||||
r"""^\s*(
|
|
||||||
break |
|
|
||||||
return\b[^;]* |
|
|
||||||
[A-Za-z_][-\w>().\[\]* ]*\s*=\s*[^;]* | # assignments
|
|
||||||
[A-Za-z_][\w]*\s*\([^;]*\) # bare function calls
|
|
||||||
)\s*;\s*(\\?)\s*$""",
|
|
||||||
re.VERBOSE,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
|
||||||
# Deciding which lines are eligible to mutate
|
|
||||||
# --------------------------------------------------------------------------- #
|
|
||||||
|
|
||||||
# Skip preprocessor control and the block of constant/error-code #defines in the
|
|
||||||
# template header: mutating buffer sizes or renumbering error codes produces
|
|
||||||
# equivalent or uninteresting mutants that swamp the signal.
|
|
||||||
_SKIP_LINE = re.compile(
|
|
||||||
r"""^\s*(
|
|
||||||
\#\s*(include|ifn?def|ifdef|if|elif|else|endif|error|pragma|undef) |
|
|
||||||
\#\s*define\s+AKERR_(MAX|LAST|NULLPOINTER|OUTOFBOUNDS|API|ATTRIBUTE|
|
|
||||||
TYPE|KEY|INDEX|FORMAT|IO|VALUE|RELATIONSHIP|EOF|CIRCULAR_REFERENCE|
|
|
||||||
ITERATOR_BREAK|NOT_IMPLEMENTED|BADEXC|NOIGNORE|USE_STDLIB)\b |
|
|
||||||
\* | // # comment bodies / line comments
|
|
||||||
)""",
|
|
||||||
re.VERBOSE,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _is_comment_or_blank(line):
|
|
||||||
s = line.strip()
|
|
||||||
return (not s) or s.startswith("//") or s.startswith("/*") or s.startswith("*")
|
|
||||||
|
|
||||||
|
|
||||||
def eligible(line):
|
|
||||||
if _is_comment_or_blank(line):
|
|
||||||
return False
|
|
||||||
if _SKIP_LINE.match(line):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class Mutant:
|
|
||||||
__slots__ = ("path", "lineno", "op", "before", "after", "col")
|
|
||||||
|
|
||||||
def __init__(self, path, lineno, op, before, after, col):
|
|
||||||
self.path = path
|
|
||||||
self.lineno = lineno
|
|
||||||
self.op = op
|
|
||||||
self.before = before
|
|
||||||
self.after = after
|
|
||||||
self.col = col
|
|
||||||
|
|
||||||
def describe(self):
|
|
||||||
return (f"{self.path}:{self.lineno} [{self.op}] "
|
|
||||||
f"col{self.col}: {self.before.strip()} -> {self.after.strip()}")
|
|
||||||
|
|
||||||
|
|
||||||
def generate_mutants(root, rel_target):
|
|
||||||
"""Enumerate all mutants for one target file."""
|
|
||||||
abspath = os.path.join(root, rel_target)
|
|
||||||
with open(abspath, "r") as fh:
|
|
||||||
lines = fh.readlines()
|
|
||||||
|
|
||||||
mutants = []
|
|
||||||
for i, line in enumerate(lines, start=1):
|
|
||||||
if not eligible(line):
|
|
||||||
continue
|
|
||||||
# substitution operators
|
|
||||||
seen = set()
|
|
||||||
for tag, s, e, repl in _op_edits(line):
|
|
||||||
key = (s, e, repl)
|
|
||||||
if key in seen:
|
|
||||||
continue
|
|
||||||
seen.add(key)
|
|
||||||
mutated = line[:s] + repl + line[e:]
|
|
||||||
if mutated == line:
|
|
||||||
continue
|
|
||||||
mutants.append(Mutant(rel_target, i, tag, line, mutated, s))
|
|
||||||
# statement deletion
|
|
||||||
m = _STMT_DELETABLE.match(line)
|
|
||||||
if m:
|
|
||||||
indent = line[: len(line) - len(line.lstrip())]
|
|
||||||
cont = "\\" if line.rstrip().endswith("\\") else ""
|
|
||||||
deleted = f"{indent}/* mutant: deleted */ {cont}\n" if cont else f"{indent};\n"
|
|
||||||
mutants.append(Mutant(rel_target, i, "SDL", line, deleted, 0))
|
|
||||||
return mutants
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
|
||||||
# Build / test orchestration against a scratch copy
|
|
||||||
# --------------------------------------------------------------------------- #
|
|
||||||
|
|
||||||
class Runner:
|
|
||||||
def __init__(self, work, timeout):
|
|
||||||
self.work = work
|
|
||||||
self.build = os.path.join(work, "build")
|
|
||||||
self.timeout = timeout
|
|
||||||
|
|
||||||
def _run(self, cmd, timeout=None):
|
|
||||||
return subprocess.run(
|
|
||||||
cmd, cwd=self.work, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
||||||
timeout=timeout,
|
|
||||||
)
|
|
||||||
|
|
||||||
def configure(self):
|
|
||||||
r = self._run(["cmake", "-S", ".", "-B", "build"], timeout=self.timeout)
|
|
||||||
return r.returncode == 0, r.stdout
|
|
||||||
|
|
||||||
def build_and_test(self):
|
|
||||||
"""Return ('killed-compile' | 'killed-test' | 'killed-timeout' | 'survived')."""
|
|
||||||
try:
|
|
||||||
b = self._run(["cmake", "--build", "build"], timeout=self.timeout)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return "killed-timeout"
|
|
||||||
if b.returncode != 0:
|
|
||||||
return "killed-compile"
|
|
||||||
try:
|
|
||||||
t = subprocess.run(
|
|
||||||
["ctest", "--test-dir", "build", "--output-on-failure",
|
|
||||||
"--stop-on-failure"],
|
|
||||||
cwd=self.work, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
||||||
timeout=self.timeout,
|
|
||||||
)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return "killed-timeout"
|
|
||||||
return "survived" if t.returncode == 0 else "killed-test"
|
|
||||||
|
|
||||||
|
|
||||||
def _xml_escape(s):
|
|
||||||
return (s.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
||||||
.replace('"', """))
|
|
||||||
|
|
||||||
|
|
||||||
def write_junit(path, records, targets):
|
|
||||||
"""Write a JUnit XML report. One <testcase> per mutant; a surviving mutant
|
|
||||||
is a <failure> (test-suite gap), a killed mutant is a passing case."""
|
|
||||||
by_file = {t: [] for t in targets}
|
|
||||||
for m, result in records:
|
|
||||||
by_file.setdefault(m.path, []).append((m, result))
|
|
||||||
|
|
||||||
total = len(records)
|
|
||||||
total_fail = sum(1 for _, r in records if r == "survived")
|
|
||||||
out = ['<?xml version="1.0" encoding="UTF-8"?>',
|
|
||||||
f'<testsuites name="mutation" tests="{total}" failures="{total_fail}">']
|
|
||||||
for f, items in by_file.items():
|
|
||||||
if not items:
|
|
||||||
continue
|
|
||||||
fails = sum(1 for _, r in items if r == "survived")
|
|
||||||
out.append(f' <testsuite name="mutation:{_xml_escape(f)}" '
|
|
||||||
f'tests="{len(items)}" failures="{fails}">')
|
|
||||||
for m, result in items:
|
|
||||||
name = _xml_escape(m.describe())
|
|
||||||
cls = "mutation." + _xml_escape(m.path)
|
|
||||||
if result == "survived":
|
|
||||||
detail = _xml_escape(f"{m.before.strip()} -> {m.after.strip()}")
|
|
||||||
out.append(f' <testcase name="{name}" classname="{cls}" time="0">')
|
|
||||||
out.append(f' <failure message="survived mutant '
|
|
||||||
f'({_xml_escape(m.op)} at {_xml_escape(m.path)}:'
|
|
||||||
f'{m.lineno})">{detail}</failure>')
|
|
||||||
out.append(' </testcase>')
|
|
||||||
else:
|
|
||||||
out.append(f' <testcase name="{name}" classname="{cls}" '
|
|
||||||
f'time="0"><system-out>{_xml_escape(result)}'
|
|
||||||
'</system-out></testcase>')
|
|
||||||
out.append(' </testsuite>')
|
|
||||||
out.append('</testsuites>')
|
|
||||||
with open(path, "w") as fh:
|
|
||||||
fh.write("\n".join(out) + "\n")
|
|
||||||
|
|
||||||
|
|
||||||
def copy_tree(src, dst):
|
|
||||||
ignore = shutil.ignore_patterns("build", ".git", "*.o", "*.so", "*~",
|
|
||||||
"#*#", "*.iso", "*.png")
|
|
||||||
shutil.copytree(src, dst, ignore=ignore, symlinks=True)
|
|
||||||
|
|
||||||
|
|
||||||
def read_lines(path):
|
|
||||||
with open(path) as fh:
|
|
||||||
return fh.readlines()
|
|
||||||
|
|
||||||
|
|
||||||
def write_lines(path, lines):
|
|
||||||
with open(path, "w") as fh:
|
|
||||||
fh.writelines(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Line-buffer stdout so progress is visible live under CI / the cmake target.
|
|
||||||
try:
|
|
||||||
sys.stdout.reconfigure(line_buffering=True)
|
|
||||||
except (AttributeError, ValueError):
|
|
||||||
pass
|
|
||||||
here = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
default_root = os.path.dirname(here)
|
|
||||||
|
|
||||||
ap = argparse.ArgumentParser(description="Mutation testing for libakerror")
|
|
||||||
ap.add_argument("--source-root", default=default_root)
|
|
||||||
ap.add_argument("--target", action="append", default=None)
|
|
||||||
ap.add_argument("--work", default=None)
|
|
||||||
ap.add_argument("--timeout", type=int, default=120)
|
|
||||||
ap.add_argument("--threshold", type=float, default=0.0)
|
|
||||||
ap.add_argument("--junit", default=None,
|
|
||||||
help="write a JUnit XML report to this path")
|
|
||||||
ap.add_argument("--max-mutants", type=int, default=0,
|
|
||||||
help="cap the run at N evenly-sampled mutants (0 = all)")
|
|
||||||
ap.add_argument("--list", action="store_true")
|
|
||||||
ap.add_argument("--keep", action="store_true")
|
|
||||||
ap.add_argument("-j", type=int, default=1)
|
|
||||||
args = ap.parse_args()
|
|
||||||
|
|
||||||
root = os.path.abspath(args.source_root)
|
|
||||||
targets = args.target or ["src/error.c", "include/akerror.tmpl.h"]
|
|
||||||
|
|
||||||
# Enumerate mutants from the pristine sources.
|
|
||||||
all_mutants = []
|
|
||||||
for t in targets:
|
|
||||||
all_mutants.extend(generate_mutants(root, t))
|
|
||||||
|
|
||||||
print(f"Generated {len(all_mutants)} mutants across {len(targets)} file(s):")
|
|
||||||
for t in targets:
|
|
||||||
n = sum(1 for m in all_mutants if m.path == t)
|
|
||||||
print(f" {t}: {n}")
|
|
||||||
|
|
||||||
# Optional even-strided sampling to bound run time (CI / smoke tests).
|
|
||||||
if args.max_mutants and len(all_mutants) > args.max_mutants:
|
|
||||||
step = len(all_mutants) / args.max_mutants
|
|
||||||
sampled = [all_mutants[int(i * step)] for i in range(args.max_mutants)]
|
|
||||||
print(f"Sampling {len(sampled)} of {len(all_mutants)} mutants "
|
|
||||||
f"(--max-mutants {args.max_mutants}).")
|
|
||||||
all_mutants = sampled
|
|
||||||
|
|
||||||
if args.list:
|
|
||||||
for m in all_mutants:
|
|
||||||
print(" " + m.describe())
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if not all_mutants:
|
|
||||||
print("No mutants generated; nothing to do.")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# Scratch working copy.
|
|
||||||
work_parent = args.work or tempfile.mkdtemp(prefix="akerr_mut_")
|
|
||||||
work = os.path.join(work_parent, "src") if args.work else work_parent
|
|
||||||
if os.path.exists(work):
|
|
||||||
shutil.rmtree(work)
|
|
||||||
print(f"\nCopying sources to scratch dir: {work}")
|
|
||||||
copy_tree(root, work)
|
|
||||||
|
|
||||||
runner = Runner(work, args.timeout)
|
|
||||||
|
|
||||||
print("Configuring baseline ...")
|
|
||||||
ok, out = runner.configure()
|
|
||||||
if not ok:
|
|
||||||
sys.stderr.write(out.decode(errors="replace"))
|
|
||||||
sys.stderr.write("\nBaseline configure FAILED; aborting.\n")
|
|
||||||
return 2
|
|
||||||
|
|
||||||
print("Verifying baseline is green (no mutation) ...")
|
|
||||||
baseline = runner.build_and_test()
|
|
||||||
if baseline != "survived":
|
|
||||||
sys.stderr.write(f"Baseline is not green ({baseline}); aborting. "
|
|
||||||
"Fix the suite before mutation testing.\n")
|
|
||||||
return 2
|
|
||||||
print("Baseline OK.\n")
|
|
||||||
|
|
||||||
# Group mutants by file so we mutate one file at a time and restore it.
|
|
||||||
killed = {"killed-compile": 0, "killed-test": 0, "killed-timeout": 0}
|
|
||||||
survivors = []
|
|
||||||
records = []
|
|
||||||
total = len(all_mutants)
|
|
||||||
|
|
||||||
# Cache pristine contents per target.
|
|
||||||
pristine = {t: read_lines(os.path.join(work, t)) for t in targets}
|
|
||||||
|
|
||||||
for idx, m in enumerate(all_mutants, start=1):
|
|
||||||
tgt_abs = os.path.join(work, m.path)
|
|
||||||
lines = list(pristine[m.path])
|
|
||||||
lines[m.lineno - 1] = m.after
|
|
||||||
write_lines(tgt_abs, lines)
|
|
||||||
try:
|
|
||||||
result = runner.build_and_test()
|
|
||||||
finally:
|
|
||||||
write_lines(tgt_abs, pristine[m.path]) # always restore
|
|
||||||
|
|
||||||
records.append((m, result))
|
|
||||||
if result == "survived":
|
|
||||||
survivors.append(m)
|
|
||||||
mark = "SURVIVED"
|
|
||||||
else:
|
|
||||||
killed[result] += 1
|
|
||||||
mark = result.upper()
|
|
||||||
print(f"[{idx}/{total}] {mark:16} {m.describe()}")
|
|
||||||
|
|
||||||
total_killed = sum(killed.values())
|
|
||||||
score = 100.0 * total_killed / total if total else 100.0
|
|
||||||
|
|
||||||
print("\n" + "=" * 72)
|
|
||||||
print("MUTATION TESTING SUMMARY")
|
|
||||||
print("=" * 72)
|
|
||||||
print(f" total mutants : {total}")
|
|
||||||
print(f" killed (test) : {killed['killed-test']}")
|
|
||||||
print(f" killed (compile): {killed['killed-compile']}")
|
|
||||||
print(f" killed (timeout): {killed['killed-timeout']}")
|
|
||||||
print(f" survived : {len(survivors)}")
|
|
||||||
print(f" mutation score : {score:.1f}%")
|
|
||||||
if survivors:
|
|
||||||
print("\nSurviving mutants (test-suite gaps -- turn these into tests):")
|
|
||||||
for m in survivors:
|
|
||||||
print(" " + m.describe())
|
|
||||||
|
|
||||||
if args.junit:
|
|
||||||
junit_path = os.path.abspath(args.junit)
|
|
||||||
write_junit(junit_path, records, targets)
|
|
||||||
print(f"\nJUnit report written to: {junit_path}")
|
|
||||||
|
|
||||||
if not args.keep and not args.work:
|
|
||||||
shutil.rmtree(work_parent, ignore_errors=True)
|
|
||||||
else:
|
|
||||||
print(f"\nScratch working copy kept at: {work}")
|
|
||||||
|
|
||||||
if args.threshold > 0 and score < args.threshold:
|
|
||||||
print(f"\nFAIL: mutation score {score:.1f}% < threshold {args.threshold:.1f}%")
|
|
||||||
return 1
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -3,8 +3,8 @@ exec_prefix=${prefix}
|
|||||||
libdir=${exec_prefix}/lib
|
libdir=${exec_prefix}/lib
|
||||||
includedir=${exec_prefix}/include
|
includedir=${exec_prefix}/include
|
||||||
|
|
||||||
Name: akerror
|
Name: sdlerror
|
||||||
Description: AKLabs C error handling library
|
Description: A C error handling library that relies on SDL3
|
||||||
Version: @PROJECT_VERSION@
|
Version: @PROJECT_VERSION@
|
||||||
Cflags: -I${includedir}/
|
Cflags: -I${includedir}/
|
||||||
Libs: -L${libdir} -lakerror
|
Libs: -L${libdir} -lsdlerror
|
||||||
522
src/error.c
522
src/error.c
@@ -1,235 +1,49 @@
|
|||||||
#include "akerror.h"
|
#include "sdlerror.h"
|
||||||
#if defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1
|
#include "stdlib.h"
|
||||||
#include <stdlib.h>
|
|
||||||
#include <stdarg.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#endif // AKERR_USE_STDLIB
|
|
||||||
|
|
||||||
akerr_ErrorContext __akerr_last_ditch;
|
ErrorContext __error_last_ditch;
|
||||||
akerr_ErrorContext *__akerr_last_ignored;
|
ErrorContext *__error_last_ignored;
|
||||||
akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
|
ErrorUnhandledErrorHandler error_handler_unhandled_error;
|
||||||
akerr_ErrorLogFunction akerr_log_method = NULL;
|
|
||||||
|
|
||||||
/*
|
char __ERROR_NAMES[MAX_ERR_VALUE+1][MAX_ERROR_NAME_LENGTH];
|
||||||
* Status-name registry.
|
|
||||||
*
|
|
||||||
* Storage is an open-addressed hash table keyed by status value. Both sizes are
|
|
||||||
* private to this translation unit -- they are deliberately NOT in the public
|
|
||||||
* header, because a consumer-visible table bound is exactly the ABI hazard this
|
|
||||||
* registry replaced. Overriding them changes only this file, so a library and
|
|
||||||
* its consumers can never disagree about the layout.
|
|
||||||
*
|
|
||||||
* The table is never resized or rehashed, so a pointer handed out by
|
|
||||||
* akerr_name_for_status() stays valid for the life of the process. Entries are
|
|
||||||
* never removed, so probing needs no tombstones.
|
|
||||||
*/
|
|
||||||
#ifndef AKERR_STATUS_NAME_SLOTS
|
|
||||||
#define AKERR_STATUS_NAME_SLOTS 4096
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef AKERR_MAX_RESERVED_STATUS_RANGES
|
ErrorContext HEAP_ERROR[MAX_HEAP_ERROR];
|
||||||
#define AKERR_MAX_RESERVED_STATUS_RANGES 64
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* Probing masks with SLOTS-1, so the slot count must be a power of two. This is
|
void error_init()
|
||||||
* the C99-portable spelling of a static assertion (a negative array bound). */
|
|
||||||
typedef char akerr_assert_name_slots_pow2[
|
|
||||||
(AKERR_STATUS_NAME_SLOTS > 0 &&
|
|
||||||
(AKERR_STATUS_NAME_SLOTS & (AKERR_STATUS_NAME_SLOTS - 1)) == 0) ? 1 : -1];
|
|
||||||
|
|
||||||
/* Cap occupancy at 75% so linear probing always meets an empty slot. */
|
|
||||||
#define AKERR_MAX_REGISTERED_STATUS_NAMES \
|
|
||||||
(AKERR_STATUS_NAME_SLOTS - (AKERR_STATUS_NAME_SLOTS / 4))
|
|
||||||
|
|
||||||
#define AKERR_MAX_STATUS_RANGE_OWNER_LENGTH 64
|
|
||||||
|
|
||||||
typedef struct
|
|
||||||
{
|
|
||||||
int status;
|
|
||||||
int used;
|
|
||||||
char name[AKERR_MAX_ERROR_NAME_LENGTH];
|
|
||||||
} akerr_StatusName;
|
|
||||||
|
|
||||||
typedef struct
|
|
||||||
{
|
|
||||||
int first;
|
|
||||||
int last;
|
|
||||||
char owner[AKERR_MAX_STATUS_RANGE_OWNER_LENGTH];
|
|
||||||
} akerr_StatusRange;
|
|
||||||
|
|
||||||
static akerr_StatusName akerr_status_names[AKERR_STATUS_NAME_SLOTS];
|
|
||||||
static int akerr_status_name_count;
|
|
||||||
static akerr_StatusRange akerr_status_ranges[AKERR_MAX_RESERVED_STATUS_RANGES];
|
|
||||||
static int akerr_status_range_count;
|
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR];
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Bounded copy into a fixed buffer. Every argument is checked: this writes
|
|
||||||
* through a caller-supplied pointer for a caller-supplied length, so a NULL or
|
|
||||||
* a non-positive capacity here is a memory error waiting to happen, not
|
|
||||||
* something to absorb and return from quietly.
|
|
||||||
*
|
|
||||||
* Exported under the internal __akerr_ prefix rather than kept static so that
|
|
||||||
* tests/err_copy_string.c can reach these guards. Both in-library callers
|
|
||||||
* validate their arguments first, so nothing else can drive them.
|
|
||||||
*/
|
|
||||||
akerr_ErrorContext *__akerr_copy_string(char *destination, int capacity,
|
|
||||||
const char *source)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
|
|
||||||
FAIL_NONZERO_RETURN(errctx, (destination == NULL || source == NULL),
|
|
||||||
AKERR_NULLPOINTER,
|
|
||||||
"__akerr_copy_string got a NULL %s",
|
|
||||||
destination == NULL ? "destination buffer" : "source string");
|
|
||||||
FAIL_NONZERO_RETURN(errctx, (capacity <= 0), AKERR_VALUE,
|
|
||||||
"__akerr_copy_string got a capacity of %d; a buffer must "
|
|
||||||
"have room for at least a terminator", capacity);
|
|
||||||
|
|
||||||
strncpy(destination, source, (size_t)capacity - 1);
|
|
||||||
destination[capacity - 1] = '\0';
|
|
||||||
SUCCEED_RETURN(errctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Compare against each element address rather than testing the address range.
|
|
||||||
* A range test also accepts pointers into the *interior* of an element, which
|
|
||||||
* would then be treated as the head of an akerr_ErrorContext and written
|
|
||||||
* through. Keep this an element-wise scan; it is not a missed optimization.
|
|
||||||
*/
|
|
||||||
int akerr_valid_error_address(akerr_ErrorContext *ptr)
|
|
||||||
{
|
|
||||||
// Is this within the memory region occupied by AKERR_ARRAY_ERROR?
|
|
||||||
if ( ptr == NULL ) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
|
||||||
if ( ptr == &AKERR_ARRAY_ERROR[i] ) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void akerr_default_logger(const char *fmt, ...)
|
|
||||||
{
|
|
||||||
#if defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1
|
|
||||||
va_list ap;
|
|
||||||
|
|
||||||
va_start(ap, fmt);
|
|
||||||
vfprintf(stderr, fmt, ap);
|
|
||||||
va_end(ap);
|
|
||||||
#else
|
|
||||||
return;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The library naming its own codes.
|
|
||||||
*
|
|
||||||
* akerr_init() returns void and runs before any consumer frame exists, so there
|
|
||||||
* is nothing to PASS an error to: this *is* the top of the stack. FINISH_NORETURN
|
|
||||||
* is the library's idiom for that position -- the same one main() uses -- so an
|
|
||||||
* unhandled failure prints its stack trace and goes to
|
|
||||||
* akerr_handler_unhandled_error, which terminates.
|
|
||||||
*
|
|
||||||
* That is fatal on purpose. The library can only fail to name its own status
|
|
||||||
* codes if the build is misconfigured -- a name table too small to hold even
|
|
||||||
* the library's own entries, or a reservation that did not take -- and the
|
|
||||||
* consequence of continuing is every later stack trace in the process printing
|
|
||||||
* "Unknown Error" for a code the library defines. That is a startup defect, and
|
|
||||||
* it is far cheaper to see it at init than to debug it from a degraded trace.
|
|
||||||
*
|
|
||||||
* The generated errno table calls this rather than registering names directly,
|
|
||||||
* so that all of this control flow lives here in one place.
|
|
||||||
*/
|
|
||||||
void __akerr_name_library_status(int status, const char *name)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(errctx, akerr_register_status_name(AKERR_LIBRARY_OWNER, status, name));
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} FINISH_NORETURN(errctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Idempotent. `inited` is set before any work so that the registry calls below
|
|
||||||
* -- and the public registry entry points, which all call akerr_init() so that
|
|
||||||
* a consumer reserving its range before anything else touches the library
|
|
||||||
* cannot have that reservation wiped by a later first-use of the pool -- see
|
|
||||||
* themselves as already initialized instead of recursing.
|
|
||||||
*/
|
|
||||||
void akerr_init()
|
|
||||||
{
|
{
|
||||||
static int inited = 0;
|
static int inited = 0;
|
||||||
if ( inited == 0 ) {
|
if ( inited == 0 ) {
|
||||||
|
for (int i = 0; i < MAX_HEAP_ERROR; i++ ) {
|
||||||
|
memset((void *)&HEAP_ERROR[i], 0x00, sizeof(ErrorContext));
|
||||||
|
HEAP_ERROR[i].heapid = i;
|
||||||
|
HEAP_ERROR[i].stacktracebufptr = (char *)&HEAP_ERROR[i].stacktracebuf;
|
||||||
|
}
|
||||||
|
__error_last_ignored = NULL;
|
||||||
|
memset((void *)&__error_last_ditch, 0x00, sizeof(ErrorContext));
|
||||||
|
__error_last_ditch.stacktracebufptr = (char *)&__error_last_ditch.stacktracebuf;
|
||||||
|
error_handler_unhandled_error = &error_default_handler_unhandled_error;
|
||||||
|
memset((void *)&__ERROR_NAMES[0], 0x00, ((MAX_ERR_VALUE+1) * MAX_ERROR_NAME_LENGTH));
|
||||||
|
|
||||||
|
error_name_for_status(ERR_NULLPOINTER, "Null Pointer Error");
|
||||||
|
error_name_for_status(ERR_OUTOFBOUNDS, "Out Of Bounds Error");
|
||||||
|
error_name_for_status(ERR_SDL, "SDL Library Error");
|
||||||
|
error_name_for_status(ERR_ATTRIBUTE, "Attribute Error");
|
||||||
|
error_name_for_status(ERR_TYPE, "Type Error");
|
||||||
|
error_name_for_status(ERR_KEY, "Key Error");
|
||||||
|
error_name_for_status(ERR_HEAP, "Heap Error");
|
||||||
|
error_name_for_status(ERR_INDEX, "Index Error");
|
||||||
|
error_name_for_status(ERR_FORMAT, "Format Error");
|
||||||
|
error_name_for_status(ERR_IO, "Input Output Error");
|
||||||
|
error_name_for_status(ERR_REGISTRY, "Registry Error");
|
||||||
|
error_name_for_status(ERR_VALUE, "Value Error");
|
||||||
|
error_name_for_status(ERR_BEHAVIOR, "Behavior Error");
|
||||||
|
error_name_for_status(ERR_RELATIONSHIP, "Relationship Error");
|
||||||
|
|
||||||
inited = 1;
|
inited = 1;
|
||||||
for (int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
|
||||||
memset((void *)&AKERR_ARRAY_ERROR[i], 0x00, sizeof(akerr_ErrorContext));
|
|
||||||
AKERR_ARRAY_ERROR[i].arrayid = i;
|
|
||||||
AKERR_ARRAY_ERROR[i].stacktracebufptr = (char *)&AKERR_ARRAY_ERROR[i].stacktracebuf;
|
|
||||||
}
|
|
||||||
__akerr_last_ignored = NULL;
|
|
||||||
memset((void *)&__akerr_last_ditch, 0x00, sizeof(akerr_ErrorContext));
|
|
||||||
__akerr_last_ditch.stacktracebufptr = (char *)&__akerr_last_ditch.stacktracebuf;
|
|
||||||
if ( akerr_log_method == NULL ) {
|
|
||||||
akerr_log_method = &akerr_default_logger;
|
|
||||||
}
|
|
||||||
akerr_handler_unhandled_error = &akerr_default_handler_unhandled_error;
|
|
||||||
memset((void *)&akerr_status_names[0], 0x00, sizeof(akerr_status_names));
|
|
||||||
memset((void *)&akerr_status_ranges[0], 0x00, sizeof(akerr_status_ranges));
|
|
||||||
akerr_status_name_count = 0;
|
|
||||||
akerr_status_range_count = 0;
|
|
||||||
|
|
||||||
/* errno and AKERR_* values are the library-owned compatibility band.
|
|
||||||
* This must precede every registration below: naming a status is only
|
|
||||||
* permitted inside a reserved range. Terminal for the same reason as
|
|
||||||
* __akerr_name_library_status(), and handled the same way: without this
|
|
||||||
* band the library owns nothing, so none of the names below could
|
|
||||||
* register either. */
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(errctx, akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
|
||||||
AKERR_LIBRARY_OWNER));
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} FINISH_NORETURN(errctx);
|
|
||||||
|
|
||||||
/* Every AKERR_* code gets a name; tests/err_error_names.c asserts the
|
|
||||||
* list is exhaustive so a new code cannot be added without one. */
|
|
||||||
__akerr_name_library_status(AKERR_NULLPOINTER, "Null Pointer Error");
|
|
||||||
__akerr_name_library_status(AKERR_OUTOFBOUNDS, "Out Of Bounds Error");
|
|
||||||
__akerr_name_library_status(AKERR_API, "API Error");
|
|
||||||
__akerr_name_library_status(AKERR_ATTRIBUTE, "Attribute Error");
|
|
||||||
__akerr_name_library_status(AKERR_TYPE, "Type Error");
|
|
||||||
__akerr_name_library_status(AKERR_KEY, "Key Error");
|
|
||||||
__akerr_name_library_status(AKERR_INDEX, "Index Error");
|
|
||||||
__akerr_name_library_status(AKERR_FORMAT, "Format Error");
|
|
||||||
__akerr_name_library_status(AKERR_IO, "Input Output Error");
|
|
||||||
__akerr_name_library_status(AKERR_VALUE, "Value Error");
|
|
||||||
__akerr_name_library_status(AKERR_RELATIONSHIP, "Relationship Error");
|
|
||||||
__akerr_name_library_status(AKERR_EOF, "End Of File");
|
|
||||||
__akerr_name_library_status(AKERR_CIRCULAR_REFERENCE, "Circular Reference Error");
|
|
||||||
__akerr_name_library_status(AKERR_ITERATOR_BREAK, "Iterator Break");
|
|
||||||
__akerr_name_library_status(AKERR_NOT_IMPLEMENTED, "Not Implemented");
|
|
||||||
__akerr_name_library_status(AKERR_BADEXC, "Invalid akerr_ErrorContext");
|
|
||||||
__akerr_name_library_status(AKERR_STATUS_RANGE_OVERLAP, "Status Range Overlap");
|
|
||||||
__akerr_name_library_status(AKERR_STATUS_RANGE_FULL, "Status Range Table Full");
|
|
||||||
__akerr_name_library_status(AKERR_STATUS_RANGE_INVALID, "Invalid Status Range");
|
|
||||||
__akerr_name_library_status(AKERR_STATUS_NAME_UNRESERVED, "Unreserved Status Name");
|
|
||||||
__akerr_name_library_status(AKERR_STATUS_NAME_FOREIGN, "Foreign Status Name");
|
|
||||||
__akerr_name_library_status(AKERR_STATUS_NAME_FULL, "Status Name Registry Full");
|
|
||||||
__akerr_name_library_status(AKERR_STATUS_NAME_INVALID, "Invalid Status Name");
|
|
||||||
#if (defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1) || (!defined(AKERR_USE_STDLIB))
|
|
||||||
akerr_init_errno();
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void akerr_default_handler_unhandled_error(akerr_ErrorContext *errctx)
|
void error_default_handler_unhandled_error(ErrorContext *errctx)
|
||||||
{
|
{
|
||||||
if ( errctx == NULL ) {
|
if ( errctx == NULL ) {
|
||||||
exit(1);
|
exit(1);
|
||||||
@@ -237,276 +51,46 @@ void akerr_default_handler_unhandled_error(akerr_ErrorContext *errctx)
|
|||||||
exit(errctx->status);
|
exit(errctx->status);
|
||||||
}
|
}
|
||||||
|
|
||||||
akerr_ErrorContext *akerr_next_error()
|
ErrorContext *heap_next_error()
|
||||||
{
|
{
|
||||||
for (int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
for (int i = 0; i < MAX_HEAP_ERROR; i++ ) {
|
||||||
if ( AKERR_ARRAY_ERROR[i].refcount == 0 ) {
|
if ( HEAP_ERROR[i].refcount == 0 ) {
|
||||||
return &AKERR_ARRAY_ERROR[i];
|
return &HEAP_ERROR[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (akerr_ErrorContext *)NULL;
|
return (ErrorContext *)NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
akerr_ErrorContext *akerr_release_error(akerr_ErrorContext *err)
|
ErrorContext *heap_release_error(ErrorContext *err)
|
||||||
{
|
{
|
||||||
int oldid = 0;
|
int oldid = 0;
|
||||||
if ( err == NULL ) {
|
if ( err == NULL ) {
|
||||||
akerr_ErrorContext *errctx = &__akerr_last_ditch;
|
ErrorContext *errctx = &__error_last_ditch;
|
||||||
FAIL_RETURN(errctx, AKERR_NULLPOINTER, "akerr_release_error got NULL context pointer");
|
FAIL_RETURN(errctx, ERR_NULLPOINTER, "heap_release_error got NULL context pointer");
|
||||||
}
|
}
|
||||||
if ( err->refcount > 0 ) {
|
if ( err->refcount > 0 ) {
|
||||||
err->refcount -= 1;
|
err->refcount -= 1;
|
||||||
}
|
}
|
||||||
if ( err->refcount == 0 ) {
|
if ( err->refcount == 0 ) {
|
||||||
oldid = err->arrayid;
|
oldid = err->heapid;
|
||||||
memset(err, 0x00, sizeof(akerr_ErrorContext));
|
memset(err, 0x00, sizeof(ErrorContext));
|
||||||
err->stacktracebufptr = (char *)&err->stacktracebuf;
|
err->stacktracebufptr = (char *)&err->stacktracebuf;
|
||||||
err->arrayid = oldid;
|
err->heapid = oldid;
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
// returns or sets the name for the given status.
|
||||||
* Scatter the status across the table. Status values are typically dense runs
|
// Call with name = NULL to retrieve a status.
|
||||||
* (errno 1..N, then a library's block at its base), which linear probing on the
|
char *error_name_for_status(int status, char *name)
|
||||||
* raw value would pile into one cluster, so mix the bits first.
|
|
||||||
*/
|
|
||||||
static unsigned akerr_status_hash(int status)
|
|
||||||
{
|
{
|
||||||
unsigned h = (unsigned)status;
|
if ( status > MAX_ERR_VALUE ) {
|
||||||
|
|
||||||
h ^= h >> 16;
|
|
||||||
h *= 0x85ebca6bu;
|
|
||||||
h ^= h >> 13;
|
|
||||||
h *= 0xc2b2ae35u;
|
|
||||||
h ^= h >> 16;
|
|
||||||
return h & (unsigned)(AKERR_STATUS_NAME_SLOTS - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Find the slot holding `status`. With create != 0, claim a free slot for it if
|
|
||||||
* it is not present yet. Returns NULL when the status is absent and either no
|
|
||||||
* slot was requested or the registry is full.
|
|
||||||
*
|
|
||||||
* The `& (AKERR_STATUS_NAME_SLOTS - 1)` below is load-bearing and fails
|
|
||||||
* silently: off by one in either direction and the probe indexes past
|
|
||||||
* akerr_status_names, writing into whatever BSS follows rather than crashing.
|
|
||||||
* A test that only counts how many names registered before the table filled
|
|
||||||
* cannot see that -- a probe sequence collapsed to two slots still registers
|
|
||||||
* "some" names. Any change to the probe sequence, the occupancy cap, or the
|
|
||||||
* power-of-two assumption needs a test that reads every entry back by its own
|
|
||||||
* distinct value; tests/err_maxval.c does.
|
|
||||||
*/
|
|
||||||
static akerr_StatusName *akerr_status_slot(int status, int create)
|
|
||||||
{
|
|
||||||
unsigned slot = akerr_status_hash(status);
|
|
||||||
|
|
||||||
for ( int probe = 0; probe < AKERR_STATUS_NAME_SLOTS; probe++ ) {
|
|
||||||
akerr_StatusName *entry = &akerr_status_names[slot];
|
|
||||||
|
|
||||||
if ( entry->used == 0 ) {
|
|
||||||
if ( create == 0 ||
|
|
||||||
akerr_status_name_count >= AKERR_MAX_REGISTERED_STATUS_NAMES ) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
entry->used = 1;
|
|
||||||
entry->status = status;
|
|
||||||
entry->name[0] = '\0';
|
|
||||||
akerr_status_name_count++;
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
if ( entry->status == status ) {
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
slot = (slot + 1u) & (unsigned)(AKERR_STATUS_NAME_SLOTS - 1);
|
|
||||||
}
|
|
||||||
/* Unreachable: occupancy is capped below the slot count, so the probe above
|
|
||||||
* always meets a free slot. Present so a future change to that cap cannot
|
|
||||||
* turn this into a runaway loop. */
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* The reservation covering `status`, or NULL if nobody has claimed it. */
|
|
||||||
static akerr_StatusRange *akerr_range_for_status(int status)
|
|
||||||
{
|
|
||||||
for ( int i = 0; i < akerr_status_range_count; i++ ) {
|
|
||||||
if ( status >= akerr_status_ranges[i].first &&
|
|
||||||
status <= akerr_status_ranges[i].last ) {
|
|
||||||
return &akerr_status_ranges[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Shared body of both registration entry points. A NULL owner means the caller
|
|
||||||
* did not identify itself (the legacy two-argument akerr_name_for_status path):
|
|
||||||
* the status must still lie inside *some* reservation, but we cannot check that
|
|
||||||
* it is the caller's. Every refusal raises an error -- a name that silently
|
|
||||||
* fails to register degrades into "Unknown Error" in stack traces, which is
|
|
||||||
* exactly the kind of quiet loss this registry exists to prevent -- so the
|
|
||||||
* message carries everything a caller needs to see in a stack trace.
|
|
||||||
*/
|
|
||||||
static akerr_ErrorContext AKERR_NOIGNORE *akerr_store_status_name(const char *owner,
|
|
||||||
int status,
|
|
||||||
const char *name)
|
|
||||||
{
|
|
||||||
akerr_StatusRange *range;
|
|
||||||
akerr_StatusName *entry;
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
|
|
||||||
FAIL_NONZERO_RETURN(errctx, (name == NULL), AKERR_STATUS_NAME_INVALID,
|
|
||||||
"Refusing to name status %d for %s: the name is NULL",
|
|
||||||
status, owner == NULL ? "an unnamed caller" : owner);
|
|
||||||
FAIL_NONZERO_RETURN(errctx,
|
|
||||||
(owner != NULL && ( owner[0] == '\0' ||
|
|
||||||
strlen(owner) >= AKERR_MAX_STATUS_RANGE_OWNER_LENGTH )),
|
|
||||||
AKERR_STATUS_NAME_INVALID,
|
|
||||||
"Refusing to name status %d (\"%s\"): the owner string "
|
|
||||||
"is empty or longer than %d characters",
|
|
||||||
status, name, AKERR_MAX_STATUS_RANGE_OWNER_LENGTH - 1);
|
|
||||||
|
|
||||||
range = akerr_range_for_status(status);
|
|
||||||
FAIL_ZERO_RETURN(errctx, range, AKERR_STATUS_NAME_UNRESERVED,
|
|
||||||
"Refusing to name status %d (\"%s\") for %s: no reserved "
|
|
||||||
"range contains it. Call akerr_reserve_status_range() first.",
|
|
||||||
status, name, owner == NULL ? "an unnamed caller" : owner);
|
|
||||||
FAIL_NONZERO_RETURN(errctx,
|
|
||||||
(owner != NULL && strcmp(owner, range->owner) != 0),
|
|
||||||
AKERR_STATUS_NAME_FOREIGN,
|
|
||||||
"Refusing to name status %d (\"%s\") for %s: that status "
|
|
||||||
"is in range %d..%d owned by %s.",
|
|
||||||
status, name, owner,
|
|
||||||
range->first, range->last, range->owner);
|
|
||||||
|
|
||||||
entry = akerr_status_slot(status, 1);
|
|
||||||
FAIL_ZERO_RETURN(errctx, entry, AKERR_STATUS_NAME_FULL,
|
|
||||||
"Status name registry is full (%d entries); dropping name "
|
|
||||||
"\"%s\" for status %d. Rebuild libakerror with a larger "
|
|
||||||
"AKERR_STATUS_NAME_SLOTS.",
|
|
||||||
AKERR_MAX_REGISTERED_STATUS_NAMES, name, status);
|
|
||||||
|
|
||||||
PASS(errctx, __akerr_copy_string(entry->name, AKERR_MAX_ERROR_NAME_LENGTH, name));
|
|
||||||
SUCCEED_RETURN(errctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Register a name for a status inside a range the caller reserved. Both strings
|
|
||||||
* are checked here rather than only inside akerr_store_status_name(): the store
|
|
||||||
* accepts a NULL owner for the legacy akerr_name_for_status() path, so a NULL
|
|
||||||
* arriving through *this* entry point would be read as "caller did not identify
|
|
||||||
* itself" and skip the ownership check entirely.
|
|
||||||
*/
|
|
||||||
akerr_ErrorContext *akerr_register_status_name(const char *owner, int status, const char *name)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
|
|
||||||
FAIL_NONZERO_RETURN(errctx, (owner == NULL), AKERR_STATUS_NAME_INVALID,
|
|
||||||
"Refusing to name status %d: the owner string is NULL. "
|
|
||||||
"Pass the same owner you reserved the range with.",
|
|
||||||
status);
|
|
||||||
FAIL_NONZERO_RETURN(errctx, (name == NULL), AKERR_STATUS_NAME_INVALID,
|
|
||||||
"Refusing to name status %d for %s: the name is NULL",
|
|
||||||
status, owner);
|
|
||||||
PASS(errctx, akerr_store_status_name(owner, status, name));
|
|
||||||
SUCCEED_RETURN(errctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Return or set a name. Status magnitude is unrelated to storage size.
|
|
||||||
*
|
|
||||||
* The set path is the legacy two-argument form. It returns a name, so it cannot
|
|
||||||
* hand an error back to its caller and cannot raise: it handles the refusal
|
|
||||||
* here, converting it to the "Unknown Error" sentinel the way any function that
|
|
||||||
* must return a value converts a caught error into one.
|
|
||||||
* akerr_register_status_name() is the form that raises.
|
|
||||||
*
|
|
||||||
* The lookup path (name == NULL) deliberately stays clear of all of this. FAIL
|
|
||||||
* calls it to render a status into a stack trace, so it must not itself need an
|
|
||||||
* error context.
|
|
||||||
*/
|
|
||||||
char *akerr_name_for_status(int status, char *name)
|
|
||||||
{
|
|
||||||
akerr_StatusName *entry;
|
|
||||||
|
|
||||||
akerr_init();
|
|
||||||
if ( name != NULL ) {
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
int refused = 0;
|
|
||||||
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(errctx, akerr_store_status_name(NULL, status, name));
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} HANDLE_DEFAULT(errctx) {
|
|
||||||
LOG_ERROR_WITH_MESSAGE(errctx, "** REFUSED STATUS NAME **");
|
|
||||||
refused = 1;
|
|
||||||
} FINISH_NORETURN(errctx);
|
|
||||||
|
|
||||||
if ( refused != 0 ) {
|
|
||||||
return "Unknown Error";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entry = akerr_status_slot(status, 0);
|
|
||||||
if ( entry == NULL ) {
|
|
||||||
return "Unknown Error";
|
return "Unknown Error";
|
||||||
}
|
}
|
||||||
return entry->name;
|
if ( name != NULL ) {
|
||||||
}
|
strncpy((char *)&__ERROR_NAMES[status], name, MAX_ERROR_NAME_LENGTH);
|
||||||
|
|
||||||
/* Reserve an inclusive status interval and reject collisions. */
|
|
||||||
akerr_ErrorContext *akerr_reserve_status_range(int first_status, int count, const char *owner)
|
|
||||||
{
|
|
||||||
int last_status;
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
|
|
||||||
FAIL_NONZERO_RETURN(errctx,
|
|
||||||
(count <= 0 || owner == NULL || owner[0] == '\0' ||
|
|
||||||
strlen(owner) >= AKERR_MAX_STATUS_RANGE_OWNER_LENGTH ||
|
|
||||||
first_status > INT_MAX - (count - 1)),
|
|
||||||
AKERR_STATUS_RANGE_INVALID,
|
|
||||||
"Invalid status range reservation: %d status values from "
|
|
||||||
"%d for %s (count must be positive, the owner string "
|
|
||||||
"non-empty and shorter than %d characters, and the range "
|
|
||||||
"must not overflow int)",
|
|
||||||
count, first_status, owner == NULL ? "(null)" : owner,
|
|
||||||
AKERR_MAX_STATUS_RANGE_OWNER_LENGTH);
|
|
||||||
last_status = first_status + count - 1;
|
|
||||||
|
|
||||||
for ( int i = 0; i < akerr_status_range_count; i++ ) {
|
|
||||||
if ( first_status <= akerr_status_ranges[i].last &&
|
|
||||||
last_status >= akerr_status_ranges[i].first ) {
|
|
||||||
if ( first_status == akerr_status_ranges[i].first &&
|
|
||||||
last_status == akerr_status_ranges[i].last &&
|
|
||||||
strcmp(owner, akerr_status_ranges[i].owner) == 0 ) {
|
|
||||||
SUCCEED_RETURN(errctx);
|
|
||||||
}
|
|
||||||
FAIL_RETURN(errctx, AKERR_STATUS_RANGE_OVERLAP,
|
|
||||||
"Status range %d..%d requested by %s overlaps %d..%d "
|
|
||||||
"owned by %s",
|
|
||||||
first_status, last_status, owner,
|
|
||||||
akerr_status_ranges[i].first,
|
|
||||||
akerr_status_ranges[i].last,
|
|
||||||
akerr_status_ranges[i].owner);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
FAIL_NONZERO_RETURN(errctx,
|
return (char *)&__ERROR_NAMES[status];
|
||||||
(akerr_status_range_count == AKERR_MAX_RESERVED_STATUS_RANGES),
|
|
||||||
AKERR_STATUS_RANGE_FULL,
|
|
||||||
"Status range table is full (%d ranges); refusing %d..%d "
|
|
||||||
"for %s.",
|
|
||||||
AKERR_MAX_RESERVED_STATUS_RANGES,
|
|
||||||
first_status, last_status, owner);
|
|
||||||
|
|
||||||
/* The owner copy is what commits the entry, so the count only advances
|
|
||||||
* once it has succeeded -- a half-written reservation would claim the
|
|
||||||
* range under an empty owner nobody could ever match. */
|
|
||||||
akerr_status_ranges[akerr_status_range_count].first = first_status;
|
|
||||||
akerr_status_ranges[akerr_status_range_count].last = last_status;
|
|
||||||
PASS(errctx, __akerr_copy_string(akerr_status_ranges[akerr_status_range_count].owner,
|
|
||||||
AKERR_MAX_STATUS_RANGE_OWNER_LENGTH, owner));
|
|
||||||
akerr_status_range_count++;
|
|
||||||
SUCCEED_RETURN(errctx);
|
|
||||||
}
|
}
|
||||||
|
|||||||
5
test.sh
5
test.sh
@@ -1,5 +0,0 @@
|
|||||||
cmake -S . -B build
|
|
||||||
cmake --build build
|
|
||||||
ctest --test-dir build --output-on-failure --output-junit "$(pwd)/ctest-junit.xml"
|
|
||||||
python3 scripts/mutation_test.py --target src/error.c --junit mutation-junit.xml --threshold 65
|
|
||||||
python3 scripts/coverage.py --junit coverage-junit.xml --threshold 90 --branch-threshold 50
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
# Mutation testing
|
|
||||||
|
|
||||||
The unit tests tell us the library works. **Mutation testing tells us the tests
|
|
||||||
work** — that they would actually fail if the library were broken.
|
|
||||||
|
|
||||||
`scripts/mutation_test.py` deliberately breaks the library in small ways
|
|
||||||
("mutants"), one at a time, and runs the whole CTest suite against each broken
|
|
||||||
copy:
|
|
||||||
|
|
||||||
* if the tests **fail**, the mutant is **killed** — good, the suite caught it;
|
|
||||||
* if the tests still **pass**, the mutant **survived** — a bug of that shape
|
|
||||||
would slip through, so it points at a missing test.
|
|
||||||
|
|
||||||
The **mutation score** is `killed / (killed + survived)`. A surviving mutant is
|
|
||||||
a to-do item: write a test that distinguishes the mutant from the original.
|
|
||||||
|
|
||||||
## Running
|
|
||||||
|
|
||||||
No third-party tools are required — just Python 3 and the normal
|
|
||||||
cmake/ctest toolchain. The harness never touches your working tree; it copies
|
|
||||||
the repo to a scratch directory and mutates the copy.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Default: mutate src/error.c and include/akerror.tmpl.h
|
|
||||||
scripts/mutation_test.py
|
|
||||||
|
|
||||||
# Faster: just the C source
|
|
||||||
scripts/mutation_test.py --target src/error.c
|
|
||||||
|
|
||||||
# See what would run without building anything
|
|
||||||
scripts/mutation_test.py --target src/error.c --list
|
|
||||||
|
|
||||||
# Gate CI: exit non-zero if the score drops below 90%
|
|
||||||
scripts/mutation_test.py --threshold 90
|
|
||||||
```
|
|
||||||
|
|
||||||
Via CMake (configures a build first if needed):
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cmake --build build --target mutation
|
|
||||||
```
|
|
||||||
|
|
||||||
Useful flags: `--timeout SECONDS` (per-suite build+test cap; a mutant that
|
|
||||||
hangs is counted as killed), `--keep` (retain the scratch copy for debugging),
|
|
||||||
`--work DIR` (use a specific scratch directory), `--junit FILE` (write a JUnit
|
|
||||||
XML report — surviving mutants appear as failing test cases).
|
|
||||||
|
|
||||||
## CI reporting
|
|
||||||
|
|
||||||
Both the unit tests and the mutation run emit JUnit XML that CI consumes:
|
|
||||||
|
|
||||||
* `ctest --test-dir build --output-junit "$(pwd)/ctest-junit.xml"` — note the
|
|
||||||
absolute path; `--output-junit` otherwise resolves relative to the test dir.
|
|
||||||
* `scripts/mutation_test.py --junit mutation-junit.xml`
|
|
||||||
|
|
||||||
`.gitea/workflows/ci.yaml` runs both and feeds the XML to
|
|
||||||
`mikepenz/action-junit-report` (with `if: always()`, so results publish even
|
|
||||||
when a gate fails). The reporter runs with `annotate_only: true`: Gitea does not
|
|
||||||
implement the Checks API the action uses to create a check run, so creating one
|
|
||||||
404s (mikepenz/action-junit-report#23). `annotate_only` skips that call and the
|
|
||||||
results surface via the job summary (`detailed_summary: true`) instead. The
|
|
||||||
generated `*-junit.xml` files are git-ignored.
|
|
||||||
|
|
||||||
## Mutation operators
|
|
||||||
|
|
||||||
Each mutant changes exactly one location by one of:
|
|
||||||
|
|
||||||
| Tag | Operator | Example |
|
|
||||||
|-----|--------------------------------|----------------------------------|
|
|
||||||
| ROR | relational operator | `==` → `!=`, `<` → `<=`, `>=` → `>` |
|
|
||||||
| LCR | logical connector | `&&` → `\|\|` |
|
|
||||||
| BCR | boolean constant | `true` → `false` |
|
|
||||||
| AOR | arithmetic / compound assign | `+` → `-`, `+=` → `-=` |
|
|
||||||
| ICR | integer literal | `0` → `1`, `1` → `0` |
|
|
||||||
| SDL | statement deletion | `err->refcount += 1;` → *(removed)* |
|
|
||||||
|
|
||||||
Preprocessor control lines, comments, and the block of error-code / buffer-size
|
|
||||||
`#define`s are skipped: mutating those produces equivalent or uninteresting
|
|
||||||
mutants that only add noise.
|
|
||||||
|
|
||||||
## Interpreting survivors
|
|
||||||
|
|
||||||
Not every survivor is a test gap — some mutants are **equivalent** (they don't
|
|
||||||
change observable behaviour, e.g. resizing an internal scratch buffer). For each
|
|
||||||
survivor, decide:
|
|
||||||
|
|
||||||
1. **Real gap** → add or strengthen a test in `tests/` so the mutant is killed,
|
|
||||||
then re-run.
|
|
||||||
2. **Equivalent mutant** → no test can catch it; leave a note. If a specific
|
|
||||||
line is a persistent source of equivalents, narrow the target with
|
|
||||||
`--target` or extend the skip rules in `scripts/mutation_test.py`.
|
|
||||||
|
|
||||||
Re-run after adding tests and confirm the score went up.
|
|
||||||
|
|
||||||
## Current status
|
|
||||||
|
|
||||||
`src/error.c` scores ~77% (the CI gate is set to 65% for headroom). The
|
|
||||||
remaining survivors are dominated by:
|
|
||||||
|
|
||||||
* **Equivalent mutants** in `akerr_init`: deleting the `memset`/`NULL` setup of
|
|
||||||
file-scope statics (`AKERR_ARRAY_ERROR`, `__akerr_last_ditch`,
|
|
||||||
`__akerr_last_ignored`) changes nothing, because C already zero-initializes
|
|
||||||
objects with static storage duration. `int oldid = 0;` → `1` is likewise
|
|
||||||
dead: it is overwritten before use.
|
|
||||||
* **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
|
|
||||||
itself the test, and `tests/err_maxval.c` covers the runtime consequence.
|
|
||||||
* **Hash and probe details** in `akerr_status_slot`: dropping one of the
|
|
||||||
multiply steps in `akerr_status_hash` leaves a worse but still correct hash,
|
|
||||||
and probing backwards (`slot - 1u`) is an equally valid sequence over a
|
|
||||||
power-of-two table. Both are behaviorally equivalent.
|
|
||||||
* **The `capacity <= 0` guard** in `akerr_copy_string`, which is defensive: both
|
|
||||||
call sites pass a positive constant.
|
|
||||||
|
|
||||||
Findings surfaced by mutation testing:
|
|
||||||
|
|
||||||
* **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
|
|
||||||
exist. `tests/err_maxval.c` covers arbitrary `int` values and registry
|
|
||||||
exhaustion.
|
|
||||||
* **Fixed:** the open-addressing probe mask (`& (AKERR_STATUS_NAME_SLOTS - 1)`)
|
|
||||||
could be mutated to `- 0` or `+ 1` — both of which index past the end of the
|
|
||||||
table — without any test noticing. `tests/err_maxval.c` only asserted that
|
|
||||||
*some* names registered before the table filled, which a collapsed probe
|
|
||||||
sequence still satisfies. It now requires a substantial number of entries and
|
|
||||||
reads every one of them back by its own distinct name, so a probe that
|
|
||||||
revisits slots fails on both counts.
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Cover the conditional break/return failure macros:
|
|
||||||
* FAIL_ZERO_BREAK / FAIL_NONZERO_BREAK / FAIL_BREAK (inside ATTEMPT)
|
|
||||||
* FAIL_ZERO_RETURN / FAIL_NONZERO_RETURN / FAIL_RETURN (direct return)
|
|
||||||
* Each is checked in both its failing and its non-failing (pass-through) state.
|
|
||||||
*/
|
|
||||||
|
|
||||||
akerr_ErrorContext *zero_break(int x)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
FAIL_ZERO_BREAK(e, x, AKERR_VALUE, "x was zero");
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} FINISH(e, true);
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
akerr_ErrorContext *nonzero_break(int x)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
FAIL_NONZERO_BREAK(e, x, AKERR_INDEX, "x was nonzero");
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} FINISH(e, true);
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
akerr_ErrorContext *always_break(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
FAIL_BREAK(e, AKERR_IO, "always");
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} FINISH(e, true);
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
akerr_ErrorContext *zero_return(int x)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
FAIL_ZERO_RETURN(e, x, AKERR_KEY, "x was zero");
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
akerr_ErrorContext *nonzero_return(int x)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
FAIL_NONZERO_RETURN(e, x, AKERR_TYPE, "x was nonzero");
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
akerr_init();
|
|
||||||
|
|
||||||
akerr_ErrorContext *r;
|
|
||||||
|
|
||||||
r = zero_break(0);
|
|
||||||
AKERR_CHECK_STATUS(r, AKERR_VALUE);
|
|
||||||
r = akerr_release_error(r);
|
|
||||||
AKERR_CHECK(zero_break(7) == NULL);
|
|
||||||
|
|
||||||
r = nonzero_break(7);
|
|
||||||
AKERR_CHECK_STATUS(r, AKERR_INDEX);
|
|
||||||
r = akerr_release_error(r);
|
|
||||||
AKERR_CHECK(nonzero_break(0) == NULL);
|
|
||||||
|
|
||||||
r = always_break();
|
|
||||||
AKERR_CHECK_STATUS(r, AKERR_IO);
|
|
||||||
r = akerr_release_error(r);
|
|
||||||
|
|
||||||
r = zero_return(0);
|
|
||||||
AKERR_CHECK_STATUS(r, AKERR_KEY);
|
|
||||||
r = akerr_release_error(r);
|
|
||||||
AKERR_CHECK(zero_return(7) == NULL);
|
|
||||||
|
|
||||||
r = nonzero_return(7);
|
|
||||||
AKERR_CHECK_STATUS(r, AKERR_TYPE);
|
|
||||||
r = akerr_release_error(r);
|
|
||||||
AKERR_CHECK(nonzero_return(0) == NULL);
|
|
||||||
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
fprintf(stderr, "err_break_variants ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
#ifndef AKERR_TEST_CAPTURE_H
|
|
||||||
#define AKERR_TEST_CAPTURE_H
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Shared test helpers for libakerror.
|
|
||||||
*
|
|
||||||
* Installs a capturing implementation of akerr_log_method so tests can assert
|
|
||||||
* on the *content* of log/stacktrace output (messages, status codes, error
|
|
||||||
* names) instead of relying solely on process exit codes.
|
|
||||||
*
|
|
||||||
* Also provides AKERR_CHECK(), a NDEBUG-proof assertion that fails the test by
|
|
||||||
* returning non-zero from main() (unlike assert(), which is compiled out in
|
|
||||||
* release builds and would silently turn a test into a no-op).
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "akerror.h"
|
|
||||||
#include <stdarg.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#define AKERR_CAPTURE_BUFSZ 65536
|
|
||||||
static char akerr_capture_buf[AKERR_CAPTURE_BUFSZ];
|
|
||||||
static size_t akerr_capture_len = 0;
|
|
||||||
|
|
||||||
static void __attribute__((unused)) akerr_capture_logger(const char *fmt, ...)
|
|
||||||
{
|
|
||||||
va_list ap;
|
|
||||||
va_start(ap, fmt);
|
|
||||||
int n = vsnprintf(akerr_capture_buf + akerr_capture_len,
|
|
||||||
AKERR_CAPTURE_BUFSZ - akerr_capture_len, fmt, ap);
|
|
||||||
va_end(ap);
|
|
||||||
if ( n > 0 ) {
|
|
||||||
akerr_capture_len += (size_t)n;
|
|
||||||
if ( akerr_capture_len >= AKERR_CAPTURE_BUFSZ ) {
|
|
||||||
akerr_capture_len = AKERR_CAPTURE_BUFSZ - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void __attribute__((unused)) akerr_capture_reset(void)
|
|
||||||
{
|
|
||||||
akerr_capture_len = 0;
|
|
||||||
akerr_capture_buf[0] = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Install the capturing logger. akerr_init() only assigns a default logger when
|
|
||||||
* akerr_log_method is NULL, and it is idempotent, so calling this either before
|
|
||||||
* or after the first PREPARE_ERROR keeps our logger in place.
|
|
||||||
*/
|
|
||||||
static void __attribute__((unused)) akerr_capture_install(void)
|
|
||||||
{
|
|
||||||
akerr_capture_reset();
|
|
||||||
akerr_log_method = &akerr_capture_logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Count array slots currently checked out of the pool (refcount != 0). */
|
|
||||||
static int __attribute__((unused)) akerr_slots_in_use(void)
|
|
||||||
{
|
|
||||||
int n = 0;
|
|
||||||
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
|
||||||
if ( AKERR_ARRAY_ERROR[i].refcount != 0 ) {
|
|
||||||
n++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
#define AKERR_CHECK(cond) \
|
|
||||||
do { \
|
|
||||||
if ( !(cond) ) { \
|
|
||||||
fprintf(stderr, "CHECK FAILED: %s at %s:%d\n", \
|
|
||||||
#cond, __FILE__, __LINE__); \
|
|
||||||
return 1; \
|
|
||||||
} \
|
|
||||||
} while ( 0 )
|
|
||||||
|
|
||||||
#define AKERR_CHECK_STATUS(errctx, expected_status) \
|
|
||||||
do { \
|
|
||||||
AKERR_CHECK((errctx) != NULL); \
|
|
||||||
AKERR_CHECK((errctx)->status == (expected_status)); \
|
|
||||||
} while ( 0 )
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Helpers for functions that report failure by returning akerr_ErrorContext *.
|
|
||||||
* Both release the context they consume, so a test that makes thousands of
|
|
||||||
* failing calls cannot exhaust the pool. AKERR_CHECK_RAISES keeps a copy of the
|
|
||||||
* message for AKERR_CHECK_MESSAGE_CONTAINS, since the context is gone by then.
|
|
||||||
*/
|
|
||||||
static char __attribute__((unused)) akerr_last_message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH];
|
|
||||||
|
|
||||||
#define AKERR_CHECK_SUCCEEDS(expr) \
|
|
||||||
do { \
|
|
||||||
akerr_ErrorContext *__akerr_result = (expr); \
|
|
||||||
if ( __akerr_result != NULL ) { \
|
|
||||||
fprintf(stderr, "UNEXPECTED ERROR from %s: %d (%s): %s" \
|
|
||||||
" at %s:%d\n", #expr, __akerr_result->status, \
|
|
||||||
akerr_name_for_status(__akerr_result->status, NULL), \
|
|
||||||
__akerr_result->message, __FILE__, __LINE__); \
|
|
||||||
RELEASE_ERROR(__akerr_result); \
|
|
||||||
return 1; \
|
|
||||||
} \
|
|
||||||
} while ( 0 )
|
|
||||||
|
|
||||||
#define AKERR_CHECK_RAISES(expr, expected_status) \
|
|
||||||
do { \
|
|
||||||
akerr_ErrorContext *__akerr_result = (expr); \
|
|
||||||
AKERR_CHECK(__akerr_result != NULL); \
|
|
||||||
snprintf(akerr_last_message, sizeof(akerr_last_message), "%s", \
|
|
||||||
__akerr_result->message); \
|
|
||||||
if ( __akerr_result->status != (expected_status) ) { \
|
|
||||||
fprintf(stderr, "WRONG STATUS from %s: got %d, want %s" \
|
|
||||||
" at %s:%d\n", #expr, __akerr_result->status, \
|
|
||||||
#expected_status, __FILE__, __LINE__); \
|
|
||||||
RELEASE_ERROR(__akerr_result); \
|
|
||||||
return 1; \
|
|
||||||
} \
|
|
||||||
RELEASE_ERROR(__akerr_result); \
|
|
||||||
AKERR_CHECK(__akerr_result == NULL); \
|
|
||||||
} while ( 0 )
|
|
||||||
|
|
||||||
#define AKERR_CHECK_MESSAGE_CONTAINS(needle) \
|
|
||||||
AKERR_CHECK(strstr(akerr_last_message, (needle)) != NULL)
|
|
||||||
|
|
||||||
#define AKERR_CHECK_CONTAINS(needle) \
|
|
||||||
AKERR_CHECK(strstr(akerr_capture_buf, (needle)) != NULL)
|
|
||||||
|
|
||||||
#define AKERR_CHECK_NOT_CONTAINS(needle) \
|
|
||||||
AKERR_CHECK(strstr(akerr_capture_buf, (needle)) == NULL)
|
|
||||||
|
|
||||||
#endif // AKERR_TEST_CAPTURE_H
|
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
#include "akerror.h"
|
#include "sdlerror.h"
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
akerr_ErrorContext *func2(void)
|
ErrorContext *func2(void)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
FAIL(errctx, AKERR_NULLPOINTER, "This is a failure in func2");
|
FAIL(errctx, ERR_NULLPOINTER, "This is a failure in func2");
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} FINISH(errctx, true);
|
} FINISH(errctx, true);
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
akerr_ErrorContext *func1(void)
|
ErrorContext *func1(void)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
@@ -31,12 +30,7 @@ int main(void)
|
|||||||
CATCH(errctx, func1());
|
CATCH(errctx, func1());
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
} HANDLE(errctx, ERR_NULLPOINTER) {
|
||||||
AKERR_CHECK_STATUS(errctx, AKERR_NULLPOINTER);
|
SDL_Log("Caught exception");
|
||||||
akerr_log_method("Caught exception");
|
|
||||||
} FINISH_NORETURN(errctx);
|
} FINISH_NORETURN(errctx);
|
||||||
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
fprintf(stderr, "err_catch ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
#include "akerror.h"
|
#include "sdlerror.h"
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
int x;
|
int x;
|
||||||
|
|
||||||
akerr_ErrorContext *func2(void)
|
ErrorContext *func2(void)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
FAIL(errctx, AKERR_NULLPOINTER, "This is a failure in func2");
|
FAIL(errctx, ERR_NULLPOINTER, "This is a failure in func2");
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} FINISH(errctx, true);
|
} FINISH(errctx, true);
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
akerr_ErrorContext *func1(void)
|
ErrorContext *func1(void)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
@@ -34,16 +33,11 @@ int main(void)
|
|||||||
CATCH(errctx, func1());
|
CATCH(errctx, func1());
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
} HANDLE(errctx, ERR_NULLPOINTER) {
|
||||||
AKERR_CHECK_STATUS(errctx, AKERR_NULLPOINTER);
|
|
||||||
if ( x == 0 ) {
|
if ( x == 0 ) {
|
||||||
akerr_log_method("Cleanup works\n");
|
fprintf(stderr, "Cleanup works\n");
|
||||||
} else {
|
return 0;
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
return 1;
|
||||||
} FINISH_NORETURN(errctx);
|
} FINISH_NORETURN(errctx);
|
||||||
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
fprintf(stderr, "err_cleanup ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
/*
|
|
||||||
* __akerr_copy_string() is the only place in the library that writes through a
|
|
||||||
* caller-supplied pointer for a caller-supplied length, so it validates every
|
|
||||||
* argument and raises rather than returning quietly. Both in-library callers
|
|
||||||
* check their arguments before calling it, so this test is what drives those
|
|
||||||
* guards -- without it they are unreachable code that no build ever exercises.
|
|
||||||
*/
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
char buf[8];
|
|
||||||
|
|
||||||
akerr_capture_install();
|
|
||||||
akerr_init();
|
|
||||||
|
|
||||||
/* The happy path: bounded, and always terminated. */
|
|
||||||
memset(buf, 'x', sizeof(buf));
|
|
||||||
AKERR_CHECK_SUCCEEDS(__akerr_copy_string(buf, (int)sizeof(buf), "abc"));
|
|
||||||
AKERR_CHECK(strcmp(buf, "abc") == 0);
|
|
||||||
|
|
||||||
/* A source longer than the buffer is truncated, never overrun. */
|
|
||||||
memset(buf, 'x', sizeof(buf));
|
|
||||||
AKERR_CHECK_SUCCEEDS(__akerr_copy_string(buf, (int)sizeof(buf), "abcdefghijkl"));
|
|
||||||
AKERR_CHECK(strlen(buf) == sizeof(buf) - 1);
|
|
||||||
AKERR_CHECK(buf[sizeof(buf) - 1] == '\0');
|
|
||||||
AKERR_CHECK(strcmp(buf, "abcdefg") == 0);
|
|
||||||
|
|
||||||
/* A capacity of exactly one holds nothing but the terminator. */
|
|
||||||
memset(buf, 'x', sizeof(buf));
|
|
||||||
AKERR_CHECK_SUCCEEDS(__akerr_copy_string(buf, 1, "abc"));
|
|
||||||
AKERR_CHECK(buf[0] == '\0');
|
|
||||||
AKERR_CHECK(buf[1] == 'x'); /* and wrote nothing past its capacity */
|
|
||||||
|
|
||||||
/* NULL pointers raise instead of faulting, and the message says which. */
|
|
||||||
AKERR_CHECK_RAISES(__akerr_copy_string(NULL, (int)sizeof(buf), "abc"),
|
|
||||||
AKERR_NULLPOINTER);
|
|
||||||
AKERR_CHECK_MESSAGE_CONTAINS("destination");
|
|
||||||
AKERR_CHECK_RAISES(__akerr_copy_string(buf, (int)sizeof(buf), NULL),
|
|
||||||
AKERR_NULLPOINTER);
|
|
||||||
AKERR_CHECK_MESSAGE_CONTAINS("source");
|
|
||||||
|
|
||||||
/* A capacity with no room for a terminator is a value error, not a write. */
|
|
||||||
memset(buf, 'x', sizeof(buf));
|
|
||||||
AKERR_CHECK_RAISES(__akerr_copy_string(buf, 0, "abc"), AKERR_VALUE);
|
|
||||||
AKERR_CHECK_MESSAGE_CONTAINS("capacity of 0");
|
|
||||||
AKERR_CHECK_RAISES(__akerr_copy_string(buf, -1, "abc"), AKERR_VALUE);
|
|
||||||
AKERR_CHECK(buf[0] == 'x'); /* nothing was written */
|
|
||||||
|
|
||||||
/* Each refusal handed its context back to the pool. */
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
|
|
||||||
fprintf(stderr, "err_copy_string ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The unhandled-error hook (akerr_handler_unhandled_error) is overridable.
|
|
||||||
* Install a non-fatal handler so an unhandled error can be asserted directly
|
|
||||||
* (status, invocation) instead of relying on process death / WILL_FAIL.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int custom_fired = 0;
|
|
||||||
static int custom_status = 0;
|
|
||||||
|
|
||||||
static void my_handler(akerr_ErrorContext *e)
|
|
||||||
{
|
|
||||||
custom_fired = 1;
|
|
||||||
custom_status = (e != NULL) ? e->status : -1;
|
|
||||||
/* deliberately does NOT exit() */
|
|
||||||
}
|
|
||||||
|
|
||||||
akerr_ErrorContext *boom(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
FAIL_RETURN(e, AKERR_TYPE, "unhandled on purpose");
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
akerr_init(); /* sets the default handler... */
|
|
||||||
akerr_handler_unhandled_error = &my_handler; /* ...which we then override */
|
|
||||||
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(e, boom());
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
/* no HANDLE for AKERR_TYPE -> stays unhandled */
|
|
||||||
} FINISH_NORETURN(e);
|
|
||||||
|
|
||||||
AKERR_CHECK(custom_fired == 1);
|
|
||||||
AKERR_CHECK(custom_status == AKERR_TYPE);
|
|
||||||
AKERR_CHECK_CONTAINS("Unhandled Error");
|
|
||||||
fprintf(stderr, "err_custom_handler ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
#include <errno.h>
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The library imports system errno codes and their descriptions at build time
|
|
||||||
* (scripts/generrno.sh -> akerr_init_errno). Verify:
|
|
||||||
* - a system errno (EACCES) has a registered, non-empty name;
|
|
||||||
* - an unregistered status returns the "Unknown Error" sentinel;
|
|
||||||
* - a system errno can be raised, propagated and handled like any AKERR_* code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int handled = 0;
|
|
||||||
|
|
||||||
akerr_ErrorContext *boom(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
FAIL_RETURN(e, EACCES, "permission denied");
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
akerr_init();
|
|
||||||
|
|
||||||
char *nm = akerr_name_for_status(EACCES, NULL);
|
|
||||||
AKERR_CHECK(nm != NULL);
|
|
||||||
AKERR_CHECK(nm[0] != '\0');
|
|
||||||
AKERR_CHECK(strcmp(nm, "Unknown Error") != 0);
|
|
||||||
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(1000000, NULL),
|
|
||||||
"Unknown Error") == 0);
|
|
||||||
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(e, boom());
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} HANDLE(e, EACCES) {
|
|
||||||
AKERR_CHECK_STATUS(e, EACCES);
|
|
||||||
handled = 1;
|
|
||||||
} FINISH_NORETURN(e);
|
|
||||||
|
|
||||||
AKERR_CHECK(handled == 1);
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
fprintf(stderr, "err_errno ok (EACCES name: \"%s\")\n", nm);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
/*
|
|
||||||
* akerr_init() registers a human-readable name for each library error code.
|
|
||||||
* Verify the names are actually installed (mutation testing showed the
|
|
||||||
* registration calls could be deleted without any test noticing).
|
|
||||||
*
|
|
||||||
* This list must stay exhaustive. AKERR_EOF, AKERR_ITERATOR_BREAK and
|
|
||||||
* AKERR_NOT_IMPLEMENTED were previously valid codes with no registered name, so
|
|
||||||
* they rendered as "Unknown Error" in every stack trace that carried them --
|
|
||||||
* the same class of silent gap that a too-small AKERR_MAX_ERR_VALUE used to
|
|
||||||
* cause. The sweep below walks the whole AKERR_* offset span so a newly added
|
|
||||||
* code without a name fails here rather than showing up in production traces.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static const struct {
|
|
||||||
int code;
|
|
||||||
const char *name;
|
|
||||||
} expected[] = {
|
|
||||||
{ AKERR_NULLPOINTER, "Null Pointer Error" },
|
|
||||||
{ AKERR_OUTOFBOUNDS, "Out Of Bounds Error" },
|
|
||||||
{ AKERR_API, "API Error" },
|
|
||||||
{ AKERR_ATTRIBUTE, "Attribute Error" },
|
|
||||||
{ AKERR_TYPE, "Type Error" },
|
|
||||||
{ AKERR_KEY, "Key Error" },
|
|
||||||
{ AKERR_INDEX, "Index Error" },
|
|
||||||
{ AKERR_FORMAT, "Format Error" },
|
|
||||||
{ AKERR_IO, "Input Output Error" },
|
|
||||||
{ AKERR_VALUE, "Value Error" },
|
|
||||||
{ AKERR_RELATIONSHIP, "Relationship Error" },
|
|
||||||
{ AKERR_EOF, "End Of File" },
|
|
||||||
{ AKERR_CIRCULAR_REFERENCE, "Circular Reference Error" },
|
|
||||||
{ AKERR_ITERATOR_BREAK, "Iterator Break" },
|
|
||||||
{ AKERR_NOT_IMPLEMENTED, "Not Implemented" },
|
|
||||||
{ AKERR_BADEXC, "Invalid akerr_ErrorContext" },
|
|
||||||
{ AKERR_STATUS_RANGE_OVERLAP, "Status Range Overlap" },
|
|
||||||
{ AKERR_STATUS_RANGE_FULL, "Status Range Table Full" },
|
|
||||||
{ AKERR_STATUS_RANGE_INVALID, "Invalid Status Range" },
|
|
||||||
{ AKERR_STATUS_NAME_UNRESERVED, "Unreserved Status Name" },
|
|
||||||
{ AKERR_STATUS_NAME_FOREIGN, "Foreign Status Name" },
|
|
||||||
{ AKERR_STATUS_NAME_FULL, "Status Name Registry Full" },
|
|
||||||
{ AKERR_STATUS_NAME_INVALID, "Invalid Status Name" },
|
|
||||||
};
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_init();
|
|
||||||
|
|
||||||
for ( unsigned i = 0; i < sizeof(expected) / sizeof(expected[0]); i++ ) {
|
|
||||||
char *nm = akerr_name_for_status(expected[i].code, NULL);
|
|
||||||
AKERR_CHECK(nm != NULL);
|
|
||||||
AKERR_CHECK(strcmp(nm, expected[i].name) == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Every value in the library's own offset span must resolve to a real name.
|
|
||||||
* AKERR_LAST_ERRNO_VALUE + 7 is the one deliberate hole (a removed code);
|
|
||||||
* anything else nameless is a code someone added without registering it.
|
|
||||||
*/
|
|
||||||
for ( int offset = 1;
|
|
||||||
offset <= AKERR_LAST_LIBRARY_STATUS - AKERR_LAST_ERRNO_VALUE;
|
|
||||||
offset++ ) {
|
|
||||||
int code = AKERR_LAST_ERRNO_VALUE + offset;
|
|
||||||
char *nm = akerr_name_for_status(code, NULL);
|
|
||||||
if ( offset == 7 ) {
|
|
||||||
AKERR_CHECK(strcmp(nm, "Unknown Error") == 0);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ( strcmp(nm, "Unknown Error") == 0 || nm[0] == '\0' ) {
|
|
||||||
fprintf(stderr, "AKERR_LAST_ERRNO_VALUE + %d (%d) has no name\n",
|
|
||||||
offset, code);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Every AKERR_* code must sit inside the band the library reserves. */
|
|
||||||
AKERR_CHECK(AKERR_LAST_LIBRARY_STATUS < AKERR_FIRST_CONSUMER_STATUS);
|
|
||||||
|
|
||||||
fprintf(stderr, "err_error_names ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
/*
|
|
||||||
* FAIL records the source file and function names with snprintf. Those names
|
|
||||||
* must be passed as %s ARGUMENTS, not used as the format string -- otherwise a
|
|
||||||
* path containing a printf conversion (say a build directory with a '%') is
|
|
||||||
* interpreted as a format and reads nonexistent varargs (undefined behavior).
|
|
||||||
*
|
|
||||||
* #line lets us make __FILE__ contain a conversion specifier; the stored name
|
|
||||||
* must come back verbatim.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#line 1 "pct%dname.c"
|
|
||||||
akerr_ErrorContext *raise_with_percent_in_filename(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
FAIL_RETURN(e, AKERR_VALUE, "boom");
|
|
||||||
}
|
|
||||||
#line 22 "tests/err_format_string.c"
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_init();
|
|
||||||
|
|
||||||
akerr_ErrorContext *e = raise_with_percent_in_filename();
|
|
||||||
AKERR_CHECK(e != NULL);
|
|
||||||
AKERR_CHECK(strcmp(e->fname, "pct%dname.c") == 0);
|
|
||||||
|
|
||||||
e = akerr_release_error(e);
|
|
||||||
fprintf(stderr, "err_format_string ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* An error whose status has no matching HANDLE block must fall through to
|
|
||||||
* HANDLE_DEFAULT, and the non-matching HANDLE body must not run.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int specific_fired = 0;
|
|
||||||
static int default_fired = 0;
|
|
||||||
static int default_status = 0;
|
|
||||||
|
|
||||||
akerr_ErrorContext *boom(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
FAIL_RETURN(e, AKERR_TYPE, "wrong type");
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(e, boom());
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} HANDLE(e, AKERR_KEY) {
|
|
||||||
specific_fired = 1; /* must NOT run: error is AKERR_TYPE */
|
|
||||||
} HANDLE_DEFAULT(e) {
|
|
||||||
default_fired = 1;
|
|
||||||
default_status = e->status;
|
|
||||||
} FINISH_NORETURN(e);
|
|
||||||
|
|
||||||
AKERR_CHECK(specific_fired == 0);
|
|
||||||
AKERR_CHECK(default_fired == 1);
|
|
||||||
AKERR_CHECK(default_status == AKERR_TYPE);
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
fprintf(stderr, "err_handle_default ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* With several distinct HANDLE blocks, only the one matching the raised status
|
|
||||||
* may run. Raise the middle code and confirm exact dispatch.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int a_fired = 0;
|
|
||||||
static int b_fired = 0;
|
|
||||||
static int c_fired = 0;
|
|
||||||
static int b_status = 0;
|
|
||||||
|
|
||||||
akerr_ErrorContext *boom(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
FAIL_RETURN(e, AKERR_TYPE, "the middle one");
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(e, boom());
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} HANDLE(e, AKERR_KEY) {
|
|
||||||
a_fired = 1;
|
|
||||||
} HANDLE(e, AKERR_TYPE) {
|
|
||||||
b_fired = 1;
|
|
||||||
b_status = e->status;
|
|
||||||
} HANDLE(e, AKERR_IO) {
|
|
||||||
c_fired = 1;
|
|
||||||
} FINISH_NORETURN(e);
|
|
||||||
|
|
||||||
AKERR_CHECK(a_fired == 0);
|
|
||||||
AKERR_CHECK(b_fired == 1);
|
|
||||||
AKERR_CHECK(b_status == AKERR_TYPE);
|
|
||||||
AKERR_CHECK(c_fired == 0);
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
fprintf(stderr, "err_handle_dispatch ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* HANDLE_GROUP lets several status codes share one handler body via
|
|
||||||
* case-fallthrough. The first member of the group uses HANDLE (which emits the
|
|
||||||
* leading break that terminates the previous case); each additional member
|
|
||||||
* uses HANDLE_GROUP; the shared body follows the last member. Verify that two
|
|
||||||
* different status codes both reach the shared body.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int group_fired = 0;
|
|
||||||
static int other_fired = 0;
|
|
||||||
static int group_status = 0;
|
|
||||||
static int other_status = 0;
|
|
||||||
|
|
||||||
akerr_ErrorContext *boom(int status)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
FAIL_RETURN(e, status, "boom %d", status);
|
|
||||||
}
|
|
||||||
|
|
||||||
akerr_ErrorContext *run(int status)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(e, boom(status));
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} HANDLE(e, AKERR_KEY)
|
|
||||||
HANDLE_GROUP(e, AKERR_INDEX) {
|
|
||||||
group_fired++;
|
|
||||||
group_status = e->status;
|
|
||||||
} HANDLE(e, AKERR_IO) {
|
|
||||||
other_fired++;
|
|
||||||
other_status = e->status;
|
|
||||||
} FINISH(e, false);
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
|
|
||||||
run(AKERR_KEY);
|
|
||||||
AKERR_CHECK(group_fired == 1);
|
|
||||||
AKERR_CHECK(group_status == AKERR_KEY);
|
|
||||||
AKERR_CHECK(other_fired == 0);
|
|
||||||
|
|
||||||
run(AKERR_INDEX);
|
|
||||||
AKERR_CHECK(group_fired == 2);
|
|
||||||
AKERR_CHECK(group_status == AKERR_INDEX);
|
|
||||||
AKERR_CHECK(other_fired == 0);
|
|
||||||
|
|
||||||
run(AKERR_IO);
|
|
||||||
AKERR_CHECK(group_fired == 2);
|
|
||||||
AKERR_CHECK(other_fired == 1);
|
|
||||||
AKERR_CHECK(other_status == AKERR_IO);
|
|
||||||
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
fprintf(stderr, "err_handle_group ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* IGNORE deliberately swallows an error: it records the context in
|
|
||||||
* __akerr_last_ignored, logs it with an "IGNORED ERROR" marker, and lets
|
|
||||||
* execution continue.
|
|
||||||
*/
|
|
||||||
|
|
||||||
akerr_ErrorContext *boom(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
FAIL_RETURN(e, AKERR_VALUE, "this error is ignored on purpose");
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
|
|
||||||
int reached_after_ignore = 0;
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
(void)e;
|
|
||||||
|
|
||||||
IGNORE(boom());
|
|
||||||
reached_after_ignore = 1;
|
|
||||||
|
|
||||||
AKERR_CHECK(__akerr_last_ignored != NULL);
|
|
||||||
AKERR_CHECK(__akerr_last_ignored->status == AKERR_VALUE);
|
|
||||||
AKERR_CHECK(reached_after_ignore == 1);
|
|
||||||
AKERR_CHECK_CONTAINS("IGNORED ERROR");
|
|
||||||
AKERR_CHECK_CONTAINS("this error is ignored on purpose");
|
|
||||||
fprintf(stderr, "err_ignore ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include <stdio.h>
|
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *improper_closure(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
ATTEMPT {
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} FINISH(errctx, true);
|
|
||||||
fprintf(stderr, "Improperly returning from improper_closure\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(errctx, improper_closure());
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} FINISH_NORETURN(errctx);
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The library naming one of its own codes is not allowed to fail quietly.
|
|
||||||
* __akerr_name_library_status() runs from akerr_init() and from the generated
|
|
||||||
* errno table, neither of which has a caller to raise into, so a refusal there
|
|
||||||
* goes through FINISH_NORETURN: stack trace, then akerr_handler_unhandled_error,
|
|
||||||
* which terminates the process.
|
|
||||||
*
|
|
||||||
* In a correct build that can only happen with a name table too small to hold
|
|
||||||
* the library's own entries, which no test can configure (the slot count is
|
|
||||||
* PRIVATE to the library target). Calling the helper for a status the library
|
|
||||||
* does not own reaches the same refusal, so this test covers the terminal path
|
|
||||||
* itself.
|
|
||||||
*
|
|
||||||
* Registered in AKERR_WILL_FAIL_TESTS: reaching the end of main() means the
|
|
||||||
* failure was swallowed, and that is the bug this test exists to catch.
|
|
||||||
*/
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_init();
|
|
||||||
|
|
||||||
/* Nobody has reserved 9999, so this registration is refused. */
|
|
||||||
__akerr_name_library_status(9999, "Not The Library's To Name");
|
|
||||||
|
|
||||||
fprintf(stderr, "err_library_status_fatal: a refused library-status "
|
|
||||||
"registration did NOT terminate\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
#include <limits.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Status magnitude is no longer coupled to a public array bound: any int is a
|
|
||||||
* legal status, and storage is a private sparse registry. What bounds the
|
|
||||||
* registry now is its *capacity*, not the value of the largest code.
|
|
||||||
*
|
|
||||||
* Covers: arbitrary int status values, name truncation, range reservation
|
|
||||||
* semantics (overlap, idempotency, endpoints, validation, overflow), and both
|
|
||||||
* capacity limits -- the range table and the name table -- each of which must
|
|
||||||
* raise rather than dropping the registration quietly.
|
|
||||||
*
|
|
||||||
* Every refusal is an error context the caller owns, so each check below also
|
|
||||||
* asserts the context returns to the pool; a leak here would exhaust the
|
|
||||||
* 128-slot pool long before these loops finish.
|
|
||||||
*/
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
akerr_init();
|
|
||||||
|
|
||||||
/* Any int is a legal status, at either extreme of the range. */
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(INT_MIN, 1, "min-owner"));
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(INT_MAX, 1, "max-owner"));
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("max-owner", INT_MAX, "Maximum Status"));
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("min-owner", INT_MIN, "Minimum Status"));
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MAX, NULL), "Maximum Status") == 0);
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MIN, NULL), "Minimum Status") == 0);
|
|
||||||
|
|
||||||
/* A name longer than the buffer is truncated and always terminated. */
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(1000000, 1, "trunc"));
|
|
||||||
const char *long_name =
|
|
||||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-extra";
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("trunc", 1000000, long_name));
|
|
||||||
char *stored = akerr_name_for_status(1000000, NULL);
|
|
||||||
AKERR_CHECK(strlen(stored) == AKERR_MAX_ERROR_NAME_LENGTH - 1);
|
|
||||||
AKERR_CHECK(stored[AKERR_MAX_ERROR_NAME_LENGTH - 1] == '\0');
|
|
||||||
|
|
||||||
/* Reservation: overlap detection, and idempotency for an exact repeat. */
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "component-a"));
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "component-a"));
|
|
||||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(260, 2, "component-b"),
|
|
||||||
AKERR_STATUS_RANGE_OVERLAP);
|
|
||||||
AKERR_CHECK_MESSAGE_CONTAINS("component-a");
|
|
||||||
|
|
||||||
/* The library's own 0..255 band is reserved and cannot be encroached on. */
|
|
||||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(255, 1, "component-b"),
|
|
||||||
AKERR_STATUS_RANGE_OVERLAP);
|
|
||||||
AKERR_CHECK_MESSAGE_CONTAINS(AKERR_LIBRARY_OWNER);
|
|
||||||
/* An exact repeat by the owner is still a no-op. */
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
|
||||||
AKERR_LIBRARY_OWNER));
|
|
||||||
|
|
||||||
/* Argument validation. */
|
|
||||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(INT_MAX, 2, "overflow"),
|
|
||||||
AKERR_STATUS_RANGE_INVALID);
|
|
||||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(300, 0, "empty"),
|
|
||||||
AKERR_STATUS_RANGE_INVALID);
|
|
||||||
AKERR_CHECK_MESSAGE_CONTAINS("empty"); /* the message names the caller */
|
|
||||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(300, -1, "negative"),
|
|
||||||
AKERR_STATUS_RANGE_INVALID);
|
|
||||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(300, 1, NULL),
|
|
||||||
AKERR_STATUS_RANGE_INVALID);
|
|
||||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(300, 1, ""),
|
|
||||||
AKERR_STATUS_RANGE_INVALID);
|
|
||||||
|
|
||||||
/* Owner strings: 63 chars fit, 64 do not, and neither does anything past. */
|
|
||||||
char owner63[AKERR_MAX_ERROR_NAME_LENGTH];
|
|
||||||
char owner64[AKERR_MAX_ERROR_NAME_LENGTH + 1];
|
|
||||||
char owner70[AKERR_MAX_ERROR_NAME_LENGTH + 7];
|
|
||||||
memset(owner63, 'a', sizeof(owner63) - 1);
|
|
||||||
owner63[sizeof(owner63) - 1] = '\0';
|
|
||||||
memset(owner64, 'b', sizeof(owner64) - 1);
|
|
||||||
owner64[sizeof(owner64) - 1] = '\0';
|
|
||||||
memset(owner70, 'c', sizeof(owner70) - 1);
|
|
||||||
owner70[sizeof(owner70) - 1] = '\0';
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(400, 1, owner63));
|
|
||||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(401, 1, owner64),
|
|
||||||
AKERR_STATUS_RANGE_INVALID);
|
|
||||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(402, 1, owner70),
|
|
||||||
AKERR_STATUS_RANGE_INVALID);
|
|
||||||
|
|
||||||
/* Partial overlaps at either endpoint, and a same-range different owner. */
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(500, 2, "endpoint"));
|
|
||||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(499, 2, "left"),
|
|
||||||
AKERR_STATUS_RANGE_OVERLAP);
|
|
||||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(500, 1, "endpoint"),
|
|
||||||
AKERR_STATUS_RANGE_OVERLAP); /* subset, not an exact repeat */
|
|
||||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(501, 1, "endpoint"),
|
|
||||||
AKERR_STATUS_RANGE_OVERLAP);
|
|
||||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(500, 2, "other"),
|
|
||||||
AKERR_STATUS_RANGE_OVERLAP); /* same range, wrong owner */
|
|
||||||
|
|
||||||
/* Claim room for the name-exhaustion sweep before filling the range table. */
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(2000000, 100000, "fill"));
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Range table capacity. The limit is private to src/error.c on purpose, so
|
|
||||||
* discover it by filling rather than by hardcoding it here.
|
|
||||||
*/
|
|
||||||
int ranges_added = 0;
|
|
||||||
akerr_ErrorContext *range_err = NULL;
|
|
||||||
for ( int i = 0; i < 100000; i++ ) {
|
|
||||||
range_err = akerr_reserve_status_range(1000 + (i * 2), 1, "pad");
|
|
||||||
if ( range_err != NULL ) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
ranges_added++;
|
|
||||||
}
|
|
||||||
AKERR_CHECK(ranges_added > 0);
|
|
||||||
AKERR_CHECK_STATUS(range_err, AKERR_STATUS_RANGE_FULL);
|
|
||||||
AKERR_CHECK(strstr(range_err->message, "range table is full") != NULL);
|
|
||||||
RELEASE_ERROR(range_err);
|
|
||||||
AKERR_CHECK(range_err == NULL);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Name table capacity. Exhaustion must be reported, not silent: a dropped
|
|
||||||
* name degrades every future stack trace for that code to "Unknown Error".
|
|
||||||
*/
|
|
||||||
int full_at = -1;
|
|
||||||
for ( int i = 0; i < 100000; i++ ) {
|
|
||||||
char name[32];
|
|
||||||
snprintf(name, sizeof(name), "Filled %d", i);
|
|
||||||
akerr_ErrorContext *name_err =
|
|
||||||
akerr_register_status_name("fill", 2000000 + i, name);
|
|
||||||
if ( name_err != NULL ) {
|
|
||||||
AKERR_CHECK_STATUS(name_err, AKERR_STATUS_NAME_FULL);
|
|
||||||
AKERR_CHECK(strstr(name_err->message, "registry is full") != NULL);
|
|
||||||
AKERR_CHECK(strstr(name_err->message, "AKERR_STATUS_NAME_SLOTS") != NULL);
|
|
||||||
RELEASE_ERROR(name_err);
|
|
||||||
AKERR_CHECK(name_err == NULL);
|
|
||||||
full_at = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The table must actually hold everything it accepted. A probe sequence
|
|
||||||
* that revisits slots instead of walking the table -- e.g. masking with
|
|
||||||
* SLOTS rather than SLOTS-1 -- both collapses the usable capacity and
|
|
||||||
* loses earlier entries, and each check below catches it independently.
|
|
||||||
* The floor assumes at least the default table size (4096 slots).
|
|
||||||
*/
|
|
||||||
AKERR_CHECK(full_at > 256);
|
|
||||||
for ( int i = 0; i < full_at; i++ ) {
|
|
||||||
char expected[32];
|
|
||||||
snprintf(expected, sizeof(expected), "Filled %d", i);
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(2000000 + i, NULL), expected) == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* A dropped name reads back as the sentinel, and earlier ones survive. */
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(2000000 + full_at, NULL),
|
|
||||||
"Unknown Error") == 0);
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MIN, NULL), "Minimum Status") == 0);
|
|
||||||
|
|
||||||
/* Thousands of refusals later, every context went back to the pool. */
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
|
|
||||||
fprintf(stderr, "err_maxval ok (%d consumer names before full)\n", full_at);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
/* Unregistered status values return the sentinel regardless of magnitude. */
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_init();
|
|
||||||
|
|
||||||
/* Below range. */
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(-1, NULL), "Unknown Error") == 0);
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(-9999, NULL), "Unknown Error") == 0);
|
|
||||||
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(1000000, NULL),
|
|
||||||
"Unknown Error") == 0);
|
|
||||||
|
|
||||||
/* A valid code must still resolve to its real name. */
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(AKERR_NULLPOINTER, NULL),
|
|
||||||
"Null Pointer Error") == 0);
|
|
||||||
|
|
||||||
fprintf(stderr, "err_name_bounds ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Reserving a range used to be pure bookkeeping: akerr_name_for_status() would
|
|
||||||
* name any status for any caller, so two components could still register names
|
|
||||||
* for the same code -- and HANDLE the same code -- with nothing detecting it.
|
|
||||||
* Reservation only caught components that both opted in AND declared ranges
|
|
||||||
* that happened to overlap.
|
|
||||||
*
|
|
||||||
* Naming a status is now permitted only inside a reservation:
|
|
||||||
* - akerr_register_status_name() requires the range to belong to the caller,
|
|
||||||
* and raises AKERR_STATUS_NAME_* when it does not;
|
|
||||||
* - the legacy two-argument akerr_name_for_status() set path cannot identify
|
|
||||||
* its caller, so it can only require that *some* reservation covers the
|
|
||||||
* status -- still enough to stop a code nobody claimed. It returns a name
|
|
||||||
* rather than an error context, so its refusals are logged instead.
|
|
||||||
* Either way the refusal is visible, because a name that fails to register
|
|
||||||
* degrades the status to "Unknown Error" in every later stack trace.
|
|
||||||
*/
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
akerr_init();
|
|
||||||
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "lib-a"));
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(512, 16, "lib-b"));
|
|
||||||
|
|
||||||
/* The owner of a range may name statuses inside it. */
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("lib-a", 256, "A Parse Error"));
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("lib-a", 271, "A Last Error"));
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "A Parse Error") == 0);
|
|
||||||
|
|
||||||
/* Naming another owner's status is refused and names the real owner. */
|
|
||||||
AKERR_CHECK_RAISES(akerr_register_status_name("lib-b", 256, "B Hijack"),
|
|
||||||
AKERR_STATUS_NAME_FOREIGN);
|
|
||||||
AKERR_CHECK_MESSAGE_CONTAINS("lib-a");
|
|
||||||
AKERR_CHECK_MESSAGE_CONTAINS("lib-b");
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "A Parse Error") == 0);
|
|
||||||
|
|
||||||
/* Including the library's own reserved band. */
|
|
||||||
AKERR_CHECK_RAISES(akerr_register_status_name("lib-b", AKERR_VALUE, "B Value"),
|
|
||||||
AKERR_STATUS_NAME_FOREIGN);
|
|
||||||
AKERR_CHECK_MESSAGE_CONTAINS(AKERR_LIBRARY_OWNER);
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(AKERR_VALUE, NULL), "Value Error") == 0);
|
|
||||||
|
|
||||||
/* A status nobody reserved cannot be named through either entry point. */
|
|
||||||
AKERR_CHECK_RAISES(akerr_register_status_name("lib-a", 9999, "Unclaimed"),
|
|
||||||
AKERR_STATUS_NAME_UNRESERVED);
|
|
||||||
AKERR_CHECK_MESSAGE_CONTAINS("no reserved range");
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(9999, NULL), "Unknown Error") == 0);
|
|
||||||
|
|
||||||
/* The legacy path has no caller to raise into, so it logs the refusal.
|
|
||||||
* It also cannot name the caller, and must say so rather than printing a
|
|
||||||
* stray owner. */
|
|
||||||
akerr_capture_reset();
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(9999, "Unclaimed Legacy"),
|
|
||||||
"Unknown Error") == 0);
|
|
||||||
AKERR_CHECK_CONTAINS("no reserved range");
|
|
||||||
AKERR_CHECK_CONTAINS("an unnamed caller");
|
|
||||||
AKERR_CHECK_CONTAINS("REFUSED STATUS NAME");
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(9999, NULL), "Unknown Error") == 0);
|
|
||||||
/* ... and hands the context it raised back to the pool. */
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
|
|
||||||
/* Boundaries: just outside lib-a's range is not lib-a's to name. */
|
|
||||||
AKERR_CHECK_RAISES(akerr_register_status_name("lib-a", 255, "Below"),
|
|
||||||
AKERR_STATUS_NAME_FOREIGN);
|
|
||||||
AKERR_CHECK_RAISES(akerr_register_status_name("lib-a", 272, "Above"),
|
|
||||||
AKERR_STATUS_NAME_UNRESERVED);
|
|
||||||
|
|
||||||
/* The legacy set path still works inside any reservation. */
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(513, "B Legacy"), "B Legacy") == 0);
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(513, NULL), "B Legacy") == 0);
|
|
||||||
|
|
||||||
/* Re-registering your own status overwrites the name. */
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("lib-a", 256, "A Renamed"));
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "A Renamed") == 0);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Argument validation. Each message must identify the caller it refused,
|
|
||||||
* since that message is the whole report a consumer gets.
|
|
||||||
*/
|
|
||||||
AKERR_CHECK_RAISES(akerr_register_status_name(NULL, 257, "No Owner"),
|
|
||||||
AKERR_STATUS_NAME_INVALID);
|
|
||||||
AKERR_CHECK_MESSAGE_CONTAINS("257");
|
|
||||||
AKERR_CHECK_RAISES(akerr_register_status_name("", 257, "Empty Owner"),
|
|
||||||
AKERR_STATUS_NAME_INVALID);
|
|
||||||
AKERR_CHECK_MESSAGE_CONTAINS("Empty Owner");
|
|
||||||
AKERR_CHECK_RAISES(akerr_register_status_name("lib-a", 257, NULL),
|
|
||||||
AKERR_STATUS_NAME_INVALID);
|
|
||||||
AKERR_CHECK_MESSAGE_CONTAINS("lib-a");
|
|
||||||
|
|
||||||
/* Over-long owner strings: 63 characters fit, 64 and beyond do not. */
|
|
||||||
char owner63[64];
|
|
||||||
char owner64[65];
|
|
||||||
char owner70[71];
|
|
||||||
memset(owner63, 'a', sizeof(owner63) - 1);
|
|
||||||
owner63[sizeof(owner63) - 1] = '\0';
|
|
||||||
memset(owner64, 'b', sizeof(owner64) - 1);
|
|
||||||
owner64[sizeof(owner64) - 1] = '\0';
|
|
||||||
memset(owner70, 'c', sizeof(owner70) - 1);
|
|
||||||
owner70[sizeof(owner70) - 1] = '\0';
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(600, 1, owner63));
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name(owner63, 600, "Long Owner"));
|
|
||||||
AKERR_CHECK_RAISES(akerr_register_status_name(owner64, 600, "Too Long"),
|
|
||||||
AKERR_STATUS_NAME_INVALID);
|
|
||||||
AKERR_CHECK_MESSAGE_CONTAINS("63");
|
|
||||||
AKERR_CHECK_RAISES(akerr_register_status_name(owner70, 600, "Far Too Long"),
|
|
||||||
AKERR_STATUS_NAME_INVALID);
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(600, NULL), "Long Owner") == 0);
|
|
||||||
|
|
||||||
/* A refused registration must not consume a slot or leave a partial entry. */
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(257, NULL), "Unknown Error") == 0);
|
|
||||||
|
|
||||||
/* Lookup is unaffected by ownership -- anyone may read any name. */
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(271, NULL), "A Last Error") == 0);
|
|
||||||
|
|
||||||
/* Every refusal above released its context. */
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
|
|
||||||
fprintf(stderr, "err_name_ownership ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* PASS bubbles an error up to the caller without a local ATTEMPT/PROCESS block:
|
|
||||||
* if the wrapped call fails, PASS returns the context from the current function.
|
|
||||||
* Verify the error reaches main and that the code after PASS is skipped on
|
|
||||||
* failure.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int reached_after_pass = 0;
|
|
||||||
|
|
||||||
akerr_ErrorContext *inner(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
FAIL_RETURN(e, AKERR_IO, "inner failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
akerr_ErrorContext *outer(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
PASS(e, inner());
|
|
||||||
reached_after_pass = 1; /* must NOT run: PASS returned already */
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
|
|
||||||
int handled = 0;
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(e, outer());
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} HANDLE(e, AKERR_IO) {
|
|
||||||
AKERR_CHECK_STATUS(e, AKERR_IO);
|
|
||||||
handled = 1;
|
|
||||||
} FINISH_NORETURN(e);
|
|
||||||
|
|
||||||
AKERR_CHECK(handled == 1);
|
|
||||||
AKERR_CHECK(reached_after_pass == 0);
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
fprintf(stderr, "err_pass ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The error pool is a fixed array of AKERR_MAX_ARRAY_ERROR slots. When every
|
|
||||||
* slot is checked out, akerr_next_error() must return NULL rather than run off
|
|
||||||
* the end of the array; and it must always hand back the lowest free slot.
|
|
||||||
* Mutation testing showed both the terminating "return NULL" and the scan
|
|
||||||
* bounds could be broken without any test noticing.
|
|
||||||
*/
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_init();
|
|
||||||
|
|
||||||
akerr_ErrorContext *slots[AKERR_MAX_ARRAY_ERROR];
|
|
||||||
|
|
||||||
/* Check out every slot. */
|
|
||||||
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
|
||||||
slots[i] = akerr_next_error();
|
|
||||||
AKERR_CHECK(slots[i] != NULL);
|
|
||||||
slots[i]->refcount = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pool is fully exhausted: the next request must fail cleanly. */
|
|
||||||
AKERR_CHECK(akerr_next_error() == NULL);
|
|
||||||
|
|
||||||
/* Free exactly the first slot; the scan must find and return it. */
|
|
||||||
slots[0]->refcount = 0;
|
|
||||||
AKERR_CHECK(akerr_next_error() == slots[0]);
|
|
||||||
|
|
||||||
/* Tidy up. */
|
|
||||||
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
|
||||||
slots[i]->refcount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fprintf(stderr, "err_pool_exhaust ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Pool-hygiene regression test. The error pool is a fixed 128-slot static
|
|
||||||
* array; a single leaked reference per error would silently exhaust it. Run a
|
|
||||||
* large number of full raise -> catch -> handle cycles and confirm every slot
|
|
||||||
* is returned to the pool (refcount 0) and the pool can still hand out
|
|
||||||
* contexts afterwards.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#define ITERATIONS 100000
|
|
||||||
|
|
||||||
static int handled_status = 0;
|
|
||||||
|
|
||||||
akerr_ErrorContext *boom(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
FAIL(e, AKERR_VALUE, "boom %d", 1);
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} FINISH(e, true);
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* One full raise -> catch -> handle cycle. Returns NULL (context released). */
|
|
||||||
akerr_ErrorContext *one_cycle(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(e, boom());
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} HANDLE(e, AKERR_VALUE) {
|
|
||||||
handled_status = e->status;
|
|
||||||
} FINISH(e, false);
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
akerr_init();
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
|
|
||||||
for ( int iter = 0; iter < ITERATIONS; iter++ ) {
|
|
||||||
handled_status = 0;
|
|
||||||
(void)one_cycle();
|
|
||||||
AKERR_CHECK(handled_status == AKERR_VALUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
|
|
||||||
akerr_ErrorContext *probe = akerr_next_error();
|
|
||||||
AKERR_CHECK(probe != NULL);
|
|
||||||
|
|
||||||
fprintf(stderr, "err_pool_refcount ok (%d cycles, 0 leaked)\n", ITERATIONS);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Regression test for the refcount leak: ENSURE_ERROR_READY must increment
|
|
||||||
* refcount only when it *acquires* a fresh context, not on every FAIL/SUCCEED.
|
|
||||||
* A function that calls FAIL more than once on the same context and then
|
|
||||||
* propagates used to arrive at the caller with refcount 2; the caller released
|
|
||||||
* once, leaking the slot. After enough leaks the pool is exhausted and the
|
|
||||||
* library exit(1)s.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int handled_status = 0;
|
|
||||||
|
|
||||||
akerr_ErrorContext *validate(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
FAIL(e, AKERR_VALUE, "condition 1 failed"); /* acquires the context */
|
|
||||||
FAIL(e, AKERR_KEY, "condition 2 failed"); /* must NOT re-acquire it */
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} FINISH(e, true); /* unhandled -> propagate */
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* One raise -> catch -> handle cycle; returns NULL (context released). */
|
|
||||||
akerr_ErrorContext *one_cycle(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(e, validate());
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} HANDLE(e, AKERR_KEY) {
|
|
||||||
handled_status = e->status;
|
|
||||||
} HANDLE(e, AKERR_VALUE) {
|
|
||||||
} FINISH(e, false);
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_init();
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
|
|
||||||
for ( int i = 0; i < 32; i++ ) {
|
|
||||||
handled_status = 0;
|
|
||||||
(void)one_cycle();
|
|
||||||
AKERR_CHECK(handled_status == AKERR_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
fprintf(stderr, "err_refcount_double_fail ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
/*
|
|
||||||
* A library may reserve its status range from its own init() before anything in
|
|
||||||
* the process has raised an error, i.e. before akerr_init() has run. That used
|
|
||||||
* to be silently destructive: akerr_init() clears the range and name tables, so
|
|
||||||
* whichever component first triggered it (via PREPARE_ERROR) wiped the earlier
|
|
||||||
* reservation, and the *next* component to claim the same range was told OK --
|
|
||||||
* producing exactly the undetected aliasing the registry exists to prevent.
|
|
||||||
*
|
|
||||||
* Every public registry entry point now calls akerr_init() itself, so the
|
|
||||||
* tables are only ever cleared before the first reservation, never after one.
|
|
||||||
*
|
|
||||||
* Note this test must not call akerr_init() or PREPARE_ERROR first -- the
|
|
||||||
* uninitialized entry is the whole point.
|
|
||||||
*/
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
|
|
||||||
/* Cold call: no akerr_init(), no PREPARE_ERROR anywhere yet. */
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "early-lib"));
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("early-lib", 256, "Early Error"));
|
|
||||||
|
|
||||||
/* Something else now uses the library for the first time. */
|
|
||||||
akerr_init();
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
(void)e;
|
|
||||||
|
|
||||||
/* The early reservation and its name must both have survived. */
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "Early Error") == 0);
|
|
||||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(256, 16, "late-lib"),
|
|
||||||
AKERR_STATUS_RANGE_OVERLAP);
|
|
||||||
AKERR_CHECK_MESSAGE_CONTAINS("early-lib");
|
|
||||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(260, 2, "late-lib"),
|
|
||||||
AKERR_STATUS_RANGE_OVERLAP);
|
|
||||||
|
|
||||||
/* The library's own initialization still happened exactly once. */
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(AKERR_NULLPOINTER, NULL),
|
|
||||||
"Null Pointer Error") == 0);
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
|
||||||
AKERR_LIBRARY_OWNER));
|
|
||||||
|
|
||||||
/* An identical repeat by the original owner is still idempotent. */
|
|
||||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "early-lib"));
|
|
||||||
|
|
||||||
/* Raising from a cold registry must not strand a pool slot either. */
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
|
|
||||||
fprintf(stderr, "err_registry_init_order ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Releasing an error context back to the pool must wipe it, so the next caller
|
|
||||||
* that checks it out never sees stale status/message/stacktrace from a previous
|
|
||||||
* error. Mutation testing showed the clearing memset in akerr_release_error
|
|
||||||
* could be deleted without any test noticing.
|
|
||||||
*/
|
|
||||||
|
|
||||||
akerr_ErrorContext *boom(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
FAIL_RETURN(e, AKERR_VALUE, "stale dirty message that must not survive");
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
akerr_init();
|
|
||||||
|
|
||||||
/* Raise and fully handle an error; FINISH_NORETURN releases it to the pool. */
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(e, boom());
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} HANDLE(e, AKERR_VALUE) {
|
|
||||||
AKERR_CHECK_STATUS(e, AKERR_VALUE);
|
|
||||||
} FINISH_NORETURN(e);
|
|
||||||
|
|
||||||
AKERR_CHECK(e == NULL);
|
|
||||||
|
|
||||||
/* The next context handed out is the slot we just released: it must be clean. */
|
|
||||||
akerr_ErrorContext *slot = akerr_next_error();
|
|
||||||
AKERR_CHECK(slot != NULL);
|
|
||||||
AKERR_CHECK(slot->status == 0);
|
|
||||||
AKERR_CHECK(slot->message[0] == '\0');
|
|
||||||
AKERR_CHECK(slot->stacktracebuf[0] == '\0');
|
|
||||||
AKERR_CHECK(strstr(slot->message, "stale dirty message") == NULL);
|
|
||||||
|
|
||||||
fprintf(stderr, "err_release_clears ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
/*
|
|
||||||
* akerr_release_error(NULL) is an API contract violation the library reports
|
|
||||||
* rather than crashes on: it raises AKERR_NULLPOINTER against the internal
|
|
||||||
* last-ditch context and hands that back, so the caller gets a describable
|
|
||||||
* error instead of a dereferenced NULL. Nothing exercised that path.
|
|
||||||
*
|
|
||||||
* The last-ditch context deliberately lives outside AKERR_ARRAY_ERROR so
|
|
||||||
* reporting this failure cannot consume a pool slot -- which is exactly what
|
|
||||||
* akerr_valid_error_address() reports on, and what this test checks.
|
|
||||||
*/
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
/* akerr_init() sets up the last-ditch context's stacktrace cursor. */
|
|
||||||
akerr_init();
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
|
|
||||||
akerr_ErrorContext *ret = akerr_release_error(NULL);
|
|
||||||
|
|
||||||
AKERR_CHECK(ret != NULL);
|
|
||||||
AKERR_CHECK(ret->status == AKERR_NULLPOINTER);
|
|
||||||
AKERR_CHECK(strstr(ret->message, "NULL context pointer") != NULL);
|
|
||||||
|
|
||||||
/* Reported through FAIL, so the stack trace names the error too. */
|
|
||||||
AKERR_CHECK(strstr(ret->stacktracebuf, "Null Pointer Error") != NULL);
|
|
||||||
|
|
||||||
/* Not a pool slot, and no slot was checked out to report the failure. */
|
|
||||||
AKERR_CHECK(akerr_valid_error_address(ret) == 0);
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
|
|
||||||
/* The last-ditch context is a singleton: the same one comes back. */
|
|
||||||
akerr_ErrorContext *again = akerr_release_error(NULL);
|
|
||||||
AKERR_CHECK(again == ret);
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
|
|
||||||
fprintf(stderr, "err_release_null ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
/*
|
|
||||||
* akerr_release_error() drops one reference and only recycles the slot when the
|
|
||||||
* last one goes away. Two of its refcount edges had no test:
|
|
||||||
*
|
|
||||||
* - refcount > 1: the release must decrement and return the context intact,
|
|
||||||
* not wipe it out from under the reference that is still held.
|
|
||||||
* - refcount == 0: releasing a context nobody holds must not underflow the
|
|
||||||
* count; it wipes and returns NULL like any other fully-released slot.
|
|
||||||
*
|
|
||||||
* The macro API never produces a refcount above 1 (ENSURE_ERROR_READY only
|
|
||||||
* increments when it acquires a fresh slot), so this test sets the count
|
|
||||||
* directly to model a caller that took an extra reference.
|
|
||||||
*/
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
akerr_init();
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
|
|
||||||
/* Check out a slot and give it two holders. */
|
|
||||||
akerr_ErrorContext *held = akerr_next_error();
|
|
||||||
AKERR_CHECK(held != NULL);
|
|
||||||
int slotid = held->arrayid;
|
|
||||||
held->refcount = 2;
|
|
||||||
held->status = AKERR_VALUE;
|
|
||||||
snprintf((char *)held->message, AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH,
|
|
||||||
"still referenced");
|
|
||||||
|
|
||||||
/* First release: one holder left, so the context survives untouched. */
|
|
||||||
akerr_ErrorContext *ret = akerr_release_error(held);
|
|
||||||
AKERR_CHECK(ret == held);
|
|
||||||
AKERR_CHECK(held->refcount == 1);
|
|
||||||
AKERR_CHECK(held->status == AKERR_VALUE);
|
|
||||||
AKERR_CHECK(strcmp(held->message, "still referenced") == 0);
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 1);
|
|
||||||
|
|
||||||
/* Second release: last holder gone, so the slot is wiped and recycled. */
|
|
||||||
ret = akerr_release_error(held);
|
|
||||||
AKERR_CHECK(ret == NULL);
|
|
||||||
AKERR_CHECK(held->refcount == 0);
|
|
||||||
AKERR_CHECK(held->status == 0);
|
|
||||||
AKERR_CHECK(held->message[0] == '\0');
|
|
||||||
/* The wipe must preserve the slot's identity and stacktrace cursor. */
|
|
||||||
AKERR_CHECK(held->arrayid == slotid);
|
|
||||||
AKERR_CHECK(held->stacktracebufptr == (char *)&held->stacktracebuf);
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Releasing an unheld slot: refcount is already 0, so there is nothing to
|
|
||||||
* decrement and the count must not go negative.
|
|
||||||
*/
|
|
||||||
akerr_ErrorContext *unheld = akerr_next_error();
|
|
||||||
AKERR_CHECK(unheld != NULL);
|
|
||||||
AKERR_CHECK(unheld->refcount == 0);
|
|
||||||
ret = akerr_release_error(unheld);
|
|
||||||
AKERR_CHECK(ret == NULL);
|
|
||||||
AKERR_CHECK(unheld->refcount == 0);
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
|
|
||||||
/* The pool is still healthy afterwards. */
|
|
||||||
akerr_ErrorContext *probe = akerr_next_error();
|
|
||||||
AKERR_CHECK(probe != NULL);
|
|
||||||
|
|
||||||
fprintf(stderr, "err_release_refcount ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Regression test for the stack-trace buffer overflow. Each frame appended a
|
|
||||||
* line with snprintf, but passed the *full* buffer length as the size rather
|
|
||||||
* than the space remaining, and advanced the cursor by snprintf's would-be
|
|
||||||
* return value. A trace that filled the buffer therefore wrote past the end of
|
|
||||||
* stacktracebuf and ran the cursor out of bounds.
|
|
||||||
*
|
|
||||||
* We place a context in a struct with a guard region right after it, position
|
|
||||||
* the trace cursor near the end of the buffer, append one more frame, and
|
|
||||||
* require that nothing was written past the buffer and the cursor stayed in
|
|
||||||
* bounds.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static struct {
|
|
||||||
akerr_ErrorContext ctx;
|
|
||||||
unsigned char guard[512];
|
|
||||||
} probe;
|
|
||||||
|
|
||||||
akerr_ErrorContext *append_frame(akerr_ErrorContext *e)
|
|
||||||
{
|
|
||||||
FAIL_RETURN(e, AKERR_VALUE,
|
|
||||||
"an error message long enough to overflow a nearly full stack trace buffer");
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_init();
|
|
||||||
|
|
||||||
memset(&probe, 0x00, sizeof(probe));
|
|
||||||
memset(probe.guard, 0xAA, sizeof(probe.guard));
|
|
||||||
|
|
||||||
akerr_ErrorContext *e = &probe.ctx;
|
|
||||||
e->refcount = 1;
|
|
||||||
/* Two bytes short of full: any real frame would overflow the old code. */
|
|
||||||
e->stacktracebufptr = probe.ctx.stacktracebuf
|
|
||||||
+ AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH - 2;
|
|
||||||
|
|
||||||
(void)append_frame(e);
|
|
||||||
|
|
||||||
/* Nothing may have been written past the end of stacktracebuf. */
|
|
||||||
for ( unsigned i = 0; i < sizeof(probe.guard); i++ ) {
|
|
||||||
AKERR_CHECK(probe.guard[i] == 0xAA);
|
|
||||||
}
|
|
||||||
/* The cursor must remain within the buffer. */
|
|
||||||
AKERR_CHECK(e->stacktracebufptr
|
|
||||||
<= probe.ctx.stacktracebuf + AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH);
|
|
||||||
|
|
||||||
fprintf(stderr, "err_stacktrace_bounds ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The registry entry points report failure the same way every other function in
|
|
||||||
* this library does: they return akerr_ErrorContext *. A refused reservation is
|
|
||||||
* therefore an ordinary error, and everything that works on an ordinary error
|
|
||||||
* must work on it -- CATCH, HANDLE, PASS, propagation to the caller, and the
|
|
||||||
* stack trace an unhandled one prints.
|
|
||||||
*
|
|
||||||
* This is what distinguishes the current design from the integer return codes
|
|
||||||
* it replaced: a consumer that ignores the result gets a compiler warning
|
|
||||||
* (AKERR_NOIGNORE), and a consumer that catches it but does not handle it gets
|
|
||||||
* the error propagated out of its init function rather than a silently
|
|
||||||
* unreserved range.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#define TEST_OWNER "exception-test"
|
|
||||||
|
|
||||||
/* A consumer init function in the shape the documentation recommends. */
|
|
||||||
static akerr_ErrorContext AKERR_NOIGNORE *component_init(int base, const char *owner)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(errctx, akerr_reserve_status_range(base, 4, owner));
|
|
||||||
CATCH(errctx, akerr_register_status_name(owner, base, "Component Error"));
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} FINISH(errctx, true);
|
|
||||||
SUCCEED_RETURN(errctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
akerr_init();
|
|
||||||
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
|
|
||||||
/* A successful reservation raises nothing at all. */
|
|
||||||
AKERR_CHECK_SUCCEEDS(component_init(256, TEST_OWNER));
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "Component Error") == 0);
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* A collision propagates out of the component's init and is caught here.
|
|
||||||
* HANDLE proves the status is a real, distinct exception a consumer can
|
|
||||||
* dispatch on -- not an opaque nonzero int.
|
|
||||||
*/
|
|
||||||
int handled_overlap = 0;
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(errctx, component_init(258, "other-component"));
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} HANDLE(errctx, AKERR_STATUS_RANGE_OVERLAP) {
|
|
||||||
handled_overlap = 1;
|
|
||||||
} HANDLE_DEFAULT(errctx) {
|
|
||||||
handled_overlap = -1;
|
|
||||||
} FINISH_NORETURN(errctx);
|
|
||||||
AKERR_CHECK(handled_overlap == 1);
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
|
|
||||||
/* The name-side refusals dispatch the same way. */
|
|
||||||
int handled_foreign = 0;
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(errctx, akerr_register_status_name("interloper", 256, "Hijack"));
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} HANDLE(errctx, AKERR_STATUS_NAME_FOREIGN) {
|
|
||||||
handled_foreign = 1;
|
|
||||||
} FINISH_NORETURN(errctx);
|
|
||||||
AKERR_CHECK(handled_foreign == 1);
|
|
||||||
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "Component Error") == 0);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* An unhandled refusal carries a stack trace naming the real owner, so the
|
|
||||||
* report a consumer gets is the library's normal one.
|
|
||||||
*/
|
|
||||||
akerr_capture_reset();
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(errctx, akerr_reserve_status_range(AKERR_VALUE, 1, "encroacher"));
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} HANDLE_DEFAULT(errctx) {
|
|
||||||
LOG_ERROR_WITH_MESSAGE(errctx, "Reservation refused");
|
|
||||||
} FINISH_NORETURN(errctx);
|
|
||||||
AKERR_CHECK_CONTAINS("Reservation refused");
|
|
||||||
AKERR_CHECK_CONTAINS("Status Range Overlap");
|
|
||||||
AKERR_CHECK_CONTAINS(AKERR_LIBRARY_OWNER);
|
|
||||||
AKERR_CHECK_CONTAINS("encroacher");
|
|
||||||
AKERR_CHECK_CONTAINS("error.c");
|
|
||||||
|
|
||||||
/* Nothing above stranded a pool slot. */
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
|
|
||||||
fprintf(stderr, "err_status_exception ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The success path: a nested call that returns cleanly (NULL) must not break
|
|
||||||
* out of the caller's ATTEMPT block, must not enter any handler, and must not
|
|
||||||
* consume a slot from the error pool.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int default_fired = 0;
|
|
||||||
|
|
||||||
akerr_ErrorContext *ok_func(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} FINISH(e, true);
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
akerr_init();
|
|
||||||
|
|
||||||
int reached_after_catch = 0;
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(e, ok_func());
|
|
||||||
reached_after_catch = 1; /* must run: no break on success */
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} HANDLE_DEFAULT(e) {
|
|
||||||
default_fired = 1; /* must NOT run */
|
|
||||||
} FINISH_NORETURN(e);
|
|
||||||
|
|
||||||
AKERR_CHECK(reached_after_catch == 1);
|
|
||||||
AKERR_CHECK(default_fired == 0);
|
|
||||||
AKERR_CHECK(e == NULL);
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
fprintf(stderr, "err_success ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* FINISH(ctx, false) closes an ATTEMPT block without propagating: an unhandled
|
|
||||||
* error is dropped (not returned, not passed to the unhandled handler) and the
|
|
||||||
* context is released back to the pool. Because we use FINISH (not
|
|
||||||
* FINISH_NORETURN) the process must continue normally and exit 0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int wrong_handler_fired = 0;
|
|
||||||
static int swallowed_status = 0;
|
|
||||||
|
|
||||||
akerr_ErrorContext *boom(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
FAIL_RETURN(e, AKERR_VALUE, "unhandled but swallowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* FINISH(e, false) belongs in a context-returning function. On the swallow
|
|
||||||
* path e is released to NULL, so this returns NULL to its caller. */
|
|
||||||
akerr_ErrorContext *swallow_it(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(e, boom());
|
|
||||||
} CLEANUP {
|
|
||||||
swallowed_status = (e != NULL) ? e->status : 0;
|
|
||||||
} PROCESS(e) {
|
|
||||||
} HANDLE(e, AKERR_KEY) {
|
|
||||||
wrong_handler_fired = 1; /* does not match AKERR_VALUE */
|
|
||||||
} FINISH(e, false);
|
|
||||||
return e; /* NULL: released even though unhandled */
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
|
|
||||||
akerr_ErrorContext *res = swallow_it();
|
|
||||||
|
|
||||||
AKERR_CHECK(wrong_handler_fired == 0);
|
|
||||||
AKERR_CHECK(swallowed_status == AKERR_VALUE);
|
|
||||||
AKERR_CHECK(res == NULL); /* released even though unhandled */
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
fprintf(stderr, "err_swallow ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,17 @@
|
|||||||
#include "akerror.h"
|
#include "sdlerror.h"
|
||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
static void expect_unhandled_nullpointer(akerr_ErrorContext *errctx)
|
ErrorContext *func2(void)
|
||||||
{
|
|
||||||
exit((errctx != NULL && errctx->status == AKERR_NULLPOINTER) ? 1 : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
akerr_ErrorContext *func2(void)
|
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
FAIL(errctx, AKERR_NULLPOINTER, "This is a failure in func2");
|
FAIL(errctx, ERR_NULLPOINTER, "This is a failure in func2");
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} FINISH(errctx, true);
|
} FINISH(errctx, true);
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
akerr_ErrorContext *func1(void)
|
ErrorContext *func1(void)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
@@ -31,9 +25,6 @@ akerr_ErrorContext *func1(void)
|
|||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
akerr_init();
|
|
||||||
akerr_handler_unhandled_error = &expect_unhandled_nullpointer;
|
|
||||||
|
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, func1());
|
CATCH(errctx, func1());
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <sys/wait.h>
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The default unhandled-error handler is the library's last stop: it exits the
|
|
||||||
* process. Both of its exits were untested -- exit(1) for a NULL context (a
|
|
||||||
* handler invoked with no error at all) and exit(errctx->status) for a real one.
|
|
||||||
*
|
|
||||||
* The handler never returns, so each case runs in a forked child and the test
|
|
||||||
* asserts the exact exit status. That is stricter than a WILL_FAIL test, which
|
|
||||||
* would pass on any non-zero exit, including one caused by an unrelated bug.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Run the default handler on ctx in a child process and return the child's exit
|
|
||||||
* status, or -1 if it did not exit normally. The _exit() sentinel catches a
|
|
||||||
* handler that returns instead of terminating.
|
|
||||||
*/
|
|
||||||
static int handler_exit_status(akerr_ErrorContext *ctx)
|
|
||||||
{
|
|
||||||
pid_t pid = fork();
|
|
||||||
if ( pid == 0 ) {
|
|
||||||
akerr_default_handler_unhandled_error(ctx);
|
|
||||||
_exit(99);
|
|
||||||
}
|
|
||||||
int status = 0;
|
|
||||||
if ( pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status) ) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return WEXITSTATUS(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
akerr_init();
|
|
||||||
|
|
||||||
/* No context to report: the handler has nothing to exit with but failure. */
|
|
||||||
AKERR_CHECK(handler_exit_status(NULL) == 1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* With a context, the status becomes the exit code. waitpid only reports
|
|
||||||
* its low 8 bits, which is where AKERR_VALUE (144) lands, and it is neither
|
|
||||||
* 0 nor the 1 used for the NULL case, so the two exits stay distinguishable.
|
|
||||||
*/
|
|
||||||
akerr_ErrorContext *slot = akerr_next_error();
|
|
||||||
AKERR_CHECK(slot != NULL);
|
|
||||||
slot->refcount = 1;
|
|
||||||
slot->status = AKERR_VALUE;
|
|
||||||
AKERR_CHECK((AKERR_VALUE & 0xff) != 0 && (AKERR_VALUE & 0xff) != 1);
|
|
||||||
AKERR_CHECK(handler_exit_status(slot) == (AKERR_VALUE & 0xff));
|
|
||||||
|
|
||||||
slot = akerr_release_error(slot);
|
|
||||||
AKERR_CHECK(slot == NULL);
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
|
|
||||||
fprintf(stderr, "err_unhandled_null ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user