From 65159a5d214c979d010cd27178f973ca94b60026 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Tue, 14 Apr 2026 17:19:14 +0900 Subject: [PATCH 1/4] feat(#461): Add io_plugin_dlopen meson option and plugin entry point types Add io_plugin_dlopen combo option (default disabled) to meson_options.txt. Define WL_IO_PLUGIN_EXPORT visibility macro and wl_io_plugin_entry_fn typedef in io_adapter.h for the Path B plugin contract (#446 section 7). --- meson_options.txt | 8 ++++++++ wirelog/io/io_adapter.h | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/meson_options.txt b/meson_options.txt index b5dd3b7a..801ed30f 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -43,6 +43,14 @@ option( description: 'Default CRC-32 variant: ethernet (standard) or castagnoli (iSCSI)' ) +option( + 'io_plugin_dlopen', + type: 'combo', + choices: ['disabled', 'enabled'], + value: 'disabled', + description: 'Enable optional CLI plugin loader via dlopen (Issue #461). Excluded from Android/iOS.' +) + option( 'mbedTLS', type: 'combo', diff --git a/wirelog/io/io_adapter.h b/wirelog/io/io_adapter.h index 3cc37c91..43fd6aed 100644 --- a/wirelog/io/io_adapter.h +++ b/wirelog/io/io_adapter.h @@ -125,6 +125,46 @@ WL_PUBLIC const wl_io_adapter_t *wl_io_find_adapter( WL_PUBLIC const char *wl_io_last_error(void); +/* ======================================================================== */ +/* Plugin Entry Point (Path B, Issue #461) */ +/* ======================================================================== */ + +/* + * Macro for plugin shared libraries to export their entry symbol. + * Users define their entry function with this attribute so the linker + * does not strip it even under -fvisibility=hidden or LTO. + */ +#if defined(_WIN32) || defined(__CYGWIN__) +#define WL_IO_PLUGIN_EXPORT __declspec(dllexport) +#elif defined(__GNUC__) || defined(__clang__) +#define WL_IO_PLUGIN_EXPORT __attribute__((visibility("default"))) +#else +#define WL_IO_PLUGIN_EXPORT +#endif + +/* + * Plugin entry point signature. + * + * A plugin shared library must export exactly one symbol named + * "wl_io_plugin_entry" with this signature. The CLI plugin loader + * calls dlopen() on the library, resolves this symbol via dlsym(), + * validates the ABI version, and bulk-registers all returned adapters. + * + * Parameters: + * n_out [out] Number of adapters in the returned array. + * abi_ver [in] Host's WL_IO_ABI_VERSION. The plugin should check + * this against its own compiled version and return NULL + * on mismatch. + * + * Returns: + * Array of adapter pointers (must remain valid for process lifetime), + * or NULL on ABI mismatch / error. + */ +typedef const wl_io_adapter_t *const *(*wl_io_plugin_entry_fn)( + uint32_t *n_out, uint32_t abi_ver); + +#define WL_IO_PLUGIN_ENTRY_SYMBOL "wl_io_plugin_entry" + #ifdef __cplusplus } #endif From c2fe30a0148f2c47505eda216a6f37627bce2558 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Tue, 14 Apr 2026 17:19:22 +0900 Subject: [PATCH 2/4] feat(#461): Implement CLI plugin loader with dlopen Add plugin_loader.c/.h implementing dlopen + dlsym("wl_io_plugin_entry") + ABI version validation + bulk adapter registration with fail-fast semantics. Tracks loaded plugins for clean dlclose at shutdown. Single-threaded CLI-only module, not thread-safe by design. --- wirelog/cli/plugin_loader.c | 151 ++++++++++++++++++++++++++++++++++++ wirelog/cli/plugin_loader.h | 40 ++++++++++ 2 files changed, 191 insertions(+) create mode 100644 wirelog/cli/plugin_loader.c create mode 100644 wirelog/cli/plugin_loader.h diff --git a/wirelog/cli/plugin_loader.c b/wirelog/cli/plugin_loader.c new file mode 100644 index 00000000..c0b59475 --- /dev/null +++ b/wirelog/cli/plugin_loader.c @@ -0,0 +1,151 @@ +/* + * plugin_loader.c - CLI Plugin Loader Implementation (Issue #461) + * + * Copyright (C) CleverPlant + * Licensed under LGPL-3.0 + * + * Loads user adapter plugins via dlopen and bulk-registers them + * with the I/O adapter registry. Gated behind the io_plugin_dlopen + * meson option; this file is only compiled when the option is enabled. + */ + +#include "plugin_loader.h" +#include "wirelog/io/io_adapter.h" + +#include +#include +#include + +/* ------------------------------------------------------------------------ */ +/* Loaded plugin tracking */ +/* ------------------------------------------------------------------------ */ + +#define MAX_PLUGINS 16 + +typedef struct { + void *handle; + const wl_io_adapter_t *const *adapters; + uint32_t n_adapters; +} loaded_plugin_t; + +static loaded_plugin_t s_plugins[MAX_PLUGINS]; +static uint32_t s_plugin_count; + +/* ------------------------------------------------------------------------ */ +/* Public API */ +/* ------------------------------------------------------------------------ */ + +int +wl_plugin_load(const char *path) +{ + if (!path) { + fprintf(stderr, "error: plugin path is NULL\n"); + return -1; + } + + if (s_plugin_count >= MAX_PLUGINS) { + fprintf(stderr, "error: too many plugins loaded (max %d)\n", + MAX_PLUGINS); + return -1; + } + + void *handle = dlopen(path, RTLD_NOW | RTLD_LOCAL); + if (!handle) { + fprintf(stderr, "error: cannot load plugin '%s': %s\n", + path, dlerror()); + return -1; + } + + /* Clear any stale dlerror */ + dlerror(); + + wl_io_plugin_entry_fn entry = + (wl_io_plugin_entry_fn)dlsym(handle, WL_IO_PLUGIN_ENTRY_SYMBOL); + + const char *err = dlerror(); + if (err || !entry) { + fprintf(stderr, + "error: plugin '%s' missing symbol '%s': %s\n", + path, WL_IO_PLUGIN_ENTRY_SYMBOL, + err ? err : "symbol resolved to NULL"); + dlclose(handle); + return -1; + } + + uint32_t n_out = 0; + const wl_io_adapter_t *const *adapters = + entry(&n_out, WL_IO_ABI_VERSION); + + if (!adapters) { + fprintf(stderr, + "error: plugin '%s' returned NULL " + "(ABI version mismatch? host=%u)\n", + path, (unsigned)WL_IO_ABI_VERSION); + dlclose(handle); + return -1; + } + + if (n_out == 0) { + fprintf(stderr, "warning: plugin '%s' returned 0 adapters\n", path); + dlclose(handle); + return 0; + } + + /* Bulk-register all adapters from this plugin */ + uint32_t registered = 0; + for (uint32_t i = 0; i < n_out; i++) { + if (!adapters[i]) { + fprintf(stderr, + "error: plugin '%s' adapter[%u] is NULL\n", + path, (unsigned)i); + continue; + } + if (wl_io_register_adapter(adapters[i]) != 0) { + fprintf(stderr, + "error: plugin '%s' adapter[%u] (%s) " + "registration failed: %s\n", + path, (unsigned)i, + adapters[i]->scheme ? adapters[i]->scheme : "(null)", + wl_io_last_error()); + /* Fail fast: unregister what we registered and close */ + for (uint32_t j = 0; j < i; j++) { + if (adapters[j] && adapters[j]->scheme) { + wl_io_unregister_adapter(adapters[j]->scheme); + } + } + dlclose(handle); + return -1; + } + registered++; + } + + /* Track the loaded plugin for cleanup */ + s_plugins[s_plugin_count].handle = handle; + s_plugins[s_plugin_count].adapters = adapters; + s_plugins[s_plugin_count].n_adapters = registered; + s_plugin_count++; + + return 0; +} + +void +wl_plugin_unload_all(void) +{ + for (uint32_t i = 0; i < s_plugin_count; i++) { + loaded_plugin_t *p = &s_plugins[i]; + + /* Unregister all adapters from this plugin */ + for (uint32_t j = 0; j < p->n_adapters; j++) { + if (p->adapters[j] && p->adapters[j]->scheme) { + wl_io_unregister_adapter(p->adapters[j]->scheme); + } + } + + dlclose(p->handle); + p->handle = NULL; + p->adapters = NULL; + p->n_adapters = 0; + } + + s_plugin_count = 0; +} diff --git a/wirelog/cli/plugin_loader.h b/wirelog/cli/plugin_loader.h new file mode 100644 index 00000000..408e6b7b --- /dev/null +++ b/wirelog/cli/plugin_loader.h @@ -0,0 +1,40 @@ +/* + * plugin_loader.h - CLI Plugin Loader Interface (Issue #461) + * + * Copyright (C) CleverPlant + * Licensed under LGPL-3.0 + */ + +#ifndef WIRELOG_CLI_PLUGIN_LOADER_H +#define WIRELOG_CLI_PLUGIN_LOADER_H + +/* + * Load adapter plugin(s) from a shared library. + * + * Opens the library at `path` via dlopen, resolves the + * "wl_io_plugin_entry" symbol, validates the ABI version, + * and bulk-registers all returned adapters. + * + * Returns 0 on success, -1 on error (message printed to stderr). + * Returns -1 if any adapter fails registration (fail-fast). + * + * Thread safety: NOT thread-safe. This is a CLI-only module; + * call from a single thread before starting the evaluation pipeline. + */ +int wl_plugin_load(const char *path); + +/* + * Unload all previously loaded plugins. + * + * Unregisters all adapters that were registered via wl_plugin_load() + * and closes the shared library handles. Safe to call if no plugins + * were loaded. + * + * Must be called only after all I/O activity has ceased (i.e., after + * the evaluation pipeline has completed and all worker threads have + * joined). The adapter pointers returned by the plugin live in the + * dlopen'd library and become invalid after dlclose. + */ +void wl_plugin_unload_all(void); + +#endif /* WIRELOG_CLI_PLUGIN_LOADER_H */ From 3d6feafeeb997bcc3b02217fc6f993b718f81144 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Tue, 14 Apr 2026 17:19:31 +0900 Subject: [PATCH 3/4] feat(#461): Wire --load-adapter CLI option with conditional dlopen build Add repeatable --load-adapter=PATH argument to CLI main.c, gated behind WL_HAVE_PLUGIN_LOADER define. Update meson.build to conditionally link libdl and include plugin_loader.c when io_plugin_dlopen=enabled. Excluded from Windows builds. --- meson.build | 21 ++++++++++++- wirelog/cli/main.c | 74 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 84 insertions(+), 11 deletions(-) diff --git a/meson.build b/meson.build index 5d6be6cf..e0ed632b 100644 --- a/meson.build +++ b/meson.build @@ -83,12 +83,15 @@ tests_option = get_option('tests') docs_option = get_option('documentation') # crc32_variant_option already defined earlier before compiler settings +io_plugin_dlopen_option = get_option('io_plugin_dlopen') + message('wirelog ' + project_version) message(' Platform: ' + platform) message(' Embedded: @0@'.format(is_embedded)) message(' IPC support: ' + ipc_option) message(' Threading: ' + threads_option) message(' CRC-32 variant: ' + crc32_variant_option) +message(' Plugin loader (dlopen): ' + io_plugin_dlopen_option) #== == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == #Dependencies @@ -277,15 +280,31 @@ pkg.generate( #CLI Driver #== == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == +# Optional plugin loader via dlopen (Issue #461) +# Excluded from Windows builds (no dlopen); Android/iOS excluded via option default. +cli_plugin_src = [] +cli_plugin_deps = [] +cli_plugin_args = [] +if io_plugin_dlopen_option == 'enabled' and not is_windows + _dl_dep = cc.find_library('dl', required: false) + if _dl_dep.found() + cli_plugin_deps += [_dl_dep] + endif + cli_plugin_src = files('wirelog/cli/plugin_loader.c') + cli_plugin_args = ['-DWL_HAVE_PLUGIN_LOADER'] +endif + #Build wirelog-cli executable (wirelog_cli_src defined in wirelog/meson.build) #Include all wirelog sources to ensure proper linking with LTO wirelog_cli = executable( 'wirelog_cli', wirelog_cli_src, + cli_plugin_src, wirelog_sources, config_h_file, include_directories: [wirelog_inc, wirelog_src_inc], - dependencies: [threads_dep, zlib_dep, nanoarrow_dep, xxhash_dep, mbedtls_dep, math_dep], + c_args: cli_plugin_args, + dependencies: [threads_dep, zlib_dep, nanoarrow_dep, xxhash_dep, mbedtls_dep, math_dep] + cli_plugin_deps, install: true, ) diff --git a/wirelog/cli/main.c b/wirelog/cli/main.c index c57813d9..e414a4ea 100644 --- a/wirelog/cli/main.c +++ b/wirelog/cli/main.c @@ -8,30 +8,45 @@ * Reads a .dl file and executes it through the full Datalog pipeline. * * Usage: wirelog [options] - * --workers N Number of worker threads (default: 1) - * --watch [interval] Watch mode: re-evaluate on stdin input (default: 1000ms) - * --help Show usage information + * --workers N Number of worker threads (default: 1) + * --watch [interval] Watch mode: re-evaluate on stdin input + * --load-adapter PATH Load adapter plugin (repeatable, requires + * -Dio_plugin_dlopen=enabled) + * --help Show usage information */ #include "driver.h" +#ifdef WL_HAVE_PLUGIN_LOADER +#include "plugin_loader.h" +#endif + #include #include #include #include +#define MAX_ADAPTER_PATHS 16 + static void print_usage(const char *progname) { fprintf(stderr, "Usage: %s [options] \n", progname); fprintf(stderr, "\nOptions:\n"); - fprintf(stderr, " --workers N Number of worker threads (default: 1)\n"); - fprintf(stderr, " --delta Execute in delta-query mode (emit delta " - "tuples to stdout)\n"); + fprintf(stderr, " --workers N Number of worker threads " + "(default: 1)\n"); + fprintf(stderr, " --delta Execute in delta-query mode " + "(emit delta tuples to stdout)\n"); fprintf(stderr, - " --watch [N] Watch mode: read facts from stdin and re-evaluate " - "(interval N ms, default 1000)\n"); - fprintf(stderr, " --help Show this help message\n"); + " --watch [N] Watch mode: read facts from stdin and " + "re-evaluate (interval N ms, default 1000)\n"); + fprintf(stderr, " --load-adapter PATH Load adapter plugin " + "(repeatable" +#ifndef WL_HAVE_PLUGIN_LOADER + ", disabled in this build" +#endif + ")\n"); + fprintf(stderr, " --help Show this help message\n"); } int @@ -42,6 +57,8 @@ main(int argc, char *argv[]) bool delta_mode = false; bool watch_mode = false; uint32_t watch_interval_ms = 1000; + const char *adapter_paths[MAX_ADAPTER_PATHS]; + int n_adapters = 0; for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { @@ -77,6 +94,28 @@ main(int argc, char *argv[]) num_workers = (uint32_t)val; continue; } + if (strcmp(argv[i], "--load-adapter") == 0) { + if (i + 1 >= argc) { + fprintf(stderr, + "error: --load-adapter requires a path argument\n"); + return 1; + } +#ifdef WL_HAVE_PLUGIN_LOADER + if (n_adapters >= MAX_ADAPTER_PATHS) { + fprintf(stderr, "error: too many --load-adapter options " + "(max %d)\n", + MAX_ADAPTER_PATHS); + return 1; + } + adapter_paths[n_adapters++] = argv[++i]; +#else + fprintf(stderr, + "error: --load-adapter is not available in this build.\n" + "Rebuild with: meson setup -Dio_plugin_dlopen=enabled\n"); + return 1; +#endif + continue; + } if (argv[i][0] == '-') { fprintf(stderr, "error: unknown option '%s'\n", argv[i]); print_usage(argv[0]); @@ -95,6 +134,17 @@ main(int argc, char *argv[]) return 1; } +#ifdef WL_HAVE_PLUGIN_LOADER + /* Load adapter plugins before reading the program */ + for (int i = 0; i < n_adapters; i++) { + if (wl_plugin_load(adapter_paths[i]) != 0) { + return 1; + } + } +#endif + (void)adapter_paths; + (void)n_adapters; + char *source = wl_read_file(filepath); if (!source) { fprintf(stderr, "error: cannot read file '%s'\n", filepath); @@ -102,9 +152,13 @@ main(int argc, char *argv[]) } int rc = wl_run_pipeline(source, num_workers, delta_mode, watch_mode, - watch_interval_ms, stdout); + watch_interval_ms, stdout); free(source); +#ifdef WL_HAVE_PLUGIN_LOADER + wl_plugin_unload_all(); +#endif + if (rc != 0) { fprintf(stderr, "error: execution failed\n"); return 1; From 9e4e58db7d94d51c7105eb0dee10a2383d525954 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Tue, 14 Apr 2026 17:19:44 +0900 Subject: [PATCH 4/4] test(#461): Add plugin loader tests with mock adapter shared libraries Add test_plugin_loader.c exercising: valid load + register + find, unload cleanup, ABI mismatch rejection, missing file error, NULL path error. Build mock_plugin_adapter.so (valid) and mock_plugin_adapter_bad_abi.so (always returns NULL) as test-only shared libraries. Tests gated behind io_plugin_dlopen=enabled. --- tests/meson.build | 45 ++++++++++ tests/mock_plugin_adapter.c | 46 ++++++++++ tests/mock_plugin_adapter_bad_abi.c | 18 ++++ tests/test_plugin_loader.c | 132 ++++++++++++++++++++++++++++ 4 files changed, 241 insertions(+) create mode 100644 tests/mock_plugin_adapter.c create mode 100644 tests/mock_plugin_adapter_bad_abi.c create mode 100644 tests/test_plugin_loader.c diff --git a/tests/meson.build b/tests/meson.build index 888269be..4bc62c50 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -2495,3 +2495,48 @@ test_io_adapter_asan_exe = executable( install: false, ) test('io_adapter_asan', test_io_adapter_asan_exe) + +# ====================================================================== +# Plugin loader tests (Issue #461) +# Only built when io_plugin_dlopen is enabled and not on Windows +# ====================================================================== +if io_plugin_dlopen_option == 'enabled' and not is_windows + + # Mock plugin shared library (valid adapter) + mock_plugin_lib = shared_library( + 'mock_plugin_adapter', + files('mock_plugin_adapter.c'), + include_directories: [wirelog_inc, wirelog_src_inc], + install: false, + ) + + # Mock plugin shared library (ABI mismatch — always returns NULL) + mock_plugin_bad_abi_lib = shared_library( + 'mock_plugin_adapter_bad_abi', + files('mock_plugin_adapter_bad_abi.c'), + include_directories: [wirelog_inc, wirelog_src_inc], + install: false, + ) + + _dl_test_dep = cc.find_library('dl', required: false) + test_plugin_loader_exe = executable( + 'test_plugin_loader', + files('test_plugin_loader.c', + '../wirelog/cli/plugin_loader.c', + '../wirelog/io/io_adapter.c', + '../wirelog/io/csv_adapter.c', + '../wirelog/io/csv_reader.c', + '../wirelog/io/io_ctx.c', + '../wirelog/intern.c'), + thread_src, + include_directories: [wirelog_inc, wirelog_src_inc], + c_args: ['-DWIRELOG_BUILDING', '-DWL_HAVE_PLUGIN_LOADER'], + dependencies: [threads_dep] + (_dl_test_dep.found() ? [_dl_test_dep] : []), + install: false, + ) + test('plugin_loader', test_plugin_loader_exe, + args: [mock_plugin_lib.full_path(), mock_plugin_bad_abi_lib.full_path()], + depends: [mock_plugin_lib, mock_plugin_bad_abi_lib], + ) + +endif diff --git a/tests/mock_plugin_adapter.c b/tests/mock_plugin_adapter.c new file mode 100644 index 00000000..f89a1534 --- /dev/null +++ b/tests/mock_plugin_adapter.c @@ -0,0 +1,46 @@ +/* + * mock_plugin_adapter.c - Mock adapter plugin for test_plugin_loader + * + * Built as a shared library (libmock_plugin_adapter.so) that exports + * the wl_io_plugin_entry symbol. Used by test_plugin_loader.c to + * exercise the plugin load + register + dispatch path. + */ + +#include "wirelog/io/io_adapter.h" + +#include +#include + +static int +mock_read(wl_io_ctx_t *ctx, int64_t **out_data, uint32_t *out_nrows, + void *user_data) +{ + (void)ctx; + (void)user_data; + *out_data = NULL; + *out_nrows = 0; + return 0; +} + +static const wl_io_adapter_t mock_adapter = { + .abi_version = WL_IO_ABI_VERSION, + .scheme = "mock_plugin", + .description = "mock plugin adapter for testing", + .read = mock_read, + .validate = NULL, + .user_data = NULL, +}; + +static const wl_io_adapter_t *const adapter_list[] = {&mock_adapter}; + +WL_IO_PLUGIN_EXPORT +const wl_io_adapter_t *const * +wl_io_plugin_entry(uint32_t *n_out, uint32_t abi_ver) +{ + if (abi_ver != WL_IO_ABI_VERSION) { + *n_out = 0; + return NULL; + } + *n_out = 1; + return adapter_list; +} diff --git a/tests/mock_plugin_adapter_bad_abi.c b/tests/mock_plugin_adapter_bad_abi.c new file mode 100644 index 00000000..ca46f6ea --- /dev/null +++ b/tests/mock_plugin_adapter_bad_abi.c @@ -0,0 +1,18 @@ +/* + * mock_plugin_adapter_bad_abi.c - ABI mismatch mock for test_plugin_loader + * + * Always returns NULL to simulate an ABI version mismatch. + */ + +#include "wirelog/io/io_adapter.h" + +#include + +WL_IO_PLUGIN_EXPORT +const wl_io_adapter_t *const * +wl_io_plugin_entry(uint32_t *n_out, uint32_t abi_ver) +{ + (void)abi_ver; + *n_out = 0; + return NULL; +} diff --git a/tests/test_plugin_loader.c b/tests/test_plugin_loader.c new file mode 100644 index 00000000..aa301846 --- /dev/null +++ b/tests/test_plugin_loader.c @@ -0,0 +1,132 @@ +/* + * test_plugin_loader.c - Plugin loader integration tests (Issue #461) + * + * Tests: load + register + find + unload, ABI mismatch error path, + * missing symbol error path, NULL path error path. + * + * Requires -Dio_plugin_dlopen=enabled to build. + */ + +#include "wirelog/io/io_adapter.h" + +#include +#include +#include + +/* We test the plugin loader by calling it directly */ +extern int wl_plugin_load(const char *path); +extern void wl_plugin_unload_all(void); + +#define TEST(name) do { printf(" [TEST] %-50s ", name); } while (0) +#define PASS() do { printf("PASS\n"); passed++; } while (0) +#define FAIL(msg) do { printf("FAIL: %s\n", msg); failed++; } while (0) + +static int passed = 0, failed = 0; + +static void +test_load_valid_plugin(const char *mock_path) +{ + TEST("load valid plugin"); + + int rc = wl_plugin_load(mock_path); + if (rc != 0) { + FAIL("wl_plugin_load returned non-zero"); + return; + } + + const wl_io_adapter_t *found = wl_io_find_adapter("mock_plugin"); + if (!found) { + FAIL("adapter 'mock_plugin' not found after load"); + return; + } + + if (strcmp(found->scheme, "mock_plugin") != 0) { + FAIL("scheme mismatch"); + return; + } + + PASS(); +} + +static void +test_unload_cleans_registry(void) +{ + TEST("unload cleans registry"); + + wl_plugin_unload_all(); + + const wl_io_adapter_t *found = wl_io_find_adapter("mock_plugin"); + if (found) { + FAIL("adapter 'mock_plugin' still found after unload"); + return; + } + + PASS(); +} + +static void +test_abi_mismatch(const char *bad_abi_path) +{ + TEST("ABI mismatch returns error"); + + int rc = wl_plugin_load(bad_abi_path); + if (rc == 0) { + FAIL("expected failure for ABI mismatch plugin"); + wl_plugin_unload_all(); + return; + } + + PASS(); +} + +static void +test_missing_file(void) +{ + TEST("missing file returns error"); + + int rc = wl_plugin_load("/nonexistent/path/libfoo.so"); + if (rc == 0) { + FAIL("expected failure for missing file"); + return; + } + + PASS(); +} + +static void +test_null_path(void) +{ + TEST("NULL path returns error"); + + int rc = wl_plugin_load(NULL); + if (rc == 0) { + FAIL("expected failure for NULL path"); + return; + } + + PASS(); +} + +int +main(int argc, char *argv[]) +{ + if (argc < 3) { + fprintf(stderr, + "usage: %s \n", argv[0]); + return 1; + } + + const char *mock_path = argv[1]; + const char *bad_abi_path = argv[2]; + + printf("test_plugin_loader\n"); + + test_null_path(); + test_missing_file(); + test_load_valid_plugin(mock_path); + test_unload_cleans_registry(); + test_abi_mismatch(bad_abi_path); + + printf("\n Results: %d passed, %d failed\n", passed, failed); + return failed > 0 ? 1 : 0; +}