test: Cycle 11 — message budget fairness, data segments, outbox isolation, combined fuzz
Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski
This commit is contained in:
parent
c4a40953ca
commit
a6391816aa
1 changed files with 170 additions and 0 deletions
|
|
@ -2059,3 +2059,173 @@ fn alloc_that_grows_memory_works() {
|
|||
let received = inbox.try_recv().expect("alloc in grown region should work");
|
||||
assert_eq!(received.0, payload);
|
||||
}
|
||||
|
||||
// ── Message budget fairness: WASM actor processes only its budget ────────────
|
||||
|
||||
#[test]
|
||||
fn wasm_actor_respects_message_budget() {
|
||||
// With actor_message_budget=2, sending 5 messages should process at most
|
||||
// 2 per tick. This verifies the budget applies to WASM actors too.
|
||||
let engine = SharedEngine::new().unwrap();
|
||||
let actor = WasmActorBuilder::new(engine, guest_wasm("echo"))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let config = RuntimeConfig {
|
||||
actor_message_budget: 2,
|
||||
..RuntimeConfig::default()
|
||||
};
|
||||
let rt = Runtime::new(config);
|
||||
let inbox = rt.new_inbox::<ByteMessage>().unwrap();
|
||||
let addr = rt.spawn(actor).unwrap();
|
||||
|
||||
// Send 5 messages
|
||||
for i in 0u8..5 {
|
||||
rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap();
|
||||
}
|
||||
|
||||
// First tick: should process at most 2
|
||||
rt.tick();
|
||||
let mut count_tick1 = 0;
|
||||
while inbox.try_recv().is_some() {
|
||||
count_tick1 += 1;
|
||||
}
|
||||
assert_eq!(count_tick1, 2, "first tick should process exactly budget=2 messages");
|
||||
|
||||
// Second tick: another 2
|
||||
rt.tick();
|
||||
let mut count_tick2 = 0;
|
||||
while inbox.try_recv().is_some() {
|
||||
count_tick2 += 1;
|
||||
}
|
||||
assert_eq!(count_tick2, 2, "second tick should process next 2 messages");
|
||||
|
||||
// Third tick: remaining 1
|
||||
rt.tick();
|
||||
let mut count_tick3 = 0;
|
||||
while inbox.try_recv().is_some() {
|
||||
count_tick3 += 1;
|
||||
}
|
||||
assert_eq!(count_tick3, 1, "third tick should process remaining 1 message");
|
||||
}
|
||||
|
||||
// ── Data segment: guest module with pre-initialized memory ──────────────────
|
||||
|
||||
#[test]
|
||||
fn guest_with_data_segment_handles_messages_correctly() {
|
||||
// A guest module with a data segment that pre-fills bytes at offset 0.
|
||||
// The host writes the incoming message starting at the alloc pointer (256),
|
||||
// which shouldn't conflict with the data segment.
|
||||
let wat = r#"
|
||||
(module
|
||||
(import "swactor" "send" (func $send (param i32 i32 i32)))
|
||||
(memory (export "memory") 1)
|
||||
;; Pre-fill offset 200-203 with "DATA"
|
||||
(data (i32.const 200) "DATA")
|
||||
|
||||
(func (export "alloc") (param i32) (result i32)
|
||||
i32.const 256 ;; alloc above data segment
|
||||
)
|
||||
(func (export "handle") (param $ptr i32) (param $len i32)
|
||||
;; Send the pre-initialized data as payload
|
||||
local.get $ptr ;; dest_ptr
|
||||
i32.const 200 ;; payload_ptr (data segment)
|
||||
i32.const 4 ;; 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"trigger")).unwrap();
|
||||
rt.tick();
|
||||
|
||||
let received = inbox.try_recv().expect("should receive data segment content");
|
||||
assert_eq!(received.0, b"DATA");
|
||||
}
|
||||
|
||||
// ── Outbox isolation: two actors' outboxes don't interfere ──────────────────
|
||||
|
||||
#[test]
|
||||
fn two_wasm_actors_outboxes_are_isolated() {
|
||||
// Two WASM actors process messages in the same tick. Their outbox
|
||||
// entries should not mix. Each Store has its own HostState.
|
||||
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, wasm_bytes).build().unwrap();
|
||||
|
||||
let rt = Runtime::new(RuntimeConfig::default());
|
||||
let inbox_a = rt.new_inbox::<ByteMessage>().unwrap();
|
||||
let inbox_b = rt.new_inbox::<ByteMessage>().unwrap();
|
||||
|
||||
let addr_a = rt.spawn(actor_a).unwrap();
|
||||
let addr_b = rt.spawn(actor_b).unwrap();
|
||||
|
||||
// Send to both in same tick
|
||||
rt.send_to(addr_a, framed_msg(inbox_a.addr(), b"msg-A")).unwrap();
|
||||
rt.send_to(addr_b, framed_msg(inbox_b.addr(), b"msg-B")).unwrap();
|
||||
rt.tick();
|
||||
|
||||
let recv_a = inbox_a.try_recv().expect("actor A should echo");
|
||||
let recv_b = inbox_b.try_recv().expect("actor B should echo");
|
||||
assert_eq!(recv_a.0, b"msg-A");
|
||||
assert_eq!(recv_b.0, b"msg-B");
|
||||
|
||||
// No cross-contamination
|
||||
assert!(inbox_a.try_recv().is_none(), "inbox_a should have exactly 1 message");
|
||||
assert!(inbox_b.try_recv().is_none(), "inbox_b should have exactly 1 message");
|
||||
}
|
||||
|
||||
// ── Property: combined alloc + handle stress never crashes ──────────────────
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn prop_random_module_behavior_never_crashes(
|
||||
alloc_val in -100i32..70000,
|
||||
trap_handle in proptest::bool::ANY,
|
||||
send_before_trap in proptest::bool::ANY,
|
||||
) {
|
||||
// Fuzz the module behavior: random alloc return, optional trap in handle,
|
||||
// optional send before the trap. The actor must never be poisoned.
|
||||
let trap_code = if trap_handle { "unreachable" } else { "" };
|
||||
let send_code = if send_before_trap {
|
||||
"local.get $ptr i32.const 32 i32.const 1 call $send"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let wat = format!(r#"
|
||||
(module
|
||||
(import "swactor" "send" (func $send (param i32 i32 i32)))
|
||||
(memory (export "memory") 1)
|
||||
(func (export "alloc") (param i32) (result i32)
|
||||
i32.const {alloc_val}
|
||||
)
|
||||
(func (export "handle") (param $ptr i32) (param $len i32)
|
||||
{send_code}
|
||||
{trap_code}
|
||||
)
|
||||
)
|
||||
"#);
|
||||
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();
|
||||
|
||||
rt.send_to(addr, ByteMessage(vec![0u8; 64])).unwrap();
|
||||
rt.tick();
|
||||
|
||||
let result = rt.send_to(addr, ByteMessage(vec![1]));
|
||||
prop_assert!(result.is_ok(), "actor must survive: alloc={alloc_val} trap={trap_handle} send_before={send_before_trap}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue