diff options
Diffstat (limited to 'tests/tuktest.h')
-rw-r--r-- | tests/tuktest.h | 1053 |
1 files changed, 1053 insertions, 0 deletions
diff --git a/tests/tuktest.h b/tests/tuktest.h new file mode 100644 index 0000000..508eace --- /dev/null +++ b/tests/tuktest.h @@ -0,0 +1,1053 @@ +/////////////////////////////////////////////////////////////////////////////// +// +/// \file tuktest.h +/// \brief Helper macros for writing simple test programs +/// \version 2023-01-08 +/// +/// Some inspiration was taken from STest by Keith Nicholas. +/// +/// This is standard C99/C11 only and thus should be fairly portable +/// outside POSIX systems too. +/// +/// This supports putting multiple tests in a single test program +/// although it is perfectly fine to have only one test per program. +/// Each test can produce one of these results: +/// - Pass +/// - Fail +/// - Skip +/// - Hard error (the remaining tests, if any, are not run) +/// +/// By default this produces an exit status that is compatible with +/// Automake and Meson, and mostly compatible with CMake.[1] +/// If a test program contains multiple tests, only one exit code can +/// be returned. Of the following, the first match is used: +/// - 99 if any test returned a hard error +/// - stdlib.h's EXIT_FAILURE if at least one test failed +/// - 77 if at least one test was skipped or no tests were run at all +/// - stdlib.h's EXIT_SUCCESS (0 on POSIX); that is, if none of the above +/// are true then there was at least one test to run and none of them +/// failed, was skipped, or returned a hard error. +/// +/// A summary of tests being run and their results are printed to stdout. +/// If you want ANSI coloring for the output, #define TUKTEST_COLOR. +/// If you only want output when something goes wrong, #define TUKTEST_QUIET. +/// +/// The downside of the above mapping is that it cannot indicate if +/// some tests were skipped and some passed. If that is likely to +/// happen it may be better to split into multiple test programs (one +/// test per program) or use the TAP mode described below. +/// +/// By using #define TUKTEST_TAP before #including this file the +/// output will be Test Anything Protocol (TAP) version 12 compatible +/// and the exit status will always be EXIT_SUCCESS. This can be easily +/// used with Automake via its tap-driver.sh. Meson supports TAP natively. +/// TAP's todo-directive isn't supported for now, mostly because it's not +/// trivially convertible to the exit-status reporting method. +/// +/// If TUKTEST_TAP is used, TUKTEST_QUIET and TUKTEST_COLOR are ignored. +/// +/// The main() function may look like this (remember to include config.h +/// or such files too if needed!): +/// +/// #include "tuktest.h" +/// +/// int main(int argc, char **argv) +/// { +/// tuktest_start(argc, argv); +/// +/// if (!is_package_foo_available()) +/// tuktest_early_skip("Optional package foo is not available"); +/// +/// if (!do_common_initializations()) +/// tuktest_error("Error during common initializations"); +/// +/// tuktest_run(testfunc1); +/// tuktest_run(testfunc2); +/// +/// return tuktest_end(); +/// } +/// +/// Using exit(tuktest_end()) as a pair to tuktest_start() is OK too. +/// +/// Each test function called via tuktest_run() should be of type +/// "void testfunc1(void)". The test functions should use the +/// various assert_CONDITION() macros. The current test stops if +/// an assertion fails (this is implemented with setjmp/longjmp). +/// Execution continues from the next test unless the failure was +/// due to assert_error() (indicating a hard error) which makes +/// the program exit() without running any remaining tests. +/// +/// Search for "define assert" in this file to find the explanations +/// of the available assertion macros. +/// +/// IMPORTANT: +/// +/// - The assert_CONDITION() macros may only be used by code that is +/// called via tuktest_run()! This includes the function named in +/// the tuktest_run() call and functions called further from there. +/// (The assert_CONDITION() macros depend on setup code in tuktest_run() +/// and other use results in undefined behavior.) +/// +/// - tuktest_start(), tuktest_early_skip, tuktest_run(), and tuktest_end() +/// must not be used in the tests called via tuktest_run()! (tuktest_end() +/// is called more freely internally by this file but such use isn't part +/// of the API.) +/// +/// - tuktest_error(), tuktest_malloc(), tuktest_free(), +/// tuktest_file_from_srcdir(), and tuktest_file_from_builddir() +/// can be used everywhere after tuktest_start() has been called. +/// (In tests running under tuktest_run(), assert_error() can be used +/// instead of tuktest_error() when a hard error occurs.) +/// +/// - Everything else is for internal use only. +/// +/// Footnotes: +/// +/// [1] As of 2022-06-02: +/// See the Automake manual "info (automake)Scripts-based Testsuites" or: +/// https://www.gnu.org/software/automake/manual/automake.html#Scripts_002dbased-Testsuites +/// +/// Meson: https://mesonbuild.com/Unit-tests.html +/// +/// CMake handles passing and failing tests by default but treats hard +/// errors as regular fails. To CMake support skipped tests correctly, +/// one has to set the SKIP_RETURN_CODE property for each test: +/// +/// set_tests_properties(foo_test_name PROPERTIES SKIP_RETURN_CODE 77) +/// +/// See: +/// https://cmake.org/cmake/help/latest/command/set_tests_properties.html +/// https://cmake.org/cmake/help/latest/prop_test/SKIP_RETURN_CODE.html +// +// Author: Lasse Collin +// +// This file has been put into the public domain. +// You can do whatever you want with this file. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef TUKTEST_H +#define TUKTEST_H + +#include <stddef.h> + +// On some (too) old systems inttypes.h doesn't exist or isn't good enough. +// Include it conditionally so that any portability tricks can be done before +// tuktest.h is included. On any modern system inttypes.h is fine as is. +#ifndef PRIu64 +# include <inttypes.h> +#endif + +#include <setjmp.h> +#include <stdlib.h> +#include <string.h> +#include <stdio.h> + + +#if defined(__GNUC__) && defined(__GNUC_MINOR__) +# define TUKTEST_GNUC_REQ(major, minor) \ + ((__GNUC__ == (major) && __GNUC_MINOR__ >= (minor)) \ + || __GNUC__ > (major)) +#else +# define TUKTEST_GNUC_REQ(major, minor) 0 +#endif + + +// This is silencing warnings about unused functions. Not all test programs +// need all functions from this header. +#if TUKTEST_GNUC_REQ(3, 0) +# define tuktest_maybe_unused __attribute__((__unused__)) +#else +# define tuktest_maybe_unused +#endif + +// We need printf("") so silence the warning about empty format string. +#if TUKTEST_GNUC_REQ(4, 2) +# pragma GCC diagnostic ignored "-Wformat-zero-length" +#endif + + +// Types and printf format macros to use in integer assertions and also for +// printing size_t values (C99's %zu isn't available on very old systems). +typedef int64_t tuktest_int; +typedef uint64_t tuktest_uint; +#define TUKTEST_PRId PRId64 +#define TUKTEST_PRIu PRIu64 +#define TUKTEST_PRIX PRIX64 + + +// When TAP mode isn't used, Automake-compatible exit statuses are used. +#define TUKTEST_EXIT_PASS EXIT_SUCCESS +#define TUKTEST_EXIT_FAIL EXIT_FAILURE +#define TUKTEST_EXIT_SKIP 77 +#define TUKTEST_EXIT_ERROR 99 + + +enum tuktest_result { + TUKTEST_PASS, + TUKTEST_FAIL, + TUKTEST_SKIP, + TUKTEST_ERROR, +}; + + +#ifdef TUKTEST_TAP +# undef TUKTEST_QUIET +# undef TUKTEST_COLOR +# undef TUKTEST_TAP +# define TUKTEST_TAP 1 +# define TUKTEST_STR_PASS "ok -" +# define TUKTEST_STR_FAIL "not ok -" +# define TUKTEST_STR_SKIP "ok - # SKIP" +# define TUKTEST_STR_ERROR "Bail out!" +#else +# define TUKTEST_TAP 0 +# ifdef TUKTEST_COLOR +# define TUKTEST_COLOR_PASS "\x1B[0;32m" +# define TUKTEST_COLOR_FAIL "\x1B[0;31m" +# define TUKTEST_COLOR_SKIP "\x1B[1;34m" +# define TUKTEST_COLOR_ERROR "\x1B[0;35m" +# define TUKTEST_COLOR_TOTAL "\x1B[1m" +# define TUKTEST_COLOR_OFF "\x1B[m" +# define TUKTEST_COLOR_IF(cond, color) ((cond) ? (color) : "" ) +# else +# define TUKTEST_COLOR_PASS "" +# define TUKTEST_COLOR_FAIL "" +# define TUKTEST_COLOR_SKIP "" +# define TUKTEST_COLOR_ERROR "" +# define TUKTEST_COLOR_TOTAL "" +# define TUKTEST_COLOR_OFF "" +# define TUKTEST_COLOR_IF(cond, color) "" +# endif +# define TUKTEST_COLOR_ADD(str, color) color str TUKTEST_COLOR_OFF +# define TUKTEST_STR_PASS \ + TUKTEST_COLOR_ADD("PASS:", TUKTEST_COLOR_PASS) +# define TUKTEST_STR_FAIL \ + TUKTEST_COLOR_ADD("FAIL:", TUKTEST_COLOR_FAIL) +# define TUKTEST_STR_SKIP \ + TUKTEST_COLOR_ADD("SKIP:", TUKTEST_COLOR_SKIP) +# define TUKTEST_STR_ERROR \ + TUKTEST_COLOR_ADD("ERROR:", TUKTEST_COLOR_ERROR) +#endif + +// NOTE: If TUKTEST_TAP is defined then TUKTEST_QUIET will get undefined above. +#ifndef TUKTEST_QUIET +# define TUKTEST_QUIET 0 +#else +# undef TUKTEST_QUIET +# define TUKTEST_QUIET 1 +#endif + + +// Counts of the passed, failed, skipped, and hard-errored tests. +// This is indexed with the enumeration constants from enum tuktest_result. +static unsigned tuktest_stats[4] = { 0, 0, 0, 0 }; + +// Copy of argc and argv from main(). These are set by tuktest_start(). +static int tuktest_argc = 0; +static char **tuktest_argv = NULL; + +// Name of the currently-running test. This exists because it's nice +// to print the main test function name even if the failing test-assertion +// fails in a function called by the main test function. +static const char *tuktest_name = NULL; + +// longjmp() target for when a test-assertion fails. +static jmp_buf tuktest_jmpenv; + + +// This declaration is needed for tuktest_malloc(). +static int tuktest_end(void); + + +// Internal helper for handling hard errors both inside and +// outside tuktest_run(). +#define tuktest_error_impl(filename, line, ...) \ +do { \ + tuktest_print_result_prefix(TUKTEST_ERROR, filename, line); \ + printf(__VA_ARGS__); \ + printf("\n"); \ + ++tuktest_stats[TUKTEST_ERROR]; \ + exit(tuktest_end()); \ +} while (0) + + +// printf() is without checking its return value in many places. This function +// is called before exiting to check the status of stdout and catch errors. +static void +tuktest_catch_stdout_errors(void) +{ + if (ferror(stdout) || fclose(stdout)) { + fputs("Error while writing to stdout\n", stderr); + exit(TUKTEST_EXIT_ERROR); + } +} + + +// A simplified basename()-like function that is good enough for +// cleaning up __FILE__. This supports / and \ as path separator. +// If the path separator is wrong then the full path will be printed; +// it's a cosmetic problem only. +static const char * +tuktest_basename(const char *filename) +{ + for (const char *p = filename + strlen(filename); p > filename; --p) + if (*p == '/' || *p == '\\') + return p + 1; + + return filename; +} + + +// Internal helper that prints the prefix of the fail/skip/error message line. +static void +tuktest_print_result_prefix(enum tuktest_result result, + const char *filename, unsigned line) +{ + // This is never called with TUKTEST_PASS but I kept it here anyway. + const char *result_str + = result == TUKTEST_PASS ? TUKTEST_STR_PASS + : result == TUKTEST_FAIL ? TUKTEST_STR_FAIL + : result == TUKTEST_SKIP ? TUKTEST_STR_SKIP + : TUKTEST_STR_ERROR; + + const char *short_filename = tuktest_basename(filename); + + if (tuktest_name != NULL) + printf("%s %s [%s:%u] ", result_str, tuktest_name, + short_filename, line); + else + printf("%s [%s:%u] ", result_str, short_filename, line); +} + + +// An entry for linked list of memory allocations. +struct tuktest_malloc_record { + struct tuktest_malloc_record *next; + void *p; +}; + +// Linked list of per-test allocations. This is used when under tuktest_run(). +// These allocations are freed in tuktest_run() and, in case of a hard error, +// also in tuktest_end(). +static struct tuktest_malloc_record *tuktest_malloc_test = NULL; + +// Linked list of global allocations. This is used allocations are made +// outside tuktest_run(). These are freed in tuktest_end(). +static struct tuktest_malloc_record *tuktest_malloc_global = NULL; + + +/// A wrapper for malloc() that never return NULL and the allocated memory is +/// automatically freed at the end of tuktest_run() (if allocation was done +/// within a test) or early in tuktest_end() (if allocation was done outside +/// tuktest_run()). +/// +/// If allocation fails, a hard error is reported and this function won't +/// return. Possible other tests won't be run (this will call exit()). +#define tuktest_malloc(size) tuktest_malloc_impl(size, __FILE__, __LINE__) + +static void * +tuktest_malloc_impl(size_t size, const char *filename, unsigned line) +{ + void *p = malloc(size == 0 ? 1 : size); + struct tuktest_malloc_record *r = malloc(sizeof(*r)); + + if (p == NULL || r == NULL) { + free(r); + free(p); + + // Avoid %zu for portability to very old systems that still + // can compile C99 code. + tuktest_error_impl(filename, line, + "tuktest_malloc(%" TUKTEST_PRIu ") failed", + (tuktest_uint)size); + } + + r->p = p; + + if (tuktest_name == NULL) { + // We were called outside tuktest_run(). + r->next = tuktest_malloc_global; + tuktest_malloc_global = r; + } else { + // We were called under tuktest_run(). + r->next = tuktest_malloc_test; + tuktest_malloc_test = r; + } + + return p; +} + + +/// Frees memory allocated using tuktest_malloc(). Usually this isn't needed +/// as the memory is freed automatically. +/// +/// NULL is silently ignored. +/// +/// NOTE: Under tuktest_run() only memory allocated there can be freed. +/// That is, allocations done outside tuktest_run() can only be freed +/// outside tuktest_run(). +#define tuktest_free(ptr) tuktest_free_impl(ptr, __FILE__, __LINE__) + +static void +tuktest_free_impl(void *p, const char *filename, unsigned line) +{ + if (p == NULL) + return; + + struct tuktest_malloc_record **r = tuktest_name != NULL + ? &tuktest_malloc_test : &tuktest_malloc_global; + + while (*r != NULL) { + struct tuktest_malloc_record *tmp = *r; + + if (tmp->p == p) { + *r = tmp->next; + free(p); + free(tmp); + return; + } + + r = &tmp->next; + } + + tuktest_error_impl(filename, line, "tuktest_free: " + "Allocation matching the pointer was not found"); +} + + +// Frees all allocates in the given record list. The argument must be +// either &tuktest_malloc_test or &tuktest_malloc_global. +static void +tuktest_free_all(struct tuktest_malloc_record **r) +{ + while (*r != NULL) { + struct tuktest_malloc_record *tmp = *r; + *r = tmp->next; + free(tmp->p); + free(tmp); + } +} + + +/// Initialize the test framework. No other functions or macros +/// from this file may be called before calling this. +/// +/// If the arguments from main() aren't available, use 0 and NULL. +/// If these are set, then only a subset of tests can be run by +/// specifying their names on the command line. +#define tuktest_start(argc, argv) \ +do { \ + tuktest_argc = argc; \ + tuktest_argv = argv; \ + if (!TUKTEST_TAP && !TUKTEST_QUIET) \ + printf("=== %s ===\n", tuktest_basename(__FILE__)); \ +} while (0) + + +/// If it can be detected early that no tests can be run, this macro can +/// be called after tuktest_start() but before any tuktest_run() to print +/// a reason why the tests were skipped. Note that this macro calls exit(). +/// +/// Using "return tuktest_end();" in main() when no tests were run has +/// the same result as tuktest_early_skip() except that then no reason +/// for the skipping can be printed. +#define tuktest_early_skip(...) \ +do { \ + printf("%s [%s:%u] ", \ + TUKTEST_TAP ? "1..0 # SKIP" : TUKTEST_STR_SKIP, \ + tuktest_basename(__FILE__), __LINE__); \ + printf(__VA_ARGS__); \ + printf("\n"); \ + if (!TUKTEST_TAP && !TUKTEST_QUIET) \ + printf("=== END ===\n"); \ + tuktest_catch_stdout_errors(); \ + exit(TUKTEST_TAP ? EXIT_SUCCESS : TUKTEST_EXIT_SKIP); \ +} while (0) + + +/// Some test programs need to do initializations before or between +/// calls to tuktest_run(). If such initializations unexpectedly fail, +/// tuktest_error() can be used to report it as a hard error outside +/// test functions, for example, in main(). Then the remaining tests +/// won't be run (this macro calls exit()). +/// +/// Typically tuktest_error() would be used before any tuktest_run() +/// calls but it is also possible to use tuktest_error() after one or +/// more tests have been run with tuktest_run(). This is in contrast to +/// tuktest_early_skip() which must never be called after tuktest_run(). +/// +/// NOTE: tuktest_start() must have been called before tuktest_error(). +/// +/// NOTE: This macro can be called from test functions running under +/// tuktest_run() but assert_error() is somewhat preferred in that context. +#define tuktest_error(...) tuktest_error_impl(__FILE__, __LINE__, __VA_ARGS__) + + +/// At the end of main() one should have "return tuktest_end();" which +/// prints the stats or the TAP plan, and handles the exit status. +/// Using exit(tuktest_end()) is OK too. +/// +/// If the test program can detect early that all tests must be skipped, +/// then tuktest_early_skip() may be useful so that the reason why the +/// tests were skipped can be printed. +static int +tuktest_end(void) +{ + tuktest_free_all(&tuktest_malloc_test); + tuktest_free_all(&tuktest_malloc_global); + + unsigned total_tests = 0; + for (unsigned i = 0; i <= TUKTEST_ERROR; ++i) + total_tests += tuktest_stats[i]; + + if (tuktest_stats[TUKTEST_ERROR] == 0 && tuktest_argc > 1 + && (unsigned)(tuktest_argc - 1) > total_tests) { + printf(TUKTEST_STR_ERROR " Fewer tests were run than " + "specified on the command line. " + "Was a test name mistyped?\n"); + ++tuktest_stats[TUKTEST_ERROR]; + } + +#if TUKTEST_TAP + // Print the plan only if no "Bail out!" has occurred. + // Print the skip directive if no tests were run. + // We cannot know the reason for the skip here though + // (see tuktest_early_skip()). + if (tuktest_stats[TUKTEST_ERROR] == 0) + printf("1..%u%s\n", total_tests, + total_tests == 0 ? " # SKIP" : ""); + + tuktest_catch_stdout_errors(); + return EXIT_SUCCESS; +#else + if (!TUKTEST_QUIET) + printf("---\n" + "%s# TOTAL: %u" TUKTEST_COLOR_OFF "\n" + "%s# PASS: %u" TUKTEST_COLOR_OFF "\n" + "%s# SKIP: %u" TUKTEST_COLOR_OFF "\n" + "%s# FAIL: %u" TUKTEST_COLOR_OFF "\n" + "%s# ERROR: %u" TUKTEST_COLOR_OFF "\n" + "=== END ===\n", + TUKTEST_COLOR_TOTAL, + total_tests, + TUKTEST_COLOR_IF( + tuktest_stats[TUKTEST_PASS] > 0, + TUKTEST_COLOR_PASS), + tuktest_stats[TUKTEST_PASS], + TUKTEST_COLOR_IF( + tuktest_stats[TUKTEST_SKIP] > 0, + TUKTEST_COLOR_SKIP), + tuktest_stats[TUKTEST_SKIP], + TUKTEST_COLOR_IF( + tuktest_stats[TUKTEST_FAIL] > 0, + TUKTEST_COLOR_FAIL), + tuktest_stats[TUKTEST_FAIL], + TUKTEST_COLOR_IF( + tuktest_stats[TUKTEST_ERROR] > 0, + TUKTEST_COLOR_ERROR), + tuktest_stats[TUKTEST_ERROR]); + + tuktest_catch_stdout_errors(); + + if (tuktest_stats[TUKTEST_ERROR] > 0) + return TUKTEST_EXIT_ERROR; + + if (tuktest_stats[TUKTEST_FAIL] > 0) + return TUKTEST_EXIT_FAIL; + + if (tuktest_stats[TUKTEST_SKIP] > 0 || total_tests == 0) + return TUKTEST_EXIT_SKIP; + + return TUKTEST_EXIT_PASS; +#endif +} + + +/// Runs the specified test function. Requires that tuktest_start() +/// has already been called and that tuktest_end() has NOT been called yet. +#define tuktest_run(testfunc) \ + tuktest_run_test(&(testfunc), #testfunc) + +tuktest_maybe_unused +static void +tuktest_run_test(void (*testfunc)(void), const char *testfunc_str) +{ + // If any command line arguments were given, only the test functions + // named on the command line will be run. + if (tuktest_argc > 1) { + int i = 1; + while (strcmp(tuktest_argv[i], testfunc_str) != 0) + if (++i == tuktest_argc) + return; + } + + // This is set so that failed assertions can print the correct + // test name even when the assertion is in a helper function + // called by the test function. + tuktest_name = testfunc_str; + + // The way setjmp() may be called is very restrictive. + // A switch statement is one of the few conforming ways + // to get the value passed to longjmp(); doing something + // like "int x = setjmp(env)" is NOT allowed (undefined behavior). + switch (setjmp(tuktest_jmpenv)) { + case 0: + testfunc(); + ++tuktest_stats[TUKTEST_PASS]; + if (!TUKTEST_QUIET) + printf(TUKTEST_STR_PASS " %s\n", tuktest_name); + break; + + case TUKTEST_FAIL: + ++tuktest_stats[TUKTEST_FAIL]; + break; + + case TUKTEST_SKIP: + ++tuktest_stats[TUKTEST_SKIP]; + break; + + default: + ++tuktest_stats[TUKTEST_ERROR]; + exit(tuktest_end()); + } + + tuktest_free_all(&tuktest_malloc_test); + tuktest_name = NULL; +} + + +// Maximum allowed file size in tuktest_file_from_* macros and functions. +#ifndef TUKTEST_FILE_SIZE_MAX +# define TUKTEST_FILE_SIZE_MAX (64L << 20) +#endif + +/// Allocates memory and reads the specified file into a buffer. +/// If the environment variable srcdir is set, it will be prefixed +/// to the filename. Otherwise the filename is used as is (and so +/// the behavior is identical to tuktest_file_from_builddir() below). +/// +/// On success the a pointer to malloc'ed memory is returned. +/// The size of the allocation and the file is stored in *size. +/// +/// If anything goes wrong, a hard error is reported and this function +/// won't return. Possible other tests won't be run (this will call exit()). +/// +/// Empty files and files over TUKTEST_FILE_SIZE_MAX are rejected. +/// The assumption is that something is wrong in these cases. +/// +/// This function can be called either from outside the tests (like in main()) +/// or from tests run via tuktest_run(). Remember to free() the memory to +/// keep Valgrind happy. +#define tuktest_file_from_srcdir(filename, sizeptr) \ + tuktest_file_from_x(getenv("srcdir"), filename, sizeptr, \ + __FILE__, __LINE__) + +/// Like tuktest_file_from_srcdir except this reads from the current directory. +#define tuktest_file_from_builddir(filename, sizeptr) \ + tuktest_file_from_x(NULL, filename, sizeptr, __FILE__, __LINE__) + +// Internal helper for the macros above. +tuktest_maybe_unused +static void * +tuktest_file_from_x(const char *prefix, const char *filename, size_t *size, + const char *prog_filename, unsigned prog_line) +{ + // If needed: buffer for holding prefix + '/' + filename + '\0'. + char *alloc_name = NULL; + + // Buffer for the data read from the file. + void *buf = NULL; + + // File being read + FILE *f = NULL; + + // Error message to use under the "error:" label. + const char *error_msg = NULL; + + if (filename == NULL) { + error_msg = "Filename is NULL"; + filename = "(NULL)"; + goto error; + } + + if (filename[0] == '\0') { + error_msg = "Filename is an empty string"; + filename = "(empty string)"; + goto error; + } + + if (size == NULL) { + error_msg = "The size argument is NULL"; + goto error; + } + + // If a prefix was given, construct the full filename. + if (prefix != NULL && prefix[0] != '\0') { + const size_t prefix_len = strlen(prefix); + const size_t filename_len = strlen(filename); + + const size_t alloc_name_size + = prefix_len + 1 + filename_len + 1; + alloc_name = tuktest_malloc_impl(alloc_name_size, + prog_filename, prog_line); + + memcpy(alloc_name, prefix, prefix_len); + alloc_name[prefix_len] = '/'; + memcpy(alloc_name + prefix_len + 1, filename, filename_len); + alloc_name[prefix_len + 1 + filename_len] = '\0'; + + // Set filename to point to the new string. alloc_name + // can be freed unconditionally as it is NULL if a prefix + // wasn't specified. + filename = alloc_name; + } + + f = fopen(filename, "rb"); + if (f == NULL) { + error_msg = "Failed to open the file"; + goto error; + } + + // Get the size of the file and store it in *size. + // + // We assume that the file isn't big and even reject very big files. + // There is no need to use fseeko/ftello from POSIX to support + // large files. Using standard C functions is portable outside POSIX. + if (fseek(f, 0, SEEK_END) != 0) { + error_msg = "Seeking failed (fseek end)"; + goto error; + } + + const long end = ftell(f); + if (end < 0) { + error_msg = "Seeking failed (ftell)"; + goto error; + } + + if (end == 0) { + error_msg = "File is empty"; + goto error; + } + + if (end > TUKTEST_FILE_SIZE_MAX) { + error_msg = "File size exceeds TUKTEST_FILE_SIZE_MAX"; + goto error; + } + + *size = (size_t)end; + rewind(f); + + buf = tuktest_malloc_impl(*size, prog_filename, prog_line); + + const size_t amount = fread(buf, 1, *size, f); + if (ferror(f)) { + error_msg = "Read error"; + goto error; + } + + if (amount != *size) { + error_msg = "File is smaller than indicated by ftell()"; + goto error; + } + + const int fclose_ret = fclose(f); + f = NULL; + if (fclose_ret != 0) { + error_msg = "Error closing the file"; + goto error; + } + + tuktest_free(alloc_name); + return buf; + +error: + if (f != NULL) + (void)fclose(f); + + tuktest_error_impl(prog_filename, prog_line, + "tuktest_file_from_x: %s: %s\n", filename, error_msg); +} + + +// Internal helper for assert_fail, assert_skip, and assert_error. +#define tuktest_print_and_jump(result, ...) \ +do { \ + tuktest_print_result_prefix(result, __FILE__, __LINE__); \ + printf(__VA_ARGS__); \ + printf("\n"); \ + longjmp(tuktest_jmpenv, result); \ +} while (0) + + +/// Unconditionally fails the test (non-zero exit status if not using TAP). +/// Execution will continue from the next test. +/// +/// A printf format string is supported. +/// If no extra message is wanted, use "" as the argument. +#define assert_fail(...) tuktest_print_and_jump(TUKTEST_FAIL, __VA_ARGS__) + + +/// Skips the test (exit status 77 if not using TAP). +/// Execution will continue from the next test. +/// +/// If you can detect early that no tests can be run, tuktest_early_skip() +/// might be a better way to skip the test(s). Especially in TAP mode this +/// makes a difference as with assert_skip() it will list a skipped specific +/// test name but with tuktest_early_skip() it will indicate that the whole +/// test program was skipped (with tuktest_early_skip() the TAP plan will +/// indicate zero tests). +/// +/// A printf format string is supported. +/// If no extra message is wanted, use "" as the argument. +#define assert_skip(...) tuktest_print_and_jump(TUKTEST_SKIP, __VA_ARGS__) + + +/// Hard error (exit status 99 if not using TAP). +/// The remaining tests in this program will not be run or reported. +/// +/// A printf format string is supported. +/// If no extra message is wanted, use "" as the argument. +#define assert_error(...) tuktest_print_and_jump(TUKTEST_ERROR, __VA_ARGS__) + + +/// Fails the test if the test expression doesn't evaluate to false. +#define assert_false(test_expr) \ +do { \ + if (test_expr) \ + assert_fail("assert_fail: '%s' is true but should be false", \ + #test_expr); \ +} while (0) + + +/// Fails the test if the test expression doesn't evaluate to true. +#define assert_true(test_expr) \ +do { \ + if (!(test_expr)) \ + assert_fail("assert_true: '%s' is false but should be true", \ + #test_expr); \ +} while (0) + + +/// Fails the test if comparing the signed integer expressions using the +/// specified comparison operator evaluates to false. For example, +/// assert_int(foobar(), >=, 0) fails the test if 'foobar() >= 0' isn't true. +/// For good error messages, the first argument should be the test expression +/// and the third argument the reference value (usually a constant). +/// +/// For equality (==) comparison there is a assert_int_eq() which +/// might be more convenient to use. +#define assert_int(test_expr, cmp_op, ref_value) \ +do { \ + const tuktest_int v_test_ = (test_expr); \ + const tuktest_int v_ref_ = (ref_value); \ + if (!(v_test_ cmp_op v_ref_)) \ + assert_fail("assert_int: '%s == %" TUKTEST_PRId \ + "' but expected '... %s %" TUKTEST_PRId "'", \ + #test_expr, v_test_, #cmp_op, v_ref_); \ +} while (0) + + +/// Like assert_int() but for unsigned integers. +/// +/// For equality (==) comparison there is a assert_uint_eq() which +/// might be more convenient to use. +#define assert_uint(test_expr, cmp_op, ref_value) \ +do { \ + const tuktest_uint v_test_ = (test_expr); \ + const tuktest_uint v_ref_ = (ref_value); \ + if (!(v_test_ cmp_op v_ref_)) \ + assert_fail("assert_uint: '%s == %" TUKTEST_PRIu \ + "' but expected '... %s %" TUKTEST_PRIu "'", \ + #test_expr, v_test_, #cmp_op, v_ref_); \ +} while (0) + + +/// Fails the test if test expression doesn't equal the expected +/// signed integer value. +#define assert_int_eq(test_expr, ref_value) \ + assert_int(test_expr, ==, ref_value) + + +/// Fails the test if test expression doesn't equal the expected +/// unsigned integer value. +#define assert_uint_eq(test_expr, ref_value) \ + assert_uint(test_expr, ==, ref_value) + + +/// Fails the test if the test expression doesn't equal the expected +/// enumeration value. This is like assert_int_eq() but the error message +/// shows the enumeration constant names instead of their numeric values +/// as long as the values are non-negative and not big. +/// +/// The third argument must be a table of string pointers. A pointer to +/// a pointer doesn't work because this determines the number of elements +/// in the array using sizeof. For example: +/// +/// const char *my_enum_names[] = { "MY_FOO", "MY_BAR", "MY_BAZ" }; +/// assert_enum_eq(some_func_returning_my_enum(), MY_BAR, my_enum_names); +/// +/// (If the reference value is out of bounds, both values are printed as +/// an integer. If only test expression is out of bounds, it is printed +/// as an integer and the reference as a string. Otherwise both are printed +/// as a string.) +#define assert_enum_eq(test_expr, ref_value, enum_strings) \ +do { \ + const tuktest_int v_test_ = (test_expr); \ + const tuktest_int v_ref_ = (ref_value); \ + if (v_test_ != v_ref_) { \ + const int array_len_ = (int)(sizeof(enum_strings) \ + / sizeof((enum_strings)[0])); \ + if (v_ref_ < 0 || v_ref_ >= array_len_) \ + assert_fail("assert_enum_eq: '%s == %" TUKTEST_PRId \ + "' but expected " \ + "'... == %" TUKTEST_PRId "'", \ + #test_expr, v_test_, v_ref_); \ + else if (v_test_ < 0 || v_test_ >= array_len_) \ + assert_fail("assert_enum_eq: '%s == %" TUKTEST_PRId \ + "' but expected '... == %s'", \ + #test_expr, v_test_, \ + (enum_strings)[v_ref_]); \ + else \ + assert_fail("assert_enum_eq: '%s == %s' " \ + "but expected '... = %s'", \ + #test_expr, (enum_strings)[v_test_], \ + (enum_strings)[v_ref_]); \ + } \ +} while (0) + + +/// Fails the test if the specified bit isn't set in the test expression. +#define assert_bit_set(test_expr, bit) \ +do { \ + const tuktest_uint v_test_ = (test_expr); \ + const unsigned v_bit_ = (bit); \ + const tuktest_uint v_mask_ = (tuktest_uint)1 << v_bit_; \ + if (!(v_test_ & v_mask_)) \ + assert_fail("assert_bit_set: '%s == 0x%" TUKTEST_PRIX \ + "' but bit %u (0x%" TUKTEST_PRIX ") " \ + "is not set", \ + #test_expr, v_test_, v_bit_, v_mask_); \ +} while (0) + + +/// Fails the test if the specified bit is set in the test expression. +#define assert_bit_not_set(test_expr, bit) \ +do { \ + const tuktest_uint v_test_ = (test_expr); \ + const unsigned v_bit_ = (bit); \ + const tuktest_uint v_mask_ = (tuktest_uint)1 << v_bit_; \ + if (v_test_ & v_mask_) \ + assert_fail("assert_bit_not_set: '%s == 0x%" TUKTEST_PRIX \ + "' but bit %u (0x%" TUKTEST_PRIX ") is set", \ + #test_expr, v_test_, v_bit_, v_mask_); \ +} while (0) + + +/// Fails the test if unless all bits that are set in the bitmask are also +/// set in the test expression. +#define assert_bitmask_set(test_expr, mask) \ +do { \ + const tuktest_uint v_mask_ = (mask); \ + const tuktest_uint v_test_ = (test_expr) & v_mask_; \ + if (v_test_ != v_mask_) \ + assert_fail("assert_bitmask_set: " \ + "'((%s) & 0x%" TUKTEST_PRIX ") == " \ + "0x%" TUKTEST_PRIX "' but expected " \ + "'... == 0x%" TUKTEST_PRIX "'", \ + #test_expr, v_mask_, v_test_, v_mask_); \ +} while (0) + + +/// Fails the test if any of the bits that are set in the bitmask are also +/// set in the test expression. +#define assert_bitmask_not_set(test_expr, mask) \ +do { \ + const tuktest_uint v_mask_ = (mask); \ + const tuktest_uint v_test_ = (test_expr) & v_mask_; \ + if (v_test_ != 0) \ + assert_fail("assert_bitmask_not_set: "\ + "'((%s) & 0x%" TUKTEST_PRIX ") == " \ + "0x%" TUKTEST_PRIX "' but expected " \ + "'... == 0'", \ + #test_expr, v_mask_, v_test_); \ +} while (0) + + +// Internal helper to add common code for string assertions. +#define tuktest_str_helper1(macro_name, test_expr, ref_value) \ + const char *v_test_ = (test_expr); \ + const char *v_ref_ = (ref_value); \ + if (v_test_ == NULL) \ + assert_fail(macro_name ": Test expression '%s' is NULL", \ + #test_expr); \ + if (v_ref_ == NULL) \ + assert_fail(macro_name ": Reference value '%s' is NULL", \ + #ref_value) + + +// Internal helper to add common code for string assertions and to check +// that the reference value isn't an empty string. +#define tuktest_str_helper2(macro_name, test_expr, ref_value) \ + tuktest_str_helper1(macro_name, test_expr, ref_value); \ + if (v_ref_[0] == '\0') \ + assert_fail(macro_name ": Reference value is an empty string") + + +/// Fails the test if the test expression evaluates to string that doesn't +/// equal to the expected string. +#define assert_str_eq(test_expr, ref_value) \ +do { \ + tuktest_str_helper1("assert_str_eq", test_expr, ref_value); \ + if (strcmp(v_ref_, v_test_) != 0) \ + assert_fail("assert_str_eq: '%s' evaluated to '%s' " \ + "but expected '%s'", \ + #test_expr, v_test_, v_ref_); \ +} while (0) + + +/// Fails the test if the test expression evaluates to a string that doesn't +/// contain the reference value as a substring. Also fails the test if +/// the reference value is an empty string. +#define assert_str_contains(test_expr, ref_value) \ +do { \ + tuktest_str_helper2("assert_str_contains", test_expr, ref_value); \ + if (strstr(v_test_, v_ref_) == NULL) \ + assert_fail("assert_str_contains: '%s' evaluated to '%s' " \ + "which doesn't contain '%s'", \ + #test_expr, v_test_, v_ref_); \ +} while (0) + + +/// Fails the test if the test expression evaluates to a string that +/// contains the reference value as a substring. Also fails the test if +/// the reference value is an empty string. +#define assert_str_doesnt_contain(test_expr, ref_value) \ +do { \ + tuktest_str_helper2("assert_str_doesnt_contain", \ + test_expr, ref_value); \ + if (strstr(v_test_, v_ref_) != NULL) \ + assert_fail("assert_str_doesnt_contain: " \ + "'%s' evaluated to '%s' which contains '%s'", \ + #test_expr, v_test_, v_ref_); \ +} while (0) + + +/// Fails the test if the first array_size elements of the test array +/// don't equal to correct_array. +/// +/// NOTE: This avoids %zu for portability to very old systems that still +/// can compile C99 code. +#define assert_array_eq(test_array, correct_array, array_size) \ +do { \ + for (size_t i_ = 0; i_ < (array_size); ++i_) \ + if ((test_array)[i_] != (correct_array)[i_]) \ + assert_fail("assert_array_eq: " \ + "%s[%" TUKTEST_PRIu "] != "\ + "%s[%" TUKTEST_PRIu "] " \ + "but should be equal", \ + #test_array, (tuktest_uint)i_, \ + #correct_array, (tuktest_uint)i_); \ +} while (0) + +#endif |