detailed tests
This commit is contained in:
@@ -3,13 +3,42 @@ project(tests LANGUAGES C)
|
||||
|
||||
enable_testing()
|
||||
|
||||
# The headers are documented as C23-first but expected to work on older standards.
|
||||
# Pinned here so that the claim is actually exercised; configure with
|
||||
# -DDLIBC_C_STANDARD=23 to build them the way the README describes.
|
||||
set(DLIBC_C_STANDARD 11 CACHE STRING "C standard the tests are compiled against")
|
||||
|
||||
# The library's stated goal is memory safety, so the take, aliasing and destructor
|
||||
# tests are worth a lot more when the allocator is watching. Off by default, as it
|
||||
# slows the tests down and needs a compiler that ships the sanitizer runtimes.
|
||||
option(DLIBC_SANITIZE "Build the tests with AddressSanitizer and UndefinedBehaviorSanitizer" OFF)
|
||||
|
||||
add_executable(test_vector
|
||||
tests/test_vector.c
|
||||
tests/vector_altcap.c
|
||||
)
|
||||
|
||||
add_executable(test_set
|
||||
tests/test_set.c
|
||||
tests/set_altcap.c
|
||||
)
|
||||
|
||||
foreach(dlibc_target test_vector test_set)
|
||||
set_target_properties(${dlibc_target} PROPERTIES
|
||||
C_STANDARD ${DLIBC_C_STANDARD}
|
||||
C_STANDARD_REQUIRED ON
|
||||
C_EXTENSIONS OFF
|
||||
)
|
||||
|
||||
if(NOT MSVC)
|
||||
target_compile_options(${dlibc_target} PRIVATE -Wall -Wextra -Wpedantic -Werror)
|
||||
|
||||
if(DLIBC_SANITIZE)
|
||||
target_compile_options(${dlibc_target} PRIVATE -g -fno-omit-frame-pointer -fsanitize=address,undefined)
|
||||
target_link_options(${dlibc_target} PRIVATE -fsanitize=address,undefined)
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
add_test(NAME test_vector COMMAND test_vector)
|
||||
add_test(NAME test_set COMMAND test_set)
|
||||
@@ -20,7 +20,16 @@ If you want to contribute, please make a pull request and I'll review it. If you
|
||||
|
||||
If you change an existing type, please run **make** in *build/* directory and **ctest** to ensure compatibility hasn't been broken.
|
||||
|
||||
If it's new, please write a **test** for it in *tests/test_<name>.c* so others can see the guidelines on how the type is used.
|
||||
Since the whole point is memory safety, please also run the suite with the sanitizers before opening a pull request:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build-asan -DDLIBC_SANITIZE=ON && cmake --build build-asan
|
||||
ctest --test-dir build-asan --output-on-failure
|
||||
```
|
||||
|
||||
Use `ctest -V` to see every individual assertion, and `-DDLIBC_C_STANDARD=23` to build against C23 instead of the default C11.
|
||||
|
||||
If it's new, please write a **test** for it in *tests/test_<name>.c* so others can see the guidelines on how the type is used. The tests use a tiny in-tree harness, *tests/dtest.h*, which is just a list of named cases and some `CHECK_` macros - there is no external framework to install.
|
||||
|
||||
## Legal Mumbo Jumbo
|
||||
Licensed under the MIT License (because GPL doesn't make sense for this (sorry Stallman))
|
||||
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
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
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
set_altcap.c - Probes set.h with a non-default DLIBC_SET_INITIAL_CAPACITY.
|
||||
|
||||
The macro is read at include time, so exercising it needs a translation unit of
|
||||
its own. Every function in set.h is static inline, so this TU gets its own
|
||||
copies built around the smaller constant while the set_t layout stays identical
|
||||
to the one in test_set.c. That means a set created here is a perfectly ordinary
|
||||
set to the rest of the program.
|
||||
|
||||
@attention This TU deliberately does not include dtest.h. The harness counters
|
||||
are file-scope statics, so an assertion made here would be tallied separately
|
||||
and never reach the summary. The probes below only gather observations, and
|
||||
test_set.c does the asserting.
|
||||
*/
|
||||
|
||||
#define DLIBC_SET_INITIAL_CAPACITY 1
|
||||
#include "../set.h"
|
||||
|
||||
/*
|
||||
@brief Records the set's capacity after each of count successive insert() calls of distinct values, starting from a freshly created set.
|
||||
@param out A buffer of at least count size_t values, filled with the capacity observed after each insert.
|
||||
@param count The number of inserts to perform.
|
||||
@return 0 on success, -1 if out is NULL, count is 0, or any insert fails.
|
||||
@attention With DLIBC_SET_INITIAL_CAPACITY at 1, the expected sequence is 1, 2, 4, 4, 8, 8, 8, 8, ...
|
||||
*/
|
||||
int set_altcap_growth_sequence(size_t* out, size_t count) {
|
||||
if (!out || count == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
set_t* set = set_create(sizeof(int));
|
||||
if (!set) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
int value = (int)i;
|
||||
if (set_insert(set, &value) != 0) {
|
||||
set_destroy(&set);
|
||||
return -1;
|
||||
}
|
||||
|
||||
out[i] = set_capacity(set);
|
||||
}
|
||||
|
||||
set_destroy(&set);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
@brief Reports the capacity of a freshly created set in this translation unit.
|
||||
@return The initial capacity, or 0 if creation failed.
|
||||
*/
|
||||
size_t set_altcap_initial_capacity(void) {
|
||||
set_t* set = set_create(sizeof(int));
|
||||
if (!set) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t capacity = set_capacity(set);
|
||||
set_destroy(&set);
|
||||
return capacity;
|
||||
}
|
||||
+1878
-23
File diff suppressed because it is too large
Load Diff
+1089
-24
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
vector_altcap.c - Probes vector.h with a non-default DLIBC_VECTOR_INITIAL_CAPACITY.
|
||||
|
||||
The macro is read at include time, so exercising it needs a translation unit of
|
||||
its own. Every function in vector.h is static inline, so this TU gets its own
|
||||
copies built around the smaller constant while the vector_t layout stays
|
||||
identical to the one in test_vector.c. That means a vector created here is a
|
||||
perfectly ordinary vector to the rest of the program.
|
||||
|
||||
@attention This TU deliberately does not include dtest.h. The harness counters
|
||||
are file-scope statics, so an assertion made here would be tallied separately
|
||||
and never reach the summary. The probes below only gather observations, and
|
||||
test_vector.c does the asserting.
|
||||
*/
|
||||
|
||||
#define DLIBC_VECTOR_INITIAL_CAPACITY 1
|
||||
#include "../vector.h"
|
||||
|
||||
/*
|
||||
@brief Records the vector's capacity after each of count successive push_back() calls, starting from a freshly created vector.
|
||||
@param out A buffer of at least count size_t values, filled with the capacity observed after each push.
|
||||
@param count The number of pushes to perform.
|
||||
@return 0 on success, -1 if out is NULL, count is 0, or any allocation fails.
|
||||
@attention With DLIBC_VECTOR_INITIAL_CAPACITY at 1, the expected sequence is 1, 2, 4, 4, 8, 8, 8, 8, ...
|
||||
*/
|
||||
int vector_altcap_growth_sequence(size_t* out, size_t count) {
|
||||
if (!out || count == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
vector_t* vec = vector_create(sizeof(int));
|
||||
if (!vec) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
int value = (int)i;
|
||||
if (vector_push_back(vec, &value) != 0) {
|
||||
vector_destroy(&vec);
|
||||
return -1;
|
||||
}
|
||||
|
||||
out[i] = vector_capacity(vec);
|
||||
}
|
||||
|
||||
vector_destroy(&vec);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
@brief Reports the capacity of a freshly created vector in this translation unit.
|
||||
@return The initial capacity, or 0 if creation failed.
|
||||
*/
|
||||
size_t vector_altcap_initial_capacity(void) {
|
||||
vector_t* vec = vector_create(sizeof(int));
|
||||
if (!vec) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t capacity = vector_capacity(vec);
|
||||
vector_destroy(&vec);
|
||||
return capacity;
|
||||
}
|
||||
Reference in New Issue
Block a user