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