/* LINK: ../../src/ext/abi.c ../../src/ext/lang_c.c ../../src/ext/lang_cpp.c ../../src/error.c ../../src/span.c */ /* tests/unit/test_abi.c * * Unit tests for the extension ABI (src/ext/abi.h) and the builtin C/C++ * language modules (plan todo 13). * * The magic LINK comment on line 1 is REQUIRED by tests/run.sh: it lists * the extra .c sources to compile into this test binary (paths relative * to tests/unit/, space-separated). munit.c and the include dirs are * added automatically by the harness. */ #ifndef _POSIX_C_SOURCE #define _POSIX_C_SOURCE 200809L /* setenv/unsetenv (POSIX.1-2008) */ #endif #include "munit.h" #include "ext/abi.h" #include #include /* ---------- helpers ---------- */ /* A fake detect fn proving the ABI invokes registered function pointers * (exactly what a future Lua binding will do). module_ctx is an int* * used as a call counter. */ static struct st_error * fake_detect(struct st_ext_ctx *ctx, void *module_ctx, struct st_toolchain *out) { int *called = module_ctx; (void)ctx; if (called != NULL) { *called = 1; } out->path = strdup("fakecc"); out->id = strdup("fake"); out->version = strdup("0.0"); return NULL; } static void reset_cc_env(void) { unsetenv("CC"); unsetenv("CFLAGS"); unsetenv("CXX"); unsetenv("CXXFLAGS"); } /* Does the host have a working ? (mirrors what detection does: * actually execute, never guess from PATH strings). */ static int host_has(const char *cmd) { char *argv[] = { (char *)cmd, "--version", NULL }; char *out = NULL; size_t out_len = 0; int status = st_ext_run_capture(argv, &out, &out_len); free(out); return status == 0; } /* ---------- (a) builtin modules register over the generic ABI ---------- */ static MunitResult test_builtins_register_languages(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx = st_ext_ctx_new(); struct st_error *err; munit_assert_not_null(ctx); /* before init: nothing registered */ munit_assert_size(st_ext_language_count(ctx), ==, 0); munit_assert_size(st_ext_check_count(ctx), ==, 0); err = st_ext_init_builtins(ctx); munit_assert_null(err); /* the C and C++ language modules registered themselves */ munit_assert_size(st_ext_language_count(ctx), ==, 2); munit_assert_string_equal(st_ext_language_name(ctx, 0), "c"); munit_assert_string_equal(st_ext_language_name(ctx, 1), "cxx"); /* each language registered its check kinds (header is universal) */ munit_assert_size(st_ext_check_count(ctx), ==, 2); munit_assert_string_equal(st_ext_check_language(ctx, 0), "c"); munit_assert_string_equal(st_ext_check_kind(ctx, 0), "header"); munit_assert_string_equal(st_ext_check_language(ctx, 1), "cxx"); munit_assert_string_equal(st_ext_check_kind(ctx, 1), "header"); /* no detection happened yet: no toolchain, no registered variables */ munit_assert_null(st_ext_language_toolchain(ctx, 0)); munit_assert_size(st_registry_var_count(st_ext_var_registry(ctx)), ==, 0); st_ext_ctx_free(ctx); return MUNIT_OK; } /* (a) detect finds a non-empty CC path + compiler id on this host. */ static MunitResult test_detect_c(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx = st_ext_ctx_new(); struct st_error *err; const struct st_toolchain *tc; const char *cc; munit_assert_not_null(ctx); munit_assert_null(st_ext_init_builtins(ctx)); reset_cc_env(); err = st_ext_detect_language(ctx, "c"); munit_assert_null(err); tc = st_ext_language_toolchain_named(ctx, "c"); munit_assert_not_null(tc); munit_assert_not_null(tc->path); munit_assert_int(strlen(tc->path), >, 0); munit_assert_not_null(tc->id); munit_assert_int(strlen(tc->id), >, 0); munit_assert_not_null(tc->version); munit_assert_int(strlen(tc->version), >, 0); /* detection recorded the compiler command in CC */ cc = st_registry_get_var(st_ext_var_registry(ctx), "CC"); munit_assert_not_null(cc); munit_assert_string_equal(cc, tc->path); st_ext_ctx_free(ctx); return MUNIT_OK; } /* (b) CC=clang env override: detection must return clang, not the * default cc. */ static MunitResult test_env_override_clang(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx = st_ext_ctx_new(); struct st_error *err; const struct st_toolchain *tc; munit_assert_not_null(ctx); munit_assert_null(st_ext_init_builtins(ctx)); munit_assert_int(setenv("CC", "clang", 1), ==, 0); err = st_ext_detect_language(ctx, "c"); if (host_has("clang")) { /* Host with clang: id and CC are clang. */ munit_assert_null(err); munit_assert_string_equal( st_registry_get_var(st_ext_var_registry(ctx), "CC"), "clang"); tc = st_ext_language_toolchain_named(ctx, "c"); munit_assert_not_null(tc); munit_assert_string_equal(tc->id, "clang"); munit_assert_string_equal(tc->path, "clang"); } else { /* Host without clang: the override is still authoritative -- a * clean error, and NO silent fallback to cc. */ munit_assert_not_null(err); munit_assert_int(st_error_category_of(err), ==, ST_ERR_IO); munit_assert_not_null(strstr(st_error_message(err), "not found")); munit_assert_null(st_registry_get_var(st_ext_var_registry(ctx), "CC")); st_error_free(err); err = NULL; } st_ext_ctx_free(ctx); return MUNIT_OK; } /* (b, host form) CC=gcc env override honored end to end. */ static MunitResult test_env_override_gcc(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx = st_ext_ctx_new(); struct st_error *err; const struct st_toolchain *tc; munit_assert_not_null(ctx); munit_assert_null(st_ext_init_builtins(ctx)); munit_assert_int(setenv("CC", "gcc", 1), ==, 0); err = st_ext_detect_language(ctx, "c"); munit_assert_null(err); munit_assert_string_equal( st_registry_get_var(st_ext_var_registry(ctx), "CC"), "gcc"); tc = st_ext_language_toolchain_named(ctx, "c"); munit_assert_not_null(tc); munit_assert_string_equal(tc->id, "gcc"); st_ext_ctx_free(ctx); return MUNIT_OK; } /* (c) a bogus CC=/nonexistent yields a clean error, not a crash. */ static MunitResult test_bogus_cc_clean_error(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx = st_ext_ctx_new(); struct st_error *err; munit_assert_not_null(ctx); munit_assert_null(st_ext_init_builtins(ctx)); munit_assert_int( setenv("CC", "/nonexistent/stupidtools/not-a-compiler", 1), ==, 0); err = st_ext_detect_language(ctx, "c"); munit_assert_not_null(err); munit_assert_int(st_error_category_of(err), ==, ST_ERR_IO); munit_assert_not_null(strstr(st_error_message(err), "not found")); /* failed detection must not half-register variables */ munit_assert_null(st_registry_get_var(st_ext_var_registry(ctx), "CC")); munit_assert_null(st_ext_language_toolchain_named(ctx, "c")); st_error_free(err); st_ext_ctx_free(ctx); return MUNIT_OK; } /* adversarial: empty PATH must yield a clean error, not a crash. */ static MunitResult test_empty_path_clean_error(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx = st_ext_ctx_new(); struct st_error *err; const char *saved = getenv("PATH"); munit_assert_not_null(ctx); munit_assert_null(st_ext_init_builtins(ctx)); reset_cc_env(); munit_assert_int(setenv("PATH", "", 1), ==, 0); err = st_ext_detect_language(ctx, "c"); munit_assert_not_null(err); munit_assert_not_null(strstr(st_error_message(err), "no C compiler found")); munit_assert_null(st_registry_get_var(st_ext_var_registry(ctx), "CC")); st_error_free(err); if (saved != NULL) { munit_assert_int(setenv("PATH", saved, 1), ==, 0); } else { munit_assert_int(unsetenv("PATH"), ==, 0); } st_ext_ctx_free(ctx); return MUNIT_OK; } /* (d) registered vars CC/CFLAGS are queryable from the generic registry; * CFLAGS defaults empty and respects the environment. */ static MunitResult test_registered_vars_queryable(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx = st_ext_ctx_new(); struct st_registry *reg; struct st_error *err; munit_assert_not_null(ctx); munit_assert_null(st_ext_init_builtins(ctx)); reg = st_ext_var_registry(ctx); reset_cc_env(); munit_assert_int(setenv("CFLAGS", "-std=c23 -pedantic", 1), ==, 0); err = st_ext_detect_language(ctx, "c"); munit_assert_null(err); munit_assert_not_null(st_registry_get_var(reg, "CC")); munit_assert_int(strlen(st_registry_get_var(reg, "CC")), >, 0); munit_assert_string_equal(st_registry_get_var(reg, "CFLAGS"), "-std=c23 -pedantic"); /* CXX/CXXFLAGS belong to the cxx module and are not set by C detect */ munit_assert_null(st_registry_get_var(reg, "CXX")); st_ext_ctx_free(ctx); return MUNIT_OK; } /* (d) the generic var registry itself: set/get/overwrite/unset semantics * with no compiler involved. */ static MunitResult test_var_registry_semantics(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_registry *reg = st_registry_new(); munit_assert_not_null(reg); munit_assert_null(st_registry_get_var(reg, "X")); munit_assert_size(st_registry_var_count(reg), ==, 0); st_registry_set_var(reg, "X", "v1"); munit_assert_string_equal(st_registry_get_var(reg, "X"), "v1"); munit_assert_size(st_registry_var_count(reg), ==, 1); /* overwrite replaces the value */ st_registry_set_var(reg, "X", "v2"); munit_assert_string_equal(st_registry_get_var(reg, "X"), "v2"); munit_assert_size(st_registry_var_count(reg), ==, 1); /* NULL value unsets */ st_registry_set_var(reg, "X", NULL); munit_assert_null(st_registry_get_var(reg, "X")); munit_assert_size(st_registry_var_count(reg), ==, 0); /* ordered iteration matches insertion order */ st_registry_set_var(reg, "CC", "cc"); st_registry_set_var(reg, "CFLAGS", ""); munit_assert_size(st_registry_var_count(reg), ==, 2); munit_assert_string_equal(st_registry_var_name(reg, 0), "CC"); munit_assert_string_equal(st_registry_var_value(reg, 0), "cc"); munit_assert_string_equal(st_registry_var_name(reg, 1), "CFLAGS"); munit_assert_string_equal(st_registry_var_value(reg, 1), ""); /* get_var is borrowed, not owned: registry still frees cleanly */ st_registry_free(reg); return MUNIT_OK; } /* the C++ language module detects over the same ABI (env CXX -> c++ -> * g++/clang++) and registers CXX/CXXFLAGS. */ static MunitResult test_detect_cxx(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx = st_ext_ctx_new(); struct st_error *err; const struct st_toolchain *tc; munit_assert_not_null(ctx); munit_assert_null(st_ext_init_builtins(ctx)); reset_cc_env(); err = st_ext_detect_language(ctx, "cxx"); if (err != NULL) { /* Host without a C++ compiler: clean error, no half-registration. */ munit_assert_not_null(strstr(st_error_message(err), "found")); munit_assert_null(st_registry_get_var(st_ext_var_registry(ctx), "CXX")); st_error_free(err); } else { tc = st_ext_language_toolchain_named(ctx, "cxx"); munit_assert_not_null(tc); munit_assert_int(strlen(tc->path), >, 0); munit_assert_int(strlen(tc->id), >, 0); munit_assert_string_equal( st_registry_get_var(st_ext_var_registry(ctx), "CXX"), tc->path); munit_assert_not_null( st_registry_get_var(st_ext_var_registry(ctx), "CXXFLAGS")); } st_ext_ctx_free(ctx); return MUNIT_OK; } /* ABI robustness: duplicate registrations and unknown lookups error. */ static MunitResult test_registry_errors(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_ext_ctx *ctx = st_ext_ctx_new(); struct st_error *err; static const char *const vars[] = { "CC", NULL }; const struct st_toolchain *tc; int fake_detect_called = 0; munit_assert_not_null(ctx); err = st_ext_register_language(ctx, "c", fake_detect, &fake_detect_called, vars); munit_assert_null(err); /* duplicate language registration errors */ err = st_ext_register_language(ctx, "c", fake_detect, NULL, vars); munit_assert_not_null(err); st_error_free(err); /* duplicate check registration errors */ err = st_ext_register_check(ctx, "c", "header", NULL); munit_assert_null(err); err = st_ext_register_check(ctx, "c", "header", NULL); munit_assert_not_null(err); st_error_free(err); /* detecting an unknown language errors, does not crash */ err = st_ext_detect_language(ctx, "nope"); munit_assert_not_null(err); st_error_free(err); /* detection routes through the registered function pointer */ err = st_ext_detect_language(ctx, "c"); munit_assert_null(err); munit_assert_int(fake_detect_called, ==, 1); tc = st_ext_language_toolchain_named(ctx, "c"); munit_assert_not_null(tc); munit_assert_string_equal(tc->id, "fake"); /* unknown-name toolchain lookup returns NULL */ munit_assert_null(st_ext_language_toolchain_named(ctx, "nope")); st_ext_ctx_free(ctx); return MUNIT_OK; } static MunitTest tests[] = { { "/abi/builtins-register", test_builtins_register_languages, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/abi/detect-c", test_detect_c, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/abi/env-override-clang", test_env_override_clang, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/abi/env-override-gcc", test_env_override_gcc, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/abi/bogus-cc-clean-error", test_bogus_cc_clean_error, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/abi/empty-path-clean-error", test_empty_path_clean_error, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/abi/registered-vars-queryable", test_registered_vars_queryable, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/abi/var-registry-semantics", test_var_registry_semantics, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/abi/detect-cxx", test_detect_cxx, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { "/abi/registry-errors", test_registry_errors, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, }; static const MunitSuite suite = { "/abi", 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); }