Compare commits
6 Commits
tests/expa
...
0c0d81249f
| Author | SHA1 | Date | |
|---|---|---|---|
|
0c0d81249f
|
|||
|
426efbb2d4
|
|||
|
4ae1decde2
|
|||
|
4212ff0b28
|
|||
|
de13b290d4
|
|||
|
3e24356f07
|
66
AGENTS.md
Normal file
66
AGENTS.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 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.
|
||||||
@@ -67,6 +67,10 @@ set(AKERR_TESTS
|
|||||||
err_release_clears
|
err_release_clears
|
||||||
err_pool_exhaust
|
err_pool_exhaust
|
||||||
err_maxval
|
err_maxval
|
||||||
|
err_refcount_double_fail
|
||||||
|
err_stacktrace_bounds
|
||||||
|
err_name_bounds
|
||||||
|
err_format_string
|
||||||
)
|
)
|
||||||
|
|
||||||
set(AKERR_WILL_FAIL_TESTS
|
set(AKERR_WILL_FAIL_TESTS
|
||||||
@@ -95,9 +99,16 @@ set_tests_properties(
|
|||||||
# notices. This is a meta-check on the tests themselves, so it is a manual
|
# 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.
|
# target (it rebuilds and re-runs the whole suite many times), not a CTest test.
|
||||||
# cmake --build build --target mutation
|
# cmake --build build --target mutation
|
||||||
|
# When embedded in another project, use a namespaced target to avoid collisions
|
||||||
|
# with mutation targets provided by sibling dependencies.
|
||||||
find_package(Python3 COMPONENTS Interpreter)
|
find_package(Python3 COMPONENTS Interpreter)
|
||||||
if(Python3_FOUND)
|
if(Python3_FOUND)
|
||||||
add_custom_target(mutation
|
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}
|
COMMAND ${Python3_EXECUTABLE}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/scripts/mutation_test.py
|
${CMAKE_CURRENT_SOURCE_DIR}/scripts/mutation_test.py
|
||||||
--source-root ${CMAKE_CURRENT_SOURCE_DIR}
|
--source-root ${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
|||||||
45
README.md
45
README.md
@@ -210,6 +210,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.
|
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
|
## 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.
|
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 +234,49 @@ ATTEMPT {
|
|||||||
|
|
||||||
When either of these two macros are used, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
|
When either of these two macros are used, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
|
||||||
|
|
||||||
|
## 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
|
# 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.
|
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.
|
||||||
|
|||||||
@@ -110,8 +110,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__); \
|
akerr_log_method("%s:%s:%d: Unable to pull an error context from the array!", __FILE__, (char *)__func__, __LINE__); \
|
||||||
exit(1); \
|
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 *
|
* Failure and success methods for functions that return akerr_ErrorContext *
|
||||||
@@ -168,11 +190,11 @@ void akerr_init_errno(void);
|
|||||||
#define FAIL(__err_context, __err, __message, ...) \
|
#define FAIL(__err_context, __err, __message, ...) \
|
||||||
ENSURE_ERROR_READY(__err_context); \
|
ENSURE_ERROR_READY(__err_context); \
|
||||||
__err_context->status = __err; \
|
__err_context->status = __err; \
|
||||||
snprintf((char *)__err_context->fname, AKERR_MAX_ERROR_FNAME_LENGTH, __FILE__); \
|
snprintf((char *)__err_context->fname, AKERR_MAX_ERROR_FNAME_LENGTH, "%s", __FILE__); \
|
||||||
snprintf((char *)__err_context->function, AKERR_MAX_ERROR_FUNCTION_LENGTH, __func__); \
|
snprintf((char *)__err_context->function, AKERR_MAX_ERROR_FUNCTION_LENGTH, "%s", __func__); \
|
||||||
__err_context->lineno = __LINE__; \
|
__err_context->lineno = __LINE__; \
|
||||||
snprintf((char *)__err_context->message, AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH, __message, ## __VA_ARGS__); \
|
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) \
|
#define SUCCEED(__err_context) \
|
||||||
@@ -198,7 +220,7 @@ void akerr_init_errno(void);
|
|||||||
VALID(__err_context, __stmt); \
|
VALID(__err_context, __stmt); \
|
||||||
if ( __err_context != NULL ) { \
|
if ( __err_context != NULL ) { \
|
||||||
if ( __err_context->status != 0 ) { \
|
if ( __err_context->status != 0 ) { \
|
||||||
__err_context->stacktracebufptr += snprintf(__err_context->stacktracebufptr, AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH, "%s:%s:%d\n", (char *)__FILE__, (char *)__func__, __LINE__); \
|
AKERR_STACKTRACE_APPEND(__err_context, "%s:%s:%d\n", (char *)__FILE__, (char *)__func__, __LINE__); \
|
||||||
break; \
|
break; \
|
||||||
} \
|
} \
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ akerr_ErrorContext *akerr_release_error(akerr_ErrorContext *err)
|
|||||||
// Call with name = NULL to retrieve a status.
|
// Call with name = NULL to retrieve a status.
|
||||||
char *akerr_name_for_status(int status, char *name)
|
char *akerr_name_for_status(int status, char *name)
|
||||||
{
|
{
|
||||||
if ( status > AKERR_MAX_ERR_VALUE ) {
|
if ( status < 0 || status > AKERR_MAX_ERR_VALUE ) {
|
||||||
return "Unknown Error";
|
return "Unknown Error";
|
||||||
}
|
}
|
||||||
if ( name != NULL ) {
|
if ( name != NULL ) {
|
||||||
|
|||||||
4
test.sh
Normal file
4
test.sh
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
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
|
||||||
@@ -63,26 +63,26 @@ int main(void)
|
|||||||
akerr_ErrorContext *r;
|
akerr_ErrorContext *r;
|
||||||
|
|
||||||
r = zero_break(0);
|
r = zero_break(0);
|
||||||
AKERR_CHECK(r != NULL && r->status == AKERR_VALUE);
|
AKERR_CHECK_STATUS(r, AKERR_VALUE);
|
||||||
r = akerr_release_error(r);
|
r = akerr_release_error(r);
|
||||||
AKERR_CHECK(zero_break(7) == NULL);
|
AKERR_CHECK(zero_break(7) == NULL);
|
||||||
|
|
||||||
r = nonzero_break(7);
|
r = nonzero_break(7);
|
||||||
AKERR_CHECK(r != NULL && r->status == AKERR_INDEX);
|
AKERR_CHECK_STATUS(r, AKERR_INDEX);
|
||||||
r = akerr_release_error(r);
|
r = akerr_release_error(r);
|
||||||
AKERR_CHECK(nonzero_break(0) == NULL);
|
AKERR_CHECK(nonzero_break(0) == NULL);
|
||||||
|
|
||||||
r = always_break();
|
r = always_break();
|
||||||
AKERR_CHECK(r != NULL && r->status == AKERR_IO);
|
AKERR_CHECK_STATUS(r, AKERR_IO);
|
||||||
r = akerr_release_error(r);
|
r = akerr_release_error(r);
|
||||||
|
|
||||||
r = zero_return(0);
|
r = zero_return(0);
|
||||||
AKERR_CHECK(r != NULL && r->status == AKERR_KEY);
|
AKERR_CHECK_STATUS(r, AKERR_KEY);
|
||||||
r = akerr_release_error(r);
|
r = akerr_release_error(r);
|
||||||
AKERR_CHECK(zero_return(7) == NULL);
|
AKERR_CHECK(zero_return(7) == NULL);
|
||||||
|
|
||||||
r = nonzero_return(7);
|
r = nonzero_return(7);
|
||||||
AKERR_CHECK(r != NULL && r->status == AKERR_TYPE);
|
AKERR_CHECK_STATUS(r, AKERR_TYPE);
|
||||||
r = akerr_release_error(r);
|
r = akerr_release_error(r);
|
||||||
AKERR_CHECK(nonzero_return(0) == NULL);
|
AKERR_CHECK(nonzero_return(0) == NULL);
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,12 @@ static int __attribute__((unused)) akerr_slots_in_use(void)
|
|||||||
} \
|
} \
|
||||||
} while ( 0 )
|
} while ( 0 )
|
||||||
|
|
||||||
|
#define AKERR_CHECK_STATUS(errctx, expected_status) \
|
||||||
|
do { \
|
||||||
|
AKERR_CHECK((errctx) != NULL); \
|
||||||
|
AKERR_CHECK((errctx)->status == (expected_status)); \
|
||||||
|
} while ( 0 )
|
||||||
|
|
||||||
#define AKERR_CHECK_CONTAINS(needle) \
|
#define AKERR_CHECK_CONTAINS(needle) \
|
||||||
AKERR_CHECK(strstr(akerr_capture_buf, (needle)) != NULL)
|
AKERR_CHECK(strstr(akerr_capture_buf, (needle)) != NULL)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "akerror.h"
|
#include "akerror.h"
|
||||||
|
#include "err_capture.h"
|
||||||
|
|
||||||
akerr_ErrorContext *func2(void)
|
akerr_ErrorContext *func2(void)
|
||||||
{
|
{
|
||||||
@@ -31,6 +32,11 @@ int main(void)
|
|||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||||
|
AKERR_CHECK_STATUS(errctx, AKERR_NULLPOINTER);
|
||||||
akerr_log_method("Caught exception");
|
akerr_log_method("Caught exception");
|
||||||
} FINISH_NORETURN(errctx);
|
} FINISH_NORETURN(errctx);
|
||||||
|
|
||||||
|
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||||
|
fprintf(stderr, "err_catch ok\n");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "akerror.h"
|
#include "akerror.h"
|
||||||
|
#include "err_capture.h"
|
||||||
|
|
||||||
int x;
|
int x;
|
||||||
|
|
||||||
@@ -34,10 +35,15 @@ int main(void)
|
|||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||||
|
AKERR_CHECK_STATUS(errctx, AKERR_NULLPOINTER);
|
||||||
if ( x == 0 ) {
|
if ( x == 0 ) {
|
||||||
fprintf(stderr, "Cleanup works\n");
|
akerr_log_method("Cleanup works\n");
|
||||||
return 0;
|
} else {
|
||||||
}
|
|
||||||
return 1;
|
return 1;
|
||||||
|
}
|
||||||
} FINISH_NORETURN(errctx);
|
} FINISH_NORETURN(errctx);
|
||||||
|
|
||||||
|
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||||
|
fprintf(stderr, "err_cleanup ok\n");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ int main(void)
|
|||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(e) {
|
} PROCESS(e) {
|
||||||
} HANDLE(e, EACCES) {
|
} HANDLE(e, EACCES) {
|
||||||
|
AKERR_CHECK_STATUS(e, EACCES);
|
||||||
handled = 1;
|
handled = 1;
|
||||||
} FINISH_NORETURN(e);
|
} FINISH_NORETURN(e);
|
||||||
|
|
||||||
|
|||||||
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 specific_fired = 0;
|
||||||
static int default_fired = 0;
|
static int default_fired = 0;
|
||||||
|
static int default_status = 0;
|
||||||
|
|
||||||
akerr_ErrorContext *boom(void)
|
akerr_ErrorContext *boom(void)
|
||||||
{
|
{
|
||||||
@@ -28,10 +29,12 @@ int main(void)
|
|||||||
specific_fired = 1; /* must NOT run: error is AKERR_TYPE */
|
specific_fired = 1; /* must NOT run: error is AKERR_TYPE */
|
||||||
} HANDLE_DEFAULT(e) {
|
} HANDLE_DEFAULT(e) {
|
||||||
default_fired = 1;
|
default_fired = 1;
|
||||||
|
default_status = e->status;
|
||||||
} FINISH_NORETURN(e);
|
} FINISH_NORETURN(e);
|
||||||
|
|
||||||
AKERR_CHECK(specific_fired == 0);
|
AKERR_CHECK(specific_fired == 0);
|
||||||
AKERR_CHECK(default_fired == 1);
|
AKERR_CHECK(default_fired == 1);
|
||||||
|
AKERR_CHECK(default_status == AKERR_TYPE);
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||||
fprintf(stderr, "err_handle_default ok\n");
|
fprintf(stderr, "err_handle_default ok\n");
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
static int a_fired = 0;
|
static int a_fired = 0;
|
||||||
static int b_fired = 0;
|
static int b_fired = 0;
|
||||||
static int c_fired = 0;
|
static int c_fired = 0;
|
||||||
|
static int b_status = 0;
|
||||||
|
|
||||||
akerr_ErrorContext *boom(void)
|
akerr_ErrorContext *boom(void)
|
||||||
{
|
{
|
||||||
@@ -29,12 +30,14 @@ int main(void)
|
|||||||
a_fired = 1;
|
a_fired = 1;
|
||||||
} HANDLE(e, AKERR_TYPE) {
|
} HANDLE(e, AKERR_TYPE) {
|
||||||
b_fired = 1;
|
b_fired = 1;
|
||||||
|
b_status = e->status;
|
||||||
} HANDLE(e, AKERR_IO) {
|
} HANDLE(e, AKERR_IO) {
|
||||||
c_fired = 1;
|
c_fired = 1;
|
||||||
} FINISH_NORETURN(e);
|
} FINISH_NORETURN(e);
|
||||||
|
|
||||||
AKERR_CHECK(a_fired == 0);
|
AKERR_CHECK(a_fired == 0);
|
||||||
AKERR_CHECK(b_fired == 1);
|
AKERR_CHECK(b_fired == 1);
|
||||||
|
AKERR_CHECK(b_status == AKERR_TYPE);
|
||||||
AKERR_CHECK(c_fired == 0);
|
AKERR_CHECK(c_fired == 0);
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||||
fprintf(stderr, "err_handle_dispatch ok\n");
|
fprintf(stderr, "err_handle_dispatch ok\n");
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
|
|
||||||
static int group_fired = 0;
|
static int group_fired = 0;
|
||||||
static int other_fired = 0;
|
static int other_fired = 0;
|
||||||
|
static int group_status = 0;
|
||||||
|
static int other_status = 0;
|
||||||
|
|
||||||
akerr_ErrorContext *boom(int status)
|
akerr_ErrorContext *boom(int status)
|
||||||
{
|
{
|
||||||
@@ -28,8 +30,10 @@ akerr_ErrorContext *run(int status)
|
|||||||
} HANDLE(e, AKERR_KEY)
|
} HANDLE(e, AKERR_KEY)
|
||||||
HANDLE_GROUP(e, AKERR_INDEX) {
|
HANDLE_GROUP(e, AKERR_INDEX) {
|
||||||
group_fired++;
|
group_fired++;
|
||||||
|
group_status = e->status;
|
||||||
} HANDLE(e, AKERR_IO) {
|
} HANDLE(e, AKERR_IO) {
|
||||||
other_fired++;
|
other_fired++;
|
||||||
|
other_status = e->status;
|
||||||
} FINISH(e, false);
|
} FINISH(e, false);
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
@@ -40,15 +44,18 @@ int main(void)
|
|||||||
|
|
||||||
run(AKERR_KEY);
|
run(AKERR_KEY);
|
||||||
AKERR_CHECK(group_fired == 1);
|
AKERR_CHECK(group_fired == 1);
|
||||||
|
AKERR_CHECK(group_status == AKERR_KEY);
|
||||||
AKERR_CHECK(other_fired == 0);
|
AKERR_CHECK(other_fired == 0);
|
||||||
|
|
||||||
run(AKERR_INDEX);
|
run(AKERR_INDEX);
|
||||||
AKERR_CHECK(group_fired == 2);
|
AKERR_CHECK(group_fired == 2);
|
||||||
|
AKERR_CHECK(group_status == AKERR_INDEX);
|
||||||
AKERR_CHECK(other_fired == 0);
|
AKERR_CHECK(other_fired == 0);
|
||||||
|
|
||||||
run(AKERR_IO);
|
run(AKERR_IO);
|
||||||
AKERR_CHECK(group_fired == 2);
|
AKERR_CHECK(group_fired == 2);
|
||||||
AKERR_CHECK(other_fired == 1);
|
AKERR_CHECK(other_fired == 1);
|
||||||
|
AKERR_CHECK(other_status == AKERR_IO);
|
||||||
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||||
fprintf(stderr, "err_handle_group ok\n");
|
fprintf(stderr, "err_handle_group ok\n");
|
||||||
|
|||||||
31
tests/err_name_bounds.c
Normal file
31
tests/err_name_bounds.c
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#include "akerror.h"
|
||||||
|
#include "err_capture.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* akerr_name_for_status must reject out-of-range status codes at BOTH ends.
|
||||||
|
* It already guarded the upper bound, but a negative status indexed
|
||||||
|
* __AKERR_ERROR_NAMES[negative] -- an out-of-bounds read (and an out-of-bounds
|
||||||
|
* write when a name is supplied). Every out-of-range status must return the
|
||||||
|
* "Unknown Error" sentinel instead.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
/* Above range (already handled; kept as a guard against regressions). */
|
||||||
|
AKERR_CHECK(strcmp(akerr_name_for_status(AKERR_MAX_ERR_VALUE + 1, 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;
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ int main(void)
|
|||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(e) {
|
} PROCESS(e) {
|
||||||
} HANDLE(e, AKERR_IO) {
|
} HANDLE(e, AKERR_IO) {
|
||||||
|
AKERR_CHECK_STATUS(e, AKERR_IO);
|
||||||
handled = 1;
|
handled = 1;
|
||||||
} FINISH_NORETURN(e);
|
} FINISH_NORETURN(e);
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
|
|
||||||
#define ITERATIONS 100000
|
#define ITERATIONS 100000
|
||||||
|
|
||||||
|
static int handled_status = 0;
|
||||||
|
|
||||||
akerr_ErrorContext *boom(void)
|
akerr_ErrorContext *boom(void)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(e);
|
PREPARE_ERROR(e);
|
||||||
@@ -31,6 +33,7 @@ akerr_ErrorContext *one_cycle(void)
|
|||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(e) {
|
} PROCESS(e) {
|
||||||
} HANDLE(e, AKERR_VALUE) {
|
} HANDLE(e, AKERR_VALUE) {
|
||||||
|
handled_status = e->status;
|
||||||
} FINISH(e, false);
|
} FINISH(e, false);
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
@@ -42,7 +45,9 @@ int main(void)
|
|||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||||
|
|
||||||
for ( int iter = 0; iter < ITERATIONS; iter++ ) {
|
for ( int iter = 0; iter < ITERATIONS; iter++ ) {
|
||||||
|
handled_status = 0;
|
||||||
(void)one_cycle();
|
(void)one_cycle();
|
||||||
|
AKERR_CHECK(handled_status == AKERR_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
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;
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ int main(void)
|
|||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(e) {
|
} PROCESS(e) {
|
||||||
} HANDLE(e, AKERR_VALUE) {
|
} HANDLE(e, AKERR_VALUE) {
|
||||||
|
AKERR_CHECK_STATUS(e, AKERR_VALUE);
|
||||||
} FINISH_NORETURN(e);
|
} FINISH_NORETURN(e);
|
||||||
|
|
||||||
AKERR_CHECK(e == NULL);
|
AKERR_CHECK(e == NULL);
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
static int wrong_handler_fired = 0;
|
static int wrong_handler_fired = 0;
|
||||||
|
static int swallowed_status = 0;
|
||||||
|
|
||||||
akerr_ErrorContext *boom(void)
|
akerr_ErrorContext *boom(void)
|
||||||
{
|
{
|
||||||
@@ -24,6 +25,7 @@ akerr_ErrorContext *swallow_it(void)
|
|||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(e, boom());
|
CATCH(e, boom());
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
|
swallowed_status = (e != NULL) ? e->status : 0;
|
||||||
} PROCESS(e) {
|
} PROCESS(e) {
|
||||||
} HANDLE(e, AKERR_KEY) {
|
} HANDLE(e, AKERR_KEY) {
|
||||||
wrong_handler_fired = 1; /* does not match AKERR_VALUE */
|
wrong_handler_fired = 1; /* does not match AKERR_VALUE */
|
||||||
@@ -38,6 +40,7 @@ int main(void)
|
|||||||
akerr_ErrorContext *res = swallow_it();
|
akerr_ErrorContext *res = swallow_it();
|
||||||
|
|
||||||
AKERR_CHECK(wrong_handler_fired == 0);
|
AKERR_CHECK(wrong_handler_fired == 0);
|
||||||
|
AKERR_CHECK(swallowed_status == AKERR_VALUE);
|
||||||
AKERR_CHECK(res == NULL); /* released even though unhandled */
|
AKERR_CHECK(res == NULL); /* released even though unhandled */
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||||
fprintf(stderr, "err_swallow ok\n");
|
fprintf(stderr, "err_swallow ok\n");
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
#include "akerror.h"
|
#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)
|
akerr_ErrorContext *func2(void)
|
||||||
{
|
{
|
||||||
@@ -25,6 +31,9 @@ akerr_ErrorContext *func1(void)
|
|||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
|
akerr_init();
|
||||||
|
akerr_handler_unhandled_error = &expect_unhandled_nullpointer;
|
||||||
|
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, func1());
|
CATCH(errctx, func1());
|
||||||
|
|||||||
Reference in New Issue
Block a user