#![allow(clippy::expect_used)]
use std::sync::Arc;
use polyplug::Runtime;
use polyplug::error::RegistryError;
use polyplug::runtime_store::RuntimeStore;
use polyplug_abi::runtime::RuntimeConfig;
use polyplug_abi::{
Array, DispatchMechanisms, DispatchType, GuestContractHandle, GuestContractInterface, HostApi,
NativeDispatch, PluginDescriptor, StringView, Version,
};
use polyplug_utils::BundleId;
use polyplug_utils::GuestContractId;
const MOCK_FUNCTIONS: [*const (); 0] = [];
unsafe extern "C" fn noop_create_instance(
_loader_data: polyplug_abi::dispatch::VmLoaderData,
_host: *const HostApi,
_args: *const (),
out_instance: *mut polyplug_abi::GuestContractInstance,
) {
if !out_instance.is_null() {
unsafe { out_instance.write(polyplug_abi::GuestContractInstance::null()) };
}
}
unsafe extern "C" fn noop_destroy_instance(
_loader_data: polyplug_abi::dispatch::VmLoaderData,
_host: *const HostApi,
_instance: polyplug_abi::GuestContractInstance,
) {
}
fn make_interface(contract_id: u64, major: u32) -> GuestContractInterface {
GuestContractInterface {
contract_id: GuestContractId::from_u64(contract_id),
contract_version: Version {
major,
minor: 0,
patch: 0,
},
dispatch_type: DispatchType::Native,
create_instance: noop_create_instance,
destroy_instance: noop_destroy_instance,
dispatch: DispatchMechanisms {
native: NativeDispatch {
function_count: 0,
functions: MOCK_FUNCTIONS.as_ptr(),
},
},
}
}
fn make_descriptor(name: &'static str, contract_name: &'static str) -> PluginDescriptor {
PluginDescriptor {
name: StringView::from_static(name.as_bytes()),
contract_name: StringView::from_static(contract_name.as_bytes()),
version: Version {
major: 1,
minor: 0,
patch: 0,
},
}
}
#[test]
fn collect_guest_contracts_filters_versions_and_vacancies() {
const CID: u64 = 0x1234_0000_0000_0001_u64;
let registry: RuntimeStore = RuntimeStore::new();
let contract_id: GuestContractId = GuestContractId::from_u64(CID);
let iface_v1: GuestContractInterface = make_interface(CID, 1);
let iface_v2: GuestContractInterface = make_interface(CID, 2);
let iface_v3: GuestContractInterface = make_interface(CID, 3);
let bundle_a: BundleId = BundleId::from_u64(0xA1);
let bundle_b: BundleId = BundleId::from_u64(0xB2);
let bundle_c: BundleId = BundleId::from_u64(0xC3);
unsafe {
registry
.register_guest_contract(
make_descriptor("a", "multi.contract"),
&iface_v1,
"multi.contract".to_owned(),
bundle_a,
)
.expect("register a");
registry
.register_guest_contract(
make_descriptor("b", "multi.contract"),
&iface_v2,
"multi.contract".to_owned(),
bundle_b,
)
.expect("register b");
registry
.register_guest_contract(
make_descriptor("c", "multi.contract"),
&iface_v3,
"multi.contract".to_owned(),
bundle_c,
)
.expect("register c");
}
let all: Vec<GuestContractHandle> = registry.collect_guest_contracts(contract_id, 0);
assert_eq!(all.len(), 3, "three providers at min_version=0");
let ge2: Vec<GuestContractHandle> = registry.collect_guest_contracts(contract_id, 2);
assert_eq!(ge2.len(), 2, "two providers at min_version=2");
registry
.invalidate_bundle(bundle_b)
.expect("invalidate bundle_b");
let after_unload: Vec<GuestContractHandle> = registry.collect_guest_contracts(contract_id, 0);
assert_eq!(
after_unload.len(),
2,
"two live providers after one unload (vacancy skipped)"
);
let none: Vec<GuestContractHandle> =
registry.collect_guest_contracts(GuestContractId::from_u64(0xDEAD), 0);
assert!(none.is_empty(), "unknown contract collects nothing");
}
#[test]
fn host_find_all_array_len_matches_live_providers_and_frees() {
const CID: u64 = 0x9999_0000_0000_0001_u64;
let host: *const HostApi =
unsafe { polyplug::ffi::polyplug_runtime_create(core::ptr::null::<RuntimeConfig>()) };
assert!(!host.is_null(), "runtime create must yield a host");
let runtime: &Runtime = unsafe { &*((*host).runtime as *const Runtime) };
let registry: &Arc<RuntimeStore> = runtime.registry();
let iface_a: GuestContractInterface = make_interface(CID, 1);
let iface_b: GuestContractInterface = make_interface(CID, 1);
let iface_c: GuestContractInterface = make_interface(CID, 1);
let bundle_a: BundleId = BundleId::from_u64(0x501);
let bundle_b: BundleId = BundleId::from_u64(0x502);
let bundle_c: BundleId = BundleId::from_u64(0x503);
unsafe {
registry
.register_guest_contract(
make_descriptor("a", "find.all"),
&iface_a,
"find.all".to_owned(),
bundle_a,
)
.expect("register a");
registry
.register_guest_contract(
make_descriptor("b", "find.all"),
&iface_b,
"find.all".to_owned(),
bundle_b,
)
.expect("register b");
registry
.register_guest_contract(
make_descriptor("c", "find.all"),
&iface_c,
"find.all".to_owned(),
bundle_c,
)
.expect("register c");
}
registry
.invalidate_bundle(bundle_b)
.expect("invalidate bundle_b");
let array: Array<GuestContractHandle> =
unsafe { ((*host).find_all_guest_contracts)(host, CID, 0) };
assert_eq!(
array.len, 2,
"Array.len must equal the live provider count (2), not a stale pre-count"
);
assert!(
!array.items.is_null(),
"non-empty array must carry a buffer"
);
let size: usize = array.len * core::mem::size_of::<GuestContractHandle>();
unsafe {
((*host).free)(host, array.items as *mut u8, size, array.align);
}
unsafe { polyplug::ffi::polyplug_runtime_destroy(host) };
}
#[test]
fn same_bundle_same_contract_twice_is_duplicate_provider() {
const CID: u64 = 0x4444_0000_0000_0001_u64;
let registry: RuntimeStore = RuntimeStore::new();
let bundle_id: BundleId = BundleId::from_u64(0x1010);
let iface: GuestContractInterface = make_interface(CID, 1);
unsafe {
registry
.register_guest_contract(
make_descriptor("first", "dup.contract"),
&iface,
"dup.contract".to_owned(),
bundle_id,
)
.expect("first register succeeds");
}
let result: Result<GuestContractHandle, RegistryError> = unsafe {
registry.register_guest_contract(
make_descriptor("second", "dup.contract"),
&iface,
"dup.contract".to_owned(),
bundle_id,
)
};
assert!(
matches!(result, Err(RegistryError::DuplicateProvider { .. })),
"same bundle + same contract must be DuplicateProvider, got {result:?}"
);
}
#[test]
fn different_bundles_same_contract_allowed() {
const CID: u64 = 0x4444_0000_0000_0002_u64;
let registry: RuntimeStore = RuntimeStore::new();
let iface_a: GuestContractInterface = make_interface(CID, 1);
let iface_b: GuestContractInterface = make_interface(CID, 1);
unsafe {
registry
.register_guest_contract(
make_descriptor("a", "multi.ok"),
&iface_a,
"multi.ok".to_owned(),
BundleId::from_u64(0x2020),
)
.expect("bundle a register");
registry
.register_guest_contract(
make_descriptor("b", "multi.ok"),
&iface_b,
"multi.ok".to_owned(),
BundleId::from_u64(0x3030),
)
.expect("bundle b register (different bundle, same contract is allowed)");
}
}
#[test]
fn min_version_is_major_floor() {
const CID: u64 = 0x5555_0000_0000_0001_u64;
let registry: RuntimeStore = RuntimeStore::new();
let contract_id: GuestContractId = GuestContractId::from_u64(CID);
let iface: GuestContractInterface = make_interface(CID, 2);
unsafe {
registry
.register_guest_contract(
make_descriptor("p", "major.floor"),
&iface,
"major.floor".to_owned(),
BundleId::from_u64(0x5050),
)
.expect("register");
}
assert!(
registry.find(contract_id, 0).is_ok(),
"major=2 satisfies min_version=0"
);
assert!(
registry.find(contract_id, 2).is_ok(),
"major=2 satisfies min_version=2 (floor is inclusive)"
);
assert!(
registry.find(contract_id, 3).is_err(),
"major=2 does NOT satisfy min_version=3"
);
assert_eq!(
registry.collect_guest_contracts(contract_id, 3).len(),
0,
"collect honours the same MAJOR floor"
);
}
#[test]
fn descriptor_honours_handle_generation() {
const CID: u64 = 0x6666_0000_0000_0001_u64;
let registry: RuntimeStore = RuntimeStore::new();
let bundle_a: BundleId = BundleId::from_u64(0x6060);
let iface_a: GuestContractInterface = make_interface(CID, 1);
let stale_handle: GuestContractHandle = unsafe {
registry
.register_guest_contract(
make_descriptor("original", "gen.contract"),
&iface_a,
"gen.contract".to_owned(),
bundle_a,
)
.expect("register original")
};
registry
.invalidate_bundle(bundle_a)
.expect("invalidate bundle_a");
let bundle_b: BundleId = BundleId::from_u64(0x6061);
let iface_b: GuestContractInterface = make_interface(0x6666_0000_0000_0002_u64, 1);
let new_handle: GuestContractHandle = unsafe {
registry
.register_guest_contract(
make_descriptor("replacement", "gen.contract.new"),
&iface_b,
"gen.contract.new".to_owned(),
bundle_b,
)
.expect("register replacement")
};
assert_eq!(
new_handle.index, stale_handle.index,
"the recycled slot index must be reused"
);
let descriptor = registry.get_guest_contract_descriptor(stale_handle);
assert!(
descriptor.is_none(),
"stale handle must not return the new occupant's descriptor, got {descriptor:?}"
);
let current = registry.get_guest_contract_descriptor(new_handle);
assert!(
current.is_some(),
"the current handle must return its descriptor"
);
}