Add unit tests + fix allocator

This commit is contained in:
NukeBird 2025-12-01 19:39:11 +03:00
parent 6c6f7eb902
commit d86948b6a9
4 changed files with 495 additions and 16 deletions

254
axl_koan.h Normal file
View file

@ -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 <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 */

View file

@ -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);
}

212
axl_memory_test.c Normal file
View file

@ -0,0 +1,212 @@
/*
* axl_memory_tests.c Comprehensive unit tests for axl_memory.h
*/
#include "axl_memory.h"
#include "axl_koan.h"
#include <string.h>
/* ------------------------------------------------------------------ */
/* 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();
}

View file

@ -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