Files
stupidtools/tests/unit/test_discovery.c
T
huntedbytheirs 783019dff4 feat(ext): add extension discovery and loading
Extension discovery (plan todo 20): scans --ext-dir dirs,
STUPIDTOOLS_EXT, the XDG user dir and a builtin-ext dir for *.lua
modules, runs each in the sandboxed Lua runtime, and enumerates its
registrations; the builtin C/C++ modules load first via
st_ext_init_builtins. Fail-fast with an error naming file and line on
any malformed/sandbox-violating module.

The CLI gains a repeatable --ext-dir flag; with flag or env present
main.c runs discovery and prints 'loaded extension: ...' lines from
actual registrations (interim wiring until todo 23).

Note: src/Makefile.am gained the ext + vendored Lua sources because
the binary otherwise cannot link discovery -- required for the
root-verifiable CLI acceptance.
2026-08-28 21:17:05 -04:00

562 lines
16 KiB
C

/* LINK: ../../src/ext/discovery.c ../../src/ext/lua.c ../../src/ext/abi.c ../../src/ext/lang_c.c ../../src/ext/lang_cpp.c ../../src/error.c ../../src/span.c ../../thirdparty/lua/lapi.c ../../thirdparty/lua/lauxlib.c ../../thirdparty/lua/lbaselib.c ../../thirdparty/lua/lcode.c ../../thirdparty/lua/lctype.c ../../thirdparty/lua/ldebug.c ../../thirdparty/lua/ldo.c ../../thirdparty/lua/ldump.c ../../thirdparty/lua/lfunc.c ../../thirdparty/lua/lgc.c ../../thirdparty/lua/llex.c ../../thirdparty/lua/lmem.c ../../thirdparty/lua/lobject.c ../../thirdparty/lua/lopcodes.c ../../thirdparty/lua/lparser.c ../../thirdparty/lua/lstate.c ../../thirdparty/lua/lstring.c ../../thirdparty/lua/ltable.c ../../thirdparty/lua/ltm.c ../../thirdparty/lua/lundump.c ../../thirdparty/lua/lvm.c ../../thirdparty/lua/lzio.c ../../thirdparty/lua/lstrlib.c ../../thirdparty/lua/ltablib.c */
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L /* mkdtemp, strdup */
#endif
/*
* tests/unit/test_discovery.c
*
* Unit tests for extension discovery and loading (src/ext/discovery.h,
* plan todo 20).
*
* The magic LINK comment on line 1 is REQUIRED by tests/run.sh: it
* lists the extra .c sources compiled into this test binary. It links
* discovery.c plus everything it needs -- the sandboxed Lua runtime
* (lua.c + the same 27 vendored Lua sources as test_lua.c, minus
* linit/lmathlib/loadlib/liolib/loslib/lua.c/luac.c, see the -lm note
* there), the extension ABI + builtin C/C++ modules, and error/span.
*
* Environment isolation: every test points XDG_DATA_HOME at an empty
* temp dir and unsets STUPIDTOOLS_EXT first, so host state can never
* leak into discovery. The builtin directory (builtin-ext relative to
* the cwd) ships no .lua modules in this todo, so it is a no-op as
* well; note that exact runtime-count assertions below assume that.
*/
#include "munit.h"
#include "ext/abi.h"
#include "ext/discovery.h"
#include "ext/lua.h"
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
/* ---------- filesystem helpers ---------- */
static char *
make_temp_dir(void)
{
char *d = strdup("/tmp/st_discovery_XXXXXX");
if (d == NULL || mkdtemp(d) == NULL) {
free(d);
return NULL;
}
return d;
}
static int
write_file(const char *dir, const char *name, const char *content)
{
char path[1024];
FILE *f;
size_t len = strlen(content);
if (snprintf(path, sizeof path, "%s/%s", dir, name) >=
(int)sizeof path) {
return -1;
}
f = fopen(path, "wb");
if (f == NULL) return -1;
if (fwrite(content, 1, len, f) != len) {
(void)fclose(f);
return -1;
}
return fclose(f) == 0 ? 0 : -1;
}
static int
write_bytes(const char *dir, const char *name, const unsigned char *bytes,
size_t len)
{
char path[1024];
FILE *f;
if (snprintf(path, sizeof path, "%s/%s", dir, name) >=
(int)sizeof path) {
return -1;
}
f = fopen(path, "wb");
if (f == NULL) return -1;
if (fwrite(bytes, 1, len, f) != len) {
(void)fclose(f);
return -1;
}
return fclose(f) == 0 ? 0 : -1;
}
/* Recursive delete (plain POSIX: unlink files, rmdir dirs). */
static void
rm_rf(const char *path)
{
struct stat st;
DIR *d;
struct dirent *e;
if (lstat(path, &st) != 0) return;
if (!S_ISDIR(st.st_mode)) {
(void)unlink(path);
return;
}
d = opendir(path);
if (d == NULL) return;
while ((e = readdir(d)) != NULL) {
char sub[1024];
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0)
continue;
if (snprintf(sub, sizeof sub, "%s/%s", path, e->d_name) >=
(int)sizeof sub)
continue;
rm_rf(sub);
}
(void)closedir(d);
(void)rmdir(path);
}
/* Fresh ctx + rt pair for one discovery run (discovery is one-shot per
* ctx: it re-runs the builtin init, so a second call would fail on
* duplicate registrations). */
static int
fresh_pair(struct st_ext_ctx **ctx, struct st_lua_rt **rt)
{
*ctx = st_ext_ctx_new();
*rt = st_lua_rt_new();
return *ctx != NULL && *rt != NULL ? 0 : -1;
}
/* ---------- (a) a valid module registers into the runtime ---------- */
static MunitResult
test_valid_module(const MunitParameter params[], void *data)
{
char *xdg = NULL;
char *ext = NULL;
struct st_ext_ctx *ctx = NULL;
struct st_lua_rt *rt = NULL;
struct st_error *err;
const char *dirs[1];
(void)params;
(void)data;
xdg = make_temp_dir();
ext = make_temp_dir();
munit_assert_not_null(xdg);
munit_assert_not_null(ext);
munit_assert_int(setenv("XDG_DATA_HOME", xdg, 1), ==, 0);
munit_assert_int(unsetenv("STUPIDTOOLS_EXT"), ==, 0);
munit_assert_int(write_file(ext, "hello.lua",
"st.register_check(\"magic\")\n"
"st.register_language(\"fortran\")\n"), ==, 0);
munit_assert_int(fresh_pair(&ctx, &rt), ==, 0);
dirs[0] = ext;
err = st_ext_discover(ctx, rt, dirs, 1);
munit_assert_null(err);
/* the module's registrations are enumerable from the runtime */
munit_assert_size(st_lua_check_count(rt), ==, 1);
munit_assert_string_equal(st_lua_check_name(rt, 0), "magic");
munit_assert_size(st_lua_language_count(rt), ==, 1);
munit_assert_string_equal(st_lua_language_name(rt, 0), "fortran");
/* the builtin C/C++ modules landed in the ABI ctx */
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");
st_lua_rt_free(rt);
st_ext_ctx_free(ctx);
rm_rf(ext);
rm_rf(xdg);
free(ext);
free(xdg);
return MUNIT_OK;
}
/* ---------- (b) malformed module: clear error naming the file, fail-fast ---------- */
static MunitResult
test_malformed_fail_fast(const MunitParameter params[], void *data)
{
char *xdg = NULL;
char *ext = NULL;
struct st_ext_ctx *ctx = NULL;
struct st_lua_rt *rt = NULL;
struct st_error *err;
const char *msg;
const char *dirs[1];
(void)params;
(void)data;
xdg = make_temp_dir();
ext = make_temp_dir();
munit_assert_not_null(xdg);
munit_assert_not_null(ext);
munit_assert_int(setenv("XDG_DATA_HOME", xdg, 1), ==, 0);
munit_assert_int(unsetenv("STUPIDTOOLS_EXT"), ==, 0);
munit_assert_int(write_file(ext, "bad.lua", "local x =\n"), ==, 0);
munit_assert_int(write_file(ext, "good.lua",
"st.register_check(\"never\")\n"), ==, 0);
munit_assert_int(fresh_pair(&ctx, &rt), ==, 0);
dirs[0] = ext;
err = st_ext_discover(ctx, rt, dirs, 1);
munit_assert_not_null(err);
munit_assert_int(st_error_category_of(err), ==, ST_ERR_INTERNAL);
msg = st_error_message(err);
munit_assert_not_null(msg);
munit_assert_not_null(strstr(msg, "extension load failed"));
munit_assert_not_null(strstr(msg, "bad.lua"));
munit_assert_not_null(strstr(msg, ":2:")); /* file AND line named */
/* fail-fast: good.lua (which sorts after bad.lua) never ran */
munit_assert_size(st_lua_check_count(rt), ==, 0);
st_error_free(err);
st_lua_rt_free(rt);
st_ext_ctx_free(ctx);
rm_rf(ext);
rm_rf(xdg);
free(ext);
free(xdg);
return MUNIT_OK;
}
/* ---------- (c) a sandbox violation yields the sandbox error ---------- */
static MunitResult
test_sandbox_violation(const MunitParameter params[], void *data)
{
char *xdg = NULL;
char *ext = NULL;
struct st_ext_ctx *ctx = NULL;
struct st_lua_rt *rt = NULL;
struct st_error *err;
const char *msg;
const char *dirs[1];
(void)params;
(void)data;
xdg = make_temp_dir();
ext = make_temp_dir();
munit_assert_not_null(xdg);
munit_assert_not_null(ext);
munit_assert_int(setenv("XDG_DATA_HOME", xdg, 1), ==, 0);
munit_assert_int(unsetenv("STUPIDTOOLS_EXT"), ==, 0);
munit_assert_int(write_file(ext, "evil.lua", "os.execute(\"id\")\n"),
==, 0);
munit_assert_int(fresh_pair(&ctx, &rt), ==, 0);
dirs[0] = ext;
err = st_ext_discover(ctx, rt, dirs, 1);
munit_assert_not_null(err);
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
msg = st_error_message(err);
munit_assert_not_null(msg);
munit_assert_not_null(strstr(msg, "sandbox blocked"));
munit_assert_not_null(strstr(msg, "evil.lua"));
st_error_free(err);
st_lua_rt_free(rt);
st_ext_ctx_free(ctx);
rm_rf(ext);
rm_rf(xdg);
free(ext);
free(xdg);
return MUNIT_OK;
}
/* ---------- (d) empty dir / missing dir / NULL dirs: clean no-op ---------- */
static MunitResult
test_empty_and_missing_dirs(const MunitParameter params[], void *data)
{
char *xdg = NULL;
char *empty = NULL;
struct st_ext_ctx *ctx = NULL;
struct st_lua_rt *rt = NULL;
struct st_error *err;
const char *dirs[2];
(void)params;
(void)data;
xdg = make_temp_dir();
empty = make_temp_dir();
munit_assert_not_null(xdg);
munit_assert_not_null(empty);
munit_assert_int(setenv("XDG_DATA_HOME", xdg, 1), ==, 0);
munit_assert_int(unsetenv("STUPIDTOOLS_EXT"), ==, 0);
munit_assert_int(fresh_pair(&ctx, &rt), ==, 0);
/* a nonexistent dir and an empty dir are both silent no-ops */
dirs[0] = "/nonexistent/st_discovery_dir";
dirs[1] = empty;
err = st_ext_discover(ctx, rt, dirs, 2);
munit_assert_null(err);
munit_assert_size(st_lua_check_count(rt), ==, 0);
munit_assert_size(st_lua_language_count(rt), ==, 0);
munit_assert_size(st_ext_language_count(ctx), ==, 2); /* builtins only */
/* NULL/0 extra dirs are valid too (env + user + builtin only) */
st_lua_rt_free(rt);
st_ext_ctx_free(ctx);
munit_assert_int(fresh_pair(&ctx, &rt), ==, 0);
err = st_ext_discover(ctx, rt, NULL, 0);
munit_assert_null(err);
munit_assert_size(st_lua_check_count(rt), ==, 0);
/* discovery is one-shot per ctx: the builtin init re-runs and
* duplicate registrations fail (documented contract) */
err = st_ext_discover(ctx, rt, NULL, 0);
munit_assert_not_null(err);
st_error_free(err);
st_lua_rt_free(rt);
st_ext_ctx_free(ctx);
rm_rf(empty);
rm_rf(xdg);
free(empty);
free(xdg);
return MUNIT_OK;
}
/* ---------- (e) a binary file named *.lua yields a load error ---------- */
static MunitResult
test_binary_file(const MunitParameter params[], void *data)
{
static const unsigned char elf_head[] = { 0x7f, 'E', 'L', 'F', 0x02,
0x01, 0x01 };
char *xdg = NULL;
char *ext = NULL;
struct st_ext_ctx *ctx = NULL;
struct st_lua_rt *rt = NULL;
struct st_error *err;
const char *msg;
const char *dirs[1];
(void)params;
(void)data;
xdg = make_temp_dir();
ext = make_temp_dir();
munit_assert_not_null(xdg);
munit_assert_not_null(ext);
munit_assert_int(setenv("XDG_DATA_HOME", xdg, 1), ==, 0);
munit_assert_int(unsetenv("STUPIDTOOLS_EXT"), ==, 0);
munit_assert_int(write_bytes(ext, "bin.lua", elf_head,
sizeof elf_head), ==, 0);
munit_assert_int(fresh_pair(&ctx, &rt), ==, 0);
dirs[0] = ext;
err = st_ext_discover(ctx, rt, dirs, 1);
munit_assert_not_null(err);
msg = st_error_message(err);
munit_assert_not_null(msg);
munit_assert_not_null(strstr(msg, "extension load failed"));
munit_assert_not_null(strstr(msg, "bin.lua"));
st_error_free(err);
st_lua_rt_free(rt);
st_ext_ctx_free(ctx);
rm_rf(ext);
rm_rf(xdg);
free(ext);
free(xdg);
return MUNIT_OK;
}
/* ---------- (f) non-.lua entries (and a DIR named *.lua) are ignored ---------- */
static MunitResult
test_ignores_non_lua(const MunitParameter params[], void *data)
{
char *xdg = NULL;
char *ext = NULL;
char sub[1024];
int sub_len;
struct st_ext_ctx *ctx = NULL;
struct st_lua_rt *rt = NULL;
struct st_error *err;
const char *dirs[1];
(void)params;
(void)data;
xdg = make_temp_dir();
ext = make_temp_dir();
munit_assert_not_null(xdg);
munit_assert_not_null(ext);
munit_assert_int(setenv("XDG_DATA_HOME", xdg, 1), ==, 0);
munit_assert_int(unsetenv("STUPIDTOOLS_EXT"), ==, 0);
munit_assert_int(write_file(ext, "notes.txt",
"st.register_check(\"hidden\")\n"), ==, 0);
sub_len = snprintf(sub, sizeof sub, "%s/sub.lua", ext);
munit_assert_int(sub_len, >=, 0);
munit_assert_int(mkdir(sub, 0777), ==, 0);
munit_assert_int(write_file(ext, "sub.lua/inner.lua",
"st.register_check(\"also-hidden\")\n"), ==, 0);
munit_assert_int(fresh_pair(&ctx, &rt), ==, 0);
dirs[0] = ext;
err = st_ext_discover(ctx, rt, dirs, 1);
munit_assert_null(err);
munit_assert_size(st_lua_check_count(rt), ==, 0);
munit_assert_size(st_lua_language_count(rt), ==, 0);
st_lua_rt_free(rt);
st_ext_ctx_free(ctx);
rm_rf(ext);
rm_rf(xdg);
free(ext);
free(xdg);
return MUNIT_OK;
}
/* ---------- (g) modules load in bytewise-lexicographic order ---------- */
static MunitResult
test_deterministic_order(const MunitParameter params[], void *data)
{
char *xdg = NULL;
char *ext = NULL;
struct st_ext_ctx *ctx = NULL;
struct st_lua_rt *rt = NULL;
struct st_error *err;
const char *dirs[1];
(void)params;
(void)data;
xdg = make_temp_dir();
ext = make_temp_dir();
munit_assert_not_null(xdg);
munit_assert_not_null(ext);
munit_assert_int(setenv("XDG_DATA_HOME", xdg, 1), ==, 0);
munit_assert_int(unsetenv("STUPIDTOOLS_EXT"), ==, 0);
/* written in reverse order on purpose: discovery must sort */
munit_assert_int(write_file(ext, "b.lua",
"st.register_check(\"second\")\n"), ==, 0);
munit_assert_int(write_file(ext, "a.lua",
"st.register_check(\"first\")\n"), ==, 0);
munit_assert_int(fresh_pair(&ctx, &rt), ==, 0);
dirs[0] = ext;
err = st_ext_discover(ctx, rt, dirs, 1);
munit_assert_null(err);
munit_assert_size(st_lua_check_count(rt), ==, 2);
munit_assert_string_equal(st_lua_check_name(rt, 0), "first");
munit_assert_string_equal(st_lua_check_name(rt, 1), "second");
st_lua_rt_free(rt);
st_ext_ctx_free(ctx);
rm_rf(ext);
rm_rf(xdg);
free(ext);
free(xdg);
return MUNIT_OK;
}
/* ---------- (h) STUPIDTOOLS_EXT is honored, after the explicit dirs ---------- */
static MunitResult
test_env_dirs(const MunitParameter params[], void *data)
{
char *xdg = NULL;
char *ext1 = NULL;
char *ext2 = NULL;
struct st_ext_ctx *ctx = NULL;
struct st_lua_rt *rt = NULL;
struct st_error *err;
const char *dirs[1];
(void)params;
(void)data;
xdg = make_temp_dir();
ext1 = make_temp_dir();
ext2 = make_temp_dir();
munit_assert_not_null(xdg);
munit_assert_not_null(ext1);
munit_assert_not_null(ext2);
munit_assert_int(setenv("XDG_DATA_HOME", xdg, 1), ==, 0);
munit_assert_int(setenv("STUPIDTOOLS_EXT", ext2, 1), ==, 0);
munit_assert_int(write_file(ext1, "one.lua",
"st.register_check(\"one\")\n"), ==, 0);
munit_assert_int(write_file(ext2, "two.lua",
"st.register_check(\"two\")\n"), ==, 0);
munit_assert_int(fresh_pair(&ctx, &rt), ==, 0);
dirs[0] = ext1;
err = st_ext_discover(ctx, rt, dirs, 1);
munit_assert_null(err);
munit_assert_size(st_lua_check_count(rt), ==, 2);
munit_assert_string_equal(st_lua_check_name(rt, 0), "one");
munit_assert_string_equal(st_lua_check_name(rt, 1), "two");
st_lua_rt_free(rt);
st_ext_ctx_free(ctx);
rm_rf(ext1);
rm_rf(ext2);
rm_rf(xdg);
free(ext1);
free(ext2);
free(xdg);
return MUNIT_OK;
}
static MunitTest tests[] = {
{ "/discovery/valid-module", test_valid_module, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/discovery/malformed-fail-fast", test_malformed_fail_fast, NULL,
NULL, MUNIT_TEST_OPTION_NONE, NULL },
{ "/discovery/sandbox-violation", test_sandbox_violation, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/discovery/empty-and-missing-dirs", test_empty_and_missing_dirs,
NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
{ "/discovery/binary-file", test_binary_file, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/discovery/ignores-non-lua", test_ignores_non_lua, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/discovery/deterministic-order", test_deterministic_order, NULL,
NULL, MUNIT_TEST_OPTION_NONE, NULL },
{ "/discovery/env-dirs", test_env_dirs, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
};
static const MunitSuite suite = {
"/discovery", 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);
}