/* 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_ (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 #include #include #include #include /* ---- 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 " 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 " 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); } /* " -n ": 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_ * 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_=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 -> _0, _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 ""` 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); }