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 148edb9ac5 - Show all commits

View file

@ -740,6 +740,191 @@ fn native_actor_communicates_with_wasm_actor() {
assert_eq!(received.0, b"from native");
}
// ── Stale outbox: sends before trap leak into next handle ─────────────────────
#[test]
#[ignore] // BUG: outbox not cleared on handle trap — stale entries leak into next call
fn outbox_entries_from_trapped_handle_do_not_leak_into_next_call() {
// A guest that calls swactor.send() successfully, then traps.
// The outbox contains the send from before the trap.
// On the next handle call (which succeeds without sending), the stale
// outbox entry should NOT be delivered.
//
// Counter incremented BEFORE the if-branch so it persists past the trap.
let wat = r#"
(module
(import "swactor" "send" (func $send (param i32 i32 i32)))
(memory (export "memory") 1)
(global $counter (mut i32) (i32.const 0))
(func (export "alloc") (param i32) (result i32)
i32.const 256
)
(func (export "handle") (param $ptr i32) (param $len i32)
;; Increment counter first (survives trap)
global.get $counter
i32.const 1
i32.add
global.set $counter
;; If counter was 0 (now 1): send then trap
global.get $counter
i32.const 1
i32.eq
if
local.get $ptr ;; dest_ptr (first 32 bytes = inbox address)
i32.const 32 ;; payload_ptr
i32.const 1 ;; payload_len
call $send
unreachable ;; trap after send
end
;; counter > 1: do nothing (no send)
)
)
"#;
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();
// First message: guest sends to outbox then traps — stale entry in outbox
// Use framed_msg so the first 32 bytes are the inbox address
rt.send_to(addr, framed_msg(inbox.addr(), b"x")).unwrap();
rt.tick();
// No message should have been delivered (handle trapped before outbox drain)
assert!(inbox.try_recv().is_none(), "trapped handle should not deliver messages");
// Second message: guest does nothing (counter=2, no send, no trap).
// If the outbox wasn't cleared, the stale entry would be drained here.
rt.send_to(addr, framed_msg(inbox.addr(), b"y")).unwrap();
rt.tick();
// Should still be empty — the stale outbox entry must not leak
assert!(
inbox.try_recv().is_none(),
"stale outbox entry from trapped call should not leak into next handle"
);
}
// ── Builder validation: invalid WASM bytes ───────────────────────────────────
#[test]
fn invalid_wasm_bytes_returns_wasmtime_error() {
let garbage = vec![0u8, 1, 2, 3]; // not valid wasm
let engine = SharedEngine::new().unwrap();
let result = WasmActorBuilder::new(engine, garbage).build();
match result {
Err(WasmActorError::Wasmtime(_)) => {} // expected — compilation failure
Err(other) => panic!("expected Wasmtime error for invalid bytes, got: {other}"),
Ok(_) => panic!("should reject invalid wasm bytes"),
}
}
// ── Guest sends zero-length payload ──────────────────────────────────────────
#[test]
fn guest_send_with_zero_length_payload_delivers_empty_message() {
// Guest calls swactor.send with payload_len=0. This should produce
// a ByteMessage(vec![]) at the destination.
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 256 ;; valid allocation
)
(func (export "handle") (param $ptr i32) (param $len i32)
;; Send with the first 32 bytes as dest, zero-length payload
local.get $ptr
i32.const 32 ;; payload_ptr (doesn't matter, len is 0)
i32.const 0 ;; payload_len
call $send
)
)
"#;
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();
// Build a framed message with the inbox address as the first 32 bytes
let msg = framed_msg(inbox.addr(), b"ignored-payload");
rt.send_to(addr, msg).unwrap();
rt.tick();
let received = inbox.try_recv().expect("should receive zero-length message");
assert!(received.0.is_empty(), "payload should be empty");
}
// ── Multiple sequential traps: actor survives repeated failures ──────────────
#[test]
fn actor_survives_multiple_sequential_traps() {
// After 3 consecutive traps, the actor should still be alive and
// able to process a non-trapping message.
let wat = r#"
(module
(import "swactor" "send" (func $send (param i32 i32 i32)))
(memory (export "memory") 1)
(global $counter (mut i32) (i32.const 0))
(func (export "alloc") (param i32) (result i32)
i32.const 256
)
(func (export "handle") (param $ptr i32) (param $len i32)
global.get $counter
i32.const 3
i32.lt_u
if
;; First 3 calls: trap
global.get $counter
i32.const 1
i32.add
global.set $counter
unreachable
end
;; 4th+ call: echo the message back using first 32 bytes as dest
local.get $ptr
local.get $ptr
i32.const 32
i32.add
local.get $len
i32.const 32
i32.sub
call $send
)
)
"#;
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();
// 3 trapping messages
for _ in 0..3 {
rt.send_to(addr, framed_msg(inbox.addr(), b"will trap")).unwrap();
rt.tick();
assert!(inbox.try_recv().is_none(), "trapped call should produce nothing");
}
// 4th message: should succeed
let payload = b"survived";
rt.send_to(addr, framed_msg(inbox.addr(), payload)).unwrap();
rt.tick();
let received = inbox.try_recv().expect("actor should work after multiple traps");
assert_eq!(received.0, payload);
}
// ── Multi-worker: WASM actors across threads ─────────────────────────────────
#[test]