bin-runner #36

Merged
zacheryasc merged 103 commits from bin-runner into master 2026-02-13 14:11:40 +00:00
Showing only changes of commit 945f64ca0b - Show all commits

View file

@ -5693,3 +5693,153 @@ fn br_table_dispatch_in_handle() {
assert_eq!(msg1.0, b"O");
assert_eq!(msg2.0, b"D");
}
// ── Two WASM actors watching each other — one stops, other gets notified ────
#[test]
fn mutual_watch_wasm_actors() {
let engine = SharedEngine::new().unwrap();
let echo1 = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap();
let echo2 = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap();
let exit_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let exit_count_clone = exit_count.clone();
struct WatchAndCount {
target: Option<ActorAddress>,
count: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
impl ActorInterface for WatchAndCount {
type Incoming = ByteMessage;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: ByteMessage) {
if let Some(t) = self.target.take() {
ctx.watch(t);
}
}
fn on_actor_exit(&mut self, _ctx: &Ctx, _exited: swactor::actor::ActorExited) {
self.count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
}
let rt = Runtime::new(RuntimeConfig::default());
let a1 = rt.spawn(echo1).unwrap();
let a2 = rt.spawn(echo2).unwrap();
// Spawn a watcher that watches a1
let watcher = WatchAndCount {
target: Some(a1),
count: exit_count_clone,
};
let watcher_addr = rt.spawn(watcher).unwrap();
// Trigger the watcher to install the watch
rt.send_to(watcher_addr, ByteMessage(vec![])).unwrap();
rt.tick();
// Stop a1 — watcher should be notified
rt.stop_actor(a1);
rt.tick();
rt.tick(); // death notification propagates
assert_eq!(
exit_count.load(std::sync::atomic::Ordering::SeqCst), 1,
"watcher should be notified when watched WASM actor stops"
);
// a2 should still be alive and functional
let inbox = rt.new_inbox::<ByteMessage>().unwrap();
rt.send_to(a2, framed_msg(inbox.addr(), b"alive")).unwrap();
rt.tick();
let msg = inbox.try_recv().expect("a2 should still be alive");
assert_eq!(msg.0, b"alive");
}
// ── Module with select instruction (ternary operator) ───────────────────────
#[test]
fn select_instruction_in_handle() {
// Guest uses `select` to choose between two values based on condition.
let wat = r#"
(module
(import "swactor" "send" (func $send (param i32 i32 i32)))
(memory (export "memory") 1)
(func (export "alloc") (param i32) (result i32) i32.const 4096)
(func (export "handle") (param $ptr i32) (param $len i32)
;; If payload > 32 bytes (has actual content), use 'Y', else 'N'
(i32.store8 (i32.const 200)
(select
(i32.const 89) ;; 'Y' (true branch)
(i32.const 78) ;; 'N' (false branch)
(i32.gt_s (local.get $len) (i32.const 32))
)
)
(call $send (local.get $ptr) (i32.const 200) (i32.const 1))
)
)
"#;
let wasm = wat::parse_str(wat).unwrap();
let engine = SharedEngine::new().unwrap();
let actor = WasmActorBuilder::new(engine, wasm).build().unwrap();
let rt = Runtime::new(RuntimeConfig::default());
let inbox = rt.new_inbox::<ByteMessage>().unwrap();
let addr = rt.spawn(actor).unwrap();
// Message with payload (len > 32)
rt.send_to(addr, framed_msg(inbox.addr(), b"content")).unwrap();
// Message without payload (len == 32)
rt.send_to(addr, framed_msg(inbox.addr(), b"")).unwrap();
rt.tick();
let msg1 = inbox.try_recv().unwrap();
let msg2 = inbox.try_recv().unwrap();
assert_eq!(msg1.0, b"Y", "message with content should select Y");
assert_eq!(msg2.0, b"N", "message without content should select N");
}
// ── WASM actor echoes to another WASM actor which echoes to native inbox ────
#[test]
fn wasm_to_wasm_to_native_relay() {
// echo1 → echo2 → inbox. Three-layer relay.
let engine = SharedEngine::new().unwrap();
let echo1 = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap();
let echo2 = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap();
let rt = Runtime::new(RuntimeConfig::default());
let inbox = rt.new_inbox::<ByteMessage>().unwrap();
let addr2 = rt.spawn(echo2).unwrap();
let addr1 = rt.spawn(echo1).unwrap();
// Send to echo1 with dest=echo2. Payload is a framed message for echo2→inbox.
let inner_payload = framed_msg(inbox.addr(), b"relay-data");
rt.send_to(addr1, framed_msg(&addr2, &inner_payload.0)).unwrap();
rt.tick(); // echo1 sends to echo2
rt.tick(); // echo2 sends to inbox
let msg = inbox.try_recv().expect("should receive relayed message");
assert_eq!(msg.0, b"relay-data");
}
// ── Property: any byte pattern in dest address never panics ─────────────────
proptest! {
#[test]
fn prop_any_dest_address_bytes_never_panic(
addr_bytes in proptest::collection::vec(proptest::num::u8::ANY, 32..=32),
payload in proptest::collection::vec(proptest::num::u8::ANY, 0..64),
) {
let engine = SharedEngine::new().unwrap();
let actor = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap();
let rt = Runtime::new(RuntimeConfig::default());
let addr = rt.spawn(actor).unwrap();
// Construct a message with arbitrary 32-byte dest address + payload
let mut msg_bytes = addr_bytes;
msg_bytes.extend_from_slice(&payload);
rt.send_to(addr, ByteMessage(msg_bytes)).unwrap();
rt.tick();
// No panic = success. Message either delivers or is silently dropped.
}
}