From bf19b9938508f70011395f4dc283ca758ec6a8d3 Mon Sep 17 00:00:00 2001 From: Ethan Werner Date: Sat, 19 Sep 2026 06:09:54 +0000 Subject: llm tooling --- tests/test_ann.c | 284 +++++++++++++++ tests/test_bin.c | 273 +++++++++++++++ tests/test_buffer.c | 134 +++++++ tests/test_clio.c | 884 +++++++++++++++++++++++++++++++++++++++++++++++ tests/test_direct.c | 157 +++++++++ tests/test_dotagents.c | 524 ++++++++++++++++++++++++++++ tests/test_escape.c | 113 ++++++ tests/test_flat.c | 132 +++++++ tests/test_frontmatter.c | 236 +++++++++++++ tests/test_genetic.c | 158 +++++++++ tests/test_interpolate.c | 112 ++++++ tests/test_pool.c | 150 ++++++++ tests/test_pushdown.c | 442 ++++++++++++++++++++++++ tests/test_stack.c | 234 +++++++++++++ tests/test_ufo.c | 143 ++++++++ tests/test_uri.c | 300 ++++++++++++++++ 16 files changed, 4276 insertions(+) create mode 100644 tests/test_ann.c create mode 100644 tests/test_bin.c create mode 100644 tests/test_buffer.c create mode 100644 tests/test_clio.c create mode 100644 tests/test_direct.c create mode 100644 tests/test_dotagents.c create mode 100644 tests/test_escape.c create mode 100644 tests/test_flat.c create mode 100644 tests/test_frontmatter.c create mode 100644 tests/test_genetic.c create mode 100644 tests/test_interpolate.c create mode 100644 tests/test_pool.c create mode 100644 tests/test_pushdown.c create mode 100644 tests/test_stack.c create mode 100644 tests/test_ufo.c create mode 100644 tests/test_uri.c (limited to 'tests') diff --git a/tests/test_ann.c b/tests/test_ann.c new file mode 100644 index 0000000..5192e36 --- /dev/null +++ b/tests/test_ann.c @@ -0,0 +1,284 @@ +// Tests for ann.h - neural network init, forward/backward pass, activations +#include +#define ANN_IMPLEMENTATION +#include "../include/ann.h" + +#include +#include + +#define EPS 1e-4 + +#define ASSERT(cond, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s (test_ann.c:%d)\n", msg, __LINE__); \ + failures++; \ + } \ + } while (0) + +#define ASSERT_FLOAT_EQ(a, b, msg) \ + do { \ + if (fabs((a) - (b)) > EPS) { \ + fprintf(stderr, "FAIL: %s (got %f, expected %f) (test_ann.c:%d)\n", \ + msg, (double)(a), (double)(b), __LINE__); \ + failures++; \ + } \ + } while (0) + +static int failures = 0; + +void test_init() { + ann_t ann = {0}; + u64 layers[] = {2, 3, 1}; + ASSERT(ann_init(&ann, 3, layers), "ann_init succeeds"); + + ASSERT(ann.layer_n == 3, "layer count"); + ASSERT(ann.buffer != NULL, "buffer allocated"); + ASSERT(ann.layer_neuron_n != NULL, "layer_neuron_n set"); + ASSERT(ann.layer_neuron_n[0] == 2, "input layer size"); + ASSERT(ann.layer_neuron_n[1] == 3, "hidden layer size"); + ASSERT(ann.layer_neuron_n[2] == 1, "output layer size"); + ASSERT(ann.neuron != NULL, "neuron pointer set"); + ASSERT(ann.weight != NULL, "weight pointer set"); + + ann_free(&ann); +} + +void test_free() { + ann_t ann = {0}; + u64 layers[] = {2, 4, 2}; + ASSERT(ann_init(&ann, 3, layers), "ann_init succeeds"); + ann_free(&ann); + ASSERT(ann.buffer == NULL, "buffer freed"); + + ann_free(&ann); +} + +void test_copy() { + ann_t src = {0}; + u64 layers[] = {2, 3, 1}; + ASSERT(ann_init(&src, 3, layers), "ann_init succeeds"); + ann_random(&src); + + ann_t dst = {0}; + ann_copy(&dst, &src); + + ASSERT(dst.layer_n == src.layer_n, "copy layer_n"); + ASSERT(dst.neuron_n == src.neuron_n, "copy neuron_n"); + ASSERT(dst.weight_n == src.weight_n, "copy weight_n"); + ASSERT(dst.buffer != src.buffer, "copy has separate buffer"); + + ann_free(&src); + ann_free(&dst); +} + +void test_random() { + srand(42); + + ann_t ann = {0}; + u64 layers[] = {2, 2, 1}; + ASSERT(ann_init(&ann, 3, layers), "ann_init succeeds"); + ann_random(&ann); + + int has_nonzero = 0; + for (u64 i = 0; i < ann.weight_n; i++) { + if (ann.weight[i] != 0) { + has_nonzero = 1; + break; + } + } + ASSERT(has_nonzero, "random sets non-zero weights"); + + ann_free(&ann); +} + +void test_forward() { + ann_t ann = {0}; + u64 layers[] = {2, 2, 1}; + ASSERT(ann_init(&ann, 3, layers), "ann_init succeeds"); + + f64 *w = ann.weight; + for (u64 i = 0; i < ann.weight_n; i++) { + w[i] = 0; + } + + f64 input[2] = {1, 1}; + f64 output[1] = {0}; + ann_propagation_forward(&ann, input, output); + + ASSERT(output[0] >= 0 && output[0] <= 1, "forward output in sigmoid range"); + + ann_free(&ann); +} + +void test_backward() { + ann_t ann = {0}; + u64 layers[] = {2, 2, 1}; + ASSERT(ann_init(&ann, 3, layers), "ann_init succeeds"); + ann_random(&ann); + + f64 input[2] = {0.5, 0.3}; + f64 output[1] = {0}; + f64 target[1] = {1}; + + f64 weight_before = ann.weight[0]; + + ann_propagation_forward(&ann, input, output); + + f64 output_before = output[0]; + + ann_propagation_backward(&ann, input, output, target, 0.1); + + f64 weight_after = ann.weight[0]; + ASSERT(weight_before != weight_after, "backward changes weights"); + + ann_propagation_forward(&ann, input, output); + ASSERT(output[0] != output_before, "output changed after weight update"); + + ann_free(&ann); +} + +void test_error() { + f64 err = ann_error_total((f64[]){0.9}, (f64[]){1.0}, 1); + ASSERT(err > 0, "error is positive"); + + f64 err_zero = ann_error_total((f64[]){1.0}, (f64[]){1.0}, 1); + ASSERT_FLOAT_EQ(err_zero, 0, "error zero when output == target"); + + f64 err1 = ann_error(0.5, 0.5); + ASSERT_FLOAT_EQ(err1, 0, "single error zero"); + + f64 err2 = ann_error(1.0, 0.0); + ASSERT_FLOAT_EQ(err2, 0.5, "single error 0.5"); +} + +void test_activations() { + ann_t ann = {0}; + u64 layers[] = {2, 2, 1}; + ASSERT(ann_init(&ann, 3, layers), "ann_init succeeds"); + + ann_set_activation(&ann, SIGMOID, SIGMOID); + f64 input[2] = {0.5, 0.5}; + f64 output[1] = {0}; + ann_propagation_forward(&ann, input, output); + ASSERT(output[0] >= 0 && output[0] <= 1, "sigmoid output range"); + + ann_set_activation(&ann, RELU, RELU); + ann_propagation_forward(&ann, input, output); + ASSERT(output[0] >= 0, "relu output non-negative"); + + ann_set_activation(&ann, TANH, TANH); + ann_propagation_forward(&ann, input, output); + ASSERT(output[0] >= -1 && output[0] <= 1, "tanh output range"); + + ann_set_activation(&ann, IDENTITY, IDENTITY); + ann_propagation_forward(&ann, input, output); + + ann_set_activation(&ann, BINARY, BINARY); + ann_propagation_forward(&ann, input, output); + ASSERT(output[0] == 0 || output[0] == 1, "binary output is 0 or 1"); + + ann_set_activation(&ann, ELU, ELU); + ann_propagation_forward(&ann, input, output); + ASSERT(isfinite(output[0]), "elu output is finite"); + + ann_set_activation(&ann, LRELU, LRELU); + ann_propagation_forward(&ann, input, output); + ASSERT(isfinite(output[0]), "lrelu output is finite"); + + ann_free(&ann); +} + +void test_train_numeric() { + srand(42); + + ann_t ann = {0}; + u64 layers[] = {2, 4, 1}; + ASSERT(ann_init(&ann, 3, layers), "ann_init succeeds"); + ann_random(&ann); + + f64 input[2] = {0, 1}; + f64 target[1] = {1}; + f64 output_before[1] = {0}; + ann_propagation_forward(&ann, input, output_before); + + ann_train_numeric(&ann, input, target, 1, 0.01); + + f64 output_after[1] = {0}; + ann_propagation_forward(&ann, input, output_after); + ASSERT(output_before[0] != output_after[0], "numeric training changes output"); + ASSERT(isfinite(output_after[0]), "output is finite after training"); + + ann_free(&ann); +} + +void test_xor_training() { + srand(42); + + ann_t ann = {0}; + u64 layers[] = {2, 4, 1}; + ASSERT(ann_init(&ann, 3, layers), "ann_init succeeds"); + ann_set_activation(&ann, SIGMOID, SIGMOID); + ann_random(&ann); + + f64 inputs[4][2] = {{0, 0}, {0, 1}, {1, 0}, {1, 1}}; + f64 targets[4] = {0, 1, 1, 0}; + f64 output[1] = {0}; + + for (int iter = 0; iter < 5000; iter++) { + for (int i = 0; i < 4; i++) { + ann_propagation_forward(&ann, inputs[i], output); + ann_propagation_backward(&ann, inputs[i], output, &targets[i], 0.5); + } + } + + f64 outputs[4]; + for (int i = 0; i < 4; i++) { + ann_propagation_forward(&ann, inputs[i], &outputs[i]); + } + + ASSERT(outputs[0] < 0.5, "XOR(0,0) < 0.5"); + ASSERT(outputs[1] > 0.5, "XOR(0,1) > 0.5"); + ASSERT(outputs[2] > 0.5, "XOR(1,0) > 0.5"); + ASSERT(outputs[3] < 0.5, "XOR(1,1) < 0.5"); + + ann_free(&ann); +} + +void test_multi_layer() { + ann_t ann = {0}; + u64 layers[] = {3, 4, 5, 2}; + ASSERT(ann_init(&ann, 4, layers), "ann_init succeeds"); + ann_random(&ann); + + ASSERT(ann.layer_neuron_n[0] == 3, "input 3"); + ASSERT(ann.layer_neuron_n[1] == 4, "hidden1 4"); + ASSERT(ann.layer_neuron_n[2] == 5, "hidden2 5"); + ASSERT(ann.layer_neuron_n[3] == 2, "output 2"); + + f64 input[3] = {0.1, 0.2, 0.3}; + f64 output[2] = {0, 0}; + ann_propagation_forward(&ann, input, output); + + ASSERT(isfinite(output[0]), "multi-layer output[0] is finite"); + ASSERT(isfinite(output[1]), "multi-layer output[1] is finite"); + + ann_free(&ann); +} + +int main() { + test_init(); + test_free(); + test_copy(); + test_random(); + test_forward(); + test_backward(); + test_error(); + test_activations(); + test_train_numeric(); + test_xor_training(); + test_multi_layer(); + + printf("test_ann: %d failures\n", failures); + return failures; +} diff --git a/tests/test_bin.c b/tests/test_bin.c new file mode 100644 index 0000000..f12f336 --- /dev/null +++ b/tests/test_bin.c @@ -0,0 +1,273 @@ +// Tests for bin.h - flat file block storage operations +#include +typedef int32_t bin_key_t; + +#define BIN_IMPLEMENTATION +#include "../include/bin.h" + +#include +#include +#include +#include + +#define ASSERT(cond, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s (test_bin.c:%d)\n", msg, __LINE__); \ + failures++; \ + } \ + } while (0) + +static int failures = 0; + +struct record { + bin_key_t key; + char _pad[4]; + double value; +}; + +#define REC(k, v) (&((struct record){.key = (k), .value = (v)})) + +void cleanup(char const *path) { + remove(path); +} + +void test_init_and_close() { + FILE *f = bin_init("/tmp/test_bin_init.bin", sizeof(struct record)); + ASSERT(f != NULL, "bin_init returns non-NULL"); + if (!f) return; + ASSERT(bin_length(f) == 0, "initial length is 0"); + ASSERT(bin_block_size(f) == sizeof(struct record), "block size"); + bin_close(f); + cleanup("/tmp/test_bin_init.bin"); +} + +void test_append_and_read() { + FILE *f = bin_init("/tmp/test_bin_append.bin", sizeof(struct record)); + ASSERT(f != NULL, "bin_init returns non-NULL"); + if (!f) return; + + for (int i = 0; i < 10; i++) { + bin_append(f, REC(i, i * 2.5), 1); + } + + ASSERT(bin_length(f) == 10, "length after 10 appends"); + + struct record buf[10]; + ASSERT(bin_read(f, 0, buf, 10) == 10, "read 10 records"); + for (int i = 0; i < 10; i++) { + ASSERT(buf[i].key == i, "read key"); + ASSERT(fabs(buf[i].value - (double)(i * 2.5)) < 1e-9, "read value"); + } + + bin_close(f); + cleanup("/tmp/test_bin_append.bin"); +} + +void test_write() { + FILE *f = bin_init("/tmp/test_bin_write.bin", sizeof(struct record)); + ASSERT(f != NULL, "bin_init returns non-NULL"); + if (!f) return; + + bin_append(f, REC(0, 0), 1); + bin_append(f, REC(1, 1), 1); + + bin_write(f, 0, REC(99, 99), 1); + + struct record r; + ASSERT(bin_read(f, 0, &r, 1) == 1, "read 1 record"); + ASSERT(r.key == 99, "write overwrites"); + ASSERT(r.value == 99, "write overwrites value"); + + bin_close(f); + cleanup("/tmp/test_bin_write.bin"); +} + +void test_insert() { + FILE *f = bin_init("/tmp/test_bin_insert.bin", sizeof(struct record)); + ASSERT(f != NULL, "bin_init returns non-NULL"); + if (!f) return; + + bin_append(f, REC(0, 0), 1); + bin_append(f, REC(2, 2), 1); + + bin_insert(f, 1, REC(1, 1), 1); + + ASSERT(bin_length(f) == 3, "length after insert"); + + struct record buf[3]; + ASSERT(bin_read(f, 0, buf, 3) == 3, "read 3 records"); + ASSERT(buf[0].key == 0, "insert at 1 preserves [0]"); + ASSERT(buf[1].key == 1, "insert at 1 sets [1]"); + ASSERT(buf[2].key == 2, "insert at 1 preserves [2]"); + + bin_close(f); + cleanup("/tmp/test_bin_insert.bin"); +} + +void test_insert_at_end() { + FILE *f = bin_init("/tmp/test_bin_insert_end.bin", sizeof(struct record)); + ASSERT(f != NULL, "bin_init returns non-NULL"); + if (!f) return; + + bin_append(f, REC(0, 0), 1); + + bin_insert(f, 1, REC(1, 1), 1); + + ASSERT(bin_length(f) == 2, "length after insert at end"); + + struct record buf[2]; + ASSERT(bin_read(f, 0, buf, 2) == 2, "read 2 records"); + ASSERT(buf[0].key == 0, "insert at end preserves [0]"); + ASSERT(buf[1].key == 1, "insert at end sets [1]"); + + bin_close(f); + cleanup("/tmp/test_bin_insert_end.bin"); +} + +void test_insert_at_beginning() { + FILE *f = bin_init("/tmp/test_bin_insert_begin.bin", sizeof(struct record)); + ASSERT(f != NULL, "bin_init returns non-NULL"); + if (!f) return; + + bin_append(f, REC(1, 1), 1); + + bin_insert(f, 0, REC(0, 0), 1); + + struct record buf[2]; + ASSERT(bin_read(f, 0, buf, 2) == 2, "read 2 records"); + ASSERT(buf[0].key == 0, "insert at 0 sets [0]"); + ASSERT(buf[1].key == 1, "insert at 0 preserves [1]"); + + bin_close(f); + cleanup("/tmp/test_bin_insert_begin.bin"); +} + +void test_open() { + FILE *f = bin_init("/tmp/test_bin_open.bin", sizeof(struct record)); + ASSERT(f != NULL, "bin_init returns non-NULL"); + if (!f) { + cleanup("/tmp/test_bin_open.bin"); + return; + } + bin_append(f, REC(42, 42), 1); + bin_close(f); + + f = bin_open("/tmp/test_bin_open.bin"); + ASSERT(f != NULL, "bin_open returns non-NULL"); + if (!f) { + cleanup("/tmp/test_bin_open.bin"); + return; + } + ASSERT(bin_length(f) == 1, "opened file has correct length"); + + struct record r; + ASSERT(bin_read(f, 0, &r, 1) == 1, "read 1 record"); + ASSERT(r.key == 42, "read from opened file"); + + bin_close(f); + cleanup("/tmp/test_bin_open.bin"); +} + +void test_search() { + FILE *f = bin_init("/tmp/test_bin_search.bin", sizeof(struct record)); + ASSERT(f != NULL, "bin_init returns non-NULL"); + if (!f) return; + + for (int i = 0; i < 20; i++) { + bin_append(f, REC(i * 5, 0), 1); + } + + i64 found = bin_search(f, 25); + ASSERT(found == 5, "search finds existing key"); + + i64 not_found = bin_search(f, 26); + ASSERT(not_found < 0, "search returns negative for missing key"); + + i64 idx = bin_fuzzy_index(not_found); + ASSERT(idx == 6, "fuzzy_index returns insertion point"); + + bin_close(f); + cleanup("/tmp/test_bin_search.bin"); +} + +void test_search_empty() { + FILE *f = bin_init("/tmp/test_bin_search_empty.bin", sizeof(struct record)); + ASSERT(f != NULL, "bin_init returns non-NULL"); + if (!f) return; + + i64 result = bin_search(f, 1); + ASSERT(result == -1, "search empty returns -1"); + + bin_close(f); + cleanup("/tmp/test_bin_search_empty.bin"); +} + +void test_append_multiple() { + FILE *f = bin_init("/tmp/test_bin_multi.bin", sizeof(struct record)); + ASSERT(f != NULL, "bin_init returns non-NULL"); + if (!f) return; + + struct record batch[] = { + {.key = 10, .value = 10}, + {.key = 20, .value = 20}, + {.key = 30, .value = 30}, + }; + + bin_append(f, batch, 3); + ASSERT(bin_length(f) == 3, "batch append length"); + + struct record buf[3]; + ASSERT(bin_read(f, 0, buf, 3) == 3, "read 3 records"); + ASSERT(buf[0].key == 10, "batch[0]"); + ASSERT(buf[1].key == 20, "batch[1]"); + ASSERT(buf[2].key == 30, "batch[2]"); + + bin_close(f); + cleanup("/tmp/test_bin_multi.bin"); +} + +void test_fuzzy_index_positive() { + u64 idx = bin_fuzzy_index(5); + ASSERT(idx == 5, "fuzzy_index passthrough positive"); +} + +void test_ok_variants() { + FILE *f = bin_init("/tmp/test_bin_ok.bin", sizeof(struct record)); + ASSERT(f != NULL, "bin_init returns non-NULL"); + if (!f) return; + + ASSERT(bin_append_ok(f, REC(1, 1), 1), "append_ok returns 1"); + ASSERT(bin_append_ok(f, NULL, 1) == 0, "append_ok NULL data returns 0"); + ASSERT(bin_append_ok(NULL, REC(2, 2), 1) == 0, "append_ok NULL file returns 0"); + + ASSERT(bin_write_ok(f, 0, REC(99, 99), 1), "write_ok returns 1"); + ASSERT(bin_write_ok(f, -1, REC(0, 0), 1) == 0, "write_ok negative index returns 0"); + ASSERT(bin_write_ok(f, 99, REC(0, 0), 1) == 0, "write_ok out-of-range returns 0"); + + ASSERT(bin_insert_ok(f, 0, REC(0, 0), 1), "insert_ok returns 1"); + ASSERT(bin_insert_ok(f, -1, REC(0, 0), 1) == 0, "insert_ok negative index returns 0"); + + ASSERT(bin_close(f), "close returns 1"); + ASSERT(bin_close(NULL) == 0, "close NULL returns 0"); + + cleanup("/tmp/test_bin_ok.bin"); +} + +int main() { + test_init_and_close(); + test_append_and_read(); + test_write(); + test_insert(); + test_insert_at_end(); + test_insert_at_beginning(); + test_open(); + test_search(); + test_search_empty(); + test_append_multiple(); + test_fuzzy_index_positive(); + test_ok_variants(); + + printf("test_bin: %d failures\n", failures); + return failures; +} diff --git a/tests/test_buffer.c b/tests/test_buffer.c new file mode 100644 index 0000000..f6e61dc --- /dev/null +++ b/tests/test_buffer.c @@ -0,0 +1,134 @@ +// Tests for buffer.h - dynamic buffer allocation and resizing +#define BUFFER_IMPLEMENTATION +#include "../include/buffer.h" + +#include +#include +#include + +#define ASSERT(cond, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s (test_buffer.c:%d)\n", msg, __LINE__); \ + failures++; \ + } \ + } while (0) + +static int failures = 0; + +void test_malloc_and_free() { + void *buf = buffer_malloc(10, sizeof(int)); + ASSERT(buf != NULL, "buffer_malloc returns non-NULL"); + buffer_free(buf); +} + +void test_capacity() { + void *buf = buffer_malloc(10, sizeof(int)); + ASSERT(buffer_capacity(buf) == 10, "initial capacity"); + buffer_free(buf); +} + +void test_count() { + void *buf = buffer_malloc(10, sizeof(int)); + ASSERT(buffer_count(buf) == 0, "initial count is 0"); + buffer_free(buf); +} + +void test_resize_grow() { + void *buf = buffer_malloc(10, sizeof(int)); + ASSERT(buf != NULL, "buffer_malloc returns non-NULL"); + if (!buf) return; + buffer_meta_t *meta = (buffer_meta_t *)_buffer_to_meta(buf); + meta->i = 9; + + void *new_buf = buffer_resize(buf); + meta = (buffer_meta_t *)_buffer_to_meta(new_buf); + ASSERT(meta->n == 20, "capacity doubles when ratio > 0.8"); + + buffer_free(new_buf); +} + +void test_resize_shrink() { + void *buf = buffer_malloc(100, sizeof(int)); + ASSERT(buf != NULL, "buffer_malloc returns non-NULL"); + if (!buf) return; + buffer_meta_t *meta = (buffer_meta_t *)_buffer_to_meta(buf); + meta->i = 5; + + void *new_buf = buffer_resize(buf); + meta = (buffer_meta_t *)_buffer_to_meta(new_buf); + ASSERT(meta->n == 50, "capacity halves when ratio < 0.2 and n > 32"); + + buffer_free(new_buf); +} + +void test_resize_no_change() { + void *buf = buffer_malloc(32, sizeof(int)); + ASSERT(buf != NULL, "buffer_malloc returns non-NULL"); + if (!buf) return; + buffer_meta_t *meta = (buffer_meta_t *)_buffer_to_meta(buf); + meta->i = 10; + + void *new_buf = buffer_resize(buf); + ASSERT(new_buf == buf, "no realloc when capacity unchanged"); + + buffer_free(buf); +} + +void test_resize_small_no_shrink() { + void *buf = buffer_malloc(30, sizeof(int)); + ASSERT(buf != NULL, "buffer_malloc returns non-NULL"); + if (!buf) return; + buffer_meta_t *meta = (buffer_meta_t *)_buffer_to_meta(buf); + meta->i = 1; + + void *new_buf = buffer_resize(buf); + meta = (buffer_meta_t *)_buffer_to_meta(new_buf); + ASSERT(meta->n == 30, "does not shrink below 32"); + + buffer_free(new_buf); +} + +void test_data_integrity() { + int *buf = (int *)buffer_malloc(10, sizeof(int)); + for (int i = 0; i < 10; i++) { + buf[i] = i * 100; + } + + buffer_meta_t *meta = (buffer_meta_t *)_buffer_to_meta(buf); + meta->i = 9; + + int *new_buf = (int *)buffer_resize(buf); + for (int i = 0; i < 10; i++) { + ASSERT(new_buf[i] == i * 100, "data preserved after resize"); + } + + buffer_free(new_buf); +} + +void test_different_sizes() { + void *buf1 = buffer_malloc(5, sizeof(char)); + void *buf2 = buffer_malloc(5, sizeof(int)); + void *buf3 = buffer_malloc(5, sizeof(double)); + ASSERT(buffer_capacity(buf1) == 5, "char capacity"); + ASSERT(buffer_capacity(buf2) == 5, "int capacity"); + ASSERT(buffer_capacity(buf3) == 5, "double capacity"); + buffer_free(buf1); + buffer_free(buf2); + buffer_free(buf3); +} + +int main() { + test_malloc_and_free(); + test_capacity(); + test_count(); + test_resize_grow(); + test_resize_shrink(); + test_resize_no_change(); + test_resize_small_no_shrink(); + test_data_integrity(); + test_different_sizes(); + + printf("test_buffer: %d failures\n", failures); + return failures; +} diff --git a/tests/test_clio.c b/tests/test_clio.c new file mode 100644 index 0000000..535df1d --- /dev/null +++ b/tests/test_clio.c @@ -0,0 +1,884 @@ +/* test_clio.c — standalone test suite for clio.h (+ escape.h). + * Zero dependencies: single TU, no linked libraries (defines + * ESCAPE_IMPLEMENTATION to pull in the esc_* symbols). */ +#define ESCAPE_IMPLEMENTATION +#include "clio.h" + +#include +#include +#include +#include +#include + +static int failures = 0; +static int checks = 0; + +#define CHECK(cond, msg) \ + do { \ + checks++; \ + if (!(cond)) { \ + failures++; \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, (msg)); \ + } \ + } while (0) + +/* Capture stdout (fd 1) into a malloc'd buffer across the test body. */ +typedef struct cap { + int pfd[2]; + int saved; +} cap_t; + +/* dup2() returns the target fd (STDOUT_FILENO == 1) on success, -1 on + * failure — compare against -1, not 0. */ +static int cap_begin(cap_t *c) { + if (pipe(c->pfd) != 0) + return -1; + c->saved = dup(STDOUT_FILENO); + if (c->saved < 0) + return -1; + if (dup2(c->pfd[1], STDOUT_FILENO) == -1) { + close(c->pfd[0]); + close(c->pfd[1]); + close(c->saved); + return -1; + } + close(c->pfd[1]); + return 0; +} + +static char *cap_end(cap_t *c, size_t *len) { + fflush(stdout); + if (dup2(c->saved, STDOUT_FILENO) == -1) { + close(c->saved); + return NULL; + } + close(c->saved); + size_t cap = 256; + size_t n = 0; + char *buf = (char *) malloc(cap); + char tmp[1024]; + ssize_t r; + while ((r = read(c->pfd[0], tmp, sizeof tmp)) > 0) { + if (n + (size_t) r > cap) { + cap = (cap + (size_t) r) * 2; + buf = (char *) realloc(buf, cap); + } + memcpy(buf + n, tmp, (size_t) r); + n += (size_t) r; + } + close(c->pfd[0]); + buf[n] = '\0'; /* callers use both strlen() and len */ + *len = n; + return buf; +} + +static int contains(const char *hay, size_t len, const char *needle) { + size_t nl = strlen(needle); + if (nl > len) + return 0; + for (size_t i = 0; i + nl <= len; i++) + if (memcmp(hay + i, needle, nl) == 0) + return 1; + return 0; +} + +/* ------------------------------------------------------------------ */ +/* Text: UTF-8 + width */ +/* ------------------------------------------------------------------ */ + +static void test_utf8(void) { + u32 cp; + char out[8]; + CHECK(clio_utf8_next("a", &cp) == 1 && cp == 'a', "ascii decode"); + CHECK(clio_utf8_next("\xC3\xA9", &cp) == 2 && cp == 0xE9, "latin1 decode"); + CHECK(clio_utf8_next("\xE4\xB8\xAD", &cp) == 3 && cp == 0x4E2D, + "cjk decode"); + CHECK(clio_utf8_next("\xF0\x9F\x98\x80", &cp) == 4 && cp == 0x1F600, + "emoji decode"); + /* truncated sequence: consumes what is left */ + CHECK(clio_utf8_next("\xE4\xB8", &cp) == 2, "truncated decode"); + CHECK(clio_utf8_enc(0x4E2D, out) == 3 && memcmp(out, "\xE4\xB8\xAD", 3) == 0, + "cjk encode"); + CHECK(clio_utf8_enc(0x1F600, out) == 4, "emoji encode"); + CHECK(clio_utf8_enc(0xD800, out) == -1, "surrogate rejected"); + CHECK(clio_utf8_enc(0x110000, out) == -1, "out of range rejected"); + + CHECK(clio_text_width("abc") == 3, "ascii width"); + CHECK(clio_text_width("h\xC3\xA9llo") == 5, "latin width"); + CHECK(clio_text_width("\xE4\xB8\xAD\xE6\x96\x87") == 4, "cjk width"); + CHECK(clio_text_width("\xF0\x9F\x98\x80") == 2, "emoji width"); + CHECK(clio_cp_width('\n') == 0, "newline zero width"); + + const char *s = "ab\xE4\xB8\xAD" "c"; /* a b 中 c */ + CHECK(clio_char_start(s, 4) == 2, "char_start cjk"); + CHECK(clio_char_start(s, 2) == 2, "char_start at boundary"); + CHECK(clio_char_start(s, 1) == 1, "char_start ascii"); + CHECK(clio_char_end(s, 6, 2) == 5, "char_end cjk"); + CHECK(clio_char_end(s, 6, 5) == 6, "char_end last"); + + /* semantics verified against bash/readline Ctrl+W via pty: + * "foo bar baz"@12 -> 7, "foo bar"@8 -> 3, "a_b c"@5 -> 3 */ + CHECK(clio_word_back("foo bar baz", 12) == 7, "word_back word"); + CHECK(clio_word_back("foo bar", 8) == 3, "word_back across spaces"); + CHECK(clio_word_back("a_b c", 5) == 3, "word_back underscore"); +} + +/* ------------------------------------------------------------------ */ +/* Canvas */ +/* ------------------------------------------------------------------ */ + +static void test_buf(void) { + clio_buf b; + clio_style s = clio_style_default(); + s.fg = 248; + s.bg = 238; + s.attr = CLIO_BOLD | CLIO_REVERSE; + + CHECK(clio_buf_init(&b, 3, 8) == 0, "buf init"); + CHECK(b.rows == 3 && b.cols == 8, "buf dims"); + + /* default cell is blank + default style */ + clio_cell *c0 = b.cells; + CHECK(c0->ch == 0 && c0->fg == 0xFFFF && c0->bg == 0xFFFF && c0->attr == 0, + "default cell"); + + CHECK(clio_buf_put(&b, 0, 0, "hi", s, 0) == 2, "put ascii"); + CHECK(b.cells[0].ch == 'h' && b.cells[1].ch == 'i', "put stored"); + CHECK(b.cells[0].fg == 248 && b.cells[0].bg == 238, "put style"); + CHECK(b.cells[0].attr == (CLIO_BOLD | CLIO_REVERSE), "put attr"); + CHECK(b.cells[2].ch == 0, "put bounds"); + + /* clipping: no wrap */ + CHECK(clio_buf_put(&b, 0, 6, "abc", s, 0) == 2, "clip no wrap"); + CHECK(b.cells[7].ch == 'b', "clip stored edge"); + clio_buf_clear(&b); + + /* wrapping onto next row */ + CHECK(clio_buf_put(&b, 0, 6, "abcd", s, 1) == 4, "wrap put"); + CHECK(b.cells[7].ch == 'b', "wrap first row edge"); + CHECK(b.cells[8].ch == 'c' && b.cells[9].ch == 'd', "wrap second row"); + + clio_buf_clear(&b); + + /* newline moves to next row at col 0 */ + clio_buf_put(&b, 0, 0, "a\nb", s, 1); + CHECK(b.cells[0].ch == 'a' && b.cells[8].ch == 'b', "newline put"); + + clio_buf_clear(&b); + + /* wide char occupies two cells */ + clio_buf_put(&b, 0, 0, "\xE4\xB8\xAD\xE6\x96\x87", s, 1); + CHECK(b.cells[0].ch == 0x4E2D && b.cells[1].ch == 0, "wide first cell"); + CHECK(b.cells[2].ch == 0x6587 && b.cells[3].ch == 0, "wide second cell"); + + clio_buf_clear(&b); + + /* zero-width chars consume no cell */ + clio_buf_put(&b, 0, 0, "\xE2\x80\x8Bx", s, 1); /* ZWSP + x */ + CHECK(b.cells[0].ch == 'x', "zero width skips"); + + clio_buf_clear(&b); + + /* fill + clear region */ + clio_buf_fill_region(&b, 0, 0, 1, 3, '*', s); + CHECK(b.cells[0].ch == '*' && b.cells[3].ch == '*', "fill row 0"); + CHECK(b.cells[8].ch == '*' && b.cells[11].ch == '*', "fill row 1"); + CHECK(b.cells[4].ch == 0, "fill bounds"); + clio_buf_clear_region(&b, 0, 1, 1, 2); + CHECK(b.cells[0].ch == '*' && b.cells[1].ch == 0 && b.cells[2].ch == 0, + "clear region"); + CHECK(b.cells[10].ch == 0 && b.cells[11].ch == '*', "clear region row 1"); + clio_buf_clear(&b); + + /* box 2x4 in top-left */ + clio_buf_box(&b, 0, 0, 4, 2, '#', s); + CHECK(b.cells[0].ch == '#' && b.cells[3].ch == '#', "box top"); + CHECK(b.cells[8].ch == '#' && b.cells[11].ch == '#', "box bottom"); + clio_buf_clear(&b); + + /* scroll up: rows 0..2 shift, bottom becomes blank */ + clio_buf_put(&b, 0, 0, "aa", s, 0); + clio_buf_put(&b, 1, 0, "bb", s, 0); + clio_buf_put(&b, 2, 0, "cc", s, 0); + clio_buf_scroll_up(&b, 0, 0, 2, 1); + CHECK(b.cells[0].ch == 'b' && b.cells[8].ch == 'c', "scroll moved"); + CHECK(b.cells[16].ch == 0, "scroll bottom blank"); + + clio_buf_free(&b); + CHECK(b.cells == NULL, "buf free"); + + /* init with zero dims clamps to 1x1 */ + CHECK(clio_buf_init(&b, 0, 0) == 0 && b.rows == 1 && b.cols == 1, + "buf init clamps"); + clio_buf_free(&b); +} + +/* ------------------------------------------------------------------ */ +/* Key decoder */ +/* ------------------------------------------------------------------ */ + +static size_t decode(clio_keydec *d, const char *in, clio_key *k, size_t cap) { + clio_keydec_init(d); + return clio_keydec_feed(d, (const unsigned char *) in, strlen(in), k, cap); +} + +static void test_keydec(void) { + clio_keydec d; + clio_key k[16]; + size_t n; + + n = decode(&d, "\x1B[C", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_RIGHT, "csi right"); + n = decode(&d, "\x1B[D", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_LEFT, "csi left"); + n = decode(&d, "\x1B[A", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_UP, "csi up"); + n = decode(&d, "\x1B[B", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_DOWN, "csi down"); + n = decode(&d, "\x1B[H", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_HOME, "csi home"); + n = decode(&d, "\x1B[F", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_END, "csi end"); + n = decode(&d, "\x1B[Z", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_BACKTAB, "csi backtab"); + n = decode(&d, "\x1B[1~", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_HOME, "csi 1~ home"); + n = decode(&d, "\x1B[7~", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_HOME, "csi 7~ home"); + n = decode(&d, "\x1B[3~", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_DELETE, "csi 3~ delete"); + n = decode(&d, "\x1B[4~", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_END, "csi 4~ end"); + n = decode(&d, "\x1B[5~", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_PAGEUP, "csi 5~ pageup"); + n = decode(&d, "\x1B[6~", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_PAGEDOWN, "csi 6~ pagedown"); + + n = decode(&d, "\x1BOA", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_UP, "ss3 up"); + n = decode(&d, "\x1BOF", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_END, "ss3 end"); + + n = decode(&d, "\t\r\x7F\x08", k, 16); + CHECK(n == 4 && k[0].kind == CLIOK_TAB && k[1].kind == CLIOK_ENTER && + k[2].kind == CLIOK_BACKSPACE && k[3].kind == CLIOK_BACKSPACE, + "tab enter backspace del8"); + + n = decode(&d, "\x01\x03\x04\x05\x0B\x0C\x15\x17\x19\x1A", k, 16); + CHECK(n == 10 && k[0].kind == CLIOK_CTRL_A && k[1].kind == CLIOK_CTRL_C && + k[2].kind == CLIOK_CTRL_D && k[3].kind == CLIOK_CTRL_E && + k[4].kind == CLIOK_CTRL_K && k[5].kind == CLIOK_CTRL_L && + k[6].kind == CLIOK_CTRL_U && k[7].kind == CLIOK_CTRL_W && + k[8].kind == CLIOK_CTRL_Y && k[9].kind == CLIOK_CTRL_Z, + "ctrl letters"); + + n = decode(&d, "\x07", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_CTRL_G, "ctrl g"); + n = decode(&d, "\x1B", k, 16); + CHECK(n == 0, "bare esc not emitted yet"); + CHECK(clio_keydec_flush(&d, &k[0]) == 1 && k[0].kind == CLIOK_ESC, + "esc flush"); + + n = decode(&d, "h\xC3\xA9\xE4\xB8\xAD", k, 16); + CHECK(n == 3 && k[0].cp == 'h' && k[1].cp == 0xE9 && k[2].cp == 0x4E2D, + "utf8 keys"); + + /* kitty protocol */ + n = decode(&d, "\x1B[13u", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_ENTER, "kitty enter"); + n = decode(&d, "\x1B[127u", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_DELETE, "kitty delete"); + n = decode(&d, "\x1B[9u", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_TAB, "kitty tab"); + n = decode(&d, "\x1B[27u", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_ESC, "kitty esc"); + + /* bracketed paste: LF inside paste is a char; start/end emit no key */ + n = decode(&d, "\x1B[200~a\nb\x1B[201~", k, 16); + CHECK(n == 3 && k[0].cp == 'a' && k[1].cp == '\n' && k[2].cp == 'b', + "bracketed paste"); + + /* bare LF outside paste: dead key */ + n = decode(&d, "\n", k, 16); + CHECK(n == 0, "bare lf dead"); + + /* split feeds: escape sequence across chunk boundaries */ + clio_keydec_init(&d); + n = clio_keydec_feed(&d, (const unsigned char *) "\x1B", 1, k, 16); + CHECK(n == 0, "split esc"); + n = clio_keydec_feed(&d, (const unsigned char *) "[", 1, k, 16); + CHECK(n == 0, "split ["); + n = clio_keydec_feed(&d, (const unsigned char *) "C", 1, k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_RIGHT, "split C"); + + /* split UTF-8 across chunk boundaries */ + clio_keydec_init(&d); + n = clio_keydec_feed(&d, (const unsigned char *) "\xC3", 1, k, 16); + CHECK(n == 0, "split utf8 lead"); + n = clio_keydec_feed(&d, (const unsigned char *) "\xA9", 1, k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_CHAR && k[0].cp == 0xE9, "split utf8"); + + /* unknown CSI emits UNKNOWN so the app can react (e.g. beep) */ + n = decode(&d, "\x1B[15~", k, 16); + CHECK(n == 1 && k[0].kind == CLIOK_UNKNOWN, "unknown csi -> UNKNOWN"); + /* cursor position report swallowed */ + n = decode(&d, "\x1B[12;40R", k, 16); + CHECK(n == 0, "cpr swallowed"); +} + +/* ------------------------------------------------------------------ */ +/* Color policy */ +/* ------------------------------------------------------------------ */ + +static void setenv0(const char *k, const char *v) { + if (v) + setenv(k, v, 1); + else + unsetenv(k); +} + +static void test_color_policy(void) { + setenv0("CLICOLOR_FORCE", NULL); + setenv0("NO_COLOR", NULL); + setenv0("CI", NULL); + setenv0("TERM", NULL); + CHECK(clio_color_policy(1) == 1, "tty on"); + CHECK(clio_color_policy(0) == 0, "notty off"); + setenv0("NO_COLOR", "1"); + CHECK(clio_color_policy(1) == 0, "NO_COLOR off"); + setenv0("NO_COLOR", NULL); + setenv0("TERM", "dumb"); + CHECK(clio_color_policy(1) == 0, "TERM=dumb off"); + setenv0("TERM", NULL); + setenv0("CI", "true"); + CHECK(clio_color_policy(1) == 0, "CI off"); + setenv0("CI", NULL); + setenv0("CLICOLOR_FORCE", "1"); + CHECK(clio_color_policy(0) == 1, "force on, notty"); + setenv0("NO_COLOR", "1"); + CHECK(clio_color_policy(1) == 1, "force beats NO_COLOR"); + setenv0("CLICOLOR_FORCE", "0"); + CHECK(clio_color_policy(1) == 0, "force 0 off"); + setenv0("CLICOLOR_FORCE", NULL); +} + +/* ------------------------------------------------------------------ */ +/* Style emission */ +/* ------------------------------------------------------------------ */ + +static void test_fstyle(void) { + cap_t c; + clio_style s = clio_style_default(); + s.fg = 38; + s.bg = 48; + s.attr = CLIO_BOLD | CLIO_UNDERLINE; + + CHECK(cap_begin(&c) == 0, "cap begin 1"); + clio_fstyle(stdout, s); + size_t len; + char *out = cap_end(&c, &len); + CHECK(out != NULL && len > 0, "fstyle wrote"); + if (out) { + /* exact sequence: reset, bold, underline, fg, bg (in that order) */ + CHECK(contains(out, len, ESC_RESET) == 1, "fstyle reset"); + CHECK(contains(out, len, ESC_BOLD) == 1, "fstyle bold"); + CHECK(contains(out, len, ESC_UNDERLINE) == 1, "fstyle underline"); + CHECK(contains(out, len, ESC_COLOR_FG_256(38)) == 1, "fstyle fg256"); + CHECK(contains(out, len, ESC_COLOR_BG_256(48)) == 1, "fstyle bg256"); + CHECK(contains(out, len, "48;5;48m") == 1, "bg uses 48 not 38"); + CHECK(contains(out, len, ESC_ITALIC) == 0, "no italic"); + /* reset first, fg before bg */ + const char *rp = strstr(out, ESC_RESET); + const char *fp = strstr(out, ESC_COLOR_FG_256(38)); + const char *bp = strstr(out, ESC_COLOR_BG_256(48)); + CHECK(rp && fp && bp && rp < fp && fp < bp, "fstyle order"); + free(out); + } + + /* default style: only reset */ + CHECK(cap_begin(&c) == 0, "cap begin 2"); + clio_fstyle(stdout, clio_style_default()); + out = cap_end(&c, &len); + CHECK(out != NULL && strcmp(out, ESC_RESET) == 0, "default = reset only"); + free(out); +} + +/* ------------------------------------------------------------------ */ +/* Flush (non-tty plain + tty diff) */ +/* ------------------------------------------------------------------ */ + +static void test_flush_plain(void) { + clio_term t; + clio_term_init(&t); + clio_buf b; + clio_buf_init(&b, 2, 8); + clio_style s = clio_style_default(); + s.fg = 248; + clio_buf_put(&b, 0, 0, "hi", s, 0); + clio_buf_put(&b, 1, 0, "yo", s, 0); + cap_t c; + CHECK(cap_begin(&c) == 0, "cap begin"); + clio_flush(&t, &b); + size_t len; + char *out = cap_end(&c, &len); + CHECK(out != NULL, "cap end"); + CHECK(len > 0, "plain wrote"); + CHECK(contains(out, len, "hi"), "plain row 0"); + CHECK(contains(out, len, "yo"), "plain row 1"); + CHECK(contains(out, len, "\x1B") == 0, "plain has no escapes"); + CHECK(contains(out, len, "hi\nyo\n") == 1, "plain exact lines"); + free(out); + clio_buf_free(&b); + clio_term_free(&t); +} + +static void test_flush_diff(void) { + clio_term t; + clio_term_init(&t); + /* force tty mode + fixed size for a deterministic test */ + t.is_tty = 1; + t.rows = 3; + t.cols = 8; + + clio_buf a; + clio_buf_init(&a, 3, 8); + clio_style s = clio_style_default(); + clio_buf_put(&a, 0, 0, "row0", s, 0); + clio_buf_put(&a, 1, 0, "row1", s, 0); + clio_buf_put(&a, 2, 0, "row2", s, 0); + + cap_t c; + CHECK(cap_begin(&c) == 0, "cap begin 1"); + clio_flush(&t, &a); + size_t len; + char *out = cap_end(&c, &len); + CHECK(contains(out, len, ESC_CLEAR_SCREEN) == 1, "first flush clears"); + CHECK(contains(out, len, "\x1B[1;1H") == 1, "first flush row 1 pos"); + CHECK(contains(out, len, "\x1B[3;1H") == 1, "first flush row 3 pos"); + CHECK(contains(out, len, "row0") == 1, "first flush content"); + free(out); + + /* change only row 2 */ + clio_buf b; + clio_buf_init(&b, 3, 8); + clio_buf_put(&b, 0, 0, "row0", s, 0); + clio_buf_put(&b, 1, 0, "row1", s, 0); + clio_buf_put(&b, 2, 0, "CHG2", s, 0); + + CHECK(cap_begin(&c) == 0, "cap begin 2"); + clio_flush(&t, &b); + out = cap_end(&c, &len); + CHECK(contains(out, len, "\x1B[3;1H") == 1, "diff row 3 pos"); + CHECK(contains(out, len, "CHG2") == 1, "diff row 2 content"); + CHECK(contains(out, len, "\x1B[1;1H") == 0, "diff skips row 1"); + CHECK(contains(out, len, ESC_CLEAR_SCREEN) == 0, "diff no clear"); + CHECK(contains(out, len, "row0") == 0, "diff skips row 0 content"); + free(out); + + /* size change forces full repaint */ + clio_buf d2; + clio_buf_init(&d2, 2, 4); + clio_buf_put(&d2, 0, 0, "xy", s, 0); + clio_buf_put(&d2, 1, 0, "zw", s, 0); + clio_buf_free(&t.last); + t.has_last = 0; + t.rows = 2; + t.cols = 4; + CHECK(cap_begin(&c) == 0, "cap begin 3"); + clio_flush(&t, &d2); + out = cap_end(&c, &len); + CHECK(contains(out, len, ESC_CLEAR_SCREEN) == 1, "resize full repaint"); + CHECK(contains(out, len, "\x1B[1;1H") == 1, "resize row 1"); + free(out); + + clio_buf_free(&a); + clio_buf_free(&b); + clio_buf_free(&d2); + clio_term_free(&t); +} + +/* ------------------------------------------------------------------ */ +/* Term toggles emit the right codes */ +/* ------------------------------------------------------------------ */ + +static void test_term_toggles(void) { + clio_term t; + clio_term_init(&t); + t.is_tty = 1; + cap_t c; + CHECK(cap_begin(&c) == 0, "cap begin"); + clio_term_alt_on(&t); + clio_term_cursor_hide(&t); + clio_term_paste_on(&t); + clio_term_kitty_on(&t); + clio_term_bell(&t); + clio_term_alt_on(&t); /* idempotent: no second emission */ + size_t len; + char *out = cap_end(&c, &len); + CHECK(contains(out, len, ESC_ALT_SCREEN_ON) == 1, "alt on"); + CHECK(contains(out, len, ESC_ALT_SCREEN_ON) == 1, "alt on once"); + CHECK(contains(out, len, ESC_CURSOR_HIDE) == 1, "cursor hide"); + CHECK(contains(out, len, ESC_BRACKETED_PASTE_ON) == 1, "paste on"); + CHECK(contains(out, len, ESC_KITTY_ON) == 1, "kitty on"); + CHECK(contains(out, len, ESC_BELL) == 1, "bell"); + free(out); + + /* shutdown restores everything, in reverse, once each */ + CHECK(cap_begin(&c) == 0, "cap begin 2"); + clio_term_shutdown(&t); + out = cap_end(&c, &len); + CHECK(contains(out, len, ESC_KITTY_OFF) == 1, "kitty off"); + CHECK(contains(out, len, ESC_BRACKETED_PASTE_OFF) == 1, "paste off"); + CHECK(contains(out, len, ESC_CURSOR_SHOW) == 1, "cursor show"); + CHECK(contains(out, len, ESC_ALT_SCREEN_OFF) == 1, "alt off"); + /* order: kitty first, alt last */ + if (out) { + const char *kp = strstr(out, ESC_KITTY_OFF); + const char *ap = strstr(out, ESC_ALT_SCREEN_OFF); + CHECK(kp && ap && kp < ap, "shutdown order"); + } + free(out); + clio_term_free(&t); +} + +/* ------------------------------------------------------------------ */ +/* Readline (scripted key source) */ +/* ------------------------------------------------------------------ */ + +typedef struct keys { + clio_key *ks; + size_t n; + size_t i; +} keys_t; + +static int keys_next(void *ud, clio_key *k, int timeout_ms) { + (void) timeout_ms; + keys_t *s = (keys_t *) ud; + if (s->i >= s->n) + return 0; + *k = s->ks[s->i++]; + return 1; +} + +static void push(keys_t *s, int kind, u32 cp) { + s->ks[s->n].kind = kind; + s->ks[s->n].cp = cp; + s->n++; +} + +static char *run_readline(keys_t *s, int echo, int rows, int cols) { + clio_term t; + clio_term_init(&t); + t.is_tty = 1; + t.rows = (size_t) rows; + t.cols = (size_t) cols; + clio_buf b; + clio_buf_init(&b, (size_t) rows, (size_t) cols); + clio_style p = clio_style_default(); + clio_style st = clio_style_default(); + s->i = 0; + char *r = clio_readline(&t, &b, 0, "> ", p, st, echo, keys_next, s, 0); + clio_buf_free(&b); + clio_term_free(&t); + return r; +} + +static void test_readline(void) { + keys_t s; + clio_key ks[64]; + s.ks = ks; + s.n = 0; + char *r; + + /* type "ab", backspace, "c" -> "ac" */ + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_CHAR, 'b'); + push(&s, CLIOK_BACKSPACE, 0); + push(&s, CLIOK_CHAR, 'c'); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r != NULL && strcmp(r, "ac") == 0, "insert backspace"); + free(r); + + /* left arrow + insert: caret between a and c, x goes there -> "axc" */ + s.n = 0; + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_CHAR, 'c'); + push(&s, CLIOK_LEFT, 0); + push(&s, CLIOK_CHAR, 'x'); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r && strcmp(r, "axc") == 0, "left insert"); + free(r); + + /* ctrl+a, 'z' -> "zaxc" */ + s.n = 0; + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_CHAR, 'x'); + push(&s, CLIOK_CHAR, 'c'); + push(&s, CLIOK_CTRL_A, 0); + push(&s, CLIOK_CHAR, 'z'); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r && strcmp(r, "zaxc") == 0, "ctrl+a insert"); + free(r); + + /* ctrl+u clears line -> "" (submit empty) */ + s.n = 0; + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_CHAR, 'b'); + push(&s, CLIOK_CTRL_U, 0); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r == NULL, "ctrl+u clears all"); + + /* ctrl+a then ctrl+k kills whole line -> empty submit */ + s.n = 0; + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_CHAR, 'b'); + push(&s, CLIOK_CHAR, 'c'); + push(&s, CLIOK_CTRL_A, 0); + push(&s, CLIOK_CTRL_K, 0); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r == NULL, "ctrl+k empty after"); + s.n = 0; + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_CHAR, 'b'); + push(&s, CLIOK_CHAR, 'c'); + push(&s, CLIOK_LEFT, 0); + push(&s, CLIOK_CTRL_K, 0); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r && strcmp(r, "ab") == 0, "ctrl+k kills rest"); + free(r); + + /* ctrl+w deletes a word */ + s.n = 0; + push(&s, CLIOK_CHAR, 'h'); + push(&s, CLIOK_CHAR, 'e'); + push(&s, CLIOK_CHAR, 'l'); + push(&s, CLIOK_CHAR, 'l'); + push(&s, CLIOK_CHAR, 'o'); + push(&s, CLIOK_CHAR, ' '); + push(&s, CLIOK_CHAR, 'w'); + push(&s, CLIOK_CHAR, 'o'); + push(&s, CLIOK_CHAR, 'r'); + push(&s, CLIOK_CHAR, 'l'); + push(&s, CLIOK_CHAR, 'd'); + push(&s, CLIOK_CTRL_W, 0); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 24, 80); + /* matches bash/readline: the preceding space is killed too */ + CHECK(r && strcmp(r, "hello") == 0, "ctrl+w word"); + free(r); + + /* ctrl+c cancels */ + s.n = 0; + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_CTRL_C, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r == NULL, "ctrl+c cancels"); + + /* escape cancels */ + s.n = 0; + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_ESC, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r == NULL, "esc cancels"); + + /* ctrl+d on empty submits */ + s.n = 0; + push(&s, CLIOK_CTRL_D, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r == NULL, "ctrl+d empty submits"); + + /* ctrl+d deletes the char after the caret (left first) */ + s.n = 0; + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_CHAR, 'b'); + push(&s, CLIOK_LEFT, 0); + push(&s, CLIOK_CTRL_D, 0); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r && strcmp(r, "a") == 0, "ctrl+d delete"); + free(r); + + /* delete key removes forward char (caret in the middle) */ + s.n = 0; + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_CHAR, 'b'); + push(&s, CLIOK_LEFT, 0); + push(&s, CLIOK_DELETE, 0); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r && strcmp(r, "a") == 0, "delete forward"); + free(r); + + /* utf8 input round trip */ + s.n = 0; + push(&s, CLIOK_CHAR, 0x4E2D); /* 中 */ + push(&s, CLIOK_CHAR, 0x6587); /* 文 */ + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 24, 80); + CHECK(r && strcmp(r, "\xE4\xB8\xAD\xE6\x96\x87") == 0, "utf8 input"); + free(r); + + /* echo=0: stars rendered, but value is real text */ + s.n = 0; + push(&s, CLIOK_CHAR, 'a'); + push(&s, CLIOK_CHAR, 'b'); + push(&s, CLIOK_CHAR, 'c'); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 0, 24, 80); + CHECK(r && strcmp(r, "abc") == 0, "echo off value"); + free(r); +} + +/* ------------------------------------------------------------------ */ +/* Long lines: no wrap, window scrolls, backspace spans the "wrap" */ +/* ------------------------------------------------------------------ */ + +static void test_readline_long(void) { + /* 10 cols, prompt "> " => 8-cell text area. */ + clio_term t; + clio_term_init(&t); + t.is_tty = 1; + t.rows = 3; + t.cols = 10; + clio_buf b; + clio_buf_init(&b, 3, 10); + clio_style p = clio_style_default(); + clio_style st = clio_style_default(); + clio_str txt; + clio_str_init(&txt); + clio_str_ins(&txt, 0, "abcdefghijkl", 12); + size_t col; + + /* caret at end: caret pinned to last col, tail visible, head scrolled */ + col = clio_readline_paint(&b, 0, "> ", p, &txt, 12, st, 1); + CHECK(col == 9, "caret pinned last col"); + CHECK(b.cells[2].ch == 'f' && b.cells[8].ch == 'l', "tail window"); + CHECK(b.cells[9].ch == 0, "nothing past caret"); + + /* one backspace of scroll: window slides left by one */ + col = clio_readline_paint(&b, 0, "> ", p, &txt, 11, st, 1); + CHECK(col == 9, "caret stays pinned"); + CHECK(b.cells[2].ch == 'e' && b.cells[9].ch == 'l', "window slid"); + + /* caret in the middle: anchored at 0, right edge clipped */ + col = clio_readline_paint(&b, 0, "> ", p, &txt, 3, st, 1); + CHECK(col == 5, "caret middle"); + CHECK(b.cells[2].ch == 'a' && b.cells[9].ch == 'h', "head window clipped"); + + /* wide chars: 8 CJK = 16 cells; window must start on a char + * boundary and the caret clamp inside the row */ + clio_str_free(&txt); + clio_str_init(&txt); + static const char cjk[] = "\xE4\xB8\xAD\xE4\xB8\xAD\xE4\xB8\xAD\xE4\xB8\xAD" + "\xE4\xB8\xAD\xE4\xB8\xAD\xE4\xB8\xAD\xE4\xB8\xAD"; + clio_str_ins(&txt, 0, cjk, sizeof cjk - 1); + col = clio_readline_paint(&b, 0, "> ", p, &txt, sizeof cjk - 1, st, 1); + CHECK(col == 9, "wide caret clamped"); + CHECK(b.cells[2].ch == 0x4E2D && b.cells[8].ch == 0x4E2D, + "wide window on boundary"); + + /* noecho: same windowing over star count */ + clio_str_free(&txt); + clio_str_init(&txt); + clio_str_ins(&txt, 0, "abcdefghijkl", 12); + col = clio_readline_paint(&b, 0, "> ", p, &txt, 12, st, 0); + CHECK(col == 9, "star caret pinned"); + CHECK(b.cells[2].ch == '*' && b.cells[8].ch == '*' && b.cells[9].ch == 0, + "star window"); + + clio_str_free(&txt); + clio_buf_free(&b); + clio_term_free(&t); + + /* integration: type past the edge, backspace across the scroll + * boundary — the logical result must be exact */ + keys_t s; + clio_key ks[64]; + s.ks = ks; + s.n = 0; + for (char ch = 'a'; ch <= 'l'; ch++) + push(&s, CLIOK_CHAR, (u32) ch); + push(&s, CLIOK_BACKSPACE, 0); + push(&s, CLIOK_BACKSPACE, 0); + push(&s, CLIOK_BACKSPACE, 0); + push(&s, CLIOK_ENTER, 0); + char *r = run_readline(&s, 1, 3, 10); + CHECK(r && strcmp(r, "abcdefghi") == 0, "backspace across wrap"); + free(r); + + /* left-arrow into the scrolled region, then backspace */ + s.n = 0; + for (char ch = 'a'; ch <= 'l'; ch++) + push(&s, CLIOK_CHAR, (u32) ch); + push(&s, CLIOK_LEFT, 0); + push(&s, CLIOK_BACKSPACE, 0); + push(&s, CLIOK_ENTER, 0); + r = run_readline(&s, 1, 3, 10); + CHECK(r && strcmp(r, "abcdefghijl") == 0, "left+backspace across wrap"); + free(r); +} + +/* ------------------------------------------------------------------ */ +/* Readline canvas paint */ +/* ------------------------------------------------------------------ */ + +static void test_readline_paint(void) { + clio_term t; + clio_term_init(&t); + t.is_tty = 1; + t.rows = 4; + t.cols = 20; + clio_buf b; + clio_buf_init(&b, 4, 20); + clio_style p = clio_style_default(); + clio_style st = clio_style_default(); + clio_str txt; + clio_str_init(&txt); + + /* type "ab", caret after 'b' (col 4) */ + clio_str_ins(&txt, 0, "ab", 2); + size_t col = clio_readline_paint(&b, 0, "> ", p, &txt, 2, st, 1); + CHECK(col == 4, "caret col echo"); + CHECK(b.cells[0].ch == '>' && b.cells[1].ch == ' ', "prompt painted"); + CHECK(b.cells[2].ch == 'a' && b.cells[3].ch == 'b', "text painted"); + + /* echo off: stars */ + clio_buf_clear(&b); + col = clio_readline_paint(&b, 0, "> ", p, &txt, 2, st, 0); + CHECK(col == 4, "caret col noecho"); + CHECK(b.cells[2].ch == '*' && b.cells[3].ch == '*', "stars painted"); + + /* caret in the middle */ + clio_str_ins(&txt, 0, "ab", 2); + clio_str_ins(&txt, 1, "X", 1); + clio_str_ins(&txt, 2, "c", 1); + col = clio_readline_paint(&b, 0, "> ", p, &txt, 2, st, 1); + CHECK(col == 4, "caret middle"); + + clio_str_free(&txt); + clio_buf_free(&b); + clio_term_free(&t); +} + +int main(void) { + test_utf8(); + test_buf(); + test_keydec(); + test_color_policy(); + test_fstyle(); + test_flush_plain(); + test_flush_diff(); + test_term_toggles(); + test_readline(); + test_readline_long(); + test_readline_paint(); + + printf("%d checks, %d failures\n", checks, failures); + return failures == 0 ? 0 : 1; +} diff --git a/tests/test_direct.c b/tests/test_direct.c new file mode 100644 index 0000000..7f0e192 --- /dev/null +++ b/tests/test_direct.c @@ -0,0 +1,157 @@ +// Tests for direct.h - computation graph connectivity and updates +#include +typedef double direct_value_t; +#define DT_IMPLEMENTATION +#include "../include/direct.h" + +#include + +#define EPS 1e-9 + +#define ASSERT(cond, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s (test_direct.c:%d)\n", msg, __LINE__); \ + failures++; \ + } \ + } while (0) + +#define ASSERT_FLOAT_EQ(a, b, msg) \ + do { \ + if (fabs((a) - (b)) > EPS) { \ + fprintf(stderr, "FAIL: %s (got %f, expected %f) (test_direct.c:%d)\n", \ + msg, (double)(a), (double)(b), __LINE__); \ + failures++; \ + } \ + } while (0) + +static int failures = 0; + +void test_connect() { + node_t a = {NULL, 1, 0, &dt_one}; + node_t b = {NULL, 2, 0, &dt_add}; + + ASSERT(dt_connect(&b, &a), "connect returns 1 on success"); + ASSERT(b.edge != NULL, "connect sets edge"); + if (!b.edge) { dt_clear_edges(&b); return; } + ASSERT(b.edge->node == &a, "connect sets correct node"); + + dt_clear_edges(&b); +} + +void test_disconnect() { + node_t a = {NULL, 1, 0, &dt_one}; + node_t b = {NULL, 2, 0, &dt_add}; + + dt_connect(&b, &a); + dt_disconnect(&b, &a); + ASSERT(b.edge == NULL, "disconnect removes edge"); +} + +void test_connect_ok_null() { + node_t a = {NULL, 1, 0, &dt_one}; + node_t b = {NULL, 2, 0, &dt_add}; + + ASSERT(!dt_connect_ok(NULL, &a), "connect_ok NULL to returns 0"); + ASSERT(!dt_connect_ok(&b, NULL), "connect_ok NULL from returns 0"); + ASSERT(!dt_connect_ok(NULL, NULL), "connect_ok both NULL returns 0"); +} + +void test_add() { + node_t a = {NULL, 0, 0, &dt_one}; + node_t b = {NULL, 0, 0, &dt_one}; + node_t sum = {NULL, 0, 0, &dt_add}; + + dt_connect(&sum, &a); + dt_connect(&sum, &b); + dt_update(&sum); + + ASSERT_FLOAT_EQ(a.x, 1, "dt_one sets a to 1"); + ASSERT_FLOAT_EQ(b.x, 1, "dt_one sets b to 1"); + ASSERT_FLOAT_EQ(sum.x, 2, "add sums edges: 0+1+1"); + + dt_clear_edges(&sum); +} + +void test_subtract() { + node_t a = {NULL, 0, 0, &dt_one}; + node_t b = {NULL, 0, 0, &dt_one}; + node_t c = {NULL, 0, 0, &dt_one}; + node_t diff = {NULL, 0, 0, &dt_subtract}; + + dt_connect(&diff, &a); + dt_connect(&diff, &b); + dt_connect(&diff, &c); + dt_update(&diff); + + ASSERT_FLOAT_EQ(diff.x, -3, "subtract: 0 - 1 - 1 - 1"); + + dt_clear_edges(&diff); +} + +void test_multiple_connections() { + node_t a = {NULL, 0, 0, &dt_one}; + node_t b = {NULL, 0, 0, &dt_one}; + node_t c = {NULL, 0, 0, &dt_one}; + node_t sum = {NULL, 5, 0, &dt_add}; + + dt_connect(&sum, &a); + dt_connect(&sum, &b); + dt_connect(&sum, &c); + dt_update(&sum); + + ASSERT_FLOAT_EQ(sum.x, 8, "add: 5 + 1 + 1 + 1"); + + dt_clear_edges(&sum); +} + +void test_chained_addition() { + node_t a = {NULL, 0, 0, &dt_one}; + node_t b = {NULL, 0, 0, &dt_one}; + node_t c = {NULL, 0, 0, &dt_one}; + + node_t ab = {NULL, 0, 0, &dt_add}; + dt_connect(&ab, &a); + dt_connect(&ab, &b); + + node_t abc = {NULL, 10, 0, &dt_add}; + dt_connect(&abc, &ab); + dt_connect(&abc, &c); + + dt_update(&abc); + ASSERT_FLOAT_EQ(ab.x, 2, "ab: 0+1+1=2"); + ASSERT_FLOAT_EQ(abc.x, 13, "abc: 10+2+1=13"); + + dt_clear_edges(&ab); + dt_clear_edges(&abc); +} + +void test_clear_edges() { + node_t a = {NULL, 1, 0, &dt_one}; + node_t b = {NULL, 0, 0, &dt_add}; + + dt_connect(&b, &a); + dt_clear_edges(&b); + ASSERT(b.edge == NULL, "clear_edges sets edge to NULL"); +} + +void test_single_node() { + node_t n = {NULL, 42, 0, &dt_one}; + dt_update(&n); + ASSERT_FLOAT_EQ(n.x, 1, "single node with dt_one"); +} + +int main() { + test_connect(); + test_disconnect(); + test_add(); + test_subtract(); + test_multiple_connections(); + test_chained_addition(); + test_clear_edges(); + test_single_node(); + test_connect_ok_null(); + + printf("test_direct: %d failures\n", failures); + return failures; +} diff --git a/tests/test_dotagents.c b/tests/test_dotagents.c new file mode 100644 index 0000000..4b977c7 --- /dev/null +++ b/tests/test_dotagents.c @@ -0,0 +1,524 @@ +/* test_dotagents.c — standalone test suite for dotagents.h (the .agents + * protocol layer). Zero dependencies: compiles to a single TU with no + * linked libraries (frontmatter.h provides everything it needs). */ +#include "dotagents.h" + +#include +#include +#include +#include +#include +#include + +static int failures = 0; +static int checks = 0; + +#define CHECK(cond, msg) \ + do { \ + checks++; \ + if (!(cond)) { \ + failures++; \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, (msg)); \ + } \ + } while (0) + +static char *tmpdir(const char *tag) { + char path[256]; + snprintf(path, sizeof path, "/tmp/llshc-da-%s-%d", tag, (int)getpid()); + /* rm -rf */ + char cmd[300]; + snprintf(cmd, sizeof cmd, "rm -rf '%s'", path); + if (system(cmd) != 0) {} + /* mkdir -p */ + snprintf(cmd, sizeof cmd, "mkdir -p '%s'", path); + if (system(cmd) != 0) + exit(1); + return fm_strdup(path); +} + +static void wfile_bin(const char *dir, const char *rel, const char *content) { + char path[512]; + snprintf(path, sizeof path, "%s/%s", dir, rel); + char *slash = strrchr(path, '/'); + if (slash) { + char dirpart[512]; + size_t n = (size_t)(slash - path); + memcpy(dirpart, path, n); + dirpart[n] = '\0'; + char cmd[600]; + snprintf(cmd, sizeof cmd, "mkdir -p '%s'", dirpart); + if (system(cmd) != 0) + exit(1); + } + FILE *f = fopen(path, "w"); + if (!f) + exit(1); + fputs(content, f); + fclose(f); +} + +static int count_warns(const dotagents *d, const char *needle) { + int n = 0; + for (size_t i = 0; i < d->warnings.len; i++) + if (strstr(d->warnings.items[i], needle)) + n++; + return n; +} + +/* ------------------------------------------------------------------ */ +/* Layer tests */ +/* ------------------------------------------------------------------ */ + +static void test_loads_global_only(void) { + char *ws = tmpdir("ws1"); + char *global = tmpdir("g1"); + wfile_bin(global, "agents.md", "global guidelines"); + wfile_bin(global, "skills/g1/skill.md", + "---\nid: g1\nname: G1\ndescription: global skill\n---\ndo g1"); + dotagents d; + dotagents_load_from(&d, ws, global); + CHECK(d.found_count == 1, "one layer found"); + CHECK(d.found_layers[0] == LAYER_GLOBAL, "layer is global"); + CHECK(d.agents_md && strcmp(d.agents_md, "global guidelines") == 0, + "agents_md"); + CHECK(d.skill_count == 1, "one skill"); + CHECK(strcmp(d.skills[0].id, "g1") == 0, "skill id"); + CHECK(strcmp(d.skills[0].body, "do g1") == 0, "skill body"); + dotagents_free(&d); + free(ws); + free(global); +} + +static void test_md_filenames_case_insensitive(void) { + char *ws = tmpdir("ws-cs"); + wfile_bin(ws, "AGENTS.MD", "workspace guidelines"); + wfile_bin(ws, "System-Prompt.md", "the system prompt"); + wfile_bin(ws, "skills/UPPER/SKILL.md", + "---\nid: upper\nname: Upper\ndescription: caps skill\n---\ndo " + "upper"); + wfile_bin(ws, "agents/REVIEWER/Agent.md", + "---\nid: reviewer\nname: Reviewer\n---\nYou review."); + wfile_bin(ws, "tasks/daily/Task.MD", + "---\nid: daily\nname: Daily\nenabled: true\n---\nRun daily."); + wfile_bin(ws, "memories/note.MD", + "---\nid: note\ntitle: Note\n---\nbody"); + dotagents d; + dotagents_load_from(&d, ws, NULL); + CHECK(d.found_count == 1, "workspace found"); + CHECK(d.agents_md && strcmp(d.agents_md, "workspace guidelines") == 0, + "AGENTS.MD"); + CHECK(d.system_prompt && strcmp(d.system_prompt, "the system prompt") == 0, + "System-Prompt.md"); + CHECK(d.skill_count == 1 && strcmp(d.skills[0].id, "upper") == 0, + "SKILL.md"); + CHECK(d.sub_agent_count == 1 && + strcmp(d.sub_agents[0].id, "reviewer") == 0, + "Agent.md"); + CHECK(d.task_count == 1 && strcmp(d.tasks[0].id, "daily") == 0, + "Task.MD"); + CHECK(d.memory_count == 1 && strcmp(d.memories[0].id, "note") == 0, + "note.MD"); + CHECK(d.warnings.len == 0, "no case warnings"); + dotagents_free(&d); + free(ws); +} + +static void test_workspace_wins(void) { + char *ws = tmpdir("ws2"); + char *global = tmpdir("g2"); + wfile_bin(global, "agents.md", "global guidelines"); + wfile_bin(global, "skills/review/skill.md", + "---\nid: review\nname: Review G\ndescription: global\n---\n" + "global body"); + wfile_bin(global, "memories/m.md", + "---\nid: m\ntitle: M Global\nimportance: low\ntags: a\n---\ng"); + wfile_bin(ws, "agents.md", "workspace guidelines"); + wfile_bin(ws, "skills/review/skill.md", + "---\nid: review\nname: Review W\ndescription: workspace\n---\n" + "workspace body"); + wfile_bin(ws, "skills/extra/skill.md", + "---\nid: extra\nname: Extra\n---\nextra body"); + dotagents d; + dotagents_load_from(&d, ws, global); + CHECK(d.agents_md && strcmp(d.agents_md, "workspace guidelines") == 0, + "workspace agents.md wins"); + const skill *review = dotagents_find_skill(&d, "review"); + CHECK(review && review->layer == LAYER_WORKSPACE, "review layer"); + CHECK(review && strcmp(review->body, "workspace body") == 0, + "review body"); + CHECK(d.skill_count == 2, "review + extra"); + CHECK(d.memory_count == 1, "only global memory"); + dotagents_free(&d); + free(ws); + free(global); +} + +static void test_json_layers_merge_by_key(void) { + char *ws = tmpdir("ws3"); + char *global = tmpdir("g3"); + wfile_bin(global, "models.json", + "{\"default\":\"ga\",\"presets\":{\"ga\":{\"model\":\"ga-" + "model\"},\"shared\":{\"model\":\"s\"}}}"); + wfile_bin(global, "mcp.json", + "{\"mcpServers\":{\"fs\":{\"command\":\"npx\"}," + "\"git\":{\"url\":\"g\"}}}"); + wfile_bin(ws, "models.json", + "{\"default\":\"wb\",\"presets\":{\"wb\":{\"provider\":\"" + "ollama\"},\"shared\":{\"model\":\"ws\"}}}"); + wfile_bin(ws, "mcp.json", "{\"mcpServers\":{\"git\":{\"url\":\"w\"}}}"); + dotagents d; + dotagents_load_from(&d, ws, global); + CHECK(d.has_models, "models present"); + CHECK(d.models.default_name && + strcmp(d.models.default_name, "wb") == 0, + "default=wb"); + CHECK(d.models.preset_count == 3, "3 presets"); + const model_preset *ga = models_file_get(&d.models, "ga"); + CHECK(ga && ga->model && strcmp(ga->model, "ga-model") == 0, "ga.model"); + const model_preset *sh = models_file_get(&d.models, "shared"); + CHECK(sh && sh->model && strcmp(sh->model, "ws") == 0, "shared.model"); + CHECK(d.mcp_count == 2, "2 mcp servers"); + const char *git = dotagents_mcp_config(&d, "git"); + CHECK(git && strstr(git, "\"url\":\"w\""), "git.url=w"); + dotagents_free(&d); + free(ws); + free(global); +} + +static void test_malformed_files_warn_and_skip(void) { + char *ws = tmpdir("ws4"); + wfile_bin(ws, "models.json", "{not json"); + wfile_bin(ws, "mcp.json", "{\"nope\":true}"); + wfile_bin(ws, "skills/broken/skill.md", "no frontmatter"); + char cmd[300]; + snprintf(cmd, sizeof cmd, "mkdir -p '%s/skills/empty'", ws); + if (system(cmd) != 0) + exit(1); + dotagents d; + dotagents_load_from(&d, ws, NULL); + CHECK(!d.has_models, "no models"); + CHECK(d.mcp_count == 0, "no mcp"); + CHECK(d.skill_count == 1 && strcmp(d.skills[0].id, "broken") == 0, + "broken skill kept"); + CHECK(d.skills[0].body && + strstr(d.skills[0].body, "no frontmatter"), + "broken skill body"); + CHECK(count_warns(&d, "models.json") > 0, "models.json warning"); + CHECK(count_warns(&d, "mcp.json") > 0, "mcp.json warning"); + CHECK(count_warns(&d, "skills/empty") > 0, "skills/empty warning"); + dotagents_free(&d); + free(ws); +} + +static void test_missing_layers_empty(void) { + char *ws = tmpdir("ws5"); + dotagents d; + dotagents_load_from(&d, ws, NULL); + CHECK(d.found_count == 0, "no layers"); + CHECK(d.skill_count == 0, "no skills"); + CHECK(d.warnings.len == 0, "no warnings"); + dotagents_free(&d); + free(ws); +} + +static void test_tasks_and_sub_agents(void) { + char *ws = tmpdir("ws6"); + wfile_bin(ws, "tasks/daily/task.md", + "---\nkind: task\nid: daily\nname: Daily Review\nintervalMinutes" + ": 60\nenabled: true\nrunOnStartup: true\n---\nReview open " + "PRs."); + wfile_bin(ws, "agents/reviewer/agent.md", + "---\nid: reviewer\nname: Reviewer\ndescription: Reviews code\n" + "role: delegation-target\nenabled: true\n---\nYou are a " + "reviewer."); + wfile_bin(ws, "agents/reviewer/config.json", + "{\"modelConfig\":{\"mcpToolsOpenaiModel\":\"gpt-4o\"}}"); + dotagents d; + dotagents_load_from(&d, ws, NULL); + CHECK(d.task_count == 1, "one task"); + CHECK(d.tasks[0].has_interval && d.tasks[0].interval_minutes == 60, + "interval"); + CHECK(d.tasks[0].run_on_startup, "run_on_startup"); + CHECK(d.tasks[0].enabled, "enabled"); + CHECK(strcmp(d.tasks[0].body, "Review open PRs.") == 0, "task body"); + CHECK(d.sub_agent_count == 1, "one sub agent"); + CHECK(strcmp(d.sub_agents[0].role, "delegation-target") == 0, "role"); + CHECK(strcmp(d.sub_agents[0].body, "You are a reviewer.") == 0, + "sub agent body"); + CHECK(d.sub_agents[0].config != NULL, "config present"); + dotagents_free(&d); + free(ws); +} + +static void test_malformed_sub_agent_config(void) { + char *ws = tmpdir("ws9"); + wfile_bin(ws, "agents/broken/agent.md", + "---\nid: broken\nname: Broken\ndescription: has a bad config\n" + "---\nYou are broken."); + wfile_bin(ws, "agents/broken/config.json", "{not json"); + dotagents d; + dotagents_load_from(&d, ws, NULL); + CHECK(d.sub_agent_count == 1, "one sub agent"); + CHECK(d.sub_agents[0].config == NULL, "config dropped"); + CHECK(count_warns(&d, "agents/broken/config.json") > 0, + "config warning"); + dotagents_free(&d); + free(ws); +} + +static void test_memories_rank(void) { + memory low, high; + memset(&low, 0, sizeof low); + low.importance = fm_strdup("low"); + memset(&high, 0, sizeof high); + high.importance = fm_strdup("high"); + CHECK(memory_importance_rank(&high) < memory_importance_rank(&low), + "high ranks before low"); + free(low.importance); + free(high.importance); +} + +static void test_project_sections(void) { + char *ws = tmpdir("ws7"); + wfile_bin(ws, "agents.md", "# Guidelines\n- be nice"); + wfile_bin(ws, "skills/cr/skill.md", + "---\nid: cr\nname: CR\ndescription: code review\n---\nreview " + "steps"); + wfile_bin(ws, "memories/p.md", + "---\nid: p\ntitle: P\nimportance: high\ntags: x\n---\nbody"); + dotagents d; + dotagents_load_from(&d, ws, NULL); + char *sections = dotagents_project_sections(&d, 50); + CHECK(strstr(sections, "- be nice"), "guidelines"); + CHECK(strstr(sections, "- cr: code review"), "skill line"); + CHECK(strstr(sections, "use_skill"), "use_skill mention"); + CHECK(strstr(sections, "search_memories"), "search_memories mention"); + char *pre = dotagents_compose_preamble(&d, "BASE", 50); + CHECK(strstr(pre, "BASE") && strstr(sections, "be nice") && + memcmp(pre, "BASE", 4) == 0, + "preamble = base + sections"); + free(sections); + free(pre); + dotagents_free(&d); + free(ws); +} + +static void test_skill_index_cap(void) { + char *ws = tmpdir("ws10"); + const char *ids[5] = {"a", "b", "c", "d", "e"}; + for (int i = 0; i < 5; i++) { + char rel[64]; + char content[128]; + snprintf(rel, sizeof rel, "skills/%s/skill.md", ids[i]); + snprintf(content, sizeof content, + "---\nid: %s\nname: %s\ndescription: skill %d\n---\nbody", + ids[i], ids[i], i); + wfile_bin(ws, rel, content); + } + dotagents d; + dotagents_load_from(&d, ws, NULL); + char *sections = dotagents_project_sections(&d, 2); + CHECK(strstr(sections, "- a: skill 0"), "a listed"); + CHECK(strstr(sections, "- b: skill 1"), "b listed"); + CHECK(!strstr(sections, "- c: skill 2"), "c not listed"); + CHECK(strstr(sections, "and 3 more"), "…and 3 more"); + free(sections); + dotagents_free(&d); + free(ws); +} + +static void test_disabled_skills_excluded(void) { + char *ws = tmpdir("ws8"); + wfile_bin(ws, "skills/off/skill.md", + "---\nid: off\ndescription: hidden\nenabled: false\n---\nx"); + dotagents d; + dotagents_load_from(&d, ws, NULL); + CHECK(d.skill_count == 1, "skill kept"); + CHECK(!d.skills[0].enabled, "disabled"); + char *sections = dotagents_project_sections(&d, 50); + CHECK(!strstr(sections, "off"), "not in sections"); + free(sections); + dotagents_free(&d); + free(ws); +} + +/* ------------------------------------------------------------------ */ +/* Spec-gap conformance */ +/* ------------------------------------------------------------------ */ + +static void test_task_profile_id(void) { + char *ws = tmpdir("ws-pid"); + wfile_bin(ws, "tasks/nightly/task.md", + "---\nkind: task\nid: nightly\nname: Nightly\nprofileId: " + "prof-9\nenabled: true\n---\nRun nightly."); + dotagents d; + dotagents_load_from(&d, ws, NULL); + CHECK(d.task_count == 1, "one task"); + CHECK(d.tasks[0].profile_id && + strcmp(d.tasks[0].profile_id, "prof-9") == 0, + "profile_id parsed"); + dotagents_free(&d); + free(ws); +} + +static void test_task_profile_id_workspace_override(void) { + char *ws = tmpdir("ws-pid2"); + char *global = tmpdir("g-pid"); + wfile_bin(global, "tasks/nightly/task.md", + "---\nid: nightly\nprofileId: g-prof\n---\ng"); + wfile_bin(ws, "tasks/nightly/task.md", + "---\nid: nightly\nprofileId: w-prof\n---\nw"); + dotagents d; + dotagents_load_from(&d, ws, global); + CHECK(d.task_count == 1, "one task"); + CHECK(d.tasks[0].profile_id && + strcmp(d.tasks[0].profile_id, "w-prof") == 0, + "workspace profileId wins"); + dotagents_free(&d); + free(ws); + free(global); +} + +static void test_agent_connection_type(void) { + char *ws = tmpdir("ws-ct"); + wfile_bin(ws, "agents/remote/agent.md", + "---\nid: remote\nname: Remote\nrole: delegation-target\n" + "connection-type: internal\nenabled: true\n---\nYou are " + "remote."); + dotagents d; + dotagents_load_from(&d, ws, NULL); + CHECK(d.sub_agent_count == 1, "one agent"); + CHECK(d.sub_agents[0].connection_type && + strcmp(d.sub_agents[0].connection_type, "internal") == 0, + "connection-type parsed"); + dotagents_free(&d); + free(ws); +} + +static void test_settings_layers_merge(void) { + char *ws = tmpdir("ws-set"); + char *global = tmpdir("g-set"); + wfile_bin(global, "speakmcp-settings.json", + "{\"theme\":\"dark\",\"telemetry\":true,\"shared\":\"g\"}"); + wfile_bin(ws, "speakmcp-settings.json", + "{\"shared\":\"w\",\"volume\":2}"); + dotagents d; + dotagents_load_from(&d, ws, global); + CHECK(d.settings_json != NULL, "settings present"); + CHECK(d.settings_json && strstr(d.settings_json, "\"shared\":\"w\""), + "shared=w"); + CHECK(d.settings_json && strstr(d.settings_json, "\"theme\":\"dark\""), + "theme kept"); + CHECK(d.settings_json && strstr(d.settings_json, "\"volume\":2"), + "volume kept"); + CHECK(d.settings_json && strstr(d.settings_json, "\"telemetry\":true"), + "telemetry kept"); + dotagents_free(&d); + free(ws); + free(global); +} + +static void test_settings_invalid_warns(void) { + char *ws = tmpdir("ws-setbad"); + wfile_bin(ws, "speakmcp-settings.json", "{nope"); + dotagents d; + dotagents_load_from(&d, ws, NULL); + CHECK(d.settings_json == NULL, "no settings"); + CHECK(count_warns(&d, "speakmcp-settings.json") > 0, "settings warning"); + dotagents_free(&d); + free(ws); +} + +static void test_layouts_ui_merge(void) { + char *ws = tmpdir("ws-lay"); + char *global = tmpdir("g-lay"); + wfile_bin(global, "layouts/ui.json", "{\"panes\":2,\"theme\":\"dark\"}"); + wfile_bin(ws, "layouts/ui.json", "{\"theme\":\"light\"}"); + dotagents d; + dotagents_load_from(&d, ws, global); + CHECK(d.layouts_json && strstr(d.layouts_json, "\"theme\":\"light\""), + "theme=w"); + CHECK(d.layouts_json && strstr(d.layouts_json, "\"panes\":2"), + "panes kept"); + dotagents_free(&d); + free(ws); + free(global); +} + +static void test_settings_only_layer_discovered(void) { + char *ws = tmpdir("ws-setonly"); + wfile_bin(ws, "speakmcp-settings.json", "{\"a\":1}"); + dotagents d; + dotagents_load_from(&d, ws, NULL); + CHECK(d.found_count == 1, "layer found"); + CHECK(d.settings_json != NULL, "settings"); + dotagents_free(&d); + free(ws); +} + +static void test_layouts_only_layer_discovered(void) { + char *ws = tmpdir("ws-layonly"); + wfile_bin(ws, "layouts/ui.json", "{\"a\":1}"); + dotagents d; + dotagents_load_from(&d, ws, NULL); + CHECK(d.found_count == 1, "layer found"); + CHECK(d.layouts_json != NULL, "layouts"); + dotagents_free(&d); + free(ws); +} + +static void test_backups_ignored(void) { + char *ws = tmpdir("ws-bk"); + wfile_bin(ws, "agents.md", "guidelines"); + wfile_bin(ws, ".backups/skills/ghost/skill.md", + "---\nid: ghost\nname: Ghost\n---\nbody"); + wfile_bin(ws, ".backups/memories/ghost.md", + "---\nid: ghost\ntitle: G\n---\nb"); + dotagents d; + dotagents_load_from(&d, ws, NULL); + CHECK(d.agents_md && strcmp(d.agents_md, "guidelines") == 0, + "agents_md"); + CHECK(d.skill_count == 0, "no ghost skill"); + CHECK(d.memory_count == 0, "no ghost memory"); + dotagents_free(&d); + free(ws); +} + +static void test_unknown_frontmatter_keys_accepted(void) { + char *ws = tmpdir("ws-unk"); + wfile_bin(ws, "agents/x/agent.md", + "---\nkind: agent\nid: x\nname: X\nmetadata: stuff\n---\nb"); + dotagents d; + dotagents_load_from(&d, ws, NULL); + CHECK(d.sub_agent_count == 1, "agent kept"); + CHECK(d.warnings.len == 0, "no warnings"); + dotagents_free(&d); + free(ws); +} + +int main(void) { + test_loads_global_only(); + test_md_filenames_case_insensitive(); + test_workspace_wins(); + test_json_layers_merge_by_key(); + test_malformed_files_warn_and_skip(); + test_missing_layers_empty(); + test_tasks_and_sub_agents(); + test_malformed_sub_agent_config(); + test_memories_rank(); + test_project_sections(); + test_skill_index_cap(); + test_disabled_skills_excluded(); + test_task_profile_id(); + test_task_profile_id_workspace_override(); + test_agent_connection_type(); + test_settings_layers_merge(); + test_settings_invalid_warns(); + test_layouts_ui_merge(); + test_settings_only_layer_discovered(); + test_layouts_only_layer_discovered(); + test_backups_ignored(); + test_unknown_frontmatter_keys_accepted(); + printf("%d checks, %d failures\n", checks, failures); + return failures ? 1 : 0; +} diff --git a/tests/test_escape.c b/tests/test_escape.c new file mode 100644 index 0000000..e749443 --- /dev/null +++ b/tests/test_escape.c @@ -0,0 +1,113 @@ +// Tests for escape.h - ANSI escape code macros and functions +#include +#include +#include +#include +#include + +#define ESCAPE_IMPLEMENTATION +#include "../include/escape.h" + +#define ASSERT(cond, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s (test_escape.c:%d)\n", msg, __LINE__); \ + failures++; \ + } \ + } while (0) + +static int failures = 0; + +void test_macros() { + ASSERT(ESC_CURSOR_UP(5)[0] == '\x1B', "cursor up starts with ESC"); + ASSERT(ESC_CURSOR_DOWN(3)[0] == '\x1B', "cursor down starts with ESC"); + ASSERT(ESC_CURSOR_LEFT(2)[0] == '\x1B', "cursor left starts with ESC"); + ASSERT(ESC_CURSOR_RIGHT(2)[0] == '\x1B', "cursor right starts with ESC"); + ASSERT(ESC_CLEAR_SCREEN[0] == '\x1B', "clear screen starts with ESC"); + ASSERT(ESC_CLEAR_LINE[0] == '\x1B', "clear line starts with ESC"); + ASSERT(ESC_RESET[0] == '\x1B', "reset starts with ESC"); + ASSERT(ESC_BOLD[0] == '\x1B', "bold starts with ESC"); + ASSERT(ESC_UNDERLINE[0] == '\x1B', "underline starts with ESC"); + ASSERT(ESC_BLINK[0] == '\x1B', "blink starts with ESC"); + ASSERT(ESC_REVERSE[0] == '\x1B', "reverse starts with ESC"); + ASSERT(ESC_CONCEAL[0] == '\x1B', "conceal starts with ESC"); + ASSERT(ESC_REVEAL[0] == '\x1B', "reveal starts with ESC"); + ASSERT(ESC_STRIKETHROUGH[0] == '\x1B', "strikethrough starts with ESC"); + ASSERT(ESC_OVERLINE[0] == '\x1B', "overline starts with ESC"); +} + +void test_color_macros() { + ASSERT(ESC_COLOR_FG(1)[0] == '\x1B', "fg color starts with ESC"); + ASSERT(ESC_COLOR_BG(4)[0] == '\x1B', "bg color starts with ESC"); + ASSERT(ESC_COLOR_FG_BRIGHT(3)[0] == '\x1B', "fg bright starts with ESC"); + ASSERT(ESC_COLOR_BG_BRIGHT(5)[0] == '\x1B', "bg bright starts with ESC"); + ASSERT(ESC_COLOR_FG_RGB(255, 128, 0)[0] == '\x1B', "fg rgb starts with ESC"); + ASSERT(ESC_COLOR_BG_RGB(0, 128, 255)[0] == '\x1B', "bg rgb starts with ESC"); + ASSERT(ESC_COLOR_FG_256(196)[0] == '\x1B', "fg 256 starts with ESC"); + ASSERT(ESC_COLOR_BG_256(21)[0] == '\x1B', "bg 256 starts with ESC"); +} + +void test_cursor_position_macro() { + ASSERT(ESC_CURSOR_POSITION(10, 20)[0] == '\x1B', "cursor position starts with ESC"); +} + +void test_scroll_macros() { + ASSERT(ESC_SCROLL_UP(3)[0] == '\x1B', "scroll up starts with ESC"); + ASSERT(ESC_SCROLL_DOWN(2)[0] == '\x1B', "scroll down starts with ESC"); +} + +void test_clear_macros() { + ASSERT(ESC_CLEAR_SCREEN_START[0] == '\x1B', "clear screen start"); + ASSERT(ESC_CLEAR_SCREEN_END[0] == '\x1B', "clear screen end"); + ASSERT(ESC_CLEAR_LINE_START[0] == '\x1B', "clear line start"); + ASSERT(ESC_CLEAR_LINE_END[0] == '\x1B', "clear line end"); + ASSERT(ESC_CURSOR_LINE_NEXT(2)[0] == '\x1B', "cursor line next"); +} + +void test_csi() { + ASSERT(CSI[0] == '\x1B', "CSI starts with ESC"); + ASSERT(CSI[1] == '[', "CSI second char is ["); +} + +void test_fvariants() { + esc_fcursor_up(stdout, 3); + + esc_fclear_line(stdout); + + esc_fcolor_fg_256(stdout, 196); + + esc_fcursor_position(stdout, 10, 20); + + esc_fscroll_up(stdout, 2); + + esc_fscroll_down(stdout, 1); + + esc_fclear_screen(stdout); + + esc_fcolor_fg_rgb(stdout, 255, 128, 0); + + esc_fcolor_bg_rgb(stdout, 0, 128, 255); + + esc_fcursor_down(stdout, 5); + esc_fcursor_left(stdout, 2); + esc_fcursor_right(stdout, 2); + esc_fcursor_line_next(stdout, 1); + esc_fclear_screen_start(stdout); + esc_fclear_screen_end(stdout); + esc_fclear_line_start(stdout); + esc_fclear_line_end(stdout); + esc_fcolor_bg_256(stdout, 21); +} + +int main() { + test_macros(); + test_color_macros(); + test_cursor_position_macro(); + test_scroll_macros(); + test_clear_macros(); + test_csi(); + test_fvariants(); + + printf("test_escape: %d failures\n", failures); + return failures; +} diff --git a/tests/test_flat.c b/tests/test_flat.c new file mode 100644 index 0000000..2710ce5 --- /dev/null +++ b/tests/test_flat.c @@ -0,0 +1,132 @@ +// Tests for flat.h - directory traversal utilities +#define FLAT_IMPLEMENTATION +#include "../include/flat.h" + +#include +#include +#include +#include +#include +#include + +#define ASSERT(cond, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s (test_flat.c:%d)\n", msg, __LINE__); \ + failures++; \ + } \ + } while (0) + +static int failures = 0; + +void test_path() { + char *p = flat_path(3, "a", "b", "c"); + ASSERT(p != NULL, "path returns non-NULL"); + ASSERT(strcmp(p, "a/b/c") == 0, "path a/b/c"); + free(p); + + p = flat_path(2, "home", "user"); + ASSERT(strcmp(p, "home/user") == 0, "path home/user"); + free(p); + + p = flat_path(1, "single"); + ASSERT(strcmp(p, "single") == 0, "path single component"); + free(p); +} + +void test_path_zero() { + char *p = flat_path(0, NULL); + ASSERT(p == NULL, "zero components returns NULL"); +} + +void test_entry_count() { + ASSERT(mkdir("/tmp/test_flat_dir", 0777) == 0 || errno == EEXIST, "mkdir test_flat_dir"); + + FILE *f1 = fopen("/tmp/test_flat_dir/file1.txt", "w"); + ASSERT(f1 != NULL, "fopen file1.txt"); + if (!f1) { rmdir("/tmp/test_flat_dir"); return; } + fclose(f1); + FILE *f2 = fopen("/tmp/test_flat_dir/file2.txt", "w"); + ASSERT(f2 != NULL, "fopen file2.txt"); + if (!f2) { unlink("/tmp/test_flat_dir/file1.txt"); rmdir("/tmp/test_flat_dir"); return; } + fclose(f2); + + int64_t count = flat_directory_entry_count("/tmp/test_flat_dir"); + ASSERT(count >= 2, "entry count >= 2"); + + unlink("/tmp/test_flat_dir/file1.txt"); + unlink("/tmp/test_flat_dir/file2.txt"); + rmdir("/tmp/test_flat_dir"); +} + +void test_entries() { + ASSERT(mkdir("/tmp/test_flat_dir2", 0777) == 0 || errno == EEXIST, "mkdir test_flat_dir2"); + + FILE *f1 = fopen("/tmp/test_flat_dir2/alpha.txt", "w"); + ASSERT(f1 != NULL, "fopen alpha.txt"); + if (!f1) { rmdir("/tmp/test_flat_dir2"); return; } + fclose(f1); + FILE *f2 = fopen("/tmp/test_flat_dir2/beta.txt", "w"); + ASSERT(f2 != NULL, "fopen beta.txt"); + if (!f2) { unlink("/tmp/test_flat_dir2/alpha.txt"); rmdir("/tmp/test_flat_dir2"); return; } + fclose(f2); + + char **entries = flat_directory_entries("/tmp/test_flat_dir2"); + ASSERT(entries != NULL, "entries returns non-NULL"); + if (!entries) { + unlink("/tmp/test_flat_dir2/alpha.txt"); + unlink("/tmp/test_flat_dir2/beta.txt"); + rmdir("/tmp/test_flat_dir2"); + return; + } + + int found_alpha = 0, found_beta = 0; + for (int64_t i = 0; entries[i]; i++) { + if (strcmp(entries[i], "alpha.txt") == 0) found_alpha = 1; + if (strcmp(entries[i], "beta.txt") == 0) found_beta = 1; + } + + ASSERT(found_alpha, "found alpha.txt"); + ASSERT(found_beta, "found beta.txt"); + + flat_free_entries(entries); + + unlink("/tmp/test_flat_dir2/alpha.txt"); + unlink("/tmp/test_flat_dir2/beta.txt"); + rmdir("/tmp/test_flat_dir2"); +} + +void test_entries_invalid() { + char **entries = flat_directory_entries("/nonexistent/path"); + ASSERT(entries == NULL, "invalid path returns NULL"); +} + +void test_free_entries_null() { + flat_free_entries(NULL); +} + +void test_entry_count_invalid() { + int64_t count = flat_directory_entry_count("/nonexistent/path"); + ASSERT(count == 0, "invalid path returns 0 count"); +} + +void test_path_with_empty() { + char *p = flat_path(3, "a", "", "c"); + ASSERT(p != NULL, "path with empty returns non-NULL"); + ASSERT(strcmp(p, "a//c") == 0, "path a//c with empty"); + free(p); +} + +int main() { + test_path(); + test_path_zero(); + test_entry_count(); + test_entries(); + test_entries_invalid(); + test_free_entries_null(); + test_entry_count_invalid(); + test_path_with_empty(); + + printf("test_flat: %d failures\n", failures); + return failures; +} diff --git a/tests/test_frontmatter.c b/tests/test_frontmatter.c new file mode 100644 index 0000000..af4c2fc --- /dev/null +++ b/tests/test_frontmatter.c @@ -0,0 +1,236 @@ +/* test_frontmatter.c — standalone test suite for frontmatter.h. + * Zero dependencies: compiles to a single TU with no linked libraries. */ +#include "frontmatter.h" + +#include +#include +#include + +static int failures = 0; +static int checks = 0; + +#define CHECK(cond, msg) \ + do { \ + checks++; \ + if (!(cond)) { \ + failures++; \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, (msg)); \ + } \ + } while (0) + +/* ------------------------------------------------------------------ */ +/* Frontmatter */ +/* ------------------------------------------------------------------ */ + +static void test_frontmatter_scalars_lists_quotes(void) { + doc d; + parse_frontmatter( + "---\nid: a-1\nname: \"A: One\"\nenabled: true\nn: 42\ntags: db, " + "arch\ntags2: [\"x\", \"y\"]\n---\nbody line", + &d); + CHECK(strcmp(d.body, "body line") == 0, "body"); + const fm_value *v = doc_get(&d, "id"); + CHECK(v && v->kind == FM_STR && strcmp(v->s, "a-1") == 0, "id"); + v = doc_get(&d, "name"); + CHECK(v && v->kind == FM_STR && strcmp(v->s, "A: One") == 0, "name"); + v = doc_get(&d, "enabled"); + CHECK(v && v->kind == FM_BOOL && v->b, "enabled"); + v = doc_get(&d, "n"); + CHECK(v && v->kind == FM_INT && v->i == 42, "n"); + v = doc_get(&d, "tags"); + CHECK(v, "tags present"); + fm_strlist items; + fm_list_items(v, &items); + CHECK(items.len == 2 && strcmp(items.items[0], "db") == 0 && + strcmp(items.items[1], "arch") == 0, + "tags csv"); + fm_sl_free(&items); + v = doc_get(&d, "tags2"); + CHECK(v && v->kind == FM_LIST && v->item_count == 2 && + strcmp(v->items[0], "x") == 0 && strcmp(v->items[1], "y") == 0, + "tags2 array"); + doc_free(&d); +} + +static void test_frontmatter_missing_or_unterminated(void) { + doc d; + parse_frontmatter("just a body\n", &d); + CHECK(d.count == 0, "no meta"); + CHECK(strcmp(d.body, "just a body") == 0, "body trimmed"); + doc_free(&d); + + parse_frontmatter("---\nid: x\nno closing fence\n", &d); + CHECK(d.count == 0, "unterminated: no meta"); + CHECK(strcmp(d.body, "---\nid: x\nno closing fence") == 0, + "unterminated: whole text is body"); + doc_free(&d); +} + +static void test_frontmatter_ignores_blank_comment_colonless(void) { + doc d; + parse_frontmatter("---\n# comment\n\nnotakey\nk: v\n---\n", &d); + CHECK(d.count == 1, "one meta entry"); + const fm_value *v = doc_get(&d, "k"); + CHECK(v && v->kind == FM_STR && strcmp(v->s, "v") == 0, "k=v"); + doc_free(&d); +} + +static void test_fm_list_items_variants(void) { + doc d; + parse_frontmatter( + "---\ncsv: a, b, c\njson: [\"x\", \"y\"]\nsingle: only\nempty:\n" + "nums: [1, 2]\n---\nbody", + &d); + fm_strlist out; + fm_list_items(doc_get(&d, "csv"), &out); + CHECK(out.len == 3 && strcmp(out.items[0], "a") == 0 && + strcmp(out.items[2], "c") == 0, + "csv"); + fm_sl_free(&out); + fm_list_items(doc_get(&d, "json"), &out); + CHECK(out.len == 2 && strcmp(out.items[0], "x") == 0, "json"); + fm_sl_free(&out); + fm_list_items(doc_get(&d, "single"), &out); + CHECK(out.len == 1 && strcmp(out.items[0], "only") == 0, "single"); + fm_sl_free(&out); + fm_list_items(doc_get(&d, "empty"), &out); + CHECK(out.len == 0, "empty list"); + fm_sl_free(&out); + fm_list_items(doc_get(&d, "nums"), &out); + CHECK(out.len == 2 && strcmp(out.items[0], "1") == 0, "non-string items"); + fm_sl_free(&out); + fm_list_items(NULL, &out); + CHECK(out.len == 0, "null value"); + fm_sl_free(&out); + doc_free(&d); +} + +/* ------------------------------------------------------------------ */ +/* Embedded JSON */ +/* ------------------------------------------------------------------ */ + +static void test_json_roundtrip(void) { + const char *src = + "{\"a\":1,\"b\":\"x\",\"c\":[true,null,1.5],\"d\":{\"e\":[1,2," + "\"z\"]}}"; + fm_json *v = fm_json_parse(src); + CHECK(v != NULL, "parse"); + CHECK(v && v->type == FMJ_OBJECT, "object"); + CHECK(v && v->count == 4, "4 keys"); + const fm_json *a = fm_json_get(v, "a"); + CHECK(a && a->type == FMJ_NUMBER && a->is_int && a->i == 1, "a=1"); + const fm_json *b = fm_json_get(v, "b"); + CHECK(b && b->type == FMJ_STRING && strcmp(b->str, "x") == 0, "b=x"); + const fm_json *c = fm_json_get(v, "c"); + CHECK(c && c->type == FMJ_ARRAY && c->count == 3, "c array"); + CHECK(c && c->items[0]->type == FMJ_BOOL && c->items[0]->b, "c[0]=true"); + CHECK(c && c->items[1]->type == FMJ_NULL, "c[1]=null"); + CHECK(c && c->items[2]->type == FMJ_NUMBER && c->items[2]->num == 1.5, + "c[2]=1.5"); + const fm_json *d = fm_json_get(v, "d"); + CHECK(d && d->type == FMJ_OBJECT, "d object"); + const fm_json *e = d ? fm_json_get(d, "e") : NULL; + CHECK(e && e->type == FMJ_ARRAY && e->count == 3, "d.e array"); + CHECK(e && e->items[1]->is_int && e->items[1]->i == 2, "d.e[1]=2"); + char *out = fm_json_serialize(v); + CHECK(out && strcmp(out, src) == 0, "roundtrip"); + free(out); + fm_json_free(v); +} + +static void test_json_escapes(void) { + fm_json *v = fm_json_parse("\"a\\\"b\\\\c\\nd\\u0041\""); + CHECK(v && v->type == FMJ_STRING, "parse"); + CHECK(v && strcmp(v->str, "a\"b\\c\ndA") == 0, "unescaped value"); + char *s = fm_json_serialize(v); + CHECK(s && strcmp(s, "\"a\\\"b\\\\c\\ndA\"") == 0, "escaped output"); + free(s); + fm_json_free(v); + + fm_json *u = fm_json_parse("\"\\u4e2d\""); + CHECK(u && u->type == FMJ_STRING, "unicode parse"); + CHECK(u && strcmp(u->str, "\xE4\xB8\xAD") == 0, "utf-8 out"); + fm_json_free(u); +} + +static void test_json_invalid(void) { + CHECK(fm_json_parse("{") == NULL, "open brace"); + CHECK(fm_json_parse("[1,") == NULL, "dangling comma"); + CHECK(fm_json_parse("{bad:1}") == NULL, "bare key"); + CHECK(fm_json_parse("{\"a\":1\"b\":2}") == NULL, "missing comma"); + CHECK(fm_json_parse("{}x") == NULL, "trailing garbage"); + CHECK(fm_json_parse("") == NULL, "empty"); + CHECK(fm_json_parse("01") == NULL, "leading zero"); + CHECK(fm_json_parse("1.") == NULL, "dangling dot"); + CHECK(fm_json_parse("-") == NULL, "bare minus"); + CHECK(fm_json_parse("tru") == NULL, "bare tru"); + CHECK(fm_json_parse("\"unterminated") == NULL, "unterminated string"); + CHECK(fm_json_parse("[1 2]") == NULL, "missing separator"); + fm_json *f1 = fm_json_parse("1.5"); + CHECK(f1 != NULL, "float ok"); + fm_json_free(f1); + fm_json *f2 = fm_json_parse("1e3"); + CHECK(f2 != NULL, "exponent ok"); + fm_json_free(f2); + fm_json *f3 = fm_json_parse("null"); + CHECK(f3 != NULL, "null ok"); + fm_json_free(f3); + fm_json *f4 = fm_json_parse("[]"); + CHECK(f4 != NULL, "empty array ok"); + fm_json_free(f4); + fm_json *f5 = fm_json_parse("{}"); + CHECK(f5 != NULL, "empty object ok"); + fm_json_free(f5); +} + +static void test_json_merge(void) { + fm_json *base = fm_json_parse("{\"a\":1,\"b\":{\"x\":1}}"); + fm_json *over = fm_json_parse("{\"b\":{\"y\":2},\"c\":3}"); + fm_json *m = fm_json_merge_owned(base, over); + char *s = fm_json_serialize(m); + CHECK(s && strcmp(s, "{\"b\":{\"y\":2},\"c\":3,\"a\":1}") == 0, + "merged content"); + free(s); + fm_json_free(m); + + fm_json *o2 = fm_json_parse("{\"z\":9}"); + fm_json *m2 = fm_json_merge_owned(NULL, o2); + char *s2 = fm_json_serialize(m2); + CHECK(s2 && strcmp(s2, "{\"z\":9}") == 0, "over only"); + free(s2); + fm_json_free(m2); + + fm_json *b2 = fm_json_parse("{\"q\":1}"); + fm_json *m3 = fm_json_merge_owned(b2, NULL); + char *s3 = fm_json_serialize(m3); + CHECK(s3 && strcmp(s3, "{\"q\":1}") == 0, "base only"); + free(s3); + fm_json_free(m3); + + fm_json *n1 = fm_json_parse("[1]"); + fm_json *n2 = fm_json_parse("\"x\""); + fm_json *m4 = fm_json_merge_owned(n1, n2); + char *s4 = fm_json_serialize(m4); + CHECK(s4 && strcmp(s4, "{}") == 0, "non-object inputs dropped"); + free(s4); + fm_json_free(m4); + + fm_json *m5 = fm_json_merge_owned(NULL, NULL); + char *s5 = fm_json_serialize(m5); + CHECK(s5 && strcmp(s5, "{}") == 0, "both null"); + free(s5); + fm_json_free(m5); +} + +int main(void) { + test_frontmatter_scalars_lists_quotes(); + test_frontmatter_missing_or_unterminated(); + test_frontmatter_ignores_blank_comment_colonless(); + test_fm_list_items_variants(); + test_json_roundtrip(); + test_json_escapes(); + test_json_invalid(); + test_json_merge(); + printf("%d checks, %d failures\n", checks, failures); + return failures ? 1 : 0; +} diff --git a/tests/test_genetic.c b/tests/test_genetic.c new file mode 100644 index 0000000..e7bb55b --- /dev/null +++ b/tests/test_genetic.c @@ -0,0 +1,158 @@ +// Tests for genetic.h - genetic algorithm generation step +typedef char *ga_individual_t; +#define GENETIC_IMPLEMENTATION +#include "../include/genetic.h" + +#include +#include +#include +#include +#include + +#define ASSERT(cond, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s (test_genetic.c:%d)\n", msg, __LINE__); \ + failures++; \ + } \ + } while (0) + +static int failures = 0; + +#define TARGET "HELLO" +#define POP_N 20 + +static void crossover_fn(ga_individual_t *child, ga_individual_t p1, ga_individual_t p2) { + if (!child || !p1 || !p2) + return; + uint32_t l1 = strlen(p1); + uint32_t l2 = strlen(p2); + if (l1 == 0 || l2 == 0) + return; + uint32_t c1 = rand() % l1; + uint32_t c2 = rand() % l2; + uint32_t len = c1 + (l2 - c2); + char *s = malloc(len + 1); + if (!s) { + free(*child); + *child = NULL; + return; + } + memcpy(s, p1, c1); + memcpy(s + c1, p2 + c2, l2 - c2); + s[len] = '\0'; + free(*child); + *child = s; +} + +static void mutation_fn(ga_individual_t *ind) { + if (!ind || !*ind) + return; + uint32_t l = strlen(*ind); + if (l == 0) + return; + (*ind)[rand() % l] = (char)(rand() % 26 + 'A'); +} + +static double fitness_fn(ga_individual_t ind, void const *data) { + if (!ind) + return DBL_MAX; + char const *target = (char const *)data; + double error = 0; + while (*ind && *target) { + error += abs((int)(*target++ - *ind++)); + } + while (*ind) error += abs((int)*ind++); + while (*target) error += abs((int)*target++); + return error; +} + +static void free_population(char *population[], int n) { + for (int i = 0; i < n; i++) + free(population[i]); +} + +void test_generation() { + srand(42); + char *population[POP_N]; + + for (int i = 0; i < POP_N; i++) { + uint32_t len = 4 + rand() % 3; + char *s = malloc(len + 1); + if (!s) { free_population(population, i); exit(1); } + for (uint32_t j = 0; j < len; j++) { + s[j] = (char)(rand() % 26 + 'A'); + } + s[len] = '\0'; + population[i] = s; + } + + double error = ga_generation(population, POP_N, 0.5, 1.0, fitness_fn, TARGET, + crossover_fn, mutation_fn); + + ASSERT(error >= 0, "error non-negative"); + + for (int i = POP_N - 1; i >= 0; i--) { + free(population[i]); + } +} + +void test_convergence() { + srand(42); + char *population[POP_N]; + + for (int i = 0; i < POP_N; i++) { + uint32_t len = strlen(TARGET); + char *s = malloc(len + 1); + if (!s) { free_population(population, i); exit(1); } + for (uint32_t j = 0; j < len; j++) { + s[j] = (char)(rand() % 26 + 'A'); + } + s[len] = '\0'; + population[i] = s; + } + + double error = 1; + int generations = 0; + while (error > 0 && generations < 500) { + error = ga_generation(population, POP_N, 0.5, 1.0, fitness_fn, TARGET, + crossover_fn, mutation_fn); + generations++; + } + + ASSERT(generations < 500, "converges within 500 generations"); + + for (int i = 0; i < POP_N; i++) { + free(population[i]); + } +} + +void test_selection_rate() { + srand(42); + char *population[POP_N]; + + for (int i = 0; i < POP_N; i++) { + char *s = malloc(6); + if (!s) { free_population(population, i); exit(1); } + strcpy(s, "HELLO"); + population[i] = s; + } + + double error = ga_generation(population, POP_N, 0.5, 0.1, fitness_fn, TARGET, + crossover_fn, mutation_fn); + ASSERT(error >= 0, "low mutation rate returns non-negative error"); + ASSERT(error == 0, "all-population-matching-target has zero error"); + + for (int i = 0; i < POP_N; i++) { + free(population[i]); + } +} + +int main() { + test_generation(); + test_convergence(); + test_selection_rate(); + + printf("test_genetic: %d failures\n", failures); + return failures; +} diff --git a/tests/test_interpolate.c b/tests/test_interpolate.c new file mode 100644 index 0000000..5e7166b --- /dev/null +++ b/tests/test_interpolate.c @@ -0,0 +1,112 @@ +// Tests for interpolate.h - interpolation and easing macros +#include +#include + +#include "../include/interpolate.h" + +#define EPS 1e-6 + +#define ASSERT(cond, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s (test_interpolate.c:%d)\n", msg, __LINE__); \ + failures++; \ + } \ + } while (0) + +#define ASSERT_FLOAT_EQ(a, b, msg) \ + do { \ + if (fabs((a) - (b)) > EPS) { \ + fprintf(stderr, "FAIL: %s (got %f, expected %f) (test_interpolate.c:%d)\n", \ + msg, (double)(a), (double)(b), __LINE__); \ + failures++; \ + } \ + } while (0) + +static int failures = 0; + +void test_flip() { + ASSERT_FLOAT_EQ(flip(0.0), 1.0, "flip(0)"); + ASSERT_FLOAT_EQ(flip(1.0), 0.0, "flip(1)"); + ASSERT_FLOAT_EQ(flip(0.5), 0.5, "flip(0.5)"); +} + +void test_smooth_start() { + ASSERT_FLOAT_EQ(smooth_start_2(0.0), 0.0, "smooth_start_2(0)"); + ASSERT_FLOAT_EQ(smooth_start_2(1.0), 1.0, "smooth_start_2(1)"); + ASSERT_FLOAT_EQ(smooth_start_2(0.5), 0.25, "smooth_start_2(0.5)"); + + ASSERT_FLOAT_EQ(smooth_start_3(0.0), 0.0, "smooth_start_3(0)"); + ASSERT_FLOAT_EQ(smooth_start_3(1.0), 1.0, "smooth_start_3(1)"); +} + +void test_smooth_stop() { + ASSERT_FLOAT_EQ(smooth_stop_2(0.0), 0.0, "smooth_stop_2(0)"); + ASSERT_FLOAT_EQ(smooth_stop_2(1.0), 1.0, "smooth_stop_2(1)"); +} + +void test_mix() { + ASSERT_FLOAT_EQ(mix(0.0, 10.0, 0.0), 0.0, "mix t=0"); + ASSERT_FLOAT_EQ(mix(0.0, 10.0, 1.0), 10.0, "mix t=1"); + ASSERT_FLOAT_EQ(mix(0.0, 10.0, 0.5), 5.0, "mix t=0.5"); +} + +void test_crossfade() { + ASSERT_FLOAT_EQ(crossfade(0.0, 10.0, 0.0), 0.0, "crossfade t=0"); + ASSERT_FLOAT_EQ(crossfade(0.0, 10.0, 1.0), 10.0, "crossfade t=1"); + ASSERT_FLOAT_EQ(crossfade(5.0, 10.0, 0.5), 7.5, "crossfade mid"); +} + +void test_scale() { + ASSERT_FLOAT_EQ(scale(10.0, 0.5), 5.0, "scale 10*0.5"); + ASSERT_FLOAT_EQ(scale(10.0, 1.0), 10.0, "scale 10*1"); + ASSERT_FLOAT_EQ(scale(10.0, 0.0), 0.0, "scale 10*0"); +} + +void test_reverse_scale() { + ASSERT_FLOAT_EQ(reverse_scale(10.0, 0.0), 10.0, "reverse_scale t=0"); + ASSERT_FLOAT_EQ(reverse_scale(10.0, 1.0), 0.0, "reverse_scale t=1"); + ASSERT_FLOAT_EQ(reverse_scale(10.0, 0.5), 5.0, "reverse_scale t=0.5"); +} + +void test_arch() { + ASSERT_FLOAT_EQ(arch_2(0.0), 0.0, "arch_2(0)"); + ASSERT_FLOAT_EQ(arch_2(1.0), 0.0, "arch_2(1)"); + ASSERT_FLOAT_EQ(arch_2(0.5), 0.25, "arch_2(0.5)"); +} + +void test_smooth_step() { + ASSERT_FLOAT_EQ(smooth_step_3(0.0), 0.0, "smooth_step_3(0)"); + ASSERT_FLOAT_EQ(smooth_step_3(1.0), 1.0, "smooth_step_3(1)"); +} + +void test_namespaced_macros() { + ASSERT_FLOAT_EQ(INTERP_FLIP(0.3), 0.7, "INTERP_FLIP"); + ASSERT_FLOAT_EQ(INTERP_SCALE(10.0, 0.4), 4.0, "INTERP_SCALE"); + ASSERT_FLOAT_EQ(INTERP_REVERSE_SCALE(10.0, 0.3), 7.0, "INTERP_REVERSE_SCALE"); + ASSERT_FLOAT_EQ(INTERP_MIX(0.0, 20.0, 0.5), 10.0, "INTERP_MIX"); + ASSERT_FLOAT_EQ(INTERP_CROSSFADE(0.0, 20.0, 0.25), 5.0, "INTERP_CROSSFADE"); + ASSERT_FLOAT_EQ(INTERP_SMOOTH_START_2(0.5), 0.25, "INTERP_SMOOTH_START_2"); + ASSERT_FLOAT_EQ(INTERP_SMOOTH_STOP_2(0.5), 0.75, "INTERP_SMOOTH_STOP_2"); + ASSERT_FLOAT_EQ(INTERP_SMOOTH_STEP_3(0.0), 0.0, "INTERP_SMOOTH_STEP_3(0)"); + ASSERT_FLOAT_EQ(INTERP_SMOOTH_STEP_3(1.0), 1.0, "INTERP_SMOOTH_STEP_3(1)"); + ASSERT_FLOAT_EQ(INTERP_ARCH_2(0.5), 0.25, "INTERP_ARCH_2"); + ASSERT_FLOAT_EQ(INTERP_SMOOTH_START_ARCH_3(0.5), 0.125, "INTERP_SMOOTH_START_ARCH_3"); + ASSERT_FLOAT_EQ(INTERP_SMOOTH_STOP_ARCH_3(0.5), 0.125, "INTERP_SMOOTH_STOP_ARCH_3"); +} + +int main() { + test_flip(); + test_smooth_start(); + test_smooth_stop(); + test_mix(); + test_crossfade(); + test_scale(); + test_reverse_scale(); + test_arch(); + test_smooth_step(); + test_namespaced_macros(); + + printf("test_interpolate: %d failures\n", failures); + return failures; +} diff --git a/tests/test_pool.c b/tests/test_pool.c new file mode 100644 index 0000000..2c75688 --- /dev/null +++ b/tests/test_pool.c @@ -0,0 +1,150 @@ +// Tests for pool.h - memory pool allocation and free-list operations +#define POOL_IMPLEMENTATION +#include "../include/pool.h" + +#include +#include +#include + +#define ASSERT(cond, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s (test_pool.c:%d)\n", msg, __LINE__); \ + failures++; \ + } \ + } while (0) + +static int failures = 0; + +void test_init_and_fini() { + pool_t *p = pool_init(32, 10); + ASSERT(p != NULL, "pool_init returns non-NULL"); + if (!p) return; + ASSERT(p->block_s == 32, "block size"); + ASSERT(p->block_n == 10, "block count"); + pool_fini(&p); +} + +void test_malloc_and_free() { + pool_t *p = pool_init(64, 5); + + void *b1 = pool_malloc(p); + void *b2 = pool_malloc(p); + void *b3 = pool_malloc(p); + + ASSERT(b1 != NULL, "malloc 1"); + ASSERT(b2 != NULL, "malloc 2"); + ASSERT(b3 != NULL, "malloc 3"); + ASSERT(b1 != b2, "malloc returns different blocks"); + ASSERT(pool_size(p) == 3, "size after 3 mallocs"); + + pool_free(p, b2); + ASSERT(pool_size(p) == 2, "size after 1 free"); + + pool_free(p, b1); + pool_free(p, b3); + ASSERT(pool_size(p) == 0, "size after all frees"); + + pool_fini(&p); +} + +void test_reuse_freed_block() { + pool_t *p = pool_init(32, 3); + + void *b1 = pool_malloc(p); + void *b2 = pool_malloc(p); + pool_free(p, b1); + + void *b3 = pool_malloc(p); + ASSERT(b3 == b1, "reuses freed block"); + + pool_free(p, b2); + pool_free(p, b3); + pool_fini(&p); +} + +void test_capacity_and_size() { + pool_t *p = pool_init(16, 20); + ASSERT(pool_capacity(p) == 20, "capacity"); + ASSERT(pool_size(p) == 0, "initial size is 0"); + + pool_malloc(p); + pool_malloc(p); + ASSERT(pool_size(p) == 2, "size after 2 mallocs"); + + pool_fini(&p); +} + +void test_exhaust_pool() { + pool_t *p = pool_init(16, 2); + pool_malloc(p); + pool_malloc(p); + void *overflow = pool_malloc(p); + ASSERT(overflow == NULL, "returns NULL when exhausted"); + + pool_fini(&p); +} + +void test_write_and_read_block() { + pool_t *p = pool_init(32, 1); + int32_t *block = pool_malloc(p); + ASSERT(block != NULL, "pool_malloc returns non-NULL"); + if (!block) { pool_fini(&p); return; } + *block = 42; + ASSERT(*block == 42, "can write and read block data"); + pool_fini(&p); +} + +void test_multiple_free_fini() { + pool_t *p = pool_init(16, 1); + pool_fini(&p); + pool_fini(&p); + pool_fini(NULL); +} + +void test_free_invalid_pointer() { + pool_t *p = pool_init(32, 3); + void *b1 = pool_malloc(p); + ASSERT(pool_size(p) == 1, "size before invalid free"); + + int stack_var; + pool_free(p, &stack_var); + ASSERT(pool_size(p) == 1, "invalid free rejected (stack ptr)"); + + pool_free(p, (void *)0x12345678); + ASSERT(pool_size(p) == 1, "invalid free rejected (random ptr)"); + + pool_free(p, NULL); + ASSERT(pool_size(p) == 1, "invalid free rejected (NULL)"); + + pool_free(NULL, b1); + ASSERT(pool_size(p) == 1, "invalid free rejected (NULL pool)"); + + pool_free(p, b1); + ASSERT(pool_size(p) == 0, "valid free accepted"); + + pool_fini(&p); +} + +void test_init_invalid_block_size() { + pool_t *p = pool_init(3, 10); + ASSERT(p == NULL, "block size < sizeof(void*) returns NULL"); + + p = pool_init(7, 10); + ASSERT(p == NULL, "block size not multiple of sizeof(void*) returns NULL"); +} + +int main() { + test_init_and_fini(); + test_malloc_and_free(); + test_reuse_freed_block(); + test_capacity_and_size(); + test_exhaust_pool(); + test_write_and_read_block(); + test_multiple_free_fini(); + test_free_invalid_pointer(); + test_init_invalid_block_size(); + + printf("test_pool: %d failures\n", failures); + return failures; +} diff --git a/tests/test_pushdown.c b/tests/test_pushdown.c new file mode 100644 index 0000000..7e2e18a --- /dev/null +++ b/tests/test_pushdown.c @@ -0,0 +1,442 @@ +// Tests for pushdown.h - pushdown automaton instruction execution +#include +typedef double stack_value_t; +#define STACK_IMPLEMENTATION +#include "../include/stack.h" + +typedef double pushdown_value_t; +#define PUSHDOWN_IMPLEMENTATION +#include "../include/pushdown.h" + +#include + +#define EPS 1e-9 + +#define ASSERT(cond, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s (test_pushdown.c:%d)\n", msg, __LINE__); \ + failures++; \ + } \ + } while (0) + +#define ASSERT_FLOAT_EQ(a, b, msg) \ + do { \ + if (fabs((a) - (b)) > EPS) { \ + fprintf(stderr, "FAIL: %s (got %f, expected %f) (test_pushdown.c:%d)\n", \ + msg, (double)(a), (double)(b), __LINE__); \ + failures++; \ + } \ + } while (0) + +static int failures = 0; + +void setup_stacks(stack_t *input, stack_t *output) { + *input = (stack_t){.data = NULL, .size = 0, .capacity = 16}; + *output = (stack_t){.data = NULL, .size = 0, .capacity = 16}; + stack_init(input); + stack_init(output); +} + +void teardown_stacks(stack_t *input, stack_t *output) { + stack_free(input); + stack_free(output); +} + +void test_push() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, 5); + pushdown_instruction_t ops[] = {PUSH, END}; + pushdown_run(&output, &input, ops); + + ASSERT(output.size == 1, "push size"); + ASSERT_FLOAT_EQ(stack_peek(&output), 5, "push value"); + + teardown_stacks(&input, &output); +} + +void test_pop() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&output, 5); + pushdown_instruction_t ops[] = {POP, END}; + pushdown_run(&output, &input, ops); + + ASSERT(output.size == 0, "pop size"); + + teardown_stacks(&input, &output); +} + +void test_skip() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, 5); + stack_push(&input, 10); + pushdown_instruction_t ops[] = {SKIP, PUSH, END}; + pushdown_run(&output, &input, ops); + + ASSERT(output.size == 1, "skip+push size"); + ASSERT_FLOAT_EQ(stack_peek(&output), 5, "skip top(10), push next(5)"); + + teardown_stacks(&input, &output); +} + +void test_add() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, 3); + stack_push(&input, 4); + pushdown_instruction_t ops[] = {PUSH, PUSH, ADD, END}; + pushdown_run(&output, &input, ops); + + ASSERT(output.size == 1, "add size"); + ASSERT_FLOAT_EQ(stack_peek(&output), 7, "add value"); + + teardown_stacks(&input, &output); +} + +void test_sub() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, 10); + stack_push(&input, 3); + pushdown_instruction_t ops[] = {PUSH, PUSH, SUB, END}; + pushdown_run(&output, &input, ops); + + ASSERT(output.size == 1, "sub size"); + ASSERT_FLOAT_EQ(stack_peek(&output), 7, "sub value"); + + teardown_stacks(&input, &output); +} + +void test_mul() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, 3); + stack_push(&input, 4); + pushdown_instruction_t ops[] = {PUSH, PUSH, MUL, END}; + pushdown_run(&output, &input, ops); + + ASSERT(output.size == 1, "mul size"); + ASSERT_FLOAT_EQ(stack_peek(&output), 12, "mul value"); + + teardown_stacks(&input, &output); +} + +void test_div() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, 10); + stack_push(&input, 2); + pushdown_instruction_t ops[] = {PUSH, PUSH, DIV, END}; + pushdown_run(&output, &input, ops); + + ASSERT(output.size == 1, "div size"); + ASSERT_FLOAT_EQ(stack_peek(&output), 5, "div value"); + + teardown_stacks(&input, &output); +} + +void test_div_zero() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, 0); + stack_push(&input, 10); + pushdown_instruction_t ops[] = {PUSH, PUSH, DIV, END}; + pushdown_run(&output, &input, ops); + + ASSERT(output.size == 1, "div zero size"); + ASSERT_FLOAT_EQ(stack_peek(&output), 0, "div zero result is 0"); + + teardown_stacks(&input, &output); +} + +void test_recip() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, 4); + pushdown_instruction_t ops[] = {PUSH, RECIP, END}; + pushdown_run(&output, &input, ops); + + ASSERT(output.size == 1, "recip size"); + ASSERT_FLOAT_EQ(stack_peek(&output), 0.25, "recip value"); + + teardown_stacks(&input, &output); +} + +void test_inc() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, 5); + pushdown_instruction_t ops[] = {PUSH, INC, END}; + pushdown_run(&output, &input, ops); + + ASSERT_FLOAT_EQ(stack_peek(&output), 6, "inc value"); + + teardown_stacks(&input, &output); +} + +void test_dec() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, 5); + pushdown_instruction_t ops[] = {PUSH, DEC, END}; + pushdown_run(&output, &input, ops); + + ASSERT_FLOAT_EQ(stack_peek(&output), 4, "dec value"); + + teardown_stacks(&input, &output); +} + +void test_sign() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, -5); + pushdown_instruction_t ops[] = {PUSH, SIGN, END}; + pushdown_run(&output, &input, ops); + ASSERT_FLOAT_EQ(stack_peek(&output), -1, "sign negative"); + teardown_stacks(&input, &output); + + setup_stacks(&input, &output); + stack_push(&input, 5); + pushdown_run(&output, &input, ops); + ASSERT_FLOAT_EQ(stack_peek(&output), 1, "sign positive"); + teardown_stacks(&input, &output); + + setup_stacks(&input, &output); + stack_push(&input, 0); + pushdown_run(&output, &input, ops); + ASSERT_FLOAT_EQ(stack_peek(&output), 0, "sign zero"); + teardown_stacks(&input, &output); +} + +void test_neg() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, 5); + pushdown_instruction_t ops[] = {PUSH, NEG, END}; + pushdown_run(&output, &input, ops); + ASSERT_FLOAT_EQ(stack_peek(&output), -5, "neg value"); + + teardown_stacks(&input, &output); +} + +void test_sin() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, 0); + pushdown_instruction_t ops[] = {PUSH, SIN, END}; + pushdown_run(&output, &input, ops); + ASSERT_FLOAT_EQ(stack_peek(&output), 0, "sin(0)"); + + teardown_stacks(&input, &output); +} + +void test_sqrt() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, 9); + pushdown_instruction_t ops[] = {PUSH, SQRT, END}; + pushdown_run(&output, &input, ops); + ASSERT_FLOAT_EQ(stack_peek(&output), 3, "sqrt(9)"); + + teardown_stacks(&input, &output); +} + +void test_abs() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, -5); + pushdown_instruction_t ops[] = {PUSH, ABS, END}; + pushdown_run(&output, &input, ops); + ASSERT_FLOAT_EQ(stack_peek(&output), 5, "abs(-5)"); + + teardown_stacks(&input, &output); +} + +void test_floor() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, 3.7); + pushdown_instruction_t ops[] = {PUSH, FLOOR, END}; + pushdown_run(&output, &input, ops); + ASSERT_FLOAT_EQ(stack_peek(&output), 3, "floor(3.7)"); + + teardown_stacks(&input, &output); +} + +void test_ceil() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, 3.2); + pushdown_instruction_t ops[] = {PUSH, CEIL, END}; + pushdown_run(&output, &input, ops); + ASSERT_FLOAT_EQ(stack_peek(&output), 4, "ceil(3.2)"); + + teardown_stacks(&input, &output); +} + +void test_copy() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, 42); + pushdown_instruction_t ops[] = {PUSH, COPY, END}; + pushdown_run(&output, &input, ops); + + ASSERT(output.size == 2, "copy size"); + + teardown_stacks(&input, &output); +} + +void test_noop() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, 5); + pushdown_instruction_t ops[] = {NOOP, END}; + pushdown_run(&output, &input, ops); + + ASSERT(output.size == 0, "noop no change"); + ASSERT(input.size == 1, "noop input unchanged"); + + teardown_stacks(&input, &output); +} + +void test_sqrt_negative() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, -1); + pushdown_instruction_t ops[] = {PUSH, SQRT, END}; + pushdown_run(&output, &input, ops); + + ASSERT(output.size == 1, "sqrt negative pushes 0"); + ASSERT_FLOAT_EQ(stack_peek(&output), 0, "sqrt negative result is 0"); + + teardown_stacks(&input, &output); +} + +void test_pow() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, 2); + stack_push(&input, 3); + pushdown_instruction_t ops[] = {PUSH, PUSH, POW, END}; + pushdown_run(&output, &input, ops); + + ASSERT(output.size == 1, "pow size"); + ASSERT_FLOAT_EQ(stack_peek(&output), 8, "pow value 2^3"); + + teardown_stacks(&input, &output); +} + +void test_log() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, 1); + pushdown_instruction_t ops[] = {PUSH, LOG, END}; + pushdown_run(&output, &input, ops); + ASSERT_FLOAT_EQ(stack_peek(&output), 0, "log(1)"); + + teardown_stacks(&input, &output); +} + +void test_complex_program() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, 3); + stack_push(&input, 4); + pushdown_instruction_t ops[] = {PUSH, PUSH, MUL, INC, END}; + pushdown_run(&output, &input, ops); + + ASSERT_FLOAT_EQ(stack_peek(&output), 13, "3*4+1"); + + teardown_stacks(&input, &output); +} + +void test_minmax() { + stack_t input, output; + setup_stacks(&input, &output); + + stack_push(&input, 3); + stack_push(&input, 7); + pushdown_instruction_t ops[] = {PUSH, PUSH, MINMAX, END}; + pushdown_run(&output, &input, ops); + + ASSERT(output.size == 2, "minmax size is 2"); + ASSERT_FLOAT_EQ(output.data[0], 3, "minmax min value"); + ASSERT_FLOAT_EQ(output.data[1], 7, "minmax max value"); + + teardown_stacks(&input, &output); +} + +void test_validate_minmax() { + pushdown_instruction_t valid[] = {PUSH, MINMAX, END}; + ASSERT(pushdown_validate(valid), "validate accepts MINMAX"); + + pushdown_instruction_t invalid[] = {PUSH, (pushdown_instruction_t)999, END}; + ASSERT(!pushdown_validate(invalid), "validate rejects out-of-range"); +} + +void test_instruction_count_bounds() { + pushdown_instruction_t ops[] = {PUSH, ADD, (pushdown_instruction_t)999, END}; + ASSERT(pushdown_instruction_count(ops) == 2, "stops at invalid instruction"); + + ASSERT(pushdown_instruction_count(NULL) == 0, "NULL returns 0"); +} + +int main() { + test_push(); + test_pop(); + test_skip(); + test_add(); + test_sub(); + test_mul(); + test_div(); + test_div_zero(); + test_recip(); + test_inc(); + test_dec(); + test_sign(); + test_neg(); + test_sin(); + test_sqrt(); + test_abs(); + test_floor(); + test_ceil(); + test_copy(); + test_noop(); + test_sqrt_negative(); + test_pow(); + test_log(); + test_complex_program(); + test_minmax(); + test_validate_minmax(); + test_instruction_count_bounds(); + + printf("test_pushdown: %d failures\n", failures); + return failures; +} diff --git a/tests/test_stack.c b/tests/test_stack.c new file mode 100644 index 0000000..8fc19a3 --- /dev/null +++ b/tests/test_stack.c @@ -0,0 +1,234 @@ +// Tests for stack.h - stack push, pop, peek, and resizing operations +#include +#include + +typedef double stack_value_t; + +#define STACK_IMPLEMENTATION +#include "../include/stack.h" + +#define EPS 1e-9 + +#define ASSERT(cond, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s (test_stack.c:%d)\n", msg, __LINE__); \ + failures++; \ + } \ + } while (0) + +#define ASSERT_FLOAT_EQ(a, b, msg) \ + do { \ + if (fabs((a) - (b)) > EPS) { \ + fprintf(stderr, "FAIL: %s (got %f, expected %f) (test_stack.c:%d)\n", \ + msg, (double)(a), (double)(b), __LINE__); \ + failures++; \ + } \ + } while (0) + +static int failures = 0; + +void test_init_and_free() { + stack_t s = {.capacity = 10}; + stack_init(&s); + ASSERT(s.size == 0, "init size should be 0"); + ASSERT(s.capacity == 10, "init capacity should be 10"); + ASSERT(s.data != NULL, "init data should not be NULL"); + stack_free(&s); +} + +void test_default_capacity() { + stack_t s = {0}; + stack_init(&s); + ASSERT(s.capacity == 16, "default capacity should be 16"); + stack_free(&s); +} + +void test_push_pop() { + stack_t s = {0}; + stack_init(&s); + + for (int i = 0; i < 5; i++) { + stack_push(&s, (stack_value_t)(i * 10)); + } + + ASSERT(s.size == 5, "size after 5 pushes"); + ASSERT_FLOAT_EQ(stack_pop(&s), 40, "pop last pushed"); + ASSERT_FLOAT_EQ(stack_pop(&s), 30, "pop second last"); + ASSERT(s.size == 3, "size after 2 pops"); + + stack_free(&s); +} + +void test_peek() { + stack_t s = {0}; + stack_init(&s); + stack_push(&s, 42); + + ASSERT_FLOAT_EQ(stack_peek(&s), 42, "peek returns top"); + ASSERT(s.size == 1, "peek does not change size"); + + stack_free(&s); +} + +void test_peek_empty() { + stack_t s = {0}; + stack_init(&s); + ASSERT_FLOAT_EQ(stack_peek(&s), 0, "peek empty returns ZERO"); + stack_free(&s); +} + +void test_pop_empty() { + stack_t s = {0}; + stack_init(&s); + ASSERT_FLOAT_EQ(stack_pop(&s), 0, "pop empty returns ZERO"); + stack_free(&s); +} + +void test_grow() { + stack_t s = {.capacity = 2}; + stack_init(&s); + + for (int i = 0; i < 10; i++) { + stack_push(&s, (stack_value_t)i); + } + + ASSERT(s.size == 10, "size after growing"); + ASSERT(s.capacity >= 10, "capacity grew"); + + for (int i = 9; i >= 0; i--) { + ASSERT_FLOAT_EQ(stack_pop(&s), (stack_value_t)i, "values preserved after grow"); + } + + stack_free(&s); +} + +void test_clear() { + stack_t s = {0}; + stack_init(&s); + stack_push(&s, 1); + stack_push(&s, 2); + stack_push(&s, 3); + + stack_clear(&s); + ASSERT_FLOAT_EQ(s.data[0], 0, "clear zeroed data[0]"); + ASSERT_FLOAT_EQ(s.data[1], 0, "clear zeroed data[1]"); + ASSERT_FLOAT_EQ(s.data[2], 0, "clear zeroed data[2]"); + + stack_free(&s); +} + +void test_from_array() { + stack_value_t arr[] = {1, 2, 3, 4, 5}; + stack_t s = {0}; + stack_from_array(&s, arr, 5); + + ASSERT(s.size == 5, "from_array size"); + ASSERT(s.capacity == 5, "from_array capacity"); + + for (int i = 4; i >= 0; i--) { + ASSERT_FLOAT_EQ(stack_pop(&s), (stack_value_t)(i + 1), "from_array values"); + } + + stack_free(&s); +} + +void test_empty() { + stack_t s = {0}; + stack_init(&s); + ASSERT(stack_empty(&s) == 1, "empty stack returns true"); + stack_push(&s, 42); + ASSERT(stack_empty(&s) == 0, "non-empty stack returns false"); + stack_free(&s); +} + +void test_from_array_single() { + stack_value_t arr[] = {42}; + stack_t s = {0}; + stack_from_array(&s, arr, 1); + + ASSERT(s.size == 1, "from_array single size"); + ASSERT(s.capacity == 1, "from_array single capacity"); + ASSERT_FLOAT_EQ(stack_pop(&s), 42, "from_array single value"); + + stack_free(&s); +} + +void test_multiple_free() { + stack_t s = {0}; + stack_init(&s); + stack_free(&s); + ASSERT(s.data == NULL, "data is NULL after free"); + ASSERT(s.size == 0, "size is 0 after free"); + ASSERT(s.capacity == 0, "capacity is 0 after free"); + stack_free(&s); + ASSERT(s.data == NULL, "second free is safe"); +} + +void test_pop_ok() { + stack_t s = {0}; + stack_init(&s); + + stack_value_t out = 0; + ASSERT(!stack_pop_ok(&s, &out), "pop_ok on empty returns 0"); + + stack_push(&s, 42); + ASSERT(stack_pop_ok(&s, &out), "pop_ok on non-empty returns 1"); + ASSERT_FLOAT_EQ(out, 42, "pop_ok value"); + ASSERT(s.size == 0, "pop_ok decrements size"); + + stack_free(&s); +} + +void test_peek_ok() { + stack_t s = {0}; + stack_init(&s); + + stack_value_t out = 0; + ASSERT(!stack_peek_ok(&s, &out), "peek_ok on empty returns 0"); + + stack_push(&s, 99); + ASSERT(stack_peek_ok(&s, &out), "peek_ok on non-empty returns 1"); + ASSERT_FLOAT_EQ(out, 99, "peek_ok value"); + ASSERT(s.size == 1, "peek_ok does not change size"); + + stack_free(&s); +} + +void test_pop_ok_null_out() { + stack_t s = {0}; + stack_init(&s); + stack_push(&s, 1); + ASSERT(!stack_pop_ok(&s, NULL), "pop_ok with NULL out returns 0"); + stack_free(&s); +} + +void test_peek_ok_null_out() { + stack_t s = {0}; + stack_init(&s); + stack_push(&s, 1); + ASSERT(!stack_peek_ok(&s, NULL), "peek_ok with NULL out returns 0"); + stack_free(&s); +} + +int main() { + test_init_and_free(); + test_default_capacity(); + test_push_pop(); + test_peek(); + test_peek_empty(); + test_pop_empty(); + test_pop_ok(); + test_peek_ok(); + test_pop_ok_null_out(); + test_peek_ok_null_out(); + test_grow(); + test_clear(); + test_from_array(); + test_from_array_single(); + test_empty(); + test_multiple_free(); + + printf("test_stack: %d failures\n", failures); + return failures; +} diff --git a/tests/test_ufo.c b/tests/test_ufo.c new file mode 100644 index 0000000..810c7cf --- /dev/null +++ b/tests/test_ufo.c @@ -0,0 +1,143 @@ +// Tests for ufo.h - Universal Function Originator evaluation and mutation +#include +#define UFO_IMPLEMENTATION +#include "../include/ufo.h" + +#include + +#define EPS 1e-6 + +#define ASSERT(cond, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s (test_ufo.c:%d)\n", msg, __LINE__); \ + failures++; \ + } \ + } while (0) + +#define ASSERT_FLOAT_EQ(a, b, msg) \ + do { \ + if (fabs((a) - (b)) > EPS) { \ + fprintf(stderr, "FAIL: %s (got %f, expected %f) (test_ufo.c:%d)\n", \ + msg, (double)(a), (double)(b), __LINE__); \ + failures++; \ + } \ + } while (0) + +static int failures = 0; + +void test_init() { + ufo_t *ufo = ufo_init(2, 3); + ASSERT(ufo != NULL, "init returns non-NULL"); + ASSERT(ufo->n == 2, "input count"); + ASSERT(ufo->v == 3, "block count"); + ASSERT(ufo->a != NULL, "weights allocated"); + ASSERT(ufo->b != NULL, "exponents allocated"); + ASSERT(ufo->w != NULL, "external weights allocated"); + ASSERT(ufo->c != NULL, "external exponents allocated"); + ASSERT(ufo->f != NULL, "unary functions allocated"); + ASSERT(ufo->fe != NULL, "external binary functions allocated"); + ASSERT(ufo->fi != NULL, "internal binary functions allocated"); + ufo_free(ufo); +} + +void test_init_defaults() { + ufo_t *ufo = ufo_init(1, 2); + ASSERT(ufo != NULL, "init returns non-NULL for init_defaults"); + if (!ufo) + return; + + ASSERT_FLOAT_EQ(ufo->w[2], 0, "bias is zero"); + + for (uint32_t j = 0; j < ufo->v; j++) { + ASSERT_FLOAT_EQ(ufo_ajk(ufo, j, ufo->n), 0, "bias weight is zero"); + } + + ufo_free(ufo); +} + +void test_evaluate_default() { + ufo_t *ufo = ufo_init(2, 3); + f64 x[] = {1.0, 2.0}; + f64 result = ufo_evaluate(ufo, x); + ASSERT(isfinite(result), "evaluate returns finite value"); + ASSERT(result > 0, "evaluate returns positive value with default weights"); + ufo_free(ufo); +} + +void test_evaluate_multiple_inputs() { + ufo_t *ufo = ufo_init(3, 2); + f64 x[] = {1.0, 2.0, 3.0}; + f64 result = ufo_evaluate(ufo, x); + ASSERT(isfinite(result), "multi-input evaluate returns finite"); + ASSERT(result > 0, "multi-input evaluate returns positive"); + ufo_free(ufo); +} + +void test_free() { + ufo_t *ufo = ufo_init(2, 5); + ASSERT(ufo != NULL, "init returns non-NULL before free"); + ufo_free(ufo); +} + +void test_different_configs() { + ufo_t *u2 = ufo_init(5, 10); + ufo_t *u3 = ufo_init(10, 2); + ASSERT(u2 != NULL, "init config 2"); + ASSERT(u3 != NULL, "init config 3"); + + f64 x2[5] = {1, 2, 3, 4, 5}; + f64 x3[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + f64 r2 = ufo_evaluate(u2, x2); + f64 r3 = ufo_evaluate(u3, x3); + + ASSERT(isfinite(r2), "config 2 returns finite"); + ASSERT(isfinite(r3), "config 3 returns finite"); + ASSERT(r2 != r3, "different configs produce different results"); + + ufo_free(u2); + ufo_free(u3); +} + +void test_weight_modification() { + ufo_t *ufo = ufo_init(2, 3); + ASSERT(ufo != NULL, "ufo_init returns non-NULL"); + if (!ufo) return; + + f64 x[] = {3.0, 4.0}; + f64 result_before = ufo_evaluate(ufo, x); + + ufo_ajk(ufo, 0, 0) = 0.0; + ufo->w[0] = 0.0; + ufo->w[1] = 0.0; + ufo->w[2] = 0.0; + + f64 result_after = ufo_evaluate(ufo, x); + ASSERT(isfinite(result_after), "modified weights evaluate finite"); + ASSERT(result_before != result_after, "modified weights change result"); + + ufo_free(ufo); +} + +void test_large_ufo() { + ufo_t *ufo = ufo_init(4, 50); + f64 x[4] = {0.1, 0.2, 0.3, 0.4}; + f64 result = ufo_evaluate(ufo, x); + ASSERT(isfinite(result), "large UFO returns finite"); + ufo_free(ufo); +} + +int main() { + test_init(); + test_init_defaults(); + test_evaluate_default(); + test_evaluate_multiple_inputs(); + test_free(); + test_different_configs(); + test_weight_modification(); + test_large_ufo(); + + printf("test_ufo: %d failures\n", failures); + return failures; +} diff --git a/tests/test_uri.c b/tests/test_uri.c new file mode 100644 index 0000000..a768c8c --- /dev/null +++ b/tests/test_uri.c @@ -0,0 +1,300 @@ +// Tests for uri.h - URI parsing and serialization +#define URI_IMPLEMENTATION +#include "../include/uri.h" + +#include +#include +#include + +#define ASSERT(cond, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s (test_uri.c:%d)\n", msg, __LINE__); \ + failures++; \ + } \ + } while (0) + +#define ASSERT_STR_EQ(a, b, msg) \ + do { \ + if ((a) == NULL || (b) == NULL || strcmp((a), (b)) != 0) { \ + fprintf(stderr, "FAIL: %s (got '%s', expected '%s') (test_uri.c:%d)\n", \ + msg, (a) ? (a) : "NULL", (b) ? (b) : "NULL", __LINE__); \ + failures++; \ + } \ + } while (0) + +static int failures = 0; + +void test_init() { + uri_t *u = uri_init(); + ASSERT(u != NULL, "init returns non-NULL"); + for (int i = 0; i < URI_END; i++) { + ASSERT(u->data[i] == NULL, "all fields NULL"); + } + uri_free(u); +} + +void test_set_and_get() { + uri_t *u = uri_init(); + + uri_set(u, SCHEME, "https"); + uri_set(u, HOST, "example.com"); + uri_set(u, PATH, "/api/v1"); + + char *uri = uri_get(u); + ASSERT(uri != NULL, "get returns non-NULL"); + ASSERT_STR_EQ(uri, "https://example.com/api/v1", "constructed URI"); + free(uri); + + uri_free(u); +} + +void test_set_with_port() { + uri_t *u = uri_init(); + + uri_set(u, SCHEME, "http"); + uri_set(u, HOST, "localhost"); + uri_set(u, PORT, "8080"); + + char *uri = uri_get(u); + ASSERT_STR_EQ(uri, "http://localhost:8080", "URI with port"); + free(uri); + + uri_free(u); +} + +void test_set_with_query() { + uri_t *u = uri_init(); + + uri_set(u, SCHEME, "https"); + uri_set(u, HOST, "example.com"); + uri_set(u, PATH, "/search"); + uri_set(u, QUERY, "q=test"); + + char *uri = uri_get(u); + ASSERT_STR_EQ(uri, "https://example.com/search?q=test", "URI with query"); + free(uri); + + uri_free(u); +} + +void test_set_with_fragment() { + uri_t *u = uri_init(); + + uri_set(u, SCHEME, "https"); + uri_set(u, HOST, "example.com"); + uri_set(u, PATH, "/page"); + uri_set(u, FRAGMENT, "section"); + + char *uri = uri_get(u); + ASSERT_STR_EQ(uri, "https://example.com/page#section", "URI with fragment"); + free(uri); + + uri_free(u); +} + +void test_full_uri() { + uri_t *u = uri_init(); + + uri_set(u, SCHEME, "https"); + uri_set(u, USERINFO, "user:pass"); + uri_set(u, HOST, "example.com"); + uri_set(u, PORT, "443"); + uri_set(u, PATH, "/path/to/resource"); + uri_set(u, QUERY, "key=value"); + uri_set(u, FRAGMENT, "anchor"); + + char *uri = uri_get(u); + ASSERT(uri != NULL, "full URI constructed"); + + uri_free(u); + free(uri); +} + +void test_parse_basic() { + uri_t *u = uri_parse("https://example.com/path"); + ASSERT(u != NULL, "parse returns non-NULL"); + + ASSERT_STR_EQ(u->data[SCHEME], "https", "parsed scheme"); + ASSERT_STR_EQ(u->data[HOST], "example.com", "parsed host"); + ASSERT_STR_EQ(u->data[PATH], "/path", "parsed path"); + + uri_free(u); +} + +void test_parse_with_port() { + uri_t *u = uri_parse("http://localhost:8080/api"); + ASSERT(u != NULL, "parse with port"); + + ASSERT_STR_EQ(u->data[SCHEME], "http", "scheme"); + ASSERT_STR_EQ(u->data[HOST], "localhost", "host"); + ASSERT_STR_EQ(u->data[PORT], "8080", "port"); + ASSERT_STR_EQ(u->data[PATH], "/api", "path"); + + uri_free(u); +} + +void test_parse_with_query() { + uri_t *u = uri_parse("https://example.com/search?q=hello"); + ASSERT(u != NULL, "parse with query"); + + ASSERT_STR_EQ(u->data[QUERY], "q=hello", "query parsed"); + + uri_free(u); +} + +void test_parse_with_fragment() { + uri_t *u = uri_parse("https://example.com/page#top"); + ASSERT(u != NULL, "parse with fragment"); + + ASSERT_STR_EQ(u->data[FRAGMENT], "top", "fragment parsed"); + + uri_free(u); +} + +void test_parse_full() { + uri_t *u = uri_parse("https://user:pass@example.com:443/path?q=1#frag"); + ASSERT(u != NULL, "parse full URI"); + + ASSERT_STR_EQ(u->data[SCHEME], "https", "scheme"); + ASSERT_STR_EQ(u->data[USERINFO], "user:pass", "userinfo parsed"); + ASSERT_STR_EQ(u->data[HOST], "example.com", "host parsed"); + ASSERT_STR_EQ(u->data[PORT], "443", "port parsed"); + ASSERT_STR_EQ(u->data[PATH], "/path", "path"); + ASSERT_STR_EQ(u->data[QUERY], "q=1", "query"); + ASSERT_STR_EQ(u->data[FRAGMENT], "frag", "fragment"); + + uri_free(u); +} + +void test_parse_invalid() { + uri_t *u = uri_parse("not-a-uri"); + ASSERT(u == NULL, "invalid URI returns NULL"); +} + +void test_set_override() { + uri_t *u = uri_init(); + + uri_set(u, SCHEME, "http"); + uri_set(u, HOST, "old.com"); + + uri_set(u, SCHEME, "https"); + uri_set(u, HOST, "new.com"); + + char *uri = uri_get(u); + ASSERT_STR_EQ(uri, "https://new.com", "override works"); + free(uri); + + uri_free(u); +} + +void test_roundtrip() { + char const *original = "https://example.com:8080/path?q=test#anchor"; + uri_t *u = uri_parse(original); + ASSERT(u != NULL, "parse for roundtrip"); + if (!u) + return; + + char *rebuilt = uri_get(u); + ASSERT(rebuilt != NULL, "rebuilt URI"); + if (!rebuilt) { + uri_free(u); + return; + } + + ASSERT(strcmp(rebuilt, original) == 0, "roundtrip matches original"); + + uri_free(u); + free(rebuilt); +} + +void test_minimal_uri() { + uri_t *u = uri_init(); + uri_set(u, SCHEME, "file"); + uri_set(u, HOST, "/tmp/file.txt"); + + char *uri = uri_get(u); + ASSERT(uri != NULL, "minimal URI"); + free(uri); + + uri_free(u); +} + +void test_set_null() { + uri_t *u = uri_init(); + uri_set(u, SCHEME, "http"); + uri_set(u, HOST, "example.com"); + uri_set(u, HOST, NULL); + + char *uri = uri_get(u); + ASSERT(uri != NULL, "uri with NULL host"); + ASSERT(strcmp(uri, "http:") == 0, "uri is just scheme"); + free(uri); + + uri_free(u); +} + +void test_remove_element() { + uri_t *u = uri_init(); + uri_set(u, SCHEME, "https"); + uri_set(u, HOST, "example.com"); + uri_set(u, PATH, "/path"); + + char *removed = uri_remove(u, SCHEME); + ASSERT(removed != NULL, "remove returns old value"); + ASSERT(strcmp(removed, "https") == 0, "removed value matches"); + free(removed); + + char *uri = uri_get(u); + ASSERT(uri != NULL, "uri after remove"); + ASSERT(strcmp(uri, "//example.com/path") == 0, "uri without scheme"); + free(uri); + + uri_free(u); +} + +void test_null_guards() { + uri_set(NULL, SCHEME, "http"); + uri_print(NULL); + uri_print_elements(NULL); + ASSERT(uri_get(NULL) == NULL, "uri_get(NULL) returns NULL"); + ASSERT(uri_remove(NULL, HOST) == NULL, "uri_remove(NULL) returns NULL"); + ASSERT(uri_parse(NULL) == NULL, "uri_parse(NULL) returns NULL"); +} + +void test_bounds_guards() { + uri_t *u = uri_init(); + + uri_set(u, (uri_element_t)999, "http"); + ASSERT(u->data[SCHEME] == NULL, "out-of-bounds set is no-op"); + + char *r = uri_remove(u, (uri_element_t)-1); + ASSERT(r == NULL, "negative index remove returns NULL"); + + uri_free(u); +} + +int main() { + test_init(); + test_set_and_get(); + test_set_with_port(); + test_set_with_query(); + test_set_with_fragment(); + test_full_uri(); + test_parse_basic(); + test_parse_with_port(); + test_parse_with_query(); + test_parse_with_fragment(); + test_parse_full(); + test_parse_invalid(); + test_set_override(); + test_roundtrip(); + test_minimal_uri(); + test_set_null(); + test_remove_element(); + test_null_guards(); + test_bounds_guards(); + + printf("test_uri: %d failures\n", failures); + return failures; +} -- cgit v1.3.1