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");
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue