Files
libakerror/tests/err_copy_string.c

59 lines
2.2 KiB
C
Raw Normal View History

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>
2026-07-30 21:53:33 -04:00
#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;
}