diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index 8a67afc..96fe7c6 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -13090,4 +13090,129 @@ fn echo_exactly_thirty_three_bytes() { let resp = inbox.try_recv().expect("single byte payload echo"); assert_eq!(resp.0, vec![0x42]); +} + +// ── Cycle 91 ───────────────────────────────────────────────────────────────── + +// Guest that doubles each byte value (saturating at 255) +#[test] +fn guest_doubles_each_byte_saturating() { + let wat = r#"(module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (func (export "alloc") (param $len i32) (result i32) i32.const 1024) + (func (export "handle") (param $ptr i32) (param $len i32) + (local $i i32) + (local $val i32) + (if (i32.lt_u (local.get $len) (i32.const 33)) (then return)) + ;; Double each byte in payload, saturate at 255 + (local.set $i (i32.const 32)) + (block $exit + (loop $loop + (br_if $exit (i32.ge_u (local.get $i) (local.get $len))) + (local.set $val + (i32.mul (i32.load8_u (i32.add (local.get $ptr) (local.get $i))) (i32.const 2))) + (if (i32.gt_u (local.get $val) (i32.const 255)) + (then (local.set $val (i32.const 255))) + ) + (i32.store8 + (i32.add (local.get $ptr) (local.get $i)) + (local.get $val)) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br $loop) + ) + ) + (call $send (local.get $ptr) + (i32.add (local.get $ptr) (i32.const 32)) + (i32.sub (local.get $len) (i32.const 32))) + ) + )"#; + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, wat::parse_str(wat).unwrap()) + .build().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + let addr = rt.spawn(actor).unwrap(); + + rt.send_to(addr, framed_msg(inbox.addr(), &[10, 100, 200])).unwrap(); + rt.tick(); + + let resp = inbox.try_recv().expect("doubled response"); + assert_eq!(resp.0, vec![20, 200, 255], "200*2=400 capped at 255"); +} + +// 20 echo actors on a 2-thread runtime +#[test] +fn twenty_echo_actors_two_threads() { + let engine = SharedEngine::new().unwrap(); + let mut cfg = RuntimeConfig::default(); + cfg.num_threads = 2; + let rt = Runtime::new(cfg); + let inbox = rt.new_inbox::().unwrap(); + + for i in 0..20u8 { + let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); + let addr = rt.spawn(actor).unwrap(); + rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); + } + + let handle = rt.run().unwrap(); + let mut total = 0; + for _ in 0..100 { + total += std::iter::from_fn(|| inbox.try_recv()).count(); + if total >= 20 { break; } + std::thread::sleep(std::time::Duration::from_millis(50)); + } + handle.shutdown(); + assert_eq!(total, 20, "all 20 actors echoed on 2-thread runtime"); +} + +// Guest with 5 pages memory — alloc at various offsets +#[test] +fn guest_five_page_memory_with_spread_alloc() { + let wat = r#"(module + (memory (export "memory") 5) + (global $next (mut i32) (i32.const 65536)) + (func (export "alloc") (param $len i32) (result i32) + (local $ptr i32) + (local.set $ptr (global.get $next)) + (global.set $next (i32.add (global.get $next) (local.get $len))) + (local.get $ptr) + ) + (func (export "handle") (param $ptr i32) (param $len i32)) + )"#; + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, wat::parse_str(wat).unwrap()) + .build().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let addr = rt.spawn(actor).unwrap(); + + // Alloc starts at page 2, spreading across pages + for _ in 0..20 { + rt.send_to(addr, ByteMessage(vec![0; 5000])).unwrap(); + } + rt.tick(); +} + +// 100KB payload too large for 1-page echo guest — message dropped gracefully +#[test] +fn hundred_kb_payload_dropped_for_one_page_guest() { + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + let addr = rt.spawn(actor).unwrap(); + + let payload: Vec = (0..100_000u32).map(|i| (i % 251) as u8).collect(); + rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); + rt.tick(); + + // Echo guest has 1 page (64KB) — 100KB payload can't be allocated + // Message should be dropped gracefully (OOB bounds check) + assert!(inbox.try_recv().is_none(), "100KB too large for 1-page guest"); + + // Actor should still be alive — send a small message + rt.send_to(addr, framed_msg(inbox.addr(), b"alive")).unwrap(); + rt.tick(); + assert_eq!(inbox.try_recv().unwrap().0, b"alive"); } \ No newline at end of file