Use the library's own error idioms inside the library
Four things in src/error.c did by hand what the macros already do, or
skipped checks the library would have caught for a consumer.
akerr_copy_string() returned void and validated only its capacity, while
writing through a caller-supplied pointer for a caller-supplied length.
It is now __akerr_copy_string() and raises: AKERR_NULLPOINTER for a NULL
destination or source, AKERR_VALUE for a capacity with no room for a
terminator. Both call sites PASS it, and the owner copy in
akerr_reserve_status_range() now gates the commit, so a failed copy
cannot leave a range claimed under an empty owner. It is exported under
the internal prefix rather than static so tests/err_copy_string.c can
drive those guards; nothing else can reach them.
__akerr_name_library_status() and the band reservation in akerr_init()
hand-rolled the log/handler/release sequence. Both now use
ATTEMPT/CATCH/PROCESS/FINISH_NORETURN. PASS does not fit: both sites are
void and have no caller to propagate to, so the terminal form of the same
idiom is the right one -- an unhandled failure prints its stack trace and
goes to akerr_handler_unhandled_error, which terminates, exactly as
before but without the bespoke plumbing. The legacy set path in
akerr_name_for_status() had the same shape and now handles its refusal
with HANDLE_DEFAULT, converting it to the "Unknown Error" sentinel.
Every remaining `if (x) { FAIL_RETURN }` in the registry is now
FAIL_ZERO_RETURN or FAIL_NONZERO_RETURN, and akerr_register_status_name()
checks both owner and name before passing either down --
akerr_store_status_name() reads a NULL owner as "caller did not identify
itself" for the legacy path, so a NULL arriving through the owned entry
point would have skipped the ownership check entirely.
New tests: err_copy_string (the guards above), err_library_status_fatal
(WILL_FAIL -- proves a refused library-status registration terminates).
Tests: ctest 31/31, mutation 80.7% (was 77.5%), line coverage 98.9%.
Branch coverage on src/error.c drops 64.5% -> 50.4%, just over its gate:
each FAIL_* site carries ~6 branch outcomes of error-construction
machinery that only run when that failure fires, and each PASS around a
call that cannot fail carries ~25, so added validation lowers the ratio
by construction. Recorded in TODO.md item 7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user