From d86948b6a9dd376a2fbaae0b9d0bb8c973b3ddb5 Mon Sep 17 00:00:00 2001 From: NukeBird Date: Mon, 1 Dec 2025 19:39:11 +0300 Subject: [PATCH] Add unit tests + fix allocator --- axl_koan.h | 254 ++++++++++++++++++++++++++++++++++++++++++++++ axl_memory.c | 20 ++-- axl_memory_test.c | 212 ++++++++++++++++++++++++++++++++++++++ makefile | 25 +++-- 4 files changed, 495 insertions(+), 16 deletions(-) create mode 100644 axl_koan.h create mode 100644 axl_memory_test.c diff --git a/axl_koan.h b/axl_koan.h new file mode 100644 index 0000000..740e737 --- /dev/null +++ b/axl_koan.h @@ -0,0 +1,254 @@ +/* + * koan.h — minimalistic unit-testing library + * Author: NukeBird, 2025 + * License: public domain / MIT — as you wish + */ +#ifndef KOAN_H +#define KOAN_H +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#define koan_isatty(_f) _isatty(_fileno(_f)) +#else +#include +#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 */ diff --git a/axl_memory.c b/axl_memory.c index 346109c..71b3a17 100644 --- a/axl_memory.c +++ b/axl_memory.c @@ -45,7 +45,7 @@ static mb_header* axl_id_to_mb(mb_handle id) return (mb_header*)&memory[id]; } -static mb_handle axl_ptr_to_id(mb_header* ptr) +static mb_handle axl_mb_to_id(mb_header* ptr) { return (mb_handle)((u8*)ptr - memory); } @@ -126,15 +126,15 @@ static b8 axl_split_mb(mb_header* block, u32 size) new_block->size = block->size - size - MB_HEADER_SIZE; new_block->is_free = true; new_block->next = block->next; - new_block->prev = axl_ptr_to_id(block); + new_block->prev = axl_mb_to_id(block); - if(new_block->next) + if(new_block->next != AXL_INVALID_MB_HANDLE) { - axl_id_to_mb(new_block->next)->prev = axl_ptr_to_id(new_block); + axl_id_to_mb(new_block->next)->prev = axl_mb_to_id(new_block); } block->size = size; - block->next = axl_ptr_to_id(new_block); + block->next = axl_mb_to_id(new_block); return true; } @@ -256,9 +256,9 @@ void axl_free(void* ptr) { block->size += next_block->size + MB_HEADER_SIZE; block->next = next_block->next; - if(block->next) + if(block->next != AXL_INVALID_MB_HANDLE) { - axl_id_to_mb(block->next)->prev = axl_ptr_to_id(block); + axl_id_to_mb(block->next)->prev = axl_mb_to_id(block); } } @@ -266,12 +266,12 @@ void axl_free(void* ptr) { prev_block->size += block->size + MB_HEADER_SIZE; prev_block->next = block->next; - if(prev_block->next) + if(prev_block->next != AXL_INVALID_MB_HANDLE) { - axl_id_to_mb(prev_block->next)->prev = axl_ptr_to_id(prev_block); + axl_id_to_mb(prev_block->next)->prev = axl_mb_to_id(prev_block); } free_block = prev_block; } - nomad_handle = axl_ptr_to_id(free_block); + nomad_handle = axl_mb_to_id(free_block); } diff --git a/axl_memory_test.c b/axl_memory_test.c new file mode 100644 index 0000000..f83eaa0 --- /dev/null +++ b/axl_memory_test.c @@ -0,0 +1,212 @@ +/* + * axl_memory_tests.c — Comprehensive unit tests for axl_memory.h + */ + +#include "axl_memory.h" +#include "axl_koan.h" +#include + +/* ------------------------------------------------------------------ */ +/* Basic allocation & initialization */ +/* ------------------------------------------------------------------ */ + +KOAN(init_called) +{ + void* p = axl_malloc(1); + ASSERT_NOT_NULL(p); + axl_free(p); +} + +KOAN(malloc_returns_different_pointers) +{ + void* a = axl_malloc(16); + void* b = axl_malloc(32); + ASSERT_NOT_NULL(a); + ASSERT_NOT_NULL(b); + ASSERT_TRUE(a != b); + axl_free(a); + axl_free(b); +} + +KOAN(malloc_exhaustion_returns_null) +{ + void* p = axl_malloc(AXL_HEAP_SIZE + 4096); /* definitely too big */ + ASSERT_NULL(p); +} + +/* ------------------------------------------------------------------ */ +/* Realloc behavior (strictly per C standard) */ +/* ------------------------------------------------------------------ */ + +KOAN(realloc_null_acts_like_malloc) +{ + void* p = axl_realloc(NULL, 64); + ASSERT_NOT_NULL(p); + axl_memset(p, 0x55, 64); + axl_free(p); +} + +KOAN(realloc_zero_size_frees_and_returns_null_or_valid) +{ + void* p = axl_malloc(100); + ASSERT_NOT_NULL(p); + void* q = axl_realloc(p, 0); + if (q != NULL) axl_free(q); + /* Original p is freed regardless */ +} + +KOAN(realloc_enlarge_preserves_content) +{ + void* p = axl_malloc(32); + axl_memset(p, 0xCD, 32); + void* q = axl_realloc(p, 128); + ASSERT_NOT_NULL(q); + for (int i = 0; i < 32; i++) + ASSERT_INT_EQ(0xCD, ((u8*)q)[i]); + axl_free(q); +} + +KOAN(realloc_shrink_preserves_prefix) +{ + void* p = axl_malloc(256); + axl_memset(p, 0xAB, 256); + void* q = axl_realloc(p, 64); + ASSERT_NOT_NULL(q); + for (int i = 0; i < 64; i++) + ASSERT_INT_EQ(0xAB, ((u8*)q)[i]); + axl_free(q); +} + +KOAN(realloc_same_size_returns_same_or_equivalent) +{ + void* p = axl_malloc(100); + + p = axl_realloc(p, 100); + ASSERT_NOT_NULL(p); + /* May return same or different pointer */ + axl_free(p); +} + +/* ------------------------------------------------------------------ */ +/* Memory operations */ +/* ------------------------------------------------------------------ */ + +KOAN(memset_fills_correctly) +{ + void* p = axl_malloc(64); + axl_memset(p, 0x55, 64); + for (int i = 0; i < 64; i++) + ASSERT_INT_EQ(0x55, ((u8*)p)[i]); + axl_free(p); +} + +KOAN(memset_zero_bytes_is_nop) +{ + void* p = axl_malloc(16); + axl_memset(p, 0xFF, 16); + axl_memset(p, 0x00, 0); /* should not change anything */ + for (int i = 0; i < 16; i++) + ASSERT_INT_EQ(0xFF, ((u8*)p)[i]); + axl_free(p); +} + +KOAN(memcpy_copies_exact_bytes) +{ + u8 src[32]; + for (int i = 0; i < 32; i++) src[i] = (u8)i; + void* dst = axl_malloc(32); + axl_memcpy(dst, src, 32); + ASSERT_INT_EQ(0, memcmp(dst, src, 32)); + axl_free(dst); +} + +KOAN(memcpy_zero_bytes_is_nop) +{ + u8 data[16] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16}; + void* p = axl_malloc(16); + axl_memcpy(p, data, 16); + axl_memcpy(p, "junk", 0); /* must not modify */ + ASSERT_INT_EQ(0, memcmp(p, data, 16)); + axl_free(p); +} + +KOAN(memcmp_returns_correct_sign) +{ + u8 a[] = {0, 0, 0, 0}; + u8 b[] = {0, 0, 0, 1}; + u8 c[] = {0, 0, 1, 0}; + + ASSERT_INT_EQ(0, axl_memcmp(a, a, 4)); + ASSERT_TRUE(axl_memcmp(a, b, 4) < 0); + ASSERT_TRUE(axl_memcmp(b, a, 4) > 0); + ASSERT_TRUE(axl_memcmp(a, c, 3) < 0 ); + ASSERT_TRUE(axl_memcmp(a, b, 2) == 0); + ASSERT_TRUE(axl_memcmp(a, c, 2) == 0); + ASSERT_TRUE(axl_memcmp(c, b, 2) == 0); + ASSERT_TRUE(axl_memcmp(a, c, 4) < 0); +} + +KOAN(memcmp_zero_length_returns_zero) +{ + u8 x = 0xFF, y = 0x00; + ASSERT_INT_EQ(0, axl_memcmp(&x, &y, 0)); +} + +/* ------------------------------------------------------------------ */ +/* Double-free and use-after-free detection (indirect) */ +/* ------------------------------------------------------------------ */ + +KOAN(double_free_does_not_crash) +{ + void* p = axl_malloc(64); + ASSERT_NOT_NULL(p); + axl_free(p); + axl_free(p); /* should be ignored or handled safely */ +} + +/* ------------------------------------------------------------------ */ +/* Large and boundary allocations */ +/* ------------------------------------------------------------------ */ + +KOAN(large_allocation_near_heap_limit) +{ + /* Allocate almost entire heap */ + void* p = axl_malloc(AXL_HEAP_SIZE - 1024); + ASSERT_NOT_NULL(p); /* may pass if overhead is small */ + if (p) axl_free(p); +} + +KOAN(allocation_of_max_size_fails) +{ + void* p = axl_malloc(AXL_HEAP_SIZE); + ASSERT_NULL(p); +} + +/* ------------------------------------------------------------------ */ +/* Stress: many small allocations */ +/* ------------------------------------------------------------------ */ + +KOAN(many_small_allocations) +{ + #define N 1000 + void* ptrs[N]; + for (int i = 0; i < N; i++) { + ptrs[i] = axl_malloc(16); + ASSERT_NOT_NULL(ptrs[i]); + *(u32*)ptrs[i] = 0xDEADBEEF; + } + for (int i = 0; i < N; i++) { + ASSERT_INT_EQ(0xDEADBEEF, *(u32*)ptrs[i]); + axl_free(ptrs[i]); + } +} + +/* ------------------------------------------------------------------ */ +/* Main */ +/* ------------------------------------------------------------------ */ + +int main(void) +{ + axl_init(); + return koan_run_all(); +} diff --git a/makefile b/makefile index 52d5224..3f3081e 100644 --- a/makefile +++ b/makefile @@ -1,16 +1,29 @@ CC = clang -CFLAGS = -Wall -Wextra -Werror -pedantic -std=c11 -nostdlib -static -Oz -ffreestanding +AXL_SOURCES = $(filter-out main.c %_test.c, $(wildcard *.c)) +TEST_EXES = $(patsubst %.c,%.exe,$(wildcard *_test.c)) +TEST_CFLAGS = -Wall -Wextra -Werror -pedantic -std=c11 -static -Oz +CFLAGS = $(TEST_CFLAGS) -nostdlib -ffreestanding LDFLAGS = -Wl,/SUBSYSTEM:CONSOLE,/ENTRY:_start -fuse-ld=lld LIBS = -lkernel32 -SOURCES = $(wildcard *.c) +SOURCES = $(AXL_SOURCES) main.c TARGET = prog.exe -all: clean $(TARGET) +all: clean test $(TARGET) -$(TARGET): $(SOURCES) +%_test.exe: %_test.c + $(CC) $(TEST_CFLAGS) $(AXL_SOURCES) $< -o $@ + +test: $(TEST_EXES) + for %%i in ($(TEST_EXES)) do ( \ + echo Running %%i & \ + %%i \ + ) + +$(TARGET): $(SOURCES) $(CC) $(CFLAGS) $(SOURCES) -o $(TARGET) $(LDFLAGS) $(LIBS) + clean: rm -f $(TARGET) + rm -f $(TEST_EXES) -.PHONY: all clean - +.PHONY: all clean test