321 lines
14 KiB
C
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
|