Template
733 lines
24 KiB
C
733 lines
24 KiB
C
/* LINK: ../../src/detect/resolve.c ../../src/gen/sh_emit.c ../../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 */
|
|
|
|
#ifndef _POSIX_C_SOURCE
|
|
#define _POSIX_C_SOURCE 200809L /* mkdtemp, system/WEXITSTATUS */
|
|
#endif
|
|
|
|
/*
|
|
* tests/unit/test_resolve.c
|
|
*
|
|
* Unit tests for feature resolution + `when` guards (todo 14):
|
|
* src/detect/resolve.c. THE MODEL: guards are PARSED + EMITTED here, at
|
|
* stupidtools generation time, but they are EVALUATED at configure time
|
|
* inside the generated ./configure — against the shell variables
|
|
* $have_<feature> (set by todo 12's probe snippets) and the normalized
|
|
* $st_os. These tests prove the emitted guard shell is syntactically valid
|
|
* (sh/bash/zsh -n), genuinely evaluates (real `sh` execution against
|
|
* seeded variables), parses malformed guards into spanned errors, maps the
|
|
* os table both C-side and via the emitted shell, and that the schema
|
|
* accepts the `when` feature property (the todo-14 DSL placement).
|
|
*
|
|
* The magic LINK comment on line 1 is REQUIRED by tests/run.sh (extra .c
|
|
* sources, relative to tests/unit/). resolve.c needs sh_emit.c (st_sh_quote)
|
|
* + error.c + span.c; schema.c/check_registry.c/value.c/parser.c/lexer.c
|
|
* are linked for the schema `when` acceptance test (st_kdl_parse +
|
|
* st_kdl_validate).
|
|
*/
|
|
#include "munit.h"
|
|
|
|
#include "detect/resolve.h"
|
|
#include "error.h"
|
|
#include "kdl/ast.h"
|
|
#include "kdl/schema.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
/* ---- per-test temp dir ------------------------------------------------- */
|
|
|
|
static char temp_dir[128];
|
|
|
|
static void *
|
|
setup(const MunitParameter params[], void *user_data)
|
|
{
|
|
(void)params;
|
|
(void)user_data;
|
|
int n = snprintf(temp_dir, sizeof temp_dir, "/tmp/st_resolve_XXXXXX");
|
|
|
|
if (n < 0) {
|
|
return NULL;
|
|
}
|
|
if (mkdtemp(temp_dir) == NULL) {
|
|
return NULL;
|
|
}
|
|
return (void *)1;
|
|
}
|
|
|
|
static void
|
|
teardown(void *fixture)
|
|
{
|
|
char cmd[160];
|
|
int n;
|
|
|
|
if (fixture == NULL || temp_dir[0] == '\0') {
|
|
return;
|
|
}
|
|
n = snprintf(cmd, sizeof cmd, "rm -rf -- '%s'", temp_dir);
|
|
if (n < 0 || (size_t)n >= sizeof cmd) {
|
|
return;
|
|
}
|
|
(void)system(cmd);
|
|
temp_dir[0] = '\0';
|
|
}
|
|
|
|
/* Join temp_dir/`name` into `buf`; returns the snprintf result so callers
|
|
* can assert it without ever stringifying a %-carrying format (the munit
|
|
* %s-stringification trap, see .omo/notepads/stupidtools/learnings.md). */
|
|
static int
|
|
mkpath(char *buf, size_t sz, const char *name)
|
|
{
|
|
return snprintf(buf, sz, "%s/%s", temp_dir, name);
|
|
}
|
|
|
|
/* Read a tiny result file under temp_dir, strip the trailing newline.
|
|
* Returns a static buffer (empty string when the file is missing). */
|
|
static const char *
|
|
read_result(const char *file)
|
|
{
|
|
static char buf[256];
|
|
char path[600];
|
|
FILE *f;
|
|
size_t n;
|
|
int m;
|
|
|
|
m = mkpath(path, sizeof path, file);
|
|
if (m < 0) {
|
|
return "";
|
|
}
|
|
f = fopen(path, "r");
|
|
if (f == NULL) {
|
|
return "";
|
|
}
|
|
n = fread(buf, 1, sizeof buf - 1, f);
|
|
fclose(f);
|
|
buf[n] = '\0';
|
|
buf[strcspn(buf, "\n")] = '\0';
|
|
return buf;
|
|
}
|
|
|
|
/* Run "sh <path>" via system(); return the shell's exit status or -1. */
|
|
static int
|
|
run_sh(const char *path)
|
|
{
|
|
char cmd[700];
|
|
int rc;
|
|
int n = snprintf(cmd, sizeof cmd, "sh '%s'", path);
|
|
|
|
if (n < 0 || (size_t)n >= sizeof cmd) {
|
|
return -1;
|
|
}
|
|
rc = system(cmd);
|
|
if (rc == -1) {
|
|
return -1;
|
|
}
|
|
return WEXITSTATUS(rc);
|
|
}
|
|
|
|
/* Run "sh <path>" capturing stdout into out_path; return exit status. */
|
|
static int
|
|
run_sh_out(const char *path, const char *out_path)
|
|
{
|
|
char cmd[1400];
|
|
int rc;
|
|
int n = snprintf(cmd, sizeof cmd, "sh '%s' > '%s' 2>/dev/null", path,
|
|
out_path);
|
|
|
|
if (n < 0 || (size_t)n >= sizeof cmd) {
|
|
return -1;
|
|
}
|
|
rc = system(cmd);
|
|
if (rc == -1) {
|
|
return -1;
|
|
}
|
|
return WEXITSTATUS(rc);
|
|
}
|
|
|
|
/* "<shell> -n <path>": return the shell's exit status or -1. */
|
|
static int
|
|
syntax_check(const char *shell, const char *path)
|
|
{
|
|
char cmd[700];
|
|
int rc;
|
|
int n = snprintf(cmd, sizeof cmd, "%s -n '%s'", shell, path);
|
|
|
|
if (n < 0 || (size_t)n >= sizeof cmd) {
|
|
return -1;
|
|
}
|
|
rc = system(cmd);
|
|
if (rc == -1) {
|
|
return -1;
|
|
}
|
|
return WEXITSTATUS(rc);
|
|
}
|
|
|
|
/* ---- guard emission helpers ------------------------------------------- */
|
|
|
|
/* Parse + emit a guard into a static buffer; returns the emitted bytes. */
|
|
static const char *
|
|
emit_guard_text(const char *guard)
|
|
{
|
|
static char buf[1024];
|
|
FILE *f = tmpfile();
|
|
struct st_when_ast *ast = NULL;
|
|
size_t n;
|
|
|
|
munit_assert_not_null(f);
|
|
munit_assert_null(st_when_parse(guard, &ast));
|
|
munit_assert_not_null(ast);
|
|
munit_assert_null(st_when_emit(f, ast));
|
|
st_when_free(ast);
|
|
munit_assert_int(fflush(f), ==, 0);
|
|
munit_assert_int(fseek(f, 0, SEEK_SET), ==, 0);
|
|
n = fread(buf, 1, sizeof buf - 1, f);
|
|
fclose(f);
|
|
buf[n] = '\0';
|
|
return buf;
|
|
}
|
|
|
|
/* Write a script that evaluates `guard` under `assigns` (raw "var=value"
|
|
* lines, test-controlled constants) and echoes yes/no to stdout; capture
|
|
* and return the result ("yes" or "no"). */
|
|
static const char *
|
|
guard_eval(const char *guard, const char *const *assigns, size_t nassign)
|
|
{
|
|
char path[600];
|
|
char out[600];
|
|
FILE *f;
|
|
struct st_when_ast *ast = NULL;
|
|
size_t i;
|
|
int m;
|
|
|
|
m = mkpath(path, sizeof path, "guard.sh");
|
|
munit_assert_int(m, >, 0);
|
|
m = mkpath(out, sizeof out, "guard.out");
|
|
munit_assert_int(m, >, 0);
|
|
|
|
f = fopen(path, "w");
|
|
munit_assert_not_null(f);
|
|
(void)fprintf(f, "#!/bin/sh\n");
|
|
for (i = 0; i < nassign; i++) {
|
|
(void)fprintf(f, "%s\n", assigns[i]);
|
|
}
|
|
(void)fprintf(f, "if ");
|
|
munit_assert_null(st_when_parse(guard, &ast));
|
|
munit_assert_not_null(ast);
|
|
munit_assert_null(st_when_emit(f, ast));
|
|
st_when_free(ast);
|
|
(void)fprintf(f, "; then\n echo yes\nelse\n echo no\nfi\n");
|
|
fclose(f);
|
|
|
|
munit_assert_int(run_sh_out(path, out), ==, 0);
|
|
return read_result("guard.out");
|
|
}
|
|
|
|
/* Emit one feature's resolution under `assigns`, then dump have_<name>
|
|
* into feature.out; return that value. */
|
|
static const char *
|
|
feature_eval(const struct st_resolve_feature *f,
|
|
const char *const *assigns, size_t nassign)
|
|
{
|
|
char path[600];
|
|
char out[600];
|
|
FILE *fp;
|
|
size_t i;
|
|
int m;
|
|
|
|
m = mkpath(path, sizeof path, "feature.sh");
|
|
munit_assert_int(m, >, 0);
|
|
m = mkpath(out, sizeof out, "feature.out");
|
|
munit_assert_int(m, >, 0);
|
|
|
|
fp = fopen(path, "w");
|
|
munit_assert_not_null(fp);
|
|
(void)fprintf(fp, "#!/bin/sh\n");
|
|
for (i = 0; i < nassign; i++) {
|
|
(void)fprintf(fp, "%s\n", assigns[i]);
|
|
}
|
|
munit_assert_null(st_resolve_emit_feature(fp, f));
|
|
(void)fprintf(fp, "printf '%%s\\n' \"$have_%s\" > '%s'\n", f->name, out);
|
|
fclose(fp);
|
|
|
|
munit_assert_int(run_sh(path), ==, 0);
|
|
return read_result("feature.out");
|
|
}
|
|
|
|
/* Run the emitted st_os_norm() over `raw`, returning the normalized name
|
|
* the shell produced (the raw uname -s string is assigned to $st_os). */
|
|
static const char *
|
|
os_norm_shell(const char *raw)
|
|
{
|
|
char path[600];
|
|
char out[600];
|
|
FILE *f;
|
|
int m;
|
|
|
|
m = mkpath(path, sizeof path, "osnorm.sh");
|
|
munit_assert_int(m, >, 0);
|
|
m = mkpath(out, sizeof out, "osnorm.out");
|
|
munit_assert_int(m, >, 0);
|
|
|
|
f = fopen(path, "w");
|
|
munit_assert_not_null(f);
|
|
(void)fprintf(f, "#!/bin/sh\n");
|
|
munit_assert_null(st_resolve_emit_os_norm(f));
|
|
(void)fprintf(f, "st_os='%s'\nst_os_norm\nprintf '%%s\\n' \"$st_os\" > '%s'\n",
|
|
raw, out);
|
|
fclose(f);
|
|
|
|
munit_assert_int(run_sh(path), ==, 0);
|
|
return read_result("osnorm.out");
|
|
}
|
|
|
|
/* ---- (content) parse + emit ------------------------------------------- */
|
|
|
|
static MunitResult
|
|
test_parse_emit_content(const MunitParameter params[], void *data)
|
|
{
|
|
(void)params;
|
|
(void)data;
|
|
struct st_when_ast *ast = NULL;
|
|
const char *s;
|
|
|
|
/* os=linux and have_pthread parses and emits the two leaf tests ANDed */
|
|
s = emit_guard_text("os=linux and have_pthread");
|
|
munit_assert_not_null(strstr(s, "\"$st_os\" = 'linux'"));
|
|
munit_assert_not_null(strstr(s, "\"$have_pthread\" = \"yes\""));
|
|
munit_assert_not_null(strstr(s, "&&"));
|
|
|
|
/* not os=macos: negated os test */
|
|
s = emit_guard_text("not os=macos");
|
|
munit_assert_not_null(strstr(s, "!"));
|
|
munit_assert_not_null(strstr(s, "\"$st_os\" = 'macos'"));
|
|
|
|
/* NULL / empty guard -> NULL ast (always true) */
|
|
munit_assert_null(st_when_parse(NULL, &ast));
|
|
munit_assert_null(ast);
|
|
munit_assert_null(st_when_parse("", &ast));
|
|
munit_assert_null(ast);
|
|
|
|
/* NULL out is a usage error, not a crash */
|
|
{
|
|
struct st_error *err = st_when_parse("os=linux", NULL);
|
|
|
|
munit_assert_not_null(err);
|
|
munit_assert_int(st_error_category_of(err), ==, ST_ERR_USAGE);
|
|
st_error_free(err);
|
|
}
|
|
/* NULL stream is a usage error */
|
|
{
|
|
struct st_when_ast *a = NULL;
|
|
struct st_error *err;
|
|
|
|
munit_assert_null(st_when_parse("os=linux", &a));
|
|
munit_assert_not_null(a);
|
|
err = st_when_emit(NULL, a);
|
|
munit_assert_not_null(err);
|
|
munit_assert_int(st_error_category_of(err), ==, ST_ERR_USAGE);
|
|
st_error_free(err);
|
|
st_when_free(a);
|
|
}
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
/* ---- (a) os=linux and have_pthread: real sh eval ---------------------- */
|
|
|
|
static MunitResult
|
|
test_guard_eval(const MunitParameter params[], void *data)
|
|
{
|
|
(void)params;
|
|
(void)data;
|
|
static const char *const yes[] = { "have_pthread=yes", "st_os=linux" };
|
|
static const char *const no[] = { "have_pthread=no", "st_os=linux" };
|
|
|
|
munit_assert_string_equal(
|
|
guard_eval("os=linux and have_pthread", yes, 2), "yes");
|
|
munit_assert_string_equal(
|
|
guard_eval("os=linux and have_pthread", no, 2), "no");
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
/* ---- (b) not os=macos: true on linux, false on macos ------------------ */
|
|
|
|
static MunitResult
|
|
test_not_os(const MunitParameter params[], void *data)
|
|
{
|
|
(void)params;
|
|
(void)data;
|
|
static const char *const linux[] = { "st_os=linux" };
|
|
static const char *const macos[] = { "st_os=macos" };
|
|
|
|
munit_assert_string_equal(guard_eval("not os=macos", linux, 1), "yes");
|
|
munit_assert_string_equal(guard_eval("not os=macos", macos, 1), "no");
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
/* ---- (c) precedence: have_a or (have_b and os=linux) ------------------ */
|
|
|
|
static MunitResult
|
|
test_precedence(const MunitParameter params[], void *data)
|
|
{
|
|
(void)params;
|
|
(void)data;
|
|
static const char *const c1[] = { "have_a=yes", "have_b=no",
|
|
"st_os=macos" };
|
|
static const char *const c2[] = { "have_a=no", "have_b=yes",
|
|
"st_os=linux" };
|
|
static const char *const c3[] = { "have_a=no", "have_b=yes",
|
|
"st_os=macos" };
|
|
|
|
/* c1: have_a true -> true regardless of the right operand */
|
|
munit_assert_string_equal(
|
|
guard_eval("have_a or (have_b and os=linux)", c1, 3), "yes");
|
|
/* c2: have_a false, (have_b && os=linux) true -> true */
|
|
munit_assert_string_equal(
|
|
guard_eval("have_a or (have_b and os=linux)", c2, 3), "yes");
|
|
/* c3: have_a false and (have_b && os=linux) false -> false */
|
|
munit_assert_string_equal(
|
|
guard_eval("have_a or (have_b and os=linux)", c3, 3), "no");
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
/* ---- (d) malformed guards -> spanned errors --------------------------- */
|
|
|
|
static void
|
|
assert_parse_error(const char *guard, const char *needle, size_t col)
|
|
{
|
|
struct st_when_ast *ast = NULL;
|
|
struct st_error *err = NULL;
|
|
|
|
err = st_when_parse(guard, &ast);
|
|
munit_assert_not_null(err);
|
|
munit_assert_null(ast);
|
|
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, ==, col);
|
|
munit_assert_true(strstr(st_error_message(err), needle) != NULL);
|
|
st_error_free(err);
|
|
}
|
|
|
|
static MunitResult
|
|
test_parse_errors(const MunitParameter params[], void *data)
|
|
{
|
|
(void)params;
|
|
(void)data;
|
|
|
|
/* the acceptance case: an unbalanced paren */
|
|
assert_parse_error("((x", "unknown token 'x'", 3);
|
|
/* empty feature name after have_ */
|
|
assert_parse_error("have_", "missing feature name", 1);
|
|
/* invalid feature identifier */
|
|
assert_parse_error("have_1x", "invalid feature name", 1);
|
|
/* empty os name */
|
|
assert_parse_error("os=", "missing os name", 1);
|
|
/* unknown word */
|
|
assert_parse_error("bogus", "unknown token 'bogus'", 1);
|
|
/* unterminated group */
|
|
assert_parse_error("(", "unexpected end", 2);
|
|
/* trailing close paren */
|
|
assert_parse_error("os=linux )", "unexpected token ')'", 10);
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
/* ---- (e) os mapping: C-side table + emitted shell --------------------- */
|
|
|
|
static MunitResult
|
|
test_os_mapping(const MunitParameter params[], void *data)
|
|
{
|
|
(void)params;
|
|
(void)data;
|
|
|
|
/* C-side mapping function */
|
|
munit_assert_string_equal(st_os_normalize("Linux"), "linux");
|
|
munit_assert_string_equal(st_os_normalize("Darwin"), "macos");
|
|
munit_assert_string_equal(st_os_normalize("FreeBSD"), "bsd");
|
|
munit_assert_string_equal(st_os_normalize("OpenBSD"), "bsd");
|
|
munit_assert_string_equal(st_os_normalize("NetBSD"), "bsd");
|
|
munit_assert_string_equal(st_os_normalize("Solaris"), "other");
|
|
munit_assert_string_equal(st_os_normalize("Linux-gnu"), "other");
|
|
munit_assert_string_equal(st_os_normalize(NULL), "other");
|
|
|
|
/* the emitted st_os_norm() reproduces the mapping under real sh */
|
|
munit_assert_string_equal(os_norm_shell("Linux"), "linux");
|
|
munit_assert_string_equal(os_norm_shell("Darwin"), "macos");
|
|
munit_assert_string_equal(os_norm_shell("FreeBSD"), "bsd");
|
|
munit_assert_string_equal(os_norm_shell("OpenBSD"), "bsd");
|
|
munit_assert_string_equal(os_norm_shell("Solaris"), "other");
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
/* ---- (f) emitted guard shells pass sh -n / bash -n / zsh -n ----------- */
|
|
|
|
static MunitResult
|
|
test_syntax(const MunitParameter params[], void *data)
|
|
{
|
|
(void)params;
|
|
(void)data;
|
|
static const char *const banned[] = {
|
|
"[[ ", "]]", "local ", "==", "<<<", "&>", "set -e",
|
|
};
|
|
static const char *const guards[] = {
|
|
"os=linux and have_pthread",
|
|
"not os=macos",
|
|
"have_a or (have_b and os=linux)",
|
|
"not (have_a and not os=bsd)",
|
|
"os=other",
|
|
};
|
|
char path[600];
|
|
FILE *f;
|
|
struct st_when_ast *ast = NULL;
|
|
size_t i;
|
|
int m;
|
|
char *bytes;
|
|
long n;
|
|
|
|
m = mkpath(path, sizeof path, "syntax.sh");
|
|
munit_assert_int(m, >, 0);
|
|
f = fopen(path, "w");
|
|
munit_assert_not_null(f);
|
|
(void)fprintf(f, "#!/bin/sh\n");
|
|
munit_assert_null(st_resolve_emit_os_norm(f));
|
|
for (i = 0; i < sizeof guards / sizeof guards[0]; i++) {
|
|
(void)fprintf(f, "if ");
|
|
munit_assert_null(st_when_parse(guards[i], &ast));
|
|
munit_assert_not_null(ast);
|
|
munit_assert_null(st_when_emit(f, ast));
|
|
st_when_free(ast);
|
|
(void)fprintf(f, "; then :; fi\n");
|
|
}
|
|
fclose(f);
|
|
|
|
munit_assert_int(syntax_check("sh", path), ==, 0);
|
|
munit_assert_int(syntax_check("bash", path), ==, 0);
|
|
munit_assert_int(syntax_check("zsh", path), ==, 0);
|
|
|
|
/* banned-construct sweep on the real emitted bytes */
|
|
f = fopen(path, "rb");
|
|
munit_assert_not_null(f);
|
|
munit_assert_int(fseek(f, 0, SEEK_END), ==, 0);
|
|
n = ftell(f);
|
|
munit_assert_int(n, >, 0);
|
|
munit_assert_int(fseek(f, 0, SEEK_SET), ==, 0);
|
|
bytes = munit_malloc((size_t)n + 1);
|
|
munit_assert_size(fread(bytes, 1, (size_t)n, f), ==, (size_t)n);
|
|
fclose(f);
|
|
bytes[n] = '\0';
|
|
for (i = 0; i < sizeof banned / sizeof banned[0]; i++) {
|
|
munit_assert_null(strstr(bytes, banned[i]));
|
|
}
|
|
free(bytes);
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
/* ---- feature aggregation: multi-check AND ----------------------------- */
|
|
|
|
static MunitResult
|
|
test_emit_feature_aggregate(const MunitParameter params[], void *data)
|
|
{
|
|
(void)params;
|
|
(void)data;
|
|
static const char *const both[] = { "have_pthread_0=yes",
|
|
"have_pthread_1=yes" };
|
|
static const char *const one[] = { "have_pthread_0=yes",
|
|
"have_pthread_1=no" };
|
|
static const char *const none[] = { "have_pthread_0=no",
|
|
"have_pthread_1=no" };
|
|
const char *check_names[2] = { "pthread_0", "pthread_1" };
|
|
struct st_resolve_feature f = { "pthread", check_names, 2, NULL };
|
|
|
|
munit_assert_string_equal(feature_eval(&f, both, 2), "yes");
|
|
munit_assert_string_equal(feature_eval(&f, one, 2), "no");
|
|
munit_assert_string_equal(feature_eval(&f, none, 2), "no");
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
/* ---- feature guard gating: when-guard forces have_<f>=no ------------- */
|
|
|
|
static MunitResult
|
|
test_emit_feature_guard(const MunitParameter params[], void *data)
|
|
{
|
|
(void)params;
|
|
(void)data;
|
|
const char *check_names[1] = { "pthread" };
|
|
struct st_when_ast *ast = NULL;
|
|
struct st_resolve_feature f;
|
|
static const char *const linux[] = { "have_pthread=yes", "st_os=linux" };
|
|
static const char *const macos[] = { "have_pthread=yes", "st_os=macos" };
|
|
|
|
munit_assert_null(st_when_parse("os=linux", &ast));
|
|
f.name = "pthread";
|
|
f.check_names = check_names;
|
|
f.check_count = 1;
|
|
f.guard = ast;
|
|
|
|
/* guard holds -> the probe's have_pthread=yes survives */
|
|
munit_assert_string_equal(feature_eval(&f, linux, 2), "yes");
|
|
/* guard fails -> have_pthread is forced to no */
|
|
munit_assert_string_equal(feature_eval(&f, macos, 2), "no");
|
|
|
|
st_when_free(ast);
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
/* ---- check-name scheme (the todo-12/16 naming contract) --------------- */
|
|
|
|
static MunitResult
|
|
test_check_name(const MunitParameter params[], void *data)
|
|
{
|
|
(void)params;
|
|
(void)data;
|
|
char buf[64];
|
|
|
|
/* single-check feature -> the feature name itself */
|
|
munit_assert_int(st_resolve_check_name("pthread", 0, 1, buf,
|
|
sizeof buf), ==, 0);
|
|
munit_assert_string_equal(buf, "pthread");
|
|
|
|
/* multi-check feature -> <name>_0, <name>_1, ... */
|
|
munit_assert_int(st_resolve_check_name("pthread", 0, 2, buf,
|
|
sizeof buf), ==, 0);
|
|
munit_assert_string_equal(buf, "pthread_0");
|
|
munit_assert_int(st_resolve_check_name("pthread", 1, 2, buf,
|
|
sizeof buf), ==, 0);
|
|
munit_assert_string_equal(buf, "pthread_1");
|
|
|
|
/* invalid feature name / out-of-range index / zero count -> -1 */
|
|
munit_assert_int(st_resolve_check_name("1bad", 0, 1, buf,
|
|
sizeof buf), ==, -1);
|
|
munit_assert_int(st_resolve_check_name("pthread", 1, 1, buf,
|
|
sizeof buf), ==, -1);
|
|
munit_assert_int(st_resolve_check_name("pthread", 0, 0, buf,
|
|
sizeof buf), ==, -1);
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
/* ---- schema: the `when` feature property (todo-14 placement) ---------- */
|
|
|
|
static MunitResult
|
|
test_schema_when(const MunitParameter params[], void *data)
|
|
{
|
|
(void)params;
|
|
(void)data;
|
|
struct st_error *err = NULL;
|
|
struct st_kdl_document *doc;
|
|
|
|
/* a feature may carry one `when "<expr>"` property */
|
|
doc = st_kdl_parse("project \"p\" version \"1.0\"\n"
|
|
"feature \"pthread\" when=\"os=linux\" "
|
|
"{ header \"pthread.h\" }",
|
|
"t.kdl", &err);
|
|
munit_assert_null(err);
|
|
munit_assert_not_null(doc);
|
|
munit_assert_null(st_kdl_validate(doc));
|
|
st_kdl_document_free(doc);
|
|
|
|
/* the guard string is NOT parsed by the schema (that is resolve's job):
|
|
* a malformed guard is still structurally a non-empty string */
|
|
doc = st_kdl_parse("project \"p\" version \"1.0\"\n"
|
|
"feature \"pthread\" when=\"((x\" "
|
|
"{ header \"pthread.h\" }",
|
|
"t.kdl", &err);
|
|
munit_assert_null(err);
|
|
munit_assert_not_null(doc);
|
|
munit_assert_null(st_kdl_validate(doc));
|
|
st_kdl_document_free(doc);
|
|
|
|
/* `when` must be a non-empty string */
|
|
doc = st_kdl_parse("project \"p\" version \"1.0\"\n"
|
|
"feature \"pthread\" when=42 "
|
|
"{ header \"pthread.h\" }",
|
|
"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_true(strstr(st_error_message(err), "non-empty string") !=
|
|
NULL);
|
|
st_error_free(err);
|
|
st_kdl_document_free(doc);
|
|
|
|
/* empty when string is rejected too */
|
|
doc = st_kdl_parse("project \"p\" version \"1.0\"\n"
|
|
"feature \"pthread\" when=\"\" "
|
|
"{ header \"pthread.h\" }",
|
|
"t.kdl", &err);
|
|
munit_assert_null(err);
|
|
munit_assert_not_null(doc);
|
|
err = st_kdl_validate(doc);
|
|
munit_assert_not_null(err);
|
|
munit_assert_true(strstr(st_error_message(err), "non-empty string") !=
|
|
NULL);
|
|
st_error_free(err);
|
|
st_kdl_document_free(doc);
|
|
|
|
/* duplicate `when` is rejected */
|
|
doc = st_kdl_parse("project \"p\" version \"1.0\"\n"
|
|
"feature \"pthread\" when=\"os=linux\" when=\"os=bsd\" "
|
|
"{ header \"pthread.h\" }",
|
|
"t.kdl", &err);
|
|
munit_assert_null(err);
|
|
munit_assert_not_null(doc);
|
|
err = st_kdl_validate(doc);
|
|
munit_assert_not_null(err);
|
|
munit_assert_true(strstr(st_error_message(err), "duplicate 'when'") !=
|
|
NULL);
|
|
st_error_free(err);
|
|
st_kdl_document_free(doc);
|
|
|
|
/* a non-when property on a feature is still rejected (unchanged) */
|
|
doc = st_kdl_parse("project \"p\" version \"1.0\"\n"
|
|
"feature \"f\" foo=1 { header \"h.h\" }",
|
|
"t.kdl", &err);
|
|
munit_assert_null(err);
|
|
munit_assert_not_null(doc);
|
|
err = st_kdl_validate(doc);
|
|
munit_assert_not_null(err);
|
|
munit_assert_true(strstr(st_error_message(err),
|
|
"unexpected property 'foo'") != NULL);
|
|
st_error_free(err);
|
|
st_kdl_document_free(doc);
|
|
|
|
return MUNIT_OK;
|
|
}
|
|
|
|
static MunitTest tests[] = {
|
|
{ "/resolve/parse-emit-content", test_parse_emit_content, NULL, NULL,
|
|
MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ "/resolve/guard-eval", test_guard_eval, setup, teardown,
|
|
MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ "/resolve/not-os", test_not_os, setup, teardown,
|
|
MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ "/resolve/precedence", test_precedence, setup, teardown,
|
|
MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ "/resolve/parse-errors", test_parse_errors, NULL, NULL,
|
|
MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ "/resolve/os-mapping", test_os_mapping, setup, teardown,
|
|
MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ "/resolve/syntax", test_syntax, setup, teardown,
|
|
MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ "/resolve/emit-feature-aggregate", test_emit_feature_aggregate, setup,
|
|
teardown, MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ "/resolve/emit-feature-guard", test_emit_feature_guard, setup, teardown,
|
|
MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ "/resolve/check-name", test_check_name, NULL, NULL,
|
|
MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ "/resolve/schema-when", test_schema_when, NULL, NULL,
|
|
MUNIT_TEST_OPTION_NONE, NULL },
|
|
{ NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
|
};
|
|
|
|
static const MunitSuite suite = {
|
|
"/resolve", 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);
|
|
}
|