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.send_to(addr, ByteMessage(vec![1u8; 10])).unwrap();
|
||||||
rt.tick();
|
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