detailed tests

This commit is contained in:
2026-08-26 21:50:20 +02:00
parent fe3f152492
commit 3fcb3dbc05
7 changed files with 3448 additions and 44 deletions
+29
View File
@@ -3,13 +3,42 @@ project(tests LANGUAGES C)
enable_testing() 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 add_executable(test_vector
tests/test_vector.c tests/test_vector.c
tests/vector_altcap.c
) )
add_executable(test_set add_executable(test_set
tests/test_set.c 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_vector COMMAND test_vector)
add_test(NAME test_set COMMAND test_set) add_test(NAME test_set COMMAND test_set)
+10 -1
View File
@@ -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 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 ## Legal Mumbo Jumbo
Licensed under the MIT License (because GPL doesn't make sense for this (sorry Stallman)) Licensed under the MIT License (because GPL doesn't make sense for this (sorry Stallman))
+320
View File
@@ -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
+63
View File
@@ -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;
}
+1876 -21
View File
File diff suppressed because it is too large Load Diff
+1089 -24
View File
File diff suppressed because it is too large Load Diff
+63
View File
@@ -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;
}