254 lines
8.5 KiB
C
254 lines
8.5 KiB
C
/*
|
|
* koan.h — minimalistic unit-testing library
|
|
* Author: NukeBird, 2025
|
|
* License: public domain / MIT — as you wish
|
|
*/
|
|
#ifndef KOAN_H
|
|
#define KOAN_H
|
|
#include <inttypes.h>
|
|
#include <math.h>
|
|
#include <setjmp.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#ifdef _WIN32
|
|
#include <io.h>
|
|
#define koan_isatty(_f) _isatty(_fileno(_f))
|
|
#else
|
|
#include <unistd.h>
|
|
#define koan_isatty(_f) isatty(fileno(_f))
|
|
#endif
|
|
|
|
struct koan_test
|
|
{
|
|
const char* name;
|
|
void (*func)(jmp_buf);
|
|
struct koan_test* next;
|
|
};
|
|
|
|
static struct koan_test* koan_tests = NULL;
|
|
static struct koan_test** koan_tail = &koan_tests;
|
|
|
|
static void koan_add_test(const char* name, void (*func)(jmp_buf))
|
|
{
|
|
struct koan_test* test = malloc(sizeof *test);
|
|
if (!test) {
|
|
fprintf(stderr, "koan: out of memory for test registration\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
test->name = name;
|
|
test->func = func;
|
|
test->next = NULL;
|
|
*koan_tail = test;
|
|
koan_tail = &test->next;
|
|
}
|
|
|
|
static char* koan_fail_msg = NULL;
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Assertion macros */
|
|
/* ------------------------------------------------------------------ */
|
|
#define KOAN_FAIL(reason) \
|
|
do { \
|
|
size_t _sz = strlen(reason) + strlen(__FILE__) + 128; \
|
|
char* _buf = malloc(_sz); \
|
|
if (_buf) { \
|
|
snprintf(_buf, _sz, "%s\n at %s:%d", reason, __FILE__, __LINE__); \
|
|
} else { \
|
|
_buf = "koan: malloc failed while formatting error message"; \
|
|
} \
|
|
koan_fail_msg = _buf; \
|
|
longjmp(koan_jmp_buf, 1); \
|
|
} while (0)
|
|
|
|
#define ASSERT_TRUE(cond) \
|
|
do { if (!(cond)) { char _r[256]; snprintf(_r, sizeof(_r), "ASSERT_TRUE failed: (%s) is false", #cond); KOAN_FAIL(_r); } } while (0)
|
|
|
|
#define ASSERT_FALSE(cond) \
|
|
do { if (cond) { char _r[256]; snprintf(_r, sizeof(_r), "ASSERT_FALSE failed: (%s) is true", #cond); KOAN_FAIL(_r); } } while (0)
|
|
|
|
#define ASSERT_INT_EQ(expected, actual) \
|
|
do { \
|
|
int64_t _e = (int64_t)(expected); \
|
|
int64_t _a = (int64_t)(actual); \
|
|
if (_e != _a) { \
|
|
char _r[256]; \
|
|
snprintf(_r, sizeof(_r), "ASSERT_INT_EQ failed: expected %" PRId64 " but got %" PRId64, _e, _a); \
|
|
KOAN_FAIL(_r); \
|
|
} \
|
|
} while (0)
|
|
|
|
#define ASSERT_UINT_EQ(expected, actual) \
|
|
do { \
|
|
uint64_t _e = (uint64_t)(expected); \
|
|
uint64_t _a = (uint64_t)(actual); \
|
|
if (_e != _a) { \
|
|
char _r[256]; \
|
|
snprintf(_r, sizeof(_r), "ASSERT_UINT_EQ failed: expected %" PRIu64 " but got %" PRIu64, _e, _a); \
|
|
KOAN_FAIL(_r); \
|
|
} \
|
|
} while (0)
|
|
|
|
#define ASSERT_DOUBLE_EQ(expected, actual, eps) \
|
|
do { \
|
|
double _e = (double)(expected); \
|
|
double _a = (double)(actual); \
|
|
double _eps = (double)(eps); \
|
|
if (fabs(_e - _a) > _eps) { \
|
|
char _r[512]; \
|
|
snprintf(_r, sizeof(_r), "ASSERT_DOUBLE_EQ failed: expected %g but got %g (diff %g > eps %g)", \
|
|
_e, _a, fabs(_e - _a), _eps); \
|
|
KOAN_FAIL(_r); \
|
|
} \
|
|
} while (0)
|
|
|
|
#define ASSERT_STR_EQ(expected, actual) \
|
|
do { \
|
|
const char* _e = (expected); \
|
|
const char* _a = (actual); \
|
|
if ((_e == NULL || _a == NULL) ? (_e != _a) : strcmp(_e, _a)) { \
|
|
char _r[512]; \
|
|
snprintf(_r, sizeof(_r), "ASSERT_STR_EQ failed: expected \"%s\" but got \"%s\"", \
|
|
_e ? _e : "(null)", _a ? _a : "(null)"); \
|
|
KOAN_FAIL(_r); \
|
|
} \
|
|
} while (0)
|
|
|
|
#define ASSERT_PTR_EQ(expected, actual) \
|
|
do { \
|
|
const void* _e = (const void*)(expected); \
|
|
const void* _a = (const void*)(actual); \
|
|
if (_e != _a) { \
|
|
char _r[256]; \
|
|
snprintf(_r, sizeof(_r), "ASSERT_PTR_EQ failed: expected %p but got %p", _e, _a); \
|
|
KOAN_FAIL(_r); \
|
|
} \
|
|
} while (0)
|
|
|
|
#define ASSERT_NULL(ptr) ASSERT_PTR_EQ(NULL, (ptr))
|
|
#define ASSERT_NOT_NULL(ptr) ASSERT_TRUE((ptr) != NULL)
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Koan definition */
|
|
/* ------------------------------------------------------------------ */
|
|
#define KOAN(name) \
|
|
static void koan_test_##name(jmp_buf); \
|
|
__attribute__((constructor)) static void koan_register_##name(void) \
|
|
{ koan_add_test(#name, koan_test_##name); } \
|
|
static void koan_test_##name(jmp_buf koan_jmp_buf)
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Time formatting */
|
|
/* ------------------------------------------------------------------ */
|
|
static void koan_format_time(double secs, char* buf, size_t bufsz)
|
|
{
|
|
double ns = secs * 1e9;
|
|
if (ns < 1e3) snprintf(buf, bufsz, "%.0f ns", ns);
|
|
else if (ns < 1e6) snprintf(buf, bufsz, "%.2f µs", ns / 1e3);
|
|
else if (ns < 1e9) snprintf(buf, bufsz, "%.2f ms", ns / 1e6);
|
|
else snprintf(buf, bufsz, "%.3f s", secs);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Progress bar (final only) */
|
|
/* ------------------------------------------------------------------ */
|
|
static void koan_print_bar(int done, int total, int passed, size_t failed, double elapsed)
|
|
{
|
|
const int w = 40;
|
|
int pos = (int)((float)done * w / total);
|
|
printf("[");
|
|
for (int i = 0; i < w; ++i) putchar(i < pos ? '=' : (i == pos ? '>' : '-'));
|
|
printf("] %3d%% %d/%d passed:%d failed:%zu %.3fs\n",
|
|
(int)((float)done * 100 / total), done, total, passed, failed, elapsed);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Run all koans — simple sequential mode only */
|
|
/* ------------------------------------------------------------------ */
|
|
struct koan_failure { const char* name; const char* msg; };
|
|
|
|
int koan_run_all(void)
|
|
{
|
|
printf("\n");
|
|
int total = 0;
|
|
for (struct koan_test* t = koan_tests; t; t = t->next) total++;
|
|
|
|
if (total == 0) {
|
|
printf("koan: no koans — no enlightenment.\n");
|
|
return 0;
|
|
}
|
|
|
|
int colorful = koan_isatty(stdout);
|
|
const char* G = colorful ? "\033[32m" : "";
|
|
const char* R = colorful ? "\033[31m" : "";
|
|
const char* Y = colorful ? "\033[33m" : "";
|
|
const char* N = colorful ? "\033[0m" : "";
|
|
|
|
const char* OK = colorful ? "PASS " : "[PASS] ";
|
|
const char* FAIL = colorful ? "FAIL " : "[FAIL] ";
|
|
|
|
printf("%sEnlightening %d %s...%s\n\n", Y, total, total == 1 ? "koan" : "koans", N);
|
|
|
|
struct koan_failure* failures = NULL;
|
|
size_t fail_cap = 0, fail_cnt = 0;
|
|
int passed = 0;
|
|
clock_t start = clock();
|
|
|
|
for (struct koan_test* t = koan_tests; t; t = t->next) {
|
|
koan_fail_msg = NULL;
|
|
clock_t ts = clock();
|
|
jmp_buf env;
|
|
int result = setjmp(env);
|
|
if (result == 0) {
|
|
t->func(env);
|
|
double sec = (double)(clock() - ts) / CLOCKS_PER_SEC;
|
|
char tbuf[32];
|
|
koan_format_time(sec, tbuf, sizeof tbuf);
|
|
printf(" %s%s%s (%s)%s\n", G, OK, t->name, tbuf, N);
|
|
passed++;
|
|
} else {
|
|
double sec = (double)(clock() - ts) / CLOCKS_PER_SEC;
|
|
char tbuf[32];
|
|
koan_format_time(sec, tbuf, sizeof tbuf);
|
|
printf(" %s%s%s (%s)%s\n", R, FAIL, t->name, tbuf, N);
|
|
|
|
if (fail_cnt == fail_cap) {
|
|
fail_cap = fail_cap ? fail_cap * 2 : 16;
|
|
failures = realloc(failures, fail_cap * sizeof *failures);
|
|
if (!failures) exit(1);
|
|
}
|
|
failures[fail_cnt].name = t->name;
|
|
failures[fail_cnt].msg = koan_fail_msg ? koan_fail_msg : "unknown failure";
|
|
fail_cnt++;
|
|
}
|
|
}
|
|
|
|
double total_time = (double)(clock() - start) / CLOCKS_PER_SEC;
|
|
koan_print_bar(total, total, passed, fail_cnt, total_time);
|
|
printf("\n");
|
|
|
|
if (fail_cnt == 0) {
|
|
printf("%sAll %d %s achieved enlightenment in %.3f s!%s\n", G, total,
|
|
total == 1 ? "koan" : "koans", total_time, N);
|
|
free(failures);
|
|
return 0;
|
|
} else {
|
|
printf("%s%zu / %d %s did not achieve enlightenment (%.3f s)%s\n\n", R,
|
|
fail_cnt, total, fail_cnt == 1 ? "koan" : "koans", total_time, N);
|
|
printf("%s--- Failed Koans ---%s\n\n", R, N);
|
|
for (size_t i = 0; i < fail_cnt; ++i) {
|
|
printf(" %sKoan \"%s\" failed%s\n", R, failures[i].name, N);
|
|
printf(" %s\n\n", failures[i].msg);
|
|
free((void*)failures[i].msg);
|
|
}
|
|
free(failures);
|
|
printf("%sEnlightenment not achieved.%s\n", R, N);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
#endif /* KOAN_H */
|