Compare commits
1 Commits
tests/expa
...
akmain
| Author | SHA1 | Date | |
|---|---|---|---|
|
350d198015
|
@@ -1,69 +0,0 @@
|
|||||||
name: libakerror CI Build
|
|
||||||
run-name: ${{ gitea.actor }} libakerror test
|
|
||||||
on: [push]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
cmake_build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: echo "Triggered by ${{ gitea.event_name }} from ${{ gitea.repository }}@${{ gitea.ref }}. Building on ${{ runner.os }}."
|
|
||||||
- name: Check out repository code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update -y
|
|
||||||
sudo apt-get install -y cmake gcc moreutils
|
|
||||||
- name: build and install
|
|
||||||
run: |
|
|
||||||
mkdir installdir
|
|
||||||
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=installdir
|
|
||||||
cmake --build build
|
|
||||||
cmake --install build
|
|
||||||
# --output-junit resolves relative to the test dir, so give an absolute
|
|
||||||
# path to land the report in the workspace root for the reporter below.
|
|
||||||
- name: test (JUnit)
|
|
||||||
run: ctest --test-dir build --output-on-failure --output-junit "$(pwd)/ctest-junit.xml"
|
|
||||||
# annotate_only: true skips creating a check run via the Checks API, which
|
|
||||||
# Gitea does not support and 404s on (mikepenz/action-junit-report#23).
|
|
||||||
# Results surface via the job summary instead.
|
|
||||||
- name: publish test results
|
|
||||||
if: always()
|
|
||||||
uses: mikepenz/action-junit-report@v4
|
|
||||||
with:
|
|
||||||
report_paths: 'ctest-junit.xml'
|
|
||||||
annotate_only: true
|
|
||||||
detailed_summary: true
|
|
||||||
include_passed: true
|
|
||||||
fail_on_failure: 'true'
|
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
|
||||||
|
|
||||||
mutation_test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out repository code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update -y
|
|
||||||
sudo apt-get install -y cmake gcc moreutils python3
|
|
||||||
# Verify the tests actually catch bugs: break the library many ways and
|
|
||||||
# confirm the suite fails. Gated on src/error.c (fast, deterministic).
|
|
||||||
# The threshold keeps headroom below the current score for equivalent
|
|
||||||
# mutants; see tests/MUTATION.md. Run the full default target locally for
|
|
||||||
# deeper (slower) coverage including the macro header.
|
|
||||||
- name: mutation testing
|
|
||||||
run: |
|
|
||||||
python3 scripts/mutation_test.py --target src/error.c --junit mutation-junit.xml --threshold 65
|
|
||||||
# Publish even when the threshold gate fails, so survivors are visible.
|
|
||||||
# Display-only (fail_on_failure: false); the --threshold above is the gate.
|
|
||||||
# annotate_only avoids the Checks API 404 on Gitea (see note above).
|
|
||||||
- name: publish mutation results
|
|
||||||
if: always()
|
|
||||||
uses: mikepenz/action-junit-report@v4
|
|
||||||
with:
|
|
||||||
report_paths: 'mutation-junit.xml'
|
|
||||||
annotate_only: true
|
|
||||||
detailed_summary: true
|
|
||||||
include_passed: true
|
|
||||||
fail_on_failure: 'false'
|
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
include/akerror.h
|
|
||||||
build/
|
|
||||||
*-junit.xml
|
|
||||||
123
CMakeLists.txt
123
CMakeLists.txt
@@ -3,111 +3,57 @@ project(akerror LANGUAGES C)
|
|||||||
|
|
||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
include(CMakePackageConfigHelpers)
|
include(CMakePackageConfigHelpers)
|
||||||
include(CTest)
|
|
||||||
|
|
||||||
set(AKERR_USE_STDLIB 1 CACHE BOOL "Use the C standard library")
|
set(AKERR_USE_STDLIB 1 CACHE BOOL "Use the C standard library")
|
||||||
set(akerror_install_cmakedir "${CMAKE_INSTALL_LIBDIR}/cmake/akerror")
|
set(akerror_install_cmakedir "${CMAKE_INSTALL_LIBDIR}/cmake/akerror")
|
||||||
|
|
||||||
set(SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/scripts/generrno.sh)
|
set(SCRIPT ${CMAKE_SOURCE_DIR}/scripts/generrno.sh)
|
||||||
set(INFILE ${CMAKE_CURRENT_SOURCE_DIR}/include/akerror.tmpl.h)
|
set(INFILE ${CMAKE_SOURCE_DIR}/include/akerror.tmpl.h)
|
||||||
|
set(OUTFILES ${CMAKE_SOURCE_DIR}/src/errno.c ${CMAKE_SOURCE_DIR}/include/akerror.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(
|
add_custom_command(
|
||||||
OUTPUT ${GENERATED_ERRNO_C} ${GENERATED_AKERROR_H}
|
OUTPUT ${OUTFILES}
|
||||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${GENERATED_DIR}
|
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_SOURCE_DIR}
|
||||||
COMMAND /usr/bin/env bash
|
COMMAND /usr/bin/env bash ${SCRIPT} ${CMAKE_SOURCE_DIR}
|
||||||
${SCRIPT}
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}
|
|
||||||
${GENERATED_DIR}
|
|
||||||
DEPENDS ${SCRIPT} ${INFILE}
|
DEPENDS ${SCRIPT} ${INFILE}
|
||||||
VERBATIM
|
VERBATIM
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(akerror SHARED
|
set_source_files_properties(src/errno.c PROPERTIES GENERATED TRUE)
|
||||||
src/error.c
|
|
||||||
${GENERATED_ERRNO_C}
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(akerror PUBLIC
|
add_custom_target(generrno DEPENDS src/errno.c)
|
||||||
$<BUILD_INTERFACE:${GENERATED_DIR}/include>
|
|
||||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
|
|
||||||
)
|
|
||||||
|
|
||||||
find_package(PkgConfig REQUIRED)
|
find_package(PkgConfig REQUIRED)
|
||||||
add_library(akerror::akerror ALIAS akerror)
|
add_library(akerror STATIC
|
||||||
|
src/error.c
|
||||||
|
src/errno.c
|
||||||
|
)
|
||||||
|
add_dependencies(akerror generrno)
|
||||||
|
|
||||||
target_compile_definitions(akerror
|
target_compile_definitions(akerror
|
||||||
PUBLIC AKERR_USE_STDLIB=${AKERR_USE_STDLIB}
|
PUBLIC AKERR_USE_STDLIB=${AKERR_USE_STDLIB}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Each test is one source file in tests/ built into test_<name> and registered
|
add_executable(test_err_catch tests/err_catch.c)
|
||||||
# as CTest <name>. Tests expected to abort (unhandled error / contract
|
add_executable(test_err_cleanup tests/err_cleanup.c)
|
||||||
# violation) go in AKERR_WILL_FAIL_TESTS; all others must exit 0.
|
add_executable(test_err_trace tests/err_trace.c)
|
||||||
set(AKERR_TESTS
|
add_executable(test_err_unhandled tests/err_unhandled.c)
|
||||||
err_catch
|
add_test(NAME err_catch COMMAND test_err_catch)
|
||||||
err_cleanup
|
add_test(NAME err_cleanup COMMAND test_err_cleanup)
|
||||||
err_trace
|
add_test(NAME err_trace COMMAND test_err_trace)
|
||||||
err_improper_closure
|
add_test(NAME err_unhandled COMMAND test_err_unhandled)
|
||||||
err_success
|
|
||||||
err_pool_refcount
|
# Specify include directories for the library's headers (if applicable)
|
||||||
err_handle_default
|
target_include_directories(akerror PUBLIC
|
||||||
err_handle_group
|
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||||
err_handle_dispatch
|
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/>
|
||||||
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)
|
||||||
set(AKERR_WILL_FAIL_TESTS
|
target_link_libraries(test_err_cleanup PRIVATE akerror)
|
||||||
err_trace
|
target_link_libraries(test_err_trace PRIVATE akerror)
|
||||||
err_improper_closure
|
target_link_libraries(test_err_unhandled PRIVATE akerror)
|
||||||
)
|
|
||||||
|
|
||||||
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}")
|
set(main_lib_dest "lib/my_library-${MY_LIBRARY_VERSION}")
|
||||||
|
install(TARGETS akerror EXPORT akerror DESTINATION "lib/")
|
||||||
install(TARGETS akerror
|
install(TARGETS akerror
|
||||||
EXPORT akerrorTargets
|
EXPORT akerrorTargets
|
||||||
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||||
@@ -116,10 +62,13 @@ install(TARGETS akerror
|
|||||||
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
|
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
|
||||||
)
|
)
|
||||||
|
|
||||||
install(FILES ${GENERATED_AKERROR_H} DESTINATION "include/")
|
install(EXPORT akerror FILE akerrorTargets.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/akerror)
|
||||||
|
|
||||||
|
install(FILES "include/akerror.h" DESTINATION "include/")
|
||||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akerror.pc DESTINATION "lib/pkgconfig/")
|
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akerror.pc DESTINATION "lib/pkgconfig/")
|
||||||
|
|
||||||
install(EXPORT akerrorTargets
|
|
||||||
|
install(EXPORT akerror
|
||||||
FILE akerrorTargets.cmake
|
FILE akerrorTargets.cmake
|
||||||
NAMESPACE akerror::
|
NAMESPACE akerror::
|
||||||
DESTINATION ${akerror_install_cmakedir}
|
DESTINATION ${akerror_install_cmakedir}
|
||||||
|
|||||||
44
README.md
44
README.md
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
This library provides a TRY/CATCH style exception handling mechanism for C.
|
This library provides a TRY/CATCH style exception handling mechanism for C.
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
# Why?
|
# Why?
|
||||||
|
|
||||||
There is nothing wrong with C as it is. This library does not claim to fix some problem with C.
|
There is nothing wrong with C as it is. This library does not claim to fix some problem with C.
|
||||||
@@ -139,14 +137,6 @@ pkg_check_modules(akerror REQUIRED akerror)
|
|||||||
target_link_libraries(YOUR_TARGET PRIVATE akerror::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
|
## (Optional) Configuring the logging function
|
||||||
|
|
||||||
@@ -232,28 +222,6 @@ ATTEMPT {
|
|||||||
|
|
||||||
When either of these two macros are used, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
|
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
|
# 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`.
|
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`.
|
||||||
@@ -342,24 +310,20 @@ FAIL_NONZERO_RETURN(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are no
|
|||||||
|
|
||||||
# Uncaught errors
|
# 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
|
## Ensuring that all error codes are captured
|
||||||
|
|
||||||
Any function which returns `akerr_ErrorContext *` should also be marked with `AKERROR_NOIGNORE`.
|
Any function which returns `akerr_ErrorContext *` should also be marked with `ERROR_NOIGNORE`.
|
||||||
|
|
||||||
```c
|
```c
|
||||||
akerr_ErrorContext AKERROR_NOIGNORE *f(...);
|
akerr_ErrorContext ERROR_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.
|
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 `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`.
|
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`.
|
||||||
|
|
||||||
```c
|
```c
|
||||||
#define AKERROR_NOIGNORE __attribute__((warn_unused_result))
|
#define ERROR_NOIGNORE __attribute__((warn_unused_result))
|
||||||
```
|
```
|
||||||
|
|
||||||
## Stack Traces
|
## Stack Traces
|
||||||
|
|||||||
@@ -6,44 +6,33 @@
|
|||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <limits.h>
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// FIXME: This is huge now. It used to be 1000 bytes, then I wanted to report errors
|
#define AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH 1024
|
||||||
// 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_NAME_LENGTH 64
|
||||||
#define AKERR_MAX_ERROR_FNAME_LENGTH PATH_MAX
|
#define AKERR_MAX_ERROR_FNAME_LENGTH 256
|
||||||
#define AKERR_MAX_ERROR_FUNCTION_LENGTH 128
|
#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_MAX_ERROR_STACKTRACE_BUF_LENGTH 2048
|
||||||
|
|
||||||
#define AKERR_LAST_ERRNO_VALUE AKERR_LAST_ERRNO_VALUE_SED
|
#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_NULLPOINTER (AKERR_LAST_ERRNO_VALUE + 1)
|
||||||
#define AKERR_OUTOFBOUNDS (AKERR_LAST_ERRNO_VALUE + 2) /** Attempt to access a datastructure outside of bounds */
|
#define AKERR_OUTOFBOUNDS (AKERR_LAST_ERRNO_VALUE + 2)
|
||||||
#define AKERR_API (AKERR_LAST_ERRNO_VALUE + 3) /** An otherwise unspecified API contract has been violated */
|
#define AKERR_API (AKERR_LAST_ERRNO_VALUE + 3)
|
||||||
#define AKERR_ATTRIBUTE (AKERR_LAST_ERRNO_VALUE + 4) /** Relates to accessing of attributes on objects */
|
#define AKERR_ATTRIBUTE (AKERR_LAST_ERRNO_VALUE + 4)
|
||||||
#define AKERR_TYPE (AKERR_LAST_ERRNO_VALUE + 5) /** An object had the incorrect type */
|
#define AKERR_TYPE (AKERR_LAST_ERRNO_VALUE + 5)
|
||||||
#define AKERR_KEY (AKERR_LAST_ERRNO_VALUE + 6) /** A key was either invalid for or not present in a map */
|
#define AKERR_KEY (AKERR_LAST_ERRNO_VALUE + 6)
|
||||||
#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_HEAP (AKERR_LAST_ERRNO_VALUE + 7)
|
||||||
#define AKERR_FORMAT (AKERR_LAST_ERRNO_VALUE + 9) /** An error occurred in the formatting of an object (usually a string) */
|
#define AKERR_INDEX (AKERR_LAST_ERRNO_VALUE + 8)
|
||||||
#define AKERR_IO (AKERR_LAST_ERRNO_VALUE + 10) /** An unspecified IO error occurred. */
|
#define AKERR_FORMAT (AKERR_LAST_ERRNO_VALUE + 9)
|
||||||
#define AKERR_VALUE (AKERR_LAST_ERRNO_VALUE + 11) /** A provided value was invalid */
|
#define AKERR_IO (AKERR_LAST_ERRNO_VALUE + 10)
|
||||||
#define AKERR_RELATIONSHIP (AKERR_LAST_ERRNO_VALUE + 12) /** An error occurred in establishing, maintaining or severing a relationship between two objects */
|
#define AKERR_REGISTRY (AKERR_LAST_ERRNO_VALUE + 11)
|
||||||
#define AKERR_EOF (AKERR_LAST_ERRNO_VALUE + 13) /** The end of a stream or file has been encountered */
|
#define AKERR_VALUE (AKERR_LAST_ERRNO_VALUE + 12)
|
||||||
#define AKERR_CIRCULAR_REFERENCE (AKERR_LAST_ERRNO_VALUE + 14) /** Indicates that a circular reference has been found in a linked list */
|
#define AKERR_BEHAVIOR (AKERR_LAST_ERRNO_VALUE + 13)
|
||||||
#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_RELATIONSHIP (AKERR_LAST_ERRNO_VALUE + 14)
|
||||||
#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
|
#ifndef AKERR_MAX_ERR_VALUE
|
||||||
/* Must be >= the highest AKERR_* offset above (AKERR_BADEXC, +17) so every
|
#define AKERR_MAX_ERR_VALUE (AKERR_LAST_ERRNO_VALUE + 14)
|
||||||
* 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
|
#elif AKERR_MAX_ERR_VALUE < 256
|
||||||
#error user-defined AKERR_MAX_ERR_VALUE must be >= 256
|
#error user-defined AKERR_MAX_ERR_VALUE must be >= 256
|
||||||
#endif
|
#endif
|
||||||
@@ -66,6 +55,7 @@ typedef struct
|
|||||||
bool reported;
|
bool reported;
|
||||||
char stacktracebuf[AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH];
|
char stacktracebuf[AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH];
|
||||||
char *stacktracebufptr;
|
char *stacktracebufptr;
|
||||||
|
int stacktracebufremaining;
|
||||||
} akerr_ErrorContext;
|
} akerr_ErrorContext;
|
||||||
|
|
||||||
#define AKERR_NOIGNORE __attribute__((warn_unused_result))
|
#define AKERR_NOIGNORE __attribute__((warn_unused_result))
|
||||||
@@ -78,13 +68,13 @@ extern akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
|
|||||||
extern akerr_ErrorLogFunction akerr_log_method;
|
extern akerr_ErrorLogFunction akerr_log_method;
|
||||||
extern akerr_ErrorContext *__akerr_last_ignored;
|
extern akerr_ErrorContext *__akerr_last_ignored;
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akerr_user_main(int argc, char **argv);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_release_error(akerr_ErrorContext *ptr);
|
akerr_ErrorContext AKERR_NOIGNORE *akerr_release_error(akerr_ErrorContext *ptr);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_next_error();
|
akerr_ErrorContext AKERR_NOIGNORE *akerr_next_error();
|
||||||
char *akerr_name_for_status(int status, char *name);
|
char *akerr_name_for_status(int status, char *name);
|
||||||
void akerr_init();
|
void akerr_init();
|
||||||
void akerr_default_handler_unhandled_error(akerr_ErrorContext *ptr);
|
void akerr_default_handler_unhandled_error(akerr_ErrorContext *ptr);
|
||||||
void akerr_default_logger(const char *f, ...);
|
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 */
|
/* defined in src/errno.c which is built dynamically at build time from system errno definitions */
|
||||||
void akerr_init_errno(void);
|
void akerr_init_errno(void);
|
||||||
|
|
||||||
@@ -187,16 +177,10 @@ void akerr_init_errno(void);
|
|||||||
switch ( 0 ) { \
|
switch ( 0 ) { \
|
||||||
case 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) \
|
#define DETECT(__err_context, __stmt) \
|
||||||
VALID(__err_context, __stmt); \
|
__stmt; \
|
||||||
if ( __err_context != NULL ) { \
|
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 ) { \
|
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__); \
|
__err_context->stacktracebufptr += snprintf(__err_context->stacktracebufptr, AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH, "%s:%s:%d\n", (char *)__FILE__, (char *)__func__, __LINE__); \
|
||||||
break; \
|
break; \
|
||||||
@@ -206,13 +190,6 @@ void akerr_init_errno(void);
|
|||||||
#define CATCH(__err_context, __stmt) \
|
#define CATCH(__err_context, __stmt) \
|
||||||
DETECT(__err_context, __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) \
|
#define IGNORE(__stmt) \
|
||||||
__akerr_last_ignored = __stmt; \
|
__akerr_last_ignored = __stmt; \
|
||||||
if ( __akerr_last_ignored != NULL ) { \
|
if ( __akerr_last_ignored != NULL ) { \
|
||||||
@@ -245,17 +222,15 @@ void akerr_init_errno(void);
|
|||||||
__err_context->stacktracebufptr = (char *)&__err_context->stacktracebuf; \
|
__err_context->stacktracebufptr = (char *)&__err_context->stacktracebuf; \
|
||||||
__err_context->handled = true;
|
__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) \
|
#define FINISH(__err_context, __pass_up) \
|
||||||
}; \
|
}; \
|
||||||
}; \
|
}; \
|
||||||
FINISH_LOGIC(__err_context, __pass_up) \
|
if ( __err_context != NULL ) { \
|
||||||
|
if ( __err_context->handled == false && __pass_up == true ) { \
|
||||||
|
__err_context->stacktracebufptr += snprintf(__err_context->stacktracebufptr, AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH, "%s:%s:%d\n", (char *)__FILE__, (char *)__func__, __LINE__); \
|
||||||
|
return __err_context; \
|
||||||
|
} \
|
||||||
|
} \
|
||||||
RELEASE_ERROR(__err_context);
|
RELEASE_ERROR(__err_context);
|
||||||
|
|
||||||
#define FINISH_NORETURN(__err_context) \
|
#define FINISH_NORETURN(__err_context) \
|
||||||
@@ -269,4 +244,12 @@ void akerr_init_errno(void);
|
|||||||
} \
|
} \
|
||||||
RELEASE_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 // _AKERR_H_
|
#endif // _AKERR_H_
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
srcdir=$1
|
srcdir=$1
|
||||||
outdir=$2
|
rm -f ${srcdir}/src/errno.c
|
||||||
|
echo "#include <akerror.h>" >> ${srcdir}/src/errno.c
|
||||||
mkdir -p ${outdir}/src
|
echo "#include <errno.h>" >> ${srcdir}/src/errno.c
|
||||||
mkdir -p ${outdir}/include
|
echo "void akerr_init_errno(void) {" >> ${srcdir}/src/errno.c
|
||||||
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)
|
maxval=$(errno --list | cut -d ' ' -f 2 | sort -g | tail -n 1)
|
||||||
errno --list | while read LINE; do
|
errno --list | while read LINE; do
|
||||||
define=$(echo "$LINE" | cut -d ' ' -f 1);
|
define=$(echo "$LINE" | cut -d ' ' -f 1);
|
||||||
value=$(echo "$LINE" | cut -d ' ' -f 2);
|
value=$(echo "$LINE" | cut -d ' ' -f 2);
|
||||||
desc=$(echo "$LINE" | cut -d ' ' -f 3-);
|
desc=$(echo "$LINE" | cut -d ' ' -f 3-);
|
||||||
echo " akerr_name_for_status(${define}, \"${desc}\");" >> ${outdir}/src/errno.c ;
|
echo " akerr_name_for_status(${define}, \"${desc}\");" >> ${srcdir}/src/errno.c ;
|
||||||
done;
|
done;
|
||||||
echo "}" >> ${outdir}/src/errno.c
|
echo "}" >> ${srcdir}/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
|
sed "s/#define AKERR_LAST_ERRNO_VALUE .*/#define AKERR_LAST_ERRNO_VALUE ${maxval}/" ${srcdir}/include/akerror.tmpl.h > ${srcdir}/include/akerror.h
|
||||||
|
|||||||
@@ -1,436 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Mutation testing harness for libakerror.
|
|
||||||
|
|
||||||
Mutation testing measures how good the test suite is at catching bugs. It works
|
|
||||||
by making many small, deliberate breakages ("mutants") to the library source --
|
|
||||||
flipping a comparison, deleting a statement, swapping true/false -- and then
|
|
||||||
running the whole CTest suite against each one. If the tests fail, the mutant is
|
|
||||||
"killed" (good: the tests noticed the bug). If the tests still pass, the mutant
|
|
||||||
"survived" (bad: a real bug of that shape would slip through unnoticed).
|
|
||||||
|
|
||||||
The mutation score is killed / (killed + survived). Surviving mutants are printed
|
|
||||||
with file:line and the exact change so they can be turned into new test cases.
|
|
||||||
|
|
||||||
This harness has no third-party dependencies (Python stdlib + the project's
|
|
||||||
normal cmake/ctest toolchain). It never mutates the real working tree: it copies
|
|
||||||
the repo to a scratch directory and mutates there.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
scripts/mutation_test.py [options]
|
|
||||||
|
|
||||||
--source-root DIR repo root to copy (default: parent of this script's dir)
|
|
||||||
--target FILE source file to mutate, relative to root; repeatable.
|
|
||||||
Default: src/error.c and include/akerror.tmpl.h
|
|
||||||
--work DIR scratch dir for the mutated copy (default: a temp dir)
|
|
||||||
--timeout SECONDS per-suite ctest timeout (default: 120)
|
|
||||||
--threshold PCT exit non-zero if mutation score < PCT (default: 0 = off)
|
|
||||||
--list only list the mutants that would be run, then exit
|
|
||||||
--keep keep the scratch working copy on exit (for debugging)
|
|
||||||
-j N (reserved) currently runs sequentially
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
|
||||||
# Mutation operators
|
|
||||||
#
|
|
||||||
# Each operator yields zero or more (start, end, replacement) edits for a single
|
|
||||||
# line of source. The driver applies exactly one edit per mutant so every mutant
|
|
||||||
# differs from the original by one localized change.
|
|
||||||
# --------------------------------------------------------------------------- #
|
|
||||||
|
|
||||||
# Relational operator replacement: map each operator to the alternatives that
|
|
||||||
# meaningfully change behaviour (not merely the strict negation).
|
|
||||||
_REL = {
|
|
||||||
"==": ["!="],
|
|
||||||
"!=": ["=="],
|
|
||||||
"<=": ["<", "=="],
|
|
||||||
">=": [">", "=="],
|
|
||||||
"<": ["<=", ">"],
|
|
||||||
">": [">=", "<"],
|
|
||||||
}
|
|
||||||
# Match a relational operator that is NOT part of ->, <<, >>, =>, <=, >=, ==, !=
|
|
||||||
# unless we intend it. We tokenize the two-char operators first, then single.
|
|
||||||
_REL_TWO = re.compile(r"(==|!=|<=|>=)")
|
|
||||||
_REL_ONE = re.compile(r"(?<![-<>=!+])([<>])(?![=<>])")
|
|
||||||
|
|
||||||
_LOGICAL = {"&&": "||", "||": "&&"}
|
|
||||||
_LOG_RE = re.compile(r"(&&|\|\|)")
|
|
||||||
|
|
||||||
_BOOL = {"true": "false", "false": "true"}
|
|
||||||
_BOOL_RE = re.compile(r"\b(true|false)\b")
|
|
||||||
|
|
||||||
# Arithmetic / compound-assignment on whitespace-delimited operands only, to
|
|
||||||
# avoid touching ++, --, ->, unary signs, or pointer/format punctuation.
|
|
||||||
_ARITH_RE = re.compile(r"(?<=\s)([+\-])(?=\s)")
|
|
||||||
_ARITH = {"+": "-", "-": "+"}
|
|
||||||
_COMPOUND_RE = re.compile(r"(\+=|-=)")
|
|
||||||
_COMPOUND = {"+=": "-=", "-=": "+="}
|
|
||||||
|
|
||||||
# Integer literal replacement: 0 <-> 1 (word-bounded, not inside identifiers or
|
|
||||||
# larger numbers, not a float).
|
|
||||||
_INT_RE = re.compile(r"(?<![\w.])([01])(?![\w.])")
|
|
||||||
_INT = {"0": "1", "1": "0"}
|
|
||||||
|
|
||||||
|
|
||||||
def _op_edits(line):
|
|
||||||
"""Yield (tag, start, end, replacement) for every candidate mutation."""
|
|
||||||
# Relational (two-char first so we don't split them with the one-char pass)
|
|
||||||
for m in _REL_TWO.finditer(line):
|
|
||||||
for alt in _REL[m.group(1)]:
|
|
||||||
yield ("ROR", m.start(1), m.end(1), alt)
|
|
||||||
for m in _REL_ONE.finditer(line):
|
|
||||||
for alt in _REL[m.group(1)]:
|
|
||||||
yield ("ROR", m.start(1), m.end(1), alt)
|
|
||||||
for m in _LOG_RE.finditer(line):
|
|
||||||
yield ("LCR", m.start(1), m.end(1), _LOGICAL[m.group(1)])
|
|
||||||
for m in _BOOL_RE.finditer(line):
|
|
||||||
yield ("BCR", m.start(1), m.end(1), _BOOL[m.group(1)])
|
|
||||||
for m in _COMPOUND_RE.finditer(line):
|
|
||||||
yield ("AOR", m.start(1), m.end(1), _COMPOUND[m.group(1)])
|
|
||||||
for m in _ARITH_RE.finditer(line):
|
|
||||||
yield ("AOR", m.start(1), m.end(1), _ARITH[m.group(1)])
|
|
||||||
for m in _INT_RE.finditer(line):
|
|
||||||
yield ("ICR", m.start(1), m.end(1), _INT[m.group(1)])
|
|
||||||
|
|
||||||
|
|
||||||
# Statement-deletion: neutralize a whole statement. We only delete statements
|
|
||||||
# that are safe to drop without guaranteeing a compile error, so a surviving
|
|
||||||
# deletion is a genuine test gap rather than compiler noise.
|
|
||||||
_STMT_DELETABLE = re.compile(
|
|
||||||
r"""^\s*(
|
|
||||||
break |
|
|
||||||
return\b[^;]* |
|
|
||||||
[A-Za-z_][-\w>().\[\]* ]*\s*=\s*[^;]* | # assignments
|
|
||||||
[A-Za-z_][\w]*\s*\([^;]*\) # bare function calls
|
|
||||||
)\s*;\s*(\\?)\s*$""",
|
|
||||||
re.VERBOSE,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
|
||||||
# Deciding which lines are eligible to mutate
|
|
||||||
# --------------------------------------------------------------------------- #
|
|
||||||
|
|
||||||
# Skip preprocessor control and the block of constant/error-code #defines in the
|
|
||||||
# template header: mutating buffer sizes or renumbering error codes produces
|
|
||||||
# equivalent or uninteresting mutants that swamp the signal.
|
|
||||||
_SKIP_LINE = re.compile(
|
|
||||||
r"""^\s*(
|
|
||||||
\#\s*(include|ifn?def|ifdef|if|elif|else|endif|error|pragma|undef) |
|
|
||||||
\#\s*define\s+AKERR_(MAX|LAST|NULLPOINTER|OUTOFBOUNDS|API|ATTRIBUTE|
|
|
||||||
TYPE|KEY|INDEX|FORMAT|IO|VALUE|RELATIONSHIP|EOF|CIRCULAR_REFERENCE|
|
|
||||||
ITERATOR_BREAK|NOT_IMPLEMENTED|BADEXC|NOIGNORE|USE_STDLIB)\b |
|
|
||||||
\* | // # comment bodies / line comments
|
|
||||||
)""",
|
|
||||||
re.VERBOSE,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _is_comment_or_blank(line):
|
|
||||||
s = line.strip()
|
|
||||||
return (not s) or s.startswith("//") or s.startswith("/*") or s.startswith("*")
|
|
||||||
|
|
||||||
|
|
||||||
def eligible(line):
|
|
||||||
if _is_comment_or_blank(line):
|
|
||||||
return False
|
|
||||||
if _SKIP_LINE.match(line):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class Mutant:
|
|
||||||
__slots__ = ("path", "lineno", "op", "before", "after", "col")
|
|
||||||
|
|
||||||
def __init__(self, path, lineno, op, before, after, col):
|
|
||||||
self.path = path
|
|
||||||
self.lineno = lineno
|
|
||||||
self.op = op
|
|
||||||
self.before = before
|
|
||||||
self.after = after
|
|
||||||
self.col = col
|
|
||||||
|
|
||||||
def describe(self):
|
|
||||||
return (f"{self.path}:{self.lineno} [{self.op}] "
|
|
||||||
f"col{self.col}: {self.before.strip()} -> {self.after.strip()}")
|
|
||||||
|
|
||||||
|
|
||||||
def generate_mutants(root, rel_target):
|
|
||||||
"""Enumerate all mutants for one target file."""
|
|
||||||
abspath = os.path.join(root, rel_target)
|
|
||||||
with open(abspath, "r") as fh:
|
|
||||||
lines = fh.readlines()
|
|
||||||
|
|
||||||
mutants = []
|
|
||||||
for i, line in enumerate(lines, start=1):
|
|
||||||
if not eligible(line):
|
|
||||||
continue
|
|
||||||
# substitution operators
|
|
||||||
seen = set()
|
|
||||||
for tag, s, e, repl in _op_edits(line):
|
|
||||||
key = (s, e, repl)
|
|
||||||
if key in seen:
|
|
||||||
continue
|
|
||||||
seen.add(key)
|
|
||||||
mutated = line[:s] + repl + line[e:]
|
|
||||||
if mutated == line:
|
|
||||||
continue
|
|
||||||
mutants.append(Mutant(rel_target, i, tag, line, mutated, s))
|
|
||||||
# statement deletion
|
|
||||||
m = _STMT_DELETABLE.match(line)
|
|
||||||
if m:
|
|
||||||
indent = line[: len(line) - len(line.lstrip())]
|
|
||||||
cont = "\\" if line.rstrip().endswith("\\") else ""
|
|
||||||
deleted = f"{indent}/* mutant: deleted */ {cont}\n" if cont else f"{indent};\n"
|
|
||||||
mutants.append(Mutant(rel_target, i, "SDL", line, deleted, 0))
|
|
||||||
return mutants
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
|
||||||
# Build / test orchestration against a scratch copy
|
|
||||||
# --------------------------------------------------------------------------- #
|
|
||||||
|
|
||||||
class Runner:
|
|
||||||
def __init__(self, work, timeout):
|
|
||||||
self.work = work
|
|
||||||
self.build = os.path.join(work, "build")
|
|
||||||
self.timeout = timeout
|
|
||||||
|
|
||||||
def _run(self, cmd, timeout=None):
|
|
||||||
return subprocess.run(
|
|
||||||
cmd, cwd=self.work, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
||||||
timeout=timeout,
|
|
||||||
)
|
|
||||||
|
|
||||||
def configure(self):
|
|
||||||
r = self._run(["cmake", "-S", ".", "-B", "build"], timeout=self.timeout)
|
|
||||||
return r.returncode == 0, r.stdout
|
|
||||||
|
|
||||||
def build_and_test(self):
|
|
||||||
"""Return ('killed-compile' | 'killed-test' | 'killed-timeout' | 'survived')."""
|
|
||||||
try:
|
|
||||||
b = self._run(["cmake", "--build", "build"], timeout=self.timeout)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return "killed-timeout"
|
|
||||||
if b.returncode != 0:
|
|
||||||
return "killed-compile"
|
|
||||||
try:
|
|
||||||
t = subprocess.run(
|
|
||||||
["ctest", "--test-dir", "build", "--output-on-failure",
|
|
||||||
"--stop-on-failure"],
|
|
||||||
cwd=self.work, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
||||||
timeout=self.timeout,
|
|
||||||
)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return "killed-timeout"
|
|
||||||
return "survived" if t.returncode == 0 else "killed-test"
|
|
||||||
|
|
||||||
|
|
||||||
def _xml_escape(s):
|
|
||||||
return (s.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
||||||
.replace('"', """))
|
|
||||||
|
|
||||||
|
|
||||||
def write_junit(path, records, targets):
|
|
||||||
"""Write a JUnit XML report. One <testcase> per mutant; a surviving mutant
|
|
||||||
is a <failure> (test-suite gap), a killed mutant is a passing case."""
|
|
||||||
by_file = {t: [] for t in targets}
|
|
||||||
for m, result in records:
|
|
||||||
by_file.setdefault(m.path, []).append((m, result))
|
|
||||||
|
|
||||||
total = len(records)
|
|
||||||
total_fail = sum(1 for _, r in records if r == "survived")
|
|
||||||
out = ['<?xml version="1.0" encoding="UTF-8"?>',
|
|
||||||
f'<testsuites name="mutation" tests="{total}" failures="{total_fail}">']
|
|
||||||
for f, items in by_file.items():
|
|
||||||
if not items:
|
|
||||||
continue
|
|
||||||
fails = sum(1 for _, r in items if r == "survived")
|
|
||||||
out.append(f' <testsuite name="mutation:{_xml_escape(f)}" '
|
|
||||||
f'tests="{len(items)}" failures="{fails}">')
|
|
||||||
for m, result in items:
|
|
||||||
name = _xml_escape(m.describe())
|
|
||||||
cls = "mutation." + _xml_escape(m.path)
|
|
||||||
if result == "survived":
|
|
||||||
detail = _xml_escape(f"{m.before.strip()} -> {m.after.strip()}")
|
|
||||||
out.append(f' <testcase name="{name}" classname="{cls}" time="0">')
|
|
||||||
out.append(f' <failure message="survived mutant '
|
|
||||||
f'({_xml_escape(m.op)} at {_xml_escape(m.path)}:'
|
|
||||||
f'{m.lineno})">{detail}</failure>')
|
|
||||||
out.append(' </testcase>')
|
|
||||||
else:
|
|
||||||
out.append(f' <testcase name="{name}" classname="{cls}" '
|
|
||||||
f'time="0"><system-out>{_xml_escape(result)}'
|
|
||||||
'</system-out></testcase>')
|
|
||||||
out.append(' </testsuite>')
|
|
||||||
out.append('</testsuites>')
|
|
||||||
with open(path, "w") as fh:
|
|
||||||
fh.write("\n".join(out) + "\n")
|
|
||||||
|
|
||||||
|
|
||||||
def copy_tree(src, dst):
|
|
||||||
ignore = shutil.ignore_patterns("build", ".git", "*.o", "*.so", "*~",
|
|
||||||
"#*#", "*.iso", "*.png")
|
|
||||||
shutil.copytree(src, dst, ignore=ignore, symlinks=True)
|
|
||||||
|
|
||||||
|
|
||||||
def read_lines(path):
|
|
||||||
with open(path) as fh:
|
|
||||||
return fh.readlines()
|
|
||||||
|
|
||||||
|
|
||||||
def write_lines(path, lines):
|
|
||||||
with open(path, "w") as fh:
|
|
||||||
fh.writelines(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Line-buffer stdout so progress is visible live under CI / the cmake target.
|
|
||||||
try:
|
|
||||||
sys.stdout.reconfigure(line_buffering=True)
|
|
||||||
except (AttributeError, ValueError):
|
|
||||||
pass
|
|
||||||
here = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
default_root = os.path.dirname(here)
|
|
||||||
|
|
||||||
ap = argparse.ArgumentParser(description="Mutation testing for libakerror")
|
|
||||||
ap.add_argument("--source-root", default=default_root)
|
|
||||||
ap.add_argument("--target", action="append", default=None)
|
|
||||||
ap.add_argument("--work", default=None)
|
|
||||||
ap.add_argument("--timeout", type=int, default=120)
|
|
||||||
ap.add_argument("--threshold", type=float, default=0.0)
|
|
||||||
ap.add_argument("--junit", default=None,
|
|
||||||
help="write a JUnit XML report to this path")
|
|
||||||
ap.add_argument("--max-mutants", type=int, default=0,
|
|
||||||
help="cap the run at N evenly-sampled mutants (0 = all)")
|
|
||||||
ap.add_argument("--list", action="store_true")
|
|
||||||
ap.add_argument("--keep", action="store_true")
|
|
||||||
ap.add_argument("-j", type=int, default=1)
|
|
||||||
args = ap.parse_args()
|
|
||||||
|
|
||||||
root = os.path.abspath(args.source_root)
|
|
||||||
targets = args.target or ["src/error.c", "include/akerror.tmpl.h"]
|
|
||||||
|
|
||||||
# Enumerate mutants from the pristine sources.
|
|
||||||
all_mutants = []
|
|
||||||
for t in targets:
|
|
||||||
all_mutants.extend(generate_mutants(root, t))
|
|
||||||
|
|
||||||
print(f"Generated {len(all_mutants)} mutants across {len(targets)} file(s):")
|
|
||||||
for t in targets:
|
|
||||||
n = sum(1 for m in all_mutants if m.path == t)
|
|
||||||
print(f" {t}: {n}")
|
|
||||||
|
|
||||||
# Optional even-strided sampling to bound run time (CI / smoke tests).
|
|
||||||
if args.max_mutants and len(all_mutants) > args.max_mutants:
|
|
||||||
step = len(all_mutants) / args.max_mutants
|
|
||||||
sampled = [all_mutants[int(i * step)] for i in range(args.max_mutants)]
|
|
||||||
print(f"Sampling {len(sampled)} of {len(all_mutants)} mutants "
|
|
||||||
f"(--max-mutants {args.max_mutants}).")
|
|
||||||
all_mutants = sampled
|
|
||||||
|
|
||||||
if args.list:
|
|
||||||
for m in all_mutants:
|
|
||||||
print(" " + m.describe())
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if not all_mutants:
|
|
||||||
print("No mutants generated; nothing to do.")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# Scratch working copy.
|
|
||||||
work_parent = args.work or tempfile.mkdtemp(prefix="akerr_mut_")
|
|
||||||
work = os.path.join(work_parent, "src") if args.work else work_parent
|
|
||||||
if os.path.exists(work):
|
|
||||||
shutil.rmtree(work)
|
|
||||||
print(f"\nCopying sources to scratch dir: {work}")
|
|
||||||
copy_tree(root, work)
|
|
||||||
|
|
||||||
runner = Runner(work, args.timeout)
|
|
||||||
|
|
||||||
print("Configuring baseline ...")
|
|
||||||
ok, out = runner.configure()
|
|
||||||
if not ok:
|
|
||||||
sys.stderr.write(out.decode(errors="replace"))
|
|
||||||
sys.stderr.write("\nBaseline configure FAILED; aborting.\n")
|
|
||||||
return 2
|
|
||||||
|
|
||||||
print("Verifying baseline is green (no mutation) ...")
|
|
||||||
baseline = runner.build_and_test()
|
|
||||||
if baseline != "survived":
|
|
||||||
sys.stderr.write(f"Baseline is not green ({baseline}); aborting. "
|
|
||||||
"Fix the suite before mutation testing.\n")
|
|
||||||
return 2
|
|
||||||
print("Baseline OK.\n")
|
|
||||||
|
|
||||||
# Group mutants by file so we mutate one file at a time and restore it.
|
|
||||||
killed = {"killed-compile": 0, "killed-test": 0, "killed-timeout": 0}
|
|
||||||
survivors = []
|
|
||||||
records = []
|
|
||||||
total = len(all_mutants)
|
|
||||||
|
|
||||||
# Cache pristine contents per target.
|
|
||||||
pristine = {t: read_lines(os.path.join(work, t)) for t in targets}
|
|
||||||
|
|
||||||
for idx, m in enumerate(all_mutants, start=1):
|
|
||||||
tgt_abs = os.path.join(work, m.path)
|
|
||||||
lines = list(pristine[m.path])
|
|
||||||
lines[m.lineno - 1] = m.after
|
|
||||||
write_lines(tgt_abs, lines)
|
|
||||||
try:
|
|
||||||
result = runner.build_and_test()
|
|
||||||
finally:
|
|
||||||
write_lines(tgt_abs, pristine[m.path]) # always restore
|
|
||||||
|
|
||||||
records.append((m, result))
|
|
||||||
if result == "survived":
|
|
||||||
survivors.append(m)
|
|
||||||
mark = "SURVIVED"
|
|
||||||
else:
|
|
||||||
killed[result] += 1
|
|
||||||
mark = result.upper()
|
|
||||||
print(f"[{idx}/{total}] {mark:16} {m.describe()}")
|
|
||||||
|
|
||||||
total_killed = sum(killed.values())
|
|
||||||
score = 100.0 * total_killed / total if total else 100.0
|
|
||||||
|
|
||||||
print("\n" + "=" * 72)
|
|
||||||
print("MUTATION TESTING SUMMARY")
|
|
||||||
print("=" * 72)
|
|
||||||
print(f" total mutants : {total}")
|
|
||||||
print(f" killed (test) : {killed['killed-test']}")
|
|
||||||
print(f" killed (compile): {killed['killed-compile']}")
|
|
||||||
print(f" killed (timeout): {killed['killed-timeout']}")
|
|
||||||
print(f" survived : {len(survivors)}")
|
|
||||||
print(f" mutation score : {score:.1f}%")
|
|
||||||
if survivors:
|
|
||||||
print("\nSurviving mutants (test-suite gaps -- turn these into tests):")
|
|
||||||
for m in survivors:
|
|
||||||
print(" " + m.describe())
|
|
||||||
|
|
||||||
if args.junit:
|
|
||||||
junit_path = os.path.abspath(args.junit)
|
|
||||||
write_junit(junit_path, records, targets)
|
|
||||||
print(f"\nJUnit report written to: {junit_path}")
|
|
||||||
|
|
||||||
if not args.keep and not args.work:
|
|
||||||
shutil.rmtree(work_parent, ignore_errors=True)
|
|
||||||
else:
|
|
||||||
print(f"\nScratch working copy kept at: {work}")
|
|
||||||
|
|
||||||
if args.threshold > 0 and score < args.threshold:
|
|
||||||
print(f"\nFAIL: mutation score {score:.1f}% < threshold {args.threshold:.1f}%")
|
|
||||||
return 1
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
33
src/error.c
33
src/error.c
@@ -14,14 +14,26 @@ char __AKERR_ERROR_NAMES[AKERR_MAX_ERR_VALUE+1][AKERR_MAX_ERROR_NAME_LENGTH];
|
|||||||
|
|
||||||
akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR];
|
akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR];
|
||||||
|
|
||||||
int akerr_valid_error_address(akerr_ErrorContext *ptr)
|
int main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
// Is this within the memory region occupied by AKERR_ARRAY_ERROR?
|
int i = 0;
|
||||||
if ( ptr == NULL ) {
|
PREPARE_ERROR(e);
|
||||||
return 1;
|
ATTEMPT {
|
||||||
|
CATCH(e, akerr_user_main(argc, argv));
|
||||||
|
if ( e == NULL || e->status == 0 ) {
|
||||||
|
/* User code claims everything went fine, doublecheck the error array before we quit */
|
||||||
|
for ( i = AKERR_MAX_ARRAY_ERROR - 1; i >= 0; i-- ) {
|
||||||
|
if ( AKERR_ARRAY_ERROR[i].status != 0 && AKERR_ARRAY_ERROR[i].refcount != 0 ) {
|
||||||
|
akerr_log_method("%s:%s:%d: Found unhandled and unreported error in the array at index %d\n", __FILE__, (char *)__func__, __LINE__, i);
|
||||||
|
e = &AKERR_ARRAY_ERROR[i];
|
||||||
}
|
}
|
||||||
return ((ptr >= &AKERR_ARRAY_ERROR[0]) &&
|
}
|
||||||
(ptr <= &AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR-1]));
|
}
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(e) {
|
||||||
|
} FINISH_NORETURN(e);
|
||||||
|
SUCCEED(e);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void akerr_default_logger(const char *fmt, ...)
|
void akerr_default_logger(const char *fmt, ...)
|
||||||
@@ -61,16 +73,15 @@ void akerr_init()
|
|||||||
akerr_name_for_status(AKERR_ATTRIBUTE, "Attribute Error");
|
akerr_name_for_status(AKERR_ATTRIBUTE, "Attribute Error");
|
||||||
akerr_name_for_status(AKERR_TYPE, "Type Error");
|
akerr_name_for_status(AKERR_TYPE, "Type Error");
|
||||||
akerr_name_for_status(AKERR_KEY, "Key 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_INDEX, "Index Error");
|
||||||
akerr_name_for_status(AKERR_FORMAT, "Format Error");
|
akerr_name_for_status(AKERR_FORMAT, "Format Error");
|
||||||
akerr_name_for_status(AKERR_IO, "Input Output 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_VALUE, "Value Error");
|
||||||
|
akerr_name_for_status(AKERR_BEHAVIOR, "Behavior Error");
|
||||||
akerr_name_for_status(AKERR_RELATIONSHIP, "Relationship 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;
|
inited = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
# Mutation testing
|
|
||||||
|
|
||||||
The unit tests tell us the library works. **Mutation testing tells us the tests
|
|
||||||
work** — that they would actually fail if the library were broken.
|
|
||||||
|
|
||||||
`scripts/mutation_test.py` deliberately breaks the library in small ways
|
|
||||||
("mutants"), one at a time, and runs the whole CTest suite against each broken
|
|
||||||
copy:
|
|
||||||
|
|
||||||
* if the tests **fail**, the mutant is **killed** — good, the suite caught it;
|
|
||||||
* if the tests still **pass**, the mutant **survived** — a bug of that shape
|
|
||||||
would slip through, so it points at a missing test.
|
|
||||||
|
|
||||||
The **mutation score** is `killed / (killed + survived)`. A surviving mutant is
|
|
||||||
a to-do item: write a test that distinguishes the mutant from the original.
|
|
||||||
|
|
||||||
## Running
|
|
||||||
|
|
||||||
No third-party tools are required — just Python 3 and the normal
|
|
||||||
cmake/ctest toolchain. The harness never touches your working tree; it copies
|
|
||||||
the repo to a scratch directory and mutates the copy.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Default: mutate src/error.c and include/akerror.tmpl.h
|
|
||||||
scripts/mutation_test.py
|
|
||||||
|
|
||||||
# Faster: just the C source
|
|
||||||
scripts/mutation_test.py --target src/error.c
|
|
||||||
|
|
||||||
# See what would run without building anything
|
|
||||||
scripts/mutation_test.py --target src/error.c --list
|
|
||||||
|
|
||||||
# Gate CI: exit non-zero if the score drops below 90%
|
|
||||||
scripts/mutation_test.py --threshold 90
|
|
||||||
```
|
|
||||||
|
|
||||||
Via CMake (configures a build first if needed):
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cmake --build build --target mutation
|
|
||||||
```
|
|
||||||
|
|
||||||
Useful flags: `--timeout SECONDS` (per-suite build+test cap; a mutant that
|
|
||||||
hangs is counted as killed), `--keep` (retain the scratch copy for debugging),
|
|
||||||
`--work DIR` (use a specific scratch directory), `--junit FILE` (write a JUnit
|
|
||||||
XML report — surviving mutants appear as failing test cases).
|
|
||||||
|
|
||||||
## CI reporting
|
|
||||||
|
|
||||||
Both the unit tests and the mutation run emit JUnit XML that CI consumes:
|
|
||||||
|
|
||||||
* `ctest --test-dir build --output-junit "$(pwd)/ctest-junit.xml"` — note the
|
|
||||||
absolute path; `--output-junit` otherwise resolves relative to the test dir.
|
|
||||||
* `scripts/mutation_test.py --junit mutation-junit.xml`
|
|
||||||
|
|
||||||
`.gitea/workflows/ci.yaml` runs both and feeds the XML to
|
|
||||||
`mikepenz/action-junit-report` (with `if: always()`, so results publish even
|
|
||||||
when a gate fails). The reporter runs with `annotate_only: true`: Gitea does not
|
|
||||||
implement the Checks API the action uses to create a check run, so creating one
|
|
||||||
404s (mikepenz/action-junit-report#23). `annotate_only` skips that call and the
|
|
||||||
results surface via the job summary (`detailed_summary: true`) instead. The
|
|
||||||
generated `*-junit.xml` files are git-ignored.
|
|
||||||
|
|
||||||
## Mutation operators
|
|
||||||
|
|
||||||
Each mutant changes exactly one location by one of:
|
|
||||||
|
|
||||||
| Tag | Operator | Example |
|
|
||||||
|-----|--------------------------------|----------------------------------|
|
|
||||||
| ROR | relational operator | `==` → `!=`, `<` → `<=`, `>=` → `>` |
|
|
||||||
| LCR | logical connector | `&&` → `\|\|` |
|
|
||||||
| BCR | boolean constant | `true` → `false` |
|
|
||||||
| AOR | arithmetic / compound assign | `+` → `-`, `+=` → `-=` |
|
|
||||||
| ICR | integer literal | `0` → `1`, `1` → `0` |
|
|
||||||
| SDL | statement deletion | `err->refcount += 1;` → *(removed)* |
|
|
||||||
|
|
||||||
Preprocessor control lines, comments, and the block of error-code / buffer-size
|
|
||||||
`#define`s are skipped: mutating those produces equivalent or uninteresting
|
|
||||||
mutants that only add noise.
|
|
||||||
|
|
||||||
## Interpreting survivors
|
|
||||||
|
|
||||||
Not every survivor is a test gap — some mutants are **equivalent** (they don't
|
|
||||||
change observable behaviour, e.g. resizing an internal scratch buffer). For each
|
|
||||||
survivor, decide:
|
|
||||||
|
|
||||||
1. **Real gap** → add or strengthen a test in `tests/` so the mutant is killed,
|
|
||||||
then re-run.
|
|
||||||
2. **Equivalent mutant** → no test can catch it; leave a note. If a specific
|
|
||||||
line is a persistent source of equivalents, narrow the target with
|
|
||||||
`--target` or extend the skip rules in `scripts/mutation_test.py`.
|
|
||||||
|
|
||||||
Re-run after adding tests and confirm the score went up.
|
|
||||||
|
|
||||||
## Current status
|
|
||||||
|
|
||||||
`src/error.c` scores ~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.
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Cover the conditional break/return failure macros:
|
|
||||||
* FAIL_ZERO_BREAK / FAIL_NONZERO_BREAK / FAIL_BREAK (inside ATTEMPT)
|
|
||||||
* FAIL_ZERO_RETURN / FAIL_NONZERO_RETURN / FAIL_RETURN (direct return)
|
|
||||||
* Each is checked in both its failing and its non-failing (pass-through) state.
|
|
||||||
*/
|
|
||||||
|
|
||||||
akerr_ErrorContext *zero_break(int x)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
FAIL_ZERO_BREAK(e, x, AKERR_VALUE, "x was zero");
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} FINISH(e, true);
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
akerr_ErrorContext *nonzero_break(int x)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
FAIL_NONZERO_BREAK(e, x, AKERR_INDEX, "x was nonzero");
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} FINISH(e, true);
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
akerr_ErrorContext *always_break(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
FAIL_BREAK(e, AKERR_IO, "always");
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} FINISH(e, true);
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
akerr_ErrorContext *zero_return(int x)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
FAIL_ZERO_RETURN(e, x, AKERR_KEY, "x was zero");
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
akerr_ErrorContext *nonzero_return(int x)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
FAIL_NONZERO_RETURN(e, x, AKERR_TYPE, "x was nonzero");
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
akerr_init();
|
|
||||||
|
|
||||||
akerr_ErrorContext *r;
|
|
||||||
|
|
||||||
r = zero_break(0);
|
|
||||||
AKERR_CHECK(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;
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
#ifndef AKERR_TEST_CAPTURE_H
|
|
||||||
#define AKERR_TEST_CAPTURE_H
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Shared test helpers for libakerror.
|
|
||||||
*
|
|
||||||
* Installs a capturing implementation of akerr_log_method so tests can assert
|
|
||||||
* on the *content* of log/stacktrace output (messages, status codes, error
|
|
||||||
* names) instead of relying solely on process exit codes.
|
|
||||||
*
|
|
||||||
* Also provides AKERR_CHECK(), a NDEBUG-proof assertion that fails the test by
|
|
||||||
* returning non-zero from main() (unlike assert(), which is compiled out in
|
|
||||||
* release builds and would silently turn a test into a no-op).
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "akerror.h"
|
|
||||||
#include <stdarg.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#define AKERR_CAPTURE_BUFSZ 65536
|
|
||||||
static char akerr_capture_buf[AKERR_CAPTURE_BUFSZ];
|
|
||||||
static size_t akerr_capture_len = 0;
|
|
||||||
|
|
||||||
static void __attribute__((unused)) akerr_capture_logger(const char *fmt, ...)
|
|
||||||
{
|
|
||||||
va_list ap;
|
|
||||||
va_start(ap, fmt);
|
|
||||||
int n = vsnprintf(akerr_capture_buf + akerr_capture_len,
|
|
||||||
AKERR_CAPTURE_BUFSZ - akerr_capture_len, fmt, ap);
|
|
||||||
va_end(ap);
|
|
||||||
if ( n > 0 ) {
|
|
||||||
akerr_capture_len += (size_t)n;
|
|
||||||
if ( akerr_capture_len >= AKERR_CAPTURE_BUFSZ ) {
|
|
||||||
akerr_capture_len = AKERR_CAPTURE_BUFSZ - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void __attribute__((unused)) akerr_capture_reset(void)
|
|
||||||
{
|
|
||||||
akerr_capture_len = 0;
|
|
||||||
akerr_capture_buf[0] = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Install the capturing logger. akerr_init() only assigns a default logger when
|
|
||||||
* akerr_log_method is NULL, and it is idempotent, so calling this either before
|
|
||||||
* or after the first PREPARE_ERROR keeps our logger in place.
|
|
||||||
*/
|
|
||||||
static void __attribute__((unused)) akerr_capture_install(void)
|
|
||||||
{
|
|
||||||
akerr_capture_reset();
|
|
||||||
akerr_log_method = &akerr_capture_logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Count array slots currently checked out of the pool (refcount != 0). */
|
|
||||||
static int __attribute__((unused)) akerr_slots_in_use(void)
|
|
||||||
{
|
|
||||||
int n = 0;
|
|
||||||
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
|
||||||
if ( AKERR_ARRAY_ERROR[i].refcount != 0 ) {
|
|
||||||
n++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
#define AKERR_CHECK(cond) \
|
|
||||||
do { \
|
|
||||||
if ( !(cond) ) { \
|
|
||||||
fprintf(stderr, "CHECK FAILED: %s at %s:%d\n", \
|
|
||||||
#cond, __FILE__, __LINE__); \
|
|
||||||
return 1; \
|
|
||||||
} \
|
|
||||||
} while ( 0 )
|
|
||||||
|
|
||||||
#define AKERR_CHECK_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
|
|
||||||
@@ -23,7 +23,7 @@ akerr_ErrorContext *func1(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int main(void)
|
akerr_ErrorContext *akerr_user_main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
@@ -32,5 +32,6 @@ int main(void)
|
|||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||||
akerr_log_method("Caught exception");
|
akerr_log_method("Caught exception");
|
||||||
} FINISH_NORETURN(errctx);
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED(errctx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ akerr_ErrorContext *func1(void)
|
|||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(void)
|
akerr_ErrorContext *akerr_user_main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
x = 12345;
|
x = 12345;
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
@@ -35,9 +35,8 @@ int main(void)
|
|||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||||
if ( x == 0 ) {
|
if ( x == 0 ) {
|
||||||
fprintf(stderr, "Cleanup works\n");
|
FAIL_RETURN(errctx, AKERR_API, "Cleanup does not work");
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
return 1;
|
} FINISH(errctx, true);
|
||||||
} FINISH_NORETURN(errctx);
|
SUCCEED(errctx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The unhandled-error hook (akerr_handler_unhandled_error) is overridable.
|
|
||||||
* Install a non-fatal handler so an unhandled error can be asserted directly
|
|
||||||
* (status, invocation) instead of relying on process death / WILL_FAIL.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int custom_fired = 0;
|
|
||||||
static int custom_status = 0;
|
|
||||||
|
|
||||||
static void my_handler(akerr_ErrorContext *e)
|
|
||||||
{
|
|
||||||
custom_fired = 1;
|
|
||||||
custom_status = (e != NULL) ? e->status : -1;
|
|
||||||
/* deliberately does NOT exit() */
|
|
||||||
}
|
|
||||||
|
|
||||||
akerr_ErrorContext *boom(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
FAIL_RETURN(e, AKERR_TYPE, "unhandled on purpose");
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
akerr_init(); /* sets the default handler... */
|
|
||||||
akerr_handler_unhandled_error = &my_handler; /* ...which we then override */
|
|
||||||
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(e, boom());
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
/* no HANDLE for AKERR_TYPE -> stays unhandled */
|
|
||||||
} FINISH_NORETURN(e);
|
|
||||||
|
|
||||||
AKERR_CHECK(custom_fired == 1);
|
|
||||||
AKERR_CHECK(custom_status == AKERR_TYPE);
|
|
||||||
AKERR_CHECK_CONTAINS("Unhandled Error");
|
|
||||||
fprintf(stderr, "err_custom_handler ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
#include <errno.h>
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The library imports system errno codes and their descriptions at build time
|
|
||||||
* (scripts/generrno.sh -> akerr_init_errno). Verify:
|
|
||||||
* - a system errno (EACCES) has a registered, non-empty name;
|
|
||||||
* - an 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;
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
/*
|
|
||||||
* akerr_init() registers a human-readable name for each library error code.
|
|
||||||
* Verify the names are actually installed (mutation testing showed the
|
|
||||||
* registration calls could be deleted without any test noticing).
|
|
||||||
*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* An error whose status has no matching HANDLE block must fall through to
|
|
||||||
* HANDLE_DEFAULT, and the non-matching HANDLE body must not run.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int specific_fired = 0;
|
|
||||||
static int default_fired = 0;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* With several distinct HANDLE blocks, only the one matching the raised status
|
|
||||||
* may run. Raise the middle code and confirm exact dispatch.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int a_fired = 0;
|
|
||||||
static int b_fired = 0;
|
|
||||||
static int c_fired = 0;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* HANDLE_GROUP lets several status codes share one handler body via
|
|
||||||
* case-fallthrough. The first member of the group uses HANDLE (which emits the
|
|
||||||
* leading break that terminates the previous case); each additional member
|
|
||||||
* uses HANDLE_GROUP; the shared body follows the last member. Verify that two
|
|
||||||
* different status codes both reach the shared body.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int group_fired = 0;
|
|
||||||
static int other_fired = 0;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* IGNORE deliberately swallows an error: it records the context in
|
|
||||||
* __akerr_last_ignored, logs it with an "IGNORED ERROR" marker, and lets
|
|
||||||
* execution continue.
|
|
||||||
*/
|
|
||||||
|
|
||||||
akerr_ErrorContext *boom(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
FAIL_RETURN(e, AKERR_VALUE, "this error is ignored on purpose");
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
|
|
||||||
int reached_after_ignore = 0;
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
(void)e;
|
|
||||||
|
|
||||||
IGNORE(boom());
|
|
||||||
reached_after_ignore = 1;
|
|
||||||
|
|
||||||
AKERR_CHECK(__akerr_last_ignored != NULL);
|
|
||||||
AKERR_CHECK(__akerr_last_ignored->status == AKERR_VALUE);
|
|
||||||
AKERR_CHECK(reached_after_ignore == 1);
|
|
||||||
AKERR_CHECK_CONTAINS("IGNORED ERROR");
|
|
||||||
AKERR_CHECK_CONTAINS("this error is ignored on purpose");
|
|
||||||
fprintf(stderr, "err_ignore ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include <stdio.h>
|
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *improper_closure(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
ATTEMPT {
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} FINISH(errctx, true);
|
|
||||||
fprintf(stderr, "Improperly returning from improper_closure\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(errctx, improper_closure());
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} FINISH_NORETURN(errctx);
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
#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;
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* PASS bubbles an error up to the caller without a local ATTEMPT/PROCESS block:
|
|
||||||
* if the wrapped call fails, PASS returns the context from the current function.
|
|
||||||
* Verify the error reaches main and that the code after PASS is skipped on
|
|
||||||
* failure.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int reached_after_pass = 0;
|
|
||||||
|
|
||||||
akerr_ErrorContext *inner(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
FAIL_RETURN(e, AKERR_IO, "inner failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
akerr_ErrorContext *outer(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
PASS(e, inner());
|
|
||||||
reached_after_pass = 1; /* must NOT run: PASS returned already */
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
|
|
||||||
int handled = 0;
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(e, outer());
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} HANDLE(e, AKERR_IO) {
|
|
||||||
handled = 1;
|
|
||||||
} FINISH_NORETURN(e);
|
|
||||||
|
|
||||||
AKERR_CHECK(handled == 1);
|
|
||||||
AKERR_CHECK(reached_after_pass == 0);
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
fprintf(stderr, "err_pass ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The error pool is a fixed array of AKERR_MAX_ARRAY_ERROR slots. When every
|
|
||||||
* slot is checked out, akerr_next_error() must return NULL rather than run off
|
|
||||||
* the end of the array; and it must always hand back the lowest free slot.
|
|
||||||
* Mutation testing showed both the terminating "return NULL" and the scan
|
|
||||||
* bounds could be broken without any test noticing.
|
|
||||||
*/
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_init();
|
|
||||||
|
|
||||||
akerr_ErrorContext *slots[AKERR_MAX_ARRAY_ERROR];
|
|
||||||
|
|
||||||
/* Check out every slot. */
|
|
||||||
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
|
||||||
slots[i] = akerr_next_error();
|
|
||||||
AKERR_CHECK(slots[i] != NULL);
|
|
||||||
slots[i]->refcount = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pool is fully exhausted: the next request must fail cleanly. */
|
|
||||||
AKERR_CHECK(akerr_next_error() == NULL);
|
|
||||||
|
|
||||||
/* Free exactly the first slot; the scan must find and return it. */
|
|
||||||
slots[0]->refcount = 0;
|
|
||||||
AKERR_CHECK(akerr_next_error() == slots[0]);
|
|
||||||
|
|
||||||
/* Tidy up. */
|
|
||||||
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
|
||||||
slots[i]->refcount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fprintf(stderr, "err_pool_exhaust ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Pool-hygiene regression test. The error pool is a fixed 128-slot static
|
|
||||||
* array; a single leaked reference per error would silently exhaust it. Run a
|
|
||||||
* large number of full raise -> catch -> handle cycles and confirm every slot
|
|
||||||
* is returned to the pool (refcount 0) and the pool can still hand out
|
|
||||||
* contexts afterwards.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#define ITERATIONS 100000
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Releasing an error context back to the pool must wipe it, so the next caller
|
|
||||||
* that checks it out never sees stale status/message/stacktrace from a previous
|
|
||||||
* error. Mutation testing showed the clearing memset in akerr_release_error
|
|
||||||
* could be deleted without any test noticing.
|
|
||||||
*/
|
|
||||||
|
|
||||||
akerr_ErrorContext *boom(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
FAIL_RETURN(e, AKERR_VALUE, "stale dirty message that must not survive");
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
akerr_init();
|
|
||||||
|
|
||||||
/* Raise and fully handle an error; FINISH_NORETURN releases it to the pool. */
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(e, boom());
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} HANDLE(e, AKERR_VALUE) {
|
|
||||||
} FINISH_NORETURN(e);
|
|
||||||
|
|
||||||
AKERR_CHECK(e == NULL);
|
|
||||||
|
|
||||||
/* The next context handed out is the slot we just released: it must be clean. */
|
|
||||||
akerr_ErrorContext *slot = akerr_next_error();
|
|
||||||
AKERR_CHECK(slot != NULL);
|
|
||||||
AKERR_CHECK(slot->status == 0);
|
|
||||||
AKERR_CHECK(slot->message[0] == '\0');
|
|
||||||
AKERR_CHECK(slot->stacktracebuf[0] == '\0');
|
|
||||||
AKERR_CHECK(strstr(slot->message, "stale dirty message") == NULL);
|
|
||||||
|
|
||||||
fprintf(stderr, "err_release_clears ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The success path: a nested call that returns cleanly (NULL) must not break
|
|
||||||
* out of the caller's ATTEMPT block, must not enter any handler, and must not
|
|
||||||
* consume a slot from the error pool.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int default_fired = 0;
|
|
||||||
|
|
||||||
akerr_ErrorContext *ok_func(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} FINISH(e, true);
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
akerr_capture_install();
|
|
||||||
akerr_init();
|
|
||||||
|
|
||||||
int reached_after_catch = 0;
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(e, ok_func());
|
|
||||||
reached_after_catch = 1; /* must run: no break on success */
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} HANDLE_DEFAULT(e) {
|
|
||||||
default_fired = 1; /* must NOT run */
|
|
||||||
} FINISH_NORETURN(e);
|
|
||||||
|
|
||||||
AKERR_CHECK(reached_after_catch == 1);
|
|
||||||
AKERR_CHECK(default_fired == 0);
|
|
||||||
AKERR_CHECK(e == NULL);
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
fprintf(stderr, "err_success ok\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* FINISH(ctx, false) closes an ATTEMPT block without propagating: an unhandled
|
|
||||||
* error is dropped (not returned, not passed to the unhandled handler) and the
|
|
||||||
* context is released back to the pool. Because we use FINISH (not
|
|
||||||
* FINISH_NORETURN) the process must continue normally and exit 0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int wrong_handler_fired = 0;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -23,12 +23,13 @@ akerr_ErrorContext *func1(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int main(void)
|
akerr_ErrorContext *akerr_user_main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, func1());
|
CATCH(errctx, func1());
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} FINISH_NORETURN(errctx);
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED(errctx);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user