Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that
file to hold outstanding items only.
The API break gets a minor bump, because pre-1.0 the soname carries
MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five
signatures changed and the ato* contract with them; UPGRADING.md is new
and lists every one, with the before/after for the cases the compiler
cannot warn about.
Section 3.1 is finished: reallocarray with the multiplication checked,
aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf.
Four functions on that list are deliberately absent rather than missing
-- sprintf, strtok, setbuf and perror -- and TODO.md now says which and
why, so nobody adds them thinking they were forgotten.
Section 1.9, the cross-cutting tests:
tests/test_pool.c drives every failure path AKERR_MAX_ARRAY_ERROR
+ 10 times and checks the pool after each round,
because a wrapper that leaks a slot fails a
hundred calls later in unrelated code. It also
asserts that each error names the function and
file it was raised from, which is what catches a
FAIL that migrates into a helper during a
refactor: status right, message right, origin
quietly lying.
tests/negative/ two sources that must FAIL to compile, built with
-Werror and registered WILL_FAIL. AKERR_NOIGNORE
and the format attributes are enforced by the
compiler and by nothing else; drop either and
every ordinary test still passes.
Thread safety is answered rather than tested: the library is not
thread-safe and cannot be made so from here, because libakerror's error
pool is an unlocked process-global array. README.md says so plainly and
TODO.md carries it as the item blocking any future pthread wrappers.
Doxygen is configured and gated. All 147 public functions have @brief,
a @param each, @throws per status and @return; EXTRACT_ALL is off and
WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an
undocumented entity. It ran to 0 warnings. The Doxyfile carries no
version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from
project(), so that stays the one place a version is written.
CI now builds against the submodule it pins instead of also installing
libakerror@main and never linking it, adds -Werror, and gains a
sanitizer job. The pre-push hook matches, and runs the docs check too.
Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The
eight uncovered lines are each uncovered on purpose and TODO.md says
which and why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
357 lines
15 KiB
C
357 lines
15 KiB
C
#ifndef AKSL_TEST_CAPTURE_H
|
|
#define AKSL_TEST_CAPTURE_H
|
|
|
|
/*
|
|
* Shared test helpers for libakstdlib.
|
|
*
|
|
* Modelled on libakerror's tests/err_capture.h, with additions for the shape of
|
|
* this library: almost every akstdlib entry point returns an
|
|
* akerr_ErrorContext * that the caller owns and must release, so the common
|
|
* assertion is "this call returned status X" rather than "this call logged Y".
|
|
*
|
|
* What is here:
|
|
*
|
|
* AKSL_CHECK() an NDEBUG-proof assertion. Fails the test by
|
|
* returning 1 from the enclosing function (unlike
|
|
* assert(), which is compiled out in release builds
|
|
* and would silently turn a test into a no-op).
|
|
* AKSL_CHECK_STATUS() run an akerror-returning expression, assert on the
|
|
* status it came back with, and release the context
|
|
* so the error pool does not leak.
|
|
* AKSL_CHECK_STATUS_MSG_CONTAINS()
|
|
* assert status first, then message content.
|
|
* AKSL_CHECK_OK() assert a call returned no error context at all.
|
|
* aksl_last_status/... the status, message and function name of the most
|
|
* recent context taken by AKSL_CHECK_STATUS.
|
|
* aksl_capture_install() swap in a capturing akerr_log_method so a test can
|
|
* assert on the *content* of stack traces and
|
|
* unhandled-error output.
|
|
* aksl_slots_in_use() how many slots are currently checked out of
|
|
* AKERR_ARRAY_ERROR, for pool-leak assertions.
|
|
* aksl_temp_file() create an empty temp file for the stream, formatted
|
|
* output and path tests to work on.
|
|
* AKSL_RUN() run one test function and tally the result.
|
|
*
|
|
* Tests are written as a set of `static int test_xxx(void)` functions that
|
|
* return 0 on success and non-zero on failure, driven from main() by AKSL_RUN.
|
|
*/
|
|
|
|
#include <akstdlib.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Log capture */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
#define AKSL_CAPTURE_BUFSZ 65536
|
|
static char aksl_capture_buf[AKSL_CAPTURE_BUFSZ];
|
|
static size_t aksl_capture_len = 0;
|
|
|
|
static void __attribute__((unused)) aksl_capture_logger(const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
int n = vsnprintf(aksl_capture_buf + aksl_capture_len,
|
|
AKSL_CAPTURE_BUFSZ - aksl_capture_len, fmt, ap);
|
|
va_end(ap);
|
|
if ( n > 0 ) {
|
|
aksl_capture_len += (size_t)n;
|
|
if ( aksl_capture_len >= AKSL_CAPTURE_BUFSZ ) {
|
|
aksl_capture_len = AKSL_CAPTURE_BUFSZ - 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void __attribute__((unused)) aksl_capture_reset(void)
|
|
{
|
|
aksl_capture_len = 0;
|
|
aksl_capture_buf[0] = '\0';
|
|
}
|
|
|
|
/*
|
|
* Install the capturing logger. akerr_init() only assigns a default logger when
|
|
* akerr_log_method is NULL, and it is idempotent, so calling this either before
|
|
* or after the first PREPARE_ERROR keeps our logger in place.
|
|
*/
|
|
static void __attribute__((unused)) aksl_capture_install(void)
|
|
{
|
|
aksl_capture_reset();
|
|
akerr_log_method = &aksl_capture_logger;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Error pool accounting */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/* Count array slots currently checked out of the pool (refcount != 0). */
|
|
static int __attribute__((unused)) aksl_slots_in_use(void)
|
|
{
|
|
int n = 0;
|
|
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
|
if ( AKERR_ARRAY_ERROR[i].refcount != 0 ) {
|
|
n++;
|
|
}
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Temp files */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
#define AKSL_TMP_MAX 256
|
|
#define AKSL_TMP_TRACKED 32
|
|
|
|
static char aksl_tmp_paths[AKSL_TMP_TRACKED][AKSL_TMP_MAX];
|
|
static int aksl_tmp_count = 0;
|
|
|
|
/*
|
|
* Unlink every path aksl_temp_file() handed out. Registered with atexit, so the
|
|
* files go away even when a test returns early on a failed assertion -- which is
|
|
* the normal case for a known-failing test and for every mutant the mutation
|
|
* harness builds. Paths a test already unlinked simply fail here, harmlessly.
|
|
*/
|
|
static void aksl_temp_cleanup(void)
|
|
{
|
|
int i = 0;
|
|
|
|
for ( i = 0; i < aksl_tmp_count; i++ ) {
|
|
unlink(aksl_tmp_paths[i]);
|
|
}
|
|
aksl_tmp_count = 0;
|
|
}
|
|
|
|
/*
|
|
* Create an empty temp file under $TMPDIR (or /tmp) and write its path into buf.
|
|
* Returns 0 on success, non-zero on failure.
|
|
*
|
|
* mkstemp both names and creates the file, so a test that opens the path for
|
|
* reading is never racing another process for the name. Tests should still
|
|
* unlink what they create -- asserting on it catches a wrapper that removed or
|
|
* renamed the file -- but aksl_temp_cleanup() is the backstop.
|
|
*/
|
|
static int __attribute__((unused)) aksl_temp_file(char *buf, size_t n)
|
|
{
|
|
const char *dir = getenv("TMPDIR");
|
|
int fd = -1;
|
|
|
|
if ( dir == NULL || dir[0] == '\0' ) {
|
|
dir = "/tmp";
|
|
}
|
|
if ( (size_t)snprintf(buf, n, "%s/aksl_test_XXXXXX", dir) >= n ) {
|
|
return 1;
|
|
}
|
|
fd = mkstemp(buf);
|
|
if ( fd < 0 ) {
|
|
return 1;
|
|
}
|
|
close(fd);
|
|
|
|
if ( aksl_tmp_count < AKSL_TMP_TRACKED ) {
|
|
if ( aksl_tmp_count == 0 && atexit(&aksl_temp_cleanup) != 0 ) {
|
|
return 0; /* tracking is best-effort; the file itself is fine */
|
|
}
|
|
snprintf(aksl_tmp_paths[aksl_tmp_count], AKSL_TMP_MAX, "%s", buf);
|
|
aksl_tmp_count++;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Taking ownership of a returned error context */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
static int aksl_last_status = 0;
|
|
static char aksl_last_message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH];
|
|
/* Sized to match akerr_ErrorContext.function, which akerror declares with
|
|
* AKERR_MAX_ERROR_FNAME_LENGTH rather than AKERR_MAX_ERROR_FUNCTION_LENGTH. */
|
|
static char aksl_last_function[AKERR_MAX_ERROR_FNAME_LENGTH];
|
|
/*
|
|
* The source location the error was raised from. Captured so that
|
|
* tests/test_pool.c can assert an error came from the wrapper it names rather
|
|
* than from somewhere further down -- an error that reports the wrong origin is
|
|
* worse than useless when the only debugging you get is a log file after the
|
|
* fact, which is the stated reason this library exists.
|
|
*/
|
|
static char aksl_last_file[AKERR_MAX_ERROR_FNAME_LENGTH];
|
|
static int aksl_last_line = 0;
|
|
|
|
/*
|
|
* Record the status/message/function of a returned context, release it back to
|
|
* the pool, and hand back the status. A NULL context means success, which is
|
|
* status 0. Failure-path assertions should go through this so that no test
|
|
* leaks a pool slot. Success-path assertions use AKSL_CHECK_OK, which also
|
|
* verifies that no bogus status-0 context was returned.
|
|
*/
|
|
static int __attribute__((unused)) aksl_take(akerr_ErrorContext *e)
|
|
{
|
|
akerr_ErrorContext *released = NULL;
|
|
|
|
aksl_last_message[0] = '\0';
|
|
aksl_last_function[0] = '\0';
|
|
aksl_last_file[0] = '\0';
|
|
aksl_last_line = 0;
|
|
if ( e == NULL ) {
|
|
aksl_last_status = 0;
|
|
return 0;
|
|
}
|
|
aksl_last_status = e->status;
|
|
snprintf(aksl_last_message, sizeof(aksl_last_message), "%s", e->message);
|
|
snprintf(aksl_last_function, sizeof(aksl_last_function), "%s", e->function);
|
|
snprintf(aksl_last_file, sizeof(aksl_last_file), "%s", e->fname);
|
|
aksl_last_line = e->lineno;
|
|
/*
|
|
* akerr_release_error is marked warn_unused_result, and a (void) cast does
|
|
* not silence that in GCC, so the result is assigned and discarded.
|
|
*/
|
|
released = akerr_release_error(e);
|
|
(void)released;
|
|
return aksl_last_status;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Assertions */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
#define AKSL_CHECK(cond) \
|
|
do { \
|
|
if ( !(cond) ) { \
|
|
fprintf(stderr, " CHECK FAILED: %s at %s:%d\n", \
|
|
#cond, __FILE__, __LINE__); \
|
|
return 1; \
|
|
} \
|
|
} while ( 0 )
|
|
|
|
/*
|
|
* Run an akerr_ErrorContext *-returning expression and assert on its status.
|
|
* The context is always released, including on the failure path, so a failing
|
|
* assertion does not also corrupt the pool-leak checks that follow it.
|
|
*/
|
|
#define AKSL_CHECK_STATUS(__expr, __expected) \
|
|
do { \
|
|
int __st = aksl_take(__expr); \
|
|
if ( __st != (__expected) ) { \
|
|
fprintf(stderr, \
|
|
" CHECK FAILED: %s\n" \
|
|
" got %d (%s) \"%s\"\n" \
|
|
" expected %d (%s)\n" \
|
|
" at %s:%d\n", \
|
|
#__expr, \
|
|
__st, akerr_name_for_status(__st, NULL), \
|
|
aksl_last_message, \
|
|
(__expected), \
|
|
akerr_name_for_status((__expected), NULL), \
|
|
__FILE__, __LINE__); \
|
|
return 1; \
|
|
} \
|
|
} while ( 0 )
|
|
|
|
/*
|
|
* Success is represented by a NULL error context, not just status 0. This catches
|
|
* wrappers that incorrectly raise an error using a stale errno value of 0.
|
|
*/
|
|
#define AKSL_CHECK_OK(__expr) \
|
|
do { \
|
|
akerr_ErrorContext *__e = (__expr); \
|
|
if ( __e != NULL ) { \
|
|
int __st = aksl_take(__e); \
|
|
fprintf(stderr, \
|
|
" CHECK FAILED: %s\n" \
|
|
" returned error context with status %d (%s) \"%s\"\n" \
|
|
" expected no error context\n" \
|
|
" at %s:%d\n", \
|
|
#__expr, \
|
|
__st, akerr_name_for_status(__st, NULL), \
|
|
aksl_last_message, \
|
|
__FILE__, __LINE__); \
|
|
return 1; \
|
|
} \
|
|
aksl_last_status = 0; \
|
|
aksl_last_message[0] = '\0'; \
|
|
aksl_last_function[0] = '\0'; \
|
|
aksl_last_file[0] = '\0'; \
|
|
aksl_last_line = 0; \
|
|
} while ( 0 )
|
|
|
|
#define AKSL_CHECK_CONTAINS(needle) \
|
|
AKSL_CHECK(strstr(aksl_capture_buf, (needle)) != NULL)
|
|
|
|
#define AKSL_CHECK_NOT_CONTAINS(needle) \
|
|
AKSL_CHECK(strstr(aksl_capture_buf, (needle)) == NULL)
|
|
|
|
/*
|
|
* Message checks are secondary: the returned akerr status is the contract. Keep
|
|
* the status and message assertions coupled so tests cannot pass on text alone.
|
|
*/
|
|
#define AKSL_CHECK_STATUS_MSG_CONTAINS(__expr, __expected, __needle) \
|
|
do { \
|
|
int __st = aksl_take(__expr); \
|
|
if ( __st != (__expected) ) { \
|
|
fprintf(stderr, \
|
|
" CHECK FAILED: %s\n" \
|
|
" got %d (%s) \"%s\"\n" \
|
|
" expected %d (%s)\n" \
|
|
" at %s:%d\n", \
|
|
#__expr, \
|
|
__st, akerr_name_for_status(__st, NULL), \
|
|
aksl_last_message, \
|
|
(__expected), \
|
|
akerr_name_for_status((__expected), NULL), \
|
|
__FILE__, __LINE__); \
|
|
return 1; \
|
|
} \
|
|
if ( strstr(aksl_last_message, (__needle)) == NULL ) { \
|
|
fprintf(stderr, \
|
|
" CHECK FAILED: message from %s\n" \
|
|
" got \"%s\"\n" \
|
|
" expected substring \"%s\"\n" \
|
|
" status %d (%s)\n" \
|
|
" at %s:%d\n", \
|
|
#__expr, \
|
|
aksl_last_message, \
|
|
(__needle), \
|
|
__st, akerr_name_for_status(__st, NULL), \
|
|
__FILE__, __LINE__); \
|
|
return 1; \
|
|
} \
|
|
} while ( 0 )
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Test driver */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/*
|
|
* Run one test function, report it, and tally failures. Also asserts that the
|
|
* test left the error pool as it found it -- a wrapper that fails to release a
|
|
* context is a bug in the library, not just in the test.
|
|
*/
|
|
#define AKSL_RUN(__failures, __fn) \
|
|
do { \
|
|
int __before = aksl_slots_in_use(); \
|
|
int __r = __fn(); \
|
|
int __after = aksl_slots_in_use(); \
|
|
if ( __r != 0 ) { \
|
|
fprintf(stderr, "FAIL %s\n", #__fn); \
|
|
(__failures)++; \
|
|
} else if ( __after != __before ) { \
|
|
fprintf(stderr, \
|
|
"FAIL %s (leaked %d error pool slot(s))\n", \
|
|
#__fn, __after - __before); \
|
|
(__failures)++; \
|
|
} else { \
|
|
fprintf(stderr, "ok %s\n", #__fn); \
|
|
} \
|
|
} while ( 0 )
|
|
|
|
#define AKSL_REPORT(__failures) \
|
|
do { \
|
|
fprintf(stderr, "%s: %d failure(s)\n", __FILE__, (__failures)); \
|
|
return (__failures) == 0 ? 0 : 1; \
|
|
} while ( 0 )
|
|
|
|
#endif // AKSL_TEST_CAPTURE_H
|