Files
2026-08-26 21:50:20 +02:00

321 lines
14 KiB
C

/*
dtest.h - A tiny test harness for the DLibC test suites.
There is no external test framework here on purpose, as the library itself is
header-only and dependency-free and the tests should be too. A suite is an array
of named cases, each a void(void) function that runs assertions through the
CHECK_ and REQUIRE_ macros below, handed to dtest_main() from main().
Assertions come in two strengths:
- CHECK_* records the failure and keeps going, so one case reports every
problem it finds rather than only the first.
- REQUIRE_* records the failure and returns from the case, for the cases where
continuing is meaningless (a create() that returned NULL, say).
Every assertion prints a line to stdout, so ctest -V shows exactly what was
verified. Failures also go to stderr with file:line. The process exit code is
what ctest actually judges: 0 if every case passed, 1 otherwise.
@attention The counters below are file-scope statics, so a translation unit that
includes this header keeps its own tallies. Only the TU that calls dtest_main()
should run assertions. Helper TUs (the alternate-capacity probes) must expose
plain functions and let the main TU do the checking.
*/
#ifndef DTEST_H
#define DTEST_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef void (*dtest_fn_t)(void); // Function pointer type for a test case
typedef struct {
const char* name; // Name of the case, as shown in the output
dtest_fn_t fn; // The case itself
} dtest_case_t;
/*
@brief Builds a dtest_case_t entry from a case function, using the function's own name as the case name.
@param fn The case function. Must be a void(void).
*/
#define DTEST_CASE(fn) { #fn, fn }
static int dtest_checks_run = 0; // Assertions evaluated in the current case
static int dtest_checks_failed = 0; // Assertions failed in the current case
static int dtest_total_checks = 0; // Assertions evaluated across the whole suite
static int dtest_total_failed = 0; // Assertions failed across the whole suite
static int dtest_cases_failed = 0; // Cases with at least one failed assertion
/*
@brief Records the outcome of a single assertion and prints it.
@param ok Non-zero if the assertion held.
@param label A short description of what was being asserted.
@param detail The observed value, already formatted, or NULL if there is nothing useful to show.
@param file The source file the assertion sits in.
@param line The line the assertion sits on.
@attention This is an internal helper. Use the CHECK_ and REQUIRE_ macros instead.
*/
static inline void dtest_record(int ok, const char* label, const char* detail, const char* file, int line) {
++dtest_checks_run;
if (ok) {
printf("[ check ] %-58s ok", label);
if (detail) {
printf(" (%s)", detail);
}
printf("\n");
return;
}
++dtest_checks_failed;
printf("[ check ] %-58s FAILED\n", label);
fprintf(stderr, "%s:%d: %s", file, line, label);
if (detail) {
fprintf(stderr, " -- %s", detail);
}
fprintf(stderr, "\n");
}
/*
@brief Asserts that a condition holds, then keeps going either way.
@param cond The condition to evaluate.
@param label A short description of what is being asserted.
*/
#define CHECK_TRUE(cond, label) \
dtest_record((cond) ? 1 : 0, (label), NULL, __FILE__, __LINE__)
/*
@brief Asserts that a condition holds, and returns from the case if it does not.
@param cond The condition to evaluate.
@param label A short description of what is being asserted.
@attention Only usable inside a case function, as it expands to a return statement.
*/
#define REQUIRE_TRUE(cond, label) \
do { \
int dtest_ok_ = (cond) ? 1 : 0; \
dtest_record(dtest_ok_, (label), NULL, __FILE__, __LINE__); \
if (!dtest_ok_) { \
return; \
} \
} while (0)
/*
@brief Asserts that an int expression equals an expected value. Used for the 0/1/-1 return codes.
@param actual The expression to evaluate.
@param expected The value it should have.
@param label A short description of what is being asserted.
*/
#define CHECK_EQ_INT(actual, expected, label) \
do { \
int dtest_a_ = (int)(actual); \
int dtest_e_ = (int)(expected); \
char dtest_buf_[96]; \
snprintf(dtest_buf_, sizeof(dtest_buf_), "got %d, want %d", dtest_a_, dtest_e_); \
dtest_record(dtest_a_ == dtest_e_, (label), dtest_buf_, __FILE__, __LINE__); \
} while (0)
/*
@brief Asserts that a size_t expression equals an expected value. Used for sizes, capacities and SET_NPOS.
@param actual The expression to evaluate.
@param expected The value it should have.
@param label A short description of what is being asserted.
*/
#define CHECK_EQ_SIZE(actual, expected, label) \
do { \
size_t dtest_a_ = (size_t)(actual); \
size_t dtest_e_ = (size_t)(expected); \
char dtest_buf_[96]; \
snprintf(dtest_buf_, sizeof(dtest_buf_), "got %zu, want %zu", dtest_a_, dtest_e_); \
dtest_record(dtest_a_ == dtest_e_, (label), dtest_buf_, __FILE__, __LINE__); \
} while (0)
/*
@brief Asserts that a pointer is NULL.
@param ptr The pointer to evaluate.
@param label A short description of what is being asserted.
*/
#define CHECK_PTR_NULL(ptr, label) \
do { \
int dtest_ok_ = ((ptr) == NULL); \
dtest_record(dtest_ok_, (label), dtest_ok_ ? NULL : "expected NULL", \
__FILE__, __LINE__); \
} while (0)
/*
@brief Asserts that a pointer is not NULL, then keeps going either way.
@param ptr The pointer to evaluate.
@param label A short description of what is being asserted.
*/
#define CHECK_PTR_NOT_NULL(ptr, label) \
do { \
int dtest_ok_ = ((ptr) != NULL); \
dtest_record(dtest_ok_, (label), dtest_ok_ ? NULL : "expected non-NULL", \
__FILE__, __LINE__); \
} while (0)
/*
@brief Asserts that a pointer is not NULL, and returns from the case if it is.
@param ptr The pointer to evaluate.
@param label A short description of what is being asserted.
@attention Only usable inside a case function, as it expands to a return statement.
*/
#define REQUIRE_PTR_NOT_NULL(ptr, label) \
do { \
int dtest_ok_ = ((ptr) != NULL); \
dtest_record(dtest_ok_, (label), dtest_ok_ ? NULL : "expected non-NULL", \
__FILE__, __LINE__); \
if (!dtest_ok_) { \
return; \
} \
} while (0)
/*
@brief Asserts that two pointers are the same address. Used for identity checks, i.e. that an operation did not reallocate.
@param a The first pointer.
@param b The second pointer.
@param label A short description of what is being asserted.
*/
#define CHECK_PTR_EQ(a, b, label) \
do { \
int dtest_ok_ = ((const void*)(a) == (const void*)(b)); \
dtest_record(dtest_ok_, (label), dtest_ok_ ? NULL : "expected same address", \
__FILE__, __LINE__); \
} while (0)
/*
@brief Asserts that two pointers are different addresses.
@param a The first pointer.
@param b The second pointer.
@param label A short description of what is being asserted.
*/
#define CHECK_PTR_NE(a, b, label) \
do { \
int dtest_ok_ = ((const void*)(a) != (const void*)(b)); \
dtest_record(dtest_ok_, (label), dtest_ok_ ? NULL : "expected different addresses", \
__FILE__, __LINE__); \
} while (0)
/*
@brief Asserts that two buffers hold the same bytes.
@param a The first buffer.
@param b The second buffer.
@param n The number of bytes to compare.
@param label A short description of what is being asserted.
*/
#define CHECK_MEM_EQ(a, b, n, label) \
do { \
int dtest_ok_ = (memcmp((a), (b), (n)) == 0); \
dtest_record(dtest_ok_, (label), dtest_ok_ ? NULL : "expected identical bytes", \
__FILE__, __LINE__); \
} while (0)
/*
@brief Asserts that a string equals an expected value.
@param actual The string to evaluate. May be NULL, which always fails.
@param expected The string it should equal.
@param label A short description of what is being asserted.
*/
#define CHECK_EQ_STR(actual, expected, label) \
do { \
const char* dtest_a_ = (actual); \
const char* dtest_e_ = (expected); \
char dtest_buf_[160]; \
snprintf(dtest_buf_, sizeof(dtest_buf_), "got \"%s\", want \"%s\"", \
dtest_a_ ? dtest_a_ : "(null)", dtest_e_ ? dtest_e_ : "(null)"); \
dtest_record(dtest_a_ && dtest_e_ && strcmp(dtest_a_, dtest_e_) == 0, \
(label), dtest_buf_, __FILE__, __LINE__); \
} while (0)
static int dtest_destructor_calls = 0; // Number of times a dtest destructor has run since the last reset
/*
@brief Resets the destructor call counter. Call at the top of any case that asserts a call count.
*/
static inline void dtest_reset_destructor_calls(void) {
dtest_destructor_calls = 0;
}
/*
@brief An element destructor that only counts its invocations and frees nothing.
@param element A pointer to the element's slot inside the container's data array.
@attention Use this on containers of plain values, where the point of the case is the call count rather than the cleanup.
*/
static inline void dtest_count_destructor(void* element) {
(void)element;
++dtest_destructor_calls;
}
/*
@brief An element destructor for a container of char*, which frees the string and counts the call.
@param element A pointer to the element's slot, i.e. a char** .
@attention This frees what the element owns and never the slot itself, exactly as the container headers require.
*/
static inline void dtest_string_destructor(void* element) {
++dtest_destructor_calls;
free(*(char**)element);
}
/*
@brief Duplicates a string onto the heap.
@param str The string to duplicate.
@return A newly allocated copy, or NULL on allocation failure.
@attention Hand-rolled rather than strdup(), which is POSIX rather than ISO C and is not visible under -std=c11 with extensions off.
*/
static inline char* dtest_dup(const char* str) {
size_t len = strlen(str) + 1;
char* copy = (char*)malloc(len);
if (!copy) {
return NULL;
}
memcpy(copy, str, len);
return copy;
}
/*
@brief Runs every case in a suite and reports the results.
@param cases The array of cases to run.
@param count The number of cases in the array.
@param suite The suite's name, used in the output banner.
@return 0 if every assertion in every case passed, 1 otherwise. Return this straight out of main() so ctest sees it.
*/
static inline int dtest_main(const dtest_case_t* cases, size_t count, const char* suite) {
printf("[==========] %s: running %zu cases\n", suite, count);
for (size_t i = 0; i < count; ++i) {
dtest_checks_run = 0;
dtest_checks_failed = 0;
dtest_reset_destructor_calls();
printf("[ RUN ] %s\n", cases[i].name);
cases[i].fn();
dtest_total_checks += dtest_checks_run;
dtest_total_failed += dtest_checks_failed;
if (dtest_checks_failed > 0) {
++dtest_cases_failed;
printf("[ FAILED ] %s (%d of %d checks failed)\n",
cases[i].name, dtest_checks_failed, dtest_checks_run);
fprintf(stderr, "FAILED: %s\n", cases[i].name);
} else if (dtest_checks_run == 0) {
// A case that asserts nothing is almost certainly a mistake, so it
// is treated as a failure rather than quietly counted as a pass
++dtest_cases_failed;
printf("[ FAILED ] %s (ran no checks)\n", cases[i].name);
fprintf(stderr, "FAILED: %s ran no checks\n", cases[i].name);
} else {
printf("[ OK ] %s (%d checks)\n", cases[i].name, dtest_checks_run);
}
}
printf("[==========] %s: %zu cases, %d checks, %d failed checks in %d cases\n",
suite, count, dtest_total_checks, dtest_total_failed, dtest_cases_failed);
return dtest_cases_failed == 0 ? 0 : 1;
}
#endif // DTEST_H