summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test_ann.c284
-rw-r--r--tests/test_bin.c273
-rw-r--r--tests/test_buffer.c134
-rw-r--r--tests/test_clio.c884
-rw-r--r--tests/test_direct.c157
-rw-r--r--tests/test_dotagents.c524
-rw-r--r--tests/test_escape.c113
-rw-r--r--tests/test_flat.c132
-rw-r--r--tests/test_frontmatter.c236
-rw-r--r--tests/test_genetic.c158
-rw-r--r--tests/test_interpolate.c112
-rw-r--r--tests/test_pool.c150
-rw-r--r--tests/test_pushdown.c442
-rw-r--r--tests/test_stack.c234
-rw-r--r--tests/test_ufo.c143
-rw-r--r--tests/test_uri.c300
16 files changed, 4276 insertions, 0 deletions
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 <stdio.h>
+#define ANN_IMPLEMENTATION
+#include "../include/ann.h"
+
+#include <math.h>
+#include <time.h>
+
+#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 <stdint.h>
+typedef int32_t bin_key_t;
+
+#define BIN_IMPLEMENTATION
+#include "../include/bin.h"
+
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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 <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+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 <stdio.h>
+typedef double direct_value_t;
+#define DT_IMPLEMENTATION
+#include "../include/direct.h"
+
+#include <math.h>
+
+#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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+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 <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#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 <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <float.h>
+
+#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 <stdio.h>
+#include <math.h>
+
+#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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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 <stdio.h>
+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 <math.h>
+
+#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 <math.h>
+#include <stdio.h>
+
+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 <stdio.h>
+#define UFO_IMPLEMENTATION
+#include "../include/ufo.h"
+
+#include <math.h>
+
+#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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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;
+}