#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
use core::sync::atomic::AtomicBool;
use core::sync::atomic::Ordering;
use std::path::PathBuf;
use std::sync::Arc;
use polyplug::error::LoaderError;
use polyplug::loader::{BundleLoader, ManifestData};
use polyplug::runtime::Runtime;
use polyplug_abi::{
Compatibility, DispatchMechanisms, DispatchType, GuestContractInstance, GuestContractInterface,
HostApi, NativeDispatch, PluginDescriptor, RuntimeConfig, StringView, Version,
};
use polyplug_utils::{BundleId, GuestContractId};
const MOCK_FNS_EMPTY: [*const (); 0] = [];
unsafe extern "C" fn noop_create_instance(
_loader_data: polyplug_abi::dispatch::VmLoaderData,
_host: *const HostApi,
_args: *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,
) {
}
struct CascadeLoader {
loader_name: &'static str,
contract_id: u64,
reload_called: Arc<AtomicBool>,
}
impl CascadeLoader {
fn register(&self, manifest: &ManifestData, runtime: &Runtime) {
let interface: &'static GuestContractInterface =
Box::leak(Box::new(GuestContractInterface {
contract_id: GuestContractId::from_u64(self.contract_id),
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: 0,
functions: MOCK_FNS_EMPTY.as_ptr(),
},
},
}));
let descriptor: PluginDescriptor = PluginDescriptor {
name: StringView::from_static(b"cascade"),
contract_name: StringView::from_static(b"cascade.contract"),
version: Version {
major: 1,
minor: 0,
patch: 0,
},
};
let bundle_id: BundleId = BundleId::new(&manifest.name);
unsafe {
runtime.registry().register_guest_contract(
descriptor,
interface,
"cascade.contract".to_owned(),
bundle_id,
)
}
.expect("contract registration should succeed");
}
}
impl BundleLoader for CascadeLoader {
fn loader_name(&self) -> &'static str {
self.loader_name
}
fn loader_language(&self) -> polyplug_abi::SupportedLanguage {
polyplug_abi::SupportedLanguage::Rust
}
fn supports_hot_reload(&self) -> bool {
true
}
fn load(
&self,
manifest: &ManifestData,
_source: &polyplug::loader::BundleSource,
runtime: &Runtime,
) -> Result<(), LoaderError> {
self.register(manifest, runtime);
Ok(())
}
fn reload(&self, manifest: &ManifestData, runtime: &Runtime) -> Result<(), LoaderError> {
self.reload_called.store(true, Ordering::SeqCst);
self.register(manifest, runtime);
Ok(())
}
}
fn hot_reload_config() -> RuntimeConfig {
RuntimeConfig {
compatibility: Compatibility::Yolo,
hot_reload_enabled: true,
on_reload: None,
on_reload_user_data: core::ptr::null_mut(),
..Default::default()
}
}
fn write_bundle(
temp: &tempfile::TempDir,
bundle_name: &str,
loader_name: &str,
needs_reinit: bool,
dep_contract: Option<&str>,
) -> PathBuf {
let bundle_dir: PathBuf = temp.path().join(bundle_name);
std::fs::create_dir_all(&bundle_dir).expect("create bundle dir");
std::fs::write(bundle_dir.join("dummy.so"), b"").expect("write dummy so");
let bundle_id: u64 = polyplug_utils::bundle_id(bundle_name);
let mut manifest: String = format!(
"id = {bundle_id}\n\
name = \"{bundle_name}\"\n\
loader = \"{loader_name}\"\n\
file = \"dummy.so\"\n\
version = \"1.0\"\n\
needs_reinit_on_dep_reload = {needs_reinit}\n"
);
if let Some(contract_name) = dep_contract {
let contract_id: u64 = polyplug_utils::guest_contract_id(contract_name, 1_u32);
manifest.push_str(&format!(
"\n[[dependency]]\n\
kind = \"contract\"\n\
contract = \"{contract_name}\"\n\
min_version = \"1.0\"\n\
contract_id = {contract_id}\n"
));
}
std::fs::write(bundle_dir.join("manifest.toml"), manifest).expect("write manifest");
bundle_dir
}
#[test]
fn cascade_reload_disabled_does_not_trigger() {
let temp: tempfile::TempDir = tempfile::TempDir::new().expect("temp dir");
let a_contract_id: u64 = polyplug_utils::guest_contract_id("dep.contract", 1_u32);
let b_reload_called: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
let runtime: Arc<Runtime> = Runtime::builder()
.config(hot_reload_config())
.loader(CascadeLoader {
loader_name: "cascade-a",
contract_id: a_contract_id,
reload_called: Arc::new(AtomicBool::new(false)),
})
.loader(CascadeLoader {
loader_name: "cascade-b",
contract_id: polyplug_utils::guest_contract_id("b.contract", 1_u32),
reload_called: Arc::clone(&b_reload_called),
})
.build()
.expect("runtime build should succeed");
let a_path: PathBuf = write_bundle(&temp, "bundle_a", "cascade-a", false, None);
let b_path: PathBuf = write_bundle(&temp, "bundle_b", "cascade-b", false, Some("dep.contract"));
runtime.load_bundle(a_path.as_path()).expect("load A");
runtime.load_bundle(b_path.as_path()).expect("load B");
runtime.reload_bundle(a_path.as_path()).expect("reload A");
assert!(
!b_reload_called.load(Ordering::SeqCst),
"B must NOT be reloaded when needs_reinit_on_dep_reload is false"
);
}
#[test]
fn cascade_reload_enabled_triggers_dependent() {
let temp: tempfile::TempDir = tempfile::TempDir::new().expect("temp dir");
let a_contract_id: u64 = polyplug_utils::guest_contract_id("dep.contract", 1_u32);
let b_reload_called: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
let runtime: Arc<Runtime> = Runtime::builder()
.config(hot_reload_config())
.loader(CascadeLoader {
loader_name: "cascade-a",
contract_id: a_contract_id,
reload_called: Arc::new(AtomicBool::new(false)),
})
.loader(CascadeLoader {
loader_name: "cascade-b",
contract_id: polyplug_utils::guest_contract_id("b.contract", 1_u32),
reload_called: Arc::clone(&b_reload_called),
})
.build()
.expect("runtime build should succeed");
let a_path: PathBuf = write_bundle(&temp, "bundle_a", "cascade-a", false, None);
let b_path: PathBuf = write_bundle(&temp, "bundle_b", "cascade-b", true, Some("dep.contract"));
runtime.load_bundle(a_path.as_path()).expect("load A");
runtime.load_bundle(b_path.as_path()).expect("load B");
runtime.reload_bundle(a_path.as_path()).expect("reload A");
assert!(
b_reload_called.load(Ordering::SeqCst),
"B must be reloaded when it depends on A and opts into cascade reload"
);
}
#[test]
fn cascade_reload_cycle_detection() {
let temp: tempfile::TempDir = tempfile::TempDir::new().expect("temp dir");
let a_contract_id: u64 = polyplug_utils::guest_contract_id("a.contract", 1_u32);
let b_contract_id: u64 = polyplug_utils::guest_contract_id("b.contract", 1_u32);
let a_reload_called: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
let b_reload_called: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
let runtime: Arc<Runtime> = Runtime::builder()
.config(hot_reload_config())
.loader(CascadeLoader {
loader_name: "cycle-a",
contract_id: a_contract_id,
reload_called: Arc::clone(&a_reload_called),
})
.loader(CascadeLoader {
loader_name: "cycle-b",
contract_id: b_contract_id,
reload_called: Arc::clone(&b_reload_called),
})
.build()
.expect("runtime build should succeed");
let a_path: PathBuf = write_bundle(&temp, "cycle_a", "cycle-a", true, Some("b.contract"));
let b_path: PathBuf = write_bundle(&temp, "cycle_b", "cycle-b", true, Some("a.contract"));
runtime.load_bundle(a_path.as_path()).expect("load A");
runtime.load_bundle(b_path.as_path()).expect("load B");
runtime.reload_bundle(a_path.as_path()).expect("reload A");
assert!(
b_reload_called.load(Ordering::SeqCst),
"B must cascade-reload from A's reload"
);
}