bin-runner #36
1 changed files with 185 additions and 0 deletions
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Reference in a new issue