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:
Claude 2026-02-13 09:02:26 +00:00
parent c4a40953ca
commit a6391816aa

View file

@ -2059,3 +2059,173 @@ fn alloc_that_grows_memory_works() {
let received = inbox.try_recv().expect("alloc in grown region should work"); let received = inbox.try_recv().expect("alloc in grown region should work");
assert_eq!(received.0, payload); 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}");
}
}