diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index 41915e0..4a1e99e 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -8501,4 +8501,181 @@ fn empty_tick_between_sends_preserves_order() { let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); assert_eq!(msgs, vec![b"A".to_vec(), b"B".to_vec(), b"C".to_vec()]); +} + +// ── Guest reverses payload bytes ──────────────────────────────────────────── + +#[test] +fn guest_reverses_payload() { + // Guest reverses the payload bytes and sends the result back. + 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 4096) + (func (export "handle") (param $ptr i32) (param $len i32) + (local $i i32) + (local $payload_start i32) + (local $payload_len i32) + (local.set $payload_start (i32.add (local.get $ptr) (i32.const 32))) + (local.set $payload_len (i32.sub (local.get $len) (i32.const 32))) + (local.set $i (i32.const 0)) + (block $break + (loop $loop + (br_if $break (i32.ge_u (local.get $i) (local.get $payload_len))) + ;; response[i] = payload[payload_len - 1 - i] + (i32.store8 + (i32.add (i32.const 200) (local.get $i)) + (i32.load8_u + (i32.add + (local.get $payload_start) + (i32.sub + (i32.sub (local.get $payload_len) (i32.const 1)) + (local.get $i) + ) + ) + ) + ) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br $loop) + ) + ) + (call $send (local.get $ptr) (i32.const 200) (local.get $payload_len)) + ) + ) + "#; + 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::().unwrap(); + let addr = rt.spawn(actor).unwrap(); + + rt.send_to(addr, framed_msg(inbox.addr(), b"ABCDE")).unwrap(); + rt.tick(); + + let msg = inbox.try_recv().unwrap(); + assert_eq!(msg.0, b"EDCBA"); +} + +// ── Guest with multiple exports beyond required ones ──────────────────────── + +#[test] +fn module_with_extra_exports_works() { + 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 4096) + (func (export "handle") (param i32 i32) + ;; silent + ) + (func (export "extra_fn_1") (result i32) i32.const 0) + (func (export "extra_fn_2") (param i32) (result i32) local.get 0) + (global (export "extra_global") i32 (i32.const 42)) + ) + "#; + 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![])).unwrap(); + rt.tick(); + // No panic = extra exports are tolerated +} + +// ── 250th test: comprehensive property combining all guest types ──────────── + +proptest! { + #[test] + fn prop_comprehensive_guest_fuzz( + guest_idx in 0usize..3, + n_msgs in 1u8..15, + payload_len in 0usize..64, + do_stop in proptest::bool::ANY, + ) { + let guests = ["echo", "double", "silent"]; + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, guest_wasm(guests[guest_idx])).build().unwrap(); + + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + let addr = rt.spawn(actor).unwrap(); + + let payload = vec![0x55u8; payload_len]; + for _ in 0..n_msgs { + let _ = rt.send_to(addr, framed_msg(inbox.addr(), &payload)); + } + rt.tick(); + rt.tick(); + + if do_stop { + rt.stop_actor(addr); + rt.tick(); + } + + while let Some(_) = inbox.try_recv() {} + } +} + +// ── Spawn echo, stop, respawn echo — address is different ─────────────────── + +#[test] +fn respawned_actor_gets_different_address() { + let engine = SharedEngine::new().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + + let a1 = rt.spawn(WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap()).unwrap(); + rt.stop_actor(a1); + rt.tick(); + rt.tick(); + + let a2 = rt.spawn(WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap()).unwrap(); + + // Addresses should differ (random UUIDs) + assert_ne!(a1, a2, "respawned actor should get a different address"); +} + +// ── Double guest: echo + double on same message to separate inboxes ───────── + +#[test] +fn echo_and_double_to_separate_inboxes() { + let engine = SharedEngine::new().unwrap(); + let echo = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); + let double = WasmActorBuilder::new(engine, guest_wasm("double")).build().unwrap(); + + let rt = Runtime::new(RuntimeConfig::default()); + let inbox_e = rt.new_inbox::().unwrap(); + let inbox_d = rt.new_inbox::().unwrap(); + let e_addr = rt.spawn(echo).unwrap(); + let d_addr = rt.spawn(double).unwrap(); + + rt.send_to(e_addr, framed_msg(inbox_e.addr(), b"X")).unwrap(); + rt.send_to(d_addr, framed_msg(inbox_d.addr(), b"Y")).unwrap(); + rt.tick(); + + let e_msgs: Vec<_> = std::iter::from_fn(|| inbox_e.try_recv().map(|m| m.0)).collect(); + let d_msgs: Vec<_> = std::iter::from_fn(|| inbox_d.try_recv().map(|m| m.0)).collect(); + + assert_eq!(e_msgs, vec![b"X".to_vec()], "echo sends 1 copy"); + assert_eq!(d_msgs, vec![b"Y".to_vec(), b"Y".to_vec()], "double sends 2 copies"); +} + +// ── WasmActorBuilder::new accepts &[u8] via Into> ────────────────── + +#[test] +fn builder_accepts_slice_reference() { + let engine = SharedEngine::new().unwrap(); + let wasm_bytes = guest_wasm("silent"); + let slice: &[u8] = &wasm_bytes; + // Into> should accept &[u8] via to_vec() + let actor = WasmActorBuilder::new(engine, slice.to_vec()).build().unwrap(); + + let rt = Runtime::new(RuntimeConfig::default()); + let addr = rt.spawn(actor).unwrap(); + rt.send_to(addr, ByteMessage(vec![])).unwrap(); + rt.tick(); } \ No newline at end of file