diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8aec74e --- /dev/null +++ b/AGENTS.md @@ -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_.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. diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..50da858 --- /dev/null +++ b/test.sh @@ -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 diff --git a/tests/err_break_variants.c b/tests/err_break_variants.c index 4cc8ff2..1df385a 100644 --- a/tests/err_break_variants.c +++ b/tests/err_break_variants.c @@ -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); diff --git a/tests/err_capture.h b/tests/err_capture.h index f05ed92..04f40f2 100644 --- a/tests/err_capture.h +++ b/tests/err_capture.h @@ -75,6 +75,12 @@ 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 ) + #define AKERR_CHECK_CONTAINS(needle) \ AKERR_CHECK(strstr(akerr_capture_buf, (needle)) != NULL) diff --git a/tests/err_catch.c b/tests/err_catch.c index 2d21b5c..bfd0083 100644 --- a/tests/err_catch.c +++ b/tests/err_catch.c @@ -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_log_method("Caught exception"); + 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; } diff --git a/tests/err_cleanup.c b/tests/err_cleanup.c index 0018c25..2da7289 100644 --- a/tests/err_cleanup.c +++ b/tests/err_cleanup.c @@ -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; } - return 1; } FINISH_NORETURN(errctx); + + AKERR_CHECK(akerr_slots_in_use() == 0); + fprintf(stderr, "err_cleanup ok\n"); + return 0; } diff --git a/tests/err_errno.c b/tests/err_errno.c index df1316a..251042a 100644 --- a/tests/err_errno.c +++ b/tests/err_errno.c @@ -37,6 +37,7 @@ int main(void) } CLEANUP { } PROCESS(e) { } HANDLE(e, EACCES) { + AKERR_CHECK_STATUS(e, EACCES); handled = 1; } FINISH_NORETURN(e); diff --git a/tests/err_handle_default.c b/tests/err_handle_default.c index 95e88ac..35b0659 100644 --- a/tests/err_handle_default.c +++ b/tests/err_handle_default.c @@ -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; diff --git a/tests/err_handle_dispatch.c b/tests/err_handle_dispatch.c index 6bbace6..ef1209f 100644 --- a/tests/err_handle_dispatch.c +++ b/tests/err_handle_dispatch.c @@ -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"); diff --git a/tests/err_handle_group.c b/tests/err_handle_group.c index 542b3c1..be84dfe 100644 --- a/tests/err_handle_group.c +++ b/tests/err_handle_group.c @@ -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"); diff --git a/tests/err_pass.c b/tests/err_pass.c index 2bde126..fbff806 100644 --- a/tests/err_pass.c +++ b/tests/err_pass.c @@ -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); diff --git a/tests/err_pool_refcount.c b/tests/err_pool_refcount.c index c0be693..b34b8e4 100644 --- a/tests/err_pool_refcount.c +++ b/tests/err_pool_refcount.c @@ -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); diff --git a/tests/err_refcount_double_fail.c b/tests/err_refcount_double_fail.c index 1ea759f..5e96b08 100644 --- a/tests/err_refcount_double_fail.c +++ b/tests/err_refcount_double_fail.c @@ -10,6 +10,8 @@ * library exit(1)s. */ +static int handled_status = 0; + akerr_ErrorContext *validate(void) { PREPARE_ERROR(e); @@ -31,6 +33,7 @@ akerr_ErrorContext *one_cycle(void) } CLEANUP { } PROCESS(e) { } HANDLE(e, AKERR_KEY) { + handled_status = e->status; } HANDLE(e, AKERR_VALUE) { } FINISH(e, false); return e; @@ -42,7 +45,9 @@ int main(void) 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); diff --git a/tests/err_release_clears.c b/tests/err_release_clears.c index 6134da8..126eb6b 100644 --- a/tests/err_release_clears.c +++ b/tests/err_release_clears.c @@ -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); diff --git a/tests/err_swallow.c b/tests/err_swallow.c index 509ea77..0416a29 100644 --- a/tests/err_swallow.c +++ b/tests/err_swallow.c @@ -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"); diff --git a/tests/err_trace.c b/tests/err_trace.c index 11f87bd..1f0f80f 100644 --- a/tests/err_trace.c +++ b/tests/err_trace.c @@ -1,4 +1,10 @@ #include "akerror.h" +#include + +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());