Compare commits
14 Commits
tests/expa
...
status-cod
| Author | SHA1 | Date | |
|---|---|---|---|
|
5ff87908e7
|
|||
|
64da04e83b
|
|||
|
ba2430bfa1
|
|||
|
f1283e21a3
|
|||
| 11d21068df | |||
|
0bb3a4d52c
|
|||
|
539293cc1c
|
|||
|
9f0034a56e
|
|||
|
0c0d81249f
|
|||
|
426efbb2d4
|
|||
|
4ae1decde2
|
|||
|
4212ff0b28
|
|||
|
de13b290d4
|
|||
|
3e24356f07
|
@@ -37,6 +37,36 @@ jobs:
|
||||
fail_on_failure: 'true'
|
||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
- name: dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y cmake gcc moreutils python3
|
||||
# Run the suite against a gcov-instrumented build and gate on coverage of
|
||||
# the library sources. Thresholds keep headroom below the current numbers
|
||||
# (src/error.c: ~94% line, ~60% branch) and apply per file as well as to
|
||||
# the total, so the generated status-name table cannot mask a regression.
|
||||
- name: coverage
|
||||
run: |
|
||||
python3 scripts/coverage.py --junit coverage-junit.xml --threshold 90 --branch-threshold 50
|
||||
# Publish even when the threshold gate fails, so gaps are visible.
|
||||
# Display-only (fail_on_failure: false); the --threshold above is the gate.
|
||||
# annotate_only avoids the Checks API 404 on Gitea (see note above).
|
||||
- name: publish coverage results
|
||||
if: always()
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
with:
|
||||
report_paths: 'coverage-junit.xml'
|
||||
annotate_only: true
|
||||
detailed_summary: true
|
||||
include_passed: true
|
||||
fail_on_failure: 'false'
|
||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||
|
||||
mutation_test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
81
AGENTS.md
Normal file
81
AGENTS.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
This is a small C library built with CMake. Core implementation lives in
|
||||
`src/error.c`. The public header is generated at build time from
|
||||
`include/akerror.tmpl.h` by `scripts/generrno.sh`, which also generates
|
||||
`src/errno.c` under the build directory. CMake package and pkg-config templates
|
||||
are in `cmake/` and `akerror.pc.in`. Tests are one-file C programs in `tests/`;
|
||||
shared test helpers live beside them, such as `tests/err_capture.h`.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
Use an out-of-tree build:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
`cmake -S . -B build` configures the build and generates `akerror.h`.
|
||||
`cmake --build build` compiles `libakerror` and test executables. `ctest`
|
||||
runs the registered unit tests. To emit CI-style results, use:
|
||||
|
||||
```sh
|
||||
ctest --test-dir build --output-on-failure --output-junit "$(pwd)/ctest-junit.xml"
|
||||
```
|
||||
|
||||
Mutation testing is available through:
|
||||
|
||||
```sh
|
||||
cmake --build build --target mutation
|
||||
scripts/mutation_test.py --target src/error.c --threshold 65
|
||||
```
|
||||
|
||||
Code coverage is available through:
|
||||
|
||||
```sh
|
||||
cmake --build build --target coverage
|
||||
scripts/coverage.py --threshold 90 --branch-threshold 50
|
||||
```
|
||||
|
||||
`scripts/coverage.py` configures its own instrumented build tree (default
|
||||
`build/coverage`, `-DAKERR_COVERAGE=ON`), runs the CTest suite there, and
|
||||
reports gcov line/branch coverage per library source. Thresholds gate each
|
||||
file as well as the total. Coverage measures `src/error.c` and the generated
|
||||
`src/errno.c` only; the public header's macros expand at their call sites, so
|
||||
mutation testing is what checks those.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
Use C99-compatible C and follow the surrounding style. Functions and types use
|
||||
the `akerr_` prefix; macros and constants use `AKERR_` or all-caps macro names
|
||||
such as `PREPARE_ERROR`. Keep generated-code changes in templates or generator
|
||||
scripts, not in build outputs. Preserve concise comments for invariants,
|
||||
macro constraints, and non-obvious error lifecycle behavior.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
Add tests as `tests/err_<behavior>.c`. Register each new test in the
|
||||
`AKERR_TESTS` list in `CMakeLists.txt`; tests that intentionally abort must
|
||||
also be listed in `AKERR_WILL_FAIL_TESTS`. Prefer focused executable tests that
|
||||
return zero on success and use existing helpers such as `AKERR_CHECK`. Run the
|
||||
CTest suite before submitting changes, and run mutation testing and coverage
|
||||
when changing core control-flow, reference counting, stack-trace, or handler
|
||||
behavior.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
Recent commits use short, imperative, sentence-case subjects, for example
|
||||
`Fix refcount leak and stack-trace buffer overflow`. Keep commits focused and
|
||||
describe the observable behavior changed. Pull requests should include a brief
|
||||
summary, tests run, and any compatibility impact for public macros, generated
|
||||
headers, installation paths, or CMake/pkg-config consumers.
|
||||
|
||||
## Agent-Specific Instructions
|
||||
|
||||
Do not overwrite uncommitted user changes. Avoid editing generated files in
|
||||
`build/`; update `include/akerror.tmpl.h`, `src/error.c`, CMake files, tests,
|
||||
or scripts instead.
|
||||
116
CMakeLists.txt
116
CMakeLists.txt
@@ -1,13 +1,64 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(akerror LANGUAGES C)
|
||||
# The status-code registry replaced the consumer-sized __AKERR_ERROR_NAMES array
|
||||
# with private storage, which is a source and ABI break for anything built
|
||||
# against an earlier header -- hence 1.0.0 and a SOVERSION, so a stale installed
|
||||
# libakerror.so can no longer be silently paired with new headers.
|
||||
project(akerror VERSION 1.0.0 LANGUAGES C)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
include(CMakePackageConfigHelpers)
|
||||
include(CTest)
|
||||
|
||||
set(AKERR_USE_STDLIB 1 CACHE BOOL "Use the C standard library")
|
||||
set(AKERR_COVERAGE 0 CACHE BOOL "Instrument the build with gcov coverage counters")
|
||||
|
||||
# Size of the private status-name hash table. Must be a power of two; usable
|
||||
# capacity is 75% of it (src/error.c asserts both). The host's errno list
|
||||
# consumes part of that at akerr_init() time, so the remainder is what all
|
||||
# consumer libraries in the process share. These are applied PRIVATE on purpose:
|
||||
# the table lives entirely in src/error.c, so raising them never changes
|
||||
# anything a consumer can see. That is what makes them safe to tune, unlike the
|
||||
# AKERR_MAX_ERR_VALUE they replaced.
|
||||
set(AKERR_STATUS_NAME_SLOTS 4096 CACHE STRING
|
||||
"Slots in the status-name table (power of two; 75% usable)")
|
||||
set(AKERR_MAX_RESERVED_STATUS_RANGES 64 CACHE STRING
|
||||
"Maximum number of status ranges that may be reserved")
|
||||
set(akerror_install_cmakedir "${CMAKE_INSTALL_LIBDIR}/cmake/akerror")
|
||||
|
||||
# Coverage instrumentation. Applied per target (not globally) so it never leaks
|
||||
# into the exported/installed target interface. Only the library is
|
||||
# instrumented: the tests are the thing doing the covering, and the public
|
||||
# header's macros cannot be measured this way at all -- GCC attributes an
|
||||
# expanded macro to its call site, so header logic shows up as test-file lines.
|
||||
# Coverage of those macros is what mutation testing (--target
|
||||
# include/akerror.tmpl.h) is for.
|
||||
if(AKERR_COVERAGE)
|
||||
if(NOT CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
||||
message(FATAL_ERROR
|
||||
"AKERR_COVERAGE requires GCC or Clang, not ${CMAKE_C_COMPILER_ID}")
|
||||
endif()
|
||||
# -O0 keeps line counts attributable; no inlining or code motion.
|
||||
set(AKERR_COVERAGE_FLAGS --coverage -O0 -g)
|
||||
if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
|
||||
# Record absolute source paths so gcov resolves sources built from the
|
||||
# generated directory (GCC 8+; harmless to check).
|
||||
include(CheckCCompilerFlag)
|
||||
check_c_compiler_flag(-fprofile-abs-path AKERR_HAVE_PROFILE_ABS_PATH)
|
||||
if(AKERR_HAVE_PROFILE_ABS_PATH)
|
||||
list(APPEND AKERR_COVERAGE_FLAGS -fprofile-abs-path)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Add coverage compile/link flags to one target, if coverage is enabled.
|
||||
function(akerr_instrument_for_coverage _target)
|
||||
if(AKERR_COVERAGE)
|
||||
target_compile_options(${_target} PRIVATE ${AKERR_COVERAGE_FLAGS})
|
||||
set_property(TARGET ${_target} APPEND_STRING
|
||||
PROPERTY LINK_FLAGS " --coverage")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
set(SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/scripts/generrno.sh)
|
||||
set(INFILE ${CMAKE_CURRENT_SOURCE_DIR}/include/akerror.tmpl.h)
|
||||
|
||||
@@ -42,8 +93,17 @@ add_library(akerror::akerror ALIAS akerror)
|
||||
|
||||
target_compile_definitions(akerror
|
||||
PUBLIC AKERR_USE_STDLIB=${AKERR_USE_STDLIB}
|
||||
PRIVATE AKERR_STATUS_NAME_SLOTS=${AKERR_STATUS_NAME_SLOTS}
|
||||
PRIVATE AKERR_MAX_RESERVED_STATUS_RANGES=${AKERR_MAX_RESERVED_STATUS_RANGES}
|
||||
)
|
||||
|
||||
set_target_properties(akerror PROPERTIES
|
||||
VERSION ${PROJECT_VERSION}
|
||||
SOVERSION ${PROJECT_VERSION_MAJOR}
|
||||
)
|
||||
|
||||
akerr_instrument_for_coverage(akerror)
|
||||
|
||||
# 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.
|
||||
@@ -67,11 +127,24 @@ set(AKERR_TESTS
|
||||
err_release_clears
|
||||
err_pool_exhaust
|
||||
err_maxval
|
||||
err_name_ownership
|
||||
err_registry_init_order
|
||||
err_status_exception
|
||||
err_copy_string
|
||||
err_library_status_fatal
|
||||
err_refcount_double_fail
|
||||
err_stacktrace_bounds
|
||||
err_name_bounds
|
||||
err_format_string
|
||||
err_unhandled_null
|
||||
err_release_null
|
||||
err_release_refcount
|
||||
)
|
||||
|
||||
set(AKERR_WILL_FAIL_TESTS
|
||||
err_trace
|
||||
err_improper_closure
|
||||
err_library_status_fatal
|
||||
)
|
||||
|
||||
foreach(_test IN LISTS AKERR_TESTS)
|
||||
@@ -81,23 +154,44 @@ foreach(_test IN LISTS AKERR_TESTS)
|
||||
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
|
||||
# Coverage and mutation testing are meta-checks on the test suite itself, and
|
||||
# both rebuild and re-run the whole suite, so they are manual targets rather
|
||||
# than CTest tests.
|
||||
find_package(Python3 COMPONENTS Interpreter)
|
||||
if(Python3_FOUND)
|
||||
add_custom_target(mutation
|
||||
# Code coverage: which library lines/branches the CTest suite reaches.
|
||||
# cmake --build build --target coverage
|
||||
# The script configures and drives its own instrumented build tree (under
|
||||
# ${CMAKE_BINARY_DIR}/coverage) so this build's binaries and its coverage
|
||||
# counters can never be stale or half-instrumented. Reports via gcov.
|
||||
add_custom_target(coverage
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/scripts/coverage.py
|
||||
--source-root ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
--build-dir ${CMAKE_CURRENT_BINARY_DIR}/coverage
|
||||
--cmake ${CMAKE_COMMAND}
|
||||
--ctest ${CMAKE_CTEST_COMMAND}
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
USES_TERMINAL
|
||||
COMMENT "Running the test suite instrumented for coverage"
|
||||
)
|
||||
|
||||
# Mutation testing: break the library in small ways and confirm the test
|
||||
# suite notices.
|
||||
# cmake --build build --target mutation
|
||||
# When embedded in another project, use a namespaced target to avoid
|
||||
# collisions with mutation targets provided by sibling dependencies.
|
||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
set(AKERR_MUTATION_TARGET mutation)
|
||||
else()
|
||||
set(AKERR_MUTATION_TARGET akerror_mutation)
|
||||
endif()
|
||||
add_custom_target(${AKERR_MUTATION_TARGET}
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/scripts/mutation_test.py
|
||||
--source-root ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
|
||||
96
README.md
96
README.md
@@ -4,6 +4,14 @@ This library provides a TRY/CATCH style exception handling mechanism for C.
|
||||
|
||||

|
||||
|
||||
## Upgrading from a pre-1.0.0 release
|
||||
|
||||
1.0.0 replaced the consumer-sized status-name array with a private, ownership-
|
||||
enforced registry. That is a source and ABI break: see
|
||||
[UPGRADING.md](UPGRADING.md) for what was removed, how to migrate, the capacity
|
||||
limits and how to raise them, and the thread-safety rules.
|
||||
|
||||
|
||||
# Why?
|
||||
|
||||
There is nothing wrong with C as it is. This library does not claim to fix some problem with C.
|
||||
@@ -64,14 +72,47 @@ Any function which uses the `PREPARE_ERROR` macro should have a return type of `
|
||||
|
||||
The library uses integer values to specify error codes inside of its context. These integer return codes are defined in `akerror.h` in the form of `AKERR_xxxxx` where `xxxxx` is the name of the error code in question. See `akerror.h` for a list of defined errors and their descriptions.
|
||||
|
||||
You can define additional error types by defining additional `AKERR_xxxxx` values. Error values up to 255 are reserved by the library (`AKERR_xxxxx` begins where `errno` stops), so please begin your error values at 256. When you add additional error codes, you need to define `-DAKERR_MAX_ERR_VALUE=n` to the compiler, where `n` is the maximum error code you have defined. If you define custom error codes, `AKERR_MAX_ERR_VALUE` must be >= 256 or the compiler will throw an error.
|
||||
You can define additional error types as integer constants. Values 0 through 255
|
||||
are reserved by libakerror (the host's errno values plus the `AKERR_*` codes);
|
||||
consumers allocate codes starting at `AKERR_FIRST_CONSUMER_STATUS` (256). Status
|
||||
names are stored sparsely, so any `int` is a legal status and no compile
|
||||
definition is needed to use large values.
|
||||
|
||||
Define a human-friendly name for the error with the `akerr_name_for_status` method somewhere in your code's initialization before the error may be used:
|
||||
Every library that may coexist in one process must reserve its range during
|
||||
initialization. `akerr_reserve_status_range()` and
|
||||
`akerr_register_status_name()` report failure the way everything else in this
|
||||
library does — they return `akerr_ErrorContext *`, and they are marked
|
||||
`AKERR_NOIGNORE` — so a collision is an exception you can `CATCH`, `HANDLE`, or
|
||||
`PASS` up out of your initialization:
|
||||
|
||||
```c
|
||||
akerr_name_for_status(129, "Some Error Code Description")
|
||||
#define MYLIB_OWNER "my-library"
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *mylib_init(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
/* Another component owning part of the range propagates to our caller. */
|
||||
PASS(errctx, akerr_reserve_status_range(256, 16, MYLIB_OWNER));
|
||||
|
||||
/* Then name each code, quoting the owner you reserved with. */
|
||||
PASS(errctx, akerr_register_status_name(MYLIB_OWNER, 256,
|
||||
"Some Error Code Description"));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
```
|
||||
|
||||
Reservations are process-local, fixed-capacity, and idempotent when the same
|
||||
owner repeats the exact same range. They preserve compile-time integer constants
|
||||
so `HANDLE` still works, since `case` labels require them.
|
||||
|
||||
Naming a status outside your reservation raises `AKERR_STATUS_NAME_FOREIGN`, and
|
||||
naming one nobody reserved raises `AKERR_STATUS_NAME_UNRESERVED`; a colliding
|
||||
reservation raises `AKERR_STATUS_RANGE_OVERLAP`, with a message naming the real
|
||||
owner. See [UPGRADING.md](UPGRADING.md) for the full list of statuses these two
|
||||
functions raise, the capacity limits and how to raise them, and thread-safety
|
||||
rules.
|
||||
|
||||
|
||||
# Installation
|
||||
|
||||
@@ -95,10 +136,14 @@ This library depends upon `stdlib`. If you don't want to link against stdlib, yo
|
||||
|
||||
- `memset` function
|
||||
- `strncpy` function
|
||||
- `strlen` function
|
||||
- `strcmp` function
|
||||
- `sprintf` function
|
||||
- `exit` function
|
||||
- `bool` type
|
||||
- `NULL` type
|
||||
- `INT_MAX` constant
|
||||
- `PATH_MAX` constant
|
||||
|
||||
... then you can compile it thusly:
|
||||
|
||||
@@ -210,6 +255,8 @@ ATTEMPT {
|
||||
|
||||
This will assign the return value of the function in question to the akerr_ErrorContext previously prepared in the current scope. If the function returns an akerr_ErrorContext that indicates any type of error, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
|
||||
|
||||
(One caveat: because this exit is implemented with a C `break`, `CATCH` must not be used inside a loop within the `ATTEMPT` block — see the section "Important: do not use CATCH or FAIL_*_BREAK inside a loop" below.)
|
||||
|
||||
## Setting errors from functions or expressions returning integer
|
||||
|
||||
For functions that return integer, such as logical comparisons or most standard library functions, use the `FAIL_ZERO_BREAK` and `FAIL_NONZERO_BREAK` macros. These macros allow you to capture an integer return code from an expression or function and set an error code in the current context based off that return.
|
||||
@@ -232,6 +279,49 @@ ATTEMPT {
|
||||
|
||||
When either of these two macros are used, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
|
||||
|
||||
## Important: do not use CATCH or FAIL_*_BREAK inside a loop
|
||||
|
||||
`CATCH`, `FAIL_ZERO_BREAK`, `FAIL_NONZERO_BREAK`, and `FAIL_BREAK` leave the `ATTEMPT` block by executing a C `break` statement. In C, `break` only exits the *innermost* enclosing `for`, `while`, `do`, or `switch`. Therefore **these macros must not be used inside a loop (or a nested `switch`) that is itself inside an `ATTEMPT` block.** If you do, the `break` escapes only the loop — not the `ATTEMPT` — and the rest of the `ATTEMPT` body then runs with an error already pending:
|
||||
|
||||
```c
|
||||
ATTEMPT {
|
||||
for ( int i = 0; i < n; i++ ) {
|
||||
CATCH(errctx, process(items[i])); // WRONG: break exits the for loop, not the ATTEMPT
|
||||
}
|
||||
// ... this code still executes, with errctx already in an error state ...
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
```
|
||||
|
||||
Note that moving the loop into a helper function does **not** fix this on its own — if the helper still wraps the loop in an `ATTEMPT` and uses `CATCH`/`FAIL_*_BREAK` inside it, it has the exact same problem. The fix is to iterate with `return`-based macros, which are unaffected by loop nesting. Use one of the two patterns below.
|
||||
|
||||
**Pattern 1 — use `PASS` (or a `FAIL_*_RETURN` macro) inside the loop.** These exit the *enclosing function* with a `return` rather than a `break`, so loop nesting is irrelevant. Use this when the loop should stop and propagate on the first error:
|
||||
|
||||
```c
|
||||
akerr_ErrorContext AKERR_NOIGNORE *process_all(Item *items, int n)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
for ( int i = 0; i < n; i++ ) {
|
||||
PASS(errctx, process(items[i])); // returns from process_all on the first error
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 2 — move the loop into a helper and `CATCH` the single call.** When you need a `CLEANUP` block or want to `HANDLE` the error locally, put the loop in its own `akerr_ErrorContext *`-returning function (written per Pattern 1) and `CATCH` that one call. The `CATCH` is then not inside a loop, so its `break` scopes to the `ATTEMPT` correctly:
|
||||
|
||||
```c
|
||||
ATTEMPT {
|
||||
CATCH(errctx, process_all(items, n)); // a single CATCH, not looped
|
||||
} CLEANUP {
|
||||
// ... always runs ...
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_VALUE) {
|
||||
// ... handle a failure from any iteration ...
|
||||
} FINISH(errctx, true);
|
||||
```
|
||||
|
||||
# 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.
|
||||
|
||||
93
TODO.md
Normal file
93
TODO.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# TODO
|
||||
|
||||
Working notes for `libakerror`. Outstanding items only.
|
||||
|
||||
## 1. The test suite has no sanitizer run
|
||||
|
||||
Mutation testing caught an out-of-bounds probe in the status-name hash table
|
||||
that the suite could not: the failure mode was a write into adjacent BSS, which
|
||||
does not crash, so every test still passed. Sharpening one test closed that
|
||||
instance, but ASan would have caught the whole class directly and independently
|
||||
of how sharp the assertions are.
|
||||
|
||||
Add a `-fsanitize=address,undefined` build to `.gitea/workflows/ci.yaml`, or a
|
||||
CMake option alongside `AKERR_COVERAGE`. This is the highest-value item here: it
|
||||
covers the whole library, not just the registry, and the library's fixed pools
|
||||
and manual buffer arithmetic are exactly what it is good at.
|
||||
|
||||
## 2. `HANDLE`-level status aliasing is still undetectable
|
||||
|
||||
Two components can compile the same integer into a `case` label without ever
|
||||
reserving a range or registering a name, and nothing sees it. Ownership
|
||||
enforcement covers *naming*, which is the part the library mediates; the `case`
|
||||
label never reaches it.
|
||||
|
||||
Closing this needs the `if`/`else if` handler ladder — rewriting
|
||||
`PROCESS`/`HANDLE`/`HANDLE_GROUP`/`HANDLE_DEFAULT`/`FINISH` so status matching
|
||||
is not restricted to integer constant expressions. That would also allow
|
||||
matching on ranges or predicates, and would let a handler resolve a code through
|
||||
its owner. It touches the most load-bearing code in the library and every
|
||||
consumer's error handling at once, so it wants its own change.
|
||||
|
||||
Note it would *not* by itself fix the "don't use `CATCH` or `FAIL_*_BREAK`
|
||||
inside a loop" hazard: that comes from exiting via `break`, not from `switch`.
|
||||
|
||||
## 3. No registry introspection
|
||||
|
||||
There is no way to ask who owns a status, or to enumerate reservations. The
|
||||
"coordinate ranges at the dependency-stack level" advice in UPGRADING.md
|
||||
therefore has no tooling behind it.
|
||||
|
||||
A read-only accessor plus a dump through `akerr_log_method` would let a startup
|
||||
self-check or a CI job print the whole map for a linked stack. Cheap, additive,
|
||||
and the natural next step for multi-component adoption.
|
||||
|
||||
## 4. No way to release a reservation
|
||||
|
||||
A plugin host that `dlopen`s many distinct plugins over a process lifetime
|
||||
accumulates ranges until the table fills. Reloading the *same* plugin is fine —
|
||||
an identical repeat by the same owner is idempotent.
|
||||
|
||||
## 5. The registry is not thread safe
|
||||
|
||||
Global mutable state, no locking, and `akerr_status_name_count++` is not atomic.
|
||||
Currently documented as an initialization-time-only API rather than enforced. If
|
||||
components start initializing on separate threads this needs either a lock or a
|
||||
documented once-per-process init barrier.
|
||||
|
||||
## 6. Deprecate the two-argument name-registration path
|
||||
|
||||
`akerr_name_for_status(status, name)` cannot identify its caller, so it can only
|
||||
check that *some* reservation covers the status, not that the caller owns it. It
|
||||
exists for migration. Once consumers have moved to
|
||||
`akerr_register_status_name()`, make the set path a no-op or remove it and leave
|
||||
`akerr_name_for_status()` as pure lookup.
|
||||
|
||||
## 7. `akerr_init()`'s own reservation failure is untested
|
||||
|
||||
`tests/err_library_status_fatal.c` covers the terminal path in
|
||||
`__akerr_name_library_status()` by naming a status the library does not own. The
|
||||
band reservation in `akerr_init()` has no such handle: it can only fail in a
|
||||
build whose tables are too small for the library's own entries, and both sizes
|
||||
are `PRIVATE` to the library target, so a test executable cannot set them.
|
||||
|
||||
Closing it means a second library target built with tiny tables plus a
|
||||
`WILL_FAIL` test linked against it — worth doing when the CMake gains a
|
||||
sanitizer variant (item 1), since that adds the same machinery.
|
||||
|
||||
Related: branch coverage on `src/error.c` now sits just above its 50% gate.
|
||||
Every `FAIL_*` site carries about six branch outcomes of error-construction
|
||||
machinery (`ENSURE_ERROR_READY`, `AKERR_STACKTRACE_APPEND`) that only run when
|
||||
that specific failure fires, and every `PASS` site around a call that cannot
|
||||
fail carries about twenty-five. Validating more inputs therefore lowers the
|
||||
ratio by construction. Before adding defensive checks, expect to add a test that
|
||||
drives them, as `tests/err_copy_string.c` does.
|
||||
|
||||
## Unrelated pre-existing issues
|
||||
|
||||
- The `AKERR_USE_STDLIB=OFF` build does not compile at all: `bool`, `PATH_MAX`
|
||||
and `NULL` are used unconditionally but only included under the stdlib branch.
|
||||
The README's dependency list states what a replacement must provide, but the
|
||||
header still needs its includes untangled for that configuration to work.
|
||||
- `CMakeLists.txt` sets `main_lib_dest` from `MY_LIBRARY_VERSION`, which is never
|
||||
defined and never read. Dead line.
|
||||
208
UPGRADING.md
Normal file
208
UPGRADING.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Upgrade notice: custom status codes (1.0.0)
|
||||
|
||||
Version 1.0.0 replaces the consumer-sized status-name array with a private
|
||||
registry, and makes status-code ownership explicit and enforced. This is a
|
||||
source and ABI break. The library now carries a version and an soname
|
||||
(`libakerror.so.1`), so a stale installed library can no longer be silently
|
||||
paired with a newer header — but anything built against a pre-1.0.0 header must
|
||||
be rebuilt.
|
||||
|
||||
What was removed:
|
||||
|
||||
* `AKERR_MAX_ERR_VALUE` — the registry is sparse and accepts any `int`, so
|
||||
consumers no longer size it. Delete every compile definition and source
|
||||
reference. A stale `-DAKERR_MAX_ERR_VALUE=...` is now harmless but useless.
|
||||
* `__AKERR_ERROR_NAMES` — the name table is private to the library. Code that
|
||||
touched this data symbol was using an undocumented interface; use
|
||||
`akerr_name_for_status()`.
|
||||
* `AKERR_STATUS_RANGE_OK` and `AKERR_STATUS_NAME_OK` — the registry functions no
|
||||
longer return an `int`. Success is a `NULL` `akerr_ErrorContext *`, like every
|
||||
other function in the library. See "The registry raises errors" below.
|
||||
|
||||
To migrate:
|
||||
|
||||
1. Rebuild libakerror and every dependent library against the new header.
|
||||
2. Move custom status codes out of the reserved `0`–`255` band. Use fixed
|
||||
integer constants beginning at `AKERR_FIRST_CONSUMER_STATUS` (256) rather
|
||||
than offsets from `AKERR_LAST_ERRNO_VALUE`, so a libc that grows an errno
|
||||
cannot move your codes.
|
||||
3. Assign a distinct range to every library that may coexist in one process, and
|
||||
coordinate those ranges at the application or dependency-stack level.
|
||||
4. Reserve the complete range with `akerr_reserve_status_range()` during
|
||||
initialization, and treat the error it returns like any other error: `CATCH`
|
||||
it, `PASS` it, or let it propagate out of your init function.
|
||||
5. Register names with `akerr_register_status_name()`, passing the same owner
|
||||
string you reserved with. **You can no longer name a status you have not
|
||||
reserved** — see "Ownership is enforced" below.
|
||||
|
||||
For example:
|
||||
|
||||
```c
|
||||
enum {
|
||||
MYLIB_ERR_BASE = AKERR_FIRST_CONSUMER_STATUS, /* 256 */
|
||||
MYLIB_ERR_PARSE = MYLIB_ERR_BASE,
|
||||
MYLIB_ERR_STORAGE,
|
||||
MYLIB_ERR_LIMIT
|
||||
};
|
||||
|
||||
#define MYLIB_OWNER "mylib"
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *mylib_init(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
/* Any collision propagates out of mylib_init() to the caller. */
|
||||
PASS(errctx, akerr_reserve_status_range(MYLIB_ERR_BASE,
|
||||
MYLIB_ERR_LIMIT - MYLIB_ERR_BASE,
|
||||
MYLIB_OWNER));
|
||||
PASS(errctx, akerr_register_status_name(MYLIB_OWNER, MYLIB_ERR_PARSE,
|
||||
"Parse Error"));
|
||||
PASS(errctx, akerr_register_status_name(MYLIB_OWNER, MYLIB_ERR_STORAGE,
|
||||
"Storage Error"));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
```
|
||||
|
||||
If your `init` cannot return an `akerr_ErrorContext *` — a C API with an `int`
|
||||
return, say — catch the error and convert it. Close with `FINISH_NORETURN`
|
||||
rather than `FINISH`: nothing can propagate out of a function that does not
|
||||
return an error context, and `FINISH(errctx, false)` in an `int`-returning
|
||||
function compiles the (dead) propagation branch anyway, which warns.
|
||||
|
||||
```c
|
||||
int mylib_init(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int rc = MYLIB_INIT_OK;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akerr_reserve_status_range(MYLIB_ERR_BASE,
|
||||
MYLIB_ERR_LIMIT - MYLIB_ERR_BASE,
|
||||
MYLIB_OWNER));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE_DEFAULT(errctx) {
|
||||
LOG_ERROR_WITH_MESSAGE(errctx, "mylib could not claim its status range");
|
||||
rc = MYLIB_INIT_FAILED; /* another component owns part of the range */
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
return rc;
|
||||
}
|
||||
```
|
||||
|
||||
You do not need to call `akerr_init()` first. Every registry entry point calls
|
||||
it for you, so a library that reserves its range before anything else in the
|
||||
process has touched libakerror keeps that reservation. (Before 1.0.0 this was
|
||||
silently destructive: `akerr_init()` cleared the tables, so a reservation made
|
||||
too early was discarded and the next component to claim the same range was told
|
||||
it was free.)
|
||||
|
||||
## Ownership is enforced
|
||||
|
||||
Reserving a range is no longer advisory bookkeeping. A status may only be named
|
||||
from inside a reservation:
|
||||
|
||||
* `akerr_register_status_name(owner, status, name)` requires that `status` fall
|
||||
in a range reserved by `owner`. Naming another component's status fails with
|
||||
`AKERR_STATUS_NAME_FOREIGN`, and naming a status nobody reserved fails with
|
||||
`AKERR_STATUS_NAME_UNRESERVED`.
|
||||
* The two-argument `akerr_name_for_status(status, name)` set path still works,
|
||||
but it cannot identify its caller, so it can only require that *some*
|
||||
reservation covers the status. Prefer the owned form: it is the one that
|
||||
catches a component writing into a range that is not its own.
|
||||
|
||||
Every refusal is reported, because a name that fails to register degrades that
|
||||
code to `"Unknown Error"` in every later stack trace.
|
||||
|
||||
This detects *name* collisions. It cannot detect two components compiling the
|
||||
same integer into a `HANDLE` label without ever registering a name, so every
|
||||
co-resident library should still reserve its range.
|
||||
|
||||
## The registry raises errors
|
||||
|
||||
`akerr_reserve_status_range()` and `akerr_register_status_name()` return
|
||||
`akerr_ErrorContext *`, exactly like the rest of the library. They are marked
|
||||
`AKERR_NOIGNORE`, so discarding the result is a compile-time warning, and an
|
||||
error you catch but do not handle propagates out of your init function instead
|
||||
of leaving you with a range you do not actually own.
|
||||
|
||||
```c
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akerr_reserve_status_range(256, 16, MYLIB_OWNER));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_STATUS_RANGE_OVERLAP) {
|
||||
/* Another component owns part of it -- the message names which. */
|
||||
} FINISH(errctx, true);
|
||||
```
|
||||
|
||||
`akerr_reserve_status_range()` raises:
|
||||
|
||||
| Status | Meaning |
|
||||
| --- | --- |
|
||||
| — (returns `NULL`) | Range reserved, or an identical range was already reserved by the same owner |
|
||||
| `AKERR_STATUS_RANGE_OVERLAP` | Part of the range is owned by someone else (the message names the owner) |
|
||||
| `AKERR_STATUS_RANGE_FULL` | No reservation slots remain |
|
||||
| `AKERR_STATUS_RANGE_INVALID` | `count < 1`, NULL/empty/over-long owner, or the range overflows `int` |
|
||||
|
||||
`akerr_register_status_name()` raises:
|
||||
|
||||
| Status | Meaning |
|
||||
| --- | --- |
|
||||
| — (returns `NULL`) | Name registered |
|
||||
| `AKERR_STATUS_NAME_UNRESERVED` | No reservation contains this status |
|
||||
| `AKERR_STATUS_NAME_FOREIGN` | The status is in a range owned by someone else |
|
||||
| `AKERR_STATUS_NAME_FULL` | The name registry is full |
|
||||
| `AKERR_STATUS_NAME_INVALID` | NULL/empty/over-long owner, or a NULL name |
|
||||
|
||||
These are ordinary status codes inside the library's reserved band, so they can
|
||||
be matched with `HANDLE`, grouped with `HANDLE_GROUP`, and they print with a
|
||||
name and a stack trace when they go unhandled.
|
||||
|
||||
`akerr_name_for_status(status, name)` is the one exception: it returns a name
|
||||
rather than an error context, so it cannot raise. A refused registration through
|
||||
that path is reported through `akerr_log_method` and reads back as
|
||||
`"Unknown Error"`.
|
||||
|
||||
Repeating an identical reservation for the same owner is a no-op. A *subset* or
|
||||
*superset* of your own range is not — it raises `AKERR_STATUS_RANGE_OVERLAP`.
|
||||
Reserve the whole range in one call.
|
||||
|
||||
The library holds itself to the same rule at startup: if `akerr_init()` cannot
|
||||
reserve its own `0`–`255` band or name one of its own codes, it logs the failure
|
||||
and terminates the program. That can only happen in a misconfigured build (a
|
||||
name table too small for the library's own entries), and the alternative is a
|
||||
process whose stack traces silently read `"Unknown Error"` for built-in codes.
|
||||
|
||||
## Capacity
|
||||
|
||||
Both tables are fixed size, allocated in BSS, and never grow:
|
||||
|
||||
| Limit | Default | Build-time override |
|
||||
| --- | --- | --- |
|
||||
| Status names | 3072 usable (4096 slots, 75% load) | `-DAKERR_STATUS_NAME_SLOTS=<power of two>` |
|
||||
| Reserved ranges | 64 | `-DAKERR_MAX_RESERVED_STATUS_RANGES=<n>` |
|
||||
|
||||
`akerr_init()` consumes one name slot per host errno value plus one per
|
||||
`AKERR_*` code — around 150 on glibc, leaving roughly 2900 for all consumers in
|
||||
the process to share. That figure varies with the host libc, so treat it as
|
||||
approximate rather than a budget to fill.
|
||||
|
||||
Both overrides are CMake cache variables and apply `PRIVATE` to the library
|
||||
target. The tables live entirely inside `src/error.c`, so raising them changes
|
||||
nothing a consumer can observe — unlike the `AKERR_MAX_ERR_VALUE` they replaced,
|
||||
where a mismatch between the library and its consumers corrupted memory. Set
|
||||
them when configuring libakerror itself:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -DAKERR_STATUS_NAME_SLOTS=16384
|
||||
```
|
||||
|
||||
Exhausting either table raises an error to the caller; it is never silent.
|
||||
|
||||
## Thread safety
|
||||
|
||||
The registry is process-global mutable state with no locking. Reserve ranges and
|
||||
register names during single-threaded initialization, before spawning threads.
|
||||
Lookups (`akerr_name_for_status(status, NULL)`) are safe to call concurrently
|
||||
once registration has finished.
|
||||
@@ -39,16 +39,43 @@
|
||||
#define AKERR_NOT_IMPLEMENTED (AKERR_LAST_ERRNO_VALUE + 16) /** A method was called that is defined but not currently implemented */
|
||||
#define AKERR_BADEXC (AKERR_LAST_ERRNO_VALUE + 17) /** The libakerr library was given an akerr_ErrorContext to parse that did not come from AKERR_ARRAY_ERROR (likely an uninitialized pointer) */
|
||||
|
||||
#ifndef AKERR_MAX_ERR_VALUE
|
||||
/* Must be >= the highest AKERR_* offset above (AKERR_BADEXC, +17) so every
|
||||
* library error code is indexable in __AKERR_ERROR_NAMES. Keep in sync when
|
||||
* adding codes; tests/err_maxval.c guards this invariant. */
|
||||
#define AKERR_MAX_ERR_VALUE (AKERR_LAST_ERRNO_VALUE + 17)
|
||||
#elif AKERR_MAX_ERR_VALUE < 256
|
||||
#error user-defined AKERR_MAX_ERR_VALUE must be >= 256
|
||||
#endif
|
||||
/*
|
||||
* Registry failures. These are ordinary status codes, not a private return
|
||||
* enumeration: akerr_reserve_status_range() and akerr_register_status_name()
|
||||
* return akerr_ErrorContext * like everything else in this library, so a refused
|
||||
* reservation can be CATCH-ed, HANDLE-d, PASS-ed, or left unhandled to produce a
|
||||
* stack trace and stop the program. Success returns NULL.
|
||||
*/
|
||||
#define AKERR_STATUS_RANGE_OVERLAP (AKERR_LAST_ERRNO_VALUE + 18) /** Some part of the range is already owned by someone else */
|
||||
#define AKERR_STATUS_RANGE_FULL (AKERR_LAST_ERRNO_VALUE + 19) /** No reservation slots remain (see AKERR_MAX_RESERVED_STATUS_RANGES) */
|
||||
#define AKERR_STATUS_RANGE_INVALID (AKERR_LAST_ERRNO_VALUE + 20) /** Bad count, bad owner string, or the range overflows int */
|
||||
#define AKERR_STATUS_NAME_UNRESERVED (AKERR_LAST_ERRNO_VALUE + 21) /** No owner has reserved a range containing this status */
|
||||
#define AKERR_STATUS_NAME_FOREIGN (AKERR_LAST_ERRNO_VALUE + 22) /** The status lies in a range reserved by a different owner */
|
||||
#define AKERR_STATUS_NAME_FULL (AKERR_LAST_ERRNO_VALUE + 23) /** The name registry is full (raise AKERR_STATUS_NAME_SLOTS) */
|
||||
#define AKERR_STATUS_NAME_INVALID (AKERR_LAST_ERRNO_VALUE + 24) /** NULL/empty/over-long owner, or a NULL name */
|
||||
|
||||
extern char __AKERR_ERROR_NAMES[AKERR_MAX_ERR_VALUE+1][AKERR_MAX_ERROR_NAME_LENGTH];
|
||||
/* The last status the library defines for itself. Everything from
|
||||
* AKERR_LAST_ERRNO_VALUE + 1 through here must have a registered name. */
|
||||
#define AKERR_LAST_LIBRARY_STATUS AKERR_STATUS_NAME_INVALID
|
||||
|
||||
/*
|
||||
* Status values 0 through 255 are reserved by libakerror at akerr_init() time:
|
||||
* the host's errno values plus the AKERR_* codes above. Consumers allocate from
|
||||
* 256 upwards. Reserving any part of this band fails with
|
||||
* AKERR_STATUS_RANGE_OVERLAP naming AKERR_LIBRARY_OWNER.
|
||||
*/
|
||||
#define AKERR_LIBRARY_OWNER "libakerror"
|
||||
#define AKERR_RESERVED_STATUS_COUNT 256
|
||||
#define AKERR_FIRST_CONSUMER_STATUS AKERR_RESERVED_STATUS_COUNT
|
||||
|
||||
/*
|
||||
* The library reserves status values 0 through 255 for itself (see akerr_init),
|
||||
* which must contain every AKERR_* code above. AKERR_LAST_ERRNO_VALUE is
|
||||
* derived from the host's errno list at build time, so on a platform with an
|
||||
* unusually large errno space these codes could escape the band and collide
|
||||
* with consumer codes allocated at 256. Fail the build instead.
|
||||
*/
|
||||
typedef char akerr_assert_codes_within_reserved_band[(AKERR_LAST_LIBRARY_STATUS < 256) ? 1 : -1];
|
||||
|
||||
#define AKERR_MAX_ARRAY_ERROR 128
|
||||
|
||||
@@ -80,13 +107,56 @@ extern akerr_ErrorContext *__akerr_last_ignored;
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_release_error(akerr_ErrorContext *ptr);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_next_error();
|
||||
/*
|
||||
* Look up (name == NULL) or register (name != NULL) the display name for a
|
||||
* status. Registration succeeds only if some owner has reserved a range
|
||||
* containing `status`; prefer akerr_register_status_name(), which also checks
|
||||
* that the range belongs to you and raises an error saying why a registration
|
||||
* was refused. This entry point cannot return an error context, so a refusal
|
||||
* here is reported through akerr_log_method and reads back as "Unknown Error".
|
||||
* Never returns NULL -- an unregistered status reads back as "Unknown Error".
|
||||
*/
|
||||
char *akerr_name_for_status(int status, char *name);
|
||||
|
||||
/*
|
||||
* Register a display name for a status you own. `owner` must match the owner
|
||||
* string passed to akerr_reserve_status_range() for the range containing
|
||||
* `status`. Returns NULL on success, or an error context whose status is one of
|
||||
* the AKERR_STATUS_NAME_* codes above -- CATCH it, HANDLE it, or let it
|
||||
* propagate.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_register_status_name(const char *owner, int status, const char *name);
|
||||
|
||||
/*
|
||||
* Claim `count` status values starting at `first_status` for `owner`. Repeating
|
||||
* an identical reservation for the same owner is a no-op; any other collision is
|
||||
* refused. Returns NULL on success, or an error context whose status is one of
|
||||
* the AKERR_STATUS_RANGE_* codes above. Treat any error as an initialization
|
||||
* failure: either handle it or let it propagate out of your init function.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_reserve_status_range(int first_status, int count, const char *owner);
|
||||
void akerr_init();
|
||||
void akerr_default_handler_unhandled_error(akerr_ErrorContext *ptr);
|
||||
void akerr_default_logger(const char *f, ...);
|
||||
int akerr_valid_error_address(akerr_ErrorContext *ptr);
|
||||
/* defined in src/errno.c which is built dynamically at build time from system errno definitions */
|
||||
void akerr_init_errno(void);
|
||||
/*
|
||||
* Internal. Names a status in the library's own reserved band on behalf of
|
||||
* akerr_init() and the generated errno table, which have no caller to raise
|
||||
* into: a failure here is logged and terminates the program. Not part of the
|
||||
* consumer API -- use akerr_register_status_name(), which raises instead.
|
||||
*/
|
||||
void __akerr_name_library_status(int status, const char *name);
|
||||
|
||||
/*
|
||||
* Internal. Bounded string copy into a fixed buffer, always NUL-terminated.
|
||||
* Raises AKERR_NULLPOINTER for a NULL destination or source and AKERR_VALUE for
|
||||
* a capacity that leaves no room for a terminator. Exported so the library's
|
||||
* own tests can drive those guards; not part of the consumer API.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *__akerr_copy_string(char *destination, int capacity,
|
||||
const char *source);
|
||||
|
||||
#define LOG_ERROR_WITH_MESSAGE(__err_context, __err_message) \
|
||||
akerr_log_method("%s%s:%s:%d: %s %d (%s): %s", (char *)&__err_context->stacktracebuf, (char *)__FILE__, (char *)__func__, __LINE__, __err_message, __err_context->status, akerr_name_for_status(__err_context->status, NULL), __err_context->message); \
|
||||
@@ -110,8 +180,30 @@ void akerr_init_errno(void);
|
||||
akerr_log_method("%s:%s:%d: Unable to pull an error context from the array!", __FILE__, (char *)__func__, __LINE__); \
|
||||
exit(1); \
|
||||
} \
|
||||
__err_context->refcount += 1; \
|
||||
}
|
||||
|
||||
/*
|
||||
* Append a formatted line to the error's stack-trace buffer, bounded by the
|
||||
* space that remains so a deep propagation chain cannot write past the end of
|
||||
* stacktracebuf. snprintf reports the length it *would* have written, which on
|
||||
* truncation exceeds what it actually wrote, so the cursor advance is clamped
|
||||
* to the remaining space.
|
||||
*/
|
||||
#define AKERR_STACKTRACE_APPEND(__err_context, ...) \
|
||||
do { \
|
||||
char *__akerr_stb = (char *)__err_context->stacktracebuf; \
|
||||
size_t __akerr_used = (size_t)(__err_context->stacktracebufptr - __akerr_stb); \
|
||||
if ( __akerr_used < AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH ) { \
|
||||
size_t __akerr_rem = AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH - __akerr_used; \
|
||||
int __akerr_n = snprintf(__err_context->stacktracebufptr, __akerr_rem, __VA_ARGS__); \
|
||||
if ( __akerr_n < 0 ) { \
|
||||
__akerr_n = 0; \
|
||||
} \
|
||||
__err_context->refcount += 1;
|
||||
__err_context->stacktracebufptr += ((size_t)__akerr_n < __akerr_rem) \
|
||||
? (size_t)__akerr_n : (__akerr_rem - 1); \
|
||||
} \
|
||||
} while ( 0 )
|
||||
|
||||
/*
|
||||
* Failure and success methods for functions that return akerr_ErrorContext *
|
||||
@@ -168,11 +260,11 @@ void akerr_init_errno(void);
|
||||
#define FAIL(__err_context, __err, __message, ...) \
|
||||
ENSURE_ERROR_READY(__err_context); \
|
||||
__err_context->status = __err; \
|
||||
snprintf((char *)__err_context->fname, AKERR_MAX_ERROR_FNAME_LENGTH, __FILE__); \
|
||||
snprintf((char *)__err_context->function, AKERR_MAX_ERROR_FUNCTION_LENGTH, __func__); \
|
||||
snprintf((char *)__err_context->fname, AKERR_MAX_ERROR_FNAME_LENGTH, "%s", __FILE__); \
|
||||
snprintf((char *)__err_context->function, AKERR_MAX_ERROR_FUNCTION_LENGTH, "%s", __func__); \
|
||||
__err_context->lineno = __LINE__; \
|
||||
snprintf((char *)__err_context->message, AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH, __message, ## __VA_ARGS__); \
|
||||
__err_context->stacktracebufptr += snprintf(__err_context->stacktracebufptr, AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH, "%s:%s:%d: %d (%s) : %s\n", (char *)__err_context->fname, (char *)__err_context->function, __err_context->lineno, __err_context->status, akerr_name_for_status(__err_context->status, NULL), (__err_context->message == NULL ? "" : __err_context->message));
|
||||
AKERR_STACKTRACE_APPEND(__err_context, "%s:%s:%d: %d (%s) : %s\n", (char *)__err_context->fname, (char *)__err_context->function, __err_context->lineno, __err_context->status, akerr_name_for_status(__err_context->status, NULL), (__err_context->message == NULL ? "" : __err_context->message));
|
||||
|
||||
|
||||
#define SUCCEED(__err_context) \
|
||||
@@ -198,7 +290,7 @@ void akerr_init_errno(void);
|
||||
VALID(__err_context, __stmt); \
|
||||
if ( __err_context != NULL ) { \
|
||||
if ( __err_context->status != 0 ) { \
|
||||
__err_context->stacktracebufptr += snprintf(__err_context->stacktracebufptr, AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH, "%s:%s:%d\n", (char *)__FILE__, (char *)__func__, __LINE__); \
|
||||
AKERR_STACKTRACE_APPEND(__err_context, "%s:%s:%d\n", (char *)__FILE__, (char *)__func__, __LINE__); \
|
||||
break; \
|
||||
} \
|
||||
}
|
||||
|
||||
407
scripts/coverage.py
Executable file
407
scripts/coverage.py
Executable file
@@ -0,0 +1,407 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Code coverage harness for libakerror.
|
||||
|
||||
Coverage measures which parts of the library the CTest suite actually executes.
|
||||
It is the complement to mutation testing (scripts/mutation_test.py): coverage
|
||||
finds code the tests never reach, mutation testing finds code the tests reach
|
||||
but do not really check.
|
||||
|
||||
What is measured is the library's own translation units: src/error.c and the
|
||||
generated src/errno.c. The macros in the public header are deliberately not
|
||||
measured -- GCC attributes an expanded macro to its call site, so header logic
|
||||
would be reported as lines of the test that used it. Use mutation testing
|
||||
(--target include/akerror.tmpl.h) to check how well those macros are tested.
|
||||
|
||||
This harness has no third-party dependencies -- just gcov, which ships with the
|
||||
compiler, plus the project's normal cmake/ctest toolchain. By default it
|
||||
configures its own instrumented build directory so the ordinary build tree is
|
||||
left alone and coverage counters can never be stale.
|
||||
|
||||
Usage:
|
||||
scripts/coverage.py [options]
|
||||
|
||||
--source-root DIR repo root (default: parent of this script's dir)
|
||||
--build-dir DIR instrumented build dir (default: <root>/build/coverage)
|
||||
--no-configure reuse the build dir as-is (do not cmake/build)
|
||||
--no-run report existing counters (do not reset and run ctest)
|
||||
--threshold PCT exit non-zero if line coverage < PCT (default: 0 = off)
|
||||
--branch-threshold P exit non-zero if branch coverage < P (default: 0 = off)
|
||||
--junit PATH write a JUnit XML report to this path
|
||||
--max-uncovered N uncovered lines to list per file (default: 40, 0 = all)
|
||||
--exclude SUBSTR skip reported paths containing SUBSTR; repeatable
|
||||
--gcov PROG gcov program (default: $GCOV or "gcov")
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Running the instrumented suite
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def configure_and_build(cmake, root, build, jobs):
|
||||
"""Configure an instrumented build dir and build everything in it."""
|
||||
cmds = [
|
||||
[cmake, "-S", root, "-B", build, "-DAKERR_COVERAGE=ON"],
|
||||
[cmake, "--build", build] + (["--parallel", str(jobs)] if jobs else []),
|
||||
]
|
||||
for cmd in cmds:
|
||||
print("+ " + " ".join(cmd))
|
||||
if subprocess.call(cmd) != 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def reset_counters(build):
|
||||
"""Delete accumulated .gcda files so each run reports one suite run."""
|
||||
removed = 0
|
||||
for dirpath, _dirs, files in os.walk(build):
|
||||
for name in files:
|
||||
if name.endswith(".gcda"):
|
||||
os.unlink(os.path.join(dirpath, name))
|
||||
removed += 1
|
||||
if removed:
|
||||
print(f"Reset {removed} coverage data file(s).")
|
||||
|
||||
|
||||
def run_ctest(build, ctest):
|
||||
"""Run the suite. Returns True if every test passed.
|
||||
|
||||
Coverage counters are flushed at exit(), and the tests that are expected to
|
||||
fail exit via the library's unhandled-error handler (exit(), not abort()),
|
||||
so WILL_FAIL tests still contribute their counters.
|
||||
"""
|
||||
cmd = [ctest, "--output-on-failure"]
|
||||
print("+ " + " ".join(cmd) + f" (in {build})")
|
||||
return subprocess.call(cmd, cwd=build) == 0
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Collecting gcov data
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
class FileCov:
|
||||
"""Merged coverage for one source file, across every object that built it.
|
||||
|
||||
A file compiled into more than one object -- or a header included by several
|
||||
translation units -- is reported once, with counts summed. Branches are
|
||||
merged by (line, index within line), so differing expansions of the same
|
||||
line contribute the union of their branches.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.lines = collections.Counter() # lineno -> execution count
|
||||
self.branches = collections.Counter() # (lineno, idx) -> taken count
|
||||
self.funcs = collections.Counter() # (name, start_line) -> count
|
||||
|
||||
def merge(self, entry):
|
||||
for ln in entry.get("lines", []):
|
||||
no = ln["line_number"]
|
||||
self.lines[no] += ln.get("count", 0)
|
||||
for idx, br in enumerate(ln.get("branches", [])):
|
||||
if br.get("throw"):
|
||||
continue
|
||||
self.branches[(no, idx)] += br.get("count", 0)
|
||||
for fn in entry.get("functions", []):
|
||||
key = (fn.get("name", "?"), fn.get("start_line", 0))
|
||||
self.funcs[key] += fn.get("execution_count", 0)
|
||||
|
||||
@staticmethod
|
||||
def _ratio(counter):
|
||||
total = len(counter)
|
||||
hit = sum(1 for v in counter.values() if v > 0)
|
||||
return hit, total
|
||||
|
||||
def line_stats(self):
|
||||
return self._ratio(self.lines)
|
||||
|
||||
def branch_stats(self):
|
||||
return self._ratio(self.branches)
|
||||
|
||||
def func_stats(self):
|
||||
return self._ratio(self.funcs)
|
||||
|
||||
def uncovered_lines(self):
|
||||
return sorted(no for no, count in self.lines.items() if count == 0)
|
||||
|
||||
|
||||
def find_notes(build):
|
||||
"""Every .gcno in the build tree: one per instrumented translation unit."""
|
||||
notes = []
|
||||
for dirpath, _dirs, files in os.walk(build):
|
||||
for name in files:
|
||||
if name.endswith(".gcno"):
|
||||
notes.append(os.path.join(dirpath, name))
|
||||
return sorted(notes)
|
||||
|
||||
|
||||
def gcov_json(gcov, note, cwd):
|
||||
"""Run gcov on one .gcno and return its parsed JSON, or None on failure.
|
||||
|
||||
--stdout keeps gcov from littering .gcov files in the build tree. A .gcno
|
||||
with no matching .gcda still reports, with all counts zero, which is the
|
||||
correct answer for a translation unit no test executed.
|
||||
"""
|
||||
cmd = [gcov, "--branch-probabilities", "--json-format", "--stdout", note]
|
||||
proc = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
out, err = proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
sys.stderr.write(f"warning: {' '.join(cmd)} failed:\n"
|
||||
f"{err.decode(errors='replace')}")
|
||||
return None
|
||||
try:
|
||||
return json.loads(out.decode(errors="replace"))
|
||||
except ValueError as exc:
|
||||
sys.stderr.write(f"warning: unparseable gcov output for {note}: {exc}\n")
|
||||
return None
|
||||
|
||||
|
||||
def display_path(path, build, root):
|
||||
"""Label a source: build-relative for generated files, else repo-relative.
|
||||
|
||||
Returns None for anything outside both trees (system headers, toolchain
|
||||
internals). Generated sources are labelled relative to the build dir so the
|
||||
report and its thresholds do not change with the build dir's location.
|
||||
"""
|
||||
for base in (build, root):
|
||||
if path.startswith(base + os.sep):
|
||||
return os.path.relpath(path, base)
|
||||
return None
|
||||
|
||||
|
||||
def collect(gcov, build, root, excludes):
|
||||
"""Merge gcov data for every instrumented unit into {display path: FileCov}."""
|
||||
covs = {}
|
||||
for note in find_notes(build):
|
||||
data = gcov_json(gcov, note, build)
|
||||
if data is None:
|
||||
continue
|
||||
# Paths in the report are relative to the directory the unit was
|
||||
# compiled in, which gcov records in the notes file.
|
||||
compile_dir = data.get("current_working_directory") or build
|
||||
for entry in data.get("files", []):
|
||||
path = entry.get("file", "")
|
||||
if not os.path.isabs(path):
|
||||
path = os.path.join(compile_dir, path)
|
||||
display = display_path(os.path.realpath(path), build, root)
|
||||
if display is None:
|
||||
continue # system headers, toolchain internals
|
||||
if any(x in display for x in excludes):
|
||||
continue
|
||||
covs.setdefault(display, FileCov()).merge(entry)
|
||||
return covs
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Reporting
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def pct(hit, total):
|
||||
return 100.0 * hit / total if total else 100.0
|
||||
|
||||
|
||||
def fmt_ratio(hit, total):
|
||||
if not total:
|
||||
return f"{'-':>9} -"
|
||||
return f"{hit:>4}/{total:<4} {pct(hit, total):5.1f}%"
|
||||
|
||||
|
||||
def compress(numbers):
|
||||
"""[1,2,3,7,9,10] -> '1-3, 7, 9-10' for readable uncovered-line lists."""
|
||||
out, start, prev = [], None, None
|
||||
for n in numbers:
|
||||
if start is None:
|
||||
start = prev = n
|
||||
elif n == prev + 1:
|
||||
prev = n
|
||||
else:
|
||||
out.append(f"{start}" if start == prev else f"{start}-{prev}")
|
||||
start = prev = n
|
||||
if start is not None:
|
||||
out.append(f"{start}" if start == prev else f"{start}-{prev}")
|
||||
return ", ".join(out)
|
||||
|
||||
|
||||
def report(covs, max_uncovered):
|
||||
"""Print the per-file table and uncovered detail; return overall percentages."""
|
||||
width = max([len(p) for p in covs] + [len("TOTAL")])
|
||||
print("\n" + "=" * 72)
|
||||
print("CODE COVERAGE SUMMARY")
|
||||
print("=" * 72)
|
||||
print(f" {'FILE':<{width}} {'LINES':^15} {'BRANCHES':^15} FUNCS")
|
||||
|
||||
totals = [0, 0, 0, 0, 0, 0] # lines hit/total, branches hit/total, funcs
|
||||
for path in sorted(covs):
|
||||
cov = covs[path]
|
||||
lh, lt = cov.line_stats()
|
||||
bh, bt = cov.branch_stats()
|
||||
fh, ft = cov.func_stats()
|
||||
for i, v in enumerate((lh, lt, bh, bt, fh, ft)):
|
||||
totals[i] += v
|
||||
print(f" {path:<{width}} {fmt_ratio(lh, lt)} {fmt_ratio(bh, bt)} "
|
||||
f"{fh}/{ft}")
|
||||
|
||||
lh, lt, bh, bt, fh, ft = totals
|
||||
print(f" {'-' * width} {'-' * 15} {'-' * 15} -----")
|
||||
print(f" {'TOTAL':<{width}} {fmt_ratio(lh, lt)} {fmt_ratio(bh, bt)} "
|
||||
f"{fh}/{ft}")
|
||||
|
||||
for path in sorted(covs):
|
||||
missing = covs[path].uncovered_lines()
|
||||
if not missing:
|
||||
continue
|
||||
shown = missing if not max_uncovered else missing[:max_uncovered]
|
||||
more = "" if len(shown) == len(missing) else \
|
||||
f" ... (+{len(missing) - len(shown)} more)"
|
||||
print(f"\n uncovered in {path} ({len(missing)} line(s)):")
|
||||
print(f" {compress(shown)}{more}")
|
||||
|
||||
return pct(lh, lt), pct(bh, bt)
|
||||
|
||||
|
||||
def _xml_escape(text):
|
||||
return (str(text).replace("&", "&").replace("<", "<")
|
||||
.replace(">", ">").replace('"', """))
|
||||
|
||||
|
||||
def write_junit(path, covs, line_threshold, branch_threshold):
|
||||
"""One <testcase> per file per metric; below-threshold is a <failure>."""
|
||||
cases = []
|
||||
for f in sorted(covs):
|
||||
cov = covs[f]
|
||||
cases.append((f, "lines", cov.line_stats(), line_threshold))
|
||||
cases.append((f, "branches", cov.branch_stats(), branch_threshold))
|
||||
for metric, thr, stats in (("lines", line_threshold,
|
||||
[c.line_stats() for c in covs.values()]),
|
||||
("branches", branch_threshold,
|
||||
[c.branch_stats() for c in covs.values()])):
|
||||
cases.append(("TOTAL", metric,
|
||||
(sum(h for h, _t in stats), sum(t for _h, t in stats)),
|
||||
thr))
|
||||
|
||||
fails = sum(1 for _f, _m, (h, t), thr in cases
|
||||
if t and thr > 0 and pct(h, t) < thr)
|
||||
out = ['<?xml version="1.0" encoding="UTF-8"?>',
|
||||
f'<testsuites name="coverage" tests="{len(cases)}" '
|
||||
f'failures="{fails}">',
|
||||
f' <testsuite name="coverage" tests="{len(cases)}" '
|
||||
f'failures="{fails}">']
|
||||
for f, metric, (hit, total), thr in cases:
|
||||
name = _xml_escape(f"{f} {metric}")
|
||||
detail = _xml_escape(f"{hit}/{total} ({pct(hit, total):.1f}%)"
|
||||
if total else "no data")
|
||||
out.append(f' <testcase name="{name}" '
|
||||
f'classname="coverage.{_xml_escape(metric)}" time="0">')
|
||||
if total and thr > 0 and pct(hit, total) < thr:
|
||||
out.append(f' <failure message="{metric} coverage '
|
||||
f'{pct(hit, total):.1f}% < threshold {thr:.1f}%">'
|
||||
f'{detail}</failure>')
|
||||
else:
|
||||
out.append(f' <system-out>{detail}</system-out>')
|
||||
out.append(' </testcase>')
|
||||
out.append(' </testsuite>')
|
||||
out.append('</testsuites>')
|
||||
with open(path, "w") as fh:
|
||||
fh.write("\n".join(out) + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
# Line-buffer stdout so progress is visible live under CI / the cmake target.
|
||||
try:
|
||||
sys.stdout.reconfigure(line_buffering=True)
|
||||
except (AttributeError, ValueError):
|
||||
pass
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
default_root = os.path.dirname(here)
|
||||
|
||||
ap = argparse.ArgumentParser(description="Code coverage for libakerror")
|
||||
ap.add_argument("--source-root", default=default_root)
|
||||
ap.add_argument("--build-dir", default=None)
|
||||
ap.add_argument("--configure", dest="configure", action="store_true",
|
||||
default=True)
|
||||
ap.add_argument("--no-configure", dest="configure", action="store_false")
|
||||
ap.add_argument("--run", dest="run", action="store_true", default=True)
|
||||
ap.add_argument("--no-run", dest="run", action="store_false")
|
||||
ap.add_argument("--threshold", type=float, default=0.0,
|
||||
help="fail if line coverage is below this percentage")
|
||||
ap.add_argument("--branch-threshold", type=float, default=0.0,
|
||||
help="fail if branch coverage is below this percentage")
|
||||
ap.add_argument("--junit", default=None,
|
||||
help="write a JUnit XML report to this path")
|
||||
ap.add_argument("--max-uncovered", type=int, default=40)
|
||||
ap.add_argument("--exclude", action="append", default=None,
|
||||
help="skip reported paths containing this substring")
|
||||
ap.add_argument("--gcov", default=os.environ.get("GCOV", "gcov"))
|
||||
ap.add_argument("--cmake", default=os.environ.get("CMAKE", "cmake"))
|
||||
ap.add_argument("--ctest", default=os.environ.get("CTEST", "ctest"))
|
||||
ap.add_argument("-j", "--jobs", type=int, default=0)
|
||||
args = ap.parse_args()
|
||||
|
||||
root = os.path.realpath(args.source_root)
|
||||
build = os.path.realpath(args.build_dir or os.path.join(root, "build",
|
||||
"coverage"))
|
||||
# The tests exercise the library; their own source is not what we measure.
|
||||
excludes = args.exclude if args.exclude is not None else ["tests/"]
|
||||
|
||||
if args.configure:
|
||||
if not configure_and_build(args.cmake, root, build, args.jobs):
|
||||
sys.stderr.write("\nInstrumented build FAILED; aborting.\n")
|
||||
return 2
|
||||
elif not os.path.isdir(build):
|
||||
sys.stderr.write(f"No build dir at {build} (drop --no-configure).\n")
|
||||
return 2
|
||||
|
||||
tests_ok = True
|
||||
if args.run:
|
||||
reset_counters(build)
|
||||
tests_ok = run_ctest(build, args.ctest)
|
||||
if not tests_ok:
|
||||
sys.stderr.write("\nwarning: some tests FAILED; coverage below is "
|
||||
"still reported, but the run is not green.\n")
|
||||
|
||||
covs = collect(args.gcov, build, root, excludes)
|
||||
if not covs:
|
||||
sys.stderr.write("No coverage data found. Was the build instrumented "
|
||||
"(-DAKERR_COVERAGE=ON) and the suite run?\n")
|
||||
return 2
|
||||
|
||||
line_pct, branch_pct = report(covs, args.max_uncovered)
|
||||
|
||||
if args.junit:
|
||||
junit_path = os.path.abspath(args.junit)
|
||||
write_junit(junit_path, covs, args.threshold, args.branch_threshold)
|
||||
print(f"\nJUnit report written to: {junit_path}")
|
||||
|
||||
rc = 0
|
||||
if not tests_ok:
|
||||
print("\nFAIL: the CTest suite did not pass.")
|
||||
rc = 1
|
||||
|
||||
# Thresholds gate every file as well as the total: a small, well-covered
|
||||
# file (the generated status-name table) must not mask a regression in a
|
||||
# bigger one. Files with no branches at all are not branch-gated.
|
||||
checks = [("total", "line", line_pct, args.threshold),
|
||||
("total", "branch", branch_pct, args.branch_threshold)]
|
||||
for path in sorted(covs):
|
||||
lh, lt = covs[path].line_stats()
|
||||
bh, bt = covs[path].branch_stats()
|
||||
checks.append((path, "line", pct(lh, lt), args.threshold))
|
||||
if bt:
|
||||
checks.append((path, "branch", pct(bh, bt), args.branch_threshold))
|
||||
for path, metric, value, threshold in checks:
|
||||
if threshold > 0 and value < threshold:
|
||||
print(f"\nFAIL: {path} {metric} coverage {value:.1f}% < threshold "
|
||||
f"{threshold:.1f}%")
|
||||
rc = 1
|
||||
return rc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -8,13 +8,24 @@ mkdir -p ${outdir}/include
|
||||
rm -f ${outdir}/src/errno.c
|
||||
echo "#include <akerror.h>" >> ${outdir}/src/errno.c
|
||||
echo "#include <errno.h>" >> ${outdir}/src/errno.c
|
||||
cat >> ${outdir}/src/errno.c <<'EOF'
|
||||
|
||||
/*
|
||||
* These names belong to the library's own reserved band, and this runs from
|
||||
* akerr_init(), which has no caller to raise into -- so it goes through
|
||||
* __akerr_name_library_status(), which reports a refusal and terminates rather
|
||||
* than leaving every later stack trace to print "Unknown Error" for an errno.
|
||||
* Keeping the branch in src/error.c also keeps this generated file free of
|
||||
* control flow no test can reach.
|
||||
*/
|
||||
EOF
|
||||
echo "void akerr_init_errno(void) {" >> ${outdir}/src/errno.c
|
||||
maxval=$(errno --list | cut -d ' ' -f 2 | sort -g | tail -n 1)
|
||||
errno --list | while read LINE; do
|
||||
define=$(echo "$LINE" | cut -d ' ' -f 1);
|
||||
value=$(echo "$LINE" | cut -d ' ' -f 2);
|
||||
desc=$(echo "$LINE" | cut -d ' ' -f 3-);
|
||||
echo " akerr_name_for_status(${define}, \"${desc}\");" >> ${outdir}/src/errno.c ;
|
||||
echo " __akerr_name_library_status(${define}, \"${desc}\");" >> ${outdir}/src/errno.c ;
|
||||
done;
|
||||
echo "}" >> ${outdir}/src/errno.c
|
||||
sed "s/#define AKERR_LAST_ERRNO_VALUE .*/#define AKERR_LAST_ERRNO_VALUE ${maxval}/" ${srcdir}/include/akerror.tmpl.h > ${outdir}/include/akerror.h
|
||||
|
||||
432
src/error.c
432
src/error.c
@@ -10,18 +10,106 @@ akerr_ErrorContext *__akerr_last_ignored;
|
||||
akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
|
||||
akerr_ErrorLogFunction akerr_log_method = NULL;
|
||||
|
||||
char __AKERR_ERROR_NAMES[AKERR_MAX_ERR_VALUE+1][AKERR_MAX_ERROR_NAME_LENGTH];
|
||||
/*
|
||||
* Status-name registry.
|
||||
*
|
||||
* Storage is an open-addressed hash table keyed by status value. Both sizes are
|
||||
* private to this translation unit -- they are deliberately NOT in the public
|
||||
* header, because a consumer-visible table bound is exactly the ABI hazard this
|
||||
* registry replaced. Overriding them changes only this file, so a library and
|
||||
* its consumers can never disagree about the layout.
|
||||
*
|
||||
* The table is never resized or rehashed, so a pointer handed out by
|
||||
* akerr_name_for_status() stays valid for the life of the process. Entries are
|
||||
* never removed, so probing needs no tombstones.
|
||||
*/
|
||||
#ifndef AKERR_STATUS_NAME_SLOTS
|
||||
#define AKERR_STATUS_NAME_SLOTS 4096
|
||||
#endif
|
||||
|
||||
#ifndef AKERR_MAX_RESERVED_STATUS_RANGES
|
||||
#define AKERR_MAX_RESERVED_STATUS_RANGES 64
|
||||
#endif
|
||||
|
||||
/* Probing masks with SLOTS-1, so the slot count must be a power of two. This is
|
||||
* the C99-portable spelling of a static assertion (a negative array bound). */
|
||||
typedef char akerr_assert_name_slots_pow2[
|
||||
(AKERR_STATUS_NAME_SLOTS > 0 &&
|
||||
(AKERR_STATUS_NAME_SLOTS & (AKERR_STATUS_NAME_SLOTS - 1)) == 0) ? 1 : -1];
|
||||
|
||||
/* Cap occupancy at 75% so linear probing always meets an empty slot. */
|
||||
#define AKERR_MAX_REGISTERED_STATUS_NAMES \
|
||||
(AKERR_STATUS_NAME_SLOTS - (AKERR_STATUS_NAME_SLOTS / 4))
|
||||
|
||||
#define AKERR_MAX_STATUS_RANGE_OWNER_LENGTH 64
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int status;
|
||||
int used;
|
||||
char name[AKERR_MAX_ERROR_NAME_LENGTH];
|
||||
} akerr_StatusName;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int first;
|
||||
int last;
|
||||
char owner[AKERR_MAX_STATUS_RANGE_OWNER_LENGTH];
|
||||
} akerr_StatusRange;
|
||||
|
||||
static akerr_StatusName akerr_status_names[AKERR_STATUS_NAME_SLOTS];
|
||||
static int akerr_status_name_count;
|
||||
static akerr_StatusRange akerr_status_ranges[AKERR_MAX_RESERVED_STATUS_RANGES];
|
||||
static int akerr_status_range_count;
|
||||
|
||||
akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR];
|
||||
|
||||
/*
|
||||
* Bounded copy into a fixed buffer. Every argument is checked: this writes
|
||||
* through a caller-supplied pointer for a caller-supplied length, so a NULL or
|
||||
* a non-positive capacity here is a memory error waiting to happen, not
|
||||
* something to absorb and return from quietly.
|
||||
*
|
||||
* Exported under the internal __akerr_ prefix rather than kept static so that
|
||||
* tests/err_copy_string.c can reach these guards. Both in-library callers
|
||||
* validate their arguments first, so nothing else can drive them.
|
||||
*/
|
||||
akerr_ErrorContext *__akerr_copy_string(char *destination, int capacity,
|
||||
const char *source)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_NONZERO_RETURN(errctx, (destination == NULL || source == NULL),
|
||||
AKERR_NULLPOINTER,
|
||||
"__akerr_copy_string got a NULL %s",
|
||||
destination == NULL ? "destination buffer" : "source string");
|
||||
FAIL_NONZERO_RETURN(errctx, (capacity <= 0), AKERR_VALUE,
|
||||
"__akerr_copy_string got a capacity of %d; a buffer must "
|
||||
"have room for at least a terminator", capacity);
|
||||
|
||||
strncpy(destination, source, (size_t)capacity - 1);
|
||||
destination[capacity - 1] = '\0';
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/*
|
||||
* Compare against each element address rather than testing the address range.
|
||||
* A range test also accepts pointers into the *interior* of an element, which
|
||||
* would then be treated as the head of an akerr_ErrorContext and written
|
||||
* through. Keep this an element-wise scan; it is not a missed optimization.
|
||||
*/
|
||||
int akerr_valid_error_address(akerr_ErrorContext *ptr)
|
||||
{
|
||||
// Is this within the memory region occupied by AKERR_ARRAY_ERROR?
|
||||
if ( ptr == NULL ) {
|
||||
return 1;
|
||||
}
|
||||
return ((ptr >= &AKERR_ARRAY_ERROR[0]) &&
|
||||
(ptr <= &AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR-1]));
|
||||
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
||||
if ( ptr == &AKERR_ARRAY_ERROR[i] ) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void akerr_default_logger(const char *fmt, ...)
|
||||
@@ -37,10 +125,48 @@ void akerr_default_logger(const char *fmt, ...)
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* The library naming its own codes.
|
||||
*
|
||||
* akerr_init() returns void and runs before any consumer frame exists, so there
|
||||
* is nothing to PASS an error to: this *is* the top of the stack. FINISH_NORETURN
|
||||
* is the library's idiom for that position -- the same one main() uses -- so an
|
||||
* unhandled failure prints its stack trace and goes to
|
||||
* akerr_handler_unhandled_error, which terminates.
|
||||
*
|
||||
* That is fatal on purpose. The library can only fail to name its own status
|
||||
* codes if the build is misconfigured -- a name table too small to hold even
|
||||
* the library's own entries, or a reservation that did not take -- and the
|
||||
* consequence of continuing is every later stack trace in the process printing
|
||||
* "Unknown Error" for a code the library defines. That is a startup defect, and
|
||||
* it is far cheaper to see it at init than to debug it from a degraded trace.
|
||||
*
|
||||
* The generated errno table calls this rather than registering names directly,
|
||||
* so that all of this control flow lives here in one place.
|
||||
*/
|
||||
void __akerr_name_library_status(int status, const char *name)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akerr_register_status_name(AKERR_LIBRARY_OWNER, status, name));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
|
||||
/*
|
||||
* Idempotent. `inited` is set before any work so that the registry calls below
|
||||
* -- and the public registry entry points, which all call akerr_init() so that
|
||||
* a consumer reserving its range before anything else touches the library
|
||||
* cannot have that reservation wiped by a later first-use of the pool -- see
|
||||
* themselves as already initialized instead of recursing.
|
||||
*/
|
||||
void akerr_init()
|
||||
{
|
||||
static int inited = 0;
|
||||
if ( inited == 0 ) {
|
||||
inited = 1;
|
||||
for (int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
||||
memset((void *)&AKERR_ARRAY_ERROR[i], 0x00, sizeof(akerr_ErrorContext));
|
||||
AKERR_ARRAY_ERROR[i].arrayid = i;
|
||||
@@ -53,25 +179,53 @@ void akerr_init()
|
||||
akerr_log_method = &akerr_default_logger;
|
||||
}
|
||||
akerr_handler_unhandled_error = &akerr_default_handler_unhandled_error;
|
||||
memset((void *)&__AKERR_ERROR_NAMES[0], 0x00, ((AKERR_MAX_ERR_VALUE+1) * AKERR_MAX_ERROR_NAME_LENGTH));
|
||||
memset((void *)&akerr_status_names[0], 0x00, sizeof(akerr_status_names));
|
||||
memset((void *)&akerr_status_ranges[0], 0x00, sizeof(akerr_status_ranges));
|
||||
akerr_status_name_count = 0;
|
||||
akerr_status_range_count = 0;
|
||||
|
||||
akerr_name_for_status(AKERR_NULLPOINTER, "Null Pointer Error");
|
||||
akerr_name_for_status(AKERR_OUTOFBOUNDS, "Out Of Bounds Error");
|
||||
akerr_name_for_status(AKERR_API, "API Error");
|
||||
akerr_name_for_status(AKERR_ATTRIBUTE, "Attribute Error");
|
||||
akerr_name_for_status(AKERR_TYPE, "Type Error");
|
||||
akerr_name_for_status(AKERR_KEY, "Key Error");
|
||||
akerr_name_for_status(AKERR_INDEX, "Index Error");
|
||||
akerr_name_for_status(AKERR_FORMAT, "Format Error");
|
||||
akerr_name_for_status(AKERR_IO, "Input Output Error");
|
||||
akerr_name_for_status(AKERR_VALUE, "Value Error");
|
||||
akerr_name_for_status(AKERR_RELATIONSHIP, "Relationship Error");
|
||||
akerr_name_for_status(AKERR_CIRCULAR_REFERENCE, "Circular Reference Error");
|
||||
akerr_name_for_status(AKERR_BADEXC, "Invalid akerr_ErrorContext");
|
||||
/* errno and AKERR_* values are the library-owned compatibility band.
|
||||
* This must precede every registration below: naming a status is only
|
||||
* permitted inside a reserved range. Terminal for the same reason as
|
||||
* __akerr_name_library_status(), and handled the same way: without this
|
||||
* band the library owns nothing, so none of the names below could
|
||||
* register either. */
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
||||
AKERR_LIBRARY_OWNER));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
/* Every AKERR_* code gets a name; tests/err_error_names.c asserts the
|
||||
* list is exhaustive so a new code cannot be added without one. */
|
||||
__akerr_name_library_status(AKERR_NULLPOINTER, "Null Pointer Error");
|
||||
__akerr_name_library_status(AKERR_OUTOFBOUNDS, "Out Of Bounds Error");
|
||||
__akerr_name_library_status(AKERR_API, "API Error");
|
||||
__akerr_name_library_status(AKERR_ATTRIBUTE, "Attribute Error");
|
||||
__akerr_name_library_status(AKERR_TYPE, "Type Error");
|
||||
__akerr_name_library_status(AKERR_KEY, "Key Error");
|
||||
__akerr_name_library_status(AKERR_INDEX, "Index Error");
|
||||
__akerr_name_library_status(AKERR_FORMAT, "Format Error");
|
||||
__akerr_name_library_status(AKERR_IO, "Input Output Error");
|
||||
__akerr_name_library_status(AKERR_VALUE, "Value Error");
|
||||
__akerr_name_library_status(AKERR_RELATIONSHIP, "Relationship Error");
|
||||
__akerr_name_library_status(AKERR_EOF, "End Of File");
|
||||
__akerr_name_library_status(AKERR_CIRCULAR_REFERENCE, "Circular Reference Error");
|
||||
__akerr_name_library_status(AKERR_ITERATOR_BREAK, "Iterator Break");
|
||||
__akerr_name_library_status(AKERR_NOT_IMPLEMENTED, "Not Implemented");
|
||||
__akerr_name_library_status(AKERR_BADEXC, "Invalid akerr_ErrorContext");
|
||||
__akerr_name_library_status(AKERR_STATUS_RANGE_OVERLAP, "Status Range Overlap");
|
||||
__akerr_name_library_status(AKERR_STATUS_RANGE_FULL, "Status Range Table Full");
|
||||
__akerr_name_library_status(AKERR_STATUS_RANGE_INVALID, "Invalid Status Range");
|
||||
__akerr_name_library_status(AKERR_STATUS_NAME_UNRESERVED, "Unreserved Status Name");
|
||||
__akerr_name_library_status(AKERR_STATUS_NAME_FOREIGN, "Foreign Status Name");
|
||||
__akerr_name_library_status(AKERR_STATUS_NAME_FULL, "Status Name Registry Full");
|
||||
__akerr_name_library_status(AKERR_STATUS_NAME_INVALID, "Invalid Status Name");
|
||||
#if (defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1) || (!defined(AKERR_USE_STDLIB))
|
||||
akerr_init_errno();
|
||||
#endif
|
||||
inited = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,15 +268,245 @@ akerr_ErrorContext *akerr_release_error(akerr_ErrorContext *err)
|
||||
}
|
||||
|
||||
|
||||
// returns or sets the name for the given status.
|
||||
// Call with name = NULL to retrieve a status.
|
||||
/*
|
||||
* Scatter the status across the table. Status values are typically dense runs
|
||||
* (errno 1..N, then a library's block at its base), which linear probing on the
|
||||
* raw value would pile into one cluster, so mix the bits first.
|
||||
*/
|
||||
static unsigned akerr_status_hash(int status)
|
||||
{
|
||||
unsigned h = (unsigned)status;
|
||||
|
||||
h ^= h >> 16;
|
||||
h *= 0x85ebca6bu;
|
||||
h ^= h >> 13;
|
||||
h *= 0xc2b2ae35u;
|
||||
h ^= h >> 16;
|
||||
return h & (unsigned)(AKERR_STATUS_NAME_SLOTS - 1);
|
||||
}
|
||||
|
||||
/*
|
||||
* Find the slot holding `status`. With create != 0, claim a free slot for it if
|
||||
* it is not present yet. Returns NULL when the status is absent and either no
|
||||
* slot was requested or the registry is full.
|
||||
*
|
||||
* The `& (AKERR_STATUS_NAME_SLOTS - 1)` below is load-bearing and fails
|
||||
* silently: off by one in either direction and the probe indexes past
|
||||
* akerr_status_names, writing into whatever BSS follows rather than crashing.
|
||||
* A test that only counts how many names registered before the table filled
|
||||
* cannot see that -- a probe sequence collapsed to two slots still registers
|
||||
* "some" names. Any change to the probe sequence, the occupancy cap, or the
|
||||
* power-of-two assumption needs a test that reads every entry back by its own
|
||||
* distinct value; tests/err_maxval.c does.
|
||||
*/
|
||||
static akerr_StatusName *akerr_status_slot(int status, int create)
|
||||
{
|
||||
unsigned slot = akerr_status_hash(status);
|
||||
|
||||
for ( int probe = 0; probe < AKERR_STATUS_NAME_SLOTS; probe++ ) {
|
||||
akerr_StatusName *entry = &akerr_status_names[slot];
|
||||
|
||||
if ( entry->used == 0 ) {
|
||||
if ( create == 0 ||
|
||||
akerr_status_name_count >= AKERR_MAX_REGISTERED_STATUS_NAMES ) {
|
||||
return NULL;
|
||||
}
|
||||
entry->used = 1;
|
||||
entry->status = status;
|
||||
entry->name[0] = '\0';
|
||||
akerr_status_name_count++;
|
||||
return entry;
|
||||
}
|
||||
if ( entry->status == status ) {
|
||||
return entry;
|
||||
}
|
||||
slot = (slot + 1u) & (unsigned)(AKERR_STATUS_NAME_SLOTS - 1);
|
||||
}
|
||||
/* Unreachable: occupancy is capped below the slot count, so the probe above
|
||||
* always meets a free slot. Present so a future change to that cap cannot
|
||||
* turn this into a runaway loop. */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* The reservation covering `status`, or NULL if nobody has claimed it. */
|
||||
static akerr_StatusRange *akerr_range_for_status(int status)
|
||||
{
|
||||
for ( int i = 0; i < akerr_status_range_count; i++ ) {
|
||||
if ( status >= akerr_status_ranges[i].first &&
|
||||
status <= akerr_status_ranges[i].last ) {
|
||||
return &akerr_status_ranges[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* Shared body of both registration entry points. A NULL owner means the caller
|
||||
* did not identify itself (the legacy two-argument akerr_name_for_status path):
|
||||
* the status must still lie inside *some* reservation, but we cannot check that
|
||||
* it is the caller's. Every refusal raises an error -- a name that silently
|
||||
* fails to register degrades into "Unknown Error" in stack traces, which is
|
||||
* exactly the kind of quiet loss this registry exists to prevent -- so the
|
||||
* message carries everything a caller needs to see in a stack trace.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *akerr_store_status_name(const char *owner,
|
||||
int status,
|
||||
const char *name)
|
||||
{
|
||||
akerr_StatusRange *range;
|
||||
akerr_StatusName *entry;
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_NONZERO_RETURN(errctx, (name == NULL), AKERR_STATUS_NAME_INVALID,
|
||||
"Refusing to name status %d for %s: the name is NULL",
|
||||
status, owner == NULL ? "an unnamed caller" : owner);
|
||||
FAIL_NONZERO_RETURN(errctx,
|
||||
(owner != NULL && ( owner[0] == '\0' ||
|
||||
strlen(owner) >= AKERR_MAX_STATUS_RANGE_OWNER_LENGTH )),
|
||||
AKERR_STATUS_NAME_INVALID,
|
||||
"Refusing to name status %d (\"%s\"): the owner string "
|
||||
"is empty or longer than %d characters",
|
||||
status, name, AKERR_MAX_STATUS_RANGE_OWNER_LENGTH - 1);
|
||||
|
||||
range = akerr_range_for_status(status);
|
||||
FAIL_ZERO_RETURN(errctx, range, AKERR_STATUS_NAME_UNRESERVED,
|
||||
"Refusing to name status %d (\"%s\") for %s: no reserved "
|
||||
"range contains it. Call akerr_reserve_status_range() first.",
|
||||
status, name, owner == NULL ? "an unnamed caller" : owner);
|
||||
FAIL_NONZERO_RETURN(errctx,
|
||||
(owner != NULL && strcmp(owner, range->owner) != 0),
|
||||
AKERR_STATUS_NAME_FOREIGN,
|
||||
"Refusing to name status %d (\"%s\") for %s: that status "
|
||||
"is in range %d..%d owned by %s.",
|
||||
status, name, owner,
|
||||
range->first, range->last, range->owner);
|
||||
|
||||
entry = akerr_status_slot(status, 1);
|
||||
FAIL_ZERO_RETURN(errctx, entry, AKERR_STATUS_NAME_FULL,
|
||||
"Status name registry is full (%d entries); dropping name "
|
||||
"\"%s\" for status %d. Rebuild libakerror with a larger "
|
||||
"AKERR_STATUS_NAME_SLOTS.",
|
||||
AKERR_MAX_REGISTERED_STATUS_NAMES, name, status);
|
||||
|
||||
PASS(errctx, __akerr_copy_string(entry->name, AKERR_MAX_ERROR_NAME_LENGTH, name));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/*
|
||||
* Register a name for a status inside a range the caller reserved. Both strings
|
||||
* are checked here rather than only inside akerr_store_status_name(): the store
|
||||
* accepts a NULL owner for the legacy akerr_name_for_status() path, so a NULL
|
||||
* arriving through *this* entry point would be read as "caller did not identify
|
||||
* itself" and skip the ownership check entirely.
|
||||
*/
|
||||
akerr_ErrorContext *akerr_register_status_name(const char *owner, int status, const char *name)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_NONZERO_RETURN(errctx, (owner == NULL), AKERR_STATUS_NAME_INVALID,
|
||||
"Refusing to name status %d: the owner string is NULL. "
|
||||
"Pass the same owner you reserved the range with.",
|
||||
status);
|
||||
FAIL_NONZERO_RETURN(errctx, (name == NULL), AKERR_STATUS_NAME_INVALID,
|
||||
"Refusing to name status %d for %s: the name is NULL",
|
||||
status, owner);
|
||||
PASS(errctx, akerr_store_status_name(owner, status, name));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/*
|
||||
* Return or set a name. Status magnitude is unrelated to storage size.
|
||||
*
|
||||
* The set path is the legacy two-argument form. It returns a name, so it cannot
|
||||
* hand an error back to its caller and cannot raise: it handles the refusal
|
||||
* here, converting it to the "Unknown Error" sentinel the way any function that
|
||||
* must return a value converts a caught error into one.
|
||||
* akerr_register_status_name() is the form that raises.
|
||||
*
|
||||
* The lookup path (name == NULL) deliberately stays clear of all of this. FAIL
|
||||
* calls it to render a status into a stack trace, so it must not itself need an
|
||||
* error context.
|
||||
*/
|
||||
char *akerr_name_for_status(int status, char *name)
|
||||
{
|
||||
if ( status > AKERR_MAX_ERR_VALUE ) {
|
||||
akerr_StatusName *entry;
|
||||
|
||||
akerr_init();
|
||||
if ( name != NULL ) {
|
||||
PREPARE_ERROR(errctx);
|
||||
int refused = 0;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akerr_store_status_name(NULL, status, name));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE_DEFAULT(errctx) {
|
||||
LOG_ERROR_WITH_MESSAGE(errctx, "** REFUSED STATUS NAME **");
|
||||
refused = 1;
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
if ( refused != 0 ) {
|
||||
return "Unknown Error";
|
||||
}
|
||||
if ( name != NULL ) {
|
||||
strncpy((char *)&__AKERR_ERROR_NAMES[status], name, AKERR_MAX_ERROR_NAME_LENGTH);
|
||||
}
|
||||
return (char *)&__AKERR_ERROR_NAMES[status];
|
||||
entry = akerr_status_slot(status, 0);
|
||||
if ( entry == NULL ) {
|
||||
return "Unknown Error";
|
||||
}
|
||||
return entry->name;
|
||||
}
|
||||
|
||||
/* Reserve an inclusive status interval and reject collisions. */
|
||||
akerr_ErrorContext *akerr_reserve_status_range(int first_status, int count, const char *owner)
|
||||
{
|
||||
int last_status;
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_NONZERO_RETURN(errctx,
|
||||
(count <= 0 || owner == NULL || owner[0] == '\0' ||
|
||||
strlen(owner) >= AKERR_MAX_STATUS_RANGE_OWNER_LENGTH ||
|
||||
first_status > INT_MAX - (count - 1)),
|
||||
AKERR_STATUS_RANGE_INVALID,
|
||||
"Invalid status range reservation: %d status values from "
|
||||
"%d for %s (count must be positive, the owner string "
|
||||
"non-empty and shorter than %d characters, and the range "
|
||||
"must not overflow int)",
|
||||
count, first_status, owner == NULL ? "(null)" : owner,
|
||||
AKERR_MAX_STATUS_RANGE_OWNER_LENGTH);
|
||||
last_status = first_status + count - 1;
|
||||
|
||||
for ( int i = 0; i < akerr_status_range_count; i++ ) {
|
||||
if ( first_status <= akerr_status_ranges[i].last &&
|
||||
last_status >= akerr_status_ranges[i].first ) {
|
||||
if ( first_status == akerr_status_ranges[i].first &&
|
||||
last_status == akerr_status_ranges[i].last &&
|
||||
strcmp(owner, akerr_status_ranges[i].owner) == 0 ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
FAIL_RETURN(errctx, AKERR_STATUS_RANGE_OVERLAP,
|
||||
"Status range %d..%d requested by %s overlaps %d..%d "
|
||||
"owned by %s",
|
||||
first_status, last_status, owner,
|
||||
akerr_status_ranges[i].first,
|
||||
akerr_status_ranges[i].last,
|
||||
akerr_status_ranges[i].owner);
|
||||
}
|
||||
}
|
||||
FAIL_NONZERO_RETURN(errctx,
|
||||
(akerr_status_range_count == AKERR_MAX_RESERVED_STATUS_RANGES),
|
||||
AKERR_STATUS_RANGE_FULL,
|
||||
"Status range table is full (%d ranges); refusing %d..%d "
|
||||
"for %s.",
|
||||
AKERR_MAX_RESERVED_STATUS_RANGES,
|
||||
first_status, last_status, owner);
|
||||
|
||||
/* The owner copy is what commits the entry, so the count only advances
|
||||
* once it has succeeded -- a half-written reservation would claim the
|
||||
* range under an empty owner nobody could ever match. */
|
||||
akerr_status_ranges[akerr_status_range_count].first = first_status;
|
||||
akerr_status_ranges[akerr_status_range_count].last = last_status;
|
||||
PASS(errctx, __akerr_copy_string(akerr_status_ranges[akerr_status_range_count].owner,
|
||||
AKERR_MAX_STATUS_RANGE_OWNER_LENGTH, owner));
|
||||
akerr_status_range_count++;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
5
test.sh
Normal file
5
test.sh
Normal file
@@ -0,0 +1,5 @@
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
ctest --test-dir build --output-on-failure --output-junit "$(pwd)/ctest-junit.xml"
|
||||
python3 scripts/mutation_test.py --target src/error.c --junit mutation-junit.xml --threshold 65
|
||||
python3 scripts/coverage.py --junit coverage-junit.xml --threshold 90 --branch-threshold 50
|
||||
@@ -94,7 +94,7 @@ 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
|
||||
`src/error.c` scores ~77% (the CI gate is set to 65% for headroom). The
|
||||
remaining survivors are dominated by:
|
||||
|
||||
* **Equivalent mutants** in `akerr_init`: deleting the `memset`/`NULL` setup of
|
||||
@@ -106,12 +106,27 @@ remaining survivors are dominated by:
|
||||
`errctx == NULL` branch, `exit(1)`): killing these needs a subprocess-based
|
||||
test that captures a child's stderr and exit code, rather than the in-process
|
||||
capturing logger the other tests use.
|
||||
* **Static assertions** (`akerr_assert_name_slots_pow2` and the occupancy cap
|
||||
it guards): a mutated compile-time assertion that still compiles has no
|
||||
runtime behavior to observe. Unkillable by construction — the assertion is
|
||||
itself the test, and `tests/err_maxval.c` covers the runtime consequence.
|
||||
* **Hash and probe details** in `akerr_status_slot`: dropping one of the
|
||||
multiply steps in `akerr_status_hash` leaves a worse but still correct hash,
|
||||
and probing backwards (`slot - 1u`) is an equally valid sequence over a
|
||||
power-of-two table. Both are behaviorally equivalent.
|
||||
* **The `capacity <= 0` guard** in `akerr_copy_string`, which is defensive: both
|
||||
call sites pass a positive constant.
|
||||
|
||||
Findings surfaced by mutation testing:
|
||||
|
||||
* **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.
|
||||
* **Superseded:** status names now use a private sparse registry, so the old
|
||||
public `AKERR_MAX_ERR_VALUE` ceiling and its consumer ABI mismatch no longer
|
||||
exist. `tests/err_maxval.c` covers arbitrary `int` values and registry
|
||||
exhaustion.
|
||||
* **Fixed:** the open-addressing probe mask (`& (AKERR_STATUS_NAME_SLOTS - 1)`)
|
||||
could be mutated to `- 0` or `+ 1` — both of which index past the end of the
|
||||
table — without any test noticing. `tests/err_maxval.c` only asserted that
|
||||
*some* names registered before the table filled, which a collapsed probe
|
||||
sequence still satisfies. It now requires a substantial number of entries and
|
||||
reads every one of them back by its own distinct name, so a probe that
|
||||
revisits slots fails on both counts.
|
||||
|
||||
@@ -63,26 +63,26 @@ int main(void)
|
||||
akerr_ErrorContext *r;
|
||||
|
||||
r = zero_break(0);
|
||||
AKERR_CHECK(r != NULL && r->status == AKERR_VALUE);
|
||||
AKERR_CHECK_STATUS(r, 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);
|
||||
AKERR_CHECK_STATUS(r, AKERR_INDEX);
|
||||
r = akerr_release_error(r);
|
||||
AKERR_CHECK(nonzero_break(0) == NULL);
|
||||
|
||||
r = always_break();
|
||||
AKERR_CHECK(r != NULL && r->status == AKERR_IO);
|
||||
AKERR_CHECK_STATUS(r, AKERR_IO);
|
||||
r = akerr_release_error(r);
|
||||
|
||||
r = zero_return(0);
|
||||
AKERR_CHECK(r != NULL && r->status == AKERR_KEY);
|
||||
AKERR_CHECK_STATUS(r, 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);
|
||||
AKERR_CHECK_STATUS(r, AKERR_TYPE);
|
||||
r = akerr_release_error(r);
|
||||
AKERR_CHECK(nonzero_return(0) == NULL);
|
||||
|
||||
|
||||
@@ -75,6 +75,53 @@ static int __attribute__((unused)) akerr_slots_in_use(void)
|
||||
} \
|
||||
} while ( 0 )
|
||||
|
||||
#define AKERR_CHECK_STATUS(errctx, expected_status) \
|
||||
do { \
|
||||
AKERR_CHECK((errctx) != NULL); \
|
||||
AKERR_CHECK((errctx)->status == (expected_status)); \
|
||||
} while ( 0 )
|
||||
|
||||
/*
|
||||
* Helpers for functions that report failure by returning akerr_ErrorContext *.
|
||||
* Both release the context they consume, so a test that makes thousands of
|
||||
* failing calls cannot exhaust the pool. AKERR_CHECK_RAISES keeps a copy of the
|
||||
* message for AKERR_CHECK_MESSAGE_CONTAINS, since the context is gone by then.
|
||||
*/
|
||||
static char __attribute__((unused)) akerr_last_message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH];
|
||||
|
||||
#define AKERR_CHECK_SUCCEEDS(expr) \
|
||||
do { \
|
||||
akerr_ErrorContext *__akerr_result = (expr); \
|
||||
if ( __akerr_result != NULL ) { \
|
||||
fprintf(stderr, "UNEXPECTED ERROR from %s: %d (%s): %s" \
|
||||
" at %s:%d\n", #expr, __akerr_result->status, \
|
||||
akerr_name_for_status(__akerr_result->status, NULL), \
|
||||
__akerr_result->message, __FILE__, __LINE__); \
|
||||
RELEASE_ERROR(__akerr_result); \
|
||||
return 1; \
|
||||
} \
|
||||
} while ( 0 )
|
||||
|
||||
#define AKERR_CHECK_RAISES(expr, expected_status) \
|
||||
do { \
|
||||
akerr_ErrorContext *__akerr_result = (expr); \
|
||||
AKERR_CHECK(__akerr_result != NULL); \
|
||||
snprintf(akerr_last_message, sizeof(akerr_last_message), "%s", \
|
||||
__akerr_result->message); \
|
||||
if ( __akerr_result->status != (expected_status) ) { \
|
||||
fprintf(stderr, "WRONG STATUS from %s: got %d, want %s" \
|
||||
" at %s:%d\n", #expr, __akerr_result->status, \
|
||||
#expected_status, __FILE__, __LINE__); \
|
||||
RELEASE_ERROR(__akerr_result); \
|
||||
return 1; \
|
||||
} \
|
||||
RELEASE_ERROR(__akerr_result); \
|
||||
AKERR_CHECK(__akerr_result == NULL); \
|
||||
} while ( 0 )
|
||||
|
||||
#define AKERR_CHECK_MESSAGE_CONTAINS(needle) \
|
||||
AKERR_CHECK(strstr(akerr_last_message, (needle)) != NULL)
|
||||
|
||||
#define AKERR_CHECK_CONTAINS(needle) \
|
||||
AKERR_CHECK(strstr(akerr_capture_buf, (needle)) != NULL)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
|
||||
akerr_ErrorContext *func2(void)
|
||||
{
|
||||
@@ -31,6 +32,11 @@ int main(void)
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
AKERR_CHECK_STATUS(errctx, AKERR_NULLPOINTER);
|
||||
akerr_log_method("Caught exception");
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
fprintf(stderr, "err_catch ok\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
|
||||
int x;
|
||||
|
||||
@@ -34,10 +35,15 @@ int main(void)
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
AKERR_CHECK_STATUS(errctx, AKERR_NULLPOINTER);
|
||||
if ( x == 0 ) {
|
||||
fprintf(stderr, "Cleanup works\n");
|
||||
return 0;
|
||||
}
|
||||
akerr_log_method("Cleanup works\n");
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
fprintf(stderr, "err_cleanup ok\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
58
tests/err_copy_string.c
Normal file
58
tests/err_copy_string.c
Normal file
@@ -0,0 +1,58 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* __akerr_copy_string() is the only place in the library that writes through a
|
||||
* caller-supplied pointer for a caller-supplied length, so it validates every
|
||||
* argument and raises rather than returning quietly. Both in-library callers
|
||||
* check their arguments before calling it, so this test is what drives those
|
||||
* guards -- without it they are unreachable code that no build ever exercises.
|
||||
*/
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char buf[8];
|
||||
|
||||
akerr_capture_install();
|
||||
akerr_init();
|
||||
|
||||
/* The happy path: bounded, and always terminated. */
|
||||
memset(buf, 'x', sizeof(buf));
|
||||
AKERR_CHECK_SUCCEEDS(__akerr_copy_string(buf, (int)sizeof(buf), "abc"));
|
||||
AKERR_CHECK(strcmp(buf, "abc") == 0);
|
||||
|
||||
/* A source longer than the buffer is truncated, never overrun. */
|
||||
memset(buf, 'x', sizeof(buf));
|
||||
AKERR_CHECK_SUCCEEDS(__akerr_copy_string(buf, (int)sizeof(buf), "abcdefghijkl"));
|
||||
AKERR_CHECK(strlen(buf) == sizeof(buf) - 1);
|
||||
AKERR_CHECK(buf[sizeof(buf) - 1] == '\0');
|
||||
AKERR_CHECK(strcmp(buf, "abcdefg") == 0);
|
||||
|
||||
/* A capacity of exactly one holds nothing but the terminator. */
|
||||
memset(buf, 'x', sizeof(buf));
|
||||
AKERR_CHECK_SUCCEEDS(__akerr_copy_string(buf, 1, "abc"));
|
||||
AKERR_CHECK(buf[0] == '\0');
|
||||
AKERR_CHECK(buf[1] == 'x'); /* and wrote nothing past its capacity */
|
||||
|
||||
/* NULL pointers raise instead of faulting, and the message says which. */
|
||||
AKERR_CHECK_RAISES(__akerr_copy_string(NULL, (int)sizeof(buf), "abc"),
|
||||
AKERR_NULLPOINTER);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("destination");
|
||||
AKERR_CHECK_RAISES(__akerr_copy_string(buf, (int)sizeof(buf), NULL),
|
||||
AKERR_NULLPOINTER);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("source");
|
||||
|
||||
/* A capacity with no room for a terminator is a value error, not a write. */
|
||||
memset(buf, 'x', sizeof(buf));
|
||||
AKERR_CHECK_RAISES(__akerr_copy_string(buf, 0, "abc"), AKERR_VALUE);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("capacity of 0");
|
||||
AKERR_CHECK_RAISES(__akerr_copy_string(buf, -1, "abc"), AKERR_VALUE);
|
||||
AKERR_CHECK(buf[0] == 'x'); /* nothing was written */
|
||||
|
||||
/* Each refusal handed its context back to the pool. */
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
fprintf(stderr, "err_copy_string ok\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
* 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;
|
||||
* - an unregistered status returns the "Unknown Error" sentinel;
|
||||
* - a system errno can be raised, propagated and handled like any AKERR_* code.
|
||||
*/
|
||||
|
||||
@@ -28,7 +28,7 @@ int main(void)
|
||||
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),
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(1000000, NULL),
|
||||
"Unknown Error") == 0);
|
||||
|
||||
PREPARE_ERROR(e);
|
||||
@@ -37,6 +37,7 @@ int main(void)
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} HANDLE(e, EACCES) {
|
||||
AKERR_CHECK_STATUS(e, EACCES);
|
||||
handled = 1;
|
||||
} FINISH_NORETURN(e);
|
||||
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
* 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.
|
||||
* This list must stay exhaustive. AKERR_EOF, AKERR_ITERATOR_BREAK and
|
||||
* AKERR_NOT_IMPLEMENTED were previously valid codes with no registered name, so
|
||||
* they rendered as "Unknown Error" in every stack trace that carried them --
|
||||
* the same class of silent gap that a too-small AKERR_MAX_ERR_VALUE used to
|
||||
* cause. The sweep below walks the whole AKERR_* offset span so a newly added
|
||||
* code without a name fails here rather than showing up in production traces.
|
||||
*/
|
||||
|
||||
static const struct {
|
||||
@@ -27,8 +30,18 @@ static const struct {
|
||||
{ AKERR_IO, "Input Output Error" },
|
||||
{ AKERR_VALUE, "Value Error" },
|
||||
{ AKERR_RELATIONSHIP, "Relationship Error" },
|
||||
{ AKERR_EOF, "End Of File" },
|
||||
{ AKERR_CIRCULAR_REFERENCE, "Circular Reference Error" },
|
||||
{ AKERR_ITERATOR_BREAK, "Iterator Break" },
|
||||
{ AKERR_NOT_IMPLEMENTED, "Not Implemented" },
|
||||
{ AKERR_BADEXC, "Invalid akerr_ErrorContext" },
|
||||
{ AKERR_STATUS_RANGE_OVERLAP, "Status Range Overlap" },
|
||||
{ AKERR_STATUS_RANGE_FULL, "Status Range Table Full" },
|
||||
{ AKERR_STATUS_RANGE_INVALID, "Invalid Status Range" },
|
||||
{ AKERR_STATUS_NAME_UNRESERVED, "Unreserved Status Name" },
|
||||
{ AKERR_STATUS_NAME_FOREIGN, "Foreign Status Name" },
|
||||
{ AKERR_STATUS_NAME_FULL, "Status Name Registry Full" },
|
||||
{ AKERR_STATUS_NAME_INVALID, "Invalid Status Name" },
|
||||
};
|
||||
|
||||
int main(void)
|
||||
@@ -41,6 +54,30 @@ int main(void)
|
||||
AKERR_CHECK(strcmp(nm, expected[i].name) == 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* Every value in the library's own offset span must resolve to a real name.
|
||||
* AKERR_LAST_ERRNO_VALUE + 7 is the one deliberate hole (a removed code);
|
||||
* anything else nameless is a code someone added without registering it.
|
||||
*/
|
||||
for ( int offset = 1;
|
||||
offset <= AKERR_LAST_LIBRARY_STATUS - AKERR_LAST_ERRNO_VALUE;
|
||||
offset++ ) {
|
||||
int code = AKERR_LAST_ERRNO_VALUE + offset;
|
||||
char *nm = akerr_name_for_status(code, NULL);
|
||||
if ( offset == 7 ) {
|
||||
AKERR_CHECK(strcmp(nm, "Unknown Error") == 0);
|
||||
continue;
|
||||
}
|
||||
if ( strcmp(nm, "Unknown Error") == 0 || nm[0] == '\0' ) {
|
||||
fprintf(stderr, "AKERR_LAST_ERRNO_VALUE + %d (%d) has no name\n",
|
||||
offset, code);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Every AKERR_* code must sit inside the band the library reserves. */
|
||||
AKERR_CHECK(AKERR_LAST_LIBRARY_STATUS < AKERR_FIRST_CONSUMER_STATUS);
|
||||
|
||||
fprintf(stderr, "err_error_names ok\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
34
tests/err_format_string.c
Normal file
34
tests/err_format_string.c
Normal file
@@ -0,0 +1,34 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* FAIL records the source file and function names with snprintf. Those names
|
||||
* must be passed as %s ARGUMENTS, not used as the format string -- otherwise a
|
||||
* path containing a printf conversion (say a build directory with a '%') is
|
||||
* interpreted as a format and reads nonexistent varargs (undefined behavior).
|
||||
*
|
||||
* #line lets us make __FILE__ contain a conversion specifier; the stored name
|
||||
* must come back verbatim.
|
||||
*/
|
||||
|
||||
#line 1 "pct%dname.c"
|
||||
akerr_ErrorContext *raise_with_percent_in_filename(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_RETURN(e, AKERR_VALUE, "boom");
|
||||
}
|
||||
#line 22 "tests/err_format_string.c"
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akerr_init();
|
||||
|
||||
akerr_ErrorContext *e = raise_with_percent_in_filename();
|
||||
AKERR_CHECK(e != NULL);
|
||||
AKERR_CHECK(strcmp(e->fname, "pct%dname.c") == 0);
|
||||
|
||||
e = akerr_release_error(e);
|
||||
fprintf(stderr, "err_format_string ok\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
static int specific_fired = 0;
|
||||
static int default_fired = 0;
|
||||
static int default_status = 0;
|
||||
|
||||
akerr_ErrorContext *boom(void)
|
||||
{
|
||||
@@ -28,10 +29,12 @@ int main(void)
|
||||
specific_fired = 1; /* must NOT run: error is AKERR_TYPE */
|
||||
} HANDLE_DEFAULT(e) {
|
||||
default_fired = 1;
|
||||
default_status = e->status;
|
||||
} FINISH_NORETURN(e);
|
||||
|
||||
AKERR_CHECK(specific_fired == 0);
|
||||
AKERR_CHECK(default_fired == 1);
|
||||
AKERR_CHECK(default_status == AKERR_TYPE);
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
fprintf(stderr, "err_handle_default ok\n");
|
||||
return 0;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
static int a_fired = 0;
|
||||
static int b_fired = 0;
|
||||
static int c_fired = 0;
|
||||
static int b_status = 0;
|
||||
|
||||
akerr_ErrorContext *boom(void)
|
||||
{
|
||||
@@ -29,12 +30,14 @@ int main(void)
|
||||
a_fired = 1;
|
||||
} HANDLE(e, AKERR_TYPE) {
|
||||
b_fired = 1;
|
||||
b_status = e->status;
|
||||
} HANDLE(e, AKERR_IO) {
|
||||
c_fired = 1;
|
||||
} FINISH_NORETURN(e);
|
||||
|
||||
AKERR_CHECK(a_fired == 0);
|
||||
AKERR_CHECK(b_fired == 1);
|
||||
AKERR_CHECK(b_status == AKERR_TYPE);
|
||||
AKERR_CHECK(c_fired == 0);
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
fprintf(stderr, "err_handle_dispatch ok\n");
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
static int group_fired = 0;
|
||||
static int other_fired = 0;
|
||||
static int group_status = 0;
|
||||
static int other_status = 0;
|
||||
|
||||
akerr_ErrorContext *boom(int status)
|
||||
{
|
||||
@@ -28,8 +30,10 @@ akerr_ErrorContext *run(int status)
|
||||
} HANDLE(e, AKERR_KEY)
|
||||
HANDLE_GROUP(e, AKERR_INDEX) {
|
||||
group_fired++;
|
||||
group_status = e->status;
|
||||
} HANDLE(e, AKERR_IO) {
|
||||
other_fired++;
|
||||
other_status = e->status;
|
||||
} FINISH(e, false);
|
||||
return e;
|
||||
}
|
||||
@@ -40,15 +44,18 @@ int main(void)
|
||||
|
||||
run(AKERR_KEY);
|
||||
AKERR_CHECK(group_fired == 1);
|
||||
AKERR_CHECK(group_status == AKERR_KEY);
|
||||
AKERR_CHECK(other_fired == 0);
|
||||
|
||||
run(AKERR_INDEX);
|
||||
AKERR_CHECK(group_fired == 2);
|
||||
AKERR_CHECK(group_status == AKERR_INDEX);
|
||||
AKERR_CHECK(other_fired == 0);
|
||||
|
||||
run(AKERR_IO);
|
||||
AKERR_CHECK(group_fired == 2);
|
||||
AKERR_CHECK(other_fired == 1);
|
||||
AKERR_CHECK(other_status == AKERR_IO);
|
||||
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
fprintf(stderr, "err_handle_group ok\n");
|
||||
|
||||
31
tests/err_library_status_fatal.c
Normal file
31
tests/err_library_status_fatal.c
Normal file
@@ -0,0 +1,31 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
|
||||
/*
|
||||
* The library naming one of its own codes is not allowed to fail quietly.
|
||||
* __akerr_name_library_status() runs from akerr_init() and from the generated
|
||||
* errno table, neither of which has a caller to raise into, so a refusal there
|
||||
* goes through FINISH_NORETURN: stack trace, then akerr_handler_unhandled_error,
|
||||
* which terminates the process.
|
||||
*
|
||||
* In a correct build that can only happen with a name table too small to hold
|
||||
* the library's own entries, which no test can configure (the slot count is
|
||||
* PRIVATE to the library target). Calling the helper for a status the library
|
||||
* does not own reaches the same refusal, so this test covers the terminal path
|
||||
* itself.
|
||||
*
|
||||
* Registered in AKERR_WILL_FAIL_TESTS: reaching the end of main() means the
|
||||
* failure was swallowed, and that is the bug this test exists to catch.
|
||||
*/
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akerr_init();
|
||||
|
||||
/* Nobody has reserved 9999, so this registration is refused. */
|
||||
__akerr_name_library_status(9999, "Not The Library's To Name");
|
||||
|
||||
fprintf(stderr, "err_library_status_fatal: a refused library-status "
|
||||
"registration did NOT terminate\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -1,92 +1,165 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
#include <regex.h>
|
||||
#include <limits.h>
|
||||
#include <string.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).
|
||||
* Status magnitude is no longer coupled to a public array bound: any int is a
|
||||
* legal status, and storage is a private sparse registry. What bounds the
|
||||
* registry now is its *capacity*, not the value of the largest code.
|
||||
*
|
||||
* 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.
|
||||
* Covers: arbitrary int status values, name truncation, range reservation
|
||||
* semantics (overlap, idempotency, endpoints, validation, overflow), and both
|
||||
* capacity limits -- the range table and the name table -- each of which must
|
||||
* raise rather than dropping the registration quietly.
|
||||
*
|
||||
* Every refusal is an error context the caller owns, so each check below also
|
||||
* asserts the context returns to the pool; a leak here would exhaust the
|
||||
* 128-slot pool long before these loops finish.
|
||||
*/
|
||||
|
||||
#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);
|
||||
akerr_capture_install();
|
||||
akerr_init();
|
||||
|
||||
/* #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);
|
||||
/* Any int is a legal status, at either extreme of the range. */
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(INT_MIN, 1, "min-owner"));
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(INT_MAX, 1, "max-owner"));
|
||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("max-owner", INT_MAX, "Maximum Status"));
|
||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("min-owner", INT_MIN, "Minimum Status"));
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MAX, NULL), "Maximum Status") == 0);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MIN, NULL), "Minimum Status") == 0);
|
||||
|
||||
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;
|
||||
/* A name longer than the buffer is truncated and always terminated. */
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(1000000, 1, "trunc"));
|
||||
const char *long_name =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-extra";
|
||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("trunc", 1000000, long_name));
|
||||
char *stored = akerr_name_for_status(1000000, NULL);
|
||||
AKERR_CHECK(strlen(stored) == AKERR_MAX_ERROR_NAME_LENGTH - 1);
|
||||
AKERR_CHECK(stored[AKERR_MAX_ERROR_NAME_LENGTH - 1] == '\0');
|
||||
|
||||
char line[4096];
|
||||
regmatch_t m[3];
|
||||
while ( fgets(line, sizeof(line), fh) != NULL ) {
|
||||
if ( regexec(&re, line, 3, m, 0) != 0 ) {
|
||||
continue;
|
||||
/* Reservation: overlap detection, and idempotency for an exact repeat. */
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "component-a"));
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "component-a"));
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(260, 2, "component-b"),
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("component-a");
|
||||
|
||||
/* The library's own 0..255 band is reserved and cannot be encroached on. */
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(255, 1, "component-b"),
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS(AKERR_LIBRARY_OWNER);
|
||||
/* An exact repeat by the owner is still a no-op. */
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
||||
AKERR_LIBRARY_OWNER));
|
||||
|
||||
/* Argument validation. */
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(INT_MAX, 2, "overflow"),
|
||||
AKERR_STATUS_RANGE_INVALID);
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(300, 0, "empty"),
|
||||
AKERR_STATUS_RANGE_INVALID);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("empty"); /* the message names the caller */
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(300, -1, "negative"),
|
||||
AKERR_STATUS_RANGE_INVALID);
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(300, 1, NULL),
|
||||
AKERR_STATUS_RANGE_INVALID);
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(300, 1, ""),
|
||||
AKERR_STATUS_RANGE_INVALID);
|
||||
|
||||
/* Owner strings: 63 chars fit, 64 do not, and neither does anything past. */
|
||||
char owner63[AKERR_MAX_ERROR_NAME_LENGTH];
|
||||
char owner64[AKERR_MAX_ERROR_NAME_LENGTH + 1];
|
||||
char owner70[AKERR_MAX_ERROR_NAME_LENGTH + 7];
|
||||
memset(owner63, 'a', sizeof(owner63) - 1);
|
||||
owner63[sizeof(owner63) - 1] = '\0';
|
||||
memset(owner64, 'b', sizeof(owner64) - 1);
|
||||
owner64[sizeof(owner64) - 1] = '\0';
|
||||
memset(owner70, 'c', sizeof(owner70) - 1);
|
||||
owner70[sizeof(owner70) - 1] = '\0';
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(400, 1, owner63));
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(401, 1, owner64),
|
||||
AKERR_STATUS_RANGE_INVALID);
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(402, 1, owner70),
|
||||
AKERR_STATUS_RANGE_INVALID);
|
||||
|
||||
/* Partial overlaps at either endpoint, and a same-range different owner. */
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(500, 2, "endpoint"));
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(499, 2, "left"),
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(500, 1, "endpoint"),
|
||||
AKERR_STATUS_RANGE_OVERLAP); /* subset, not an exact repeat */
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(501, 1, "endpoint"),
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(500, 2, "other"),
|
||||
AKERR_STATUS_RANGE_OVERLAP); /* same range, wrong owner */
|
||||
|
||||
/* Claim room for the name-exhaustion sweep before filling the range table. */
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(2000000, 100000, "fill"));
|
||||
|
||||
/*
|
||||
* Range table capacity. The limit is private to src/error.c on purpose, so
|
||||
* discover it by filling rather than by hardcoding it here.
|
||||
*/
|
||||
int ranges_added = 0;
|
||||
akerr_ErrorContext *range_err = NULL;
|
||||
for ( int i = 0; i < 100000; i++ ) {
|
||||
range_err = akerr_reserve_status_range(1000 + (i * 2), 1, "pad");
|
||||
if ( range_err != NULL ) {
|
||||
break;
|
||||
}
|
||||
|
||||
char name[64];
|
||||
int nlen = (int)(m[1].rm_eo - m[1].rm_so);
|
||||
if ( nlen >= (int)sizeof(name) ) {
|
||||
nlen = (int)sizeof(name) - 1;
|
||||
ranges_added++;
|
||||
}
|
||||
memcpy(name, line + m[1].rm_so, nlen);
|
||||
name[nlen] = '\0';
|
||||
AKERR_CHECK(ranges_added > 0);
|
||||
AKERR_CHECK_STATUS(range_err, AKERR_STATUS_RANGE_FULL);
|
||||
AKERR_CHECK(strstr(range_err->message, "range table is full") != NULL);
|
||||
RELEASE_ERROR(range_err);
|
||||
AKERR_CHECK(range_err == NULL);
|
||||
|
||||
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);
|
||||
/*
|
||||
* Name table capacity. Exhaustion must be reported, not silent: a dropped
|
||||
* name degrades every future stack trace for that code to "Unknown Error".
|
||||
*/
|
||||
int full_at = -1;
|
||||
for ( int i = 0; i < 100000; i++ ) {
|
||||
char name[32];
|
||||
snprintf(name, sizeof(name), "Filled %d", i);
|
||||
akerr_ErrorContext *name_err =
|
||||
akerr_register_status_name("fill", 2000000 + i, name);
|
||||
if ( name_err != NULL ) {
|
||||
AKERR_CHECK_STATUS(name_err, AKERR_STATUS_NAME_FULL);
|
||||
AKERR_CHECK(strstr(name_err->message, "registry is full") != NULL);
|
||||
AKERR_CHECK(strstr(name_err->message, "AKERR_STATUS_NAME_SLOTS") != NULL);
|
||||
RELEASE_ERROR(name_err);
|
||||
AKERR_CHECK(name_err == NULL);
|
||||
full_at = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* The table must actually hold everything it accepted. A probe sequence
|
||||
* that revisits slots instead of walking the table -- e.g. masking with
|
||||
* SLOTS rather than SLOTS-1 -- both collapses the usable capacity and
|
||||
* loses earlier entries, and each check below catches it independently.
|
||||
* The floor assumes at least the default table size (4096 slots).
|
||||
*/
|
||||
AKERR_CHECK(full_at > 256);
|
||||
for ( int i = 0; i < full_at; i++ ) {
|
||||
char expected[32];
|
||||
snprintf(expected, sizeof(expected), "Filled %d", i);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(2000000 + i, NULL), expected) == 0);
|
||||
}
|
||||
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);
|
||||
/* A dropped name reads back as the sentinel, and earlier ones survive. */
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(2000000 + full_at, NULL),
|
||||
"Unknown Error") == 0);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(INT_MIN, NULL), "Minimum Status") == 0);
|
||||
|
||||
/* Guard against parsing a different header than the one compiled in. */
|
||||
AKERR_CHECK(max_err_value == (AKERR_MAX_ERR_VALUE - AKERR_LAST_ERRNO_VALUE));
|
||||
/* Thousands of refusals later, every context went back to the pool. */
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
/* 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);
|
||||
fprintf(stderr, "err_maxval ok (%d consumer names before full)\n", full_at);
|
||||
return 0;
|
||||
}
|
||||
|
||||
24
tests/err_name_bounds.c
Normal file
24
tests/err_name_bounds.c
Normal file
@@ -0,0 +1,24 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
#include <string.h>
|
||||
|
||||
/* Unregistered status values return the sentinel regardless of magnitude. */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akerr_init();
|
||||
|
||||
/* Below range. */
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(-1, NULL), "Unknown Error") == 0);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(-9999, NULL), "Unknown Error") == 0);
|
||||
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(1000000, NULL),
|
||||
"Unknown Error") == 0);
|
||||
|
||||
/* A valid code must still resolve to its real name. */
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(AKERR_NULLPOINTER, NULL),
|
||||
"Null Pointer Error") == 0);
|
||||
|
||||
fprintf(stderr, "err_name_bounds ok\n");
|
||||
return 0;
|
||||
}
|
||||
126
tests/err_name_ownership.c
Normal file
126
tests/err_name_ownership.c
Normal file
@@ -0,0 +1,126 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* Reserving a range used to be pure bookkeeping: akerr_name_for_status() would
|
||||
* name any status for any caller, so two components could still register names
|
||||
* for the same code -- and HANDLE the same code -- with nothing detecting it.
|
||||
* Reservation only caught components that both opted in AND declared ranges
|
||||
* that happened to overlap.
|
||||
*
|
||||
* Naming a status is now permitted only inside a reservation:
|
||||
* - akerr_register_status_name() requires the range to belong to the caller,
|
||||
* and raises AKERR_STATUS_NAME_* when it does not;
|
||||
* - the legacy two-argument akerr_name_for_status() set path cannot identify
|
||||
* its caller, so it can only require that *some* reservation covers the
|
||||
* status -- still enough to stop a code nobody claimed. It returns a name
|
||||
* rather than an error context, so its refusals are logged instead.
|
||||
* Either way the refusal is visible, because a name that fails to register
|
||||
* degrades the status to "Unknown Error" in every later stack trace.
|
||||
*/
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akerr_capture_install();
|
||||
akerr_init();
|
||||
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "lib-a"));
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(512, 16, "lib-b"));
|
||||
|
||||
/* The owner of a range may name statuses inside it. */
|
||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("lib-a", 256, "A Parse Error"));
|
||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("lib-a", 271, "A Last Error"));
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "A Parse Error") == 0);
|
||||
|
||||
/* Naming another owner's status is refused and names the real owner. */
|
||||
AKERR_CHECK_RAISES(akerr_register_status_name("lib-b", 256, "B Hijack"),
|
||||
AKERR_STATUS_NAME_FOREIGN);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("lib-a");
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("lib-b");
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "A Parse Error") == 0);
|
||||
|
||||
/* Including the library's own reserved band. */
|
||||
AKERR_CHECK_RAISES(akerr_register_status_name("lib-b", AKERR_VALUE, "B Value"),
|
||||
AKERR_STATUS_NAME_FOREIGN);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS(AKERR_LIBRARY_OWNER);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(AKERR_VALUE, NULL), "Value Error") == 0);
|
||||
|
||||
/* A status nobody reserved cannot be named through either entry point. */
|
||||
AKERR_CHECK_RAISES(akerr_register_status_name("lib-a", 9999, "Unclaimed"),
|
||||
AKERR_STATUS_NAME_UNRESERVED);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("no reserved range");
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(9999, NULL), "Unknown Error") == 0);
|
||||
|
||||
/* The legacy path has no caller to raise into, so it logs the refusal.
|
||||
* It also cannot name the caller, and must say so rather than printing a
|
||||
* stray owner. */
|
||||
akerr_capture_reset();
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(9999, "Unclaimed Legacy"),
|
||||
"Unknown Error") == 0);
|
||||
AKERR_CHECK_CONTAINS("no reserved range");
|
||||
AKERR_CHECK_CONTAINS("an unnamed caller");
|
||||
AKERR_CHECK_CONTAINS("REFUSED STATUS NAME");
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(9999, NULL), "Unknown Error") == 0);
|
||||
/* ... and hands the context it raised back to the pool. */
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
/* Boundaries: just outside lib-a's range is not lib-a's to name. */
|
||||
AKERR_CHECK_RAISES(akerr_register_status_name("lib-a", 255, "Below"),
|
||||
AKERR_STATUS_NAME_FOREIGN);
|
||||
AKERR_CHECK_RAISES(akerr_register_status_name("lib-a", 272, "Above"),
|
||||
AKERR_STATUS_NAME_UNRESERVED);
|
||||
|
||||
/* The legacy set path still works inside any reservation. */
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(513, "B Legacy"), "B Legacy") == 0);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(513, NULL), "B Legacy") == 0);
|
||||
|
||||
/* Re-registering your own status overwrites the name. */
|
||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("lib-a", 256, "A Renamed"));
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "A Renamed") == 0);
|
||||
|
||||
/*
|
||||
* Argument validation. Each message must identify the caller it refused,
|
||||
* since that message is the whole report a consumer gets.
|
||||
*/
|
||||
AKERR_CHECK_RAISES(akerr_register_status_name(NULL, 257, "No Owner"),
|
||||
AKERR_STATUS_NAME_INVALID);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("257");
|
||||
AKERR_CHECK_RAISES(akerr_register_status_name("", 257, "Empty Owner"),
|
||||
AKERR_STATUS_NAME_INVALID);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("Empty Owner");
|
||||
AKERR_CHECK_RAISES(akerr_register_status_name("lib-a", 257, NULL),
|
||||
AKERR_STATUS_NAME_INVALID);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("lib-a");
|
||||
|
||||
/* Over-long owner strings: 63 characters fit, 64 and beyond do not. */
|
||||
char owner63[64];
|
||||
char owner64[65];
|
||||
char owner70[71];
|
||||
memset(owner63, 'a', sizeof(owner63) - 1);
|
||||
owner63[sizeof(owner63) - 1] = '\0';
|
||||
memset(owner64, 'b', sizeof(owner64) - 1);
|
||||
owner64[sizeof(owner64) - 1] = '\0';
|
||||
memset(owner70, 'c', sizeof(owner70) - 1);
|
||||
owner70[sizeof(owner70) - 1] = '\0';
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(600, 1, owner63));
|
||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name(owner63, 600, "Long Owner"));
|
||||
AKERR_CHECK_RAISES(akerr_register_status_name(owner64, 600, "Too Long"),
|
||||
AKERR_STATUS_NAME_INVALID);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("63");
|
||||
AKERR_CHECK_RAISES(akerr_register_status_name(owner70, 600, "Far Too Long"),
|
||||
AKERR_STATUS_NAME_INVALID);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(600, NULL), "Long Owner") == 0);
|
||||
|
||||
/* A refused registration must not consume a slot or leave a partial entry. */
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(257, NULL), "Unknown Error") == 0);
|
||||
|
||||
/* Lookup is unaffected by ownership -- anyone may read any name. */
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(271, NULL), "A Last Error") == 0);
|
||||
|
||||
/* Every refusal above released its context. */
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
fprintf(stderr, "err_name_ownership ok\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -35,6 +35,7 @@ int main(void)
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} HANDLE(e, AKERR_IO) {
|
||||
AKERR_CHECK_STATUS(e, AKERR_IO);
|
||||
handled = 1;
|
||||
} FINISH_NORETURN(e);
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
#define ITERATIONS 100000
|
||||
|
||||
static int handled_status = 0;
|
||||
|
||||
akerr_ErrorContext *boom(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
@@ -31,6 +33,7 @@ akerr_ErrorContext *one_cycle(void)
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} HANDLE(e, AKERR_VALUE) {
|
||||
handled_status = e->status;
|
||||
} FINISH(e, false);
|
||||
return e;
|
||||
}
|
||||
@@ -42,7 +45,9 @@ int main(void)
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
for ( int iter = 0; iter < ITERATIONS; iter++ ) {
|
||||
handled_status = 0;
|
||||
(void)one_cycle();
|
||||
AKERR_CHECK(handled_status == AKERR_VALUE);
|
||||
}
|
||||
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
56
tests/err_refcount_double_fail.c
Normal file
56
tests/err_refcount_double_fail.c
Normal file
@@ -0,0 +1,56 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
|
||||
/*
|
||||
* Regression test for the refcount leak: ENSURE_ERROR_READY must increment
|
||||
* refcount only when it *acquires* a fresh context, not on every FAIL/SUCCEED.
|
||||
* A function that calls FAIL more than once on the same context and then
|
||||
* propagates used to arrive at the caller with refcount 2; the caller released
|
||||
* once, leaking the slot. After enough leaks the pool is exhausted and the
|
||||
* library exit(1)s.
|
||||
*/
|
||||
|
||||
static int handled_status = 0;
|
||||
|
||||
akerr_ErrorContext *validate(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
ATTEMPT {
|
||||
FAIL(e, AKERR_VALUE, "condition 1 failed"); /* acquires the context */
|
||||
FAIL(e, AKERR_KEY, "condition 2 failed"); /* must NOT re-acquire it */
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true); /* unhandled -> propagate */
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/* One raise -> catch -> handle cycle; returns NULL (context released). */
|
||||
akerr_ErrorContext *one_cycle(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
ATTEMPT {
|
||||
CATCH(e, validate());
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} HANDLE(e, AKERR_KEY) {
|
||||
handled_status = e->status;
|
||||
} HANDLE(e, AKERR_VALUE) {
|
||||
} FINISH(e, false);
|
||||
return e;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akerr_init();
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
for ( int i = 0; i < 32; i++ ) {
|
||||
handled_status = 0;
|
||||
(void)one_cycle();
|
||||
AKERR_CHECK(handled_status == AKERR_KEY);
|
||||
}
|
||||
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
fprintf(stderr, "err_refcount_double_fail ok\n");
|
||||
return 0;
|
||||
}
|
||||
55
tests/err_registry_init_order.c
Normal file
55
tests/err_registry_init_order.c
Normal file
@@ -0,0 +1,55 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* A library may reserve its status range from its own init() before anything in
|
||||
* the process has raised an error, i.e. before akerr_init() has run. That used
|
||||
* to be silently destructive: akerr_init() clears the range and name tables, so
|
||||
* whichever component first triggered it (via PREPARE_ERROR) wiped the earlier
|
||||
* reservation, and the *next* component to claim the same range was told OK --
|
||||
* producing exactly the undetected aliasing the registry exists to prevent.
|
||||
*
|
||||
* Every public registry entry point now calls akerr_init() itself, so the
|
||||
* tables are only ever cleared before the first reservation, never after one.
|
||||
*
|
||||
* Note this test must not call akerr_init() or PREPARE_ERROR first -- the
|
||||
* uninitialized entry is the whole point.
|
||||
*/
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akerr_capture_install();
|
||||
|
||||
/* Cold call: no akerr_init(), no PREPARE_ERROR anywhere yet. */
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "early-lib"));
|
||||
AKERR_CHECK_SUCCEEDS(akerr_register_status_name("early-lib", 256, "Early Error"));
|
||||
|
||||
/* Something else now uses the library for the first time. */
|
||||
akerr_init();
|
||||
PREPARE_ERROR(e);
|
||||
(void)e;
|
||||
|
||||
/* The early reservation and its name must both have survived. */
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "Early Error") == 0);
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(256, 16, "late-lib"),
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS("early-lib");
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(260, 2, "late-lib"),
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
|
||||
/* The library's own initialization still happened exactly once. */
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(AKERR_NULLPOINTER, NULL),
|
||||
"Null Pointer Error") == 0);
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
||||
AKERR_LIBRARY_OWNER));
|
||||
|
||||
/* An identical repeat by the original owner is still idempotent. */
|
||||
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(256, 16, "early-lib"));
|
||||
|
||||
/* Raising from a cold registry must not strand a pool slot either. */
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
fprintf(stderr, "err_registry_init_order ok\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -27,6 +27,7 @@ int main(void)
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} HANDLE(e, AKERR_VALUE) {
|
||||
AKERR_CHECK_STATUS(e, AKERR_VALUE);
|
||||
} FINISH_NORETURN(e);
|
||||
|
||||
AKERR_CHECK(e == NULL);
|
||||
|
||||
43
tests/err_release_null.c
Normal file
43
tests/err_release_null.c
Normal file
@@ -0,0 +1,43 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* akerr_release_error(NULL) is an API contract violation the library reports
|
||||
* rather than crashes on: it raises AKERR_NULLPOINTER against the internal
|
||||
* last-ditch context and hands that back, so the caller gets a describable
|
||||
* error instead of a dereferenced NULL. Nothing exercised that path.
|
||||
*
|
||||
* The last-ditch context deliberately lives outside AKERR_ARRAY_ERROR so
|
||||
* reporting this failure cannot consume a pool slot -- which is exactly what
|
||||
* akerr_valid_error_address() reports on, and what this test checks.
|
||||
*/
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akerr_capture_install();
|
||||
/* akerr_init() sets up the last-ditch context's stacktrace cursor. */
|
||||
akerr_init();
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
akerr_ErrorContext *ret = akerr_release_error(NULL);
|
||||
|
||||
AKERR_CHECK(ret != NULL);
|
||||
AKERR_CHECK(ret->status == AKERR_NULLPOINTER);
|
||||
AKERR_CHECK(strstr(ret->message, "NULL context pointer") != NULL);
|
||||
|
||||
/* Reported through FAIL, so the stack trace names the error too. */
|
||||
AKERR_CHECK(strstr(ret->stacktracebuf, "Null Pointer Error") != NULL);
|
||||
|
||||
/* Not a pool slot, and no slot was checked out to report the failure. */
|
||||
AKERR_CHECK(akerr_valid_error_address(ret) == 0);
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
/* The last-ditch context is a singleton: the same one comes back. */
|
||||
akerr_ErrorContext *again = akerr_release_error(NULL);
|
||||
AKERR_CHECK(again == ret);
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
fprintf(stderr, "err_release_null ok\n");
|
||||
return 0;
|
||||
}
|
||||
71
tests/err_release_refcount.c
Normal file
71
tests/err_release_refcount.c
Normal file
@@ -0,0 +1,71 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* akerr_release_error() drops one reference and only recycles the slot when the
|
||||
* last one goes away. Two of its refcount edges had no test:
|
||||
*
|
||||
* - refcount > 1: the release must decrement and return the context intact,
|
||||
* not wipe it out from under the reference that is still held.
|
||||
* - refcount == 0: releasing a context nobody holds must not underflow the
|
||||
* count; it wipes and returns NULL like any other fully-released slot.
|
||||
*
|
||||
* The macro API never produces a refcount above 1 (ENSURE_ERROR_READY only
|
||||
* increments when it acquires a fresh slot), so this test sets the count
|
||||
* directly to model a caller that took an extra reference.
|
||||
*/
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akerr_capture_install();
|
||||
akerr_init();
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
/* Check out a slot and give it two holders. */
|
||||
akerr_ErrorContext *held = akerr_next_error();
|
||||
AKERR_CHECK(held != NULL);
|
||||
int slotid = held->arrayid;
|
||||
held->refcount = 2;
|
||||
held->status = AKERR_VALUE;
|
||||
snprintf((char *)held->message, AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH,
|
||||
"still referenced");
|
||||
|
||||
/* First release: one holder left, so the context survives untouched. */
|
||||
akerr_ErrorContext *ret = akerr_release_error(held);
|
||||
AKERR_CHECK(ret == held);
|
||||
AKERR_CHECK(held->refcount == 1);
|
||||
AKERR_CHECK(held->status == AKERR_VALUE);
|
||||
AKERR_CHECK(strcmp(held->message, "still referenced") == 0);
|
||||
AKERR_CHECK(akerr_slots_in_use() == 1);
|
||||
|
||||
/* Second release: last holder gone, so the slot is wiped and recycled. */
|
||||
ret = akerr_release_error(held);
|
||||
AKERR_CHECK(ret == NULL);
|
||||
AKERR_CHECK(held->refcount == 0);
|
||||
AKERR_CHECK(held->status == 0);
|
||||
AKERR_CHECK(held->message[0] == '\0');
|
||||
/* The wipe must preserve the slot's identity and stacktrace cursor. */
|
||||
AKERR_CHECK(held->arrayid == slotid);
|
||||
AKERR_CHECK(held->stacktracebufptr == (char *)&held->stacktracebuf);
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
/*
|
||||
* Releasing an unheld slot: refcount is already 0, so there is nothing to
|
||||
* decrement and the count must not go negative.
|
||||
*/
|
||||
akerr_ErrorContext *unheld = akerr_next_error();
|
||||
AKERR_CHECK(unheld != NULL);
|
||||
AKERR_CHECK(unheld->refcount == 0);
|
||||
ret = akerr_release_error(unheld);
|
||||
AKERR_CHECK(ret == NULL);
|
||||
AKERR_CHECK(unheld->refcount == 0);
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
/* The pool is still healthy afterwards. */
|
||||
akerr_ErrorContext *probe = akerr_next_error();
|
||||
AKERR_CHECK(probe != NULL);
|
||||
|
||||
fprintf(stderr, "err_release_refcount ok\n");
|
||||
return 0;
|
||||
}
|
||||
54
tests/err_stacktrace_bounds.c
Normal file
54
tests/err_stacktrace_bounds.c
Normal file
@@ -0,0 +1,54 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* Regression test for the stack-trace buffer overflow. Each frame appended a
|
||||
* line with snprintf, but passed the *full* buffer length as the size rather
|
||||
* than the space remaining, and advanced the cursor by snprintf's would-be
|
||||
* return value. A trace that filled the buffer therefore wrote past the end of
|
||||
* stacktracebuf and ran the cursor out of bounds.
|
||||
*
|
||||
* We place a context in a struct with a guard region right after it, position
|
||||
* the trace cursor near the end of the buffer, append one more frame, and
|
||||
* require that nothing was written past the buffer and the cursor stayed in
|
||||
* bounds.
|
||||
*/
|
||||
|
||||
static struct {
|
||||
akerr_ErrorContext ctx;
|
||||
unsigned char guard[512];
|
||||
} probe;
|
||||
|
||||
akerr_ErrorContext *append_frame(akerr_ErrorContext *e)
|
||||
{
|
||||
FAIL_RETURN(e, AKERR_VALUE,
|
||||
"an error message long enough to overflow a nearly full stack trace buffer");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akerr_init();
|
||||
|
||||
memset(&probe, 0x00, sizeof(probe));
|
||||
memset(probe.guard, 0xAA, sizeof(probe.guard));
|
||||
|
||||
akerr_ErrorContext *e = &probe.ctx;
|
||||
e->refcount = 1;
|
||||
/* Two bytes short of full: any real frame would overflow the old code. */
|
||||
e->stacktracebufptr = probe.ctx.stacktracebuf
|
||||
+ AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH - 2;
|
||||
|
||||
(void)append_frame(e);
|
||||
|
||||
/* Nothing may have been written past the end of stacktracebuf. */
|
||||
for ( unsigned i = 0; i < sizeof(probe.guard); i++ ) {
|
||||
AKERR_CHECK(probe.guard[i] == 0xAA);
|
||||
}
|
||||
/* The cursor must remain within the buffer. */
|
||||
AKERR_CHECK(e->stacktracebufptr
|
||||
<= probe.ctx.stacktracebuf + AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH);
|
||||
|
||||
fprintf(stderr, "err_stacktrace_bounds ok\n");
|
||||
return 0;
|
||||
}
|
||||
100
tests/err_status_exception.c
Normal file
100
tests/err_status_exception.c
Normal file
@@ -0,0 +1,100 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* The registry entry points report failure the same way every other function in
|
||||
* this library does: they return akerr_ErrorContext *. A refused reservation is
|
||||
* therefore an ordinary error, and everything that works on an ordinary error
|
||||
* must work on it -- CATCH, HANDLE, PASS, propagation to the caller, and the
|
||||
* stack trace an unhandled one prints.
|
||||
*
|
||||
* This is what distinguishes the current design from the integer return codes
|
||||
* it replaced: a consumer that ignores the result gets a compiler warning
|
||||
* (AKERR_NOIGNORE), and a consumer that catches it but does not handle it gets
|
||||
* the error propagated out of its init function rather than a silently
|
||||
* unreserved range.
|
||||
*/
|
||||
|
||||
#define TEST_OWNER "exception-test"
|
||||
|
||||
/* A consumer init function in the shape the documentation recommends. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *component_init(int base, const char *owner)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akerr_reserve_status_range(base, 4, owner));
|
||||
CATCH(errctx, akerr_register_status_name(owner, base, "Component Error"));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akerr_capture_install();
|
||||
akerr_init();
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
/* A successful reservation raises nothing at all. */
|
||||
AKERR_CHECK_SUCCEEDS(component_init(256, TEST_OWNER));
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "Component Error") == 0);
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
/*
|
||||
* A collision propagates out of the component's init and is caught here.
|
||||
* HANDLE proves the status is a real, distinct exception a consumer can
|
||||
* dispatch on -- not an opaque nonzero int.
|
||||
*/
|
||||
int handled_overlap = 0;
|
||||
ATTEMPT {
|
||||
CATCH(errctx, component_init(258, "other-component"));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_STATUS_RANGE_OVERLAP) {
|
||||
handled_overlap = 1;
|
||||
} HANDLE_DEFAULT(errctx) {
|
||||
handled_overlap = -1;
|
||||
} FINISH_NORETURN(errctx);
|
||||
AKERR_CHECK(handled_overlap == 1);
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
/* The name-side refusals dispatch the same way. */
|
||||
int handled_foreign = 0;
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akerr_register_status_name("interloper", 256, "Hijack"));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_STATUS_NAME_FOREIGN) {
|
||||
handled_foreign = 1;
|
||||
} FINISH_NORETURN(errctx);
|
||||
AKERR_CHECK(handled_foreign == 1);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(256, NULL), "Component Error") == 0);
|
||||
|
||||
/*
|
||||
* An unhandled refusal carries a stack trace naming the real owner, so the
|
||||
* report a consumer gets is the library's normal one.
|
||||
*/
|
||||
akerr_capture_reset();
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akerr_reserve_status_range(AKERR_VALUE, 1, "encroacher"));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE_DEFAULT(errctx) {
|
||||
LOG_ERROR_WITH_MESSAGE(errctx, "Reservation refused");
|
||||
} FINISH_NORETURN(errctx);
|
||||
AKERR_CHECK_CONTAINS("Reservation refused");
|
||||
AKERR_CHECK_CONTAINS("Status Range Overlap");
|
||||
AKERR_CHECK_CONTAINS(AKERR_LIBRARY_OWNER);
|
||||
AKERR_CHECK_CONTAINS("encroacher");
|
||||
AKERR_CHECK_CONTAINS("error.c");
|
||||
|
||||
/* Nothing above stranded a pool slot. */
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
fprintf(stderr, "err_status_exception ok\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
|
||||
static int wrong_handler_fired = 0;
|
||||
static int swallowed_status = 0;
|
||||
|
||||
akerr_ErrorContext *boom(void)
|
||||
{
|
||||
@@ -24,6 +25,7 @@ akerr_ErrorContext *swallow_it(void)
|
||||
ATTEMPT {
|
||||
CATCH(e, boom());
|
||||
} CLEANUP {
|
||||
swallowed_status = (e != NULL) ? e->status : 0;
|
||||
} PROCESS(e) {
|
||||
} HANDLE(e, AKERR_KEY) {
|
||||
wrong_handler_fired = 1; /* does not match AKERR_VALUE */
|
||||
@@ -38,6 +40,7 @@ int main(void)
|
||||
akerr_ErrorContext *res = swallow_it();
|
||||
|
||||
AKERR_CHECK(wrong_handler_fired == 0);
|
||||
AKERR_CHECK(swallowed_status == AKERR_VALUE);
|
||||
AKERR_CHECK(res == NULL); /* released even though unhandled */
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
fprintf(stderr, "err_swallow ok\n");
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
#include "akerror.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
static void expect_unhandled_nullpointer(akerr_ErrorContext *errctx)
|
||||
{
|
||||
exit((errctx != NULL && errctx->status == AKERR_NULLPOINTER) ? 1 : 0);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *func2(void)
|
||||
{
|
||||
@@ -25,6 +31,9 @@ akerr_ErrorContext *func1(void)
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akerr_init();
|
||||
akerr_handler_unhandled_error = &expect_unhandled_nullpointer;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, func1());
|
||||
|
||||
61
tests/err_unhandled_null.c
Normal file
61
tests/err_unhandled_null.c
Normal file
@@ -0,0 +1,61 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
/*
|
||||
* The default unhandled-error handler is the library's last stop: it exits the
|
||||
* process. Both of its exits were untested -- exit(1) for a NULL context (a
|
||||
* handler invoked with no error at all) and exit(errctx->status) for a real one.
|
||||
*
|
||||
* The handler never returns, so each case runs in a forked child and the test
|
||||
* asserts the exact exit status. That is stricter than a WILL_FAIL test, which
|
||||
* would pass on any non-zero exit, including one caused by an unrelated bug.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Run the default handler on ctx in a child process and return the child's exit
|
||||
* status, or -1 if it did not exit normally. The _exit() sentinel catches a
|
||||
* handler that returns instead of terminating.
|
||||
*/
|
||||
static int handler_exit_status(akerr_ErrorContext *ctx)
|
||||
{
|
||||
pid_t pid = fork();
|
||||
if ( pid == 0 ) {
|
||||
akerr_default_handler_unhandled_error(ctx);
|
||||
_exit(99);
|
||||
}
|
||||
int status = 0;
|
||||
if ( pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status) ) {
|
||||
return -1;
|
||||
}
|
||||
return WEXITSTATUS(status);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akerr_capture_install();
|
||||
akerr_init();
|
||||
|
||||
/* No context to report: the handler has nothing to exit with but failure. */
|
||||
AKERR_CHECK(handler_exit_status(NULL) == 1);
|
||||
|
||||
/*
|
||||
* With a context, the status becomes the exit code. waitpid only reports
|
||||
* its low 8 bits, which is where AKERR_VALUE (144) lands, and it is neither
|
||||
* 0 nor the 1 used for the NULL case, so the two exits stay distinguishable.
|
||||
*/
|
||||
akerr_ErrorContext *slot = akerr_next_error();
|
||||
AKERR_CHECK(slot != NULL);
|
||||
slot->refcount = 1;
|
||||
slot->status = AKERR_VALUE;
|
||||
AKERR_CHECK((AKERR_VALUE & 0xff) != 0 && (AKERR_VALUE & 0xff) != 1);
|
||||
AKERR_CHECK(handler_exit_status(slot) == (AKERR_VALUE & 0xff));
|
||||
|
||||
slot = akerr_release_error(slot);
|
||||
AKERR_CHECK(slot == NULL);
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
fprintf(stderr, "err_unhandled_null ok\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user