/* 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 #include #include #include #include #include /* ---------- 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); }