test: Cycle 8 — off-by-one boundary, spawn-stop lifecycle, 3-hop + 10-hop chain, memory.grow exhaust
Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski
This commit is contained in:
parent
2806227376
commit
8ae620ebbb
1 changed files with 181 additions and 0 deletions
|
|
@ -1555,3 +1555,184 @@ fn exact_fit_allocation_at_memory_boundary_succeeds() {
|
|||
rt.send_to(addr, ByteMessage(vec![1u8; 10])).unwrap();
|
||||
rt.tick();
|
||||
}
|
||||
|
||||
// ── Off-by-one: alloc returns exactly memory size ────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn alloc_returns_exactly_memory_size_drops_message() {
|
||||
// alloc returns 65536 (exactly the size of 1-page memory).
|
||||
// Any non-zero length message means end > mem.len(), so it should be dropped.
|
||||
// For a zero-length message, ptr=65536, end=65536, which equals mem.len()
|
||||
// so end > mem.len() is false — that path technically works (no-op write).
|
||||
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 65536 ;; exactly at memory boundary
|
||||
)
|
||||
(func (export "handle") (param i32 i32))
|
||||
)
|
||||
"#;
|
||||
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 addr = rt.spawn(actor).unwrap();
|
||||
|
||||
// Non-zero message: end = 65536 + 5 = 65541 > 65536 → dropped
|
||||
rt.send_to(addr, ByteMessage(vec![0u8; 5])).unwrap();
|
||||
rt.tick();
|
||||
|
||||
// Actor survives
|
||||
rt.send_to(addr, ByteMessage(vec![1u8; 5])).unwrap();
|
||||
rt.tick();
|
||||
}
|
||||
|
||||
// ── Lifecycle: spawn and immediately stop without processing messages ────────
|
||||
|
||||
#[test]
|
||||
fn spawn_and_stop_without_messages_is_clean() {
|
||||
// WASM actor spawned, immediately stopped, never processes a message.
|
||||
// The wasmtime Store should be dropped cleanly.
|
||||
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();
|
||||
|
||||
// Stop immediately, no messages sent
|
||||
rt.stop_actor(addr).unwrap();
|
||||
rt.tick(); // process stop
|
||||
rt.tick(); // cleanup_dead
|
||||
|
||||
// Actor is gone
|
||||
let result = rt.send_to(addr, ByteMessage(vec![1]));
|
||||
assert!(result.is_err(), "stopped actor should reject messages");
|
||||
}
|
||||
|
||||
// ── 3-hop relay: WASM A → WASM B → WASM C → inbox ──────────────────────────
|
||||
|
||||
#[test]
|
||||
fn three_hop_wasm_relay_delivers_final_payload() {
|
||||
// Three echo actors in sequence: A echoes to B, B echoes to C, C echoes to inbox.
|
||||
let engine = SharedEngine::new().unwrap();
|
||||
let wasm_bytes = guest_wasm("echo");
|
||||
|
||||
let actor_a = WasmActorBuilder::new(engine.clone(), wasm_bytes.clone()).build().unwrap();
|
||||
let actor_b = WasmActorBuilder::new(engine.clone(), wasm_bytes.clone()).build().unwrap();
|
||||
let actor_c = WasmActorBuilder::new(engine, wasm_bytes).build().unwrap();
|
||||
|
||||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
let inbox = rt.new_inbox::<ByteMessage>().unwrap();
|
||||
|
||||
let addr_a = rt.spawn(actor_a).unwrap();
|
||||
let addr_b = rt.spawn(actor_b).unwrap();
|
||||
let addr_c = rt.spawn(actor_c).unwrap();
|
||||
|
||||
// Build nested framed message: A sends to B, B sends to C, C sends to inbox
|
||||
let final_payload = b"3-hops";
|
||||
let msg_for_c = framed_msg(inbox.addr(), final_payload);
|
||||
let msg_for_b = framed_msg(&addr_c, &msg_for_c.0);
|
||||
let msg_for_a = framed_msg(&addr_b, &msg_for_b.0);
|
||||
|
||||
rt.send_to(addr_a, msg_for_a).unwrap();
|
||||
rt.tick(); // A → B
|
||||
rt.tick(); // B → C
|
||||
rt.tick(); // C → inbox
|
||||
|
||||
let received = inbox.try_recv().expect("3-hop relay should deliver");
|
||||
assert_eq!(received.0, final_payload);
|
||||
}
|
||||
|
||||
// ── memory.grow exhaustion: guest grows until failure ────────────────────────
|
||||
|
||||
#[test]
|
||||
fn memory_grow_until_failure_actor_survives() {
|
||||
// Guest calls memory.grow repeatedly until it returns -1 (failure).
|
||||
// The actor should survive and the send should still work using
|
||||
// memory from before the failed grow.
|
||||
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
|
||||
)
|
||||
(func (export "handle") (param $ptr i32) (param $len i32)
|
||||
(local $result i32)
|
||||
;; Grow memory repeatedly until failure
|
||||
(block $done
|
||||
(loop $grow
|
||||
(local.set $result (memory.grow (i32.const 100)))
|
||||
(br_if $done (i32.eq (local.get $result) (i32.const -1)))
|
||||
(br $grow)
|
||||
)
|
||||
)
|
||||
;; After grow failure, write marker and send from original page
|
||||
(i32.store8 (i32.const 200) (i32.const 77))
|
||||
local.get $ptr ;; dest_ptr
|
||||
i32.const 200 ;; payload_ptr
|
||||
i32.const 1 ;; 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();
|
||||
|
||||
rt.send_to(addr, framed_msg(inbox.addr(), b"grow-exhaust")).unwrap();
|
||||
rt.tick();
|
||||
|
||||
let received = inbox.try_recv().expect("actor should work after grow exhaustion");
|
||||
assert_eq!(received.0, vec![77]);
|
||||
}
|
||||
|
||||
// ── Stress: many WASM actors in a chain ──────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn ten_wasm_actors_chain_relay() {
|
||||
// 10 echo actors in a chain: actor[0]→actor[1]→...→actor[9]→inbox.
|
||||
// Tests that many WASM actors coexist and messages propagate through them.
|
||||
let engine = SharedEngine::new().unwrap();
|
||||
let wasm_bytes = guest_wasm("echo");
|
||||
|
||||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
let inbox = rt.new_inbox::<ByteMessage>().unwrap();
|
||||
|
||||
let mut addrs = Vec::new();
|
||||
for _ in 0..10 {
|
||||
let actor = WasmActorBuilder::new(engine.clone(), wasm_bytes.clone())
|
||||
.build()
|
||||
.unwrap();
|
||||
addrs.push(rt.spawn(actor).unwrap());
|
||||
}
|
||||
|
||||
// Build nested framed message from the inside out:
|
||||
// actor[9] receives [inbox_addr | final_payload] → echoes final_payload to inbox
|
||||
// actor[8] receives [addr[9] | msg_for_9] → echoes msg_for_9 to actor[9]
|
||||
// ...
|
||||
// actor[0] receives [addr[1] | msg_for_1] → echoes msg_for_1 to actor[1]
|
||||
let final_payload = b"chain-10";
|
||||
let mut msg = framed_msg(inbox.addr(), final_payload);
|
||||
for addr in addrs[1..].iter().rev() {
|
||||
msg = framed_msg(addr, &msg.0);
|
||||
}
|
||||
|
||||
rt.send_to(addrs[0], msg).unwrap();
|
||||
for _ in 0..10 {
|
||||
rt.tick();
|
||||
}
|
||||
|
||||
let received = inbox.try_recv().expect("10-actor chain should deliver");
|
||||
assert_eq!(received.0, final_payload);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue