#![allow(clippy::expect_used)]
use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use polyplug::error::RuntimeError;
use polyplug::runtime::Runtime;
use polyplug::runtime_store::RuntimeStore;
use polyplug_abi::runtime::ReloadPhase;
use polyplug_abi::{
GuestContractHandle, GuestContractInterface, PluginDescriptor, ReloadPhaseType, StringView,
Version,
};
use polyplug_utils::BundleId;
use polyplug_utils::GuestContractId;
use crate::common::TestNativeLoader;
use crate::fixtures::{
RELOAD_V1_DIR, hot_reload_config, make_hot_reload_runtime, resolve_version_fn, v1_so_path,
v2_so_path,
};
static INTERFACE_V1: GuestContractInterface = make_interface!(
GuestContractId::from_u64(0xDEAD_BEEF_0000_0001_u64),
Version {
major: 1,
minor: 0,
patch: 0,
}
);
static INTERFACE_V2: GuestContractInterface = make_interface!(
GuestContractId::from_u64(0xDEAD_BEEF_0000_0001_u64),
Version {
major: 2,
minor: 0,
patch: 0,
}
);
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 test_swap_interface_changes_interface_pointer() {
let registry: RuntimeStore = RuntimeStore::new();
let descriptor: PluginDescriptor = make_descriptor("swap_test_plugin", "swap.test.contract");
let handle: GuestContractHandle = unsafe {
registry.register_guest_contract(
descriptor,
&INTERFACE_V1,
"swap.test.contract".to_owned(),
BundleId::from_u64(2_u64),
)
}
.expect("registration should succeed");
let _epoch_guard: crossbeam_epoch::Guard = crossbeam_epoch::pin();
let resolve_result_before: Result<*const GuestContractInterface, _> =
registry.resolve_guest_contract(handle);
assert!(
resolve_result_before.is_ok(),
"handle should be valid before swap"
);
let interface_ptr_before: *const GuestContractInterface =
resolve_result_before.expect("resolve before swap should succeed");
let version_before: &Version = unsafe { &(*interface_ptr_before).contract_version };
assert_eq!(
version_before.major, 1,
"before swap: should have version 1"
);
let new_arc: Arc<GuestContractInterface> = Arc::new(INTERFACE_V2);
registry
.swap_guest_contract_interface(handle.index, new_arc)
.expect("swap_interface should succeed");
let resolve_result_after: Result<
*const GuestContractInterface,
polyplug::error::RegistryError,
> = registry.resolve_guest_contract(handle);
assert!(
resolve_result_after.is_ok(),
"handle should still be valid after swap (no generation tracking)"
);
let interface_ptr_after: *const GuestContractInterface =
resolve_result_after.expect("resolve after swap should succeed");
let version_after: &Version = unsafe { &(*interface_ptr_after).contract_version };
assert_eq!(version_after.major, 2, "after swap: should have version 2");
}
#[test]
fn test_direct_swap_interface() {
let registry: RuntimeStore = RuntimeStore::new();
let descriptor: PluginDescriptor = make_descriptor("swap_plugin", "swap.direct.contract");
let handle: GuestContractHandle = unsafe {
registry.register_guest_contract(
descriptor,
&INTERFACE_V1,
"swap.direct.contract".to_owned(),
BundleId::from_u64(3_u64),
)
}
.expect("registration should succeed");
let _epoch_guard: crossbeam_epoch::Guard = crossbeam_epoch::pin();
let interface_ptr_before: *const GuestContractInterface = registry
.resolve_guest_contract(handle)
.expect("resolve should succeed before swap");
let version_before: &Version = unsafe { &(*interface_ptr_before).contract_version };
assert_eq!(version_before.major, 1, "before swap: V1");
let new_arc: Arc<GuestContractInterface> = Arc::new(INTERFACE_V2);
registry
.swap_guest_contract_interface(handle.index, new_arc)
.expect("swap_interface should succeed");
let interface_ptr_after: *const GuestContractInterface = registry
.resolve_guest_contract(handle)
.expect("resolve should succeed after swap");
let version_after: &Version = unsafe { &(*interface_ptr_after).contract_version };
assert_eq!(version_after.major, 2, "after swap: V2");
}
#[test]
fn stress_rapid_reload_cycles_100() {
const CYCLES: u32 = 100_u32;
let rt: Arc<Runtime> = make_hot_reload_runtime();
rt.load_bundle(std::path::Path::new(RELOAD_V1_DIR))
.expect("load v1");
let contract_id: u64 = GuestContractId::new("reload.test", 1).id();
for i in 0_u32..CYCLES {
let so_path: PathBuf = if i % 2_u32 == 0_u32 {
v2_so_path()
} else {
v1_so_path()
};
rt.reload_bundle(so_path.as_path())
.unwrap_or_else(|e: RuntimeError| {
panic!("reload failed at cycle {i}: {e}");
});
let version_fn: extern "C" fn() -> u32 = resolve_version_fn(&rt, contract_id)
.unwrap_or_else(|| {
panic!("interface not resolvable after reload at cycle {i}");
});
let version: u32 = version_fn();
let expected: u32 = if i % 2_u32 == 0_u32 { 200_u32 } else { 100_u32 };
assert_eq!(
version, expected,
"cycle {i}: expected version {expected}, got {version}"
);
}
let final_fn: extern "C" fn() -> u32 =
resolve_version_fn(&rt, contract_id).expect("final interface resolution must succeed");
assert_eq!(
final_fn(),
100_u32,
"after 100 cycles (last = v1) version must be 100"
);
}
#[test]
fn stress_memory_interface_swap_cycles() {
const CYCLES: usize = 50_usize;
let registry: RuntimeStore = RuntimeStore::new();
let descriptor: PluginDescriptor = PluginDescriptor {
name: StringView::from_static(b"stress-mem-plugin"),
contract_name: StringView::from_static(b"stress.mem.contract"),
version: Version {
major: 1,
minor: 0,
patch: 0,
},
};
let handle: polyplug_abi::GuestContractHandle = unsafe {
registry
.register_guest_contract(
descriptor,
&INTERFACE_V1,
"stress.mem.contract".to_owned(),
BundleId::from_u64(0xDEAD_BEEF_0000_0001_u64),
)
.expect("register must succeed")
};
for cycle in 0_usize..CYCLES {
let new_interface: &'static GuestContractInterface = if cycle % 2_usize == 0_usize {
&INTERFACE_V2
} else {
&INTERFACE_V1
};
let new_arc: Arc<GuestContractInterface> = Arc::new(*new_interface);
registry
.swap_guest_contract_interface(handle.index, new_arc)
.unwrap_or_else(|e| panic!("swap_interface failed at cycle {cycle}: {e}"));
}
}
#[test]
fn stress_reload_callback_fires_on_every_cycle() {
const CYCLES: u32 = 100_u32;
let events: Arc<Mutex<Vec<ReloadPhase>>> = Arc::new(Mutex::new(Vec::new()));
let events_clone: Arc<Mutex<Vec<ReloadPhase>>> = Arc::clone(&events);
let rt: Arc<Runtime> = Runtime::builder()
.config(polyplug_abi::runtime::RuntimeConfig {
hot_reload_enabled: true,
..polyplug_abi::runtime::RuntimeConfig::default()
})
.loader(TestNativeLoader::new())
.on_reload(move |_user_data: *mut core::ffi::c_void, ev: ReloadPhase| {
events_clone
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(ev);
})
.build()
.expect("build runtime");
rt.load_bundle(std::path::Path::new(RELOAD_V1_DIR))
.expect("load v1");
for i in 0_u32..CYCLES {
let so_path: PathBuf = if i % 2_u32 == 0_u32 {
v2_so_path()
} else {
v1_so_path()
};
rt.reload_bundle(so_path.as_path())
.unwrap_or_else(|e: RuntimeError| {
panic!("reload failed at cycle {i}: {e}");
});
}
let recorded_events: std::sync::MutexGuard<'_, Vec<ReloadPhase>> =
events.lock().unwrap_or_else(|e| e.into_inner());
let reloaded_count: usize = recorded_events
.iter()
.filter(|ev| ev.phase_type == ReloadPhaseType::Reloaded)
.count();
assert_eq!(
reloaded_count as u32, CYCLES,
"expected {CYCLES} Reloaded callbacks, got {}",
reloaded_count
);
for (idx, ev) in recorded_events.iter().enumerate() {
if ev.phase_type == ReloadPhaseType::Reloaded {
assert!(
!(ev.bundle_name.ptr.is_null() || ev.bundle_name.len == 0),
"event {idx}: bundle_name must not be empty"
);
}
}
}
#[test]
fn stress_concurrent_reload_threads_no_panic() {
const ROUNDS_PER_THREAD: u32 = 40_u32;
let rt: Arc<Runtime> = make_hot_reload_runtime();
rt.load_bundle(std::path::Path::new(RELOAD_V1_DIR))
.expect("load v1");
let contract_id: u64 = GuestContractId::new("reload.test", 1).id();
let rt_a: Arc<Runtime> = Arc::clone(&rt);
let rt_b: Arc<Runtime> = Arc::clone(&rt);
let reloader_a: std::thread::JoinHandle<()> = std::thread::spawn(move || {
for i in 0_u32..ROUNDS_PER_THREAD {
let so_path: PathBuf = if i % 2_u32 == 0_u32 {
v2_so_path()
} else {
v1_so_path()
};
let _: Result<(), RuntimeError> = rt_a.reload_bundle(so_path.as_path());
}
});
let reloader_b: std::thread::JoinHandle<()> = std::thread::spawn(move || {
for i in 0_u32..ROUNDS_PER_THREAD {
let so_path: PathBuf = if i % 2_u32 == 0_u32 {
v1_so_path()
} else {
v2_so_path()
};
let _: Result<(), RuntimeError> = rt_b.reload_bundle(so_path.as_path());
}
});
reloader_a.join().expect("reloader_a must not panic");
reloader_b.join().expect("reloader_b must not panic");
let final_fn: extern "C" fn() -> u32 = resolve_version_fn(&rt, contract_id)
.expect("interface must be resolvable after concurrent reloads");
let version: u32 = final_fn();
assert!(
version == 100_u32 || version == 200_u32,
"final version must be 100 or 200, got {version}"
);
}
#[test]
fn concurrent_reloads_are_mutually_exclusive() {
const THREADS: usize = 4_usize;
const ROUNDS_PER_THREAD: u32 = 50_u32;
let in_section: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0_usize));
let max_seen: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0_usize));
let overlap: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
let cb_in: Arc<AtomicUsize> = Arc::clone(&in_section);
let cb_max: Arc<AtomicUsize> = Arc::clone(&max_seen);
let cb_overlap: Arc<AtomicBool> = Arc::clone(&overlap);
let rt: Arc<Runtime> = Runtime::builder()
.config(hot_reload_config())
.loader(TestNativeLoader::new())
.on_reload(move |_ud: *mut core::ffi::c_void, phase: ReloadPhase| {
match phase.phase_type {
ReloadPhaseType::Preparing => {
let now: usize = cb_in.fetch_add(1_usize, Ordering::SeqCst) + 1_usize;
if now > 1_usize {
cb_overlap.store(true, Ordering::SeqCst);
}
cb_max.fetch_max(now, Ordering::SeqCst);
}
ReloadPhaseType::Reloaded | ReloadPhaseType::Failed => {
let _: Result<usize, usize> =
cb_in.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |v: usize| {
Some(v.saturating_sub(1_usize))
});
}
_ => {}
}
})
.build()
.expect("runtime build must succeed");
rt.load_bundle(std::path::Path::new(RELOAD_V1_DIR))
.expect("load v1");
let contract_id: u64 = GuestContractId::new("reload.test", 1).id();
let barrier: Arc<std::sync::Barrier> = Arc::new(std::sync::Barrier::new(THREADS));
let mut handles: Vec<std::thread::JoinHandle<()>> = Vec::with_capacity(THREADS);
for t in 0_usize..THREADS {
let rt_t: Arc<Runtime> = Arc::clone(&rt);
let bar: Arc<std::sync::Barrier> = Arc::clone(&barrier);
handles.push(std::thread::spawn(move || {
bar.wait();
for i in 0_u32..ROUNDS_PER_THREAD {
let so_path: PathBuf = if (i as usize + t) % 2_usize == 0_usize {
v2_so_path()
} else {
v1_so_path()
};
let _: Result<(), RuntimeError> = rt_t.reload_bundle(so_path.as_path());
assert!(
resolve_version_fn(&rt_t, contract_id).is_some(),
"thread {t} round {i}: contract must stay resolvable across concurrent reloads"
);
}
}));
}
for handle in handles {
handle.join().expect("reloader thread must not panic");
}
assert_eq!(
in_section.load(Ordering::SeqCst),
0_usize,
"every reload critical section must have exited"
);
assert!(
!overlap.load(Ordering::SeqCst),
"two reloads were in their critical sections at once (max concurrency = {}) — reload serialization is broken",
max_seen.load(Ordering::SeqCst)
);
let final_fn: extern "C" fn() -> u32 =
resolve_version_fn(&rt, contract_id).expect("final interface must resolve");
let version: u32 = final_fn();
assert!(
version == 100_u32 || version == 200_u32,
"final version must be 100 or 200, got {version}"
);
}