summaryrefslogtreecommitdiffstats
path: root/tests/test_frontmatter.c
diff options
context:
space:
mode:
authorEthan Werner <mail@ethanwerner.com>2026-09-19 06:09:54 +0000
committerEthan Werner <mail@ethanwerner.com>2026-09-19 06:09:54 +0000
commitbf19b9938508f70011395f4dc283ca758ec6a8d3 (patch)
tree46dc4c442ed8d8d43040433c6a4794e76f8e618a /tests/test_frontmatter.c
parent96ecf29e8be977f51f351308ef2c7c492bb829aa (diff)
llm toolingHEADmaster
Diffstat (limited to 'tests/test_frontmatter.c')
-rw-r--r--tests/test_frontmatter.c236
1 files changed, 236 insertions, 0 deletions
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;
+}