diff --git a/CMakeLists.txt b/CMakeLists.txt index 9109d35..8a0547a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,6 +66,7 @@ set(AKERR_TESTS err_error_names err_release_clears err_pool_exhaust + err_maxval ) set(AKERR_WILL_FAIL_TESTS diff --git a/include/akerror.tmpl.h b/include/akerror.tmpl.h index 1373730..8179279 100644 --- a/include/akerror.tmpl.h +++ b/include/akerror.tmpl.h @@ -40,7 +40,10 @@ #define AKERR_BADEXC (AKERR_LAST_ERRNO_VALUE + 17) /** The libakerr library was given an akerr_ErrorContext to parse that did not come from AKERR_ARRAY_ERROR (likely an uninitialized pointer) */ #ifndef AKERR_MAX_ERR_VALUE -#define AKERR_MAX_ERR_VALUE (AKERR_LAST_ERRNO_VALUE + 15) +/* 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 diff --git a/tests/MUTATION.md b/tests/MUTATION.md index 856d999..714dd1a 100644 --- a/tests/MUTATION.md +++ b/tests/MUTATION.md @@ -77,7 +77,7 @@ Re-run after adding tests and confirm the score went up. ## Current status -`src/error.c` scores ~71% (the CI gate is set to 65% for headroom). The +`src/error.c` scores ~74% (the CI gate is set to 65% for headroom). The remaining survivors are dominated by: * **Equivalent mutants** in `akerr_init`: deleting the `memset`/`NULL` setup of @@ -90,11 +90,11 @@ remaining survivors are dominated by: test that captures a child's stderr and exit code, rather than the in-process capturing logger the other tests use. -Findings worth noting (surfaced by mutation testing, not yet fixed): +Findings surfaced by mutation testing: -* `AKERR_MAX_ERR_VALUE` is `AKERR_LAST_ERRNO_VALUE + 15`, but `AKERR_NOT_IMPLEMENTED` - (+16) and `AKERR_BADEXC` (+17) exceed it. `akerr_name_for_status` rejects any - status `> AKERR_MAX_ERR_VALUE`, so those two codes can never store or return a - name — the `akerr_name_for_status(AKERR_BADEXC, ...)` call in `akerr_init` is - dead code (which is why deleting it survives). Bumping `AKERR_MAX_ERR_VALUE` to - `+ 17` would fix it. +* **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. diff --git a/tests/err_error_names.c b/tests/err_error_names.c index 80b6621..c987935 100644 --- a/tests/err_error_names.c +++ b/tests/err_error_names.c @@ -7,9 +7,9 @@ * Verify the names are actually installed (mutation testing showed the * registration calls could be deleted without any test noticing). * - * Note: AKERR_NOT_IMPLEMENTED and AKERR_BADEXC are intentionally omitted -- they - * exceed AKERR_MAX_ERR_VALUE, so akerr_name_for_status cannot store or return - * their names (see tests/MUTATION.md). + * Note: AKERR_EOF, AKERR_ITERATOR_BREAK and AKERR_NOT_IMPLEMENTED are omitted -- + * they are valid codes but akerr_init does not register a display name for them, + * so akerr_name_for_status returns an empty string rather than a known name. */ static const struct { @@ -28,6 +28,7 @@ static const struct { { AKERR_VALUE, "Value Error" }, { AKERR_RELATIONSHIP, "Relationship Error" }, { AKERR_CIRCULAR_REFERENCE, "Circular Reference Error" }, + { AKERR_BADEXC, "Invalid akerr_ErrorContext" }, }; int main(void) diff --git a/tests/err_maxval.c b/tests/err_maxval.c new file mode 100644 index 0000000..0f3aaff --- /dev/null +++ b/tests/err_maxval.c @@ -0,0 +1,52 @@ +#include "akerror.h" +#include "err_capture.h" + +/* + * AKERR_MAX_ERR_VALUE sizes the __AKERR_ERROR_NAMES table and is the upper bound + * akerr_name_for_status() will accept. 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). + * + * Guard the invariant: the AKERR_* range reserved above AKERR_LAST_ERRNO_VALUE + * must be larger than the number of AKERR_* codes defined, and every code must + * be individually indexable. + */ + +/* Every AKERR_* library error code. Keep in sync with akerror.h. */ +static const int akerr_codes[] = { + AKERR_NULLPOINTER, + AKERR_OUTOFBOUNDS, + AKERR_API, + AKERR_ATTRIBUTE, + AKERR_TYPE, + AKERR_KEY, + AKERR_INDEX, + AKERR_FORMAT, + AKERR_IO, + AKERR_VALUE, + AKERR_RELATIONSHIP, + AKERR_EOF, + AKERR_CIRCULAR_REFERENCE, + AKERR_ITERATOR_BREAK, + AKERR_NOT_IMPLEMENTED, + AKERR_BADEXC, +}; + +int main(void) +{ + int n = (int)(sizeof(akerr_codes) / sizeof(akerr_codes[0])); + + /* AKERR_MAX_ERR_VALUE must reserve more codes than are actually defined. */ + AKERR_CHECK((AKERR_MAX_ERR_VALUE - AKERR_LAST_ERRNO_VALUE) > n); + + /* Stronger: every defined code must fall within the addressable range so + * its name can be stored and retrieved. */ + for ( int i = 0; i < n; i++ ) { + AKERR_CHECK(akerr_codes[i] > AKERR_LAST_ERRNO_VALUE); + AKERR_CHECK(akerr_codes[i] <= AKERR_MAX_ERR_VALUE); + } + + fprintf(stderr, "err_maxval ok (%d codes, range %d)\n", + n, AKERR_MAX_ERR_VALUE - AKERR_LAST_ERRNO_VALUE); + return 0; +}