Files

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);
}