#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 #include #include #include #include #include /* ---------------------------------------------------------------------- */ /* 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