/* LINK: ../../src/detect/check_registry.c ../../src/kdl/schema.c ../../src/kdl/parser.c ../../src/kdl/lexer.c ../../src/kdl/value.c ../../src/error.c ../../src/span.c */ /* tests/unit/test_check_registry.c * * Unit tests for the declarative feature-check registry (todo 10). * * 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). check_registry.c needs error.c (it * frees/re-owns value-model errors); schema.c needs value.c + error.c; * parser.c + lexer.c build documents end-to-end from source text; * span.c via error.c's st_span_print. error.c and span.c extend the * todo-10 file list because both linked units reference them. * * The registry under test maps a feature's CHECK child node (its name = * the check kind, its first arg = the target) onto the 8 declarative * kinds pinned in src/detect/check_registry.h, and src/kdl/schema.c's * HOOK enforces each kind's argument/property shape. Assertions check * REAL error properties: category (ST_ERR_KDL_SCHEMA), message text * (must name the offending node), and exact line/col spans. */ #include "munit.h" #include "detect/check_registry.h" #include "error.h" #include "kdl/ast.h" #include "kdl/schema.h" #include #include #include /* ---- fixture loading -------------------------------------------------- */ /* Locate tests/fixtures/stupid.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/stupid.kdl", "../fixtures/stupid.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. 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; } /* ---- helpers ---------------------------------------------------------- */ /* Parse a single-node document and return its one node (parse is * asserted to succeed). The document is leaked on purpose: tests free * it via the returned node's owning doc... it is NOT: the DOCUMENT is * returned alongside so the caller can free it. */ static struct st_kdl_node * parse_one_node(const char *src, struct st_kdl_document **doc_out) { struct st_error *err = NULL; *doc_out = st_kdl_parse(src, "t.kdl", &err); munit_assert_null(err); munit_assert_not_null(*doc_out); return (*doc_out)->nodes; } /* Parse `src` and validate; assert a ST_ERR_KDL_SCHEMA error whose * message contains `needle` and whose span is exactly (line, col). */ static void assert_schema_error(const char *src, const char *needle, size_t line, size_t col) { struct st_error *err = NULL; struct st_kdl_document *doc = st_kdl_parse(src, "t.kdl", &err); munit_assert_null(err); munit_assert_not_null(doc); err = st_kdl_validate(doc); munit_assert_not_null(err); munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA); munit_assert_not_null(err->span); munit_assert_size(err->span->line, ==, line); munit_assert_size(err->span->col, ==, col); munit_assert_true(strstr(st_error_message(err), needle) != NULL); st_error_free(err); st_kdl_document_free(doc); } /* Parse `src` and validate; assert success. */ static void assert_valid(const char *src) { struct st_error *err = NULL; struct st_kdl_document *doc = st_kdl_parse(src, "t.kdl", &err); munit_assert_null(err); munit_assert_not_null(doc); err = st_kdl_validate(doc); munit_assert_null(err); st_kdl_document_free(doc); } /* The 8 kinds in DSL order, each with a representative target argument. * `arg_col` is the column of the target token in the single-line source * `feature "f" { }` (col of kw + len(kw) + 1). */ struct kind_case { const char *kw; enum st_check_kind kind; const char *arg; size_t arg_col; }; static const struct kind_case kind_cases[] = { { "header", ST_CHECK_HEADER, "pthread.h", 22 }, { "function", ST_CHECK_FUNCTION, "strdup", 24 }, { "library", ST_CHECK_LIBRARY, "pthread", 23 }, { "type", ST_CHECK_TYPE, "size_t", 20 }, { "sizeof", ST_CHECK_SIZEOF, "long", 22 }, { "program", ST_CHECK_PROGRAM, "pkg-config", 23 }, { "compiler_flag", ST_CHECK_COMPILER_FLAG, "-fsanitize=address", 29 }, { "pkg_config", ST_CHECK_PKG_CONFIG, "openssl", 26 }, }; /* ---- tests ------------------------------------------------------------ */ /* (a) all 8 kinds map from a representative DSL node; kind name and * shape-table accessors round-trip; the table itself is coherent * (COUNT entries, each self-indexed, exactly one required arg). */ static MunitResult test_all_kinds_map(const MunitParameter params[], void *data) { (void)params; (void)data; size_t i; munit_assert_size(st_check_kind_count(), ==, 8); munit_assert_size(sizeof(kind_cases) / sizeof(kind_cases[0]), ==, 8); for (i = 0; i < st_check_kind_count(); i++) { char src[96]; int n; struct st_error *err = NULL; struct st_kdl_document *doc = NULL; struct st_kdl_node *node; enum st_check_kind kind; const struct st_check_shape *shape; /* a representative single-node document, e.g. header "pthread.h" */ n = snprintf(src, sizeof src, "%s \"%s\"", kind_cases[i].kw, kind_cases[i].arg); munit_assert_int(n, >=, 0); munit_assert_size((size_t)n, <, sizeof src); node = parse_one_node(src, &doc); munit_assert_not_null(node); err = NULL; kind = st_check_kind_from_node(node, &err); munit_assert_null(err); munit_assert_int(kind, ==, kind_cases[i].kind); st_kdl_document_free(doc); /* name round-trip: kind -> DSL keyword */ munit_assert_string_equal(st_check_kind_name(kind_cases[i].kind), kind_cases[i].kw); /* shape-table accessors agree with the kind */ shape = st_check_kind_shape(kind_cases[i].kind); munit_assert_not_null(shape); munit_assert_int(shape->kind, ==, kind_cases[i].kind); munit_assert_string_equal(shape->name, kind_cases[i].kw); munit_assert_size(shape->required_args, ==, 1); munit_assert_not_null(shape->arg_meaning); munit_assert_true(shape->arg_meaning[0] != '\0'); /* only library and pkg_config take the optional `version` * keyword-argument pair */ if (kind_cases[i].kind == ST_CHECK_LIBRARY || kind_cases[i].kind == ST_CHECK_PKG_CONFIG) { munit_assert_size(shape->optional_args, ==, 2); } else { munit_assert_size(shape->optional_args, ==, 0); } } /* out-of-range accessors degrade safely */ munit_assert_null(st_check_kind_shape(ST_CHECK_KIND_COUNT)); munit_assert_string_equal(st_check_kind_name(ST_CHECK_KIND_COUNT), "?"); return MUNIT_OK; } /* (a) a quoted node name maps like a bare one (names are KDL strings in * every string form), and each kind validates end-to-end inside a * feature block. */ static MunitResult test_quoted_and_feature_forms(const MunitParameter params[], void *data) { (void)params; (void)data; size_t i; for (i = 0; i < st_check_kind_count(); i++) { char src[160]; int n; struct st_error *err = NULL; struct st_kdl_document *doc = NULL; struct st_kdl_node *node; enum st_check_kind kind; /* quoted name form: "header" "pthread.h" */ n = snprintf(src, sizeof src, "\"%s\" \"%s\"", kind_cases[i].kw, kind_cases[i].arg); munit_assert_int(n, >=, 0); node = parse_one_node(src, &doc); kind = st_check_kind_from_node(node, &err); munit_assert_null(err); munit_assert_int(kind, ==, kind_cases[i].kind); st_kdl_document_free(doc); /* full feature form validates clean (the schema HOOK path) */ n = snprintf(src, sizeof src, "project \"p\" version \"1.0\"\n" "feature \"f\" { %s \"%s\" }", kind_cases[i].kw, kind_cases[i].arg); munit_assert_int(n, >=, 0); assert_valid(src); } return MUNIT_OK; } /* (b) an unknown check kind errors with the node's name + span, both at * the registry API and through schema validation; an empty node name and * a NULL node also error. */ static MunitResult test_unknown_kind(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_error *err = NULL; struct st_kdl_document *doc = NULL; struct st_kdl_node *node; enum st_check_kind kind; /* registry API: bare unknown name */ node = parse_one_node("bogus \"x\"", &doc); kind = st_check_kind_from_node(node, &err); munit_assert_not_null(err); munit_assert_int(kind, ==, ST_CHECK_KIND_COUNT); munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA); munit_assert_not_null(err->span); munit_assert_size(err->span->line, ==, 1); munit_assert_size(err->span->col, ==, 1); munit_assert_true(strstr(st_error_message(err), "'bogus'") != NULL); st_error_free(err); st_kdl_document_free(doc); /* registry API: quoted unknown name */ node = parse_one_node("\"wat\" \"x\"", &doc); kind = st_check_kind_from_node(node, &err); munit_assert_not_null(err); munit_assert_int(kind, ==, ST_CHECK_KIND_COUNT); munit_assert_true(strstr(st_error_message(err), "'wat'") != NULL); st_error_free(err); st_kdl_document_free(doc); /* schema end-to-end: the offending node + its span are named */ assert_schema_error("project \"p\" version \"1.0\"\n" "feature \"f\" { bogus \"x\" }", "'bogus'", 2, 15); /* a hand-built node with an empty name token errors */ { struct st_kdl_node empty = { 0 }; kind = st_check_kind_from_node(&empty, &err); munit_assert_not_null(err); munit_assert_int(kind, ==, ST_CHECK_KIND_COUNT); munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA); munit_assert_not_null(err->span); munit_assert_true(strstr(st_error_message(err), "empty name") != NULL); st_error_free(err); } /* a NULL node errors without a span */ kind = st_check_kind_from_node(NULL, &err); munit_assert_not_null(err); munit_assert_int(kind, ==, ST_CHECK_KIND_COUNT); munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA); munit_assert_null(err->span); st_error_free(err); return MUNIT_OK; } /* (c) a check without its required argument errors with the check node's * span; an empty-string target is equally rejected. */ static MunitResult test_missing_arg(const MunitParameter params[], void *data) { (void)params; (void)data; assert_schema_error("project \"p\" version \"1.0\"\n" "feature \"f\" { header }", "check 'header' requires an argument", 2, 15); assert_schema_error("project \"p\" version \"1.0\"\n" "feature \"f\" { library \"\" }", "must be a non-empty string", 2, 23); return MUNIT_OK; } /* (d) the todo-9 fixture still validates clean end-to-end, and its * feature children map to the expected kinds through the registry. */ static MunitResult test_fixture_valid(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; enum st_check_kind expect[2] = { ST_CHECK_HEADER, ST_CHECK_LIBRARY }; 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); munit_assert_null(st_kdl_validate(doc)); for (n = doc->nodes; n != NULL; n = n->next) { if (n->name.kind == ST_TOK_IDENT && n->name.len == 7 && memcmp(n->name.text, "feature", 7) == 0) { struct st_kdl_node *c; size_t ci = 0; for (c = n->children; c != NULL; c = c->next, ci++) { enum st_check_kind kind; if (ci == 0) { /* first child: `header` in pthread, `library` in math */ if (c->name.kind == ST_TOK_IDENT && c->name.len == 6 && memcmp(c->name.text, "header", 6) == 0) { kind = st_check_kind_from_node(c, &err); munit_assert_null(err); munit_assert_int(kind, ==, ST_CHECK_HEADER); } else { kind = st_check_kind_from_node(c, &err); munit_assert_null(err); munit_assert_int(kind, ==, ST_CHECK_LIBRARY); } } else { /* second child exists only in feature "pthread" */ munit_assert_size(ci, ==, 1); kind = st_check_kind_from_node(c, &err); munit_assert_null(err); munit_assert_int(kind, ==, expect[ci]); } } } } st_kdl_document_free(doc); free(src); return MUNIT_OK; } /* (e) required-arg enforcement per kind: each of the 8 kinds validates * with its target argument, and fails - naming the check node - without * it, with a wrong-typed target, with a second argument, or with a * children block. */ static MunitResult test_required_arg_per_kind(const MunitParameter params[], void *data) { (void)params; (void)data; size_t i; for (i = 0; i < st_check_kind_count(); i++) { char src[192]; char needle[128]; int n; /* missing target: span = the check name (col 15) */ n = snprintf(src, sizeof src, "project \"p\" version \"1.0\"\nfeature \"f\" { %s }", kind_cases[i].kw); munit_assert_int(n, >=, 0); n = snprintf(needle, sizeof needle, "check '%s' requires an argument", kind_cases[i].kw); munit_assert_int(n, >=, 0); assert_schema_error(src, needle, 2, 15); /* wrong-typed target: number where a string belongs */ n = snprintf(src, sizeof src, "project \"p\" version \"1.0\"\nfeature \"f\" { %s 42 }", kind_cases[i].kw); munit_assert_int(n, >=, 0); n = snprintf(needle, sizeof needle, "check '%s' argument must be a non-empty string (got " "int)", kind_cases[i].kw); munit_assert_int(n, >=, 0); assert_schema_error(src, needle, 2, kind_cases[i].arg_col); /* a second positional argument is rejected; for library and * pkg_config it must be the literal `version` keyword */ n = snprintf(src, sizeof src, "project \"p\" version \"1.0\"\n" "feature \"f\" { %s \"x\" \"y\" }", kind_cases[i].kw); munit_assert_int(n, >=, 0); if (kind_cases[i].kind == ST_CHECK_LIBRARY || kind_cases[i].kind == ST_CHECK_PKG_CONFIG) { n = snprintf(needle, sizeof needle, "check '%s': expected the keyword 'version'", kind_cases[i].kw); } else { n = snprintf(needle, sizeof needle, "check '%s' takes exactly one argument", kind_cases[i].kw); } munit_assert_int(n, >=, 0); assert_schema_error(src, needle, 2, kind_cases[i].arg_col + 4); /* a children block is rejected (child token at arg_col + 6) */ n = snprintf(src, sizeof src, "project \"p\" version \"1.0\"\n" "feature \"f\" { %s \"x\" { a } }", kind_cases[i].kw); munit_assert_int(n, >=, 0); n = snprintf(needle, sizeof needle, "check '%s' takes no children", kind_cases[i].kw); munit_assert_int(n, >=, 0); assert_schema_error(src, needle, 2, kind_cases[i].arg_col + 6); } return MUNIT_OK; } /* the optional `version` constraint on library/pkg_config: the literal * `version` keyword followed by a non-empty unannotated string (spelled * like the project node's version, todo 9's pinned decision). Other * kinds reject any further argument, and NO check takes properties. */ static MunitResult test_version_constraint(const MunitParameter params[], void *data) { (void)params; (void)data; assert_valid("project \"p\" version \"1.0\"\n" "feature \"f\" { library \"curl\" version \">=7.0\" }"); assert_valid("project \"p\" version \"1.0\"\n" "feature \"f\" { pkg_config \"openssl\" version " "\">=1.1\" }"); assert_schema_error( "project \"p\" version \"1.0\"\nfeature \"f\" { library \"l\" " "version }", "'version' requires the version constraint argument", 2, 27); assert_schema_error( "project \"p\" version \"1.0\"\nfeature \"f\" { library \"l\" " "version 42 }", "version must be a non-empty string (got int)", 2, 35); assert_schema_error( "project \"p\" version \"1.0\"\nfeature \"f\" { library \"l\" " "\"x\" }", "expected the keyword 'version'", 2, 27); assert_schema_error( "project \"p\" version \"1.0\"\nfeature \"f\" { library \"l\" " "version \"1\" \"x\" }", "takes at most three arguments", 2, 39); assert_schema_error( "project \"p\" version \"1.0\"\nfeature \"f\" { header \"h.h\" " "version \">=1\" }", "takes exactly one argument", 2, 28); assert_schema_error( "project \"p\" version \"1.0\"\nfeature \"f\" { library \"l\" " "version=\">=1\" }", "has unexpected property 'version' (checks take no properties)", 2, 27); return MUNIT_OK; } static MunitTest tests[] = { { "/registry/all-kinds-map", test_all_kinds_map, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/registry/quoted-and-feature-forms", test_quoted_and_feature_forms, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/registry/unknown-kind", test_unknown_kind, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/registry/missing-arg", test_missing_arg, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/registry/fixture-valid", test_fixture_valid, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/registry/required-arg-per-kind", test_required_arg_per_kind, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/registry/version-constraint", test_version_constraint, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, }; static const MunitSuite suite = { "/registry", 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); }