Template
feat(detect): define feature-check registry schema
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
* check_registry.c - declarative feature-check kinds for stupidtools
|
||||
* (todo 10).
|
||||
*
|
||||
* The 8 check kinds a `feature` node's children can carry, their DSL
|
||||
* keyword spellings, and their argument/property shapes - as DATA (the
|
||||
* st_check_shapes table), plus the node->kind mapping. See
|
||||
* detect/check_registry.h for the semantics; this module deliberately
|
||||
* does NOT generate probes (todo 11) and does NOT execute anything.
|
||||
*
|
||||
* Check node names may be any KDL string form (identifier, quoted,
|
||||
* raw, multi-line): non-identifier names are interpreted through the
|
||||
* value model (kdl/value.h) before comparison. A value-model failure on
|
||||
* the name (bad escape etc.) is re-owned as a schema error, exactly
|
||||
* like src/kdl/schema.c does.
|
||||
*
|
||||
* ERRORS AND SPANS
|
||||
* ----------------
|
||||
* st_check_kind_from_node() reports failures as owned ST_ERR_KDL_SCHEMA
|
||||
* errors whose span is heap-allocated IN THE SAME BLOCK as the error
|
||||
* struct (the parser.c pattern), so err->span stays valid until
|
||||
* st_error_free() and is never dangling. The span points at the node's
|
||||
* NAME token (its `file` pointer borrows the caller's source buffer -
|
||||
* the same lifetime contract as the parser). The single span-less error
|
||||
* is the defensive NULL-node case: no source position exists to point
|
||||
* at.
|
||||
*
|
||||
* Copyright (c) 2026 huntedbytheirs
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "detect/check_registry.h"
|
||||
|
||||
#include "error.h"
|
||||
#include "kdl/ast.h"
|
||||
#include "kdl/value.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ---- the shape table (the data contract for todos 11/16) ------------- */
|
||||
|
||||
/* `library` and `pkg_config` carry an optional version constraint as two
|
||||
* further positional args: the literal `version` keyword followed by the
|
||||
* constraint string (see the header comment). */
|
||||
const struct st_check_shape st_check_shapes[ST_CHECK_KIND_COUNT] = {
|
||||
{ ST_CHECK_HEADER, "header", "header name", 1, 0 },
|
||||
{ ST_CHECK_FUNCTION, "function", "function name", 1, 0 },
|
||||
{ ST_CHECK_LIBRARY, "library", "library name (linked as -l<name>)",
|
||||
1, 2 },
|
||||
{ ST_CHECK_TYPE, "type", "type name", 1, 0 },
|
||||
{ ST_CHECK_SIZEOF, "sizeof", "type name", 1, 0 },
|
||||
{ ST_CHECK_PROGRAM, "program", "program name (checked via "
|
||||
"command -v)", 1, 0 },
|
||||
{ ST_CHECK_COMPILER_FLAG, "compiler_flag", "compiler flag", 1, 0 },
|
||||
{ ST_CHECK_PKG_CONFIG, "pkg_config", "pkg-config package name", 1,
|
||||
2 },
|
||||
};
|
||||
|
||||
/* ---- owned errors ----------------------------------------------------- */
|
||||
|
||||
/* Build an owned ST_ERR_KDL_SCHEMA error whose span lives in the same
|
||||
* allocation as the error struct (aligned right after it). Returns NULL
|
||||
* only on allocation failure. */
|
||||
static struct st_error *
|
||||
err_at_owned(struct st_span sp, const char *msg)
|
||||
{
|
||||
const size_t align = _Alignof(struct st_span);
|
||||
const size_t esize =
|
||||
(sizeof(struct st_error) + align - 1) & ~(align - 1);
|
||||
struct st_error *e;
|
||||
struct st_span *spc;
|
||||
size_t mlen;
|
||||
|
||||
if (msg == NULL) {
|
||||
msg = "";
|
||||
}
|
||||
e = malloc(esize + sizeof(struct st_span));
|
||||
if (e == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
mlen = strlen(msg);
|
||||
e->message = malloc(mlen + 1);
|
||||
if (e->message == NULL) {
|
||||
free(e);
|
||||
return NULL;
|
||||
}
|
||||
memcpy(e->message, msg, mlen + 1);
|
||||
e->category = ST_ERR_KDL_SCHEMA;
|
||||
spc = (struct st_span *)((unsigned char *)e + esize);
|
||||
*spc = sp;
|
||||
e->span = spc;
|
||||
return e;
|
||||
}
|
||||
|
||||
/* Re-own a value-model (ST_ERR_KDL_PARSE) error as a schema error: copy
|
||||
* the message and the span VALUE into a fresh block. The caller frees
|
||||
* the original error. */
|
||||
static struct st_error *
|
||||
schema_err_from(const struct st_error *e)
|
||||
{
|
||||
struct st_span sp;
|
||||
|
||||
if (e->span != NULL) {
|
||||
sp = *e->span;
|
||||
} else {
|
||||
sp = (struct st_span){ NULL, 0, 0 };
|
||||
}
|
||||
return err_at_owned(sp, e->message != NULL ? e->message : "");
|
||||
}
|
||||
|
||||
/* ---- name matching ---------------------------------------------------- */
|
||||
|
||||
/* Compare a node name token against a literal, accepting every string
|
||||
* form (identifiers compared directly; other forms via the value
|
||||
* model). Returns NULL and sets *out on success, or an owned schema
|
||||
* error when the name is a string the value model cannot interpret. */
|
||||
static struct st_error *
|
||||
ref_equals(const struct st_kdl_token_ref *name, const char *s, bool *out)
|
||||
{
|
||||
if (name->kind == ST_TOK_IDENT) {
|
||||
*out = name->len == strlen(s) &&
|
||||
memcmp(name->text, s, name->len) == 0;
|
||||
return NULL;
|
||||
}
|
||||
{
|
||||
struct st_kdl_value v;
|
||||
struct st_error *e = st_kdl_value_from_token(name, &v);
|
||||
|
||||
if (e != NULL) {
|
||||
struct st_error *r = schema_err_from(e);
|
||||
|
||||
st_error_free(e);
|
||||
*out = false;
|
||||
return r;
|
||||
}
|
||||
*out = v.kind == ST_KDL_VAL_STRING && strcmp(v.as.str, s) == 0;
|
||||
st_kdl_value_free(&v);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* Copy a node name token into a fixed buffer for MESSAGE text,
|
||||
* truncating to fit (message-only; never fatal). */
|
||||
static void
|
||||
name_into(const struct st_kdl_token_ref *name, char *buf, size_t cap)
|
||||
{
|
||||
if (cap == 0) {
|
||||
return;
|
||||
}
|
||||
if (name->kind == ST_TOK_IDENT) {
|
||||
size_t n = name->len < cap - 1 ? name->len : cap - 1;
|
||||
|
||||
memcpy(buf, name->text, n);
|
||||
buf[n] = '\0';
|
||||
return;
|
||||
}
|
||||
{
|
||||
struct st_kdl_value v;
|
||||
struct st_error *e = st_kdl_value_from_token(name, &v);
|
||||
|
||||
if (e == NULL && v.kind == ST_KDL_VAL_STRING) {
|
||||
snprintf(buf, cap, "%s", v.as.str);
|
||||
} else {
|
||||
snprintf(buf, cap, "?");
|
||||
}
|
||||
st_error_free(e);
|
||||
st_kdl_value_free(&v);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- public API ------------------------------------------------------- */
|
||||
|
||||
enum st_check_kind
|
||||
st_check_kind_from_node(const struct st_kdl_node *node,
|
||||
struct st_error **err)
|
||||
{
|
||||
char msg[192];
|
||||
size_t i;
|
||||
|
||||
if (err != NULL) {
|
||||
*err = NULL;
|
||||
}
|
||||
if (node == NULL) {
|
||||
if (err != NULL) {
|
||||
*err = st_error_kdl_schema("check node is NULL");
|
||||
}
|
||||
return ST_CHECK_KIND_COUNT;
|
||||
}
|
||||
if (node->name.len == 0) {
|
||||
if (err != NULL) {
|
||||
*err = err_at_owned(node->name.span,
|
||||
"check node has an empty name");
|
||||
}
|
||||
return ST_CHECK_KIND_COUNT;
|
||||
}
|
||||
for (i = 0; i < ST_CHECK_KIND_COUNT; i++) {
|
||||
bool eq = false;
|
||||
struct st_error *e = ref_equals(&node->name,
|
||||
st_check_shapes[i].name, &eq);
|
||||
|
||||
if (e != NULL) {
|
||||
if (err != NULL) {
|
||||
*err = e;
|
||||
} else {
|
||||
st_error_free(e);
|
||||
}
|
||||
return ST_CHECK_KIND_COUNT;
|
||||
}
|
||||
if (eq) {
|
||||
return st_check_shapes[i].kind;
|
||||
}
|
||||
}
|
||||
if (err != NULL) {
|
||||
char nm[65];
|
||||
|
||||
name_into(&node->name, nm, sizeof nm);
|
||||
snprintf(msg, sizeof msg,
|
||||
"unknown check kind '%s' (expected 'header', 'function', "
|
||||
"'library', 'type', 'sizeof', 'program', 'compiler_flag', "
|
||||
"or 'pkg_config')", nm);
|
||||
*err = err_at_owned(node->name.span, msg);
|
||||
}
|
||||
return ST_CHECK_KIND_COUNT;
|
||||
}
|
||||
|
||||
const char *
|
||||
st_check_kind_name(enum st_check_kind kind)
|
||||
{
|
||||
if ((size_t)kind >= ST_CHECK_KIND_COUNT) {
|
||||
return "?";
|
||||
}
|
||||
return st_check_shapes[kind].name;
|
||||
}
|
||||
|
||||
const struct st_check_shape *
|
||||
st_check_kind_shape(enum st_check_kind kind)
|
||||
{
|
||||
if ((size_t)kind >= ST_CHECK_KIND_COUNT) {
|
||||
return NULL;
|
||||
}
|
||||
return &st_check_shapes[kind];
|
||||
}
|
||||
|
||||
size_t
|
||||
st_check_kind_count(void)
|
||||
{
|
||||
return ST_CHECK_KIND_COUNT;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* check_registry.h - declarative feature-check kinds for stupidtools
|
||||
* (todo 10).
|
||||
*
|
||||
* A `feature "name" { check* }` block's children are CHECK nodes. Each
|
||||
* check node's NAME is one of eight fixed keywords - the check KINDS -
|
||||
* and its FIRST (and only) positional argument is the TARGET the check
|
||||
* is about:
|
||||
*
|
||||
* check := <name> <arg> props*
|
||||
*
|
||||
* The eight kinds mirror GNU Autoconf's "Existing Tests" (manual §5) and
|
||||
* pkg-config's PKG_CHECK_MODULES:
|
||||
*
|
||||
* header arg = header name -> #include <arg> probe
|
||||
* e.g. header "pthread.h"
|
||||
* function arg = function name -> link probe for the symbol
|
||||
* e.g. function "strdup"
|
||||
* library arg = library name -> -l<arg> link probe
|
||||
* e.g. library "pthread"
|
||||
* type arg = type name -> compile probe for the type
|
||||
* e.g. type "size_t"
|
||||
* sizeof arg = type name -> sizeof(<arg>) probe
|
||||
* e.g. sizeof "long"
|
||||
* program arg = program name -> command -v <arg> probe
|
||||
* e.g. program "pkg-config"
|
||||
* compiler_flag arg = flag -> compile probe with <arg> added
|
||||
* e.g. compiler_flag "-fsanitize=address"
|
||||
* pkg_config arg = package name -> pkg-config --cflags --libs <arg>
|
||||
* e.g. pkg_config "openssl"
|
||||
*
|
||||
* WHAT THIS TODO OWNS
|
||||
* -------------------
|
||||
* Only the DECLARATIVE SHAPE: the kind enumeration, the kind<->keyword
|
||||
* mapping, and per-kind argument/property shape as DATA (the table
|
||||
* below). It deliberately does NOT implement probe generation (todo 11)
|
||||
* or probe execution/caching (todo 12). src/kdl/schema.c consumes the
|
||||
* mapping + table to enforce the DSL (see its todo-10 HOOK); todos 11
|
||||
* and 16 consume the same table to generate probes and substitute
|
||||
* results - THIS HEADER IS THE CONTRACT for them.
|
||||
*
|
||||
* OPTIONAL `version` CONSTRAINT (library and pkg_config)
|
||||
* ------------------------------------------------------
|
||||
* `library` and `pkg_config` may carry an optional version constraint,
|
||||
* spelled EXACTLY like the project node's version (pinned by todo 9):
|
||||
* the literal keyword `version` followed by a constraint string, as two
|
||||
* further positional arguments:
|
||||
*
|
||||
* library "curl" version ">=7.0"
|
||||
* pkg_config "openssl" version ">=1.1"
|
||||
*
|
||||
* The constraint is "<op><version>": <op> one of >= <= = > <, <version>
|
||||
* a dotted numeric version. Todo 10 pins only the SHAPE (the keyword +
|
||||
* a non-empty unannotated string); the OPERATOR SEMANTICS are
|
||||
* interpreted by todo 11 (probe generation) and todo 16 (result
|
||||
* substitution). The spelling is an argument pair, not a KDL property,
|
||||
* because this parser classifies `version "..."` as two positional
|
||||
* arguments - the same reason the project node spells it that way. The
|
||||
* property form `version="..."` is NOT part of the DSL and is rejected
|
||||
* (see PROPERTIES below).
|
||||
*
|
||||
* PROPERTIES
|
||||
* ----------
|
||||
* No check kind accepts properties in this todo (the grammar's `props*`
|
||||
* is always empty for now); the schema validator rejects them with a
|
||||
* spanned error. The shape table carries no props column for the same
|
||||
* reason - todos 11/16 derive everything from the argument counts.
|
||||
*
|
||||
* ERRORS
|
||||
* ------
|
||||
* st_check_kind_from_node() reports unknown/empty check names as owned
|
||||
* ST_ERR_KDL_SCHEMA errors whose span is heap-allocated IN THE SAME
|
||||
* BLOCK as the error (the parser.c pattern), so err->span stays valid
|
||||
* until st_error_free(err). The span points at the node's name token.
|
||||
*
|
||||
* Copyright (c) 2026 huntedbytheirs
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#ifndef ST_DETECT_CHECK_REGISTRY_H
|
||||
#define ST_DETECT_CHECK_REGISTRY_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
struct st_error;
|
||||
struct st_kdl_node;
|
||||
|
||||
/* The eight declarative check kinds, in canonical order. */
|
||||
enum st_check_kind {
|
||||
ST_CHECK_HEADER = 0, /* header "pthread.h" */
|
||||
ST_CHECK_FUNCTION, /* function "strdup" */
|
||||
ST_CHECK_LIBRARY, /* library "pthread" -> -lpthread */
|
||||
ST_CHECK_TYPE, /* type "size_t" */
|
||||
ST_CHECK_SIZEOF, /* sizeof "long" */
|
||||
ST_CHECK_PROGRAM, /* program "pkg-config" -> command -v */
|
||||
ST_CHECK_COMPILER_FLAG, /* compiler_flag "-fsanitize=address" */
|
||||
ST_CHECK_PKG_CONFIG, /* pkg_config "openssl" */
|
||||
ST_CHECK_KIND_COUNT, /* sentinel: number of real kinds above */
|
||||
};
|
||||
|
||||
/* The DSL shape of one check kind, as DATA (consumed by the schema
|
||||
* validator and by todos 11/16). */
|
||||
struct st_check_shape {
|
||||
enum st_check_kind kind; /* the kind this row describes */
|
||||
const char *name; /* DSL keyword spelling, e.g. "header" */
|
||||
const char *arg_meaning; /* what the required argument names, e.g.
|
||||
"header name" */
|
||||
size_t required_args; /* positional args the node MUST carry */
|
||||
size_t optional_args; /* further positional args tolerated; 2
|
||||
for library/pkg_config = the literal
|
||||
`version` keyword followed by the
|
||||
version constraint string (see the
|
||||
header comment); 0 elsewhere */
|
||||
};
|
||||
|
||||
/* The registry table: one row per kind, indexed by the ST_CHECK_*
|
||||
* values (st_check_shapes[k].kind == k). */
|
||||
extern const struct st_check_shape st_check_shapes[ST_CHECK_KIND_COUNT];
|
||||
|
||||
/* Map a feature's check child node to its kind by the node NAME. The
|
||||
* name may be any KDL string form (bare identifier or quoted). Returns
|
||||
* the kind and leaves *err untouched on success. On failure returns
|
||||
* ST_CHECK_KIND_COUNT and sets *err to an owned ST_ERR_KDL_SCHEMA error
|
||||
* whose span points at the node's name: an unknown keyword, an empty
|
||||
* name, or (defensively) a NULL node. */
|
||||
enum st_check_kind st_check_kind_from_node(const struct st_kdl_node *node,
|
||||
struct st_error **err);
|
||||
|
||||
/* Stable DSL keyword for a kind (e.g. "header"). Kinds outside the
|
||||
* table yield "?" rather than indexing out of range. */
|
||||
const char *st_check_kind_name(enum st_check_kind kind);
|
||||
|
||||
/* The shape-table row for a kind, or NULL when `kind` is not a real
|
||||
* kind. */
|
||||
const struct st_check_shape *st_check_kind_shape(enum st_check_kind kind);
|
||||
|
||||
/* Number of real kinds (ST_CHECK_KIND_COUNT). */
|
||||
size_t st_check_kind_count(void);
|
||||
|
||||
#endif /* ST_DETECT_CHECK_REGISTRY_H */
|
||||
+125
-37
@@ -27,6 +27,7 @@
|
||||
|
||||
#include "kdl/schema.h"
|
||||
|
||||
#include "detect/check_registry.h"
|
||||
#include "error.h"
|
||||
#include "kdl/ast.h"
|
||||
#include "kdl/value.h"
|
||||
@@ -438,16 +439,124 @@ done:
|
||||
return e;
|
||||
}
|
||||
|
||||
/* One check child of a feature: its kind has already been resolved by
|
||||
* the registry (st_check_kind_from_node). Enforce the kind's DSL shape
|
||||
* from the registry table - the required positional argument (a
|
||||
* non-empty unannotated string), at most the kind's optional arguments
|
||||
* (for library/pkg_config: the literal `version` keyword followed by a
|
||||
* non-empty unannotated constraint string, mirroring the project
|
||||
* node's spelling), no properties, and no children. */
|
||||
static struct st_error *
|
||||
validate_check(const struct st_kdl_node *c, enum st_check_kind kind,
|
||||
const char *feature)
|
||||
{
|
||||
const struct st_check_shape *shape = st_check_kind_shape(kind);
|
||||
char msg[384];
|
||||
char cname[65];
|
||||
char cctx[192];
|
||||
struct st_error *e;
|
||||
struct st_kdl_arg *a;
|
||||
size_t nargs = 1;
|
||||
size_t max_args;
|
||||
size_t i;
|
||||
char *tmp = NULL;
|
||||
|
||||
name_into(&c->name, cname, sizeof cname);
|
||||
|
||||
if (c->args == NULL) {
|
||||
snprintf(msg, sizeof msg,
|
||||
"feature '%s': check '%s' requires an argument (the %s)",
|
||||
feature, cname, shape->arg_meaning);
|
||||
return err_at_owned(c->name.span, msg);
|
||||
}
|
||||
for (a = c->args->next; a != NULL; a = a->next) {
|
||||
nargs++;
|
||||
}
|
||||
max_args = shape->required_args + shape->optional_args;
|
||||
if (nargs > max_args) {
|
||||
/* span the first argument beyond the kind's shape */
|
||||
a = c->args;
|
||||
for (i = 0; i < max_args; i++) {
|
||||
a = a->next;
|
||||
}
|
||||
if (shape->optional_args == 0) {
|
||||
snprintf(msg, sizeof msg,
|
||||
"feature '%s': check '%s' takes exactly one argument",
|
||||
feature, cname);
|
||||
} else {
|
||||
snprintf(msg, sizeof msg,
|
||||
"feature '%s': check '%s' takes at most three arguments "
|
||||
"(the %s, then the optional 'version' keyword and its "
|
||||
"constraint)", feature, cname, shape->arg_meaning);
|
||||
}
|
||||
return err_at_owned(a->value.span, msg);
|
||||
}
|
||||
snprintf(cctx, sizeof cctx, "feature '%s': check '%s' argument",
|
||||
feature, cname);
|
||||
e = require_string_arg(&c->args->value, c->args->annotation, cctx,
|
||||
&tmp);
|
||||
free(tmp);
|
||||
if (e != NULL) {
|
||||
return e;
|
||||
}
|
||||
if (shape->optional_args == 2) {
|
||||
a = c->args->next;
|
||||
if (a != NULL) {
|
||||
if (a->annotation != NULL ||
|
||||
!(a->value.kind == ST_TOK_IDENT && a->value.len == 7 &&
|
||||
memcmp(a->value.text, "version", 7) == 0)) {
|
||||
snprintf(msg, sizeof msg,
|
||||
"feature '%s': check '%s': expected the keyword "
|
||||
"'version' as the second argument", feature, cname);
|
||||
return err_at_owned(a->value.span, msg);
|
||||
}
|
||||
if (a->next == NULL) {
|
||||
snprintf(msg, sizeof msg,
|
||||
"feature '%s': check '%s': 'version' requires the "
|
||||
"version constraint argument", feature, cname);
|
||||
return err_at_owned(a->value.span, msg);
|
||||
}
|
||||
{
|
||||
char vctx[192];
|
||||
|
||||
snprintf(vctx, sizeof vctx,
|
||||
"feature '%s': check '%s' version", feature,
|
||||
cname);
|
||||
e = require_string_arg(&a->next->value,
|
||||
a->next->annotation, vctx, &tmp);
|
||||
free(tmp);
|
||||
if (e != NULL) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (c->props != NULL) {
|
||||
char key[65];
|
||||
|
||||
name_into(&c->props->key, key, sizeof key);
|
||||
snprintf(msg, sizeof msg,
|
||||
"feature '%s': check '%s' has unexpected property '%s' "
|
||||
"(checks take no properties)", feature, cname, key);
|
||||
return err_at_owned(c->props->key.span, msg);
|
||||
}
|
||||
if (c->children != NULL) {
|
||||
snprintf(msg, sizeof msg,
|
||||
"feature '%s': check '%s' takes no children", feature, cname);
|
||||
return err_at_owned(c->children->name.span, msg);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* feature "name" { <checks> }: the name shape (no properties) plus a
|
||||
* children block of CHECK nodes, validated STRUCTURALLY ONLY. The 8
|
||||
* check kinds and their exact argument/property shapes are todo 10's job;
|
||||
* until its registry lands, every child is accepted provided it has at
|
||||
* least one non-empty string argument and no children (its properties
|
||||
* are deliberately left for todo 10 to validate). */
|
||||
* children block of CHECK nodes. Each child's NAME is dispatched through
|
||||
* the feature-check registry (src/detect/check_registry.h, todo 10) to
|
||||
* one of the 8 kinds - header, function, library, type, sizeof,
|
||||
* program, compiler_flag, pkg_config - and its argument/property shape
|
||||
* is enforced from the registry table (see validate_check). */
|
||||
static struct st_error *
|
||||
validate_feature(const struct st_kdl_node *n)
|
||||
{
|
||||
char msg[192];
|
||||
char name[65];
|
||||
struct st_error *e;
|
||||
char *name_str = NULL;
|
||||
@@ -461,6 +570,7 @@ validate_feature(const struct st_kdl_node *n)
|
||||
|
||||
if (n->props != NULL) {
|
||||
char key[65];
|
||||
char msg[192];
|
||||
|
||||
name_into(&n->props->key, key, sizeof key);
|
||||
snprintf(msg, sizeof msg, "feature '%s' has unexpected property "
|
||||
@@ -471,44 +581,22 @@ validate_feature(const struct st_kdl_node *n)
|
||||
|
||||
for (c = n->children; c != NULL; c = c->next) {
|
||||
/*
|
||||
* HOOK (todo 10): dispatch on the check node NAME through the
|
||||
* HOOK (todo 10): dispatch the check node NAME through the
|
||||
* feature-check registry (src/detect/check_registry.h) to
|
||||
* validate the 8 kinds - header, function, library, type,
|
||||
* sizeof, program, compiler_flag, pkg_config - and their
|
||||
* argument/property shapes. Until then, structural checks only:
|
||||
* argument/property shapes.
|
||||
*/
|
||||
if (c->args == NULL) {
|
||||
char cname[65];
|
||||
enum st_check_kind kind;
|
||||
struct st_error *ke = NULL;
|
||||
|
||||
name_into(&c->name, cname, sizeof cname);
|
||||
snprintf(msg, sizeof msg,
|
||||
"feature '%s': check '%s' requires an argument", name,
|
||||
cname);
|
||||
e = err_at_owned(c->name.span, msg);
|
||||
kind = st_check_kind_from_node(c, &ke);
|
||||
if (ke != NULL) {
|
||||
e = ke;
|
||||
goto done;
|
||||
}
|
||||
{
|
||||
char cname[65];
|
||||
char cctx[192];
|
||||
char *tmp = NULL;
|
||||
|
||||
name_into(&c->name, cname, sizeof cname);
|
||||
snprintf(cctx, sizeof cctx, "feature '%s': check '%s' argument",
|
||||
name, cname);
|
||||
e = require_string_arg(&c->args->value, c->args->annotation,
|
||||
cctx, &tmp);
|
||||
free(tmp);
|
||||
if (e != NULL) {
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
if (c->children != NULL) {
|
||||
char cname[65];
|
||||
|
||||
name_into(&c->name, cname, sizeof cname);
|
||||
snprintf(msg, sizeof msg,
|
||||
"feature '%s': check '%s' takes no children", name, cname);
|
||||
e = err_at_owned(c->children->name.span, msg);
|
||||
e = validate_check(c, kind, name);
|
||||
if (e != NULL) {
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,551 @@
|
||||
/* 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 <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. 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" { <kw> <arg> }` (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);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
/* LINK: ../../src/kdl/schema.c ../../src/kdl/value.c ../../src/kdl/parser.c ../../src/kdl/lexer.c ../../src/error.c ../../src/span.c */
|
||||
/* LINK: ../../src/kdl/schema.c ../../src/detect/check_registry.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).
|
||||
@@ -396,24 +396,29 @@ test_target_children(const MunitParameter params[], void *data)
|
||||
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. */
|
||||
/* feature children: each check's NAME must be one of the 8 kinds from
|
||||
* the feature-check registry (todo 10), and the kind's argument shape is
|
||||
* enforced (exactly one non-empty string target, no children; the
|
||||
* optional `version` property is todo 10's too - see
|
||||
* test_check_registry.c for the per-kind table). */
|
||||
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 */
|
||||
/* the two fixture kinds plus the rest of the 8 kinds pass */
|
||||
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\" }");
|
||||
|
||||
/* unknown check kinds are rejected (registry dispatch, todo 10) */
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\nfeature \"f\" { frobnicate \"x\" }",
|
||||
"'frobnicate'", 2, 15);
|
||||
|
||||
/* but a check still needs its argument (a non-empty string) */
|
||||
assert_schema_error(
|
||||
|
||||
Reference in New Issue
Block a user