#![allow(clippy::expect_used)]
use core::ffi::c_void;
use std::sync::{Arc, Mutex};
use polyplug::Runtime;
use polyplug::error::{LoaderError, RegistryError, RuntimeError};
use polyplug::loader::{BundleLoader, BundleSource, ManifestData};
use polyplug_abi::HostApi;
use polyplug_abi::dispatch::{DispatchMechanisms, DispatchType, NativeDispatch};
use polyplug_abi::guest::{GuestContractInstance, GuestContractInterface};
use polyplug_abi::plugin::PluginDescriptor;
use polyplug_abi::runtime::{Compatibility, RuntimeConfig};
use polyplug_abi::types::{LogLevel, StringView, Version};
use polyplug_utils::{BundleId, GuestContractId};
const CONTRACT_NAME: &str = "compat.test";
const BUNDLE_NAME: &str = "compat_test_bundle";
unsafe extern "C" fn noop_create_instance(
_loader_data: polyplug_abi::dispatch::VmLoaderData,
_host: *const HostApi,
_ctx: *const (),
out_instance: *mut GuestContractInstance,
) {
if !out_instance.is_null() {
unsafe { out_instance.write(GuestContractInstance::null()) };
}
}
unsafe extern "C" fn noop_destroy_instance(
_loader_data: polyplug_abi::dispatch::VmLoaderData,
_host: *const HostApi,
_instance: GuestContractInstance,
) {
}
fn leak_native_interface(function_count: u32) -> &'static GuestContractInterface {
Box::leak(Box::new(GuestContractInterface {
contract_id: GuestContractId::new(CONTRACT_NAME, 1_u32),
contract_version: Version {
major: 1,
minor: 0,
patch: 0,
},
dispatch_type: DispatchType::Native,
create_instance: noop_create_instance,
destroy_instance: noop_destroy_instance,
dispatch: DispatchMechanisms {
native: NativeDispatch {
function_count,
functions: core::ptr::null(),
},
},
}))
}
struct CompatTestLoader {
actual_function_count: u32,
}
impl BundleLoader for CompatTestLoader {
fn loader_name(&self) -> &'static str {
"compat-test"
}
fn loader_language(&self) -> polyplug_abi::SupportedLanguage {
polyplug_abi::SupportedLanguage::Rust
}
fn supports_hot_reload(&self) -> bool {
false
}
fn load(
&self,
manifest: &ManifestData,
_source: &BundleSource,
runtime: &Runtime,
) -> Result<(), LoaderError> {
let bundle_id: BundleId = BundleId::new(&manifest.name);
runtime.push_init_bundle_id(bundle_id.id());
let interface: &'static GuestContractInterface =
leak_native_interface(self.actual_function_count);
let descriptor: PluginDescriptor = PluginDescriptor {
name: StringView::from_static(BUNDLE_NAME.as_bytes()),
contract_name: StringView::from_static(CONTRACT_NAME.as_bytes()),
version: Version {
major: 1,
minor: 0,
patch: 0,
},
};
let result: Result<_, _> = unsafe {
runtime.registry().register_guest_contract(
descriptor,
interface,
CONTRACT_NAME.to_owned(),
bundle_id,
)
};
runtime.pop_init_bundle_id();
result
.map(|_| ())
.map_err(|e: RegistryError| LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: e.to_string(),
})
}
fn reload(&self, _manifest: &ManifestData, _runtime: &Runtime) -> Result<(), LoaderError> {
Err(LoaderError::HotReloadUnsupported {
loader_name: self.loader_name().to_owned(),
})
}
}
fn mismatched_manifest() -> ManifestData {
let mut function_count: std::collections::HashMap<String, u32> =
std::collections::HashMap::new();
function_count.insert(format!("{CONTRACT_NAME}@1"), 2_u32);
ManifestData {
loader: "compat-test".to_owned(),
name: BUNDLE_NAME.to_owned(),
dependencies: Vec::new(),
id: polyplug_utils::bundle_id(BUNDLE_NAME),
version: "1.0".to_owned(),
file: "inline.code".to_owned(),
provides: vec![format!("{CONTRACT_NAME}@1")],
function_count,
needs_reinit_on_dep_reload: false,
bundle_dependencies: Vec::new(),
path: std::path::PathBuf::new(),
}
}
unsafe extern "C" fn capture_log(
user_data: *mut c_void,
level: u32,
scope: StringView,
message: StringView,
) {
let sink: &Mutex<Vec<(u32, String, String)>> =
unsafe { &*(user_data as *const Mutex<Vec<(u32, String, String)>>) };
let (scope_owned, message_owned): (String, String) =
unsafe { (scope.as_str().to_owned(), message.as_str().to_owned()) };
sink.lock()
.expect("sink lock")
.push((level, scope_owned, message_owned));
}
fn build_runtime(
compatibility: Compatibility,
sink: &Mutex<Vec<(u32, String, String)>>,
) -> Arc<Runtime> {
let config: RuntimeConfig = RuntimeConfig {
compatibility,
log: Some(capture_log),
log_user_data: sink as *const Mutex<Vec<(u32, String, String)>> as *mut c_void,
log_max_level: LogLevel::Trace as u32,
..Default::default()
};
Runtime::builder()
.config(config)
.loader(CompatTestLoader {
actual_function_count: 1,
})
.build()
.expect("build runtime")
}
#[test]
fn relaxed_runtime_loads_mismatched_bundle_and_warns() {
let sink: Box<Mutex<Vec<(u32, String, String)>>> = Box::new(Mutex::new(Vec::new()));
let runtime: Arc<Runtime> = build_runtime(Compatibility::Relaxed, &sink);
let result: Result<(), RuntimeError> =
runtime.load_bundle_from_source(mismatched_manifest(), BundleSource::Code(String::new()));
assert!(
result.is_ok(),
"Relaxed runtime must LOAD the function_count-mismatched bundle, got: {:?}",
result.err()
);
let captured: Vec<(u32, String, String)> = sink.lock().expect("sink lock").clone();
assert!(
captured.iter().any(|(level, scope, msg)| {
*level == LogLevel::Warn as u32
&& scope == "runtime"
&& msg.contains("declared function_count 2 but interface exports 1")
}),
"Relaxed runtime must DELIVER the function_count-mismatch warning to the logger, got: {captured:?}"
);
}
#[test]
fn strict_runtime_rejects_mismatched_bundle() {
let sink: Box<Mutex<Vec<(u32, String, String)>>> = Box::new(Mutex::new(Vec::new()));
let runtime: Arc<Runtime> = build_runtime(Compatibility::Strict, &sink);
let result: Result<(), RuntimeError> =
runtime.load_bundle_from_source(mismatched_manifest(), BundleSource::Code(String::new()));
match result {
Err(RuntimeError::Loader(_)) => {}
other => panic!(
"Strict runtime must REJECT the function_count-mismatched bundle with a Loader error, got: {other:?}"
),
}
}