/* LINK: ../../src/gen/args.c ../../src/gen/config.c ../../src/gen/configure.c ../../src/gen/sh_emit.c ../../src/detect/probe.c ../../src/detect/checks.c ../../src/detect/check_registry.c ../../src/detect/resolve.c ../../src/kdl/schema.c ../../src/kdl/parser.c ../../src/kdl/lexer.c ../../src/kdl/value.c ../../src/ext/abi.c ../../src/ext/lang_c.c ../../src/ext/lang_cpp.c ../../src/error.c ../../src/span.c */ #ifndef _POSIX_C_SOURCE #define _POSIX_C_SOURCE 200809L /* mkdtemp/unsetenv/setenv (POSIX.1-2008) */ #endif /* tests/unit/test_gen_configure.c * * Unit + integration tests for the ./configure generator (todo 16): * src/gen/configure.c. THE MODEL: the generator EMITS a POSIX-sh script * from a schema-validated DSL document + an extension ctx; the emitted * script RUNS at configure time (probes + @VAR@ substitution). These tests * prove the emitted script is syntactically valid (sh/bash/zsh -n, no * banned constructs), REALLY runs (exit 0, Makefile written, CC/LIBS/ * prefix correct), honors --prefix, errors cleanly on an unknown @VAR@, a * missing Makefile.in and an unrecognized option, carries values with * '/', spaces and '&' byte-exact, keeps $(...) / backticks inert, and * cross-compile-skips RUN probes. * * The magic LINK comment on line 1 is REQUIRED by tests/run.sh (extra .c * sources, relative to tests/unit/). configure.c pulls in the whole detect * chain (probe/checks/check_registry/resolve) + sh_emit + abi (the C/C++ * modules register CC/CFLAGS) + kdl (parse/lexer/value/schema) + error/span. */ #include "munit.h" #include "detect/check_registry.h" #include "error.h" #include "ext/abi.h" #include "gen/configure.h" #include "kdl/ast.h" #include "kdl/schema.h" #include #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; if (snprintf(temp_dir, sizeof temp_dir, "/tmp/st_gen_XXXXXX") < 0) { return NULL; } if (mkdtemp(temp_dir) == NULL) { return NULL; } return (void *)1; } static void teardown(void *fixture) { char cmd[160]; if (fixture == NULL || temp_dir[0] == '\0') { return; } if (snprintf(cmd, sizeof cmd, "rm -rf -- '%s'", temp_dir) < 0) { return; } (void)system(cmd); /* best-effort; mkdtemp names are [A-Za-z0-9_]* */ temp_dir[0] = '\0'; } /* ---- small file/process helpers --------------------------------------- */ /* Join temp_dir/`name` into `buf`; returns the snprintf result. */ static int mkpath(char *buf, size_t sz, const char *name) { return snprintf(buf, sz, "%s/%s", temp_dir, name); } /* Slurp a file into a NUL-terminated heap buffer, or NULL. Caller frees. */ static char * slurp(const char *path) { FILE *f; long n; char *buf; 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 = malloc((size_t)n + 1); if (buf == NULL) { fclose(f); return NULL; } if (fread(buf, 1, (size_t)n, f) != (size_t)n) { free(buf); fclose(f); return NULL; } fclose(f); buf[n] = '\0'; return buf; } /* Slurp temp_dir/Makefile, or NULL. Caller frees. */ static char * makefile_text(void) { char path[600]; if (mkpath(path, sizeof path, "Makefile") < 0) { return NULL; } return slurp(path); } /* Slurp temp_dir/run.err, or NULL. Caller frees. */ static char * stderr_text(void) { char path[600]; if (mkpath(path, sizeof path, "run.err") < 0) { return NULL; } return slurp(path); } /* Write `content` to temp_dir/Makefile.in. Returns 0 on success. */ static int write_makefile_in(const char *content) { char path[600]; FILE *f; if (mkpath(path, sizeof path, "Makefile.in") < 0) { return -1; } f = fopen(path, "w"); if (f == NULL) { return -1; } if (fputs(content, f) == EOF) { fclose(f); return -1; } fclose(f); return 0; } /* Run "sh ./configure " with cwd = temp_dir, stderr to run.err. * Returns the configure exit status, or -1 if it could not be spawned. */ static int run_configure(const char *args) { char cmd[2400]; int n; int spawned; n = snprintf(cmd, sizeof cmd, "cd '%s' && sh ./configure %s 2>'%s/run.err'", temp_dir, args != NULL ? args : "", temp_dir); if (n < 0 || (size_t)n >= sizeof cmd) { return -1; } spawned = system(cmd); if (spawned == -1) { return -1; } return WEXITSTATUS(spawned); } /* Run "cd temp_dir && sh ./configure " with stdout captured to * run.out and stderr to run.err. `env` is a prefix like "CC=clang " (the * exported VAR=VALUE form) or NULL/"" for none. Returns the exit status, * or -1 if configure could not be spawned. */ static int run_configure_full(const char *env, const char *args) { char cmd[2400]; int n; int spawned; n = snprintf(cmd, sizeof cmd, "cd '%s' && %ssh ./configure %s >'%s/run.out' 2>'%s/run.err'", temp_dir, env != NULL ? env : "", args != NULL ? args : "", temp_dir, temp_dir); if (n < 0 || (size_t)n >= sizeof cmd) { return -1; } spawned = system(cmd); if (spawned == -1) { return -1; } return WEXITSTATUS(spawned); } /* " -n ": the shell's exit status or -1. */ static int syntax_check(const char *shell, const char *path) { char cmd[700]; int n; int spawned; n = snprintf(cmd, sizeof cmd, "%s -n '%s'", shell, path); if (n < 0 || (size_t)n >= sizeof cmd) { return -1; } spawned = system(cmd); if (spawned == -1) { return -1; } return WEXITSTATUS(spawned); } /* Run "./config.status " with cwd = temp_dir; stdout to status.out, * stderr to status.err. Returns the exit status, or -1 if not spawned. */ static int run_status(const char *args) { char cmd[2400]; int n; int spawned; n = snprintf(cmd, sizeof cmd, "cd '%s' && ./config.status %s >'%s/status.out' " "2>'%s/status.err'", temp_dir, args != NULL ? args : "", temp_dir, temp_dir); if (n < 0 || (size_t)n >= sizeof cmd) { return -1; } spawned = system(cmd); if (spawned == -1) { return -1; } return WEXITSTATUS(spawned); } /* Slurp temp_dir/, or NULL. Caller frees. */ static char * slurp_named(const char *name) { char path[600]; if (mkpath(path, sizeof path, name) < 0) { return NULL; } return slurp(path); } /* Slurp temp_dir/run.out (stdout of the last run_configure_full), or * NULL. Caller frees. */ static char * stdout_text(void) { return slurp_named("run.out"); } /* Remove temp_dir/ (a missing file is not an error: the stale_state * checks remove outputs that may or may not exist). Returns 0 on success. */ static int remove_named(const char *name) { char path[600]; if (mkpath(path, sizeof path, name) < 0) { return -1; } (void)remove(path); return 0; } /* Parse + validate + emit an INLINE document into temp_dir/configure. */ static int emit_inline_configure(struct st_ext_ctx *ctx, const char *docsrc) { struct st_kdl_document *doc = NULL; struct st_error *err = NULL; char conf[600]; doc = st_kdl_parse(docsrc, "inline.kdl", &err); if (err != NULL) { st_error_free(err); return -1; } if (doc == NULL) { return -1; } err = st_kdl_validate(doc); if (err != NULL) { st_error_free(err); st_kdl_document_free(doc); return -1; } if (mkpath(conf, sizeof conf, "configure") < 0) { st_kdl_document_free(doc); return -1; } err = st_gen_configure_emit_path(conf, doc, ctx); st_kdl_document_free(doc); if (err != NULL) { st_error_free(err); return -1; } return 0; } /* ---- fixture loading (same as test_schema.c) -------------------------- */ 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; } /* Build a ctx with the builtins + C detected (env CC/CFLAGS/etc. reset). * Returns NULL on any failure. */ static struct st_ext_ctx * build_ctx(void) { struct st_ext_ctx *ctx = st_ext_ctx_new(); struct st_error *err; if (ctx == NULL) { return NULL; } unsetenv("CC"); unsetenv("CFLAGS"); unsetenv("CXX"); unsetenv("CXXFLAGS"); err = st_ext_init_builtins(ctx); if (err != NULL) { st_error_free(err); st_ext_ctx_free(ctx); return NULL; } err = st_ext_detect_language(ctx, "c"); if (err != NULL) { st_error_free(err); st_ext_ctx_free(ctx); return NULL; } return ctx; } /* Parse + validate + emit the fixture into temp_dir/configure, using * `ctx`. Returns 0 on success, -1 on failure. */ static int emit_fixture_configure(struct st_ext_ctx *ctx) { const char *path = fixture_path(); char conf[600]; char *src; struct st_kdl_document *doc = NULL; struct st_error *err = NULL; if (path == NULL || ctx == NULL) { return -1; } src = slurp(path); if (src == NULL) { return -1; } doc = st_kdl_parse(src, path, &err); if (err != NULL) { st_error_free(err); free(src); return -1; } if (doc == NULL) { free(src); return -1; } err = st_kdl_validate(doc); if (err != NULL) { st_error_free(err); st_kdl_document_free(doc); free(src); return -1; } if (mkpath(conf, sizeof conf, "configure") < 0) { st_kdl_document_free(doc); free(src); return -1; } err = st_gen_configure_emit_path(conf, doc, ctx); /* the document borrows `src`'s token slices, so free src only after * the document is released */ st_kdl_document_free(doc); free(src); if (err != NULL) { st_error_free(err); return -1; } return 0; } /* The standard Makefile.in every end-to-end case uses. */ #define STD_MAKEFILE_IN \ "CC = @CC@\n" \ "LIBS = @LIBS@\n" \ "prefix = @prefix@\n" \ "FLAGS = @CFLAGS@\n" /* ---- (a) the emitted configure passes the three-shell syntax oracle --- */ static MunitResult test_syntax(const MunitParameter params[], void *data) { (void)params; (void)data; static const char *const banned[] = { "[[ ", "]]", "local ", "==", "<<<", "&>", "set -e", }; struct st_ext_ctx *ctx; char conf[600]; char *bytes; size_t i; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); munit_assert_int(mkpath(conf, sizeof conf, "configure"), >, 0); munit_assert_int(syntax_check("sh", conf), ==, 0); munit_assert_int(syntax_check("bash", conf), ==, 0); munit_assert_int(syntax_check("zsh", conf), ==, 0); bytes = slurp(conf); munit_assert_not_null(bytes); for (i = 0; i < sizeof(banned) / sizeof(banned[0]); i++) { munit_assert_null(strstr(bytes, banned[i])); } /* the preamble is structurally marked for todo 18 */ munit_assert_not_null(strstr(bytes, "# --- PREAMBLE ---")); free(bytes); return MUNIT_OK; } /* ---- (b) real end-to-end run ------------------------------------------ */ static MunitResult test_end_to_end(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; const char *cc; char cc_buf[64]; char want[128]; char *mk; int cc_len; ctx = build_ctx(); munit_assert_not_null(ctx); cc = st_registry_get_var(st_ext_var_registry(ctx), "CC"); munit_assert_not_null(cc); /* copy before st_ext_ctx_free() — the registry value is borrowed */ cc_len = snprintf(cc_buf, sizeof cc_buf, "%s", cc); munit_assert_int(cc_len, >, 0); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); munit_assert_int(write_makefile_in(STD_MAKEFILE_IN), ==, 0); munit_assert_int(run_configure(NULL), ==, 0); mk = makefile_text(); munit_assert_not_null(mk); /* CC substituted from the registry value */ { int n = snprintf(want, sizeof want, "CC = %s", cc_buf); munit_assert_int(n, >, 0); munit_assert_not_null(strstr(mk, want)); } /* LIBS accumulated from library checks (pthread + m) */ munit_assert_not_null(strstr(mk, "-lpthread")); munit_assert_not_null(strstr(mk, "-lm")); /* prefix defaults to /usr/local */ munit_assert_not_null(strstr(mk, "prefix = /usr/local")); free(mk); return MUNIT_OK; } /* ---- (c) --prefix is honored ------------------------------------------ */ static MunitResult test_prefix(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char *mk; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); munit_assert_int(write_makefile_in(STD_MAKEFILE_IN), ==, 0); munit_assert_int(run_configure("--prefix=/opt/x"), ==, 0); mk = makefile_text(); munit_assert_not_null(mk); munit_assert_not_null(strstr(mk, "prefix = /opt/x")); free(mk); return MUNIT_OK; } /* ---- (d) an unknown @VAR@ errors naming the variable ------------------ */ static MunitResult test_unknown_var(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char *err; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); munit_assert_int(write_makefile_in("CC = @NOPE@\n"), ==, 0); munit_assert_int(run_configure(NULL), !=, 0); err = stderr_text(); munit_assert_not_null(err); munit_assert_not_null(strstr(err, "NOPE")); free(err); return MUNIT_OK; } /* ---- (e) a missing Makefile.in is a clean error ----------------------- */ static MunitResult test_missing_makefile(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char *err; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); /* no Makefile.in written at all */ munit_assert_int(run_configure(NULL), !=, 0); err = stderr_text(); munit_assert_not_null(err); munit_assert_not_null(strstr(err, "Makefile.in")); free(err); return MUNIT_OK; } /* ---- (f) '/', spaces and '&' survive byte-exact ----------------------- */ static MunitResult test_bytes_exact(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char *mk; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); /* spaces via CFLAGS; '&' and '/' via --prefix */ munit_assert_int(write_makefile_in(STD_MAKEFILE_IN), ==, 0); munit_assert_int(setenv("CFLAGS", "a b c", 1), ==, 0); munit_assert_int(run_configure("--prefix='/opt/x & y/z'"), ==, 0); unsetenv("CFLAGS"); mk = makefile_text(); munit_assert_not_null(mk); munit_assert_not_null(strstr(mk, "FLAGS = a b c")); munit_assert_not_null(strstr(mk, "prefix = /opt/x & y/z")); free(mk); return MUNIT_OK; } /* ---- unrecognized --flag is a clean error ----------------------------- */ static MunitResult test_unrecognized_option(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char *err; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); munit_assert_int(run_configure("--bogus"), !=, 0); err = stderr_text(); munit_assert_not_null(err); munit_assert_not_null(strstr(err, "unrecognized option")); free(err); return MUNIT_OK; } /* ---- injection: a $(...) VALUE stays inert (not executed) ------------- */ static MunitResult test_injection_value(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char marker[600]; char *mk; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); munit_assert_int(mkpath(marker, sizeof marker, "pwned1"), >, 0); /* prefix gets a command-substitution payload; it must be a literal. */ munit_assert_int(write_makefile_in("prefix = @prefix@\n"), ==, 0); munit_assert_int(run_configure("--prefix='$(touch pwned1)'"), ==, 0); munit_assert_int(access(marker, F_OK), ==, -1); mk = makefile_text(); munit_assert_not_null(mk); munit_assert_not_null(strstr(mk, "prefix = $(touch pwned1)")); free(mk); return MUNIT_OK; } /* ---- injection: an unknown @$(...)@ name stays inert (not executed) --- */ static MunitResult test_injection_unknown_name(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char marker[600]; char *err; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); munit_assert_int(mkpath(marker, sizeof marker, "pwned2"), >, 0); munit_assert_int(write_makefile_in("X = @$(touch pwned2)@\n"), ==, 0); munit_assert_int(run_configure(NULL), !=, 0); munit_assert_int(access(marker, F_OK), ==, -1); err = stderr_text(); munit_assert_not_null(err); munit_assert_not_null(strstr(err, "unknown variable")); free(err); return MUNIT_OK; } /* ---- cross-compile: RUN probes skip with a warning -------------------- */ static MunitResult test_cross_compile(const MunitParameter params[], void *data) { (void)params; (void)data; static const char *const docsrc = "project \"x\" version \"1.0\"\n" "feature \"sz\" { sizeof \"long\" }\n"; struct st_ext_ctx *ctx; struct st_kdl_document *doc = NULL; struct st_error *err = NULL; char conf[600]; char *e; ctx = build_ctx(); munit_assert_not_null(ctx); doc = st_kdl_parse(docsrc, "inline.kdl", &err); munit_assert_null(err); munit_assert_not_null(doc); munit_assert_null(st_kdl_validate(doc)); munit_assert_int(mkpath(conf, sizeof conf, "configure"), >, 0); munit_assert_null(st_gen_configure_emit_path(conf, doc, ctx)); st_kdl_document_free(doc); st_ext_ctx_free(ctx); munit_assert_int(write_makefile_in("CC = @CC@\n"), ==, 0); munit_assert_int(run_configure("--host=aarch64-linux --build=x86_64-linux"), ==, 0); e = stderr_text(); munit_assert_not_null(e); munit_assert_not_null(strstr(e, "cross-compiling")); free(e); return MUNIT_OK; } /* ---- todo 17 (a): the three new sections stay shell-safe ------------- */ static MunitResult test_config_sections_syntax(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char conf[600]; char *bytes; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); munit_assert_int(mkpath(conf, sizeof conf, "configure"), >, 0); munit_assert_int(syntax_check("sh", conf), ==, 0); munit_assert_int(syntax_check("bash", conf), ==, 0); munit_assert_int(syntax_check("zsh", conf), ==, 0); bytes = slurp(conf); munit_assert_not_null(bytes); /* the three sections are structurally marked (--- not ===, per the * F3 banned-construct grep) */ munit_assert_not_null(strstr(bytes, "# --- config.log:")); munit_assert_not_null(strstr(bytes, "# --- config.h ---")); munit_assert_not_null(strstr(bytes, "# --- config.status ---")); munit_assert_not_null(strstr(bytes, "'## results'")); free(bytes); return MUNIT_OK; } /* ---- todo 17 (b): a real run writes config.h/config.log/config.status -- */ static MunitResult test_config_artifacts(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char status[600]; char *ch; char *cl; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); munit_assert_int(write_makefile_in(STD_MAKEFILE_IN), ==, 0); munit_assert_int(run_configure(NULL), ==, 0); /* config.h: the ST_CONFIG_H guard + one define per resolved feature * (content asserted, not mere existence) */ ch = slurp_named("config.h"); munit_assert_not_null(ch); munit_assert_not_null(strstr(ch, "#ifndef ST_CONFIG_H")); munit_assert_not_null(strstr(ch, "#define ST_CONFIG_H")); munit_assert_not_null(strstr(ch, "#define HAVE_PTHREAD 1")); munit_assert_not_null(strstr(ch, "#define HAVE_MATH 1")); free(ch); /* config.log: the transcript header + the ## results summary */ cl = slurp_named("config.log"); munit_assert_not_null(cl); munit_assert_not_null( strstr(cl, "It was created by stupidtools configure")); munit_assert_not_null(strstr(cl, "## results")); munit_assert_not_null(strstr(cl, "have_pthread=yes")); munit_assert_not_null(strstr(cl, "have_math=yes")); free(cl); /* config.status: exists, is executable, and is valid POSIX-sh */ munit_assert_int(mkpath(status, sizeof status, "config.status"), >, 0); munit_assert_int(access(status, F_OK), ==, 0); munit_assert_int(access(status, X_OK), ==, 0); munit_assert_int(syntax_check("sh", status), ==, 0); munit_assert_int(syntax_check("bash", status), ==, 0); munit_assert_int(syntax_check("zsh", status), ==, 0); return MUNIT_OK; } /* ---- todo 17 (c): config.status --config prints the original args ----- */ static MunitResult test_config_status_config(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char *out; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); munit_assert_int(write_makefile_in(STD_MAKEFILE_IN), ==, 0); munit_assert_int(run_configure("--prefix=/opt/x"), ==, 0); munit_assert_int(run_status("--config"), ==, 0); out = slurp_named("status.out"); munit_assert_not_null(out); munit_assert_string_equal(out, "--prefix=/opt/x\n"); free(out); return MUNIT_OK; } /* ---- todo 17 (d): config.status --recheck re-runs configure ----------- */ static MunitResult test_config_status_recheck(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char *mk; char *ch; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); munit_assert_int(write_makefile_in(STD_MAKEFILE_IN), ==, 0); munit_assert_int(run_configure("--prefix=/opt/x"), ==, 0); /* stale_state: destroy the outputs, then --recheck regenerates them * (observable regeneration, not a cached no-op) */ munit_assert_int(remove_named("Makefile"), ==, 0); munit_assert_int(remove_named("config.h"), ==, 0); munit_assert_int(run_status("--recheck"), ==, 0); mk = makefile_text(); munit_assert_not_null(mk); munit_assert_not_null(strstr(mk, "prefix = /opt/x")); free(mk); ch = slurp_named("config.h"); munit_assert_not_null(ch); munit_assert_not_null(strstr(ch, "#define HAVE_PTHREAD 1")); free(ch); return MUNIT_OK; } /* ---- todo 17 (e): a failed feature gets no HAVE_ define --------------- */ static MunitResult test_config_failed_feature(const MunitParameter params[], void *data) { (void)params; (void)data; static const char *const docsrc = "project \"x\" version \"1.0\"\n" "feature \"nope\" { header \"nope_missing_xyz.h\" }\n" "feature \"ok\" { header \"unistd.h\" }\n"; struct st_ext_ctx *ctx; char *ch; char *cl; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_inline_configure(ctx, docsrc), ==, 0); st_ext_ctx_free(ctx); munit_assert_int(write_makefile_in("CC = @CC@\n"), ==, 0); munit_assert_int(run_configure(NULL), ==, 0); ch = slurp_named("config.h"); munit_assert_not_null(ch); munit_assert_null(strstr(ch, "HAVE_NOPE")); munit_assert_not_null(strstr(ch, "#define HAVE_OK 1")); free(ch); cl = slurp_named("config.log"); munit_assert_not_null(cl); munit_assert_not_null(strstr(cl, "have_nope=no")); munit_assert_not_null(strstr(cl, "have_ok=yes")); free(cl); return MUNIT_OK; } /* ---- todo 17 (f): injected values never execute ----------------------- */ static MunitResult test_config_status_injection(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char pwned1[600]; char pwned2[600]; char *mk; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); munit_assert_int(mkpath(pwned1, sizeof pwned1, "pwned1"), >, 0); munit_assert_int(mkpath(pwned2, sizeof pwned2, "pwned2"), >, 0); munit_assert_int(write_makefile_in("prefix = @prefix@\n"), ==, 0); /* The payloads use ${IFS} instead of a literal space: they contain no * whitespace, so they survive the (documented, v1) unquoted recheck * expansion as one word - yet they would create the marker files if * any shell ever executed them. */ munit_assert_int(run_configure("--prefix='$(touch${IFS}pwned1)'"), ==, 0); munit_assert_int(access(pwned1, F_OK), ==, -1); munit_assert_int(run_status("--config"), ==, 0); munit_assert_int(access(pwned1, F_OK), ==, -1); munit_assert_int(run_status("--recheck"), ==, 0); munit_assert_int(access(pwned1, F_OK), ==, -1); mk = makefile_text(); munit_assert_not_null(mk); munit_assert_not_null(strstr(mk, "prefix = $(touch${IFS}pwned1)")); free(mk); /* payload 2: backticks - same guarantee, via a fresh configure run */ munit_assert_int(run_configure("--prefix='`touch${IFS}pwned2`'"), ==, 0); munit_assert_int(access(pwned2, F_OK), ==, -1); munit_assert_int(run_status("--config"), ==, 0); munit_assert_int(access(pwned2, F_OK), ==, -1); munit_assert_int(run_status("--recheck"), ==, 0); munit_assert_int(access(pwned2, F_OK), ==, -1); mk = makefile_text(); munit_assert_not_null(mk); munit_assert_not_null(strstr(mk, "prefix = `touch${IFS}pwned2`")); free(mk); return MUNIT_OK; } /* ---- todo 17 (g): config.status --help and unknown-option handling ---- */ static MunitResult test_config_status_help(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char *out; char *e; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); munit_assert_int(write_makefile_in(STD_MAKEFILE_IN), ==, 0); munit_assert_int(run_configure(NULL), ==, 0); munit_assert_int(run_status("--help"), ==, 0); out = slurp_named("status.out"); munit_assert_not_null(out); munit_assert_not_null(strstr(out, "Usage")); free(out); munit_assert_int(run_status("--bogus"), !=, 0); e = slurp_named("status.err"); munit_assert_not_null(e); munit_assert_not_null(strstr(e, "unrecognized option")); free(e); return MUNIT_OK; } /* ---- todo 18 (a): the full preamble stays shell-safe + carries the * parser machinery ------------------------------------------------------ */ static MunitResult test_args_syntax(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char conf[600]; char *bytes; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); munit_assert_int(mkpath(conf, sizeof conf, "configure"), >, 0); munit_assert_int(syntax_check("sh", conf), ==, 0); munit_assert_int(syntax_check("bash", conf), ==, 0); munit_assert_int(syntax_check("zsh", conf), ==, 0); bytes = slurp(conf); munit_assert_not_null(bytes); munit_assert_not_null(strstr(bytes, "# --- PREAMBLE ---")); munit_assert_not_null(strstr(bytes, "st_usage()")); /* the fixture's option "debug" -> enable/disable arms + default */ munit_assert_not_null(strstr(bytes, "--enable-debug) enable_debug=yes")); munit_assert_not_null(strstr(bytes, "--disable-debug) enable_debug=no")); munit_assert_not_null(strstr(bytes, "enable_debug=no")); /* VAR=VALUE parser machinery (identifier check + quoted eval) */ munit_assert_not_null(strstr(bytes, "st_val_q=")); munit_assert_not_null(strstr(bytes, "st_var=${st_arg%%=*}")); free(bytes); return MUNIT_OK; } /* ---- todo 18 (b): --help lists the standard + per-option flags -------- */ static MunitResult test_args_help(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char *out; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); /* help exits 0 BEFORE probing/substitution (no Makefile.in needed) */ munit_assert_int(run_configure_full(NULL, "--help"), ==, 0); out = stdout_text(); munit_assert_not_null(out); munit_assert_not_null(strstr(out, "Usage:")); munit_assert_not_null(strstr(out, "--prefix")); munit_assert_not_null(strstr(out, "--exec-prefix")); munit_assert_not_null(strstr(out, "--host")); munit_assert_not_null(strstr(out, "--build")); munit_assert_not_null(strstr(out, "VAR=VALUE")); munit_assert_not_null(strstr(out, "--enable-debug")); munit_assert_not_null(strstr(out, "--disable-debug")); munit_assert_not_null(strstr(out, "--version")); free(out); return MUNIT_OK; } /* ---- todo 18 (g): --version prints name + version, exits 0 ------------ */ static MunitResult test_args_version(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char *out; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); munit_assert_int(run_configure_full(NULL, "--version"), ==, 0); out = stdout_text(); munit_assert_not_null(out); munit_assert_not_null(strstr(out, "stupidtools")); munit_assert_not_null(strstr(out, "1.0.0")); free(out); return MUNIT_OK; } /* ---- todo 18 (c): CC=clang as positional AND as env override ---------- */ static MunitResult test_args_cc_override(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char *mk; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); /* positional form: ./configure CC=clang */ munit_assert_int(write_makefile_in("CC = @CC@\n"), ==, 0); munit_assert_int(run_configure_full(NULL, "CC=clang"), ==, 0); mk = makefile_text(); munit_assert_not_null(mk); munit_assert_not_null(strstr(mk, "CC = clang")); free(mk); /* exported form: CC=clang ./configure */ munit_assert_int(run_configure_full("CC=clang ", NULL), ==, 0); mk = makefile_text(); munit_assert_not_null(mk); munit_assert_not_null(strstr(mk, "CC = clang")); free(mk); return MUNIT_OK; } /* ---- todo 18 (d): --enable-debug / --disable-debug / default ---------- */ static MunitResult test_args_enable_debug(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char *mk; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); munit_assert_int(write_makefile_in("DEBUG = @enable_debug@\n"), ==, 0); munit_assert_int(run_configure_full(NULL, "--enable-debug"), ==, 0); mk = makefile_text(); munit_assert_not_null(mk); munit_assert_not_null(strstr(mk, "DEBUG = yes")); free(mk); /* the fixture option has default=#false, so no flag -> no */ munit_assert_int(run_configure_full(NULL, NULL), ==, 0); mk = makefile_text(); munit_assert_not_null(mk); munit_assert_not_null(strstr(mk, "DEBUG = no")); free(mk); munit_assert_int(run_configure_full(NULL, "--disable-debug"), ==, 0); mk = makefile_text(); munit_assert_not_null(mk); munit_assert_not_null(strstr(mk, "DEBUG = no")); free(mk); return MUNIT_OK; } /* ---- todo 18 (e): --host != --build -> cross_compiling=yes ------------ */ static MunitResult test_args_cross_compiling(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char *mk; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); munit_assert_int(write_makefile_in("CROSS = @cross_compiling@\n" "HOST = @host@\n" "BUILD = @build@\n"), ==, 0); munit_assert_int(run_configure_full(NULL, "--host=aarch64-linux-gnu " "--build=x86_64-linux-gnu"), ==, 0); mk = makefile_text(); munit_assert_not_null(mk); munit_assert_not_null(strstr(mk, "CROSS = yes")); munit_assert_not_null(strstr(mk, "HOST = aarch64-linux-gnu")); munit_assert_not_null(strstr(mk, "BUILD = x86_64-linux-gnu")); free(mk); munit_assert_int(run_configure_full(NULL, NULL), ==, 0); mk = makefile_text(); munit_assert_not_null(mk); munit_assert_not_null(strstr(mk, "CROSS = no")); free(mk); return MUNIT_OK; } /* ---- todo 18 (f): unknown --* -> exit 1 + "unrecognized option" ------- */ static MunitResult test_args_bogus(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char *err; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); munit_assert_int(run_configure_full(NULL, "--bogus"), !=, 0); err = stderr_text(); munit_assert_not_null(err); munit_assert_not_null(strstr(err, "unrecognized option")); munit_assert_not_null(strstr(err, "Usage:")); /* usage goes to stderr */ free(err); munit_assert_int(run_configure_full(NULL, "--bogus=1"), !=, 0); err = stderr_text(); munit_assert_not_null(err); munit_assert_not_null(strstr(err, "unrecognized option")); free(err); /* --enable- with no name falls through to the unknown-option error */ munit_assert_int(run_configure_full(NULL, "--enable-"), !=, 0); err = stderr_text(); munit_assert_not_null(err); munit_assert_not_null(strstr(err, "unrecognized option")); free(err); return MUNIT_OK; } /* ---- malformed values: missing/empty/option-looking --prefix ---------- */ static MunitResult test_args_malformed(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char *err; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); /* missing value (space form, end of argv) */ munit_assert_int(run_configure_full(NULL, "--prefix"), !=, 0); err = stderr_text(); munit_assert_not_null(err); munit_assert_not_null(strstr(err, "requires an argument")); free(err); /* empty value */ munit_assert_int(run_configure_full(NULL, "--prefix="), !=, 0); err = stderr_text(); munit_assert_not_null(err); munit_assert_not_null(strstr(err, "non-empty argument")); free(err); /* an option-looking value is rejected */ munit_assert_int(run_configure_full(NULL, "--prefix --bogus"), !=, 0); err = stderr_text(); munit_assert_not_null(err); munit_assert_not_null(strstr(err, "requires an argument")); free(err); return MUNIT_OK; } /* ---- --prefix (space form) and --exec-prefix -------------------- */ static MunitResult test_args_prefix_space(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char *mk; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); munit_assert_int(write_makefile_in("prefix = @prefix@\n" "EP = @exec_prefix@\n"), ==, 0); munit_assert_int(run_configure_full(NULL, "--prefix /opt/space"), ==, 0); mk = makefile_text(); munit_assert_not_null(mk); munit_assert_not_null(strstr(mk, "prefix = /opt/space")); /* exec_prefix defaults to prefix when not given */ munit_assert_not_null(strstr(mk, "EP = /opt/space")); free(mk); munit_assert_int(run_configure_full(NULL, "--prefix '/opt/x y'"), ==, 0); mk = makefile_text(); munit_assert_not_null(mk); munit_assert_not_null(strstr(mk, "prefix = /opt/x y")); free(mk); munit_assert_int(run_configure_full(NULL, "--exec-prefix=/opt/ep"), ==, 0); mk = makefile_text(); munit_assert_not_null(mk); munit_assert_not_null(strstr(mk, "EP = /opt/ep")); munit_assert_not_null(strstr(mk, "prefix = /usr/local")); free(mk); return MUNIT_OK; } /* ---- positional VAR=VALUE injection stays inert ----------------------- */ static MunitResult test_args_injection(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx; char pwned3[600]; char pwned4[600]; char *mk; char *err; ctx = build_ctx(); munit_assert_not_null(ctx); munit_assert_int(emit_fixture_configure(ctx), ==, 0); st_ext_ctx_free(ctx); munit_assert_int(mkpath(pwned3, sizeof pwned3, "pwned3"), >, 0); munit_assert_int(mkpath(pwned4, sizeof pwned4, "pwned4"), >, 0); munit_assert_int(write_makefile_in("FLAGS = @CFLAGS@\n"), ==, 0); munit_assert_int(run_configure_full(NULL, "'CFLAGS=$(touch pwned3)'"), ==, 0); munit_assert_int(access(pwned3, F_OK), ==, -1); mk = makefile_text(); munit_assert_not_null(mk); munit_assert_not_null(strstr(mk, "FLAGS = $(touch pwned3)")); free(mk); munit_assert_int(run_configure_full(NULL, "'CFLAGS=`touch pwned4`'"), ==, 0); munit_assert_int(access(pwned4, F_OK), ==, -1); mk = makefile_text(); munit_assert_not_null(mk); munit_assert_not_null(strstr(mk, "FLAGS = `touch pwned4`")); free(mk); /* a non-identifier VAR name is a clean error */ munit_assert_int(run_configure_full(NULL, "B-AD=1"), !=, 0); err = stderr_text(); munit_assert_not_null(err); munit_assert_not_null(strstr(err, "invalid variable assignment")); free(err); return MUNIT_OK; } /* ---- a hostile option NAME errors at GENERATION time ------------------ */ static MunitResult test_args_option_name(const MunitParameter params[], void *data) { (void)params; (void)data; static const char *const docsrc = "project \"x\" version \"1.0\"\n" "option \"x; rm -rf /\" default=#false\n" "feature \"f\" { header \"unistd.h\" }\n"; struct st_ext_ctx *ctx; struct st_kdl_document *doc = NULL; struct st_error *err = NULL; char conf[600]; ctx = build_ctx(); munit_assert_not_null(ctx); doc = st_kdl_parse(docsrc, "inline.kdl", &err); munit_assert_null(err); munit_assert_not_null(doc); munit_assert_null(st_kdl_validate(doc)); /* schema-valid, hostile name */ munit_assert_int(mkpath(conf, sizeof conf, "configure"), >, 0); err = st_gen_configure_emit_path(conf, doc, ctx); munit_assert_not_null(err); munit_assert_not_null(strstr(st_error_message(err), "not a valid shell identifier")); st_error_free(err); err = NULL; /* nothing was emitted: the truncated output is an empty file */ { char *bytes = slurp(conf); munit_assert_not_null(bytes); munit_assert_string_equal(bytes, ""); free(bytes); } st_kdl_document_free(doc); st_ext_ctx_free(ctx); return MUNIT_OK; } static MunitTest tests[] = { { "/gen/syntax", test_syntax, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/end-to-end", test_end_to_end, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/prefix", test_prefix, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/unknown-var", test_unknown_var, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/missing-makefile", test_missing_makefile, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/bytes-exact", test_bytes_exact, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/unrecognized-option", test_unrecognized_option, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/injection-value", test_injection_value, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/injection-unknown-name", test_injection_unknown_name, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/cross-compile", test_cross_compile, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/config-sections-syntax", test_config_sections_syntax, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/config-artifacts", test_config_artifacts, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/config-status-config", test_config_status_config, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/config-status-recheck", test_config_status_recheck, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/config-failed-feature", test_config_failed_feature, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/config-status-injection", test_config_status_injection, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/config-status-help", test_config_status_help, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/args/syntax", test_args_syntax, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/args/help", test_args_help, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/args/version", test_args_version, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/args/cc-override", test_args_cc_override, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/args/enable-debug", test_args_enable_debug, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/args/cross-compiling", test_args_cross_compiling, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/args/bogus", test_args_bogus, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/args/malformed", test_args_malformed, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/args/prefix-space", test_args_prefix_space, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/args/injection", test_args_injection, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/gen/args/option-name", test_args_option_name, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, }; static const MunitSuite suite = { "/gen", 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); }