/* 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; }