From bf19b9938508f70011395f4dc283ca758ec6a8d3 Mon Sep 17 00:00:00 2001 From: Ethan Werner Date: Sat, 19 Sep 2026 06:09:54 +0000 Subject: llm tooling --- tests/test_frontmatter.c | 236 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 tests/test_frontmatter.c (limited to 'tests/test_frontmatter.c') diff --git a/tests/test_frontmatter.c b/tests/test_frontmatter.c new file mode 100644 index 0000000..af4c2fc --- /dev/null +++ b/tests/test_frontmatter.c @@ -0,0 +1,236 @@ +/* test_frontmatter.c — standalone test suite for frontmatter.h. + * Zero dependencies: compiles to a single TU with no linked libraries. */ +#include "frontmatter.h" + +#include +#include +#include + +static int failures = 0; +static int checks = 0; + +#define CHECK(cond, msg) \ + do { \ + checks++; \ + if (!(cond)) { \ + failures++; \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, (msg)); \ + } \ + } while (0) + +/* ------------------------------------------------------------------ */ +/* Frontmatter */ +/* ------------------------------------------------------------------ */ + +static void test_frontmatter_scalars_lists_quotes(void) { + doc d; + parse_frontmatter( + "---\nid: a-1\nname: \"A: One\"\nenabled: true\nn: 42\ntags: db, " + "arch\ntags2: [\"x\", \"y\"]\n---\nbody line", + &d); + CHECK(strcmp(d.body, "body line") == 0, "body"); + const fm_value *v = doc_get(&d, "id"); + CHECK(v && v->kind == FM_STR && strcmp(v->s, "a-1") == 0, "id"); + v = doc_get(&d, "name"); + CHECK(v && v->kind == FM_STR && strcmp(v->s, "A: One") == 0, "name"); + v = doc_get(&d, "enabled"); + CHECK(v && v->kind == FM_BOOL && v->b, "enabled"); + v = doc_get(&d, "n"); + CHECK(v && v->kind == FM_INT && v->i == 42, "n"); + v = doc_get(&d, "tags"); + CHECK(v, "tags present"); + fm_strlist items; + fm_list_items(v, &items); + CHECK(items.len == 2 && strcmp(items.items[0], "db") == 0 && + strcmp(items.items[1], "arch") == 0, + "tags csv"); + fm_sl_free(&items); + v = doc_get(&d, "tags2"); + CHECK(v && v->kind == FM_LIST && v->item_count == 2 && + strcmp(v->items[0], "x") == 0 && strcmp(v->items[1], "y") == 0, + "tags2 array"); + doc_free(&d); +} + +static void test_frontmatter_missing_or_unterminated(void) { + doc d; + parse_frontmatter("just a body\n", &d); + CHECK(d.count == 0, "no meta"); + CHECK(strcmp(d.body, "just a body") == 0, "body trimmed"); + doc_free(&d); + + parse_frontmatter("---\nid: x\nno closing fence\n", &d); + CHECK(d.count == 0, "unterminated: no meta"); + CHECK(strcmp(d.body, "---\nid: x\nno closing fence") == 0, + "unterminated: whole text is body"); + doc_free(&d); +} + +static void test_frontmatter_ignores_blank_comment_colonless(void) { + doc d; + parse_frontmatter("---\n# comment\n\nnotakey\nk: v\n---\n", &d); + CHECK(d.count == 1, "one meta entry"); + const fm_value *v = doc_get(&d, "k"); + CHECK(v && v->kind == FM_STR && strcmp(v->s, "v") == 0, "k=v"); + doc_free(&d); +} + +static void test_fm_list_items_variants(void) { + doc d; + parse_frontmatter( + "---\ncsv: a, b, c\njson: [\"x\", \"y\"]\nsingle: only\nempty:\n" + "nums: [1, 2]\n---\nbody", + &d); + fm_strlist out; + fm_list_items(doc_get(&d, "csv"), &out); + CHECK(out.len == 3 && strcmp(out.items[0], "a") == 0 && + strcmp(out.items[2], "c") == 0, + "csv"); + fm_sl_free(&out); + fm_list_items(doc_get(&d, "json"), &out); + CHECK(out.len == 2 && strcmp(out.items[0], "x") == 0, "json"); + fm_sl_free(&out); + fm_list_items(doc_get(&d, "single"), &out); + CHECK(out.len == 1 && strcmp(out.items[0], "only") == 0, "single"); + fm_sl_free(&out); + fm_list_items(doc_get(&d, "empty"), &out); + CHECK(out.len == 0, "empty list"); + fm_sl_free(&out); + fm_list_items(doc_get(&d, "nums"), &out); + CHECK(out.len == 2 && strcmp(out.items[0], "1") == 0, "non-string items"); + fm_sl_free(&out); + fm_list_items(NULL, &out); + CHECK(out.len == 0, "null value"); + fm_sl_free(&out); + doc_free(&d); +} + +/* ------------------------------------------------------------------ */ +/* Embedded JSON */ +/* ------------------------------------------------------------------ */ + +static void test_json_roundtrip(void) { + const char *src = + "{\"a\":1,\"b\":\"x\",\"c\":[true,null,1.5],\"d\":{\"e\":[1,2," + "\"z\"]}}"; + fm_json *v = fm_json_parse(src); + CHECK(v != NULL, "parse"); + CHECK(v && v->type == FMJ_OBJECT, "object"); + CHECK(v && v->count == 4, "4 keys"); + const fm_json *a = fm_json_get(v, "a"); + CHECK(a && a->type == FMJ_NUMBER && a->is_int && a->i == 1, "a=1"); + const fm_json *b = fm_json_get(v, "b"); + CHECK(b && b->type == FMJ_STRING && strcmp(b->str, "x") == 0, "b=x"); + const fm_json *c = fm_json_get(v, "c"); + CHECK(c && c->type == FMJ_ARRAY && c->count == 3, "c array"); + CHECK(c && c->items[0]->type == FMJ_BOOL && c->items[0]->b, "c[0]=true"); + CHECK(c && c->items[1]->type == FMJ_NULL, "c[1]=null"); + CHECK(c && c->items[2]->type == FMJ_NUMBER && c->items[2]->num == 1.5, + "c[2]=1.5"); + const fm_json *d = fm_json_get(v, "d"); + CHECK(d && d->type == FMJ_OBJECT, "d object"); + const fm_json *e = d ? fm_json_get(d, "e") : NULL; + CHECK(e && e->type == FMJ_ARRAY && e->count == 3, "d.e array"); + CHECK(e && e->items[1]->is_int && e->items[1]->i == 2, "d.e[1]=2"); + char *out = fm_json_serialize(v); + CHECK(out && strcmp(out, src) == 0, "roundtrip"); + free(out); + fm_json_free(v); +} + +static void test_json_escapes(void) { + fm_json *v = fm_json_parse("\"a\\\"b\\\\c\\nd\\u0041\""); + CHECK(v && v->type == FMJ_STRING, "parse"); + CHECK(v && strcmp(v->str, "a\"b\\c\ndA") == 0, "unescaped value"); + char *s = fm_json_serialize(v); + CHECK(s && strcmp(s, "\"a\\\"b\\\\c\\ndA\"") == 0, "escaped output"); + free(s); + fm_json_free(v); + + fm_json *u = fm_json_parse("\"\\u4e2d\""); + CHECK(u && u->type == FMJ_STRING, "unicode parse"); + CHECK(u && strcmp(u->str, "\xE4\xB8\xAD") == 0, "utf-8 out"); + fm_json_free(u); +} + +static void test_json_invalid(void) { + CHECK(fm_json_parse("{") == NULL, "open brace"); + CHECK(fm_json_parse("[1,") == NULL, "dangling comma"); + CHECK(fm_json_parse("{bad:1}") == NULL, "bare key"); + CHECK(fm_json_parse("{\"a\":1\"b\":2}") == NULL, "missing comma"); + CHECK(fm_json_parse("{}x") == NULL, "trailing garbage"); + CHECK(fm_json_parse("") == NULL, "empty"); + CHECK(fm_json_parse("01") == NULL, "leading zero"); + CHECK(fm_json_parse("1.") == NULL, "dangling dot"); + CHECK(fm_json_parse("-") == NULL, "bare minus"); + CHECK(fm_json_parse("tru") == NULL, "bare tru"); + CHECK(fm_json_parse("\"unterminated") == NULL, "unterminated string"); + CHECK(fm_json_parse("[1 2]") == NULL, "missing separator"); + fm_json *f1 = fm_json_parse("1.5"); + CHECK(f1 != NULL, "float ok"); + fm_json_free(f1); + fm_json *f2 = fm_json_parse("1e3"); + CHECK(f2 != NULL, "exponent ok"); + fm_json_free(f2); + fm_json *f3 = fm_json_parse("null"); + CHECK(f3 != NULL, "null ok"); + fm_json_free(f3); + fm_json *f4 = fm_json_parse("[]"); + CHECK(f4 != NULL, "empty array ok"); + fm_json_free(f4); + fm_json *f5 = fm_json_parse("{}"); + CHECK(f5 != NULL, "empty object ok"); + fm_json_free(f5); +} + +static void test_json_merge(void) { + fm_json *base = fm_json_parse("{\"a\":1,\"b\":{\"x\":1}}"); + fm_json *over = fm_json_parse("{\"b\":{\"y\":2},\"c\":3}"); + fm_json *m = fm_json_merge_owned(base, over); + char *s = fm_json_serialize(m); + CHECK(s && strcmp(s, "{\"b\":{\"y\":2},\"c\":3,\"a\":1}") == 0, + "merged content"); + free(s); + fm_json_free(m); + + fm_json *o2 = fm_json_parse("{\"z\":9}"); + fm_json *m2 = fm_json_merge_owned(NULL, o2); + char *s2 = fm_json_serialize(m2); + CHECK(s2 && strcmp(s2, "{\"z\":9}") == 0, "over only"); + free(s2); + fm_json_free(m2); + + fm_json *b2 = fm_json_parse("{\"q\":1}"); + fm_json *m3 = fm_json_merge_owned(b2, NULL); + char *s3 = fm_json_serialize(m3); + CHECK(s3 && strcmp(s3, "{\"q\":1}") == 0, "base only"); + free(s3); + fm_json_free(m3); + + fm_json *n1 = fm_json_parse("[1]"); + fm_json *n2 = fm_json_parse("\"x\""); + fm_json *m4 = fm_json_merge_owned(n1, n2); + char *s4 = fm_json_serialize(m4); + CHECK(s4 && strcmp(s4, "{}") == 0, "non-object inputs dropped"); + free(s4); + fm_json_free(m4); + + fm_json *m5 = fm_json_merge_owned(NULL, NULL); + char *s5 = fm_json_serialize(m5); + CHECK(s5 && strcmp(s5, "{}") == 0, "both null"); + free(s5); + fm_json_free(m5); +} + +int main(void) { + test_frontmatter_scalars_lists_quotes(); + test_frontmatter_missing_or_unterminated(); + test_frontmatter_ignores_blank_comment_colonless(); + test_fm_list_items_variants(); + test_json_roundtrip(); + test_json_escapes(); + test_json_invalid(); + test_json_merge(); + printf("%d checks, %d failures\n", checks, failures); + return failures ? 1 : 0; +} -- cgit v1.3.1