Compare commits
31 Commits
new_readme
...
tests/expa
| Author | SHA1 | Date | |
|---|---|---|---|
|
8a026d3006
|
|||
|
792e646957
|
|||
|
43516c7e73
|
|||
|
536a269aad
|
|||
|
10f7203e8f
|
|||
|
43f46dca64
|
|||
|
e5f761662c
|
|||
|
4daa411f3f
|
|||
|
4fad0cec59
|
|||
|
5b1a6c2cc7
|
|||
|
675c60b5e0
|
|||
|
93f5e93480
|
|||
|
b5435041f2
|
|||
|
235033d633
|
|||
|
7700af06a1
|
|||
|
be2dba8416
|
|||
|
03e9b8a96d
|
|||
|
768a235da4
|
|||
|
4a09ca87fd
|
|||
|
51b6b23b4c
|
|||
|
166a478e6a
|
|||
|
0c29f5d69f
|
|||
|
c840536e1d
|
|||
|
14e903a81c
|
|||
|
6acae958ff
|
|||
|
cb9af93e0b
|
|||
|
c526bb1ba3
|
|||
|
6821752ec7
|
|||
|
f2ce97224a
|
|||
|
3e4f0d1cda
|
|||
|
cf0f2bbeac
|
69
.gitea/workflows/ci.yaml
Normal file
69
.gitea/workflows/ci.yaml
Normal file
@@ -0,0 +1,69 @@
|
||||
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 }}."
|
||||
|
||||
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
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
include/akerror.h
|
||||
build/
|
||||
*-junit.xml
|
||||
153
CMakeLists.txt
153
CMakeLists.txt
@@ -1,60 +1,139 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(sdlerror LANGUAGES C)
|
||||
project(akerror LANGUAGES C)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
include(CMakePackageConfigHelpers)
|
||||
include(CTest)
|
||||
|
||||
set(sdlerror_install_cmakedir "${CMAKE_INSTALL_LIBDIR}/cmake/sdlerror")
|
||||
set(AKERR_USE_STDLIB 1 CACHE BOOL "Use the C standard library")
|
||||
set(akerror_install_cmakedir "${CMAKE_INSTALL_LIBDIR}/cmake/akerror")
|
||||
|
||||
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(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3-static)
|
||||
add_library(akerror::akerror ALIAS akerror)
|
||||
|
||||
# Check for SDL3 using pkg-config
|
||||
pkg_check_modules(SDL3 REQUIRED sdl3)
|
||||
|
||||
# Add include directories
|
||||
include_directories(${SDL3_INCLUDE_DIRS})
|
||||
add_library(sdlerror STATIC
|
||||
src/error.c
|
||||
target_compile_definitions(akerror
|
||||
PUBLIC AKERR_USE_STDLIB=${AKERR_USE_STDLIB}
|
||||
)
|
||||
|
||||
add_executable(test_err_catch tests/err_catch.c)
|
||||
add_executable(test_err_cleanup tests/err_cleanup.c)
|
||||
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}/>
|
||||
# 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
|
||||
)
|
||||
target_link_libraries(test_err_catch PRIVATE sdlerror SDL3::SDL3)
|
||||
target_link_libraries(test_err_cleanup PRIVATE sdlerror SDL3::SDL3)
|
||||
target_link_libraries(test_err_trace PRIVATE sdlerror SDL3::SDL3)
|
||||
|
||||
set(AKERR_WILL_FAIL_TESTS
|
||||
err_trace
|
||||
err_improper_closure
|
||||
)
|
||||
|
||||
foreach(_test IN LISTS AKERR_TESTS)
|
||||
add_executable(test_${_test} tests/${_test}.c)
|
||||
target_include_directories(test_${_test} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests)
|
||||
target_link_libraries(test_${_test} PRIVATE akerror)
|
||||
add_test(NAME ${_test} COMMAND test_${_test})
|
||||
endforeach()
|
||||
|
||||
# err_maxval parses the generated header to check AKERR_MAX_ERR_VALUE covers
|
||||
# every AKERR_* code, so it needs the path to the header it was built against.
|
||||
target_compile_definitions(test_err_maxval PRIVATE
|
||||
AKERR_GENERATED_HEADER="${GENERATED_AKERROR_H}")
|
||||
|
||||
set_tests_properties(
|
||||
${AKERR_WILL_FAIL_TESTS}
|
||||
PROPERTIES WILL_FAIL TRUE
|
||||
)
|
||||
|
||||
# Mutation testing: break the library in small ways and confirm the test suite
|
||||
# notices. This is a meta-check on the tests themselves, so it is a manual
|
||||
# target (it rebuilds and re-runs the whole suite many times), not a CTest test.
|
||||
# cmake --build build --target mutation
|
||||
find_package(Python3 COMPONENTS Interpreter)
|
||||
if(Python3_FOUND)
|
||||
add_custom_target(mutation
|
||||
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}")
|
||||
install(TARGETS sdlerror EXPORT sdlerror DESTINATION "lib/")
|
||||
install(FILES "include/sdlerror.h" DESTINATION "include/")
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/sdlerror.pc DESTINATION "lib/pkgconfig/")
|
||||
install(TARGETS akerror
|
||||
EXPORT akerrorTargets
|
||||
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
|
||||
)
|
||||
|
||||
install(FILES ${GENERATED_AKERROR_H} DESTINATION "include/")
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akerror.pc DESTINATION "lib/pkgconfig/")
|
||||
|
||||
install(EXPORT sdlerror
|
||||
FILE sdlerrorTargets.cmake
|
||||
NAMESPACE sdlerror::
|
||||
DESTINATION ${sdlerror_install_cmakedir}
|
||||
install(EXPORT akerrorTargets
|
||||
FILE akerrorTargets.cmake
|
||||
NAMESPACE akerror::
|
||||
DESTINATION ${akerror_install_cmakedir}
|
||||
)
|
||||
|
||||
configure_package_config_file(
|
||||
cmake/sdlerror.cmake.in
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/sdlerrorConfig.cmake"
|
||||
INSTALL_DESTINATION ${sdlerror_install_cmakedir}
|
||||
cmake/akerror.cmake.in
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/akerrorConfig.cmake"
|
||||
INSTALL_DESTINATION ${akerror_install_cmakedir}
|
||||
)
|
||||
|
||||
install(FILES
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/sdlerrorConfig.cmake"
|
||||
DESTINATION ${sdlerror_install_cmakedir}
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/akerrorConfig.cmake"
|
||||
DESTINATION ${akerror_install_cmakedir}
|
||||
)
|
||||
|
||||
# pkgconfig
|
||||
@@ -62,4 +141,4 @@ set(prefix ${CMAKE_INSTALL_PREFIX})
|
||||
set(exec_prefix "\${prefix}")
|
||||
set(libdir "\${exec_prefix}/lib")
|
||||
set(includedir "\${prefix}/include")
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/sdlerror.pc.in ${CMAKE_CURRENT_BINARY_DIR}/sdlerror.pc @ONLY)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/akerror.pc.in ${CMAKE_CURRENT_BINARY_DIR}/akerror.pc @ONLY)
|
||||
|
||||
249
README.md
249
README.md
@@ -2,19 +2,19 @@
|
||||
|
||||
This library provides a TRY/CATCH style exception handling mechanism for C.
|
||||
|
||||
# Dependencies
|
||||

|
||||
|
||||
This library depends on the `SDL3` library and `stdlib`. Specifically it uses the SDL_Log method from SDL3.
|
||||
# Why?
|
||||
|
||||
# Installation
|
||||
There is nothing wrong with C as it is. This library does not claim to fix some problem with C.
|
||||
|
||||
```bash
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
cmake --install build
|
||||
```
|
||||
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.
|
||||
|
||||
# Philosophy of Use
|
||||
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:
|
||||
|
||||
@@ -25,60 +25,147 @@ This library has 6 guiding principles:
|
||||
* 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 by defining additional `AKERR_xxxxx` values. Error values up to 255 are reserved by the library (`AKERR_xxxxx` begins where `errno` stops), so please begin your error values at 256. When you add additional error codes, you need to define `-DAKERR_MAX_ERR_VALUE=n` to the compiler, where `n` is the maximum error code you have defined. If you define custom error codes, `AKERR_MAX_ERR_VALUE` must be >= 256 or the compiler will throw an error.
|
||||
|
||||
Define a human-friendly name for the error with the `akerr_name_for_status` method somewhere in your code's initialization before the error may be used:
|
||||
|
||||
```c
|
||||
akerr_name_for_status(129, "Some Error Code Description")
|
||||
```
|
||||
|
||||
|
||||
# Installation
|
||||
|
||||
```bash
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
cmake --install build
|
||||
```
|
||||
|
||||
## Templating and autogenerated code
|
||||
|
||||
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
|
||||
- `sprintf` function
|
||||
- `exit` function
|
||||
- `bool` type
|
||||
- `NULL` type
|
||||
|
||||
... then you can compile it thusly:
|
||||
|
||||
```
|
||||
cmake -S . -B build -DAKERR_USE_STDLIB=OFF
|
||||
cmake --build build
|
||||
cmake --install build
|
||||
```
|
||||
|
||||
# Using the library
|
||||
|
||||
## Simply
|
||||
## Setting up your project
|
||||
|
||||
Include it
|
||||
|
||||
```c
|
||||
#include <sdlerror.h>
|
||||
#include <akerror.h>
|
||||
```
|
||||
|
||||
Link against it
|
||||
Link the library directly, or
|
||||
|
||||
```sh
|
||||
cc -lsdlerror
|
||||
cc -lakerror
|
||||
```
|
||||
|
||||
.. Done.
|
||||
|
||||
## CMake dependencies
|
||||
|
||||
Using pkg-config:
|
||||
Using pkg-config, or
|
||||
|
||||
```sh
|
||||
pkg-config sdlerror --cflags
|
||||
pkg-config sdlerror --ldflags
|
||||
pkg-config akerror --cflags
|
||||
pkg-config akerror --ldflags
|
||||
```
|
||||
|
||||
Using cmake:
|
||||
|
||||
```cmake
|
||||
find_package(sdlerror REQUIRED)
|
||||
pkg_check_modules(sdlerror REQUIRED sdlerror)
|
||||
target_link_libraries(YOUR_TARGET PRIVATE sdlerror::sdlerror)
|
||||
find_package(akerror REQUIRED)
|
||||
pkg_check_modules(akerror REQUIRED akerror)
|
||||
target_link_libraries(YOUR_TARGET PRIVATE akerror::akerror)
|
||||
```
|
||||
|
||||
# Functions and Return Codes
|
||||
Using this project as a submodule with cmake:
|
||||
|
||||
This library can perform tests on any function or expression that returns an integer value.
|
||||
```cmake
|
||||
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
|
||||
|
||||
Any function which uses the `PREPARE_ERROR` macro should have a return type of `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 `sdlerror.h` in the form of `ERR_xxxxx` where `xxxxx` is the name of the error code in question. See `sdlerror.h` for a list of defined errors and their descriptions.
|
||||
|
||||
You can define additional error types by defining additional `ERR_xxxxx` values. Begin your error values at 128. Define a human-friendly name for the error with the `error_name_for_status` method:
|
||||
|
||||
```c
|
||||
error_name_for_status(129, "Some Error Code Description")
|
||||
target_link_libraries(YOUR_PROJECT PRIVATE akerror::akerror)
|
||||
```
|
||||
|
||||
When you add additional error codes, you need to define `-DMAX_ERR_VALUE=n` where `n` is the maximum error code you have defined.
|
||||
|
||||
# Setting up the error context
|
||||
## (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.
|
||||
|
||||
@@ -86,11 +173,9 @@ Before you can use any of these macros you must set up an error context inside o
|
||||
PREPARE_ERROR(errctx);
|
||||
```
|
||||
|
||||
This will create a 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.
|
||||
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.
|
||||
|
||||
#
|
||||
|
||||
# Attempting an operation
|
||||
## Attempting an Operation
|
||||
|
||||
```c
|
||||
ATTEMPT {
|
||||
@@ -106,15 +191,16 @@ ATTEMPT {
|
||||
|
||||
`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 ErrorContext, and a boolean regarding whether or not to pass unhandled errors up to the calling function. Unless you have a good reason not to, this should be true.
|
||||
`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 ErrorContext *
|
||||
## Capturing errors from functions which return akerr_ErrorContext *
|
||||
|
||||
For functions that return `ErrorContext *`, you should use the `CATCH` macro.
|
||||
For functions that return `akerr_ErrorContext *`, you should use the `CATCH` macro.
|
||||
|
||||
```c
|
||||
ATTEMPT {
|
||||
@@ -122,7 +208,7 @@ ATTEMPT {
|
||||
} // ...
|
||||
```
|
||||
|
||||
This will call assign the return value of the function in question to the ErrorContext previously prepared in the current scope. If the function returns an ErrorContext that indicates any type of error, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
|
||||
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.
|
||||
|
||||
## Setting errors from functions or expressions returning integer
|
||||
|
||||
@@ -132,7 +218,7 @@ Here is an example of checking for a NULL pointer
|
||||
|
||||
```c
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(errctx, (somePointer == NULL), ERR_NULLPOINTER, "Someone gave me a NULL pointer")
|
||||
FAIL_ZERO_BREAK(errctx, (somePointer == NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
|
||||
} // ...
|
||||
```
|
||||
|
||||
@@ -140,12 +226,34 @@ Here is an example of checking for two strings that are not equal
|
||||
|
||||
```c
|
||||
ATTEMPT {
|
||||
FAIL_NONZERO_BREAK(errctx, strcmp("not", "equal"), ERR_VALUE, "Strings are not equal")
|
||||
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.
|
||||
|
||||
# Passing errors
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
PREPARE_ERROR(e);
|
||||
PASS(e, some_method_that_may_fail());
|
||||
SUCCEED_RETURN(e);
|
||||
```
|
||||
|
||||
This does the same thing as this, but with less code:
|
||||
|
||||
```
|
||||
PREPARE_ERROR(e);
|
||||
ATTEMPT {
|
||||
CATCH(e, some_method_that_may_fail());
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
```
|
||||
|
||||
# Handling errors
|
||||
|
||||
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`.
|
||||
@@ -156,7 +264,7 @@ In order to handle a specific error code, use the `HANDLE` macro.
|
||||
|
||||
```c
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, ERR_NULLPOINTER) {
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
// Something is complaining about a null pointer error. Do something about it.
|
||||
} // ...
|
||||
```
|
||||
@@ -167,35 +275,35 @@ In order to handle a group of related errors that all require the same failure b
|
||||
|
||||
```c
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, ERR_IO) {
|
||||
} HANDLE_GROUP(errctx, ERR_KEY) {
|
||||
} HANDLE_GROUP(errctx, ERR_INDEX) {
|
||||
} 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.
|
||||
|
||||
The fallthrough behavior stops as soon as another `HANDLE` macro is encountered. For example, in this example, `ERR_IO`, `ERR_KEY` and `ERR_INDEX` are all handled as a group, but `ERR_RELATIONSHIP` is not.
|
||||
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.
|
||||
|
||||
```c
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, ERR_IO) {
|
||||
} HANDLE_GROUP(errctx, ERR_KEY) {
|
||||
} HANDLE_GROUP(errctx, ERR_INDEX) {
|
||||
} HANDLE(errctx, AKERR_IO) {
|
||||
} HANDLE_GROUP(errctx, AKERR_KEY) {
|
||||
} HANDLE_GROUP(errctx, AKERR_INDEX) {
|
||||
// This code handles 3 error cases
|
||||
} HANDLE(errctx, ERR_RELATIONSHIP) {
|
||||
} HANDLE(errctx, AKERR_RELATIONSHIP) {
|
||||
// This code handles 1 error case
|
||||
}
|
||||
```
|
||||
|
||||
# Returning success or failure from functions returning ErrorContext *
|
||||
# Returning success or failure from functions returning akerr_ErrorContext *
|
||||
|
||||
If at all possible, when using this library, your functiions should return `ErrorContext *`. When returning from such functions, you should use the `SUCCEED_RETURN` and `FAIL_RETURN` macros.
|
||||
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
|
||||
|
||||
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 ErrorContext to a successful state and exits the function.
|
||||
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.
|
||||
|
||||
```c
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -215,7 +323,7 @@ The function allows you to provide printf-style variable arguments to provide a
|
||||
|
||||
```c
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_RETURN(ERR_BEHAVIOR, "Something went horribly wrong!")
|
||||
FAIL_RETURN(AKERR_BEHAVIOR, "Something went horribly wrong!")
|
||||
```
|
||||
|
||||
## Conditionally failing and returning
|
||||
@@ -224,30 +332,34 @@ In addition to `FAIL_RETURN` you can also test for zero or non-zero conditions,
|
||||
|
||||
```c
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, (somePointer == NULL), ERR_NULLPOINTER, "Someone gave me a NULL pointer")
|
||||
FAIL_ZERO_RETURN(errctx, (somePointer == NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
|
||||
```
|
||||
|
||||
```c
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_NONZERO_RETURN(errctx, strcmp("not", "equal"), ERR_VALUE, "Strings are not equal")
|
||||
FAIL_NONZERO_RETURN(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")
|
||||
```
|
||||
|
||||
# Uncaught errors
|
||||
|
||||
## Misbehaving methods
|
||||
|
||||
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.
|
||||
|
||||
## Ensuring that all error codes are captured
|
||||
|
||||
Any function which returns `ErrorContext *` should also be marked with `ERROR_NOIGNORE`.
|
||||
Any function which returns `akerr_ErrorContext *` should also be marked with `AKERROR_NOIGNORE`.
|
||||
|
||||
```c
|
||||
ErrorContext ERROR_NOIGNORE *f(...);
|
||||
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.
|
||||
|
||||
Beware that `ERROR_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`.
|
||||
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
|
||||
#define ERROR_NOIGNORE __attribute__((warn_unused_result))
|
||||
#define AKERROR_NOIGNORE __attribute__((warn_unused_result))
|
||||
```
|
||||
|
||||
## Stack Traces
|
||||
@@ -259,10 +371,10 @@ Consider the `tests/err_trace.c` program which intentionally triggers this behav
|
||||
```
|
||||
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 heap (refcount 1)
|
||||
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 heap (refcount 1)
|
||||
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
|
||||
```
|
||||
@@ -277,3 +389,4 @@ From bottom to top, we have:
|
||||
* 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
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ exec_prefix=${prefix}
|
||||
libdir=${exec_prefix}/lib
|
||||
includedir=${exec_prefix}/include
|
||||
|
||||
Name: sdlerror
|
||||
Description: A C error handling library that relies on SDL3
|
||||
Name: akerror
|
||||
Description: AKLabs C error handling library
|
||||
Version: @PROJECT_VERSION@
|
||||
Cflags: -I${includedir}/
|
||||
Libs: -L${libdir} -lsdlerror
|
||||
Libs: -L${libdir} -lakerror
|
||||
@@ -2,4 +2,4 @@
|
||||
include(CMakeFindDependencyMacro) # If your library has dependencies
|
||||
# find_dependency(AnotherDependency REQUIRED) # Example dependency
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/sdlerror.cmake")
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/akerrorTargets.cmake")
|
||||
272
include/akerror.tmpl.h
Normal file
272
include/akerror.tmpl.h
Normal file
@@ -0,0 +1,272 @@
|
||||
#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) */
|
||||
|
||||
#ifndef AKERR_MAX_ERR_VALUE
|
||||
/* Must be >= the highest AKERR_* offset above (AKERR_BADEXC, +17) so every
|
||||
* library error code is indexable in __AKERR_ERROR_NAMES. Keep in sync when
|
||||
* adding codes; tests/err_maxval.c guards this invariant. */
|
||||
#define AKERR_MAX_ERR_VALUE (AKERR_LAST_ERRNO_VALUE + 17)
|
||||
#elif AKERR_MAX_ERR_VALUE < 256
|
||||
#error user-defined AKERR_MAX_ERR_VALUE must be >= 256
|
||||
#endif
|
||||
|
||||
extern char __AKERR_ERROR_NAMES[AKERR_MAX_ERR_VALUE+1][AKERR_MAX_ERROR_NAME_LENGTH];
|
||||
|
||||
#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();
|
||||
char *akerr_name_for_status(int status, char *name);
|
||||
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);
|
||||
|
||||
#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;
|
||||
|
||||
/*
|
||||
* 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, __FILE__); \
|
||||
snprintf((char *)__err_context->function, AKERR_MAX_ERROR_FUNCTION_LENGTH, __func__); \
|
||||
__err_context->lineno = __LINE__; \
|
||||
snprintf((char *)__err_context->message, AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH, __message, ## __VA_ARGS__); \
|
||||
__err_context->stacktracebufptr += snprintf(__err_context->stacktracebufptr, AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH, "%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 ) { \
|
||||
__err_context->stacktracebufptr += snprintf(__err_context->stacktracebufptr, AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH, "%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_
|
||||
@@ -1,243 +0,0 @@
|
||||
#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_
|
||||
20
scripts/generrno.sh
Normal file
20
scripts/generrno.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/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
|
||||
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_for_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
|
||||
436
scripts/mutation_test.py
Executable file
436
scripts/mutation_test.py
Executable file
@@ -0,0 +1,436 @@
|
||||
#!/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())
|
||||
128
src/error.c
128
src/error.c
@@ -1,49 +1,81 @@
|
||||
#include "sdlerror.h"
|
||||
#include "stdlib.h"
|
||||
#include "akerror.h"
|
||||
#if defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1
|
||||
#include <stdlib.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#endif // AKERR_USE_STDLIB
|
||||
|
||||
ErrorContext __error_last_ditch;
|
||||
ErrorContext *__error_last_ignored;
|
||||
ErrorUnhandledErrorHandler error_handler_unhandled_error;
|
||||
akerr_ErrorContext __akerr_last_ditch;
|
||||
akerr_ErrorContext *__akerr_last_ignored;
|
||||
akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
|
||||
akerr_ErrorLogFunction akerr_log_method = NULL;
|
||||
|
||||
char __ERROR_NAMES[MAX_ERR_VALUE+1][MAX_ERROR_NAME_LENGTH];
|
||||
char __AKERR_ERROR_NAMES[AKERR_MAX_ERR_VALUE+1][AKERR_MAX_ERROR_NAME_LENGTH];
|
||||
|
||||
ErrorContext HEAP_ERROR[MAX_HEAP_ERROR];
|
||||
akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR];
|
||||
|
||||
void error_init()
|
||||
int akerr_valid_error_address(akerr_ErrorContext *ptr)
|
||||
{
|
||||
// Is this within the memory region occupied by AKERR_ARRAY_ERROR?
|
||||
if ( ptr == NULL ) {
|
||||
return 1;
|
||||
}
|
||||
return ((ptr >= &AKERR_ARRAY_ERROR[0]) &&
|
||||
(ptr <= &AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR-1]));
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
void akerr_init()
|
||||
{
|
||||
static int 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;
|
||||
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;
|
||||
}
|
||||
__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");
|
||||
__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_ERROR_NAMES[0], 0x00, ((AKERR_MAX_ERR_VALUE+1) * AKERR_MAX_ERROR_NAME_LENGTH));
|
||||
|
||||
akerr_name_for_status(AKERR_NULLPOINTER, "Null Pointer Error");
|
||||
akerr_name_for_status(AKERR_OUTOFBOUNDS, "Out Of Bounds Error");
|
||||
akerr_name_for_status(AKERR_API, "API Error");
|
||||
akerr_name_for_status(AKERR_ATTRIBUTE, "Attribute Error");
|
||||
akerr_name_for_status(AKERR_TYPE, "Type Error");
|
||||
akerr_name_for_status(AKERR_KEY, "Key Error");
|
||||
akerr_name_for_status(AKERR_INDEX, "Index Error");
|
||||
akerr_name_for_status(AKERR_FORMAT, "Format Error");
|
||||
akerr_name_for_status(AKERR_IO, "Input Output Error");
|
||||
akerr_name_for_status(AKERR_VALUE, "Value Error");
|
||||
akerr_name_for_status(AKERR_RELATIONSHIP, "Relationship Error");
|
||||
akerr_name_for_status(AKERR_CIRCULAR_REFERENCE, "Circular Reference Error");
|
||||
akerr_name_for_status(AKERR_BADEXC, "Invalid akerr_ErrorContext");
|
||||
#if (defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1) || (!defined(AKERR_USE_STDLIB))
|
||||
akerr_init_errno();
|
||||
#endif
|
||||
inited = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void error_default_handler_unhandled_error(ErrorContext *errctx)
|
||||
void akerr_default_handler_unhandled_error(akerr_ErrorContext *errctx)
|
||||
{
|
||||
if ( errctx == NULL ) {
|
||||
exit(1);
|
||||
@@ -51,31 +83,31 @@ void error_default_handler_unhandled_error(ErrorContext *errctx)
|
||||
exit(errctx->status);
|
||||
}
|
||||
|
||||
ErrorContext *heap_next_error()
|
||||
akerr_ErrorContext *akerr_next_error()
|
||||
{
|
||||
for (int i = 0; i < MAX_HEAP_ERROR; i++ ) {
|
||||
if ( HEAP_ERROR[i].refcount == 0 ) {
|
||||
return &HEAP_ERROR[i];
|
||||
for (int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
||||
if ( AKERR_ARRAY_ERROR[i].refcount == 0 ) {
|
||||
return &AKERR_ARRAY_ERROR[i];
|
||||
}
|
||||
}
|
||||
return (ErrorContext *)NULL;
|
||||
return (akerr_ErrorContext *)NULL;
|
||||
}
|
||||
|
||||
ErrorContext *heap_release_error(ErrorContext *err)
|
||||
akerr_ErrorContext *akerr_release_error(akerr_ErrorContext *err)
|
||||
{
|
||||
int oldid = 0;
|
||||
if ( err == NULL ) {
|
||||
ErrorContext *errctx = &__error_last_ditch;
|
||||
FAIL_RETURN(errctx, ERR_NULLPOINTER, "heap_release_error got NULL context pointer");
|
||||
akerr_ErrorContext *errctx = &__akerr_last_ditch;
|
||||
FAIL_RETURN(errctx, AKERR_NULLPOINTER, "akerr_release_error got NULL context pointer");
|
||||
}
|
||||
if ( err->refcount > 0 ) {
|
||||
err->refcount -= 1;
|
||||
}
|
||||
if ( err->refcount == 0 ) {
|
||||
oldid = err->heapid;
|
||||
memset(err, 0x00, sizeof(ErrorContext));
|
||||
oldid = err->arrayid;
|
||||
memset(err, 0x00, sizeof(akerr_ErrorContext));
|
||||
err->stacktracebufptr = (char *)&err->stacktracebuf;
|
||||
err->heapid = oldid;
|
||||
err->arrayid = oldid;
|
||||
return NULL;
|
||||
}
|
||||
return err;
|
||||
@@ -84,13 +116,13 @@ ErrorContext *heap_release_error(ErrorContext *err)
|
||||
|
||||
// returns or sets the name for the given status.
|
||||
// Call with name = NULL to retrieve a status.
|
||||
char *error_name_for_status(int status, char *name)
|
||||
char *akerr_name_for_status(int status, char *name)
|
||||
{
|
||||
if ( status > MAX_ERR_VALUE ) {
|
||||
if ( status > AKERR_MAX_ERR_VALUE ) {
|
||||
return "Unknown Error";
|
||||
}
|
||||
if ( name != NULL ) {
|
||||
strncpy((char *)&__ERROR_NAMES[status], name, MAX_ERROR_NAME_LENGTH);
|
||||
strncpy((char *)&__AKERR_ERROR_NAMES[status], name, AKERR_MAX_ERROR_NAME_LENGTH);
|
||||
}
|
||||
return (char *)&__ERROR_NAMES[status];
|
||||
return (char *)&__AKERR_ERROR_NAMES[status];
|
||||
}
|
||||
|
||||
117
tests/MUTATION.md
Normal file
117
tests/MUTATION.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# 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 ~74% (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.
|
||||
|
||||
Findings surfaced by mutation testing:
|
||||
|
||||
* **Fixed:** `AKERR_MAX_ERR_VALUE` was `AKERR_LAST_ERRNO_VALUE + 15`, below
|
||||
`AKERR_NOT_IMPLEMENTED` (+16) and `AKERR_BADEXC` (+17). `akerr_name_for_status`
|
||||
rejects any status `> AKERR_MAX_ERR_VALUE`, so those codes could never store or
|
||||
return a name and the `akerr_name_for_status(AKERR_BADEXC, ...)` call in
|
||||
`akerr_init` was dead code (which is why deleting it survived). The max is now
|
||||
`+ 17`, and `tests/err_maxval.c` guards the invariant so it can't regress.
|
||||
92
tests/err_break_variants.c
Normal file
92
tests/err_break_variants.c
Normal file
@@ -0,0 +1,92 @@
|
||||
#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(r != NULL && r->status == AKERR_VALUE);
|
||||
r = akerr_release_error(r);
|
||||
AKERR_CHECK(zero_break(7) == NULL);
|
||||
|
||||
r = nonzero_break(7);
|
||||
AKERR_CHECK(r != NULL && r->status == AKERR_INDEX);
|
||||
r = akerr_release_error(r);
|
||||
AKERR_CHECK(nonzero_break(0) == NULL);
|
||||
|
||||
r = always_break();
|
||||
AKERR_CHECK(r != NULL && r->status == AKERR_IO);
|
||||
r = akerr_release_error(r);
|
||||
|
||||
r = zero_return(0);
|
||||
AKERR_CHECK(r != NULL && r->status == AKERR_KEY);
|
||||
r = akerr_release_error(r);
|
||||
AKERR_CHECK(zero_return(7) == NULL);
|
||||
|
||||
r = nonzero_return(7);
|
||||
AKERR_CHECK(r != NULL && r->status == 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;
|
||||
}
|
||||
84
tests/err_capture.h
Normal file
84
tests/err_capture.h
Normal file
@@ -0,0 +1,84 @@
|
||||
#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_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,17 +1,17 @@
|
||||
#include "sdlerror.h"
|
||||
#include "akerror.h"
|
||||
|
||||
ErrorContext *func2(void)
|
||||
akerr_ErrorContext *func2(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
FAIL(errctx, ERR_NULLPOINTER, "This is a failure in func2");
|
||||
FAIL(errctx, AKERR_NULLPOINTER, "This is a failure in func2");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *func1(void)
|
||||
akerr_ErrorContext *func1(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
@@ -30,7 +30,7 @@ int main(void)
|
||||
CATCH(errctx, func1());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, ERR_NULLPOINTER) {
|
||||
SDL_Log("Caught exception");
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
akerr_log_method("Caught exception");
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
#include "sdlerror.h"
|
||||
#include "akerror.h"
|
||||
|
||||
int x;
|
||||
|
||||
ErrorContext *func2(void)
|
||||
akerr_ErrorContext *func2(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
FAIL(errctx, ERR_NULLPOINTER, "This is a failure in func2");
|
||||
FAIL(errctx, AKERR_NULLPOINTER, "This is a failure in func2");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *func1(void)
|
||||
akerr_ErrorContext *func1(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
@@ -33,7 +33,7 @@ int main(void)
|
||||
CATCH(errctx, func1());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, ERR_NULLPOINTER) {
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
if ( x == 0 ) {
|
||||
fprintf(stderr, "Cleanup works\n");
|
||||
return 0;
|
||||
|
||||
45
tests/err_custom_handler.c
Normal file
45
tests/err_custom_handler.c
Normal file
@@ -0,0 +1,45 @@
|
||||
#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;
|
||||
}
|
||||
47
tests/err_errno.c
Normal file
47
tests/err_errno.c
Normal file
@@ -0,0 +1,47 @@
|
||||
#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 out-of-range 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(AKERR_MAX_ERR_VALUE + 5000, NULL),
|
||||
"Unknown Error") == 0);
|
||||
|
||||
PREPARE_ERROR(e);
|
||||
ATTEMPT {
|
||||
CATCH(e, boom());
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} HANDLE(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;
|
||||
}
|
||||
46
tests/err_error_names.c
Normal file
46
tests/err_error_names.c
Normal file
@@ -0,0 +1,46 @@
|
||||
#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).
|
||||
*
|
||||
* Note: AKERR_EOF, AKERR_ITERATOR_BREAK and AKERR_NOT_IMPLEMENTED are omitted --
|
||||
* they are valid codes but akerr_init does not register a display name for them,
|
||||
* so akerr_name_for_status returns an empty string rather than a known name.
|
||||
*/
|
||||
|
||||
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_CIRCULAR_REFERENCE, "Circular Reference Error" },
|
||||
{ AKERR_BADEXC, "Invalid akerr_ErrorContext" },
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
fprintf(stderr, "err_error_names ok\n");
|
||||
return 0;
|
||||
}
|
||||
38
tests/err_handle_default.c
Normal file
38
tests/err_handle_default.c
Normal file
@@ -0,0 +1,38 @@
|
||||
#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;
|
||||
|
||||
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;
|
||||
} FINISH_NORETURN(e);
|
||||
|
||||
AKERR_CHECK(specific_fired == 0);
|
||||
AKERR_CHECK(default_fired == 1);
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
fprintf(stderr, "err_handle_default ok\n");
|
||||
return 0;
|
||||
}
|
||||
42
tests/err_handle_dispatch.c
Normal file
42
tests/err_handle_dispatch.c
Normal file
@@ -0,0 +1,42 @@
|
||||
#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;
|
||||
|
||||
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;
|
||||
} HANDLE(e, AKERR_IO) {
|
||||
c_fired = 1;
|
||||
} FINISH_NORETURN(e);
|
||||
|
||||
AKERR_CHECK(a_fired == 0);
|
||||
AKERR_CHECK(b_fired == 1);
|
||||
AKERR_CHECK(c_fired == 0);
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
fprintf(stderr, "err_handle_dispatch ok\n");
|
||||
return 0;
|
||||
}
|
||||
56
tests/err_handle_group.c
Normal file
56
tests/err_handle_group.c
Normal file
@@ -0,0 +1,56 @@
|
||||
#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;
|
||||
|
||||
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++;
|
||||
} HANDLE(e, AKERR_IO) {
|
||||
other_fired++;
|
||||
} FINISH(e, false);
|
||||
return e;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akerr_capture_install();
|
||||
|
||||
run(AKERR_KEY);
|
||||
AKERR_CHECK(group_fired == 1);
|
||||
AKERR_CHECK(other_fired == 0);
|
||||
|
||||
run(AKERR_INDEX);
|
||||
AKERR_CHECK(group_fired == 2);
|
||||
AKERR_CHECK(other_fired == 0);
|
||||
|
||||
run(AKERR_IO);
|
||||
AKERR_CHECK(group_fired == 2);
|
||||
AKERR_CHECK(other_fired == 1);
|
||||
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
fprintf(stderr, "err_handle_group ok\n");
|
||||
return 0;
|
||||
}
|
||||
34
tests/err_ignore.c
Normal file
34
tests/err_ignore.c
Normal file
@@ -0,0 +1,34 @@
|
||||
#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;
|
||||
}
|
||||
22
tests/err_improper_closure.c
Normal file
22
tests/err_improper_closure.c
Normal file
@@ -0,0 +1,22 @@
|
||||
#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);
|
||||
}
|
||||
92
tests/err_maxval.c
Normal file
92
tests/err_maxval.c
Normal file
@@ -0,0 +1,92 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
#include <regex.h>
|
||||
|
||||
/*
|
||||
* AKERR_MAX_ERR_VALUE sizes the __AKERR_ERROR_NAMES table and is the upper bound
|
||||
* akerr_name_for_status() accepts. If it is smaller than the highest AKERR_*
|
||||
* code, those codes silently lose their names (this was a real bug: the max was
|
||||
* +15 while AKERR_BADEXC is +17).
|
||||
*
|
||||
* Rather than hardcode the list of codes (which rots the moment someone adds a
|
||||
* code), this test parses the generated akerror.h, discovers every
|
||||
* #define AKERR_<NAME> (AKERR_LAST_ERRNO_VALUE + <N>)
|
||||
* finds the highest offset actually defined, and verifies AKERR_MAX_ERR_VALUE
|
||||
* covers it. The path to the header the library was built from is injected by
|
||||
* CMake as AKERR_GENERATED_HEADER.
|
||||
*/
|
||||
|
||||
#ifndef AKERR_GENERATED_HEADER
|
||||
#error "AKERR_GENERATED_HEADER (path to generated akerror.h) must be defined"
|
||||
#endif
|
||||
|
||||
int main(void)
|
||||
{
|
||||
FILE *fh = fopen(AKERR_GENERATED_HEADER, "r");
|
||||
AKERR_CHECK(fh != NULL);
|
||||
|
||||
/* #define AKERR_NAME (AKERR_LAST_ERRNO_VALUE + N) */
|
||||
regex_t re;
|
||||
const char *pattern =
|
||||
"^[[:space:]]*#define[[:space:]]+(AKERR_[A-Za-z0-9_]+)[[:space:]]+"
|
||||
"\\(AKERR_LAST_ERRNO_VALUE[[:space:]]*\\+[[:space:]]*([0-9]+)\\)";
|
||||
AKERR_CHECK(regcomp(&re, pattern, REG_EXTENDED) == 0);
|
||||
|
||||
int highest_code = -1; /* highest offset among real error codes */
|
||||
char highest_name[64] = "";
|
||||
int max_err_value = -1; /* offset parsed from AKERR_MAX_ERR_VALUE */
|
||||
int code_count = 0;
|
||||
|
||||
char line[4096];
|
||||
regmatch_t m[3];
|
||||
while ( fgets(line, sizeof(line), fh) != NULL ) {
|
||||
if ( regexec(&re, line, 3, m, 0) != 0 ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
char name[64];
|
||||
int nlen = (int)(m[1].rm_eo - m[1].rm_so);
|
||||
if ( nlen >= (int)sizeof(name) ) {
|
||||
nlen = (int)sizeof(name) - 1;
|
||||
}
|
||||
memcpy(name, line + m[1].rm_so, nlen);
|
||||
name[nlen] = '\0';
|
||||
|
||||
char num[16];
|
||||
int vlen = (int)(m[2].rm_eo - m[2].rm_so);
|
||||
if ( vlen >= (int)sizeof(num) ) {
|
||||
vlen = (int)sizeof(num) - 1;
|
||||
}
|
||||
memcpy(num, line + m[2].rm_so, vlen);
|
||||
num[vlen] = '\0';
|
||||
int offset = atoi(num);
|
||||
|
||||
if ( strcmp(name, "AKERR_MAX_ERR_VALUE") == 0 ) {
|
||||
max_err_value = offset;
|
||||
} else {
|
||||
code_count++;
|
||||
if ( offset > highest_code ) {
|
||||
highest_code = offset;
|
||||
snprintf(highest_name, sizeof(highest_name), "%s", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
regfree(&re);
|
||||
fclose(fh);
|
||||
|
||||
/* We must have actually parsed both the codes and the ceiling. */
|
||||
AKERR_CHECK(code_count > 0);
|
||||
AKERR_CHECK(highest_code > 0);
|
||||
AKERR_CHECK(max_err_value > 0);
|
||||
|
||||
/* Guard against parsing a different header than the one compiled in. */
|
||||
AKERR_CHECK(max_err_value == (AKERR_MAX_ERR_VALUE - AKERR_LAST_ERRNO_VALUE));
|
||||
|
||||
/* The actual invariant: every defined code is indexable in the names table. */
|
||||
AKERR_CHECK(max_err_value >= highest_code);
|
||||
|
||||
fprintf(stderr,
|
||||
"err_maxval ok (%d codes parsed, highest %s at +%d, max +%d)\n",
|
||||
code_count, highest_name, highest_code, max_err_value);
|
||||
return 0;
|
||||
}
|
||||
46
tests/err_pass.c
Normal file
46
tests/err_pass.c
Normal file
@@ -0,0 +1,46 @@
|
||||
#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) {
|
||||
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;
|
||||
}
|
||||
39
tests/err_pool_exhaust.c
Normal file
39
tests/err_pool_exhaust.c
Normal file
@@ -0,0 +1,39 @@
|
||||
#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;
|
||||
}
|
||||
55
tests/err_pool_refcount.c
Normal file
55
tests/err_pool_refcount.c
Normal file
@@ -0,0 +1,55 @@
|
||||
#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
|
||||
|
||||
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) {
|
||||
} 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++ ) {
|
||||
(void)one_cycle();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
44
tests/err_release_clears.c
Normal file
44
tests/err_release_clears.c
Normal file
@@ -0,0 +1,44 @@
|
||||
#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) {
|
||||
} 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;
|
||||
}
|
||||
44
tests/err_success.c
Normal file
44
tests/err_success.c
Normal file
@@ -0,0 +1,44 @@
|
||||
#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;
|
||||
}
|
||||
45
tests/err_swallow.c
Normal file
45
tests/err_swallow.c
Normal file
@@ -0,0 +1,45 @@
|
||||
#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;
|
||||
|
||||
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 {
|
||||
} 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(res == NULL); /* released even though unhandled */
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
fprintf(stderr, "err_swallow ok\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
#include "sdlerror.h"
|
||||
#include "akerror.h"
|
||||
|
||||
ErrorContext *func2(void)
|
||||
akerr_ErrorContext *func2(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
FAIL(errctx, ERR_NULLPOINTER, "This is a failure in func2");
|
||||
FAIL(errctx, AKERR_NULLPOINTER, "This is a failure in func2");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ErrorContext *func1(void)
|
||||
akerr_ErrorContext *func1(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
|
||||
Reference in New Issue
Block a user