/* LINK: ../../src/kdl/parser.c ../../src/kdl/lexer.c ../../src/error.c ../../src/span.c */ /* tests/unit/test_parser.c * * Unit tests for the KDL 2.0.0 recursive-descent parser (todo 7). * * The magic LINK comment on line 1 is REQUIRED by tests/run.sh: it lists * the extra .c sources to compile into this test binary (paths relative * to tests/unit/, space-separated). parser.c depends on lexer.c (token * stream), error.c (typed errors) and span.c (caret rendering). * * These tests assert on the REAL AST structure — node counts, argument * and property order, children nesting, spans — never just "no crash". * AST token slices are borrowed from the source buffer, so the source * must outlive the document (both kept alive until st_kdl_document_free). */ #include "munit.h" #include "error.h" #include "kdl/ast.h" #include "kdl/lexer.h" #include "span.h" #include #include #include #include /* ---- fixture loading -------------------------------------------------- */ /* Locate tests/fixtures/basic.kdl. The harness runs the test binary with * cwd = repo top (make check) or tests/unit (manual run); probe both. */ static const char * fixture_path(void) { static const char *const candidates[] = { "tests/fixtures/basic.kdl", "../fixtures/basic.kdl", }; size_t i; for (i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) { FILE *f = fopen(candidates[i], "rb"); if (f != NULL) { fclose(f); return candidates[i]; } } return NULL; } /* Slurp the fixture into a NUL-terminated buffer (the lexer requires NUL * termination). Returns NULL on any I/O failure. Caller frees. */ static char * slurp_fixture(const char *path) { FILE *f; long n; char *buf; if (path == NULL) { return NULL; } f = fopen(path, "rb"); if (f == NULL) { return NULL; } if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return NULL; } n = ftell(f); if (n < 0 || fseek(f, 0, SEEK_SET) != 0) { fclose(f); return NULL; } buf = munit_malloc((size_t)n + 1); if (fread(buf, 1, (size_t)n, f) != (size_t)n) { free(buf); fclose(f); return NULL; } fclose(f); buf[n] = '\0'; return buf; } /* ---- token-ref assertions --------------------------------------------- */ static void assert_ref(const struct st_kdl_token_ref *ref, enum st_token_kind kind, const char *text, size_t line, size_t col) { munit_assert_not_null(ref); munit_assert_int(ref->kind, ==, kind); if (text != NULL) { munit_assert_not_null(ref->text); munit_assert_size(ref->len, ==, strlen(text)); munit_assert_memory_equal(ref->len, ref->text, text); } munit_assert_size(ref->span.line, ==, line); munit_assert_size(ref->span.col, ==, col); } /* ---- tests ------------------------------------------------------------ */ /* (a) tests/fixtures/basic.kdl parses to the expected AST: node count, * names, arg order, prop key/value kinds, children nesting, spans. */ static MunitResult test_parse_fixture(const MunitParameter params[], void *data) { (void)params; (void)data; const char *path = fixture_path(); char *src; struct st_error *err = NULL; struct st_kdl_document *doc; struct st_kdl_node *n; struct st_kdl_prop *p; munit_assert_not_null(path); src = slurp_fixture(path); munit_assert_not_null(src); doc = st_kdl_parse(src, path, &err); munit_assert_not_null(doc); munit_assert_null(err); /* --- three top-level nodes: project, target, credits ------------- */ n = doc->nodes; munit_assert_not_null(n); assert_ref(&n->name, ST_TOK_IDENT, "project", 7, 1); munit_assert_null(n->args); p = n->props; munit_assert_not_null(p); assert_ref(&p->key, ST_TOK_IDENT, "name", 7, 9); assert_ref(&p->value, ST_TOK_STRING, "\"stupidtools\"", 7, 14); p = p->next; munit_assert_not_null(p); assert_ref(&p->key, ST_TOK_IDENT, "version", 7, 28); assert_ref(&p->value, ST_TOK_STRING, "\"1.0.0\"", 7, 36); munit_assert_null(p->next); /* --- target "default" with 6 children ---------------------------- */ n = n->next; munit_assert_not_null(n); assert_ref(&n->name, ST_TOK_IDENT, "target", 9, 1); munit_assert_not_null(n->args); assert_ref(&n->args->value, ST_TOK_STRING, "\"default\"", 9, 8); munit_assert_null(n->args->next); munit_assert_null(n->props); { struct st_kdl_node *c = n->children; /* child 1: src "src/main.c" */ munit_assert_not_null(c); assert_ref(&c->name, ST_TOK_IDENT, "src", 10, 5); munit_assert_not_null(c->args); assert_ref(&c->args->value, ST_TOK_STRING, "\"src/main.c\"", 10, 9); munit_assert_null(c->args->next); /* child 2: src "src/kdl/lexer.c" (after ';') */ c = c->next; munit_assert_not_null(c); assert_ref(&c->name, ST_TOK_IDENT, "src", 10, 24); assert_ref(&c->args->value, ST_TOK_STRING, "\"src/kdl/lexer.c\"", 10, 28); munit_assert_null(c->args->next); /* child 3: cc-flags "-std=c23" "-Wall" */ c = c->next; munit_assert_not_null(c); assert_ref(&c->name, ST_TOK_IDENT, "cc-flags", 11, 5); munit_assert_not_null(c->args); assert_ref(&c->args->value, ST_TOK_STRING, "\"-std=c23\"", 11, 14); assert_ref(&c->args->next->value, ST_TOK_STRING, "\"-Wall\"", 11, 25); munit_assert_null(c->args->next->next); /* child 4: jobs 8 */ c = c->next; munit_assert_not_null(c); assert_ref(&c->name, ST_TOK_IDENT, "jobs", 12, 5); assert_ref(&c->args->value, ST_TOK_NUMBER, "8", 12, 10); munit_assert_null(c->args->next); /* child 5: optimize #true debug=#false */ c = c->next; munit_assert_not_null(c); assert_ref(&c->name, ST_TOK_IDENT, "optimize", 13, 5); assert_ref(&c->args->value, ST_TOK_TRUE, "#true", 13, 14); munit_assert_null(c->args->next); munit_assert_not_null(c->props); assert_ref(&c->props->key, ST_TOK_IDENT, "debug", 13, 20); assert_ref(&c->props->value, ST_TOK_FALSE, "#false", 13, 26); munit_assert_null(c->props->next); /* child 6: feature "unit-tests" { option "munit" { default #true } } */ c = c->next; munit_assert_not_null(c); assert_ref(&c->name, ST_TOK_IDENT, "feature", 14, 5); assert_ref(&c->args->value, ST_TOK_STRING, "\"unit-tests\"", 14, 13); munit_assert_null(c->args->next); munit_assert_not_null(c->children); assert_ref(&c->children->name, ST_TOK_IDENT, "option", 15, 9); assert_ref(&c->children->args->value, ST_TOK_STRING, "\"munit\"", 15, 16); munit_assert_not_null(c->children->children); assert_ref(&c->children->children->name, ST_TOK_IDENT, "default", 15, 26); assert_ref(&c->children->children->args->value, ST_TOK_TRUE, "#true", 15, 34); munit_assert_null(c->children->children->args->next); munit_assert_null(c->next); } /* --- credits with type annotation (meta) -------------------------- */ n = n->next; munit_assert_not_null(n); munit_assert_not_null(n->type); /* the TYPE token's span starts at the opening '('; its text slice is * the inner identifier without the parens (see lexer.h) */ assert_ref(n->type, ST_TOK_TYPE, "meta", 21, 1); assert_ref(&n->name, ST_TOK_IDENT, "credits", 21, 7); assert_ref(&n->args->value, ST_TOK_STRING, "\"huntedbytheirs\"", 21, 15); munit_assert_null(n->args->next); munit_assert_null(n->props); munit_assert_null(n->next); st_kdl_document_free(doc); free(src); return MUNIT_OK; } /* (b) `node {` — unterminated children block errors with the span of the * `{` token. */ static MunitResult test_unterminated_children(const MunitParameter params[], void *data) { (void)params; (void)data; const char *src = "node {"; struct st_error *err = NULL; struct st_kdl_document *doc; doc = st_kdl_parse(src, "t.kdl", &err); munit_assert_null(doc); munit_assert_not_null(err); munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_PARSE); /* the span must point at the `{` (line 1, col 6), not at EOF */ munit_assert_not_null(err->span); munit_assert_size(err->span->line, ==, 1); munit_assert_size(err->span->col, ==, 6); munit_assert_true(strstr(st_error_message(err), "children") != NULL); st_error_free(err); return MUNIT_OK; } /* (c) a stray `}` at top level errors with the span of the `}`. */ static MunitResult test_stray_rbrace(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_error *err = NULL; struct st_kdl_document *doc; doc = st_kdl_parse("}", "t.kdl", &err); munit_assert_null(doc); munit_assert_not_null(err); munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_PARSE); munit_assert_not_null(err->span); munit_assert_size(err->span->line, ==, 1); munit_assert_size(err->span->col, ==, 1); st_error_free(err); /* also stray inside a completed children block: `node { child } }` */ err = NULL; doc = st_kdl_parse("node { child } }", "t.kdl", &err); munit_assert_null(doc); munit_assert_not_null(err); munit_assert_size(err->span->col, ==, 16); st_error_free(err); return MUNIT_OK; } /* (d) prop classification: a token followed by `=` is a key, the next * value token is its value; bare values are args, in source order. */ static MunitResult test_prop_classification(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_error *err = NULL; struct st_kdl_document *doc; struct st_kdl_node *n; struct st_kdl_arg *a; struct st_kdl_prop *p; doc = st_kdl_parse("node key=value key2=\"v2\" 1 2 #true", "t.kdl", &err); munit_assert_not_null(doc); munit_assert_null(err); n = doc->nodes; munit_assert_not_null(n); munit_assert_null(n->next); p = n->props; munit_assert_not_null(p); assert_ref(&p->key, ST_TOK_IDENT, "key", 1, 6); assert_ref(&p->value, ST_TOK_IDENT, "value", 1, 10); p = p->next; munit_assert_not_null(p); assert_ref(&p->key, ST_TOK_IDENT, "key2", 1, 16); assert_ref(&p->value, ST_TOK_STRING, "\"v2\"", 1, 21); munit_assert_null(p->next); a = n->args; munit_assert_not_null(a); assert_ref(&a->value, ST_TOK_NUMBER, "1", 1, 26); a = a->next; munit_assert_not_null(a); assert_ref(&a->value, ST_TOK_NUMBER, "2", 1, 28); a = a->next; munit_assert_not_null(a); assert_ref(&a->value, ST_TOK_TRUE, "#true", 1, 30); munit_assert_null(a->next); st_kdl_document_free(doc); return MUNIT_OK; } /* (e) interleaved args/props keep their relative order within each list: * `node a=1 b c=2 d` -> args [b, d], props [a=1, c=2]. */ static MunitResult test_interleaved_order(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_error *err = NULL; struct st_kdl_document *doc; struct st_kdl_node *n; doc = st_kdl_parse("node a=1 b c=2 d", "t.kdl", &err); munit_assert_not_null(doc); munit_assert_null(err); n = doc->nodes; munit_assert_not_null(n); /* args, in order: b then d */ munit_assert_not_null(n->args); assert_ref(&n->args->value, ST_TOK_IDENT, "b", 1, 10); assert_ref(&n->args->next->value, ST_TOK_IDENT, "d", 1, 16); munit_assert_null(n->args->next->next); /* props, in order: a=1 then c=2 */ munit_assert_not_null(n->props); assert_ref(&n->props->key, ST_TOK_IDENT, "a", 1, 6); assert_ref(&n->props->value, ST_TOK_NUMBER, "1", 1, 8); assert_ref(&n->props->next->key, ST_TOK_IDENT, "c", 1, 12); assert_ref(&n->props->next->value, ST_TOK_NUMBER, "2", 1, 14); munit_assert_null(n->props->next->next); st_kdl_document_free(doc); return MUNIT_OK; } /* (f) newline terminates a node: `a 1\nb 2` is two nodes, not one node * with four entries. */ static MunitResult test_newline_separates_nodes(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_error *err = NULL; struct st_kdl_document *doc; struct st_kdl_node *n; doc = st_kdl_parse("a 1\nb 2", "t.kdl", &err); munit_assert_not_null(doc); munit_assert_null(err); n = doc->nodes; munit_assert_not_null(n); assert_ref(&n->name, ST_TOK_IDENT, "a", 1, 1); assert_ref(&n->args->value, ST_TOK_NUMBER, "1", 1, 3); munit_assert_null(n->args->next); n = n->next; munit_assert_not_null(n); assert_ref(&n->name, ST_TOK_IDENT, "b", 2, 1); assert_ref(&n->args->value, ST_TOK_NUMBER, "2", 2, 3); munit_assert_null(n->args->next); munit_assert_null(n->next); st_kdl_document_free(doc); return MUNIT_OK; } /* (g) semicolons separate nodes; repeated separators are trivia. */ static MunitResult test_semicolon_separators(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_error *err = NULL; struct st_kdl_document *doc; struct st_kdl_node *n; size_t count = 0; doc = st_kdl_parse("node ; ; node", "t.kdl", &err); munit_assert_not_null(doc); munit_assert_null(err); for (n = doc->nodes; n != NULL; n = n->next) { assert_ref(&n->name, ST_TOK_IDENT, "node", 1, (size_t)(count == 0 ? 1 : 10)); count++; } munit_assert_size(count, ==, 2); st_kdl_document_free(doc); return MUNIT_OK; } /* (h) an empty document parses to a document with zero nodes. */ static MunitResult test_empty_document(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_error *err = NULL; struct st_kdl_document *doc; doc = st_kdl_parse("", "t.kdl", &err); munit_assert_not_null(doc); munit_assert_null(err); munit_assert_null(doc->nodes); st_kdl_document_free(doc); /* whitespace/comment-only is also an empty document */ err = NULL; doc = st_kdl_parse(" // nothing\n/* nor here */\n/- ;", "t.kdl", &err); munit_assert_not_null(doc); munit_assert_null(err); munit_assert_null(doc->nodes); st_kdl_document_free(doc); return MUNIT_OK; } /* (i) `node = x` is invalid: the node name cannot be a property key. */ static MunitResult test_equals_after_name(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_error *err = NULL; struct st_kdl_document *doc; doc = st_kdl_parse("node = x", "t.kdl", &err); munit_assert_null(doc); munit_assert_not_null(err); munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_PARSE); munit_assert_not_null(err->span); munit_assert_size(err->span->line, ==, 1); munit_assert_size(err->span->col, ==, 6); st_error_free(err); /* a non-string value before `=` is equally invalid: `node 1 = 2` */ err = NULL; doc = st_kdl_parse("node 1 = 2", "t.kdl", &err); munit_assert_null(doc); munit_assert_not_null(err); munit_assert_size(err->span->col, ==, 8); st_error_free(err); return MUNIT_OK; } /* (j) a property needs a value: `node key =` errors at EOF; `node key = {` * errors at the `{`. */ static MunitResult test_prop_missing_value(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_error *err = NULL; struct st_kdl_document *doc; doc = st_kdl_parse("node key =", "t.kdl", &err); munit_assert_null(doc); munit_assert_not_null(err); munit_assert_true(strstr(st_error_message(err), "value") != NULL); munit_assert_not_null(err->span); munit_assert_size(err->span->col, ==, 11); st_error_free(err); err = NULL; doc = st_kdl_parse("node key = {", "t.kdl", &err); munit_assert_null(doc); munit_assert_not_null(err); munit_assert_true(strstr(st_error_message(err), "value") != NULL); munit_assert_size(err->span->col, ==, 12); st_error_free(err); return MUNIT_OK; } /* (k) a lexer error is propagated with its span, copied into owned * storage: the span must still be readable after st_kdl_parse returns. */ static MunitResult test_lexer_error_propagates(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_error *err = NULL; struct st_kdl_document *doc; doc = st_kdl_parse("node \"unterminated", "t.kdl", &err); munit_assert_null(doc); munit_assert_not_null(err); munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_PARSE); munit_assert_not_null(err->span); munit_assert_size(err->span->line, ==, 1); munit_assert_size(err->span->col, ==, 6); munit_assert_true(strstr(st_error_message(err), "unterminated") != NULL); st_error_free(err); return MUNIT_OK; } /* (l) type annotation attaches to the node name; quoted node names and * string keys work. */ static MunitResult test_annotation_and_string_names(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_error *err = NULL; struct st_kdl_document *doc; struct st_kdl_node *n; doc = st_kdl_parse("(u8)size 4\n\"quoted node\" x\np \"k s\"=1", "t.kdl", &err); munit_assert_not_null(doc); munit_assert_null(err); n = doc->nodes; munit_assert_not_null(n); munit_assert_not_null(n->type); assert_ref(n->type, ST_TOK_TYPE, "u8", 1, 1); assert_ref(&n->name, ST_TOK_IDENT, "size", 1, 5); assert_ref(&n->args->value, ST_TOK_NUMBER, "4", 1, 10); munit_assert_null(n->args->next); n = n->next; munit_assert_not_null(n); assert_ref(&n->name, ST_TOK_STRING, "\"quoted node\"", 2, 1); assert_ref(&n->args->value, ST_TOK_IDENT, "x", 2, 15); munit_assert_null(n->args->next); n = n->next; munit_assert_not_null(n); assert_ref(&n->name, ST_TOK_IDENT, "p", 3, 1); munit_assert_not_null(n->props); assert_ref(&n->props->key, ST_TOK_STRING, "\"k s\"", 3, 3); assert_ref(&n->props->value, ST_TOK_NUMBER, "1", 3, 9); munit_assert_null(n->props->next); munit_assert_null(n->next); st_kdl_document_free(doc); return MUNIT_OK; } /* (m) single-line children blocks and nested blocks parse recursively. */ static MunitResult test_children_nesting(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_error *err = NULL; struct st_kdl_document *doc; struct st_kdl_node *n; doc = st_kdl_parse("p { a; b; c { d } }", "t.kdl", &err); munit_assert_not_null(doc); munit_assert_null(err); n = doc->nodes; munit_assert_not_null(n); assert_ref(&n->name, ST_TOK_IDENT, "p", 1, 1); munit_assert_not_null(n->children); assert_ref(&n->children->name, ST_TOK_IDENT, "a", 1, 5); assert_ref(&n->children->next->name, ST_TOK_IDENT, "b", 1, 8); assert_ref(&n->children->next->next->name, ST_TOK_IDENT, "c", 1, 11); munit_assert_not_null(n->children->next->next->children); assert_ref(&n->children->next->next->children->name, ST_TOK_IDENT, "d", 1, 15); munit_assert_null(n->children->next->next->next); munit_assert_null(n->next); st_kdl_document_free(doc); return MUNIT_OK; } /* (n) `/-` and comments are trivia: a slashdash between entries vanishes, * and a node ends at the slashdash/semicolon regardless of line. */ static MunitResult test_trivia_skipped(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_error *err = NULL; struct st_kdl_document *doc; struct st_kdl_node *n; size_t count = 0; doc = st_kdl_parse("node a=1 /-\n; // still trivia\nb", "t.kdl", &err); munit_assert_not_null(doc); munit_assert_null(err); n = doc->nodes; munit_assert_not_null(n); assert_ref(&n->name, ST_TOK_IDENT, "node", 1, 1); munit_assert_not_null(n->props); assert_ref(&n->props->key, ST_TOK_IDENT, "a", 1, 6); assert_ref(&n->props->value, ST_TOK_NUMBER, "1", 1, 8); munit_assert_null(n->props->next); munit_assert_null(n->args); for (n = n->next; n != NULL; n = n->next) { count++; } munit_assert_size(count, ==, 1); assert_ref(&doc->nodes->next->name, ST_TOK_IDENT, "b", 3, 1); st_kdl_document_free(doc); return MUNIT_OK; } /* (o) `st_kdl_document_free(NULL)` is a safe no-op. */ static MunitResult test_free_null(const MunitParameter params[], void *data) { (void)params; (void)data; st_kdl_document_free(NULL); return MUNIT_OK; } static MunitTest tests[] = { { "/parse/fixture-basic-kdl", test_parse_fixture, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/parse/unterminated-children", test_unterminated_children, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/parse/stray-rbrace", test_stray_rbrace, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/parse/prop-classification", test_prop_classification, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/parse/interleaved-order", test_interleaved_order, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/parse/newline-separates-nodes", test_newline_separates_nodes, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/parse/semicolon-separators", test_semicolon_separators, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/parse/empty-document", test_empty_document, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/parse/equals-after-name", test_equals_after_name, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/parse/prop-missing-value", test_prop_missing_value, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/parse/lexer-error-propagates", test_lexer_error_propagates, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/parse/annotation-and-string-names", test_annotation_and_string_names, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/parse/children-nesting", test_children_nesting, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/parse/trivia-skipped", test_trivia_skipped, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/parse/free-null", test_free_null, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, }; static const MunitSuite suite = { "/parser", tests, NULL, 1, MUNIT_SUITE_OPTION_NONE, }; int main(int argc, char *argv[MUNIT_ARRAY_PARAM(argc + 1)]) { return munit_suite_main(&suite, NULL, argc, argv); }