Template
530 lines
19 KiB
C
530 lines
19 KiB
C
/* LINK: ../../src/kdl/schema.c ../../src/kdl/value.c ../../src/kdl/parser.c ../../src/kdl/lexer.c ../../src/error.c ../../src/span.c */
|
|
/* tests/unit/test_schema.c
|
|
*
|
|
* Unit tests for the stupidtools DSL schema validator (todo 9).
|
|
*
|
|
* 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). schema.c depends on value.c (typed
|
|
* value interpretation) and error.c (typed errors); parser.c + lexer.c
|
|
* are linked so documents can be built end-to-end from source text, and
|
|
* span.c via error.c's st_span_print.
|
|
*
|
|
* The grammar under test is pinned in src/kdl/schema.h. Assertions check
|
|
* REAL error properties: category (always ST_ERR_KDL_SCHEMA), message
|
|
* text (must name the offending node), and exact line/col spans — never
|
|
* just "returns an error".
|
|
*/
|
|
#include "munit.h"
|
|
|
|
#include "error.h"
|
|
#include "kdl/ast.h"
|
|
#include "kdl/schema.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
/* ---- 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 (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;
|
|
}
|
|
|
|
/* ---- helpers ---------------------------------------------------------- */
|
|
|
|
/* Parse `src` (asserting parse success) and validate; assert the result
|
|
* is 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` (asserting parse success) 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);
|
|
}
|
|
|
|
/* ---- tests ------------------------------------------------------------ */
|
|
|
|
/* (a) tests/fixtures/stupid.kdl validates clean and carries the expected
|
|
* canonical shapes: project first (name + version keyword + semver), a
|
|
* target with src/feature children, features with header/library check
|
|
* placeholders, and an option with a bool default. */
|
|
static MunitResult
|
|
test_valid_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;
|
|
size_t top_count = 0;
|
|
size_t target_children = 0;
|
|
|
|
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);
|
|
|
|
/* validates clean (the core acceptance of this todo) */
|
|
munit_assert_null(st_kdl_validate(doc));
|
|
|
|
/* five top-level nodes: project, feature, feature, target, option */
|
|
for (n = doc->nodes; n != NULL; n = n->next) {
|
|
top_count++;
|
|
}
|
|
munit_assert_size(top_count, ==, 5);
|
|
|
|
/* project spans (hand-verified against cat -n of the fixture) */
|
|
n = doc->nodes;
|
|
munit_assert_size(n->name.span.line, ==, 16);
|
|
munit_assert_size(n->name.span.col, ==, 1);
|
|
munit_assert_not_null(n->args);
|
|
munit_assert_size(n->args->value.span.col, ==, 9); /* "stupidtools" */
|
|
munit_assert_size(n->args->next->value.span.col, ==, 23); /* version kw */
|
|
munit_assert_size(n->args->next->next->value.span.col, ==, 31); /* "1.0.0" */
|
|
|
|
/* target "default" has 6 children: 4 src + 2 feature refs */
|
|
for (n = doc->nodes; n != NULL && top_count-- > 0; n = n->next) {
|
|
if (n->name.kind == ST_TOK_IDENT && n->name.len == 6 &&
|
|
memcmp(n->name.text, "target", 6) == 0) {
|
|
struct st_kdl_node *c;
|
|
for (c = n->children; c != NULL; c = c->next) {
|
|
target_children++;
|
|
}
|
|
}
|
|
}
|
|
munit_assert_size(target_children, ==, 6);
|
|
|
|
/* option "debug" default=#false: key and value spans */
|
|
n = doc->nodes->next->next->next->next;
|
|
munit_assert_size(n->name.span.line, ==, 43);
|
|
munit_assert_size(n->args->value.span.col, ==, 8); /* "debug" */
|
|
munit_assert_not_null(n->props);
|
|
munit_assert_size(n->props->key.span.col, ==, 16); /* default */
|
|
munit_assert_size(n->props->value.span.col, ==, 24); /* #false */
|
|
|
|
st_kdl_document_free(doc);
|
|
free(src);
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
/* (c) missing project: an empty document (and a NULL document) yields a
|
|
* schema error with NO span (no file name is available for an empty
|
|
* document). */
|
|
static MunitResult
|
|
test_missing_project(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);
|
|
err = st_kdl_validate(doc);
|
|
munit_assert_not_null(err);
|
|
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
|
|
munit_assert_null(err->span);
|
|
munit_assert_true(
|
|
strstr(st_error_message(err), "missing required 'project'") != NULL);
|
|
st_error_free(err);
|
|
st_kdl_document_free(doc);
|
|
|
|
/* comment-only documents are equally empty */
|
|
doc = st_kdl_parse("// nothing\n", "t.kdl", &err);
|
|
munit_assert_not_null(doc);
|
|
err = st_kdl_validate(doc);
|
|
munit_assert_not_null(err);
|
|
munit_assert_null(err->span);
|
|
st_error_free(err);
|
|
st_kdl_document_free(doc);
|
|
|
|
/* a NULL document is the same failure */
|
|
err = st_kdl_validate(NULL);
|
|
munit_assert_not_null(err);
|
|
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
|
|
st_error_free(err);
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
/* (c/d) project must be FIRST: a document starting with a non-project
|
|
* node errors naming that node with its span. */
|
|
static MunitResult
|
|
test_project_not_first(const MunitParameter params[], void *data)
|
|
{
|
|
(void)params;
|
|
(void)data;
|
|
|
|
assert_schema_error("target \"x\" { src \"a.c\" }\n"
|
|
"project \"p\" version \"1.0\"",
|
|
"'target'", 1, 1);
|
|
assert_schema_error("feature \"f\" { header \"h.h\" }\n"
|
|
"project \"p\" version \"1.0\"",
|
|
"'feature'", 1, 1);
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
/* (b) unknown top-level node names the node + span, in any position after
|
|
* the project. */
|
|
static MunitResult
|
|
test_unknown_top_level(const MunitParameter params[], void *data)
|
|
{
|
|
(void)params;
|
|
(void)data;
|
|
|
|
assert_schema_error("project \"p\" version \"1.0\"\n"
|
|
"bogus \"x\"\n",
|
|
"'bogus'", 2, 1);
|
|
assert_schema_error("project \"p\" version \"1.0\"\n"
|
|
"target \"t\" { src \"a.c\" }\n"
|
|
"wat 1\n",
|
|
"'wat'", 3, 1);
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
/* duplicate project nodes are rejected with the second project's span. */
|
|
static MunitResult
|
|
test_duplicate_project(const MunitParameter params[], void *data)
|
|
{
|
|
(void)params;
|
|
(void)data;
|
|
|
|
assert_schema_error("project \"p\" version \"1.0\"\n"
|
|
"project \"q\" version \"2.0\"\n"
|
|
"target \"t\" { src \"a.c\" }\n",
|
|
"duplicate 'project'", 2, 1);
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
/* (e) missing/extra required arguments on every node shape. */
|
|
static MunitResult
|
|
test_missing_args(const MunitParameter params[], void *data)
|
|
{
|
|
(void)params;
|
|
(void)data;
|
|
|
|
/* project argument shapes */
|
|
assert_schema_error("project", "requires a name argument", 1, 1);
|
|
assert_schema_error("project \"p\"", "requires a version", 1, 1);
|
|
assert_schema_error("project \"p\" version",
|
|
"requires the version string", 1, 13);
|
|
assert_schema_error("project \"p\" version \"1.0\" extra",
|
|
"exactly three arguments", 1, 27);
|
|
|
|
/* target/feature/option names */
|
|
assert_schema_error("project \"p\" version \"1.0\"\ntarget",
|
|
"target requires a name argument", 2, 1);
|
|
assert_schema_error("project \"p\" version \"1.0\"\ntarget \"t\" \"u\"",
|
|
"takes exactly one argument", 2, 12);
|
|
assert_schema_error("project \"p\" version \"1.0\"\nfeature",
|
|
"feature requires a name argument", 2, 1);
|
|
assert_schema_error("project \"p\" version \"1.0\"\noption",
|
|
"option requires a name argument", 2, 1);
|
|
|
|
/* project-only: a build file needs at least one target or feature */
|
|
assert_schema_error("project \"p\" version \"1.0\"",
|
|
"missing 'target' or 'feature'", 1, 1);
|
|
assert_schema_error("project \"p\" version \"1.0\"\noption \"x\"",
|
|
"missing 'target' or 'feature'", 1, 1);
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
/* wrong argument types are rejected via the value model, naming what was
|
|
* expected and what was found. */
|
|
static MunitResult
|
|
test_wrong_arg_types(const MunitParameter params[], void *data)
|
|
{
|
|
(void)params;
|
|
(void)data;
|
|
|
|
assert_schema_error("project 42 version \"1.0\"",
|
|
"project name must be a non-empty string (got int)",
|
|
1, 9);
|
|
assert_schema_error("project \"p\" 99 \"1.0\"",
|
|
"expected the keyword 'version'", 1, 13);
|
|
assert_schema_error("project \"p\" version 42",
|
|
"version must be a non-empty string (got int)", 1, 21);
|
|
assert_schema_error("project \"p\" version \"\"",
|
|
"version must be a non-empty string", 1, 21);
|
|
assert_schema_error("project \"p\" version \"1.0\"\ntarget 42",
|
|
"target name must be a non-empty string (got int)",
|
|
2, 8);
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
/* project extras: properties and children are not part of the pinned
|
|
* grammar. */
|
|
static MunitResult
|
|
test_project_extras(const MunitParameter params[], void *data)
|
|
{
|
|
(void)params;
|
|
(void)data;
|
|
|
|
assert_schema_error("project \"p\" version \"1.0\" foo=1",
|
|
"unexpected property 'foo'", 1, 27);
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\" { target \"t\" { src \"a.c\" } }",
|
|
"takes no children", 1, 29);
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
/* (g) unknown target children name the child + span; known children get
|
|
* their argument/property/children shapes enforced. */
|
|
static MunitResult
|
|
test_target_children(const MunitParameter params[], void *data)
|
|
{
|
|
(void)params;
|
|
(void)data;
|
|
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\"\ntarget \"t\" { cc-flags \"-O2\" }",
|
|
"'cc-flags'", 2, 14);
|
|
assert_schema_error("project \"p\" version \"1.0\"\ntarget \"t\" { jobs 8 }",
|
|
"'jobs'", 2, 14);
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\"\ntarget \"t\" { src }",
|
|
"'src' requires a file argument", 2, 14);
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\"\ntarget \"t\" { src 42 }",
|
|
"'src' file must be a non-empty string (got int)", 2, 18);
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\"\ntarget \"t\" { src \"a.c\" extra }",
|
|
"'src' takes exactly one argument", 2, 24);
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\"\ntarget \"t\" { src \"a.c\" foo=1 }",
|
|
"'src' takes no properties", 2, 24);
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\"\ntarget \"t\" { src \"a.c\" { x } }",
|
|
"'src' takes no children", 2, 26);
|
|
|
|
/* feature references inside a target */
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\"\ntarget \"t\" { feature }",
|
|
"'feature' requires a name argument", 2, 14);
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\"\ntarget \"t\" { feature 42 }",
|
|
"'feature' name must be a non-empty string (got int)", 2, 22);
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\"\ntarget \"t\" { feature \"p\" foo=1 }",
|
|
"'feature' takes no properties", 2, 26);
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
/* feature children: structural validation only (the 8 check kinds are
|
|
* todo 10). Any check name passes provided it has >= 1 non-empty string
|
|
* argument and no children; check-kind semantics are out of scope here. */
|
|
static MunitResult
|
|
test_feature_checks_structural(const MunitParameter params[], void *data)
|
|
{
|
|
(void)params;
|
|
(void)data;
|
|
|
|
/* the two fixture kinds plus arbitrary names all pass structurally */
|
|
assert_valid("project \"p\" version \"1.0\"\n"
|
|
"feature \"f\" { header \"h.h\"\nlibrary \"l\" }");
|
|
assert_valid("project \"p\" version \"1.0\"\n"
|
|
"feature \"f\" { type \"size_t\"\nsizeof \"int\"\n"
|
|
"function \"strdup\"\nprogram \"gcc\"\n"
|
|
"compiler_flag \"-Wall\"\npkg_config \"zlib\" }");
|
|
assert_valid("project \"p\" version \"1.0\"\n"
|
|
"feature \"f\" { frobnicate \"x\" }");
|
|
|
|
/* but a check still needs its argument (a non-empty string) */
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\"\nfeature \"f\" { header }",
|
|
"check 'header' requires an argument", 2, 15);
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\"\nfeature \"f\" { header 42 }",
|
|
"check 'header' argument must be a non-empty string (got int)",
|
|
2, 22);
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\"\nfeature \"f\" { header \"h.h\" { x } }",
|
|
"check 'header' takes no children", 2, 30);
|
|
|
|
/* feature shapes */
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\"\nfeature \"f\" \"g\" { header \"h.h\" }",
|
|
"feature 'f' takes exactly one argument", 2, 13);
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\"\nfeature \"f\" foo=1 { header \"h.h\" }",
|
|
"feature 'f' has unexpected property 'foo'", 2, 13);
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
/* (f) option: `default` is an optional bool property; non-bool defaults
|
|
* are rejected with the value's span, as are other properties, duplicate
|
|
* defaults, children, and annotated values. */
|
|
static MunitResult
|
|
test_option_default(const MunitParameter params[], void *data)
|
|
{
|
|
(void)params;
|
|
(void)data;
|
|
|
|
assert_valid("project \"p\" version \"1.0\"\n"
|
|
"target \"t\" { src \"a.c\" }\n"
|
|
"option \"debug\" default=#true");
|
|
assert_valid("project \"p\" version \"1.0\"\n"
|
|
"target \"t\" { src \"a.c\" }\n"
|
|
"option \"x\"");
|
|
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\"\n"
|
|
"target \"t\" { src \"a.c\" }\n"
|
|
"option \"x\" default=\"yes\"",
|
|
"'default' must be a boolean (got string)", 3, 20);
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\"\n"
|
|
"target \"t\" { src \"a.c\" }\n"
|
|
"option \"x\" default=1",
|
|
"'default' must be a boolean (got int)", 3, 20);
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\"\n"
|
|
"target \"t\" { src \"a.c\" }\n"
|
|
"option \"x\" default=#null",
|
|
"'default' must be a boolean (got null)", 3, 20);
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\"\n"
|
|
"target \"t\" { src \"a.c\" }\n"
|
|
"option \"x\" default=(bool)#true",
|
|
"'default' must not carry a type annotation", 3, 26);
|
|
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\"\n"
|
|
"target \"t\" { src \"a.c\" }\n"
|
|
"option \"x\" foo=1",
|
|
"unexpected property 'foo' (only 'default' is allowed)", 3, 12);
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\"\n"
|
|
"target \"t\" { src \"a.c\" }\n"
|
|
"option \"x\" default=#true default=#false",
|
|
"duplicate 'default' property", 3, 26);
|
|
assert_schema_error(
|
|
"project \"p\" version \"1.0\"\n"
|
|
"target \"t\" { src \"a.c\" }\n"
|
|
"option \"x\" { default #true }",
|
|
"option 'x' takes no children", 3, 14);
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
static MunitTest tests[] = {
|
|
{ "/schema/valid-fixture", test_valid_fixture, NULL, NULL,
|
|
MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ "/schema/missing-project", test_missing_project, NULL, NULL,
|
|
MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ "/schema/project-not-first", test_project_not_first, NULL, NULL,
|
|
MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ "/schema/unknown-top-level", test_unknown_top_level, NULL, NULL,
|
|
MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ "/schema/duplicate-project", test_duplicate_project, NULL, NULL,
|
|
MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ "/schema/missing-args", test_missing_args, NULL, NULL,
|
|
MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ "/schema/wrong-arg-types", test_wrong_arg_types, NULL, NULL,
|
|
MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ "/schema/project-extras", test_project_extras, NULL, NULL,
|
|
MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ "/schema/target-children", test_target_children, NULL, NULL,
|
|
MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ "/schema/feature-checks-structural", test_feature_checks_structural,
|
|
NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ "/schema/option-default", test_option_default, NULL, NULL,
|
|
MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
|
};
|
|
|
|
static const MunitSuite suite = {
|
|
"/schema", 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);
|
|
}
|