// SPDX-License-Identifier: 0BSD

///////////////////////////////////////////////////////////////////////////////
//
/// \file       tuktest.h
/// \brief      Helper macros for writing simple test programs
/// \version    2024-02-14
///
/// Some inspiration was taken from Seatest by Keith Nicholas and
/// from STest which is a fork of Seatest by Jia Tan.
///
/// 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 make 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
//
///////////////////////////////////////////////////////////////////////////////

#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) || defined(__clang__)
#	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) || defined(__clang__)
#	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
