feat(ext): add builtin C/C++ language modules

This commit is contained in:
2026-08-28 20:17:58 -04:00
parent 439aa0d856
commit 468aa1c4ef
8 changed files with 1670 additions and 0 deletions
+462
View File
@@ -0,0 +1,462 @@
/* 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 <stdlib.h>
#include <string.h>
/* ---------- 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 <cmd>? (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);
}