use std::sync::{Arc, Mutex};
use super::*;
use crate::atom::{Atom, AtomTable};
use crate::module::ModuleRegistry;
use crate::native::BifRegistryImpl;
use crate::native::native_process::{
NativeContext, NativeHandler, NativeHandlerFactory, NativeOutcome,
};
use crate::process::ExitReason;
use crate::term::Term;
struct Echo {
reply_to: u64,
}
impl NativeHandler for Echo {
fn handle(&mut self, ctx: &mut NativeContext<'_>) -> NativeOutcome {
match ctx.recv() {
Some(message) => {
ctx.send(self.reply_to, message);
NativeOutcome::Stop(ExitReason::Normal)
}
None => NativeOutcome::Wait,
}
}
}
struct Collector {
sink: Arc<Mutex<Option<i64>>>,
}
impl NativeHandler for Collector {
fn handle(&mut self, ctx: &mut NativeContext<'_>) -> NativeOutcome {
while let Some(message) = ctx.recv() {
if let Some(value) = message.as_small_int()
&& let Ok(mut guard) = self.sink.lock()
{
*guard = Some(value);
}
}
NativeOutcome::Wait
}
}
fn scheduler() -> WasmScheduler {
let atom_table = Arc::new(AtomTable::with_common_atoms());
let modules = Arc::new(ModuleRegistry::new());
let bifs = Arc::new(BifRegistryImpl::new());
WasmScheduler::new(atom_table, modules, bifs)
}
fn drain_until_exit(scheduler: &mut WasmScheduler, pid: u64, max_turns: usize) -> bool {
for _ in 0..max_turns {
let exited = scheduler.run_native_until_idle();
if exited.contains(&pid) {
return true;
}
}
false
}
fn drain_run_until_idle(scheduler: &mut WasmScheduler, pid: u64, max_turns: usize) -> bool {
for _ in 0..max_turns {
let summary = scheduler.run_until_idle();
if summary.exited.contains(&pid) {
return true;
}
}
false
}
#[test]
fn native_actor_runs_through_unified_run_until_idle_pump() {
let mut scheduler = scheduler();
let sink = Arc::new(Mutex::new(None));
let collector = scheduler.spawn_native_root({
let sink = Arc::clone(&sink);
Box::new(move || {
Box::new(Collector {
sink: Arc::clone(&sink),
})
})
});
let echo = scheduler.spawn_native_root(Box::new(move || {
Box::new(Echo {
reply_to: collector,
})
}));
let summary = scheduler.run_until_idle();
assert!(
summary.exited.is_empty(),
"nothing exits before a message arrives"
);
assert!(
summary.executed >= 1,
"the native actors received a slice through the unified pump"
);
scheduler
.send_owned(echo, &crate::ets::OwnedTerm::immediate(Term::small_int(99)))
.expect("message delivers to the parked echo actor");
assert!(
drain_run_until_idle(&mut scheduler, echo, 4),
"the echo actor exits via the unified pump after handling its message"
);
assert_eq!(
scheduler.native_exit_reason(echo),
Some(ExitReason::Normal),
"the echo actor stopped normally under the unified pump"
);
for _ in 0..4 {
let _summary = scheduler.run_until_idle();
if sink.lock().expect("sink lock").is_some() {
break;
}
}
assert_eq!(
*sink.lock().expect("sink lock"),
Some(99),
"the forwarded value is observable end-to-end through run_until_idle"
);
}
#[test]
fn native_actor_spawns_receives_one_message_and_replies_with_captured_result() {
let mut scheduler = scheduler();
let sink = Arc::new(Mutex::new(None));
let collector = scheduler.spawn_native_root({
let sink = Arc::clone(&sink);
Box::new(move || {
Box::new(Collector {
sink: Arc::clone(&sink),
})
})
});
let echo = scheduler.spawn_native_root(Box::new(move || {
Box::new(Echo {
reply_to: collector,
})
}));
let exited = scheduler.run_native_until_idle();
assert!(exited.is_empty(), "nothing exits before a message arrives");
assert_eq!(
*sink.lock().expect("sink lock"),
None,
"collector has received nothing yet"
);
scheduler
.send_owned(echo, &crate::ets::OwnedTerm::immediate(Term::small_int(42)))
.expect("message delivers to the parked echo actor");
assert!(
drain_until_exit(&mut scheduler, echo, 4),
"the echo actor exits after handling its one message"
);
assert_eq!(
scheduler.native_exit_reason(echo),
Some(ExitReason::Normal),
"the echo actor stopped normally"
);
for _ in 0..4 {
let _exited = scheduler.run_native_until_idle();
if sink.lock().expect("sink lock").is_some() {
break;
}
}
assert_eq!(
*sink.lock().expect("sink lock"),
Some(42),
"the result the native actor produced is observable end-to-end"
);
}
struct Parent {
reply_to: u64,
}
impl NativeHandler for Parent {
fn handle(&mut self, ctx: &mut NativeContext<'_>) -> NativeOutcome {
let Some(_trigger) = ctx.recv() else {
return NativeOutcome::Wait;
};
let reply_to = self.reply_to;
let child = ctx
.spawn_native(Box::new(move || Box::new(Echo { reply_to })), None)
.expect("cooperative spawn_native succeeds");
ctx.send(child, Term::small_int(7));
NativeOutcome::Stop(ExitReason::Normal)
}
}
#[test]
fn handler_spawns_child_and_sends_it_a_message_cooperatively() {
let mut scheduler = scheduler();
let sink = Arc::new(Mutex::new(None));
let collector = scheduler.spawn_native_root({
let sink = Arc::clone(&sink);
Box::new(move || {
Box::new(Collector {
sink: Arc::clone(&sink),
})
})
});
let parent = scheduler.spawn_native_root(Box::new(move || {
Box::new(Parent {
reply_to: collector,
})
}));
let _first = scheduler.run_native_until_idle();
scheduler
.send_owned(
parent,
&crate::ets::OwnedTerm::immediate(Term::atom(Atom::OK)),
)
.expect("trigger delivers to the parent");
assert!(
drain_until_exit(&mut scheduler, parent, 8),
"the parent exits after spawning and sending"
);
assert_eq!(
scheduler.native_exit_reason(parent),
Some(ExitReason::Normal),
"parent stopped after spawning and sending"
);
for _ in 0..8 {
let _exited = scheduler.run_native_until_idle();
if sink.lock().expect("sink lock").is_some() {
break;
}
}
assert_eq!(
*sink.lock().expect("sink lock"),
Some(7),
"the cooperatively-spawned child received and forwarded the message"
);
}
struct SelfTicker {
delay: std::time::Duration,
tick_value: i64,
sink: Arc<Mutex<Option<i64>>>,
armed: bool,
}
impl NativeHandler for SelfTicker {
fn handle(&mut self, ctx: &mut NativeContext<'_>) -> NativeOutcome {
if let Some(message) = ctx.recv() {
if let Some(value) = message.as_small_int()
&& let Ok(mut guard) = self.sink.lock()
{
*guard = Some(value);
}
return NativeOutcome::Stop(ExitReason::Normal);
}
if !self.armed {
self.armed = true;
let reference = ctx.schedule(self.delay, Term::small_int(self.tick_value));
assert!(
reference.is_some(),
"the cooperative scheduler supplies a real timer wheel"
);
}
NativeOutcome::Wait
}
}
#[test]
fn native_actor_self_tick_is_delivered_when_the_timer_fires() {
let mut scheduler = scheduler();
let sink = Arc::new(Mutex::new(None));
let delay = std::time::Duration::from_secs(10);
let ticker = scheduler.spawn_native_root({
let sink = Arc::clone(&sink);
Box::new(move || {
Box::new(SelfTicker {
delay,
tick_value: 1234,
sink: Arc::clone(&sink),
armed: false,
})
})
});
let exited = scheduler.run_native_until_idle();
assert!(exited.is_empty(), "the ticker parks after arming its timer");
assert_eq!(
*sink.lock().expect("sink lock"),
None,
"no tick before the delay elapses"
);
let start = std::time::Instant::now();
let woken_early = scheduler.tick_native_timers_at(start + std::time::Duration::from_secs(5));
assert!(
woken_early.is_empty(),
"the self-tick must not fire before its delay"
);
let _early_turn = scheduler.run_native_until_idle();
assert_eq!(
*sink.lock().expect("sink lock"),
None,
"still no tick before the delay elapses"
);
let woken = scheduler.tick_native_timers_at(start + delay + std::time::Duration::from_secs(5));
assert_eq!(
woken,
vec![ticker],
"the expired self-tick wakes exactly the scheduling actor"
);
assert!(
drain_until_exit(&mut scheduler, ticker, 4),
"the rescheduled actor runs and exits after receiving its self-tick"
);
assert_eq!(
scheduler.native_exit_reason(ticker),
Some(ExitReason::Normal),
"the ticker stopped normally after handling its tick"
);
assert_eq!(
*sink.lock().expect("sink lock"),
Some(1234),
"the scheduled timer message was delivered to the actor's mailbox"
);
}
const CMD_CRASH: i64 = 1;
const CMD_WORK: i64 = 2;
struct Worker {
sink: Arc<Mutex<Vec<i64>>>,
}
impl NativeHandler for Worker {
fn handle(&mut self, ctx: &mut NativeContext<'_>) -> NativeOutcome {
match ctx.recv().and_then(Term::as_small_int) {
Some(CMD_CRASH) => NativeOutcome::Stop(ExitReason::Error),
Some(CMD_WORK) => {
if let Ok(mut guard) = self.sink.lock() {
guard.push(CMD_WORK);
}
NativeOutcome::Stop(ExitReason::Normal)
}
_ => NativeOutcome::Wait,
}
}
}
struct Supervisor {
sink: Arc<Mutex<Vec<i64>>>,
restarts: Arc<Mutex<u32>>,
started: bool,
child_pid: Arc<Mutex<Option<u64>>>,
}
impl Supervisor {
fn child_factory(sink: Arc<Mutex<Vec<i64>>>) -> NativeHandlerFactory {
Box::new(move || {
Box::new(Worker {
sink: Arc::clone(&sink),
})
})
}
}
impl NativeHandler for Supervisor {
fn handle(&mut self, ctx: &mut NativeContext<'_>) -> NativeOutcome {
if !self.started {
self.started = true;
ctx.set_trap_exit(true);
let child = ctx
.spawn_native(
Self::child_factory(Arc::clone(&self.sink)),
Some(ctx.self_pid()),
)
.expect("supervisor spawns its linked child");
*self.child_pid.lock().expect("child pid lock") = Some(child);
ctx.send(child, Term::small_int(CMD_CRASH));
return NativeOutcome::Wait;
}
let Some(message) = ctx.recv() else {
return NativeOutcome::Wait;
};
let tuple = crate::term::boxed::Tuple::new(message)
.expect("a trapping supervisor receives the EXIT signal as a tuple");
assert_eq!(tuple.arity(), 3, "EXIT signal is a 3-tuple");
assert_eq!(
tuple.get(0).and_then(Term::as_atom),
Some(Atom::EXIT),
"first element is the 'EXIT' atom"
);
assert_eq!(
tuple.get(2).and_then(Term::as_atom),
Some(Atom::ERROR),
"the reported reason is the child's crash reason"
);
let child = ctx
.spawn_native(
Self::child_factory(Arc::clone(&self.sink)),
Some(ctx.self_pid()),
)
.expect("supervisor restarts the child via the factory");
*self.child_pid.lock().expect("child pid lock") = Some(child);
*self.restarts.lock().expect("restart counter lock") += 1;
ctx.send(child, Term::small_int(CMD_WORK));
NativeOutcome::Stop(ExitReason::Normal)
}
}
#[test]
fn supervisor_restarts_crashed_supervised_child_via_factory() {
let mut scheduler = scheduler();
let sink = Arc::new(Mutex::new(Vec::new()));
let restarts = Arc::new(Mutex::new(0));
let child_pid = Arc::new(Mutex::new(None));
let supervisor = scheduler.spawn_native_root({
let sink = Arc::clone(&sink);
let restarts = Arc::clone(&restarts);
let child_pid = Arc::clone(&child_pid);
Box::new(move || {
Box::new(Supervisor {
sink: Arc::clone(&sink),
restarts: Arc::clone(&restarts),
started: false,
child_pid: Arc::clone(&child_pid),
})
})
});
let _first = scheduler.run_native_until_idle();
scheduler
.send_owned(
supervisor,
&crate::ets::OwnedTerm::immediate(Term::atom(Atom::OK)),
)
.expect("trigger delivers to the supervisor");
assert!(
drain_until_exit(&mut scheduler, supervisor, 12),
"the supervisor exits after observing the crash and restarting"
);
assert_eq!(
scheduler.native_exit_reason(supervisor),
Some(ExitReason::Normal),
"the supervisor stopped normally after restarting the child"
);
assert_eq!(
*restarts.lock().expect("restart counter lock"),
1,
"the supervisor restarted the child exactly once via the factory"
);
for _ in 0..12 {
let _exited = scheduler.run_native_until_idle();
if !sink.lock().expect("sink lock").is_empty() {
break;
}
}
assert_eq!(
*sink.lock().expect("sink lock"),
vec![CMD_WORK],
"the restarted child received its message and ran"
);
let restarted = child_pid
.lock()
.expect("child pid lock")
.expect("a child pid");
assert_eq!(
scheduler.native_exit_reason(restarted),
Some(ExitReason::Normal),
"the restarted child stopped normally after doing its work"
);
}
struct Bystander;
impl NativeHandler for Bystander {
fn handle(&mut self, _ctx: &mut NativeContext<'_>) -> NativeOutcome {
NativeOutcome::Wait
}
}
struct Linker {
bystander_pid: Arc<Mutex<Option<u64>>>,
}
impl NativeHandler for Linker {
fn handle(&mut self, ctx: &mut NativeContext<'_>) -> NativeOutcome {
let bystander = ctx
.spawn_native(Box::new(|| Box::new(Bystander)), Some(ctx.self_pid()))
.expect("linker spawns its linked bystander");
*self.bystander_pid.lock().expect("bystander pid lock") = Some(bystander);
NativeOutcome::Stop(ExitReason::Error)
}
}
#[test]
fn linked_non_trapping_process_dies_on_abnormal_link_exit() {
let mut scheduler = scheduler();
let bystander_pid = Arc::new(Mutex::new(None));
let linker = scheduler.spawn_native_root({
let bystander_pid = Arc::clone(&bystander_pid);
Box::new(move || {
Box::new(Linker {
bystander_pid: Arc::clone(&bystander_pid),
})
})
});
assert!(
drain_until_exit(&mut scheduler, linker, 4),
"the linker exits after spawning and crashing"
);
assert_eq!(
scheduler.native_exit_reason(linker),
Some(ExitReason::Error),
"the linker crashed abnormally"
);
let bystander = bystander_pid
.lock()
.expect("bystander pid lock")
.expect("a bystander pid");
assert_eq!(
scheduler.native_exit_reason(bystander),
Some(ExitReason::Error),
"the non-trapping bystander died from the abnormal link exit"
);
}
#[test]
fn linked_non_trapping_process_survives_normal_link_exit() {
struct NormalLinker {
bystander_pid: Arc<Mutex<Option<u64>>>,
}
impl NativeHandler for NormalLinker {
fn handle(&mut self, ctx: &mut NativeContext<'_>) -> NativeOutcome {
let bystander = ctx
.spawn_native(Box::new(|| Box::new(Bystander)), Some(ctx.self_pid()))
.expect("linker spawns its linked bystander");
*self.bystander_pid.lock().expect("bystander pid lock") = Some(bystander);
NativeOutcome::Stop(ExitReason::Normal)
}
}
let mut scheduler = scheduler();
let bystander_pid = Arc::new(Mutex::new(None));
let linker = scheduler.spawn_native_root({
let bystander_pid = Arc::clone(&bystander_pid);
Box::new(move || {
Box::new(NormalLinker {
bystander_pid: Arc::clone(&bystander_pid),
})
})
});
assert!(
drain_until_exit(&mut scheduler, linker, 4),
"the linker exits normally after spawning"
);
for _ in 0..4 {
let _exited = scheduler.run_native_until_idle();
}
let bystander = bystander_pid
.lock()
.expect("bystander pid lock")
.expect("a bystander pid");
assert_eq!(
scheduler.native_exit_reason(bystander),
None,
"a Normal link exit does not kill a non-trapping survivor"
);
}
struct ChainNode {
depth: usize,
max_depth: usize,
started: bool,
pids: Arc<Vec<Mutex<Option<u64>>>>,
crash_on_message: bool,
}
impl NativeHandler for ChainNode {
fn handle(&mut self, ctx: &mut NativeContext<'_>) -> NativeOutcome {
if !self.started {
self.started = true;
let child_depth = self.depth + 1;
if child_depth < self.max_depth {
let max_depth = self.max_depth;
let pids = Arc::clone(&self.pids);
let factory: NativeHandlerFactory = Box::new(move || {
Box::new(ChainNode {
depth: child_depth,
max_depth,
started: false,
pids: Arc::clone(&pids),
crash_on_message: false,
})
});
let child = ctx
.spawn_native(factory, Some(ctx.self_pid()))
.expect("chain node spawns its linked child");
*self.pids[child_depth].lock().expect("chain pid lock") = Some(child);
}
return NativeOutcome::Wait;
}
if self.crash_on_message && ctx.recv().and_then(Term::as_small_int) == Some(CMD_CRASH) {
return NativeOutcome::Stop(ExitReason::Error);
}
NativeOutcome::Wait
}
}
#[test]
fn abnormal_link_exit_cascades_transitively_through_a_nontrapping_chain() {
let mut scheduler = scheduler();
const DEPTH: usize = 3;
let pids: Arc<Vec<Mutex<Option<u64>>>> =
Arc::new((0..DEPTH).map(|_| Mutex::new(None)).collect());
let head = scheduler.spawn_native_root({
let pids = Arc::clone(&pids);
Box::new(move || {
Box::new(ChainNode {
depth: 0,
max_depth: DEPTH,
started: false,
pids: Arc::clone(&pids),
crash_on_message: true,
})
})
});
*pids[0].lock().expect("chain pid lock") = Some(head);
for _ in 0..=DEPTH {
let _settle = scheduler.run_native_until_idle();
}
let child = pids[1]
.lock()
.expect("chain pid lock")
.expect("a child pid");
let grandchild = pids[2]
.lock()
.expect("chain pid lock")
.expect("a grandchild pid");
assert_eq!(
scheduler.native_exit_reason(child),
None,
"the child is alive before the crash"
);
assert_eq!(
scheduler.native_exit_reason(grandchild),
None,
"the grandchild is alive before the crash"
);
scheduler
.send_owned(
head,
&crate::ets::OwnedTerm::immediate(Term::small_int(CMD_CRASH)),
)
.expect("the crash trigger delivers to the head");
assert!(
drain_until_exit(&mut scheduler, head, 4),
"the head exits after receiving its crash trigger"
);
assert_eq!(
scheduler.native_exit_reason(head),
Some(ExitReason::Error),
"the head crashed abnormally"
);
assert_eq!(
scheduler.native_exit_reason(child),
Some(ExitReason::Error),
"the directly-linked child died from the head's abnormal exit"
);
assert_eq!(
scheduler.native_exit_reason(grandchild),
Some(ExitReason::Error),
"the grandchild died from the TRANSITIVE cascade through the child"
);
}
use std::cell::RefCell;
use std::rc::Rc;
use crate::native::{NativeKey, ProcessContext, WasmAsyncNifFacility};
const ASYNC_MFA: NativeKey = (Atom::OK, Atom::ERROR, 1);
struct FakeHost {
started: Rc<RefCell<Vec<(NativeKey, usize)>>>,
}
impl WasmAsyncNifFacility for FakeHost {
fn start_async_nif(
&self,
mfa: NativeKey,
args: &[Term],
context: &mut ProcessContext<'_>,
) -> Result<Term, Term> {
let _pid = context.pid();
self.started.borrow_mut().push((mfa, args.len()));
Ok(Term::NIL)
}
}
struct AsyncCaller {
started_ok: Arc<Mutex<Option<bool>>>,
outcome: Arc<Mutex<Option<(bool, i64)>>>,
issued: bool,
}
impl NativeHandler for AsyncCaller {
fn handle(&mut self, ctx: &mut NativeContext<'_>) -> NativeOutcome {
if !self.issued {
self.issued = true;
let started = ctx.start_async(ASYNC_MFA, &[Term::small_int(5)]).is_ok();
*self.started_ok.lock().expect("started lock") = Some(started);
return NativeOutcome::Wait;
}
let Some(message) = ctx.recv() else {
return NativeOutcome::Wait;
};
let tuple = crate::term::boxed::Tuple::new(message)
.expect("the async completion is delivered as a {tag, Value} tuple");
let is_ok = tuple.get(0).and_then(Term::as_atom) == Some(Atom::OK);
let value = tuple
.get(1)
.and_then(Term::as_small_int)
.expect("completion payload is a small int in this test");
*self.outcome.lock().expect("outcome lock") = Some((is_ok, value));
NativeOutcome::Stop(ExitReason::Normal)
}
}
#[test]
fn native_handler_starts_host_async_op_suspends_and_resumes_on_completion() {
let mut scheduler = scheduler();
let started = Rc::new(RefCell::new(Vec::new()));
let facility: Rc<dyn WasmAsyncNifFacility> = Rc::new(FakeHost {
started: Rc::clone(&started),
});
scheduler.set_wasm_async_nif_facility(Some(facility));
let started_ok = Arc::new(Mutex::new(None));
let outcome = Arc::new(Mutex::new(None));
let pid = scheduler.spawn_native_root({
let started_ok = Arc::clone(&started_ok);
let outcome = Arc::clone(&outcome);
Box::new(move || {
Box::new(AsyncCaller {
started_ok: Arc::clone(&started_ok),
outcome: Arc::clone(&outcome),
issued: false,
})
})
});
let summary = scheduler.run_until_idle();
assert!(
summary.waiting.contains(&pid),
"the handler parked pending the async completion"
);
assert!(
!summary.exited.contains(&pid),
"the handler did NOT exit/return a value yet"
);
assert!(scheduler.waiting.contains(&pid), "process is parked");
assert_eq!(
*started_ok.lock().expect("started lock"),
Some(true),
"start_async succeeded through the installed host facility"
);
assert_eq!(
*started.borrow(),
vec![(ASYNC_MFA, 1usize)],
"the SAME async-NIF seam fired once with the handler's MFA and one arg"
);
for _ in 0..3 {
let summary = scheduler.run_until_idle();
assert!(
!summary.exited.contains(&pid),
"no completion => the handler stays suspended across turns"
);
}
assert_eq!(
*outcome.lock().expect("outcome lock"),
None,
"the handler has observed no result before the host completes the op"
);
assert!(
scheduler.waiting.contains(&pid),
"still parked before completion"
);
let completed = scheduler.complete_async(
pid,
WasmAsyncCompletion::Ok(crate::ets::OwnedTerm::immediate(Term::small_int(5))),
);
assert!(completed, "complete_async wakes the parked native process");
assert!(
drain_run_until_idle(&mut scheduler, pid, 4),
"the handler resumes and exits after the completion is delivered"
);
assert_eq!(
scheduler.native_exit_reason(pid),
Some(ExitReason::Normal),
"the resumed handler stopped normally"
);
assert_eq!(
*outcome.lock().expect("outcome lock"),
Some((true, 5)),
"the handler resumed and observed the async result as {{ok, 5}}"
);
}
#[test]
fn native_async_rejection_is_delivered_as_error_completion() {
let mut scheduler = scheduler();
let started = Rc::new(RefCell::new(Vec::new()));
let facility: Rc<dyn WasmAsyncNifFacility> = Rc::new(FakeHost {
started: Rc::clone(&started),
});
scheduler.set_wasm_async_nif_facility(Some(facility));
let started_ok = Arc::new(Mutex::new(None));
let outcome = Arc::new(Mutex::new(None));
let pid = scheduler.spawn_native_root({
let started_ok = Arc::clone(&started_ok);
let outcome = Arc::clone(&outcome);
Box::new(move || {
Box::new(AsyncCaller {
started_ok: Arc::clone(&started_ok),
outcome: Arc::clone(&outcome),
issued: false,
})
})
});
let summary = scheduler.run_until_idle();
assert!(summary.waiting.contains(&pid), "handler parked on the op");
let completed = scheduler.complete_async(
pid,
WasmAsyncCompletion::Error(crate::ets::OwnedTerm::immediate(Term::small_int(7))),
);
assert!(completed, "complete_async wakes the parked native process");
assert!(
drain_run_until_idle(&mut scheduler, pid, 4),
"the handler resumes after the rejection is delivered"
);
assert_eq!(
*outcome.lock().expect("outcome lock"),
Some((false, 7)),
"the handler observed the rejection as {{error, 7}}"
);
}
#[test]
fn start_async_without_facility_errors_and_does_not_park() {
let mut scheduler = scheduler();
let started_ok = Arc::new(Mutex::new(None));
let outcome = Arc::new(Mutex::new(None));
let _pid = scheduler.spawn_native_root({
let started_ok = Arc::clone(&started_ok);
let outcome = Arc::clone(&outcome);
Box::new(move || {
Box::new(AsyncCaller {
started_ok: Arc::clone(&started_ok),
outcome: Arc::clone(&outcome),
issued: false,
})
})
});
let _summary = scheduler.run_until_idle();
assert_eq!(
*started_ok.lock().expect("started lock"),
Some(false),
"start_async fails closed when no WasmAsyncNifFacility is installed"
);
}
#[test]
fn has_pending_work_tracks_ready_processes_then_clears_when_drained() {
let mut scheduler = scheduler();
assert!(
!scheduler.has_pending_work(),
"a fresh scheduler has no pending work"
);
let collector = Arc::new(Mutex::new(None));
let sink = Arc::clone(&collector);
let collector_pid = scheduler.spawn_native_root(Box::new(move || {
Box::new(Collector {
sink: Arc::clone(&sink),
})
}));
let echo = scheduler.spawn_native_root({
Box::new(move || {
Box::new(Echo {
reply_to: collector_pid,
})
})
});
assert!(
scheduler.has_pending_work(),
"freshly spawned runnable actors are pending work"
);
assert!(scheduler.send(echo, Term::small_int(7)));
assert!(
drain_until_exit(&mut scheduler, echo, 8),
"the echo actor runs and exits"
);
for _ in 0..4 {
if !scheduler.has_pending_work() {
break;
}
let _summary = scheduler.run_until_idle();
}
assert_eq!(
*collector.lock().expect("collector lock"),
Some(7),
"the collector received the echoed reply"
);
assert!(
!scheduler.has_pending_work(),
"a scheduler whose processes are parked-waiting with no armed timer is idle"
);
}
#[test]
fn has_pending_work_is_true_while_a_native_deliver_timer_is_armed() {
let mut scheduler = scheduler();
let sink = Arc::new(Mutex::new(None));
let delay = std::time::Duration::from_secs(10);
let ticker = scheduler.spawn_native_root({
let sink = Arc::clone(&sink);
Box::new(move || {
Box::new(SelfTicker {
delay,
tick_value: 99,
sink: Arc::clone(&sink),
armed: false,
})
})
});
let _exited = scheduler.run_native_until_idle();
assert!(
scheduler.has_pending_work(),
"an armed Deliver timer keeps the scheduler pending even with an empty ready queue"
);
let start = web_time::Instant::now();
let woken = scheduler.tick_native_timers_at(start + delay + std::time::Duration::from_secs(5));
assert_eq!(woken, vec![ticker], "the expired self-tick wakes the actor");
assert!(
drain_until_exit(&mut scheduler, ticker, 4),
"the woken actor runs and exits"
);
assert!(
!scheduler.has_pending_work(),
"once the timer has fired and the actor exited, the scheduler is idle"
);
}
struct Yielder {
remaining: u32,
}
impl NativeHandler for Yielder {
fn handle(&mut self, _ctx: &mut NativeContext<'_>) -> NativeOutcome {
if self.remaining == 0 {
return NativeOutcome::Stop(ExitReason::Normal);
}
self.remaining -= 1;
NativeOutcome::Continue
}
}
struct OrderRecorder {
id: i64,
order: Arc<Mutex<Vec<i64>>>,
}
impl NativeHandler for OrderRecorder {
fn handle(&mut self, _ctx: &mut NativeContext<'_>) -> NativeOutcome {
if let Ok(mut guard) = self.order.lock() {
guard.push(self.id);
}
NativeOutcome::Stop(ExitReason::Normal)
}
}
#[test]
fn run_until_idle_drains_ready_queue_in_priority_order() {
use crate::process::Priority;
let mut scheduler = scheduler();
let order = Arc::new(Mutex::new(Vec::new()));
let spawn_recorder = |scheduler: &mut WasmScheduler, id: i64| -> u64 {
let order = Arc::clone(&order);
scheduler.spawn_native_root(Box::new(move || {
Box::new(OrderRecorder {
id,
order: Arc::clone(&order),
})
}))
};
let low = spawn_recorder(&mut scheduler, 1);
let normal = spawn_recorder(&mut scheduler, 2);
let high = spawn_recorder(&mut scheduler, 3);
let max = spawn_recorder(&mut scheduler, 4);
while scheduler.ready.pop().is_some() {}
for (pid, priority) in [
(low, Priority::Low),
(normal, Priority::Normal),
(high, Priority::High),
(max, Priority::Max),
] {
scheduler
.processes
.get_mut(&pid)
.expect("spawned process is retained")
.set_priority(priority);
scheduler.ready.push(pid, priority);
}
let summary = scheduler.run_until_idle();
assert_eq!(
summary.exited.len(),
4,
"all four recorders exit in one turn"
);
assert_eq!(
*order.lock().expect("order lock"),
vec![4, 3, 2, 1],
"ready queue drains Max > High > Normal > Low"
);
}
#[test]
fn run_until_idle_summary_counts_executed_and_yielded_across_reyields() {
let mut scheduler = scheduler();
let pid = scheduler.spawn_native_root(Box::new(|| Box::new(Yielder { remaining: 2 })));
let turn1 = scheduler.run_until_idle();
assert_eq!(turn1.executed, 1, "exactly one slice ran in turn 1");
assert_eq!(turn1.yielded, vec![pid], "the yielding slice is reported");
assert!(
turn1.exited.is_empty(),
"nothing exits while still yielding"
);
assert!(
scheduler.has_pending_work(),
"a re-queued yielder leaves pending work"
);
let turn2 = scheduler.run_until_idle();
assert_eq!(turn2.executed, 1);
assert_eq!(turn2.yielded, vec![pid]);
assert!(turn2.exited.is_empty());
let turn3 = scheduler.run_until_idle();
assert_eq!(
turn3.executed, 1,
"the final slice still counts as executed"
);
assert!(turn3.yielded.is_empty(), "the final slice does not yield");
assert_eq!(turn3.exited, vec![pid], "the final slice exits");
assert!(
!scheduler.has_pending_work(),
"no ready work and no armed timer once the yielder exits"
);
}
#[test]
fn run_until_idle_exit_results_and_pending_work_consistent_after_mid_round_exit() {
let mut scheduler = scheduler();
let stopper = scheduler.spawn_native_root(Box::new(|| Box::new(Yielder { remaining: 0 })));
let survivor = scheduler.spawn_native_root(Box::new(|| Box::new(Yielder { remaining: 5 })));
let summary = scheduler.run_until_idle();
assert!(
summary.exited.contains(&stopper),
"the zero-remaining actor exits this turn"
);
assert!(
!summary.exited.contains(&survivor),
"the still-yielding actor does not exit this turn"
);
let recorded: Vec<u64> = scheduler
.exit_results()
.into_iter()
.map(|(pid, _term)| pid)
.collect();
assert!(
recorded.contains(&stopper),
"exit_results records the exited native actor"
);
assert!(
!recorded.contains(&survivor),
"exit_results does not record a live actor"
);
assert!(
scheduler.has_pending_work(),
"the re-queued survivor keeps the scheduler pending"
);
assert!(
scheduler.take_exit_result(stopper).is_some(),
"the exited actor's captured result is retrievable"
);
assert!(
scheduler.take_exit_result(stopper).is_none(),
"a result is taken at most once"
);
}
#[test]
fn run_until_idle_interleaves_native_timer_delivery_with_a_ready_process() {
let mut scheduler = scheduler();
let sink = Arc::new(Mutex::new(None));
let delay = std::time::Duration::from_secs(10);
let ticker = scheduler.spawn_native_root({
let sink = Arc::clone(&sink);
Box::new(move || {
Box::new(SelfTicker {
delay,
tick_value: 77,
sink: Arc::clone(&sink),
armed: false,
})
})
});
let _arm = scheduler.run_until_idle();
assert!(
scheduler.has_pending_work(),
"the armed Deliver timer keeps work pending"
);
let start = web_time::Instant::now();
let woken = scheduler.tick_native_timers_at(start + delay + std::time::Duration::from_secs(5));
assert_eq!(woken, vec![ticker], "the due self-tick wakes the ticker");
let yielder = scheduler.spawn_native_root(Box::new(|| Box::new(Yielder { remaining: 0 })));
let summary = scheduler.run_until_idle();
assert!(
summary.exited.contains(&yielder),
"the ready yielder runs and exits in the interleaved turn"
);
assert!(
summary.exited.contains(&ticker),
"the timer-woken ticker also runs and exits in the same turn"
);
assert_eq!(
*sink.lock().expect("sink lock"),
Some(77),
"the interleaved timer delivery reached the ticker's mailbox"
);
assert!(
!scheduler.has_pending_work(),
"with both actors exited and no armed timer, the scheduler is idle"
);
}