#![allow(clippy::expect_used)]
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use polyplug::error::LoaderError;
use polyplug::error::RuntimeError;
use polyplug::loader::BundleLoader;
use polyplug::loader::manifest::ManifestData;
use polyplug::runtime::Runtime;
use polyplug::runtime::RuntimeBuilder;
use polyplug_abi::AbiError;
use polyplug_abi::AbiErrorCode;
use polyplug_abi::GuestContractHandle;
use polyplug_abi::GuestContractInstance;
use polyplug_abi::GuestContractInterface;
use polyplug_abi::runtime::Compatibility;
use polyplug_abi::runtime::RuntimeConfig;
use polyplug_abi::types::LogLevel;
use polyplug_lua::LuaConfig;
use polyplug_lua::LuaLoader;
use polyplug_utils::GuestContractId;
fn make_runtime() -> Arc<Runtime> {
RuntimeBuilder::new()
.loader(LuaLoader::new(LuaConfig::default()))
.build()
.expect("runtime build must succeed")
}
fn write_temp_bundle(name: &str, content: &[u8]) -> (tempfile::TempDir, PathBuf) {
let dir: tempfile::TempDir = tempfile::tempdir().expect("tempdir");
let path: PathBuf = dir.path().join("bundle.lua");
std::fs::write(&path, content).expect("write bundle.lua");
let bundle_id: u64 = polyplug_utils::bundle_id(name);
let manifest: String = format!(
r#"id = {}
name = "{}"
loader = "lua"
file = "bundle.lua"
"#,
bundle_id, name
);
std::fs::write(dir.path().join("manifest.toml"), &manifest).expect("write manifest.toml");
(dir, path)
}
fn valid_plugin_script() -> &'static [u8] {
br#"
local ffi = require("ffi")
local function impl_noop(_instance, _args_ptr, _out_ptr)
end
function polyplug_init(_registrar_ptr, _ctx_ptr)
return {
["test.loader"] = {
contract_version = 1,
plugin_name = "test-loader-unit",
factory = function(_host) return {} end,
functions = { [0] = impl_noop },
},
}, { code = 0 }
end
"#
}
fn two_function_plugin_script() -> &'static [u8] {
br#"
local ffi = require("ffi")
local function impl_a(_instance, _args_ptr, _out_ptr) end
local function impl_b(_instance, _args_ptr, _out_ptr) end
function polyplug_init(_registrar_ptr, _ctx_ptr)
return {
["test.two"] = {
contract_version = 1,
plugin_name = "test-two-unit",
factory = function(_host) return {} end,
functions = { [0] = impl_a, [1] = impl_b },
},
}, { code = 0 }
end
"#
}
fn two_contract_plugin_script() -> &'static [u8] {
br#"
local ffi = require("ffi")
local function impl_first(_instance, _args_ptr, _out_ptr) end
local function impl_second_a(_instance, _args_ptr, _out_ptr) end
local function impl_second_b(_instance, _args_ptr, _out_ptr) end
function polyplug_init(_registrar_ptr, _ctx_ptr)
return {
["test.first"] = {
contract_version = 1,
plugin_name = "test-multi-first",
factory = function(_host) return {} end,
functions = { [0] = impl_first },
},
["test.second"] = {
contract_version = 1,
plugin_name = "test-multi-second",
factory = function(_host) return {} end,
functions = { [0] = impl_second_a, [1] = impl_second_b },
},
}, { code = 0 }
end
"#
}
fn make_manifest(path: &Path, name: &str) -> ManifestData {
let bundle_id: u64 = polyplug_utils::bundle_id(name);
ManifestData {
id: bundle_id,
name: name.to_owned(),
loader: "lua".to_owned(),
file: path
.file_name()
.expect("bundle path must have a file name")
.to_string_lossy()
.into_owned(),
path: path
.parent()
.expect("bundle path must have a parent directory")
.to_path_buf(),
version: String::new(),
provides: Vec::new(),
function_count: HashMap::new(),
dependencies: Vec::new(),
needs_reinit_on_dep_reload: false,
bundle_dependencies: Vec::new(),
}
}
fn load_script(path: &Path, name: &str) -> Result<(), LoaderError> {
let loader: LuaLoader = LuaLoader::new(LuaConfig::default());
let runtime: Arc<Runtime> = make_runtime();
let manifest: ManifestData = make_manifest(path, name);
loader.load(
&manifest,
&polyplug::loader::BundleSource::Path(manifest.path.clone()),
&runtime,
)
}
#[test]
fn lua_loader_loader_name_is_lua() {
let loader: LuaLoader = LuaLoader::new(LuaConfig::default());
assert_eq!(loader.loader_name(), "lua");
}
#[test]
fn lua_state_initializes_on_first_load() {
let (_dir, path) = write_temp_bundle("lua_loader_init_test", valid_plugin_script());
let result: Result<(), LoaderError> = load_script(&path, "lua_loader_init_test");
assert!(
result.is_ok(),
"Lua VM must initialize and bundle must load: {:?}",
result.err()
);
}
#[test]
fn lua_state_init_is_idempotent() {
let (_dir, path) = write_temp_bundle("lua_loader_idempotent", valid_plugin_script());
load_script(&path, "lua_loader_idempotent").expect("first load must succeed");
let result: Result<(), LoaderError> = load_script(&path, "lua_loader_idempotent");
assert!(
result.is_ok(),
"second load must succeed (idempotent VM init): {:?}",
result.err()
);
}
#[test]
fn load_valid_bundle_succeeds() {
let (_dir, path) = write_temp_bundle("lua_loader_valid", valid_plugin_script());
let result: Result<(), LoaderError> = load_script(&path, "lua_loader_valid");
assert!(result.is_ok(), "valid bundle must load: {:?}", result.err());
}
#[test]
fn load_syntax_error_returns_script_load_failed() {
let (_dir, path) = write_temp_bundle(
"lua_loader_syntax_error",
b"function polyplug_init( -- SYNTAX ERROR: unclosed paren\n",
);
let result: Result<(), LoaderError> = load_script(&path, "lua_loader_syntax_error");
assert!(result.is_err(), "syntax error must produce an Err");
let err: LoaderError = result.expect_err("expected Err for syntax error");
assert!(
matches!(err, LoaderError::InitFailed { .. }),
"expected InitFailed for syntax error, got: {:?}",
err
);
}
#[test]
fn load_runtime_error_in_init_returns_init_raised_error() {
let (_dir, path) = write_temp_bundle(
"lua_loader_runtime_err",
b"function polyplug_init(_reg, _ctx)\n error('deliberate runtime error')\nend\n",
);
let result: Result<(), LoaderError> = load_script(&path, "lua_loader_runtime_err");
assert!(result.is_err(), "runtime error in init must produce Err");
let err: LoaderError = result.expect_err("expected Err for runtime error in init");
assert!(
matches!(err, LoaderError::InitFailed { .. }),
"expected InitFailed for runtime error in init, got: {:?}",
err
);
}
#[test]
fn load_missing_polyplug_init_returns_typed_error() {
let (_dir, path) =
write_temp_bundle("lua_loader_no_init", b"local x = 1 -- no polyplug_init\n");
let result: Result<(), LoaderError> = load_script(&path, "lua_loader_no_init");
assert!(result.is_err(), "missing init must produce Err");
let err: LoaderError = result.expect_err("expected Err for missing polyplug_init");
assert!(
matches!(err, LoaderError::InitFailed { .. }),
"expected InitFailed for missing polyplug_init, got: {:?}",
err
);
}
#[test]
fn load_nonexistent_path_returns_script_load_failed() {
let dir: tempfile::TempDir = tempfile::tempdir().expect("tempdir");
let path: PathBuf = dir.path().join("this_file_does_not_exist_42.lua");
let result: Result<(), LoaderError> = load_script(&path, "nonexistent");
assert!(result.is_err(), "missing file must produce Err");
let err: LoaderError = result.expect_err("expected Err for nonexistent file");
assert!(
matches!(err, LoaderError::InitFailed { .. }),
"expected InitFailed for missing file, got: {:?}",
err
);
}
#[test]
fn vtable_is_registered_after_load() {
let (_dir, path) = write_temp_bundle("lua_loader_vtable", valid_plugin_script());
let loader: LuaLoader = LuaLoader::new(LuaConfig::default());
let runtime: Arc<Runtime> = make_runtime();
let manifest: ManifestData = make_manifest(&path, "lua_loader_vtable");
loader
.load(
&manifest,
&polyplug::loader::BundleSource::Path(manifest.path.clone()),
&runtime,
)
.expect("valid bundle must load");
let contract_id: u64 = polyplug_utils::guest_contract_id("test.loader", 1);
let handle: Result<GuestContractHandle, polyplug::error::RegistryError> = runtime
.registry()
.find(GuestContractId::from_u64(contract_id), 0);
assert!(
handle.is_ok(),
"registry must contain test.loader@1 after load"
);
let handle: GuestContractHandle = handle.expect("handle must be Ok");
let vtable_ptr: Result<*const GuestContractInterface, polyplug::error::RegistryError> =
runtime.registry().resolve_guest_contract(handle);
assert!(vtable_ptr.is_ok(), "handle must resolve to a vtable");
let vtable: &GuestContractInterface = unsafe { &*vtable_ptr.expect("vtable must resolve") };
assert_function_count(vtable, 1);
}
#[test]
fn registrations_attributed_to_real_bundle_id() {
let (_dir, path) = write_temp_bundle("lua_loader_attribution", valid_plugin_script());
let loader: LuaLoader = LuaLoader::new(LuaConfig::default());
let runtime: Arc<Runtime> = make_runtime();
let manifest: ManifestData = make_manifest(&path, "lua_loader_attribution");
let bundle_id: u64 = manifest.id;
loader
.load(
&manifest,
&polyplug::loader::BundleSource::Path(manifest.path.clone()),
&runtime,
)
.expect("valid bundle must load");
let contract_id: u64 = polyplug_utils::guest_contract_id("test.loader", 1);
let by_real: Result<GuestContractHandle, polyplug::error::RegistryError> =
runtime.find_guest_contract_by_bundle(bundle_id, contract_id, 0);
assert!(
by_real.is_ok(),
"contract must be attributed to the real bundle id {bundle_id}, not bundle 0"
);
let by_zero: Result<GuestContractHandle, polyplug::error::RegistryError> =
runtime.find_guest_contract_by_bundle(0, contract_id, 0);
assert!(
by_zero.is_err(),
"contract must not be attributed to bundle 0"
);
runtime
.registry()
.invalidate_bundle(polyplug_utils::BundleId::from_u64(bundle_id))
.expect("invalidate by real bundle id must succeed");
let after: Result<GuestContractHandle, polyplug::error::RegistryError> = runtime
.registry()
.find(GuestContractId::from_u64(contract_id), 0);
assert!(
after.is_err(),
"contract must be gone after invalidating the real bundle id"
);
}
fn assert_function_count(vtable: &GuestContractInterface, expected: u32) {
assert_eq!(
vtable.dispatch_type,
polyplug_abi::DispatchType::VirtualMachine,
"Lua loader must use VM dispatch"
);
for fn_id in 0..expected {
let mut result: AbiError = AbiError::ok();
unsafe {
(vtable.dispatch.vm.call)(
vtable.dispatch.vm.loader_data,
GuestContractInstance::null(),
fn_id,
core::ptr::null::<()>(),
core::ptr::null_mut::<()>(),
core::ptr::null_mut(),
&mut result as *mut AbiError,
);
}
assert_eq!(
result.code,
AbiErrorCode::Ok as u32,
"fn_id {fn_id} must dispatch to Ok"
);
}
let mut missing: AbiError = AbiError::ok();
unsafe {
(vtable.dispatch.vm.call)(
vtable.dispatch.vm.loader_data,
GuestContractInstance::null(),
expected,
core::ptr::null::<()>(),
core::ptr::null_mut::<()>(),
core::ptr::null_mut(),
&mut missing as *mut AbiError,
);
}
assert_eq!(
missing.code,
AbiErrorCode::FunctionNotAvailable as u32,
"fn_id {expected} must report FunctionNotAvailable"
);
}
#[test]
fn vtable_function_count_matches_script() {
let (_dir, path) = write_temp_bundle("lua_loader_two_fn", two_function_plugin_script());
let loader: LuaLoader = LuaLoader::new(LuaConfig::default());
let runtime: Arc<Runtime> = make_runtime();
let manifest: ManifestData = make_manifest(&path, "lua_loader_two_fn");
loader
.load(
&manifest,
&polyplug::loader::BundleSource::Path(manifest.path.clone()),
&runtime,
)
.expect("two-function bundle must load");
let contract_id: u64 = polyplug_utils::guest_contract_id("test.two", 1);
let handle: Result<GuestContractHandle, polyplug::error::RegistryError> = runtime
.registry()
.find(GuestContractId::from_u64(contract_id), 0);
let handle: GuestContractHandle = handle.expect("test.two@1 must be registered");
let vtable_ptr: Result<*const GuestContractInterface, polyplug::error::RegistryError> =
runtime.registry().resolve_guest_contract(handle);
let vtable: &GuestContractInterface = unsafe { &*vtable_ptr.expect("vtable must resolve") };
assert_function_count(vtable, 2);
}
#[test]
fn vtable_contract_id_matches_computed_hash() {
let (_dir, path) = write_temp_bundle("lua_loader_cid", valid_plugin_script());
let loader: LuaLoader = LuaLoader::new(LuaConfig::default());
let runtime: Arc<Runtime> = make_runtime();
let manifest: ManifestData = make_manifest(&path, "lua_loader_cid");
loader
.load(
&manifest,
&polyplug::loader::BundleSource::Path(manifest.path.clone()),
&runtime,
)
.expect("valid bundle must load");
let expected_cid: u64 = polyplug_utils::guest_contract_id("test.loader", 1);
let handle: Result<GuestContractHandle, polyplug::error::RegistryError> = runtime
.registry()
.find(GuestContractId::from_u64(expected_cid), 0);
let handle: GuestContractHandle = handle.expect("test.loader@1 must be registered");
let vtable_ptr: Result<*const GuestContractInterface, polyplug::error::RegistryError> =
runtime.registry().resolve_guest_contract(handle);
let vtable: &GuestContractInterface = unsafe { &*vtable_ptr.expect("vtable must resolve") };
assert_eq!(
vtable.contract_id,
GuestContractId::from_u64(expected_cid),
"contract_id in vtable must match FNV-1a hash of 'test.loader@1'"
);
}
#[test]
fn sequential_loads_both_succeed() {
let (_dir1, path1) = write_temp_bundle("lua_loader_seq1", valid_plugin_script());
let (_dir2, path2) = write_temp_bundle("lua_loader_seq2", two_function_plugin_script());
let loader: LuaLoader = LuaLoader::new(LuaConfig::default());
let runtime: Arc<Runtime> = make_runtime();
let manifest1: ManifestData = make_manifest(&path1, "lua_loader_seq1");
let manifest2: ManifestData = make_manifest(&path2, "lua_loader_seq2");
loader
.load(
&manifest1,
&polyplug::loader::BundleSource::Path(manifest1.path.clone()),
&runtime,
)
.expect("first sequential load must succeed");
loader
.load(
&manifest2,
&polyplug::loader::BundleSource::Path(manifest2.path.clone()),
&runtime,
)
.expect("second sequential load must succeed");
let cid1: u64 = polyplug_utils::guest_contract_id("test.loader", 1);
let cid2: u64 = polyplug_utils::guest_contract_id("test.two", 1);
let handle1: Result<GuestContractHandle, polyplug::error::RegistryError> =
runtime.registry().find(GuestContractId::from_u64(cid1), 0);
assert!(handle1.is_ok(), "test.loader must be registered");
let handle2: Result<GuestContractHandle, polyplug::error::RegistryError> =
runtime.registry().find(GuestContractId::from_u64(cid2), 0);
assert!(handle2.is_ok(), "test.two must be registered");
}
#[test]
fn multi_contract_bundle_registers_all_contracts() {
let (_dir, path) = write_temp_bundle("lua_loader_multi", two_contract_plugin_script());
let loader: LuaLoader = LuaLoader::new(LuaConfig::default());
let runtime: Arc<Runtime> = make_runtime();
let manifest: ManifestData = make_manifest(&path, "lua_loader_multi");
loader
.load(
&manifest,
&polyplug::loader::BundleSource::Path(manifest.path.clone()),
&runtime,
)
.expect("multi-contract bundle must load");
let first_cid: u64 = polyplug_utils::guest_contract_id("test.first", 1);
let second_cid: u64 = polyplug_utils::guest_contract_id("test.second", 1);
let first_handle: GuestContractHandle = runtime
.registry()
.find(GuestContractId::from_u64(first_cid), 0)
.expect("test.first@1 must be registered");
let second_handle: GuestContractHandle = runtime
.registry()
.find(GuestContractId::from_u64(second_cid), 0)
.expect("test.second@1 must be registered");
let first_vtable_ptr: *const GuestContractInterface = runtime
.registry()
.resolve_guest_contract(first_handle)
.expect("test.first handle must resolve");
let second_vtable_ptr: *const GuestContractInterface = runtime
.registry()
.resolve_guest_contract(second_handle)
.expect("test.second handle must resolve");
let first_vtable: &GuestContractInterface = unsafe { &*first_vtable_ptr };
let second_vtable: &GuestContractInterface = unsafe { &*second_vtable_ptr };
assert_function_count(first_vtable, 1);
assert_function_count(second_vtable, 2);
}
#[test]
fn concurrent_loaders_do_not_race() {
let (_dir, path) = write_temp_bundle("lua_loader_thread_safety", valid_plugin_script());
let path_arc: std::sync::Arc<PathBuf> = std::sync::Arc::new(path);
let handles: Vec<std::thread::JoinHandle<Result<(), LoaderError>>> = (0_u32..4_u32)
.map(|_| {
let p: std::sync::Arc<PathBuf> = std::sync::Arc::clone(&path_arc);
std::thread::spawn(move || {
let loader: LuaLoader = LuaLoader::new(LuaConfig::default());
let runtime: Arc<Runtime> = RuntimeBuilder::new()
.loader(LuaLoader::new(LuaConfig::default()))
.build()
.expect("runtime build must succeed");
let manifest: ManifestData = ManifestData {
id: polyplug_utils::bundle_id("lua_loader_thread_safety"),
name: "lua_loader_thread_safety".to_owned(),
loader: "lua".to_owned(),
file: p
.file_name()
.expect("bundle path must have a file name")
.to_string_lossy()
.into_owned(),
path: p
.parent()
.expect("bundle path must have a parent directory")
.to_path_buf(),
version: String::new(),
provides: Vec::new(),
function_count: HashMap::new(),
dependencies: Vec::new(),
needs_reinit_on_dep_reload: false,
bundle_dependencies: Vec::new(),
};
loader.load(
&manifest,
&polyplug::loader::BundleSource::Path(manifest.path.clone()),
&runtime,
)
})
})
.collect::<Vec<std::thread::JoinHandle<Result<(), LoaderError>>>>();
for handle in handles {
let result: Result<(), LoaderError> = handle
.join()
.expect("thread must not panic during concurrent load");
let _ = result;
}
}
#[test]
fn vtable_function_dispatch_returns_abi_ok() {
let (_dir, path) = write_temp_bundle("lua_loader_dispatch", valid_plugin_script());
let loader: LuaLoader = LuaLoader::new(LuaConfig::default());
let runtime: Arc<Runtime> = make_runtime();
let manifest: ManifestData = make_manifest(&path, "lua_loader_dispatch");
loader
.load(
&manifest,
&polyplug::loader::BundleSource::Path(manifest.path.clone()),
&runtime,
)
.expect("valid bundle must load");
let contract_id: u64 = polyplug_utils::guest_contract_id("test.loader", 1);
let handle: GuestContractHandle = runtime
.registry()
.find(GuestContractId::from_u64(contract_id), 0)
.expect("test.loader@1 must be registered");
let vtable_ptr: *const GuestContractInterface = runtime
.registry()
.resolve_guest_contract(handle)
.expect("handle must resolve to vtable");
let vtable: &GuestContractInterface = unsafe { &*vtable_ptr };
assert_eq!(
vtable.dispatch_type,
polyplug_abi::DispatchType::VirtualMachine,
"Lua loader must use VM dispatch"
);
let mut result: AbiError = AbiError::ok();
unsafe {
(vtable.dispatch.vm.call)(
vtable.dispatch.vm.loader_data,
GuestContractInstance::null(),
0, core::ptr::null::<()>(),
core::ptr::null_mut::<()>(),
core::ptr::null_mut(),
&mut result as *mut AbiError,
);
}
assert_eq!(
result.code,
AbiErrorCode::Ok as u32,
"noop function must return Ok, got code={}",
result.code
);
}
fn make_runtime_with_hot_reload(enabled: bool) -> Arc<Runtime> {
RuntimeBuilder::new()
.config(RuntimeConfig {
compatibility: Compatibility::Strict,
hot_reload_enabled: enabled,
on_reload: None,
on_reload_user_data: core::ptr::null_mut(),
..Default::default()
})
.loader(LuaLoader::new(LuaConfig::default()))
.build()
.expect("runtime build must succeed")
}
#[test]
fn lua_reload_disabled_returns_error() {
let (_dir, path) = write_temp_bundle("lua_reload_disabled", valid_plugin_script());
let loader: LuaLoader = LuaLoader::new(LuaConfig::default());
assert!(
loader.supports_hot_reload(),
"the lua loader supports hot-reload; only the config flag must gate it here"
);
let runtime: Arc<Runtime> = make_runtime_with_hot_reload(false);
let result: Result<(), RuntimeError> = runtime.reload_bundle(path.as_path());
assert!(
matches!(result, Err(RuntimeError::HotReloadDisabled)),
"reload_bundle with hot_reload_enabled=false must return HotReloadDisabled, got: {:?}",
result
);
}
fn lua_fixture_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("crates/ parent must exist")
.parent()
.expect("workspace root must exist")
.join("tests")
.join("fixtures")
.join("test_plugin_lua")
}
fn fixture_manifest(path: &Path) -> ManifestData {
let name: &str = "test_plugin_lua";
ManifestData {
id: polyplug_utils::bundle_id(name),
name: name.to_owned(),
loader: "lua".to_owned(),
file: "test_plugin.lua".to_owned(),
path: path.to_path_buf(),
version: "1.0.0".to_owned(),
provides: Vec::new(),
function_count: HashMap::new(),
dependencies: Vec::new(),
needs_reinit_on_dep_reload: false,
bundle_dependencies: Vec::new(),
}
}
fn dispatch_add(runtime: &Runtime, a: u32, b: u32) -> u32 {
let contract_id: u64 = polyplug_utils::guest_contract_id("test.add", 1);
let handle: GuestContractHandle = runtime
.registry()
.find(GuestContractId::from_u64(contract_id), 0)
.expect("test.add@1 must be registered");
let vtable_ptr: *const GuestContractInterface = runtime
.registry()
.resolve_guest_contract(handle)
.expect("handle must resolve to a vtable");
let vtable: &GuestContractInterface = unsafe { &*vtable_ptr };
let args: [u32; 2] = [a, b];
let mut out: u32 = 0;
let mut result: AbiError = AbiError::ok();
unsafe {
(vtable.dispatch.vm.call)(
vtable.dispatch.vm.loader_data,
GuestContractInstance::null(),
0,
args.as_ptr() as *const (),
&mut out as *mut u32 as *mut (),
core::ptr::null_mut(),
&mut result as *mut AbiError,
);
}
assert_eq!(
result.code,
AbiErrorCode::Ok as u32,
"add dispatch must return Ok, got code={}",
result.code
);
out
}
#[test]
fn code_source_loads_and_dispatches_like_path() {
let fixture_dir: PathBuf = lua_fixture_dir();
let entry: PathBuf = fixture_dir.join("test_plugin.lua");
let source_text: String =
std::fs::read_to_string(&entry).expect("fixture test_plugin.lua must be readable");
let path_runtime: Arc<Runtime> = make_runtime();
path_runtime
.load_bundle_from_source(
fixture_manifest(&fixture_dir),
polyplug::loader::BundleSource::Path(fixture_dir.clone()),
)
.expect("path-sourced fixture load must succeed");
let path_result: u32 = dispatch_add(&path_runtime, 7, 35);
let code_runtime: Arc<Runtime> = make_runtime();
code_runtime
.load_bundle_from_source(
fixture_manifest(&fixture_dir),
polyplug::loader::BundleSource::Code(source_text),
)
.expect("code-sourced fixture load must succeed");
let code_result: u32 = dispatch_add(&code_runtime, 7, 35);
assert_eq!(
code_result, 42,
"code-sourced add(7, 35) must compute 42, got {code_result}"
);
assert_eq!(
code_result, path_result,
"code-sourced dispatch must match path-sourced dispatch"
);
}
#[test]
fn bytes_source_with_valid_utf8_loads_and_dispatches() {
let fixture_dir: PathBuf = lua_fixture_dir();
let entry: PathBuf = fixture_dir.join("test_plugin.lua");
let source_bytes: Vec<u8> =
std::fs::read(&entry).expect("fixture test_plugin.lua must be readable");
let runtime: Arc<Runtime> = make_runtime();
runtime
.load_bundle_from_source(
fixture_manifest(&fixture_dir),
polyplug::loader::BundleSource::Bytes(source_bytes),
)
.expect("bytes-sourced fixture load must succeed");
let result: u32 = dispatch_add(&runtime, 20, 22);
assert_eq!(result, 42, "bytes-sourced add(20, 22) must compute 42");
}
#[test]
fn bytes_source_with_invalid_utf8_returns_structured_error() {
let fixture_dir: PathBuf = lua_fixture_dir();
let invalid: Vec<u8> = vec![0x66, 0x6e, 0xFF, 0xFE, 0x00];
let runtime: Arc<Runtime> = make_runtime();
let result: Result<(), RuntimeError> = runtime.load_bundle_from_source(
fixture_manifest(&fixture_dir),
polyplug::loader::BundleSource::Bytes(invalid),
);
assert!(result.is_err(), "invalid UTF-8 bytes must produce Err");
let err: RuntimeError = result.expect_err("expected Err for invalid UTF-8 bytes");
match err {
RuntimeError::Loader(LoaderError::InvalidSourceEncoding {
loader,
source_kind,
bundle,
}) => {
assert_eq!(loader, "lua", "loader must be the Lua runtime name");
assert_eq!(source_kind, "bytes", "source_kind must be bytes");
assert_eq!(
bundle, "test_plugin_lua",
"bundle must be the manifest bundle name"
);
}
other => panic!("expected LoaderError::InvalidSourceEncoding, got: {other:?}"),
}
}
#[test]
fn lua_reload_reinitializes_contracts() {
let (dir, _path) = write_temp_bundle("lua_reload_reinit", valid_plugin_script());
let runtime: Arc<Runtime> = make_runtime_with_hot_reload(true);
let bundle_dir: PathBuf = dir.path().to_path_buf();
runtime
.load_bundle(&bundle_dir)
.expect("initial bundle load must succeed");
let contract_id: u64 = polyplug_utils::guest_contract_id("test.loader", 1);
runtime
.find_guest_contract(contract_id, 0)
.expect("contract must resolve after initial load");
runtime
.reload_bundle(&bundle_dir)
.expect("reload must succeed when hot-reload is enabled");
runtime
.find_guest_contract(contract_id, 0)
.expect("contract must remain resolvable after reload");
}
fn failing_plugin_script() -> &'static [u8] {
br#"
local function impl_fail(_instance, _args_ptr, _out_ptr)
error("boom from lua guest")
end
function polyplug_init(_registrar_ptr, _ctx_ptr)
return {
["test.loader"] = {
contract_version = 1,
plugin_name = "test-loader-fail",
factory = function(_host) return {} end,
functions = { [0] = impl_fail },
},
}, { code = 0 }
end
"#
}
#[test]
fn dispatch_failure_is_logged_through_host_logger() {
let (_dir, path) = write_temp_bundle("lua_loader_dispatch_log", failing_plugin_script());
let records: Arc<std::sync::Mutex<Vec<(LogLevel, String, String)>>> =
Arc::new(std::sync::Mutex::new(Vec::new()));
let records_clone: Arc<std::sync::Mutex<Vec<(LogLevel, String, String)>>> =
Arc::clone(&records);
let runtime: Arc<Runtime> = RuntimeBuilder::new()
.logger(move |level: LogLevel, scope: &str, msg: &str| {
records_clone.lock().expect("records lock").push((
level,
scope.to_owned(),
msg.to_owned(),
));
})
.build()
.expect("runtime build must succeed");
let loader: LuaLoader = LuaLoader::new(LuaConfig::default());
let manifest: ManifestData = make_manifest(&path, "lua_loader_dispatch_log");
loader
.load(
&manifest,
&polyplug::loader::BundleSource::Path(manifest.path.clone()),
&runtime,
)
.expect("failing-function bundle must still load");
let contract_id: u64 = polyplug_utils::guest_contract_id("test.loader", 1);
let handle: GuestContractHandle = runtime
.registry()
.find(GuestContractId::from_u64(contract_id), 0)
.expect("test.loader@1 must be registered");
let vtable_ptr: *const GuestContractInterface = runtime
.registry()
.resolve_guest_contract(handle)
.expect("handle must resolve to vtable");
let vtable: &GuestContractInterface = unsafe { &*vtable_ptr };
let mut result: AbiError = AbiError::ok();
unsafe {
(vtable.dispatch.vm.call)(
vtable.dispatch.vm.loader_data,
GuestContractInstance::null(),
0,
core::ptr::null::<()>(),
core::ptr::null_mut::<()>(),
core::ptr::null_mut(),
&mut result as *mut AbiError,
);
}
assert_eq!(
result.code,
AbiErrorCode::Generic as u32,
"failing guest function must return Generic, got code={}",
result.code
);
let captured: Vec<(LogLevel, String, String)> = records.lock().expect("records lock").clone();
assert!(
captured.iter().any(|(level, scope, msg)| {
*level == LogLevel::Error
&& scope == "loader.lua"
&& msg.starts_with("Lua function call failed")
&& msg.contains("boom from lua guest")
}),
"expected an (Error, \"loader.lua\", \"Lua function call failed: ...boom...\") record, got: {captured:?}"
);
}
fn logging_plugin_script() -> &'static [u8] {
br#"
local pg = require("polyplug_guest")
local function new_logger(host)
local self = {}
function self:log_fn(_args_ptr, _out_ptr)
pg.log(host, pg.LogLevel.Info, "guest.test-log", "hello from lua guest")
pg.log(host, 99, "guest.test-log", "out of range level")
end
return self
end
function polyplug_init(_host_ptr, _ctx_ptr)
return {
["test.loader"] = {
contract_version = 1,
plugin_name = "test-loader-guest-log",
factory = new_logger,
functions = { [0] = function(instance, a, o) instance:log_fn(a, o) end },
},
}, { code = 0 }
end
"#
}
#[test]
fn guest_log_bridge_delivers_records_and_clamps_level() {
let (_dir, path) = write_temp_bundle("lua_loader_guest_log", logging_plugin_script());
let records: Arc<std::sync::Mutex<Vec<(LogLevel, String, String)>>> =
Arc::new(std::sync::Mutex::new(Vec::new()));
let records_clone: Arc<std::sync::Mutex<Vec<(LogLevel, String, String)>>> =
Arc::clone(&records);
let runtime: Arc<Runtime> = RuntimeBuilder::new()
.logger(move |level: LogLevel, scope: &str, msg: &str| {
records_clone.lock().expect("records lock").push((
level,
scope.to_owned(),
msg.to_owned(),
));
})
.build()
.expect("runtime build must succeed");
let loader: LuaLoader = LuaLoader::new(LuaConfig::default());
let manifest: ManifestData = make_manifest(&path, "lua_loader_guest_log");
loader
.load(
&manifest,
&polyplug::loader::BundleSource::Path(manifest.path.clone()),
&runtime,
)
.expect("logging bundle must load");
let contract_id: u64 = polyplug_utils::guest_contract_id("test.loader", 1);
let handle: GuestContractHandle = runtime
.registry()
.find(GuestContractId::from_u64(contract_id), 0)
.expect("test.loader@1 must be registered");
let vtable_ptr: *const GuestContractInterface = runtime
.registry()
.resolve_guest_contract(handle)
.expect("handle must resolve to vtable");
let vtable: &GuestContractInterface = unsafe { &*vtable_ptr };
let mut result: AbiError = AbiError::ok();
unsafe {
(vtable.dispatch.vm.call)(
vtable.dispatch.vm.loader_data,
GuestContractInstance::null(),
0,
core::ptr::null::<()>(),
core::ptr::null_mut::<()>(),
core::ptr::null_mut(),
&mut result as *mut AbiError,
);
}
assert_eq!(
result.code,
AbiErrorCode::Ok as u32,
"logging guest function must dispatch Ok, got code={}",
result.code
);
let captured: Vec<(LogLevel, String, String)> = records.lock().expect("records lock").clone();
assert!(
captured.contains(&(
LogLevel::Info,
String::from("guest.test-log"),
String::from("hello from lua guest"),
)),
"expected verbatim (Info, \"guest.test-log\", \"hello from lua guest\") record, got: {captured:?}"
);
assert!(
captured.contains(&(
LogLevel::Error,
String::from("guest.test-log"),
String::from("out of range level"),
)),
"expected out-of-range level 99 to clamp to Error, got: {captured:?}"
);
}
#[test]
fn load_init_returning_error_code_fails_load() {
let (_dir, path) = write_temp_bundle(
"lua_loader_init_err_code",
br#"
local function impl_noop(_instance, _args_ptr, _out_ptr) end
function polyplug_init(_reg, _ctx)
return {
["test.initerr"] = {
contract_version = 1,
plugin_name = "test-init-err",
factory = function(_host) return {} end,
functions = { [0] = impl_noop },
},
}, { code = 1 } -- AbiErrorCode.Generic
end
"#,
);
let result: Result<(), LoaderError> = load_script(&path, "lua_loader_init_err_code");
assert!(result.is_err(), "non-zero init code must fail the load");
let err: LoaderError = result.expect_err("expected Err for non-zero init code");
match &err {
LoaderError::InitFailed { error, .. } => {
assert!(
error.contains("returned error code 1"),
"error must carry the returned code, got: {error}"
);
}
other => panic!("expected InitFailed, got: {other:?}"),
}
}
#[test]
fn load_init_returning_ok_code_succeeds() {
let (_dir, path) = write_temp_bundle(
"lua_loader_init_ok_code",
br#"
local function impl_noop(_instance, _args_ptr, _out_ptr) end
function polyplug_init(_reg, _ctx)
return {
["test.initok"] = {
contract_version = 1,
plugin_name = "test-init-ok",
factory = function(_host) return {} end,
functions = { [0] = impl_noop },
},
}, { code = 0 } -- AbiErrorCode.Ok
end
"#,
);
let result: Result<(), LoaderError> = load_script(&path, "lua_loader_init_ok_code");
assert!(result.is_ok(), "explicit Ok return must load: {result:?}");
}