Template
feat(ext): add builtin C/C++ language modules
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user