use std::sync::Arc;
use std::sync::mpsc;
use std::time::Duration;
use beamr::atom::{Atom, AtomTable};
use beamr::ets::OwnedTerm;
use beamr::loader::load_module;
use beamr::module::ModuleRegistry;
use beamr::native::BifRegistryImpl;
use beamr::process::ExitReason;
use beamr::scheduler::{Scheduler, SchedulerConfig};
use beamr::term::Term;
const DEADLINE: Duration = Duration::from_secs(5);
fn start(atoms: &AtomTable) -> Scheduler {
let bifs = BifRegistryImpl::new();
let registry = Arc::new(ModuleRegistry::new());
load_module(
include_bytes!("fixtures/fc_probe.beam"),
atoms,
®istry,
&bifs,
)
.expect("fc_probe fixture loads");
Scheduler::new(
SchedulerConfig {
thread_count: Some(1),
..SchedulerConfig::default()
},
Arc::clone(®istry),
)
.expect("scheduler starts")
}
fn run_bounded(
scheduler: &Arc<Scheduler>,
function: Atom,
arg: Term,
pid_slot: &mut u64,
) -> Option<(ExitReason, OwnedTerm)> {
let module = scheduler_module();
let pid = scheduler.spawn(module, function, vec![arg]).expect("spawn");
*pid_slot = pid;
let (tx, rx) = mpsc::channel();
let sched = Arc::clone(scheduler);
std::thread::spawn(move || {
let _ = tx.send(sched.run_until_exit(pid));
});
rx.recv_timeout(DEADLINE).ok()
}
fn scheduler_module() -> Atom {
AtomTable::with_common_atoms().intern("fc_probe")
}
#[test]
fn no_matching_clause_raises_function_clause_instead_of_looping() {
let atoms = AtomTable::with_common_atoms();
let scheduler = Arc::new(start(&atoms));
let probe = atoms.intern("probe");
let b = Term::atom(atoms.intern("b"));
let mut pid = 0;
let (reason, _result) = run_bounded(&scheduler, probe, b, &mut pid).unwrap_or_else(|| {
panic!("probe(b) did not terminate within {DEADLINE:?} — func_info looped (pre-fix defect)")
});
let exception = scheduler.take_exit_exception(pid);
scheduler.shutdown();
assert_eq!(
reason,
ExitReason::Error,
"an unmatched function clause is process-fatal"
);
let exception = exception.expect("function_clause must surface as an exit exception");
assert_eq!(
exception.view().class,
Term::atom(Atom::ERROR),
"class must be error"
);
assert_eq!(
exception.view().reason,
Term::atom(Atom::FUNCTION_CLAUSE),
"reason must be the function_clause atom"
);
}
#[test]
fn function_clause_is_catchable_in_loaded_bytecode() {
let atoms = AtomTable::with_common_atoms();
let scheduler = Arc::new(start(&atoms));
let caught_fn = atoms.intern("caught");
let b = Term::atom(atoms.intern("b"));
let caught_atom = atoms.intern("caught");
let mut pid = 0;
let (reason, result) = run_bounded(&scheduler, caught_fn, b, &mut pid).unwrap_or_else(|| {
panic!("caught(b) did not terminate within {DEADLINE:?} (pre-fix defect)")
});
scheduler.shutdown();
assert_eq!(
reason,
ExitReason::Normal,
"the bytecode try/catch handles function_clause and exits normally"
);
assert_eq!(
result.root(),
Term::atom(caught_atom),
"`try probe(b) catch error:function_clause -> caught` must observe the raise \
and return `caught` — Rust-side surfacing alone is not evidence"
);
}