Compare commits
22 Commits
timestamps
...
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
|
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
|
||||
123
CMakeLists.txt
123
CMakeLists.txt
@@ -3,57 +3,111 @@ project(akerror LANGUAGES C)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
include(CMakePackageConfigHelpers)
|
||||
include(CTest)
|
||||
|
||||
set(AKERR_USE_STDLIB 1 CACHE BOOL "Use the C standard library")
|
||||
set(akerror_install_cmakedir "${CMAKE_INSTALL_LIBDIR}/cmake/akerror")
|
||||
|
||||
set(SCRIPT ${CMAKE_SOURCE_DIR}/scripts/generrno.sh)
|
||||
set(INFILE ${CMAKE_SOURCE_DIR}/include/akerror.tmpl.h)
|
||||
set(OUTFILES ${CMAKE_SOURCE_DIR}/src/errno.c ${CMAKE_SOURCE_DIR}/include/akerror.h)
|
||||
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 ${OUTFILES}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_SOURCE_DIR}
|
||||
COMMAND /usr/bin/env bash ${SCRIPT} ${CMAKE_SOURCE_DIR}
|
||||
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
|
||||
)
|
||||
|
||||
set_source_files_properties(src/errno.c PROPERTIES GENERATED TRUE)
|
||||
add_library(akerror SHARED
|
||||
src/error.c
|
||||
${GENERATED_ERRNO_C}
|
||||
)
|
||||
|
||||
add_custom_target(generrno DEPENDS src/errno.c)
|
||||
target_include_directories(akerror PUBLIC
|
||||
$<BUILD_INTERFACE:${GENERATED_DIR}/include>
|
||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
|
||||
)
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
add_library(akerror STATIC
|
||||
src/error.c
|
||||
src/errno.c
|
||||
)
|
||||
add_dependencies(akerror generrno)
|
||||
add_library(akerror::akerror ALIAS akerror)
|
||||
|
||||
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_executable(test_err_unhandled tests/err_unhandled.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)
|
||||
add_test(NAME err_unhandled COMMAND test_err_unhandled)
|
||||
|
||||
# Specify include directories for the library's headers (if applicable)
|
||||
target_include_directories(akerror 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 akerror)
|
||||
target_link_libraries(test_err_cleanup PRIVATE akerror)
|
||||
target_link_libraries(test_err_trace PRIVATE akerror)
|
||||
target_link_libraries(test_err_unhandled PRIVATE akerror)
|
||||
|
||||
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 akerror EXPORT akerror DESTINATION "lib/")
|
||||
install(TARGETS akerror
|
||||
EXPORT akerrorTargets
|
||||
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
@@ -62,13 +116,10 @@ install(TARGETS akerror
|
||||
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
|
||||
)
|
||||
|
||||
install(EXPORT akerror FILE akerrorTargets.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/akerror)
|
||||
|
||||
install(FILES "include/akerror.h" DESTINATION "include/")
|
||||
install(FILES ${GENERATED_AKERROR_H} DESTINATION "include/")
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akerror.pc DESTINATION "lib/pkgconfig/")
|
||||
|
||||
|
||||
install(EXPORT akerror
|
||||
install(EXPORT akerrorTargets
|
||||
FILE akerrorTargets.cmake
|
||||
NAMESPACE akerror::
|
||||
DESTINATION ${akerror_install_cmakedir}
|
||||
|
||||
44
README.md
44
README.md
@@ -2,6 +2,8 @@
|
||||
|
||||
This library provides a TRY/CATCH style exception handling mechanism for C.
|
||||
|
||||

|
||||
|
||||
# Why?
|
||||
|
||||
There is nothing wrong with C as it is. This library does not claim to fix some problem with C.
|
||||
@@ -137,6 +139,14 @@ pkg_check_modules(akerror REQUIRED akerror)
|
||||
target_link_libraries(YOUR_TARGET PRIVATE akerror::akerror)
|
||||
```
|
||||
|
||||
Using this project as a submodule with cmake:
|
||||
|
||||
```cmake
|
||||
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
|
||||
|
||||
target_link_libraries(YOUR_PROJECT PRIVATE akerror::akerror)
|
||||
```
|
||||
|
||||
|
||||
## (Optional) Configuring the logging function
|
||||
|
||||
@@ -222,6 +232,28 @@ ATTEMPT {
|
||||
|
||||
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`.
|
||||
@@ -310,20 +342,24 @@ FAIL_NONZERO_RETURN(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are no
|
||||
|
||||
# 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 `akerr_ErrorContext *` should also be marked with `ERROR_NOIGNORE`.
|
||||
Any function which returns `akerr_ErrorContext *` should also be marked with `AKERROR_NOIGNORE`.
|
||||
|
||||
```c
|
||||
akerr_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
|
||||
|
||||
@@ -6,33 +6,44 @@
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <limits.h>
|
||||
#endif
|
||||
|
||||
#define AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH 1024
|
||||
// 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 256
|
||||
#define AKERR_MAX_ERROR_FNAME_LENGTH PATH_MAX
|
||||
#define AKERR_MAX_ERROR_FUNCTION_LENGTH 128
|
||||
#define AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH 2048
|
||||
#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)
|
||||
#define AKERR_OUTOFBOUNDS (AKERR_LAST_ERRNO_VALUE + 2)
|
||||
#define AKERR_API (AKERR_LAST_ERRNO_VALUE + 3)
|
||||
#define AKERR_ATTRIBUTE (AKERR_LAST_ERRNO_VALUE + 4)
|
||||
#define AKERR_TYPE (AKERR_LAST_ERRNO_VALUE + 5)
|
||||
#define AKERR_KEY (AKERR_LAST_ERRNO_VALUE + 6)
|
||||
#define AKERR_HEAP (AKERR_LAST_ERRNO_VALUE + 7)
|
||||
#define AKERR_INDEX (AKERR_LAST_ERRNO_VALUE + 8)
|
||||
#define AKERR_FORMAT (AKERR_LAST_ERRNO_VALUE + 9)
|
||||
#define AKERR_IO (AKERR_LAST_ERRNO_VALUE + 10)
|
||||
#define AKERR_REGISTRY (AKERR_LAST_ERRNO_VALUE + 11)
|
||||
#define AKERR_VALUE (AKERR_LAST_ERRNO_VALUE + 12)
|
||||
#define AKERR_BEHAVIOR (AKERR_LAST_ERRNO_VALUE + 13)
|
||||
#define AKERR_RELATIONSHIP (AKERR_LAST_ERRNO_VALUE + 14)
|
||||
#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
|
||||
#define AKERR_MAX_ERR_VALUE (AKERR_LAST_ERRNO_VALUE + 14)
|
||||
/* 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
|
||||
@@ -66,8 +77,6 @@ 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;
|
||||
extern akerr_ErrorContext *__akerr_last_prepared;
|
||||
extern int __akerr_last_attempt_id;
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_release_error(akerr_ErrorContext *ptr);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_next_error();
|
||||
@@ -75,6 +84,7 @@ 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);
|
||||
|
||||
@@ -177,10 +187,16 @@ void akerr_init_errno(void);
|
||||
switch ( 0 ) { \
|
||||
case 0: \
|
||||
|
||||
#define DETECT(__err_context, __stmt) \
|
||||
#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 ) { \
|
||||
__err_context->stacktracebufptr += snprintf(__err_context->stacktracebufptr, AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH, "%s:%s:%d: Detected error %d from array (refcount %d)\n", (char *)__FILE__, (char *)__func__, __LINE__, __err_context->arrayid, __err_context->refcount); \
|
||||
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; \
|
||||
@@ -190,6 +206,13 @@ void akerr_init_errno(void);
|
||||
#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 ) { \
|
||||
@@ -222,18 +245,17 @@ void akerr_init_errno(void);
|
||||
__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) \
|
||||
}; \
|
||||
}; \
|
||||
if ( __err_context != NULL ) { \
|
||||
__err_context->stacktracebufptr += snprintf(__err_context->stacktracebufptr, AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH, "%s:%s:%d\n", (char *)__FILE__, (char *)__func__, __LINE__); \
|
||||
if ( __err_context->handled == false && __pass_up == true && __err_context->arrayid > 0 ) { \
|
||||
return __err_context; \
|
||||
} else if ( __err_context->handled == false ) { \
|
||||
LOG_ERROR_WITH_MESSAGE(__err_context, "Unhandled Error"); \
|
||||
akerr_handler_unhandled_error(__err_context); \
|
||||
} \
|
||||
} \
|
||||
FINISH_LOGIC(__err_context, __pass_up) \
|
||||
RELEASE_ERROR(__err_context);
|
||||
|
||||
#define FINISH_NORETURN(__err_context) \
|
||||
@@ -247,12 +269,4 @@ void akerr_init_errno(void);
|
||||
} \
|
||||
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 // _AKERR_H_
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
srcdir=$1
|
||||
rm -f ${srcdir}/src/errno.c
|
||||
echo "#include <akerror.h>" >> ${srcdir}/src/errno.c
|
||||
echo "#include <errno.h>" >> ${srcdir}/src/errno.c
|
||||
echo "void akerr_init_errno(void) {" >> ${srcdir}/src/errno.c
|
||||
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}\");" >> ${srcdir}/src/errno.c ;
|
||||
echo " akerr_name_for_status(${define}, \"${desc}\");" >> ${outdir}/src/errno.c ;
|
||||
done;
|
||||
echo "}" >> ${srcdir}/src/errno.c
|
||||
sed "s/#define AKERR_LAST_ERRNO_VALUE .*/#define AKERR_LAST_ERRNO_VALUE ${maxval}/" ${srcdir}/include/akerror.tmpl.h > ${srcdir}/include/akerror.h
|
||||
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())
|
||||
19
src/error.c
19
src/error.c
@@ -14,6 +14,16 @@ char __AKERR_ERROR_NAMES[AKERR_MAX_ERR_VALUE+1][AKERR_MAX_ERROR_NAME_LENGTH];
|
||||
|
||||
akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR];
|
||||
|
||||
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
|
||||
@@ -51,15 +61,16 @@ void akerr_init()
|
||||
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_HEAP, "Heap 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_REGISTRY, "Registry Error");
|
||||
akerr_name_for_status(AKERR_VALUE, "Value Error");
|
||||
akerr_name_for_status(AKERR_BEHAVIOR, "Behavior 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;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user