Template
463 lines
15 KiB
C
463 lines
15 KiB
C
/* 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);
|
|
}
|