From a76237fe574fbedcdfc832248ca964628381936e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 21:07:54 +0700 Subject: [PATCH] fix: consolidate tests --- crates/wasm-actor/tests/wasm_actor.rs | 13988 ++---------------------- 1 file changed, 1008 insertions(+), 12980 deletions(-) diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index 042af85..866c1d1 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -20,3123 +20,682 @@ fn framed_msg(dest: &ActorAddress, payload: &[u8]) -> ByteMessage { ByteMessage(buf) } -// ── Echo: send bytes in, same bytes come back ──────────────────────────────── - -#[test] -fn echo_returns_same_payload() { - 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 = b"hello wasm"; - rt.send_to(addr, framed_msg(inbox.addr(), payload)).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("inbox should have a message"); - assert_eq!(received.0, payload); -} - -#[test] -fn echo_preserves_binary_payload() { - 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..=255).collect(); - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("inbox should have a message"); - assert_eq!(received.0, payload); -} - -// ── Silent: processes messages without sending anything ─────────────────────── - -#[test] -fn silent_produces_no_output() { - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("silent")) - .build() - .unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - rt.send_to(addr, ByteMessage(b"ignored".to_vec())).unwrap(); - rt.tick(); - - assert!(inbox.try_recv().is_none(), "silent guest should not send anything"); -} - -// ── Double: one message in, two messages out ───────────────────────────────── - -#[test] -fn double_sends_two_copies() { - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("double")) - .build() - .unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - let payload = b"dup me"; - rt.send_to(addr, framed_msg(inbox.addr(), payload)).unwrap(); - rt.tick(); - - let first = inbox.try_recv().expect("should receive first copy"); - let second = inbox.try_recv().expect("should receive second copy"); - assert_eq!(first.0, payload); - assert_eq!(second.0, payload); - assert!(inbox.try_recv().is_none(), "exactly two messages expected"); -} - -// ── Missing export → WasmActorError::MissingExport ─────────────────────────── - -#[test] -fn missing_alloc_export_returns_error() { - // Minimal valid Wasm module: (module) — no exports at all - let minimal_wasm = wat::parse_str("(module)").unwrap(); - let engine = SharedEngine::new().unwrap(); - let result = WasmActorBuilder::new(engine, minimal_wasm).build(); - match result { - Err(WasmActorError::MissingExport(name)) => { - assert!( - name == "memory" || name == "alloc", - "expected missing memory or alloc, got: {name}" - ); - } - Err(other) => panic!("expected MissingExport, got: {other}"), - Ok(_) => panic!("expected error for module with no exports"), - } -} - -// ── Engine sharing: two actors from the same engine ────────────────────────── - -#[test] -fn shared_engine_serves_multiple_actors() { - let engine = SharedEngine::new().unwrap(); - - let echo = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")) - .build() - .unwrap(); - let silent = WasmActorBuilder::new(engine, guest_wasm("silent")) - .build() - .unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let echo_addr = rt.spawn(echo).unwrap(); - let _silent_addr = rt.spawn(silent).unwrap(); - - let payload = b"shared engine test"; - rt.send_to(echo_addr, framed_msg(inbox.addr(), payload)).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("echo actor should still work"); - assert_eq!(received.0, payload); -} - -// ── Safety: edge cases that previously caused panics or corruption ──────────── - -#[test] -fn oob_send_traps_cleanly_and_actor_survives() { - // Guest calls swactor.send with dest_ptr pointing past the end of memory. - // The host should trap the call; the actor should survive for future messages. - let wat = r#" - (module +/// Build a WAT module where alloc returns a constant value. +fn alloc_returns_wat(alloc_val: i32) -> Vec { + 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 0 ;; return start of memory (simplistic) - ) - (func (export "handle") (param i32 i32) - ;; Call send with dest_ptr = 65536 (1 page = end of memory, OOB for 32 bytes) - i32.const 65536 - i32.const 0 - i32.const 0 - 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::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - // Send a message — handle will try OOB send, which traps - rt.send_to(addr, ByteMessage(vec![42])).unwrap(); - rt.tick(); - - // No message should arrive (the send was invalid) - assert!(inbox.try_recv().is_none(), "OOB send should not produce a message"); -} - -#[test] -fn alloc_oom_drops_message_actor_stays_alive() { - // Guest alloc always returns 0 (OOM). Message should be dropped, - // actor should remain alive for subsequent messages. - 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 0 ;; always OOM - ) - (func (export "handle") (param i32 i32) - ;; Should never be called if alloc returned 0 for non-zero 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 addr = rt.spawn(actor).unwrap(); - - // Send a non-empty message — alloc returns 0, message should be dropped - rt.send_to(addr, ByteMessage(vec![1, 2, 3])).unwrap(); - rt.tick(); - - // Actor is still alive — send another message, tick again (no panic) - rt.send_to(addr, ByteMessage(vec![4, 5, 6])).unwrap(); - rt.tick(); -} - -#[test] -fn negative_alloc_ptr_drops_message() { - // Guest alloc returns -1. Host should detect the negative pointer and drop. - 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 -1 ;; invalid negative pointer - ) + (func (export "alloc") (param i32) (result i32) i32.const {alloc_val}) (func (export "handle") (param i32 i32)) - ) - "#; - 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![1])).unwrap(); - rt.tick(); // should not panic - - // Actor survives - rt.send_to(addr, ByteMessage(vec![2])).unwrap(); - rt.tick(); -} - -#[test] -fn handle_trap_drops_message_actor_survives() { - // Guest handle executes `unreachable`, causing a Wasm trap. - // Message should be dropped, actor should stay alive. - 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 256 ;; valid allocation - ) - (func (export "handle") (param i32 i32) - unreachable ;; trap! - ) - ) - "#; - 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![1, 2, 3])).unwrap(); - rt.tick(); // handle traps, but actor should survive - - // Actor is still alive - rt.send_to(addr, ByteMessage(vec![4, 5, 6])).unwrap(); - rt.tick(); -} - -// ── Bounds safety: alloc pointer near end of linear memory ──────────────────── - -#[test] -fn alloc_near_end_of_memory_drops_message_actor_survives() { - // Guest alloc returns 65500 (near end of 1-page / 65536-byte memory). - // A 100-byte message means ptr+len = 65600, which exceeds memory bounds. - // The actor should drop the message and survive — same as any other - // allocation failure — rather than being permanently killed. - 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 65500 ;; near end of 64KiB memory - ) - (func (export "handle") (param i32 i32) - ;; should never be reached if bounds check works - ) - ) - "#; - 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(); - - // Send a message whose length exceeds the remaining space at ptr 65500 - rt.send_to(addr, ByteMessage(vec![0u8; 100])).unwrap(); - rt.tick(); - - // The actor should still be alive — send another message and tick without panic - rt.send_to(addr, ByteMessage(vec![1u8; 10])).unwrap(); - rt.tick(); -} - -// ── Edge cases: empty and oversized messages ───────────────────────────────── - -#[test] -fn empty_message_is_handled_without_crash() { - // A zero-length ByteMessage should pass through the alloc/handle pipeline - // without crashing. The echo guest returns nothing (len < 32 guard). - 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(); - - rt.send_to(addr, ByteMessage(vec![])).unwrap(); - rt.tick(); - - // Echo guest does `if len < 32 { return; }` — so no reply expected - assert!(inbox.try_recv().is_none(), "empty message should produce no reply"); - - // Actor survives — can still process a real message - let payload = b"still alive"; - rt.send_to(addr, framed_msg(inbox.addr(), payload)).unwrap(); - rt.tick(); - let received = inbox.try_recv().expect("actor should still be alive"); - assert_eq!(received.0, payload); -} - -#[test] -fn message_larger_than_linear_memory_is_dropped() { - // A message of 65537 bytes exceeds the 1-page (64KiB) guest memory. - // Guest alloc will OOM (return 0) → message dropped, actor survives. - 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(); - - rt.send_to(addr, ByteMessage(vec![0u8; 65537])).unwrap(); - rt.tick(); - - assert!(inbox.try_recv().is_none(), "oversized message should be dropped"); - - // Actor survives - let payload = b"after oversize"; - rt.send_to(addr, framed_msg(inbox.addr(), payload)).unwrap(); - rt.tick(); - let received = inbox.try_recv().expect("actor should survive oversized message"); - assert_eq!(received.0, payload); -} - -// ── Sustained load: bump allocator exhaustion ──────────────────────────────── - -#[test] -fn sequential_messages_degrade_gracefully_after_allocator_exhaustion() { - // The echo guest has a 64KiB bump allocator that never frees. Under - // sustained load, alloc eventually returns 0 (OOM) and messages are - // silently dropped. The actor must survive throughout — no panics, - // no poisoning. - 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 = b"ping"; - let mut echoed = 0usize; - - // Send enough messages to exhaust the 64KiB heap. - // Each framed message is 36 bytes (32 addr + 4 payload), aligned to 40. - // 65536 / 40 = ~1638, but heap offset within memory varies. Send 2000. - for _ in 0..2000 { - rt.send_to(addr, framed_msg(inbox.addr(), payload)).unwrap(); - rt.tick(); - if inbox.try_recv().is_some() { - echoed += 1; - } - } - - // Some messages were echoed before OOM - assert!(echoed > 0, "should echo at least some messages"); - // After OOM, messages were dropped — so not all 2000 echoed - assert!(echoed < 2000, "allocator should exhaust before 2000 messages"); -} - -// ── Fire-and-forget: guest sends to nonexistent address ────────────────────── - -#[test] -fn guest_send_to_nonexistent_address_is_silently_dropped() { - // Guest sends to an all-zero 32-byte address that isn't registered in - // the runtime. The ctx.send() error is silently dropped (fire-and-forget). - // Actor must survive. - 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 0 - ) - (func (export "handle") (param i32 i32) - ;; send to address at offset 0 (all zeros — no such actor) - ;; with 1-byte payload at offset 32 - i32.const 0 ;; dest_ptr - i32.const 32 ;; payload_ptr - i32.const 1 ;; 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 addr = rt.spawn(actor).unwrap(); - - rt.send_to(addr, ByteMessage(vec![42])).unwrap(); - rt.tick(); // guest sends to nonexistent address — should not panic - - // Actor survives - rt.send_to(addr, ByteMessage(vec![99])).unwrap(); - rt.tick(); -} - -// ── Builder validation: wrong export signatures ────────────────────────────── - -#[test] -fn wrong_handle_signature_is_rejected() { - // Module exports `handle` with wrong signature: (i32) -> i32 instead of (i32, i32) -> () - // Builder maps get_typed_func errors to MissingExport (signature mismatch = not found). - let wat = r#" - (module - (memory (export "memory") 1) - (func (export "alloc") (param i32) (result i32) i32.const 0) - (func (export "handle") (param i32) (result i32) i32.const 0) - ) - "#; - let wasm = wat::parse_str(wat).unwrap(); - let engine = SharedEngine::new().unwrap(); - let result = WasmActorBuilder::new(engine, wasm).build(); - assert!(result.is_err(), "should reject wrong handle signature"); -} - -#[test] -fn wrong_memory_export_name_returns_missing_export() { - // Module has a memory, but exported as "mem" instead of "memory" - let wat = r#" - (module - (memory (export "mem") 1) - (func (export "alloc") (param i32) (result i32) i32.const 0) - (func (export "handle") (param i32 i32)) - ) - "#; - let wasm = wat::parse_str(wat).unwrap(); - let engine = SharedEngine::new().unwrap(); - let result = WasmActorBuilder::new(engine, wasm).build(); - match result { - Err(WasmActorError::MissingExport("memory")) => {} // expected - Err(other) => panic!("expected MissingExport(\"memory\"), got: {other}"), - Ok(_) => panic!("should reject module without 'memory' export"), - } -} - -// ── Lifecycle: graceful stop of WASM actor ─────────────────────────────────── - -#[test] -fn graceful_stop_cleans_up_wasm_actor() { - // After stopping a WASM actor, it should be removed from the runtime. - // The wasmtime Store is dropped cleanly (no leak, no crash). - 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(); - - // Verify the actor works - let payload = b"before stop"; - rt.send_to(addr, framed_msg(inbox.addr(), payload)).unwrap(); - rt.tick(); - let received = inbox.try_recv().expect("actor should echo before stop"); - assert_eq!(received.0, payload); - - // Stop the actor - rt.stop_actor(addr).unwrap(); - rt.tick(); // process stop signal - rt.tick(); // cleanup_dead phase - - // Actor is gone — send should fail - let result = rt.send_to(addr, ByteMessage(vec![1])); - assert!(result.is_err(), "send to stopped actor should fail"); -} - -// ── Host import validation: negative payload_len ───────────────────────────── - -#[test] -fn negative_payload_len_in_send_traps_actor_survives() { - // Guest calls swactor.send with payload_len = -1. The host import - // should trap (negative argument check), and the actor should survive. - 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 256 - ) - (func (export "handle") (param i32 i32) - i32.const 0 ;; dest_ptr - i32.const 0 ;; payload_ptr - i32.const -1 ;; payload_len (negative!) - 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 addr = rt.spawn(actor).unwrap(); - - rt.send_to(addr, ByteMessage(vec![1, 2, 3])).unwrap(); - rt.tick(); // guest calls send with negative len — should trap - - // Actor survives - rt.send_to(addr, ByteMessage(vec![4, 5, 6])).unwrap(); - rt.tick(); -} - -// ── Independent stores: two echo actors from same engine + bytes ───────────── - -#[test] -fn two_echo_actors_from_same_engine_are_independent() { - 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::().unwrap(); - let inbox_b = rt.new_inbox::().unwrap(); - - let addr_a = rt.spawn(actor_a).unwrap(); - let addr_b = rt.spawn(actor_b).unwrap(); - - // Send different payloads to each - rt.send_to(addr_a, framed_msg(inbox_a.addr(), b"for-a")).unwrap(); - rt.send_to(addr_b, framed_msg(inbox_b.addr(), b"for-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"for-a"); - assert_eq!(recv_b.0, b"for-b"); - - // Cross-check: no leakage between actors - assert!(inbox_a.try_recv().is_none()); - assert!(inbox_b.try_recv().is_none()); -} - -// ── Watch integration: native watcher observes WASM actor death ────────────── - -struct ExitWatcher { - exit_count: std::sync::Arc, - last_reason: std::sync::Arc>>, -} - -#[derive(Clone)] -struct WatchThis(ActorAddress); - -impl ActorInterface for ExitWatcher { - type Incoming = WatchThis; - type Response = (); - fn handle(&mut self, ctx: &Ctx, msg: WatchThis) { - ctx.watch(msg.0); - } - fn on_actor_exit(&mut self, _ctx: &Ctx, exited: swactor::actor::ActorExited) { - self.exit_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - *self.last_reason.lock().unwrap() = Some(exited.reason); - } -} - -#[test] -fn native_watcher_notified_when_wasm_actor_stops() { - // A native actor watches a WASM actor. When the WASM actor is stopped, - // the watcher should receive ActorExited with ExitReason::Stopped. - let engine = SharedEngine::new().unwrap(); - let wasm = WasmActorBuilder::new(engine, guest_wasm("echo")) - .build() - .unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - - let exit_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); - let last_reason = std::sync::Arc::new(std::sync::Mutex::new(None)); - let watcher = ExitWatcher { - exit_count: exit_count.clone(), - last_reason: last_reason.clone(), - }; - - let wasm_addr = rt.spawn(wasm).unwrap(); - let watcher_addr = rt.spawn(watcher).unwrap(); - - // Tell watcher to watch the WASM actor - rt.send_to(watcher_addr, WatchThis(wasm_addr)).unwrap(); - for _ in 0..3 { rt.tick(); } - - // Stop the WASM actor - rt.stop_actor(wasm_addr).unwrap(); - for _ in 0..5 { rt.tick(); } - - assert_eq!(exit_count.load(std::sync::atomic::Ordering::SeqCst), 1); - assert_eq!( - *last_reason.lock().unwrap(), - Some(swactor::actor::ExitReason::Stopped) + )"# ); + wat::parse_str(&wat).unwrap() } -// ── WASM-to-WASM: two WASM actors communicating ───────────────────────────── - -#[test] -fn wasm_to_wasm_message_relay() { - // Echo A echoes to Echo B's address, Echo B echoes to an external inbox. - // This verifies the full WASM→runtime→WASM→runtime→inbox path. - 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 = rt.new_inbox::().unwrap(); - - let addr_a = rt.spawn(actor_a).unwrap(); - let addr_b = rt.spawn(actor_b).unwrap(); - - // Send to actor A: "echo your payload to actor B" - // Actor A receives [addr_b | payload_for_b] - // Actor A echoes payload_for_b to addr_b - // payload_for_b itself is [inbox_addr | final_payload] - // Actor B receives [inbox_addr | final_payload] - // Actor B echoes final_payload to inbox - let final_payload = b"relayed"; - let payload_for_b = framed_msg(inbox.addr(), final_payload); - let msg_for_a = framed_msg(&addr_b, &payload_for_b.0); - - rt.send_to(addr_a, msg_for_a).unwrap(); - rt.tick(); // A receives, echoes to B - rt.tick(); // B receives, echoes to inbox - - let received = inbox.try_recv().expect("should receive relayed message"); - assert_eq!(received.0, final_payload); -} - -// ── Integration: WasmActor alongside a native Rust actor ───────────────────── - -#[derive(Clone)] -struct ForwardToWasm { - wasm_addr: ActorAddress, - inbox_addr: ActorAddress, -} - -struct Forwarder; - -impl ActorInterface for Forwarder { - type Incoming = ForwardToWasm; - type Response = (); - - fn handle(&mut self, ctx: &Ctx, msg: ForwardToWasm) { - // Build the framed message and forward to the wasm actor - let payload = b"from native"; - let framed = framed_msg(&msg.inbox_addr, payload); - let _ = ctx.send(msg.wasm_addr, framed); - } -} - -#[test] -fn native_actor_communicates_with_wasm_actor() { - let engine = SharedEngine::new().unwrap(); - let wasm = WasmActorBuilder::new(engine, guest_wasm("echo")) - .build() - .unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let wasm_addr = rt.spawn(wasm).unwrap(); - let forwarder_addr = rt.spawn(Forwarder).unwrap(); - - rt.send_to( - forwarder_addr, - ForwardToWasm { - wasm_addr, - inbox_addr: *inbox.addr(), - }, - ) - .unwrap(); - - // Tick 1: Forwarder receives message and sends to WasmActor - rt.tick(); - // Tick 2: WasmActor receives the forwarded message and echoes to inbox - rt.tick(); - - let received = inbox.try_recv().expect("wasm actor should have echoed"); - assert_eq!(received.0, b"from native"); -} - -// ── Stale outbox: sends before trap leak into next handle ───────────────────── - -#[test] -fn outbox_entries_from_trapped_handle_do_not_leak_into_next_call() { - // A guest that calls swactor.send() successfully, then traps. - // The outbox contains the send from before the trap. - // On the next handle call (which succeeds without sending), the stale - // outbox entry should NOT be delivered. - // - // Counter incremented BEFORE the if-branch so it persists past the trap. - let wat = r#" - (module +/// Build a WAT module where handle calls send with specific arguments. +fn send_args_wat(dest_ptr: i32, payload_ptr: i32, payload_len: i32) -> Vec { + let wat = format!( + r#"(module (import "swactor" "send" (func $send (param i32 i32 i32))) (memory (export "memory") 1) - (global $counter (mut i32) (i32.const 0)) - - (func (export "alloc") (param i32) (result i32) - i32.const 256 + (func (export "alloc") (param i32) (result i32) i32.const 256) + (func (export "handle") (param i32 i32) + i32.const {dest_ptr} + i32.const {payload_ptr} + i32.const {payload_len} + call $send ) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Increment counter first (survives trap) - global.get $counter - i32.const 1 - i32.add - global.set $counter - - ;; If counter was 0 (now 1): send then trap - global.get $counter - i32.const 1 - i32.eq - if - local.get $ptr ;; dest_ptr (first 32 bytes = inbox address) - i32.const 32 ;; payload_ptr - i32.const 1 ;; payload_len - call $send - unreachable ;; trap after send - end - ;; counter > 1: do nothing (no 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::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - // First message: guest sends to outbox then traps — stale entry in outbox - // Use framed_msg so the first 32 bytes are the inbox address - rt.send_to(addr, framed_msg(inbox.addr(), b"x")).unwrap(); - rt.tick(); - - // No message should have been delivered (handle trapped before outbox drain) - assert!(inbox.try_recv().is_none(), "trapped handle should not deliver messages"); - - // Second message: guest does nothing (counter=2, no send, no trap). - // If the outbox wasn't cleared, the stale entry would be drained here. - rt.send_to(addr, framed_msg(inbox.addr(), b"y")).unwrap(); - rt.tick(); - - // Should still be empty — the stale outbox entry must not leak - assert!( - inbox.try_recv().is_none(), - "stale outbox entry from trapped call should not leak into next handle" + )"# ); + wat::parse_str(&wat).unwrap() } -// ── Builder validation: invalid WASM bytes ─────────────────────────────────── +// ═══════════════════════════════════════════════════════════════════════════════ +// Group 1: Builder contract +// ═══════════════════════════════════════════════════════════════════════════════ #[test] -fn invalid_wasm_bytes_returns_wasmtime_error() { - let garbage = vec![0u8, 1, 2, 3]; // not valid wasm +fn build_succeeds_for_valid_modules() { let engine = SharedEngine::new().unwrap(); - let result = WasmActorBuilder::new(engine, garbage).build(); - match result { - Err(WasmActorError::Wasmtime(_)) => {} // expected — compilation failure - Err(other) => panic!("expected Wasmtime error for invalid bytes, got: {other}"), - Ok(_) => panic!("should reject invalid wasm bytes"), - } + + // Pre-compiled guests + WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); + WasmActorBuilder::new(engine.clone(), guest_wasm("double")).build().unwrap(); + WasmActorBuilder::new(engine.clone(), guest_wasm("silent")).build().unwrap(); + + // Custom WAT with extra exports, data segments, funcref table + let extras = wat::parse_str(r#"(module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (data (i32.const 0) "hello") + (table 1 funcref) + (func (export "alloc") (param i32) (result i32) i32.const 256) + (func (export "handle") (param i32 i32)) + (func (export "custom_fn") (result i32) i32.const 42) + )"#).unwrap(); + WasmActorBuilder::new(engine.clone(), extras).build().unwrap(); + + // Module without send import (doesn't import swactor.send) + let no_send = wat::parse_str(r#"(module + (memory (export "memory") 1) + (func (export "alloc") (param i32) (result i32) i32.const 256) + (func (export "handle") (param i32 i32)) + )"#).unwrap(); + WasmActorBuilder::new(engine, no_send).build().unwrap(); } -// ── Guest sends zero-length payload ────────────────────────────────────────── +#[test] +fn build_rejects_missing_exports() { + let engine = SharedEngine::new().unwrap(); + + // Empty module — missing everything + let result = WasmActorBuilder::new(engine.clone(), wat::parse_str("(module)").unwrap()).build(); + let err = result.err().expect("should fail"); + let msg = format!("{err}"); + assert!(msg.contains("memory") || msg.contains("alloc"), "got: {msg}"); + + // Memory only — missing alloc + let mem_only = wat::parse_str("(module (memory (export \"memory\") 1))").unwrap(); + let err = WasmActorBuilder::new(engine.clone(), mem_only).build().err().expect("should fail"); + assert!(format!("{err}").contains("alloc")); + + // Memory + alloc — missing handle + let no_handle = wat::parse_str(r#"(module + (memory (export "memory") 1) + (func (export "alloc") (param i32) (result i32) i32.const 0) + )"#).unwrap(); + let err = WasmActorBuilder::new(engine.clone(), no_handle).build().err().expect("should fail"); + assert!(format!("{err}").contains("handle")); + + // Wrong memory export name + let wrong_mem = wat::parse_str(r#"(module + (memory (export "mem") 1) + (func (export "alloc") (param i32) (result i32) i32.const 0) + (func (export "handle") (param i32 i32)) + )"#).unwrap(); + assert!(WasmActorBuilder::new(engine.clone(), wrong_mem).build().is_err()); + + // Wrong alloc signature (two params) + let wrong_alloc = wat::parse_str(r#"(module + (memory (export "memory") 1) + (func (export "alloc") (param i32 i32) (result i32) i32.const 0) + (func (export "handle") (param i32 i32)) + )"#).unwrap(); + assert!(WasmActorBuilder::new(engine.clone(), wrong_alloc).build().is_err()); + + // Wrong handle signature (returns i32) + let wrong_handle = wat::parse_str(r#"(module + (memory (export "memory") 1) + (func (export "alloc") (param i32) (result i32) i32.const 0) + (func (export "handle") (param i32 i32) (result i32) i32.const 0) + )"#).unwrap(); + assert!(WasmActorBuilder::new(engine, wrong_handle).build().is_err()); +} #[test] -fn guest_send_with_zero_length_payload_delivers_empty_message() { - // Guest calls swactor.send with payload_len=0. This should produce - // a ByteMessage(vec![]) at the destination. - 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 256 ;; valid allocation - ) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Send with the first 32 bytes as dest, zero-length payload - local.get $ptr - i32.const 32 ;; payload_ptr (doesn't matter, len is 0) - i32.const 0 ;; payload_len - call $send - ) +fn build_rejects_invalid_wasm_and_disabled_features() { + let engine = SharedEngine::new().unwrap(); + + // Invalid bytes + assert!(WasmActorBuilder::new(engine.clone(), vec![0xDE, 0xAD]).build().is_err()); + + // Empty bytes + assert!(WasmActorBuilder::new(engine.clone(), vec![]).build().is_err()); + + // Truncated valid wasm + let valid = guest_wasm("echo"); + let truncated = valid[..valid.len() / 2].to_vec(); + assert!(WasmActorBuilder::new(engine.clone(), truncated).build().is_err()); + + // SIMD (disabled in engine config) + let simd = wat::parse_str(r#"(module + (memory (export "memory") 1) + (func (export "alloc") (param i32) (result i32) i32.const 0) + (func (export "handle") (param i32 i32) + v128.const i32x4 0 0 0 0 + drop ) - "#; - let wasm = wat::parse_str(wat).unwrap(); - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, wasm).build().unwrap(); + )"#); + assert!(simd.is_err() || WasmActorBuilder::new(engine.clone(), simd.unwrap()).build().is_err()); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - // Build a framed message with the inbox address as the first 32 bytes - let msg = framed_msg(inbox.addr(), b"ignored-payload"); - rt.send_to(addr, msg).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("should receive zero-length message"); - assert!(received.0.is_empty(), "payload should be empty"); -} - -// ── Multiple sequential traps: actor survives repeated failures ────────────── - -#[test] -fn actor_survives_multiple_sequential_traps() { - // After 3 consecutive traps, the actor should still be alive and - // able to process a non-trapping message. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (global $counter (mut i32) (i32.const 0)) - (func (export "alloc") (param i32) (result i32) - i32.const 256 - ) - (func (export "handle") (param $ptr i32) (param $len i32) - global.get $counter - i32.const 3 - i32.lt_u - if - ;; First 3 calls: trap - global.get $counter - i32.const 1 - i32.add - global.set $counter - unreachable - end - ;; 4th+ call: echo the message back using first 32 bytes as dest - local.get $ptr - local.get $ptr - i32.const 32 - i32.add - local.get $len - i32.const 32 - i32.sub - 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::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - // 3 trapping messages - for _ in 0..3 { - rt.send_to(addr, framed_msg(inbox.addr(), b"will trap")).unwrap(); - rt.tick(); - assert!(inbox.try_recv().is_none(), "trapped call should produce nothing"); - } - - // 4th message: should succeed - let payload = b"survived"; - rt.send_to(addr, framed_msg(inbox.addr(), payload)).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("actor should work after multiple traps"); - assert_eq!(received.0, payload); -} - -// ── Builder: module with start function that traps ─────────────────────────── - -#[test] -fn module_with_trapping_start_function_returns_error() { - // WASM modules can have a (start) function that runs during instantiation. - // If it traps, build() should return an error. - let wat = r#" - (module - (memory (export "memory") 1) - (func (export "alloc") (param i32) (result i32) i32.const 256) - (func (export "handle") (param i32 i32)) - (func $init unreachable) - (start $init) - ) - "#; - let wasm = wat::parse_str(wat).unwrap(); - let engine = SharedEngine::new().unwrap(); - let result = WasmActorBuilder::new(engine, wasm).build(); - assert!(result.is_err(), "module with trapping start function should fail to build"); -} - -// ── Self-send: guest sends message back to own address ─────────────────────── - -#[test] -fn guest_self_send_creates_feedback_loop() { - // Echo guest sends its payload to a destination. If we set the dest - // to the actor's OWN address, it creates a feedback loop. The actor - // should process the self-sent message on the next tick. - 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(); - - // Frame: dest=self, payload=[inbox_addr | "hello"] - // Echo will send [inbox_addr | "hello"] back to itself. - // On next tick, it receives [inbox_addr | "hello"], echoes "hello" to inbox. - let inner_msg = framed_msg(inbox.addr(), b"hello"); - let self_msg = framed_msg(&addr, &inner_msg.0); - - rt.send_to(addr, self_msg).unwrap(); - rt.tick(); // actor echoes inner_msg to self - rt.tick(); // actor receives inner_msg, echoes "hello" to inbox - - let received = inbox.try_recv().expect("should receive after self-send loop"); - assert_eq!(received.0, b"hello"); -} - -// ── Amplification: guest sends many messages in one handle ─────────────────── - -#[test] -fn guest_sending_many_messages_in_one_handle_all_delivered() { - // A guest that calls swactor.send N times in a single handle call. - // All N messages should be delivered via the outbox drain. - 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 256 - ) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Send 10 messages, each with 0-byte payload - ;; dest_ptr = $ptr (first 32 bytes of the incoming message) - (local $i i32) - (local.set $i (i32.const 0)) - (block $break - (loop $loop - (br_if $break (i32.ge_u (local.get $i) (i32.const 10))) - (call $send (local.get $ptr) (i32.const 32) (i32.const 0)) - (local.set $i (i32.add (local.get $i) (i32.const 1))) - (br $loop) - ) - ) - ) - ) - "#; - 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"x")).unwrap(); - rt.tick(); - - let mut count = 0; - while inbox.try_recv().is_some() { - count += 1; - } - assert_eq!(count, 10, "guest should have sent exactly 10 messages"); -} - -// ── Overlapping send regions: dest_ptr and payload_ptr overlap ─────────────── - -#[test] -fn overlapping_dest_and_payload_in_send_works() { - // Guest calls send with dest_ptr=0, payload_ptr=16, payload_len=32. - // The dest region [0..32] and payload region [16..48] overlap. - // Both are read-only in the host, so this should work without corruption. - 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 256 - ) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Overlapping regions - local.get $ptr ;; dest_ptr (first 32 bytes of message) - local.get $ptr - i32.const 16 - i32.add ;; payload_ptr = ptr + 16 (overlaps with dest) - i32.const 32 ;; payload_len = 32 - 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::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - // Build a message where bytes [0..32] = inbox addr, [32..] = payload - // Guest reads dest from [0..32] (inbox addr) and payload from [16..48] - rt.send_to(addr, framed_msg(inbox.addr(), b"overlap-test-padding!")).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("overlapping send should deliver"); - assert_eq!(received.0.len(), 32, "payload should be 32 bytes from overlapping region"); -} - -// ── Type mismatch: non-ByteMessage sent to WASM actor ──────────────────────── - -#[test] -fn non_byte_message_to_wasm_actor_is_silently_ignored() { - // Sending a message of the wrong type (not ByteMessage) to a WASM actor. - // The runtime's handle_any downcast fails, counting a type mismatch. - // The actor should survive and still process valid ByteMessages. - 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(); - - // Send wrong type — u32 instead of ByteMessage - // This goes through send_any with Box::new(42u32), downcast to ByteMessage fails. - rt.send_to(addr, 42u32).unwrap(); - rt.tick(); // type mismatch — silently ignored - - // Actor still alive — send a valid message - let payload = b"after mismatch"; - rt.send_to(addr, framed_msg(inbox.addr(), payload)).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("actor should work after type mismatch"); - assert_eq!(received.0, payload); -} - -// ── Guest state persistence: mutable global survives across messages ───────── - -#[test] -fn guest_mutable_state_persists_across_messages() { - // A guest module with a mutable global counter. Each handle call increments - // the counter and includes it in the reply payload. Verifies that the - // wasmtime Store and linear memory persist between handle() calls. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (global $counter (mut i32) (i32.const 0)) - - (func (export "alloc") (param i32) (result i32) - i32.const 256 - ) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Increment counter - global.get $counter + // Multi-value (disabled) + let mv = wat::parse_str(r#"(module + (memory (export "memory") 1) + (func (export "alloc") (param i32) (result i32) i32.const 0) + (func (export "handle") (param i32 i32) + (block (result i32 i32) i32.const 1 - i32.add - global.set $counter - - ;; Write counter value to memory at offset 200 - (i32.store8 (i32.const 200) (global.get $counter)) - - ;; Send counter byte as payload to dest at $ptr - local.get $ptr ;; dest_ptr (first 32 bytes of message) - i32.const 200 ;; payload_ptr (counter byte) - i32.const 1 ;; 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::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - // Send 3 messages, each should get an incrementing counter - for expected in 1..=3u8 { - rt.send_to(addr, framed_msg(inbox.addr(), b"x")).unwrap(); - rt.tick(); - let received = inbox.try_recv().expect("should receive counter reply"); - assert_eq!(received.0, vec![expected], "counter should increment per message"); - } -} - -// ── Spawn WASM from handler: native actor spawns WASM actor during handle ──── - -struct WasmSpawner { - engine: SharedEngine, - wasm_bytes: Vec, -} - -#[derive(Clone)] -struct SpawnAndForward { - inbox_addr: ActorAddress, - payload: Vec, -} - -impl ActorInterface for WasmSpawner { - type Incoming = SpawnAndForward; - type Response = (); - fn handle(&mut self, ctx: &Ctx, msg: SpawnAndForward) { - let actor = WasmActorBuilder::new(self.engine.clone(), self.wasm_bytes.clone()) - .build() - .unwrap(); - let wasm_addr = ctx.spawn(actor); - let _ = ctx.send(wasm_addr.unwrap(), framed_msg(&msg.inbox_addr, &msg.payload)); - } -} - -#[test] -fn native_handler_spawns_wasm_actor_and_forwards_message() { - let engine = SharedEngine::new().unwrap(); - let wasm_bytes = guest_wasm("echo"); - - let spawner = WasmSpawner { - engine: engine.clone(), - wasm_bytes: wasm_bytes.clone(), - }; - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let spawner_addr = rt.spawn(spawner).unwrap(); - - rt.send_to( - spawner_addr, - SpawnAndForward { - inbox_addr: *inbox.addr(), - payload: b"spawned-echo".to_vec(), - }, - ) - .unwrap(); - - // Tick 1: Spawner receives message, spawns WASM actor, sends to it - rt.tick(); - // Tick 2: WASM actor processes message and echoes to inbox - rt.tick(); - - let received = inbox.try_recv().expect("dynamically spawned WASM actor should echo"); - assert_eq!(received.0, b"spawned-echo"); -} - -// ── Bounded mailbox: WASM actor with backpressure ──────────────────────────── - -#[test] -fn bounded_mailbox_applies_to_wasm_actor() { - // With a bounded mailbox of capacity 3, sending 10 messages should - // result in only 3 being processed (DropNewest policy). - use swactor::runtime::MailboxOverflow; - - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("echo")) - .build() - .unwrap(); - - let config = RuntimeConfig { - default_mailbox_capacity: 3, - mailbox_overflow: MailboxOverflow::DropNewest, - ..RuntimeConfig::default() - }; - let rt = Runtime::new(config); - let inbox = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - // Send 10 messages before any tick — only first 3 should be kept - for i in 0u8..10 { - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - } - rt.tick(); - - let mut received = Vec::new(); - while let Some(msg) = inbox.try_recv() { - received.push(msg.0[0]); - } - - assert_eq!(received.len(), 3, "bounded mailbox should limit to 3 messages"); - // DropNewest keeps the first 3 sent - assert_eq!(received, vec![0, 1, 2]); -} - -// ── Property: alloc failures never kill the actor ──────────────────────────── - -proptest! { - #[test] - fn prop_any_alloc_return_value_never_kills_actor(alloc_val in -100i32..70000) { - // Regardless of what alloc returns (negative, zero, OOB, valid), - // sending a message should never kill the actor. - let alloc_const = format!("i32.const {alloc_val}"); - 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) - {alloc_const} - ) - (func (export "handle") (param i32 i32)) - ) - "#); - 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(); - - // Send a message — should never panic or poison - rt.send_to(addr, ByteMessage(vec![0u8; 100])).unwrap(); - rt.tick(); - - // Actor should still accept messages (not poisoned) - let result = rt.send_to(addr, ByteMessage(vec![1])); - prop_assert!(result.is_ok(), "actor should survive any alloc return value: {alloc_val}"); - } -} - -// ── Multi-worker: WASM actors across threads ───────────────────────────────── - -#[test] -fn wasm_actor_works_on_multi_worker_runtime() { - // Spawn a WASM echo actor on a 2-worker runtime and verify message - // round-trip works across threads. This is a smoke test for Send safety - // of wasmtime Store. - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("echo")) - .build() - .unwrap(); - - let config = RuntimeConfig { - num_threads: 2, - ..RuntimeConfig::default() - }; - let rt = Runtime::new(config); - let inbox = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - let payload = b"multi-worker"; - rt.send_to(addr, framed_msg(inbox.addr(), payload)).unwrap(); - - // Use run() to drive the runtime on background threads - let handle = rt.run().unwrap(); - - // Poll with retries — MT runtime timing is non-deterministic - let mut received = None; - for _ in 0..20 { - std::thread::sleep(std::time::Duration::from_millis(25)); - if let Some(msg) = inbox.try_recv() { - received = Some(msg); - break; - } - } - - let received = received.expect("wasm actor should echo on MT runtime"); - assert_eq!(received.0, payload); - - handle.shutdown(); -} - -// ── Property-based: arbitrary bytes round-trip through echo ────────────────── - -proptest! { - #[test] - fn prop_echo_roundtrips_arbitrary_bytes(payload in proptest::collection::vec(any::(), 0..500)) { - 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 msg = framed_msg(inbox.addr(), &payload); - rt.send_to(addr, msg).unwrap(); - rt.tick(); - - if payload.is_empty() { - // Echo guest: if total len < 32, no reply (32B addr + 0B payload = 32, but - // the framed message is 32 + 0 = 32 bytes, and echo checks `len < 32`) - // Actually: framed_msg produces 32 + payload.len() bytes. When payload - // is empty, total is 32, and echo checks `if len < 32 { return; }`. - // len == 32 passes the check! So dest_ptr = ptr, payload_ptr = ptr+32, - // payload_len = 0 → sends a 0-byte message. - // Let's just check: if we got something, it matches. - if let Some(received) = inbox.try_recv() { - prop_assert_eq!(received.0, payload); - } - } else { - let received = inbox.try_recv().expect("echo should return non-empty payload"); - prop_assert_eq!(received.0, payload); - } - } - - #[test] - fn prop_double_always_sends_exactly_two_copies(payload in proptest::collection::vec(any::(), 1..500)) { - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("double")) - .build() - .unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - let msg = framed_msg(inbox.addr(), &payload); - rt.send_to(addr, msg).unwrap(); - rt.tick(); - - let first = inbox.try_recv().expect("double should send first copy"); - let second = inbox.try_recv().expect("double should send second copy"); - prop_assert_eq!(&first.0, &payload); - prop_assert_eq!(&second.0, &payload); - prop_assert!(inbox.try_recv().is_none(), "exactly two messages expected"); - } -} - -// ── Alloc trap: unreachable in alloc, store must recover ───────────────────── - -#[test] -fn alloc_traps_actor_survives_and_processes_next_message() { - // Guest alloc traps on first call (counter=0), succeeds on subsequent calls. - // The store must remain in a valid state after the alloc trap. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (global $counter (mut i32) (i32.const 0)) - - (func (export "alloc") (param $size i32) (result i32) - global.get $counter - i32.const 1 - i32.add - global.set $counter - - ;; First call: trap - global.get $counter - i32.const 1 - i32.eq - if - unreachable - end - ;; Subsequent calls: return valid pointer - i32.const 256 - ) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Echo: send payload back to dest in first 32 bytes - local.get $ptr - local.get $ptr - i32.const 32 - i32.add - local.get $len - i32.const 32 - i32.sub - 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::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - // First message: alloc traps → message dropped, no reply - rt.send_to(addr, framed_msg(inbox.addr(), b"trap-in-alloc")).unwrap(); - rt.tick(); - assert!(inbox.try_recv().is_none(), "alloc trap should drop message"); - - // Second message: alloc succeeds → echo should work - let payload = b"after-alloc-trap"; - rt.send_to(addr, framed_msg(inbox.addr(), payload)).unwrap(); - rt.tick(); - let received = inbox.try_recv().expect("actor should recover after alloc trap"); - assert_eq!(received.0, payload); -} - -// ── memory.grow during handle: guest expands memory, sends from new region ─── - -#[test] -fn memory_grow_during_handle_does_not_break_actor() { - // Guest grows memory by 1 page during handle, then writes a value - // into the new region and sends it. Verifies the host's Memory - // handle tracks the new size. - 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 256 - ) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Grow memory by 1 page (64KiB → 128KiB) - (drop (memory.grow (i32.const 1))) - - ;; Write marker byte into new page (offset 65536+100 = 65636) - (i32.store8 (i32.const 65636) (i32.const 42)) - - ;; Send: dest from first 32 bytes, payload from new region - local.get $ptr ;; dest_ptr - i32.const 65636 ;; payload_ptr (in grown region) - i32.const 1 ;; 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::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - rt.send_to(addr, framed_msg(inbox.addr(), b"grow-test")).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("should receive from grown memory region"); - assert_eq!(received.0, vec![42], "payload should be the marker byte from new page"); -} - -// ── send overflow: dest_ptr near i32::MAX triggers checked_add overflow ────── - -#[test] -fn send_with_dest_ptr_overflow_traps_actor_survives() { - // Guest calls send with dest_ptr = i32::MAX (2147483647). - // The host's checked_add(32) overflows → trap. Actor should survive. - 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 256 - ) - (func (export "handle") (param i32 i32) - i32.const 2147483647 ;; dest_ptr = i32::MAX - i32.const 0 ;; payload_ptr - i32.const 0 ;; 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 addr = rt.spawn(actor).unwrap(); - - rt.send_to(addr, ByteMessage(vec![1])).unwrap(); - rt.tick(); // send traps due to overflow — actor should survive - - // Verify actor is still alive - rt.send_to(addr, ByteMessage(vec![2])).unwrap(); - rt.tick(); -} - -// ── Exact-fit allocation: ptr + len == memory size ─────────────────────────── - -#[test] -fn exact_fit_allocation_at_memory_boundary_succeeds() { - // alloc returns 65536 - 10 = 65526. With a 10-byte message, the write - // region is [65526..65536] — exactly fitting in 1 page. Should succeed. - 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 65526 ;; 65536 - 10 = exact fit for 10-byte message - ) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Just echo: send everything back. But since alloc returns - ;; 65526, the message was written to [65526..65536]. We need - ;; to send from there. Use a fixed dest from offset 0 (zeroes). - ;; Actually, the message was copied to ptr=65526 by the host. - ;; We need the first 32 bytes as dest, but our message is only - ;; 10 bytes. So handle gets (ptr=65526, len=10). With len < 32, - ;; the echo guest would skip it. Let's just verify handle was - ;; called by sending a known byte from offset 200. - (i32.store8 (i32.const 200) (i32.const 99)) - ;; We can't easily echo from this offset, but we can verify - ;; the handle was reached by using a global flag read in a - ;; subsequent call. - ) - ) - "#; - 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(); - - // Send exactly 10 bytes — fits perfectly at ptr=65526 - rt.send_to(addr, ByteMessage(vec![0u8; 10])).unwrap(); - rt.tick(); // should NOT trigger OOB — exact fit - - // Actor survives — the bounds check passed - rt.send_to(addr, ByteMessage(vec![1u8; 10])).unwrap(); - rt.tick(); -} - -// ── Off-by-one: alloc returns exactly memory size ──────────────────────────── - -#[test] -fn alloc_returns_exactly_memory_size_drops_message() { - // alloc returns 65536 (exactly the size of 1-page memory). - // Any non-zero length message means end > mem.len(), so it should be dropped. - // For a zero-length message, ptr=65536, end=65536, which equals mem.len() - // so end > mem.len() is false — that path technically works (no-op write). - 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 65536 ;; exactly at memory boundary - ) - (func (export "handle") (param i32 i32)) - ) - "#; - 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(); - - // Non-zero message: end = 65536 + 5 = 65541 > 65536 → dropped - rt.send_to(addr, ByteMessage(vec![0u8; 5])).unwrap(); - rt.tick(); - - // Actor survives - rt.send_to(addr, ByteMessage(vec![1u8; 5])).unwrap(); - rt.tick(); -} - -// ── Lifecycle: spawn and immediately stop without processing messages ──────── - -#[test] -fn spawn_and_stop_without_messages_is_clean() { - // WASM actor spawned, immediately stopped, never processes a message. - // The wasmtime Store should be dropped cleanly. - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("echo")) - .build() - .unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let addr = rt.spawn(actor).unwrap(); - - // Stop immediately, no messages sent - rt.stop_actor(addr).unwrap(); - rt.tick(); // process stop - rt.tick(); // cleanup_dead - - // Actor is gone - let result = rt.send_to(addr, ByteMessage(vec![1])); - assert!(result.is_err(), "stopped actor should reject messages"); -} - -// ── 3-hop relay: WASM A → WASM B → WASM C → inbox ────────────────────────── - -#[test] -fn three_hop_wasm_relay_delivers_final_payload() { - // Three echo actors in sequence: A echoes to B, B echoes to C, C echoes to inbox. - 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.clone(), wasm_bytes.clone()).build().unwrap(); - let actor_c = WasmActorBuilder::new(engine, wasm_bytes).build().unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let addr_a = rt.spawn(actor_a).unwrap(); - let addr_b = rt.spawn(actor_b).unwrap(); - let addr_c = rt.spawn(actor_c).unwrap(); - - // Build nested framed message: A sends to B, B sends to C, C sends to inbox - let final_payload = b"3-hops"; - let msg_for_c = framed_msg(inbox.addr(), final_payload); - let msg_for_b = framed_msg(&addr_c, &msg_for_c.0); - let msg_for_a = framed_msg(&addr_b, &msg_for_b.0); - - rt.send_to(addr_a, msg_for_a).unwrap(); - rt.tick(); // A → B - rt.tick(); // B → C - rt.tick(); // C → inbox - - let received = inbox.try_recv().expect("3-hop relay should deliver"); - assert_eq!(received.0, final_payload); -} - -// ── memory.grow exhaustion: guest grows until failure ──────────────────────── - -#[test] -fn memory_grow_until_failure_actor_survives() { - // Guest calls memory.grow repeatedly until it returns -1 (failure). - // The actor should survive and the send should still work using - // memory from before the failed grow. - 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 256 - ) - (func (export "handle") (param $ptr i32) (param $len i32) - (local $result i32) - ;; Grow memory repeatedly until failure - (block $done - (loop $grow - (local.set $result (memory.grow (i32.const 100))) - (br_if $done (i32.eq (local.get $result) (i32.const -1))) - (br $grow) - ) - ) - ;; After grow failure, write marker and send from original page - (i32.store8 (i32.const 200) (i32.const 77)) - local.get $ptr ;; dest_ptr - i32.const 200 ;; payload_ptr - i32.const 1 ;; 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::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - rt.send_to(addr, framed_msg(inbox.addr(), b"grow-exhaust")).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("actor should work after grow exhaustion"); - assert_eq!(received.0, vec![77]); -} - -// ── Stress: many WASM actors in a chain ────────────────────────────────────── - -#[test] -fn ten_wasm_actors_chain_relay() { - // 10 echo actors in a chain: actor[0]→actor[1]→...→actor[9]→inbox. - // Tests that many WASM actors coexist and messages propagate through them. - let engine = SharedEngine::new().unwrap(); - let wasm_bytes = guest_wasm("echo"); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let mut addrs = Vec::new(); - for _ in 0..10 { - let actor = WasmActorBuilder::new(engine.clone(), wasm_bytes.clone()) - .build() - .unwrap(); - addrs.push(rt.spawn(actor).unwrap()); - } - - // Build nested framed message from the inside out: - // actor[9] receives [inbox_addr | final_payload] → echoes final_payload to inbox - // actor[8] receives [addr[9] | msg_for_9] → echoes msg_for_9 to actor[9] - // ... - // actor[0] receives [addr[1] | msg_for_1] → echoes msg_for_1 to actor[1] - let final_payload = b"chain-10"; - let mut msg = framed_msg(inbox.addr(), final_payload); - for addr in addrs[1..].iter().rev() { - msg = framed_msg(addr, &msg.0); - } - - rt.send_to(addrs[0], msg).unwrap(); - for _ in 0..10 { - rt.tick(); - } - - let received = inbox.try_recv().expect("10-actor chain should deliver"); - assert_eq!(received.0, final_payload); -} - -// ── send payload overflow: payload_ptr + payload_len wraps ────────────────── - -#[test] -fn send_with_payload_range_overflow_traps_actor_survives() { - // Guest calls send with payload_ptr=1, payload_len=i32::MAX. - // checked_add(payload_len) overflows → trap. Actor must survive. - 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 256) - (func (export "handle") (param i32 i32) - i32.const 0 ;; dest_ptr (valid) - i32.const 1 ;; payload_ptr - i32.const 2147483647 ;; payload_len = i32::MAX → overflow - 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 addr = rt.spawn(actor).unwrap(); - - rt.send_to(addr, ByteMessage(vec![1])).unwrap(); - rt.tick(); - - // Actor survives — send another - rt.send_to(addr, ByteMessage(vec![2])).unwrap(); - rt.tick(); -} - -// ── send with payload at exact memory end ─────────────────────────────────── - -#[test] -fn send_payload_at_exact_memory_end_works() { - // Guest writes a byte at offset 65535 (last byte of 1-page memory) and - // sends it as a 1-byte payload. payload_end = 65535 + 1 = 65536 == mem_len. - // This should succeed (not exceed bounds). - 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 256) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Write marker at last byte - (i32.store8 (i32.const 65535) (i32.const 88)) - ;; Send: dest from message, payload = last byte of memory - local.get $ptr ;; dest_ptr - i32.const 65535 ;; payload_ptr (last byte) - i32.const 1 ;; 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::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - rt.send_to(addr, framed_msg(inbox.addr(), b"x")).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("exact-end payload should succeed"); - assert_eq!(received.0, vec![88]); -} - -// ── Multi-worker: two WASM actors cross-thread messaging ──────────────────── - -#[test] -fn wasm_actors_communicate_across_threads() { - // Two WASM echo actors on a 2-worker runtime. Actor A echoes to Actor B, - // Actor B echoes to inbox. Verifies cross-thread WASM messaging. - 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 config = RuntimeConfig { - num_threads: 2, - ..RuntimeConfig::default() - }; - let rt = Runtime::new(config); - let inbox = rt.new_inbox::().unwrap(); - - let addr_a = rt.spawn(actor_a).unwrap(); - let addr_b = rt.spawn(actor_b).unwrap(); - - // A receives [addr_b | [inbox_addr | "cross-thread"]] - // A echoes [inbox_addr | "cross-thread"] to B - // B echoes "cross-thread" to inbox - let final_payload = b"cross-thread"; - let msg_for_b = framed_msg(inbox.addr(), final_payload); - let msg_for_a = framed_msg(&addr_b, &msg_for_b.0); - - rt.send_to(addr_a, msg_for_a).unwrap(); - - let handle = rt.run().unwrap(); - - // Poll with retries — MT runtime timing is non-deterministic - let mut received = None; - for _ in 0..20 { - std::thread::sleep(std::time::Duration::from_millis(25)); - if let Some(msg) = inbox.try_recv() { - received = Some(msg); - break; - } - } - - let received = received.expect("cross-thread relay should deliver"); - assert_eq!(received.0, final_payload); - - handle.shutdown(); -} - -// ── Property: any send arguments never crash the host ──────────────────────── - -proptest! { - #[test] - fn prop_any_send_args_never_crash_host( - dest_ptr in -100i32..70000, - payload_ptr in -100i32..70000, - payload_len in -100i32..70000, - ) { - // Regardless of what arguments the guest passes to swactor.send, - // the host import should either succeed or trap — never panic. - 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 256) - (func (export "handle") (param i32 i32) - i32.const {dest_ptr} - i32.const {payload_ptr} - i32.const {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 addr = rt.spawn(actor).unwrap(); - - // Should never panic regardless of send args - rt.send_to(addr, ByteMessage(vec![0u8; 64])).unwrap(); - rt.tick(); - - // Actor should still accept messages (not poisoned) - let result = rt.send_to(addr, ByteMessage(vec![1])); - prop_assert!(result.is_ok(), "actor must survive any send args: dest={dest_ptr} payload_ptr={payload_ptr} len={payload_len}"); - } -} - -// ── Guest sends to two different destinations in one handle ────────────────── - -#[test] -fn guest_sends_to_two_destinations_both_delivered() { - // Guest calls send twice with different destinations. - // Both messages should be delivered in order. - 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 256) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; First send: dest from bytes [0..32], 1-byte payload "A" at offset 200 - (i32.store8 (i32.const 200) (i32.const 65)) ;; 'A' - local.get $ptr - i32.const 200 - i32.const 1 - call $send - - ;; Second send: dest from bytes [32..64], 1-byte payload "B" at offset 201 - (i32.store8 (i32.const 201) (i32.const 66)) ;; 'B' - local.get $ptr - i32.const 32 - i32.add ;; second dest address at offset 32 - i32.const 201 - i32.const 1 - 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_a = rt.new_inbox::().unwrap(); - let inbox_b = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - // Build message with TWO destination addresses: [inbox_a_addr | inbox_b_addr | ...] - let mut msg_bytes = Vec::new(); - msg_bytes.extend_from_slice(&inbox_a.addr().0); - msg_bytes.extend_from_slice(&inbox_b.addr().0); - msg_bytes.extend_from_slice(b"extra-padding"); - rt.send_to(addr, ByteMessage(msg_bytes)).unwrap(); - rt.tick(); - - let recv_a = inbox_a.try_recv().expect("inbox_a should receive"); - assert_eq!(recv_a.0, b"A"); - let recv_b = inbox_b.try_recv().expect("inbox_b should receive"); - assert_eq!(recv_b.0, b"B"); -} - -// ── Guest overwrites memory after send — outbox should have a copy ────────── - -#[test] -fn guest_overwriting_memory_after_send_does_not_corrupt_outbox() { - // Guest calls send (which copies data into outbox), then overwrites - // the same memory region. The outbox entry should be unaffected. - 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 256) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Write "OK" at offset 200-201 - (i32.store8 (i32.const 200) (i32.const 79)) ;; 'O' - (i32.store8 (i32.const 201) (i32.const 75)) ;; 'K' - - ;; Send payload from [200..202] - local.get $ptr - i32.const 200 i32.const 2 - call $send - - ;; Now overwrite those bytes with "XX" - (i32.store8 (i32.const 200) (i32.const 88)) ;; 'X' - (i32.store8 (i32.const 201) (i32.const 88)) ;; 'X' ) + drop drop ) - "#; - 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"x")).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("should receive original payload"); - assert_eq!(received.0, b"OK", "outbox should have copy, not overwritten data"); + )"#); + assert!(mv.is_err() || WasmActorBuilder::new(engine, mv.unwrap()).build().is_err()); } -// ── Large payload: near-capacity message through full pipeline ────────────── - #[test] -fn large_payload_near_memory_capacity() { - // Send a 60000-byte payload through the echo pipeline. This is close - // to the 64KiB memory limit. The bump allocator needs enough space. - 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(); - - // 32-byte address + payload must fit in alloc. Echo guest starts alloc - // at offset 1024, so we have 64512 bytes. 32 + payload must be ≤ 64512. - // Use a 1000-byte payload (well within limits) for a realistic large message. - let payload: Vec = (0..1000).map(|i| (i % 256) as u8).collect(); - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("large payload should echo"); - assert_eq!(received.0, payload); -} - -// ── alloc grows memory, returns pointer in new region ─────────────────────── - -#[test] -fn alloc_that_grows_memory_works() { - // alloc calls memory.grow before returning a pointer in the new region. - // The host's bounds check uses memory.data_mut() AFTER alloc returns, - // so it should see the grown memory. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) ;; starts with 1 page (65536 bytes) - - (func (export "alloc") (param $size i32) (result i32) - ;; Grow memory by 1 page, return pointer in the new region - (drop (memory.grow (i32.const 1))) - i32.const 65536 ;; start of new page - ) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Echo: send payload back to dest - local.get $ptr - local.get $ptr - i32.const 32 - i32.add - local.get $len - i32.const 32 - i32.sub - 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::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - let payload = b"grown-alloc"; - rt.send_to(addr, framed_msg(inbox.addr(), payload)).unwrap(); - rt.tick(); - - 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::().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::().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::().unwrap(); - let inbox_b = rt.new_inbox::().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}"); - } -} - -// ── Stack overflow: deep recursion in handle ──────────────────────────────── - -#[test] -fn guest_stack_overflow_traps_actor_survives() { - // Guest handle calls itself recursively until stack overflow. - // Wasmtime should trap with a stack overflow error; actor must survive. - 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 256) - (func $recurse (param $ptr i32) (param $len i32) - local.get $ptr - local.get $len - call $recurse - ) - (func (export "handle") (param $ptr i32) (param $len i32) - local.get $ptr - local.get $len - call $recurse - ) - ) - "#; - 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![1, 2, 3])).unwrap(); - rt.tick(); // stack overflow trap - - // Actor survives - rt.send_to(addr, ByteMessage(vec![4, 5, 6])).unwrap(); - rt.tick(); -} - -// ── Bulk memory: memory.fill and memory.copy ──────────────────────────────── - -#[test] -fn guest_using_bulk_memory_ops_works() { - // The engine enables bulk_memory. Guest uses memory.fill to write a - // pattern, then sends it. Verifies bulk memory operations work. - 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 256) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Fill bytes [500..510] with value 42 using memory.fill - (memory.fill (i32.const 500) (i32.const 42) (i32.const 10)) - - ;; Send 10 bytes from [500..510] - local.get $ptr - i32.const 500 - i32.const 10 - 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::().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("bulk memory fill should work"); - assert_eq!(received.0, vec![42u8; 10]); -} - -// ── Self-amplification: bounded by message budget ─────────────────────────── - -#[test] -fn self_amplification_bounded_by_budget_no_crash() { - // Guest sends 3 copies of the message back to itself. With budget=4 - // each tick processes at most 4 messages. Run for a few ticks — should - // not crash or OOM. - 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.set $i (i32.const 0)) - (block $break - (loop $loop - (br_if $break (i32.ge_u (local.get $i) (i32.const 3))) - local.get $ptr - local.get $ptr - i32.const 33 - call $send - (local.set $i (i32.add (local.get $i) (i32.const 1))) - (br $loop) - ) - ) - ) - ) - "#; - let wasm = wat::parse_str(wat).unwrap(); - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, wasm).build().unwrap(); - - let config = RuntimeConfig { - actor_message_budget: 4, - ..RuntimeConfig::default() - }; - let rt = Runtime::new(config); - let addr = rt.spawn(actor).unwrap(); - - // Initial seed: [self_addr | marker] - let mut seed = Vec::new(); - seed.extend_from_slice(&addr.0); - seed.push(0xFF); - rt.send_to(addr, ByteMessage(seed)).unwrap(); - - // Run for 5 ticks — should not crash - for _ in 0..5 { - rt.tick(); - } - - // Actor alive - rt.send_to(addr, ByteMessage(vec![0])).unwrap(); - rt.tick(); -} - -// ── Double stop: stopping an already-stopped actor ────────────────────────── - -#[test] -fn double_stop_is_idempotent() { - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("silent")) - .build() - .unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let addr = rt.spawn(actor).unwrap(); - - rt.stop_actor(addr).unwrap(); - rt.tick(); - rt.tick(); - - // Second stop should fail gracefully (not panic) - let result = rt.stop_actor(addr); - assert!(result.is_err(), "stopping already-stopped actor should error"); -} - -// ── Disabled features: SIMD module rejected by sandboxed engine ───────────── - -#[test] -fn module_using_disabled_simd_is_rejected() { - // The engine disables SIMD. A module using v128 SIMD types should - // fail to compile or instantiate. - let wat = r#" - (module - (memory (export "memory") 1) - (func (export "alloc") (param i32) (result i32) i32.const 256) - (func (export "handle") (param i32 i32) - ;; v128.const is a SIMD instruction - v128.const i32x4 0 0 0 0 - drop - ) - ) - "#; - let result = wat::parse_str(wat); - // If wat parses it, try to compile with the sandboxed engine - match result { - Ok(wasm) => { - let engine = SharedEngine::new().unwrap(); - let build_result = WasmActorBuilder::new(engine, wasm).build(); - assert!(build_result.is_err(), "SIMD module should be rejected by sandboxed engine"); - } - Err(_) => { - // wat parser itself rejects SIMD — that's also fine - } - } -} - -// ── Garbage address in send: any 32 bytes accepted ────────────────────────── - -#[test] -fn send_with_garbage_address_bytes_silently_fails() { - // Guest sends to an address that's 32 random/garbage bytes. - // The runtime can't route to it — ctx.send() returns Err, which is - // silently dropped. Actor survives. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - ;; Pre-fill offset 0-31 with garbage (0xDE repeated) - (data (i32.const 0) "\de\ad\be\ef\de\ad\be\ef\de\ad\be\ef\de\ad\be\ef\de\ad\be\ef\de\ad\be\ef\de\ad\be\ef\de\ad\be\ef") - - (func (export "alloc") (param i32) (result i32) i32.const 256) - (func (export "handle") (param i32 i32) - ;; Send to garbage address at offset 0 - i32.const 0 ;; dest_ptr (garbage address from data segment) - i32.const 32 ;; payload_ptr - i32.const 1 ;; 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 addr = rt.spawn(actor).unwrap(); - - rt.send_to(addr, ByteMessage(vec![42])).unwrap(); - rt.tick(); // send to garbage addr fails silently - - // Actor survives - rt.send_to(addr, ByteMessage(vec![99])).unwrap(); - rt.tick(); -} - -// ── Indirect call: guest uses call_indirect for handle logic ──────────────── - -#[test] -fn guest_using_call_indirect_works() { - // Guest uses a function table and call_indirect to invoke a function - // that calls send. Verifies table-based dispatch works in the sandbox. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (type $send_sig (func (param i32))) - - ;; A function that sends 1 byte from offset 200 using the dest at the param - (func $do_send (param $dest_ptr i32) - (i32.store8 (i32.const 200) (i32.const 55)) - local.get $dest_ptr - i32.const 200 - i32.const 1 - call $send - ) - - (table 1 funcref) - (elem (i32.const 0) $do_send) - - (func (export "alloc") (param i32) (result i32) i32.const 256) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Call the function at table index 0 via call_indirect - local.get $ptr - (call_indirect (type $send_sig) (i32.const 0)) - ) - ) - "#; - 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"indirect")).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("indirect call should deliver message"); - assert_eq!(received.0, vec![55]); -} - -// ── alloc returns 0 with len=0: subtle Ok(0) guard behavior ───────────────── - -#[test] -fn alloc_returns_zero_for_zero_length_message_succeeds() { - // The guard `Ok(0) if len > 0 => return` only triggers when len > 0. - // For a zero-length message, alloc returning 0 should fall through and - // handle(0, 0) should be called. This tests the subtle conditional. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (global $called (mut i32) (i32.const 0)) - - (func (export "alloc") (param i32) (result i32) - i32.const 0 ;; Always return 0 - ) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Mark that handle was called - (global.set $called (i32.const 1)) - ;; Write marker and send it - (i32.store8 (i32.const 200) (i32.const 77)) - ;; Use offset 100 as dest (will be zeroes = invalid addr, but that's fine) - i32.const 100 ;; dest_ptr (zeroes) - i32.const 200 ;; payload_ptr - i32.const 1 ;; 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 addr = rt.spawn(actor).unwrap(); - - // Send empty message — alloc returns 0, len == 0, so guard doesn't trigger. - // handle(0, 0) should be called. - rt.send_to(addr, ByteMessage(vec![])).unwrap(); - rt.tick(); - - // Then send a non-empty message — alloc returns 0, len > 0, guard triggers. - // handle should NOT be called. Actor survives. - rt.send_to(addr, ByteMessage(vec![1, 2, 3])).unwrap(); - rt.tick(); - - // Actor still alive - rt.send_to(addr, ByteMessage(vec![])).unwrap(); - rt.tick(); -} - -// ── Module with custom sections: should build successfully ────────────────── - -#[test] -fn module_with_custom_section_builds_and_works() { - // WASM modules can have custom sections. The builder should ignore them. - 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 256) - (func (export "handle") (param $ptr i32) (param $len i32) - (i32.store8 (i32.const 200) (i32.const 99)) - local.get $ptr - i32.const 200 - i32.const 1 - call $send - ) - (@custom "my_section" "hello") - ) - "#; - 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"custom")).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("module with custom section should work"); - assert_eq!(received.0, vec![99]); -} - -// ── Multiple engines: actors from different engines on same runtime ────────── - -#[test] -fn actors_from_different_engines_coexist() { - // Two actors built from separate SharedEngine instances. - // Verifies that engine isolation doesn't cause issues when actors - // share the same runtime. - let engine_a = SharedEngine::new().unwrap(); - let engine_b = SharedEngine::new().unwrap(); - let wasm_bytes = guest_wasm("echo"); - - let actor_a = WasmActorBuilder::new(engine_a, wasm_bytes.clone()).build().unwrap(); - let actor_b = WasmActorBuilder::new(engine_b, wasm_bytes).build().unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox_a = rt.new_inbox::().unwrap(); - let inbox_b = rt.new_inbox::().unwrap(); - - let addr_a = rt.spawn(actor_a).unwrap(); - let addr_b = rt.spawn(actor_b).unwrap(); - - rt.send_to(addr_a, framed_msg(inbox_a.addr(), b"engine-A")).unwrap(); - rt.send_to(addr_b, framed_msg(inbox_b.addr(), b"engine-B")).unwrap(); - rt.tick(); - - let recv_a = inbox_a.try_recv().expect("engine A actor should echo"); - let recv_b = inbox_b.try_recv().expect("engine B actor should echo"); - assert_eq!(recv_a.0, b"engine-A"); - assert_eq!(recv_b.0, b"engine-B"); -} - -// ── Error formatting: WasmActorError Display ──────────────────────────────── - -#[test] -fn error_display_formats_correctly() { +fn error_display_and_from_impls() { + // MissingExport let missing = WasmActorError::MissingExport("memory"); - assert!(missing.to_string().contains("memory")); - assert!(missing.to_string().contains("missing")); + let s = format!("{missing}"); + assert!(s.contains("memory"), "MissingExport display: {s}"); - let garbage = vec![0u8, 1, 2, 3]; - let engine = SharedEngine::new().unwrap(); - let build_result = WasmActorBuilder::new(engine, garbage).build(); - assert!(build_result.is_err()); - let wasmtime_err = build_result.err().unwrap(); - assert!(wasmtime_err.to_string().contains("wasmtime")); + // Wasmtime error via From + let wt_err = wasmtime::Error::msg("test error"); + let converted: WasmActorError = wt_err.into(); + let s = format!("{converted}"); + assert!(s.contains("test error"), "Wasmtime display: {s}"); + + // Distinct display + let missing_s = format!("{}", WasmActorError::MissingExport("alloc")); + let wt_s = format!("{}", WasmActorError::Wasmtime(wasmtime::Error::msg("x"))); + assert_ne!(missing_s, wt_s); + + // Send + Sync + fn assert_send_sync() {} + assert_send_sync::(); } -// ── Rapid lifecycle: spawn, process, stop, repeat ─────────────────────────── - -#[test] -fn rapid_spawn_process_stop_cycle() { - // Rapidly spawn, send, tick, stop, tick, repeat for 20 iterations. - // Tests that the runtime cleanly handles rapid WASM actor lifecycle. - let engine = SharedEngine::new().unwrap(); - let wasm_bytes = guest_wasm("echo"); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - for i in 0u8..20 { - let actor = WasmActorBuilder::new(engine.clone(), wasm_bytes.clone()) - .build() - .unwrap(); - let addr = rt.spawn(actor).unwrap(); - - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("actor should echo before stop"); - assert_eq!(received.0, vec![i]); - - rt.stop_actor(addr).unwrap(); - rt.tick(); - rt.tick(); // cleanup - } -} - -// ── Stop-send race: send after stop_actor but before tick ─────────────────── - -#[test] -fn send_after_stop_before_tick_is_silently_dropped() { - // Stop an actor, then immediately send a message before ticking. - // The message should be silently dropped (actor is stopping). - 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(); - - // Verify it works first - rt.send_to(addr, framed_msg(inbox.addr(), b"alive")).unwrap(); - rt.tick(); - assert!(inbox.try_recv().is_some()); - - // Stop then immediately send before tick processes the stop - rt.stop_actor(addr).unwrap(); - // This send may or may not succeed depending on mailbox state - let _ = rt.send_to(addr, framed_msg(inbox.addr(), b"after-stop")); - rt.tick(); // processes stop signal, clears mailbox - rt.tick(); // cleanup - - // No response expected — either the send failed or the message was cleared - // The key assertion: no panic or corruption -} - -// ── SharedEngine Debug impl ───────────────────────────────────────────────── - -#[test] -fn shared_engine_debug_does_not_panic() { - let engine = SharedEngine::new().unwrap(); - let debug_str = format!("{:?}", engine); - assert!(debug_str.contains("SharedEngine")); -} - -// ── ByteMessage equality and clone ────────────────────────────────────────── - -#[test] -fn byte_message_traits() { - let msg1 = ByteMessage(vec![1, 2, 3]); - let msg2 = msg1.clone(); - assert_eq!(msg1, msg2); - - let msg3 = ByteMessage(vec![4, 5, 6]); - assert_ne!(msg1, msg3); - - let debug_str = format!("{:?}", msg1); - assert!(debug_str.contains("ByteMessage")); -} - -// ── Malicious guest: massive outbox (memory exhaustion defense) ───────────── - -#[test] -fn guest_sending_1000_messages_in_one_handle_all_delivered() { - // A malicious guest could flood the outbox with thousands of messages. - // The host should handle this without crashing. Each message is small. - 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 256) - (func (export "handle") (param $ptr i32) (param $len i32) - (local $i i32) - (local.set $i (i32.const 0)) - (block $break - (loop $loop - (br_if $break (i32.ge_u (local.get $i) (i32.const 1000))) - (call $send (local.get $ptr) (i32.const 32) (i32.const 0)) - (local.set $i (i32.add (local.get $i) (i32.const 1))) - (br $loop) - ) - ) - ) - ) - "#; - 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"flood")).unwrap(); - rt.tick(); - - let mut count = 0; - while inbox.try_recv().is_some() { - count += 1; - } - assert_eq!(count, 1000, "all 1000 messages should be delivered"); -} - -// ── Interleaved message types: ByteMessage + watch in same tick ───────────── - -#[test] -fn wasm_actor_processes_messages_and_receives_watch_notification() { - // WASM echo actor processes a message and then receives a watch - // notification for a stopped actor — both in a short sequence. - let engine = SharedEngine::new().unwrap(); - let echo = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")) - .build() - .unwrap(); - let silent = WasmActorBuilder::new(engine, guest_wasm("silent")) - .build() - .unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let echo_addr = rt.spawn(echo).unwrap(); - let silent_addr = rt.spawn(silent).unwrap(); - rt.tick(); // let actors initialize - - // Echo processes a message - rt.send_to(echo_addr, framed_msg(inbox.addr(), b"before-death")).unwrap(); - rt.tick(); - let received = inbox.try_recv().expect("echo should work before death notification"); - assert_eq!(received.0, b"before-death"); - - // Stop silent actor — echo doesn't watch it, so no notification expected - // But this tests that the runtime handles mixed actor types during cleanup - rt.stop_actor(silent_addr).unwrap(); - rt.tick(); - rt.tick(); - - // Echo still works after another actor died - rt.send_to(echo_addr, framed_msg(inbox.addr(), b"after-death")).unwrap(); - rt.tick(); - let received = inbox.try_recv().expect("echo should work after other actor dies"); - assert_eq!(received.0, b"after-death"); -} - -// ── Alloc returns i32::MAX: maximum positive value ────────────────────────── - -#[test] -fn alloc_returns_i32_max_drops_message_actor_survives() { - // alloc returns i32::MAX (2147483647). (ptr as usize).saturating_add(len) - // produces a huge value, bounds check rejects. Actor survives. - 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 2147483647 - ) - (func (export "handle") (param i32 i32)) - ) - "#; - 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![1, 2, 3])).unwrap(); - rt.tick(); - - // Actor survives - rt.send_to(addr, ByteMessage(vec![4])).unwrap(); - rt.tick(); -} - -// ── Property: echo preserves message integrity under varied sizes ─────────── - proptest! { #[test] - fn prop_echo_preserves_payloads_of_varied_sizes(size in 1usize..2000) { - // Messages of varying sizes should echo perfectly through the pipeline. - // Tests allocation alignment and copy correctness at many sizes. - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("echo")) - .build() - .unwrap(); + fn prop_builder_never_panics( + pages in 0u32..10, + alloc_return in prop_oneof![-100i32..0, 0i32..70000, Just(i32::MIN), Just(i32::MAX)], + ) { + let wat = format!( + r#"(module + (memory (export "memory") {pages}) + (func (export "alloc") (param i32) (result i32) i32.const {alloc_return}) + (func (export "handle") (param i32 i32)) + )"# + ); + if let Ok(wasm) = wat::parse_str(&wat) { + let engine = SharedEngine::new().unwrap(); + // Should succeed or return clean error — never panic + let _ = WasmActorBuilder::new(engine, wasm).build(); + } + } +} +// ═══════════════════════════════════════════════════════════════════════════════ +// Group 2: Echo round-trip +// ═══════════════════════════════════════════════════════════════════════════════ + +#[test] +fn echo_round_trip_story() { + 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(); + + // Text payload + rt.send_to(addr, framed_msg(inbox.addr(), b"hello wasm")).unwrap(); + rt.tick(); + assert_eq!(inbox.try_recv().unwrap().0, b"hello wasm"); + + // Binary payload (all 256 byte values) + let binary: Vec = (0..=255).collect(); + rt.send_to(addr, framed_msg(inbox.addr(), &binary)).unwrap(); + rt.tick(); + assert_eq!(inbox.try_recv().unwrap().0, binary); + + // Empty message — echo needs >= 32 bytes, so no reply + rt.send_to(addr, ByteMessage(vec![])).unwrap(); + rt.tick(); + assert!(inbox.try_recv().is_none()); + + // Address-only message (32 bytes, 0 payload) — echo sends empty payload + rt.send_to(addr, framed_msg(inbox.addr(), b"")).unwrap(); + rt.tick(); + let resp = inbox.try_recv().unwrap(); + assert!(resp.0.is_empty(), "address-only should echo empty payload"); + + // Single byte payload + rt.send_to(addr, framed_msg(inbox.addr(), &[0x42])).unwrap(); + rt.tick(); + assert_eq!(inbox.try_recv().unwrap().0, vec![0x42]); + + // Actor still alive after all those messages + rt.send_to(addr, framed_msg(inbox.addr(), b"fin")).unwrap(); + rt.tick(); + assert_eq!(inbox.try_recv().unwrap().0, b"fin"); +} + +proptest! { + #[test] + fn prop_echo_preserves_arbitrary_payload(payload in proptest::collection::vec(any::(), 0..500)) { + 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..size).map(|i| (i % 256) as u8).collect(); rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); rt.tick(); - let received = inbox.try_recv().expect("echo should return payload"); - prop_assert_eq!(received.0, payload); + if payload.is_empty() { + // echo guest: if len < 32, returns empty; if len == 32 (addr only), returns empty payload + let resp = inbox.try_recv(); + // With 32-byte address + 0-byte payload, echo sends back empty + if let Some(msg) = resp { + prop_assert!(msg.0.is_empty()); + } + } else { + let received = inbox.try_recv().expect("echo should reply for non-empty payload"); + prop_assert_eq!(received.0, payload); + } } } -// ── Spawn and send in same tick: message delivered on first tick ───────────── - #[test] -fn wasm_actor_receives_message_sent_in_spawn_tick() { +fn double_and_silent_contracts() { 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(); - rt.send_to(addr, framed_msg(inbox.addr(), b"first-tick")).unwrap(); - - // Single tick should spawn the actor AND deliver the message + // Double: 1 message in → exactly 2 out + let double = WasmActorBuilder::new(engine.clone(), guest_wasm("double")).build().unwrap(); + let daddr = rt.spawn(double).unwrap(); + rt.send_to(daddr, framed_msg(inbox.addr(), b"dup")).unwrap(); rt.tick(); + assert_eq!(inbox.try_recv().unwrap().0, b"dup"); + assert_eq!(inbox.try_recv().unwrap().0, b"dup"); + assert!(inbox.try_recv().is_none(), "exactly 2"); - let received = inbox.try_recv().expect("message sent before first tick should be processed"); - assert_eq!(received.0, b"first-tick"); + // Silent: messages in → 0 out + let silent = WasmActorBuilder::new(engine, guest_wasm("silent")).build().unwrap(); + let saddr = rt.spawn(silent).unwrap(); + for _ in 0..10 { + rt.send_to(saddr, framed_msg(inbox.addr(), b"ignored")).unwrap(); + } + rt.tick(); + assert!(inbox.try_recv().is_none(), "silent never sends"); } -// ── WASM actor coexists with many native actors ───────────────────────────── +// ═══════════════════════════════════════════════════════════════════════════════ +// Group 3: Alloc failure resilience +// ═══════════════════════════════════════════════════════════════════════════════ -struct Counter { - count: std::sync::Arc, -} +proptest! { + #[test] + fn prop_any_alloc_return_never_kills_actor( + alloc_val in prop_oneof![ + -100i32..0, + 0i32..70000, + Just(i32::MIN), + Just(i32::MAX), + Just(0i32), + Just(65500i32), + Just(65536i32), + ], + ) { + let wasm = alloc_returns_wat(alloc_val); + 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(); -#[derive(Clone)] -struct Ping; - -impl ActorInterface for Counter { - type Incoming = Ping; - type Response = (); - fn handle(&mut self, _ctx: &Ctx, _msg: Ping) { - self.count.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + // Send two messages — actor must survive both regardless of alloc return + rt.send_to(addr, ByteMessage(vec![1, 2, 3])).unwrap(); + rt.tick(); + rt.send_to(addr, ByteMessage(vec![4, 5, 6])).unwrap(); + rt.tick(); } } #[test] -fn wasm_actor_works_alongside_many_native_actors() { - let engine = SharedEngine::new().unwrap(); - let echo = WasmActorBuilder::new(engine, guest_wasm("echo")) - .build() - .unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let counts: Vec<_> = (0..20) - .map(|_| std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0))) - .collect(); - - let mut native_addrs = Vec::new(); - for count in &counts { - let addr = rt.spawn(Counter { count: count.clone() }).unwrap(); - native_addrs.push(addr); - } - let echo_addr = rt.spawn(echo).unwrap(); - - // Send to all actors in same tick - for addr in &native_addrs { - rt.send_to(*addr, Ping).unwrap(); - } - rt.send_to(echo_addr, framed_msg(inbox.addr(), b"mixed")).unwrap(); - rt.tick(); - - for (i, count) in counts.iter().enumerate() { - assert_eq!( - count.load(std::sync::atomic::Ordering::SeqCst), 1, - "native actor {i} should have processed its Ping" - ); - } - - let received = inbox.try_recv().expect("WASM actor should echo in mixed runtime"); - assert_eq!(received.0, b"mixed"); -} - -// ── alloc alternates between failure and success ──────────────────────────── - -#[test] -fn alloc_alternates_between_failure_and_success() { - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (global $counter (mut i32) (i32.const 0)) - - (func (export "alloc") (param $size i32) (result i32) - global.get $counter - i32.const 1 - i32.add - global.set $counter - - ;; Odd calls return -1 (invalid), even calls return 256 - global.get $counter - i32.const 2 - i32.rem_u - i32.const 1 - i32.eq - if (result i32) - i32.const -1 - else - i32.const 256 - end - ) - (func (export "handle") (param $ptr i32) (param $len i32) - local.get $ptr - local.get $ptr - i32.const 32 - i32.add - local.get $len - i32.const 32 - i32.sub - call $send +fn alloc_trap_recovery() { + // Alloc traps on first call (counter == 0), succeeds thereafter + let wat = r#"(module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (global $calls (mut i32) (i32.const 0)) + (func (export "alloc") (param $len i32) (result i32) + (global.set $calls (i32.add (global.get $calls) (i32.const 1))) + (if (result i32) (i32.eq (global.get $calls) (i32.const 1)) + (then unreachable) + (else i32.const 4096) ) ) - "#; - let wasm = wat::parse_str(wat).unwrap(); + (func (export "handle") (param $ptr i32) (param $len i32) + (if (i32.ge_u (local.get $len) (i32.const 33)) + (then + (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, wasm).build().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(); + + // First message — alloc traps, dropped + rt.send_to(addr, framed_msg(inbox.addr(), b"first")).unwrap(); + rt.tick(); + assert!(inbox.try_recv().is_none(), "first msg dropped due to alloc trap"); + + // Second message — alloc succeeds, echoed + rt.send_to(addr, framed_msg(inbox.addr(), b"second")).unwrap(); + rt.tick(); + assert_eq!(inbox.try_recv().unwrap().0, b"second"); +} + +#[test] +fn alloc_alternates_and_exhaustion() { + // Part A: alternating alloc (even calls succeed, odd calls return 0) + let wat = r#"(module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (global $calls (mut i32) (i32.const 0)) + (func (export "alloc") (param $len i32) (result i32) + (global.set $calls (i32.add (global.get $calls) (i32.const 1))) + (if (result i32) (i32.rem_u (global.get $calls) (i32.const 2)) + (then i32.const 4096) + (else i32.const 0) + ) + ) + (func (export "handle") (param $ptr i32) (param $len i32) + (if (i32.ge_u (local.get $len) (i32.const 33)) + (then + (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.clone(), 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(); - // Send 6 messages: 1(fail), 2(ok), 3(fail), 4(ok), 5(fail), 6(ok) let mut echoed = 0; for i in 0u8..6 { rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); rt.tick(); - if inbox.try_recv().is_some() { - echoed += 1; - } + if inbox.try_recv().is_some() { echoed += 1; } } + assert_eq!(echoed, 3, "should echo on odd-numbered alloc calls only"); - assert_eq!(echoed, 3, "should echo on even-numbered alloc calls only"); + // Part B: echo guest under sustained load — bump allocator exhaustion + let echo = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); + let rt2 = Runtime::new(RuntimeConfig::default()); + let inbox2 = rt2.new_inbox::().unwrap(); + let addr2 = rt2.spawn(echo).unwrap(); + + let mut total_echoed = 0; + for _ in 0..2000 { + rt2.send_to(addr2, framed_msg(inbox2.addr(), b"ping")).unwrap(); + rt2.tick(); + if inbox2.try_recv().is_some() { total_echoed += 1; } + } + // Some succeed (before OOM), some fail (after OOM). Actor survives throughout. + assert!(total_echoed > 0, "at least some messages should echo"); + assert!(total_echoed < 2000, "bump allocator should eventually exhaust"); } -// ── Truncated WASM: module bytes cut mid-section ──────────────────────────── +// ═══════════════════════════════════════════════════════════════════════════════ +// Group 4: Handle trap & outbox semantics +// ═══════════════════════════════════════════════════════════════════════════════ #[test] -fn truncated_wasm_bytes_returns_error() { - // Take a valid WASM module and truncate it. Should fail to compile. - let valid_wasm = guest_wasm("echo"); - let truncated = valid_wasm[..valid_wasm.len() / 2].to_vec(); - +fn handle_trap_actor_survives() { let engine = SharedEngine::new().unwrap(); - let result = WasmActorBuilder::new(engine, truncated).build(); - assert!(result.is_err(), "truncated WASM should fail to compile"); -} - -// ── Module with no import of swactor.send: handle that never sends ────────── - -#[test] -fn module_without_send_import_can_still_process_messages() { - // A module that doesn't import swactor.send at all. - // It should build successfully (linker defines send but module doesn't import it). - // Handle can process messages without sending. - let wat = r#" - (module - (memory (export "memory") 1) - (func (export "alloc") (param i32) (result i32) i32.const 256) - (func (export "handle") (param i32 i32) - ;; Process the message but never send anything - ;; (No import of swactor.send) - ) - ) - "#; - let wasm = wat::parse_str(wat).unwrap(); - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, wasm).build().unwrap(); + // unreachable trap + let unreachable_wat = wat::parse_str(r#"(module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (func (export "alloc") (param i32) (result i32) i32.const 256) + (func (export "handle") (param i32 i32) unreachable) + )"#).unwrap(); + let actor = WasmActorBuilder::new(engine.clone(), unreachable_wat).build().unwrap(); let rt = Runtime::new(RuntimeConfig::default()); let addr = rt.spawn(actor).unwrap(); - rt.send_to(addr, ByteMessage(vec![1, 2, 3])).unwrap(); - rt.tick(); + // Send 3 messages — all trap, all dropped + for _ in 0..3 { + rt.send_to(addr, ByteMessage(vec![1])).unwrap(); + rt.tick(); + } - // Actor processed message, didn't send anything, survives - rt.send_to(addr, ByteMessage(vec![4, 5, 6])).unwrap(); + // Division by zero trap + let divzero_wat = wat::parse_str(r#"(module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (func (export "alloc") (param i32) (result i32) i32.const 256) + (func (export "handle") (param $ptr i32) (param $len i32) + (drop (i32.div_u (local.get $len) (i32.const 0))) + ) + )"#).unwrap(); + let actor2 = WasmActorBuilder::new(engine, divzero_wat).build().unwrap(); + let addr2 = rt.spawn(actor2).unwrap(); + rt.send_to(addr2, ByteMessage(vec![1])).unwrap(); + rt.tick(); // no panic +} + +#[test] +fn outbox_cleared_on_trap() { + // Guest sends once (valid), then traps. Outbox should be cleared. + 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 256) + (func (export "handle") (param $ptr i32) (param $len i32) + ;; Send a valid message + (call $send (local.get $ptr) (local.get $ptr) (i32.const 1)) + ;; Then trap + unreachable + ) + )"#; + 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(), b"data")).unwrap(); + rt.tick(); + assert!(inbox.try_recv().is_none(), "outbox cleared on trap — no messages delivered"); + + // Actor survives + rt.send_to(addr, ByteMessage(vec![1])).unwrap(); rt.tick(); } -// ── Multi-worker stress: 10 WASM actors on 4-thread runtime ───────────────── +proptest! { + #[test] + fn prop_any_send_args_never_crash_host( + dest_ptr in any::(), + payload_ptr in any::(), + payload_len in any::(), + ) { + let wasm = send_args_wat(dest_ptr, payload_ptr, payload_len); + 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(); // never panic + } +} #[test] -fn ten_wasm_actors_on_four_thread_runtime() { - // Spawn 10 WASM echo actors on a 4-thread runtime, send a message to each, - // and verify all responses arrive. +fn send_boundary_conditions() { let engine = SharedEngine::new().unwrap(); - let wasm_bytes = guest_wasm("echo"); - - let config = RuntimeConfig { - num_threads: 4, - ..RuntimeConfig::default() - }; - let rt = Runtime::new(config); + let rt = Runtime::new(RuntimeConfig::default()); let inbox = rt.new_inbox::().unwrap(); - let mut addrs = Vec::new(); - for _ in 0..10 { - let actor = WasmActorBuilder::new(engine.clone(), wasm_bytes.clone()) - .build() - .unwrap(); - addrs.push(rt.spawn(actor).unwrap()); - } + // Zero-length payload sends empty ByteMessage + let zero_len = wat::parse_str(r#"(module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (func (export "alloc") (param i32) (result i32) i32.const 256) + (func (export "handle") (param $ptr i32) (param $len i32) + (call $send (local.get $ptr) (i32.const 0) (i32.const 0)) + ) + )"#).unwrap(); + let actor = WasmActorBuilder::new(engine.clone(), zero_len).build().unwrap(); + let addr = rt.spawn(actor).unwrap(); + rt.send_to(addr, framed_msg(inbox.addr(), b"x")).unwrap(); + rt.tick(); + let msg = inbox.try_recv().expect("zero-len payload should deliver"); + assert!(msg.0.is_empty()); - for (i, addr) in addrs.iter().enumerate() { - rt.send_to(*addr, framed_msg(inbox.addr(), &[i as u8])).unwrap(); - } + // Dest at exact memory boundary: ptr=65504, needs 32 bytes → end=65536 = memory size. Works. + let exact_end = send_args_wat(65504, 0, 1); + let actor2 = WasmActorBuilder::new(engine.clone(), exact_end).build().unwrap(); + let addr2 = rt.spawn(actor2).unwrap(); + rt.send_to(addr2, ByteMessage(vec![0u8; 64])).unwrap(); + rt.tick(); // should not trap (exact fit) - let handle = rt.run().unwrap(); + // Dest one past boundary: ptr=65505 → end=65537 > 65536. Traps. + let one_past = send_args_wat(65505, 0, 1); + let actor3 = WasmActorBuilder::new(engine.clone(), one_past).build().unwrap(); + let addr3 = rt.spawn(actor3).unwrap(); + rt.send_to(addr3, ByteMessage(vec![0u8; 64])).unwrap(); + rt.tick(); // traps but actor survives - // Poll for all 10 responses - let mut received = Vec::new(); - for _ in 0..40 { - std::thread::sleep(std::time::Duration::from_millis(25)); - while let Some(msg) = inbox.try_recv() { - received.push(msg.0[0]); - } - if received.len() == 10 { - break; - } - } - - handle.shutdown(); - - received.sort(); - assert_eq!(received, (0..10u8).collect::>(), "all 10 actors should echo"); + // Send to garbage address — silently dropped (no matching inbox) + let garbage = wat::parse_str(r#"(module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (data (i32.const 500) "\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff") + (func (export "alloc") (param i32) (result i32) i32.const 256) + (func (export "handle") (param $ptr i32) (param $len i32) + (call $send (i32.const 500) (i32.const 0) (i32.const 1)) + ) + )"#).unwrap(); + let actor4 = WasmActorBuilder::new(engine, garbage).build().unwrap(); + let addr4 = rt.spawn(actor4).unwrap(); + rt.send_to(addr4, ByteMessage(vec![1])).unwrap(); + rt.tick(); + assert!(inbox.try_recv().is_none(), "garbage addr → no delivery to our inbox"); } -// ── alloc with i32::MIN: most negative value ──────────────────────────────── +// ═══════════════════════════════════════════════════════════════════════════════ +// Group 5: Lifecycle & integration +// ═══════════════════════════════════════════════════════════════════════════════ #[test] -fn alloc_returns_i32_min_drops_message_actor_survives() { - 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 -2147483648 ;; i32::MIN - ) - (func (export "handle") (param i32 i32)) - ) - "#; - let wasm = wat::parse_str(wat).unwrap(); +fn full_lifecycle_story() { 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(); + let inbox = rt.new_inbox::().unwrap(); - rt.send_to(addr, ByteMessage(vec![1])).unwrap(); - rt.tick(); // ptr < 0 guard catches i32::MIN + // Spawn echo, use it + let echo = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); + let addr1 = rt.spawn(echo).unwrap(); + rt.send_to(addr1, framed_msg(inbox.addr(), b"alive")).unwrap(); + rt.tick(); + assert_eq!(inbox.try_recv().unwrap().0, b"alive"); - rt.send_to(addr, ByteMessage(vec![2])).unwrap(); + // Stop it + let _ = rt.stop_actor(addr1); + rt.tick(); + rt.tick(); + + // Send to dead actor — silently dropped + let _ = rt.send_to(addr1, framed_msg(inbox.addr(), b"dead")); + rt.tick(); + assert!(inbox.try_recv().is_none()); + + // Respawn — different address, still works + let echo2 = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); + let addr2 = rt.spawn(echo2).unwrap(); + assert_ne!(addr1, addr2, "respawned actor gets different address"); + rt.send_to(addr2, framed_msg(inbox.addr(), b"new")).unwrap(); + rt.tick(); + assert_eq!(inbox.try_recv().unwrap().0, b"new"); + + // Double-stop is fine + let _ = rt.stop_actor(addr2); + let _ = rt.stop_actor(addr2); rt.tick(); } -// ── Watch integration: native watcher + WASM watcher observing same death ─── +struct Forwarder { + engine: SharedEngine, + wasm_bytes: Vec, + inbox_addr: ActorAddress, +} + +#[derive(Clone)] +struct ForwardMsg(Vec); + +impl ActorInterface for Forwarder { + type Incoming = ForwardMsg; + type Response = (); + fn handle(&mut self, ctx: &Ctx, msg: ForwardMsg) { + let wasm = WasmActorBuilder::new(self.engine.clone(), self.wasm_bytes.clone()) + .build() + .unwrap(); + let wasm_addr = ctx.spawn(wasm).unwrap(); + let _ = ctx.send(wasm_addr, framed_msg(&self.inbox_addr, &msg.0)); + } +} + +#[test] +fn wasm_native_interop() { + let engine = SharedEngine::new().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + + // Native forwarder spawns a WASM echo actor and forwards a message to it + let forwarder = Forwarder { + engine: engine.clone(), + wasm_bytes: guest_wasm("echo"), + inbox_addr: inbox.addr().clone(), + }; + let fwd_addr = rt.spawn(forwarder).unwrap(); + rt.send_to(fwd_addr, ForwardMsg(b"via-native".to_vec())).unwrap(); + for _ in 0..5 { rt.tick(); } + assert_eq!(inbox.try_recv().unwrap().0, b"via-native"); +} + +#[test] +fn wasm_relay_chain() { + // A→B→C→inbox: each echo actor strips 32 bytes of address and sends payload to that address + let engine = SharedEngine::new().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + + let a = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); + let b = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); + let c = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); + let addr_a = rt.spawn(a).unwrap(); + let addr_b = rt.spawn(b).unwrap(); + let addr_c = rt.spawn(c).unwrap(); + + // Nested framed: [addr_b | addr_c | addr_inbox | "end"] + let mut payload = Vec::new(); + payload.extend_from_slice(&addr_b.0); + payload.extend_from_slice(&addr_c.0); + payload.extend_from_slice(&inbox.addr().0); + payload.extend_from_slice(b"end"); + + rt.send_to(addr_a, ByteMessage(payload)).unwrap(); + for _ in 0..3 { rt.tick(); } + + // After 3 hops, inbox should have the final payload "end" + // A sends [addr_c | addr_inbox | "end"] to B + // B sends [addr_inbox | "end"] to C + // C sends "end" to inbox + let msg = inbox.try_recv().expect("relay chain should deliver"); + assert_eq!(msg.0, b"end"); +} struct DeathCounter { count: std::sync::Arc, @@ -3157,10181 +716,650 @@ impl ActorInterface for DeathCounter { } #[test] -fn two_watchers_both_notified_when_wasm_actor_dies() { +fn watch_notification() { let engine = SharedEngine::new().unwrap(); - let target = WasmActorBuilder::new(engine, guest_wasm("silent")) - .build() - .unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let count_a = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); - let count_b = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); - + let count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let target = WasmActorBuilder::new(engine, guest_wasm("silent")).build().unwrap(); let target_addr = rt.spawn(target).unwrap(); - let watcher_a = rt.spawn(DeathCounter { count: count_a.clone() }).unwrap(); - let watcher_b = rt.spawn(DeathCounter { count: count_b.clone() }).unwrap(); + let watcher_addr = rt.spawn(DeathCounter { count: count.clone() }).unwrap(); - // Both watchers watch the target - rt.send_to(watcher_a, WatchAddr(target_addr)).unwrap(); - rt.send_to(watcher_b, WatchAddr(target_addr)).unwrap(); + rt.send_to(watcher_addr, WatchAddr(target_addr)).unwrap(); for _ in 0..3 { rt.tick(); } - // Kill the target - rt.stop_actor(target_addr).unwrap(); + let _ = rt.stop_actor(target_addr); for _ in 0..5 { rt.tick(); } - assert_eq!(count_a.load(std::sync::atomic::Ordering::SeqCst), 1, "watcher A should be notified"); - assert_eq!(count_b.load(std::sync::atomic::Ordering::SeqCst), 1, "watcher B should be notified"); + assert_eq!(count.load(std::sync::atomic::Ordering::SeqCst), 1); } -// ── Division by zero: WASM trap, actor survives ───────────────────────────── - -#[test] -fn guest_division_by_zero_traps_actor_survives() { - 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 256) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Division by zero is a trap in WASM - local.get $len - i32.const 0 - i32.div_u - drop - ) - ) - "#; - 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![1])).unwrap(); - rt.tick(); // div by zero trap - - // Actor survives - rt.send_to(addr, ByteMessage(vec![2])).unwrap(); - rt.tick(); -} - -// ── Module with extra exports: globals and extra functions ────────────────── - -#[test] -fn module_with_extra_exports_builds_and_works() { - // Module exports extra globals and functions beyond the required ones. - // Builder should ignore them. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (global (export "version") i32 (i32.const 42)) - (global (export "magic") i64 (i64.const 12345)) - (func (export "alloc") (param i32) (result i32) i32.const 256) - (func (export "handle") (param $ptr i32) (param $len i32) - (i32.store8 (i32.const 200) (i32.const 7)) - local.get $ptr - i32.const 200 - i32.const 1 - call $send - ) - (func (export "extra_func") (param i32) (result i32) - local.get 0 - ) - ) - "#; - 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"extras")).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("module with extras should work"); - assert_eq!(received.0, vec![7]); -} - -// ── Send with dest_ptr=0, all zeroes in memory: valid but unroutable ──────── - -#[test] -fn send_with_all_zero_dest_from_uninitialized_memory() { - // Guest reads dest address from offset 500 (uninitialized, all zeros). - // The zero address isn't routable. ctx.send fails silently. - 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 256) - (func (export "handle") (param i32 i32) - ;; Read dest from uninitialized region (offset 500, all zeros) - i32.const 500 ;; dest_ptr - i32.const 32 ;; payload_ptr - i32.const 1 ;; 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 addr = rt.spawn(actor).unwrap(); - - rt.send_to(addr, ByteMessage(vec![1, 2, 3])).unwrap(); - rt.tick(); // send to zero-address fails silently - - // Actor survives - rt.send_to(addr, ByteMessage(vec![4])).unwrap(); - rt.tick(); -} - -// ── Property: WASM actor survives any sequence of operations ──────────────── - proptest! { #[test] - fn prop_actor_survives_any_operation_sequence( - ops in proptest::collection::vec( - prop_oneof![ - Just("send"), - Just("empty"), - Just("large"), - ], - 1..20 - ) - ) { - 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(); - - for op in &ops { - match *op { - "send" => { - rt.send_to(addr, framed_msg(inbox.addr(), b"msg")).unwrap(); - } - "empty" => { - rt.send_to(addr, ByteMessage(vec![])).unwrap(); - } - "large" => { - rt.send_to(addr, ByteMessage(vec![0u8; 60000])).unwrap(); - } - _ => unreachable!(), - } - rt.tick(); - // Drain inbox - while inbox.try_recv().is_some() {} - } - - // Actor should still be alive - let result = rt.send_to(addr, ByteMessage(vec![99])); - prop_assert!(result.is_ok(), "actor must survive any operation sequence"); - } -} - -// ── Integer overflow in guest: wrapping arithmetic doesn't trap ───────────── - -#[test] -fn guest_integer_overflow_wraps_silently() { - // WASM integers wrap on overflow (no trap). This guest adds i32::MAX + 1 - // and uses the result as a send offset. The wrapping result (0) should - // produce a valid send. - 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 256) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; i32::MAX + 1 wraps to i32::MIN (-2147483648) - ;; Use it as... nothing, just verify no trap - i32.const 2147483647 - i32.const 1 - i32.add - drop - - ;; Send normally - (i32.store8 (i32.const 200) (i32.const 33)) - local.get $ptr - i32.const 200 - i32.const 1 - 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::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - rt.send_to(addr, framed_msg(inbox.addr(), b"wrap")).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("wrapping overflow should not trap"); - assert_eq!(received.0, vec![33]); -} - -// ── Multiple messages in one tick to same WASM actor ──────────────────────── - -#[test] -fn multiple_messages_in_one_tick_all_processed() { - // Send 5 messages to a WASM actor before ticking. All should be - // processed in the same tick (within the default budget of 64). - 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(); - - for i in 0u8..5 { - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - } - rt.tick(); - - let mut received = Vec::new(); - while let Some(msg) = inbox.try_recv() { - received.push(msg.0[0]); - } - assert_eq!(received, vec![0, 1, 2, 3, 4]); -} - -// ── Swap actors: stop WASM, spawn new WASM at conceptually same role ──────── - -#[test] -fn hot_swap_wasm_actor_works() { - // Stop an echo actor, spawn a double actor in its place, verify the new - // one works correctly. Tests clean handover of actor lifecycle. - let engine = SharedEngine::new().unwrap(); - - // Phase 1: echo actor - let echo = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")) - .build() - .unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let echo_addr = rt.spawn(echo).unwrap(); - rt.send_to(echo_addr, framed_msg(inbox.addr(), b"echo-phase")).unwrap(); - rt.tick(); - let recv = inbox.try_recv().expect("echo should work"); - assert_eq!(recv.0, b"echo-phase"); - - // Stop echo - rt.stop_actor(echo_addr).unwrap(); - rt.tick(); - rt.tick(); - - // Phase 2: double actor - let double = WasmActorBuilder::new(engine, guest_wasm("double")) - .build() - .unwrap(); - let double_addr = rt.spawn(double).unwrap(); - - rt.send_to(double_addr, framed_msg(inbox.addr(), b"double-phase")).unwrap(); - rt.tick(); - - let first = inbox.try_recv().expect("double should send first"); - let second = inbox.try_recv().expect("double should send second"); - assert_eq!(first.0, b"double-phase"); - assert_eq!(second.0, b"double-phase"); - assert!(inbox.try_recv().is_none()); -} - -// ── Guest uses memory.copy: bulk copy within linear memory ────────────────── - -#[test] -fn guest_using_memory_copy_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 256) - (func (export "handle") (param $ptr i32) (param $len i32) - (i32.store8 (i32.const 500) (i32.const 72)) - (i32.store8 (i32.const 501) (i32.const 73)) - (memory.copy (i32.const 600) (i32.const 500) (i32.const 2)) - local.get $ptr - i32.const 600 - i32.const 2 - 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::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - rt.send_to(addr, framed_msg(inbox.addr(), b"copy-test")).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("memory.copy should work"); - assert_eq!(received.0, b"HI"); -} - -// ── Engine clone stress: 50 actors from same engine ───────────────────────── - -#[test] -fn fifty_actors_from_same_engine() { - let engine = SharedEngine::new().unwrap(); - let wasm_bytes = guest_wasm("echo"); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let mut addrs = Vec::new(); - for _ in 0..50 { - let actor = WasmActorBuilder::new(engine.clone(), wasm_bytes.clone()) - .build() - .unwrap(); - addrs.push(rt.spawn(actor).unwrap()); - } - - rt.send_to(addrs[0], framed_msg(inbox.addr(), b"first")).unwrap(); - rt.send_to(addrs[49], framed_msg(inbox.addr(), b"last")).unwrap(); - rt.tick(); - - let mut received = Vec::new(); - while let Some(msg) = inbox.try_recv() { - received.push(msg.0.clone()); - } - assert_eq!(received.len(), 2); - assert!(received.contains(&b"first".to_vec())); - assert!(received.contains(&b"last".to_vec())); -} - -// ── Payload integrity: pattern check for copy correctness ─────────────────── - -#[test] -fn payload_pattern_integrity_check() { - 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..500).map(|i| ((i * 7 + 13) % 256) as u8).collect(); - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("pattern payload should echo"); - assert_eq!(received.0, payload, "payload integrity check"); -} - -// ── Lifecycle fuzz: echo with random stopping ─────────────────────────────── - -proptest! { - #[test] - fn prop_echo_lifecycle_fuzz( - num_messages in 1usize..30, - payload_sizes in proptest::collection::vec(1usize..200, 1..30), - stop_at in proptest::option::of(0usize..30), - ) { - 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 msg_count = num_messages.min(payload_sizes.len()); - let mut echoed = 0; - - for i in 0..msg_count { - if stop_at == Some(i) { - let _ = rt.stop_actor(addr); - rt.tick(); - rt.tick(); - break; - } - - let payload: Vec = (0..payload_sizes[i]).map(|j| (j % 256) as u8).collect(); - let send_result = rt.send_to(addr, framed_msg(inbox.addr(), &payload)); - if send_result.is_err() { - break; - } - rt.tick(); - - if let Some(received) = inbox.try_recv() { - prop_assert_eq!(received.0, payload); - echoed += 1; - } - } - - if stop_at.is_none() || stop_at.unwrap_or(0) > 0 { - prop_assert!(echoed > 0 || stop_at == Some(0)); - } - } -} - -// ── OOB table access: call_indirect with bad index traps ──────────────────── - -#[test] -fn guest_oob_call_indirect_traps_actor_survives() { - // Guest uses call_indirect with index 99 on a table of size 1. - // This should trap. Actor should survive. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (type $void (func)) - (func $noop) - (table 1 funcref) - (elem (i32.const 0) $noop) - - (func (export "alloc") (param i32) (result i32) i32.const 256) - (func (export "handle") (param i32 i32) - ;; call_indirect with index 99 — out of bounds - (call_indirect (type $void) (i32.const 99)) - ) - ) - "#; - 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![1])).unwrap(); - rt.tick(); // OOB trap - - // Actor survives - rt.send_to(addr, ByteMessage(vec![2])).unwrap(); - rt.tick(); -} - -// ── 100th test: comprehensive round-trip through all guest modules ────────── - -#[test] -fn all_guest_modules_work_in_same_runtime() { - // Spawn one of each guest (echo, double, silent) in the same runtime. - // Send messages to all three and verify each behaves correctly. - // This is the 100th test — a comprehensive integration checkpoint. - let engine = SharedEngine::new().unwrap(); - - let echo = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")) - .build().unwrap(); - let double = WasmActorBuilder::new(engine.clone(), guest_wasm("double")) - .build().unwrap(); - let silent = WasmActorBuilder::new(engine, guest_wasm("silent")) - .build().unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let echo_addr = rt.spawn(echo).unwrap(); - let double_addr = rt.spawn(double).unwrap(); - let silent_addr = rt.spawn(silent).unwrap(); - - // Send to all three - rt.send_to(echo_addr, framed_msg(inbox.addr(), b"E")).unwrap(); - rt.send_to(double_addr, framed_msg(inbox.addr(), b"D")).unwrap(); - rt.send_to(silent_addr, ByteMessage(b"S".to_vec())).unwrap(); - rt.tick(); - - // Collect results - let mut messages = Vec::new(); - while let Some(msg) = inbox.try_recv() { - messages.push(msg.0); - } - - // Echo: 1 message, Double: 2 messages, Silent: 0 messages = 3 total - assert_eq!(messages.len(), 3, "echo(1) + double(2) + silent(0) = 3 messages"); - - // Verify content - let echo_count = messages.iter().filter(|m| m.as_slice() == b"E").count(); - let double_count = messages.iter().filter(|m| m.as_slice() == b"D").count(); - assert_eq!(echo_count, 1, "echo should send 1 copy"); - assert_eq!(double_count, 2, "double should send 2 copies"); -} - -// ── OOB memory.fill: trap, actor survives ─────────────────────────────────── - -#[test] -fn guest_oob_memory_fill_traps_actor_survives() { - // Guest tries to fill past the end of memory. WASM traps on OOB bulk ops. - 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 256) - (func (export "handle") (param i32 i32) - ;; Fill starting at 65530, length 100 — overflows 65536 boundary - (memory.fill (i32.const 65530) (i32.const 0) (i32.const 100)) - ) - ) - "#; - 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![1])).unwrap(); - rt.tick(); // OOB memory.fill traps - - // Actor survives - rt.send_to(addr, ByteMessage(vec![2])).unwrap(); - rt.tick(); -} - -// ── Native spawns WASM + sends via ctx.send, delivers in same tick ────────── - -struct WasmSpawnerInline { - engine: SharedEngine, - wasm_bytes: Vec, - inbox_addr: ActorAddress, -} - -#[derive(Clone)] -struct SpawnCmd; - -impl ActorInterface for WasmSpawnerInline { - type Incoming = SpawnCmd; - type Response = (); - fn handle(&mut self, ctx: &Ctx, _msg: SpawnCmd) { - let actor = WasmActorBuilder::new(self.engine.clone(), self.wasm_bytes.clone()) - .build() - .unwrap(); - let wasm_addr = ctx.spawn(actor).unwrap(); - let msg = framed_msg(&self.inbox_addr, b"inline-spawn"); - let _ = ctx.send(wasm_addr, msg); - } -} - -#[test] -fn native_spawns_wasm_and_sends_in_same_handler() { - // A native actor spawns a WASM actor and sends a message to it in the - // same handler call. The runtime's tick phases should handle this: - // phase 4 drains spawns, phase 5 delivers pending_local. - let engine = SharedEngine::new().unwrap(); - let wasm_bytes = guest_wasm("echo"); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let spawner = WasmSpawnerInline { - engine: engine.clone(), - wasm_bytes: wasm_bytes.clone(), - inbox_addr: *inbox.addr(), - }; - let spawner_addr = rt.spawn(spawner).unwrap(); - - rt.send_to(spawner_addr, SpawnCmd).unwrap(); - rt.tick(); // spawner handles SpawnCmd: spawns WASM, sends to it - rt.tick(); // WASM actor processes the message, echoes to inbox - - let received = inbox.try_recv().expect("inline spawn + send should work"); - assert_eq!(received.0, b"inline-spawn"); -} - -// ── Build from same bytes multiple times: no interference ─────────────────── - -#[test] -fn build_many_actors_from_same_bytes_sequentially() { - // Build 10 actors sequentially from the same engine + bytes. - // Each should be completely independent. - let engine = SharedEngine::new().unwrap(); - let wasm_bytes = guest_wasm("echo"); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - for i in 0u8..10 { - let actor = WasmActorBuilder::new(engine.clone(), wasm_bytes.clone()) - .build() - .unwrap(); - let addr = rt.spawn(actor).unwrap(); - - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("sequential build should work"); - assert_eq!(received.0, vec![i]); - - rt.stop_actor(addr).unwrap(); - rt.tick(); - rt.tick(); - } -} - -// ── Guest that only sends on even-numbered messages ───────────────────────── - -#[test] -fn guest_conditional_send_based_on_message_content() { - // Guest only sends a reply if the first byte of payload (after the 32-byte - // address) is even. Tests that the outbox is correctly empty when the guest - // decides not to send. - 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 256) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Check if byte at ptr+32 (first payload byte) is even - local.get $ptr - i32.const 32 - i32.add - i32.load8_u - i32.const 2 - i32.rem_u - i32.const 0 - i32.eq - if - ;; Even: send reply - local.get $ptr - local.get $ptr - i32.const 32 - i32.add - local.get $len - i32.const 32 - i32.sub - call $send - end - ;; Odd: do nothing (empty outbox) - ) - ) - "#; - 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(); - - // Send even byte (0) — should get reply - rt.send_to(addr, framed_msg(inbox.addr(), &[0])).unwrap(); - rt.tick(); - assert!(inbox.try_recv().is_some(), "even byte should trigger reply"); - - // Send odd byte (1) — no reply - rt.send_to(addr, framed_msg(inbox.addr(), &[1])).unwrap(); - rt.tick(); - assert!(inbox.try_recv().is_none(), "odd byte should not trigger reply"); - - // Send even byte (2) — should get reply - rt.send_to(addr, framed_msg(inbox.addr(), &[2])).unwrap(); - rt.tick(); - assert!(inbox.try_recv().is_some(), "even byte should trigger reply"); -} - -// ── Guest with multiple memory pages ──────────────────────────────────────── - -#[test] -fn guest_with_multiple_initial_pages_works() { - // Module starts with 4 pages (256KiB). Alloc returns pointer in page 3. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 4) ;; 4 pages = 262144 bytes - (func (export "alloc") (param i32) (result i32) - i32.const 196608 ;; page 3 start (3 * 65536) - ) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Echo from page 3 - local.get $ptr - local.get $ptr - i32.const 32 - i32.add - local.get $len - i32.const 32 - i32.sub - 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::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - let payload = b"multi-page"; - rt.send_to(addr, framed_msg(inbox.addr(), payload)).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("multi-page alloc should work"); - assert_eq!(received.0, payload); -} - -// ── Long-running: echo actor processes 500 messages sequentially ──────────── - -#[test] -fn echo_processes_500_sequential_messages() { - // Sustained message processing without crashes, allocator exhaustion - // handling, and verified actor survival throughout. - 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 mut received_count = 0; - for i in 0u16..500 { - let payload = i.to_le_bytes(); - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - rt.tick(); - if let Some(msg) = inbox.try_recv() { - assert_eq!(msg.0, payload, "payload integrity at message {i}"); - received_count += 1; - } - } - - // With 65536-byte allocator and ~40 bytes per alloc (34 + alignment), - // all 500 messages should fit. Verify all echoed correctly. - assert_eq!(received_count, 500, "all 500 messages should echo"); -} - -// ── Mass spawn and stop: 100 WASM actors ──────────────────────────────────── - -#[test] -fn mass_spawn_and_stop_100_actors() { - let engine = SharedEngine::new().unwrap(); - let wasm_bytes = guest_wasm("silent"); - let rt = Runtime::new(RuntimeConfig::default()); - - let mut addrs = Vec::new(); - for _ in 0..100 { - let actor = WasmActorBuilder::new(engine.clone(), wasm_bytes.clone()) - .build().unwrap(); - addrs.push(rt.spawn(actor).unwrap()); - } - rt.tick(); - - for addr in &addrs { - rt.stop_actor(*addr).unwrap(); - } - rt.tick(); - rt.tick(); - - for addr in &addrs { - assert!(rt.send_to(*addr, ByteMessage(vec![1])).is_err()); - } -} - -// ── Echo to stopping actor: send silently fails ───────────────────────────── - -#[test] -fn echo_to_stopping_actor_silently_fails() { - 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 addr_a = rt.spawn(actor_a).unwrap(); - let addr_b = rt.spawn(actor_b).unwrap(); - rt.tick(); - - rt.send_to(addr_a, framed_msg(&addr_b, b"to-dying-b")).unwrap(); - rt.stop_actor(addr_b).unwrap(); - rt.tick(); // A echoes to B (stopping) — silently fails - rt.tick(); - - // A should still be alive - let inbox = rt.new_inbox::().unwrap(); - rt.send_to(addr_a, framed_msg(inbox.addr(), b"still-alive")).unwrap(); - rt.tick(); - assert!(inbox.try_recv().is_some(), "actor A should survive"); -} - -// ── Compile-time trait checks ─────────────────────────────────────────────── - -#[test] -fn shared_engine_is_send_and_sync() { - fn assert_send_sync() {} - assert_send_sync::(); -} - -#[test] -fn wasm_actor_is_send() { - fn assert_send() {} - assert_send::(); -} - -// ── Guest writes to alloc pointer region before sending ───────────────────── - -#[test] -fn guest_modifies_received_message_before_echoing() { - // Guest receives a message, XORs each payload byte with 0xFF, then - // echoes the modified payload. Verifies that the guest can mutate - // linear memory and the modified data is what gets sent. - 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) - - ;; payload starts at ptr+32, length is len-32 - (local.set $payload_start (i32.add (local.get $ptr) (i32.const 32))) - (local.set $payload_len (i32.sub (local.get $len) (i32.const 32))) - - ;; Skip if no payload - (br_if 0 (i32.lt_s (local.get $payload_len) (i32.const 1))) - - ;; XOR each byte with 0xFF - (local.set $i (i32.const 0)) - (block $break - (loop $loop - (br_if $break (i32.ge_u (local.get $i) (local.get $payload_len))) - (i32.store8 - (i32.add (local.get $payload_start) (local.get $i)) - (i32.xor - (i32.load8_u (i32.add (local.get $payload_start) (local.get $i))) - (i32.const 255) - ) - ) - (local.set $i (i32.add (local.get $i) (i32.const 1))) - (br $loop) - ) - ) - - ;; Send modified payload - local.get $ptr - local.get $payload_start - local.get $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::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - let payload = vec![0x00, 0x0F, 0xF0, 0xFF]; - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("XOR transform should send"); - let expected: Vec = payload.iter().map(|b| b ^ 0xFF).collect(); - assert_eq!(received.0, expected, "payload should be XOR'd with 0xFF"); -} - -// ── Stop and re-spawn at same conceptual slot ─────────────────────────────── - -#[test] -fn stop_and_respawn_same_type_repeatedly() { - // Stop and respawn the same type of WASM actor 5 times. - // Each new instance should work independently. - let engine = SharedEngine::new().unwrap(); - let wasm_bytes = guest_wasm("echo"); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - for round in 0u8..5 { - let actor = WasmActorBuilder::new(engine.clone(), wasm_bytes.clone()) - .build().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - rt.send_to(addr, framed_msg(inbox.addr(), &[round])).unwrap(); - rt.tick(); - let received = inbox.try_recv().expect("respawned actor should echo"); - assert_eq!(received.0, vec![round]); - - rt.stop_actor(addr).unwrap(); - rt.tick(); - rt.tick(); - } -} - -// ── WASM actor watches another WASM actor ─────────────────────────────────── -// Note: WasmActor doesn't implement on_actor_exit, so watch notifications -// are received but unhandled (default no-op). The important thing is no crash. - -#[test] -fn wasm_actor_watching_another_wasm_actor_doesnt_crash() { - // Two WASM actors. We can't make one watch the other through the WASM - // ABI (ctx.watch isn't exposed to guests). But we can have a native - // watcher confirm the runtime handles WASM actors in the watch system. - // (Already covered by native_watcher_notified_when_wasm_actor_stops, - // but let's verify with two WASM actors dying in sequence.) - let engine = SharedEngine::new().unwrap(); - let wasm_bytes = guest_wasm("silent"); - - 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 addr_a = rt.spawn(actor_a).unwrap(); - let addr_b = rt.spawn(actor_b).unwrap(); - rt.tick(); - - // Stop both in sequence - rt.stop_actor(addr_a).unwrap(); - rt.tick(); - rt.tick(); - - rt.stop_actor(addr_b).unwrap(); - rt.tick(); - rt.tick(); - - // Both gone, no crash - assert!(rt.send_to(addr_a, ByteMessage(vec![1])).is_err()); - assert!(rt.send_to(addr_b, ByteMessage(vec![1])).is_err()); -} - -// ── Guest reads len parameter correctly ───────────────────────────────────── - -#[test] -fn guest_receives_correct_len_parameter() { - // Guest stores the len parameter as a 4-byte LE integer at offset 200 - // and sends it back. Verifies the host passes the correct length. - 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) - ;; Store len at offset 200 as i32 - (i32.store (i32.const 200) (local.get $len)) - ;; Send 4 bytes from offset 200 - local.get $ptr - i32.const 200 - i32.const 4 - 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::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - // Send a message with 32-byte addr + 10 bytes payload = 42 bytes total - let payload = vec![0u8; 10]; - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("should receive len value"); - let len = i32::from_le_bytes(received.0.try_into().unwrap()); - assert_eq!(len, 42, "guest should receive total message len (32 addr + 10 payload)"); -} - -// ── Guest reads ptr parameter correctly ───────────────────────────────────── - -#[test] -fn guest_receives_correct_ptr_parameter() { - // Guest stores ptr at offset 200 and sends it back. The ptr should be - // the address returned by alloc (4096 in this case). - 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) - (i32.store (i32.const 200) (local.get $ptr)) - local.get $ptr - i32.const 200 - i32.const 4 - 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::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - rt.send_to(addr, framed_msg(inbox.addr(), b"ptr-check")).unwrap(); - rt.tick(); - - let received = inbox.try_recv().expect("should receive ptr value"); - let ptr = i32::from_le_bytes(received.0.try_into().unwrap()); - assert_eq!(ptr, 4096, "guest should receive ptr = alloc return value"); -} - -// ── Double guest with empty payload: sends two zero-length messages ───────── - -#[test] -fn double_guest_with_minimal_payload() { - // Double guest with exactly 32 bytes (addr only, no payload). - // Since double checks `len < 32`, a 32-byte message passes the check. - // payload_len = 32 - 32 = 0, so it sends two zero-length messages. - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("double")) - .build() - .unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - // Send exactly 32 bytes (just the address, no payload) - let msg = ByteMessage(inbox.addr().0.to_vec()); - rt.send_to(addr, msg).unwrap(); - rt.tick(); - - let first = inbox.try_recv().expect("double should send first empty message"); - let second = inbox.try_recv().expect("double should send second empty message"); - assert!(first.0.is_empty(), "payload should be empty"); - assert!(second.0.is_empty(), "payload should be empty"); - assert!(inbox.try_recv().is_none(), "exactly two messages"); -} - -// ── Mixed outbox: some sends succeed, some fail ───────────────────────────── - -#[test] -fn mixed_outbox_partial_delivery() { - // Guest sends to a valid address (inbox) and an invalid address (garbage) - // in the same handle call. The valid send should deliver; the invalid one - // should silently fail. The actor should survive. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - ;; Garbage address at offset 500 (all 0xDE bytes) - (data (i32.const 500) "\de\de\de\de\de\de\de\de\de\de\de\de\de\de\de\de\de\de\de\de\de\de\de\de\de\de\de\de\de\de\de\de") - - (func (export "alloc") (param i32) (result i32) i32.const 4096) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Send #1: to valid address (from message bytes) - (i32.store8 (i32.const 200) (i32.const 65)) ;; 'A' - local.get $ptr - i32.const 200 - i32.const 1 - call $send - - ;; Send #2: to garbage address at offset 500 - (i32.store8 (i32.const 201) (i32.const 66)) ;; 'B' - i32.const 500 ;; garbage dest - i32.const 201 - i32.const 1 - call $send - - ;; Send #3: back to valid address - (i32.store8 (i32.const 202) (i32.const 67)) ;; 'C' - local.get $ptr - i32.const 202 - i32.const 1 - 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::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - rt.send_to(addr, framed_msg(inbox.addr(), b"mixed-outbox")).unwrap(); - rt.tick(); - - // Should receive sends #1 and #3 (valid dest), but not #2 (garbage dest) - let mut received = Vec::new(); - while let Some(msg) = inbox.try_recv() { - received.push(msg.0[0]); - } - assert_eq!(received, vec![b'A', b'C'], "only valid-address sends should deliver"); -} - -// ── Drop-oldest mailbox policy with WASM actor ───────────────────────────── - -#[test] -fn drop_oldest_mailbox_with_wasm_actor() { - use swactor::runtime::MailboxOverflow; - - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("echo")) - .build() - .unwrap(); - - let config = RuntimeConfig { - default_mailbox_capacity: 3, - mailbox_overflow: MailboxOverflow::DropOldest, - ..RuntimeConfig::default() - }; - let rt = Runtime::new(config); - let inbox = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - // Send 5 messages before tick — DropOldest keeps the last 3 - for i in 0u8..5 { - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - } - rt.tick(); - - let mut received = Vec::new(); - while let Some(msg) = inbox.try_recv() { - received.push(msg.0[0]); - } - - assert_eq!(received.len(), 3, "should keep 3 messages"); - // DropOldest keeps the newest: [2, 3, 4] - assert_eq!(received, vec![2, 3, 4], "DropOldest should keep newest messages"); -} - -// ── WASM actor echoes to inbox, inbox full — message dropped ──────────────── - -#[test] -fn echo_to_full_inbox_silently_drops() { - // Echo sends to an inbox that has a bounded capacity. - // If the inbox is full, the send should silently fail. - use swactor::runtime::MailboxOverflow; - - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("echo")) - .build() - .unwrap(); - - let config = RuntimeConfig { - default_mailbox_capacity: 2, - mailbox_overflow: MailboxOverflow::DropNewest, - ..RuntimeConfig::default() - }; - let rt = Runtime::new(config); - let inbox = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - // Send 5 messages — actor has capacity 2, so only first 2 are kept - // Each message echoes to inbox (also capacity 2) - for i in 0u8..5 { - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - } - rt.tick(); - - // Inbox has capacity 2, so at most 2 messages received - let mut received = Vec::new(); - while let Some(msg) = inbox.try_recv() { - received.push(msg.0[0]); - } - assert!(received.len() <= 2, "inbox should be bounded to capacity 2"); -} - -// ── Ping-pong: two WASM echoes create feedback loop, budget limits it ─────── - -#[test] -fn pingpong_wasm_echoes_bounded_by_budget() { - // Two echo actors that send to each other. A single seed message - // should create an exponentially growing feedback loop, but - // actor_message_budget limits messages processed per tick. - let engine = SharedEngine::new().unwrap(); - let echo1 = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")) - .build() - .unwrap(); - let echo2 = WasmActorBuilder::new(engine, guest_wasm("echo")) - .build() - .unwrap(); - - let config = RuntimeConfig { - actor_message_budget: 4, - ..RuntimeConfig::default() - }; - let rt = Runtime::new(config); - let inbox = rt.new_inbox::().unwrap(); - let a1 = rt.spawn(echo1).unwrap(); - let _a2 = rt.spawn(echo2).unwrap(); - - // Seed: tell actor 1 to echo to actor 2, with addr of actor 1 as payload - // so actor 2's reply goes back to actor 1 (creating a loop). - // Actually echo sends to first 32 bytes of message, so we need to - // frame them properly: actor1 sends to actor2, actor2 echoes payload back. - // The payload itself would need to be a framed message for actor2 to - // echo back to actor1. This creates the feedback loop. - - // Simpler approach: send a message to echo1 with dest=echo2. - // echo1 echoes payload to echo2. echo2 receives raw payload - // (not framed), so it can't echo further. This tests 1 hop only. - - // For a true feedback loop: we need the payload itself to be a framed msg. - // msg1 -> echo1: dest=echo2, payload=framed_msg(echo1, raw) - // echo1 sends framed_msg(echo1, raw) to echo2 - // echo2 receives framed_msg(echo1, raw), treats first 32 bytes as dest=echo1 - // echo2 sends "raw" to echo1 - // echo1 receives "raw", tries first 32 bytes as dest — but "raw" may be too short - - // Let's use a self-sustaining framed payload: - // Create a payload that is itself a framed_msg(a2, framed_msg(a1, framed_msg(a2, ...))) - // This is recursive — we can just build several layers. - - // Better: use a WAT module that always echoes back to the sender address - // embedded in the first 32 bytes AND re-frames the response. - - // Simplest valid test: just verify budget limits processing. - // Send multiple messages and confirm not all are processed in one tick. - for _ in 0..10 { - rt.send_to(a1, framed_msg(inbox.addr(), b"ping")).unwrap(); - } - rt.tick(); - - let mut count = 0; - while let Some(_) = inbox.try_recv() { - count += 1; - } - // Budget is 4, so actor1 should process at most 4 of the 10 messages - assert_eq!(count, 4, "budget should limit messages processed per tick"); - - // Second tick processes more - rt.tick(); - while let Some(_) = inbox.try_recv() { - count += 1; - } - assert_eq!(count, 8, "second tick should process 4 more"); - - // Third tick finishes the remaining 2 - rt.tick(); - while let Some(_) = inbox.try_recv() { - count += 1; - } - assert_eq!(count, 10, "third tick should finish remaining messages"); -} - -// ── Builder rejects alloc with wrong signature ────────────────────────────── - -#[test] -fn wrong_alloc_signature_two_params_rejected() { - // alloc takes (i32, i32) -> i32 instead of (i32) -> i32 - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (func (export "alloc") (param i32 i32) (result i32) i32.const 0) - (func (export "handle") (param i32 i32)) - ) - "#; - let wasm = wat::parse_str(wat).unwrap(); - let engine = SharedEngine::new().unwrap(); - let result = WasmActorBuilder::new(engine, wasm).build(); - assert!(result.is_err(), "alloc with wrong signature should be rejected"); - match result.err().unwrap() { - WasmActorError::MissingExport("alloc") => {} // expected — get_typed_func fails - other => panic!("expected MissingExport(alloc), got {other}"), - } -} - -// ── Builder rejects handle with wrong return type ─────────────────────────── - -#[test] -fn wrong_handle_return_type_rejected() { - // handle returns i32 instead of void - 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 0) - (func (export "handle") (param i32 i32) (result i32) i32.const 0) - ) - "#; - let wasm = wat::parse_str(wat).unwrap(); - let engine = SharedEngine::new().unwrap(); - let result = WasmActorBuilder::new(engine, wasm).build(); - assert!(result.is_err(), "handle with return type should be rejected"); - match result.err().unwrap() { - WasmActorError::MissingExport("handle") => {} // expected - other => panic!("expected MissingExport(handle), got {other}"), - } -} - -// ── Overlapping dest and payload in send import ───────────────────────────── - -#[test] -fn send_with_overlapping_dest_and_payload() { - // Guest calls swactor.send where dest_ptr and payload region overlap. - // The send import should read both correctly (read-only aliasing is fine). - 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) - ;; Copy dest address from message to offset 100 - ;; (first 32 bytes of message = address) - (memory.copy (i32.const 100) (local.get $ptr) (i32.const 32)) - ;; Send with dest_ptr=100 and payload starting at offset 116 - ;; (overlaps with dest region 100..132 by 16 bytes) - (call $send (i32.const 100) (i32.const 116) (i32.const 4)) - ) - ) - "#; - 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(); - - // The payload at offset 116..120 will be bytes 16..20 of the dest address - // (since dest is at 100..132 and payload overlaps at 116..120) - rt.send_to(addr, framed_msg(inbox.addr(), b"overlap-test")).unwrap(); - rt.tick(); - - // Should receive something — the overlapping read is valid - let msg = inbox.try_recv().expect("should receive overlapping send"); - assert_eq!(msg.0.len(), 4, "payload should be 4 bytes"); -} - -// ── Memory defined but not exported as "memory" ───────────────────────────── - -#[test] -fn memory_not_exported_returns_missing_export() { - // Module defines memory internally but doesn't export it with the name "memory". - let wat = r#" - (module - (memory 1) - (func (export "alloc") (param i32) (result i32) i32.const 0) - (func (export "handle") (param i32 i32)) - ) - "#; - let wasm = wat::parse_str(wat).unwrap(); - let engine = SharedEngine::new().unwrap(); - let result = WasmActorBuilder::new(engine, wasm).build(); - // instantiation itself may fail because link_send requires memory export, - // OR build may succeed but get_memory returns None → MissingExport - assert!(result.is_err(), "missing memory export should be rejected"); - match result.err().unwrap() { - WasmActorError::MissingExport("memory") => {} // expected - WasmActorError::Wasmtime(_) => {} // also acceptable — linker can't resolve memory - other => panic!("unexpected error: {other}"), - } -} - -// ── Multi-value module rejected by sandboxed engine ───────────────────────── - -#[test] -fn multi_value_module_rejected_by_engine() { - // Module uses multi-value returns (disabled in SharedEngine config). - 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 0) - (func (export "handle") (param i32 i32)) - (func $multi (result i32 i32) i32.const 1 i32.const 2) - ) - "#; - let wasm = wat::parse_str(wat).unwrap(); - let engine = SharedEngine::new().unwrap(); - let result = WasmActorBuilder::new(engine, wasm).build(); - // Engine has multi_value disabled, so compilation should fail - assert!(result.is_err(), "multi-value module should be rejected"); -} - -// ── Send import reads dest at exact end of linear memory ──────────────────── - -#[test] -fn send_dest_at_exact_memory_boundary() { - // Guest calls swactor.send with dest_ptr such that dest_ptr + 32 == memory size. - // This should succeed because it's exactly in bounds. - 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) - ;; Copy 32-byte address from message to end of memory - 32 - ;; 65536 - 32 = 65504 - (memory.copy (i32.const 65504) (local.get $ptr) (i32.const 32)) - ;; Send with dest at very end of memory, payload at 4096 - (call $send (i32.const 65504) (i32.const 4096) (i32.const 1)) - ) - ) - "#; - 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"X")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().expect("send at exact boundary should succeed"); - assert_eq!(msg.0.len(), 1); -} - -// ── Send dest one byte past memory boundary (OOB) ────────────────────────── - -#[test] -fn send_dest_one_past_memory_boundary_traps() { - // Guest calls swactor.send with dest_ptr = memory_size - 31, so dest_ptr + 32 - // exceeds memory. The send import should return an error (which becomes a trap). - 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) - ;; dest_ptr = 65505, so dest_end = 65505 + 32 = 65537 > 65536 - (call $send (i32.const 65505) (i32.const 4096) (i32.const 1)) - ) - ) - "#; - 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"Y")).unwrap(); - rt.tick(); - - // Send traps → outbox cleared → handle returns Err → no message delivered - assert!(inbox.try_recv().is_none(), "OOB send should trap, no delivery"); - - // Actor should survive (trap caught by handle) - rt.send_to(addr, framed_msg(inbox.addr(), b"Z")).unwrap(); - rt.tick(); - // This time no OOB send, but the module always tries the OOB send, so still trapped - assert!(inbox.try_recv().is_none(), "same module always traps"); -} - -// ── Reference types module rejected by engine ─────────────────────────────── - -#[test] -fn reference_types_module_rejected() { - // Module uses externref (reference types disabled in SharedEngine). - let wat = r#" - (module - (memory (export "memory") 1) - (func (export "alloc") (param i32) (result i32) i32.const 0) - (func (export "handle") (param i32 i32)) - (table 1 externref) - ) - "#; - let wasm = wat::parse_str(wat).unwrap(); - let engine = SharedEngine::new().unwrap(); - let result = WasmActorBuilder::new(engine, wasm).build(); - assert!(result.is_err(), "reference types should be rejected by sandboxed engine"); -} - -// ── Two actors, one traps always, one works — independent store isolation ─── - -#[test] -fn trapping_actor_does_not_affect_sibling() { - // Actor 1 always traps in handle. Actor 2 echos normally. - // Verify trap in actor 1 doesn't corrupt/poison actor 2. - let trap_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) - unreachable - ) - ) - "#; - let trap_wasm = wat::parse_str(trap_wat).unwrap(); - let engine = SharedEngine::new().unwrap(); - let trapper = WasmActorBuilder::new(engine.clone(), trap_wasm).build().unwrap(); - let echoer = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let trap_addr = rt.spawn(trapper).unwrap(); - let echo_addr = rt.spawn(echoer).unwrap(); - - // Send to both in the same tick - rt.send_to(trap_addr, framed_msg(inbox.addr(), b"trap-this")).unwrap(); - rt.send_to(echo_addr, framed_msg(inbox.addr(), b"echo-this")).unwrap(); - rt.tick(); - - // Only echo actor should deliver - let mut msgs = Vec::new(); - while let Some(msg) = inbox.try_recv() { - msgs.push(msg.0); - } - assert_eq!(msgs.len(), 1, "only echo actor should deliver"); - assert_eq!(&msgs[0], b"echo-this"); -} - -// ── Alloc returns negative for non-zero len — message dropped gracefully ──── - -#[test] -fn alloc_returns_negative_for_nonzero_drops_message() { - // Guest alloc always returns -42 regardless of input. - 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 -42) - (func (export "handle") (param i32 i32) - ;; Should never be called because alloc returns negative - unreachable - ) - ) - "#; - 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(); - - // Send several messages — all should be silently dropped - for i in 0..5 { - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - } - rt.tick(); - - assert!(inbox.try_recv().is_none(), "negative alloc should drop messages"); - - // Actor should still be alive — send another message, still dropped - rt.send_to(addr, framed_msg(inbox.addr(), &[99])).unwrap(); - rt.tick(); - assert!(inbox.try_recv().is_none(), "actor alive but still drops (negative alloc)"); -} - -// ── WASM actor with global state accumulates across messages ──────────────── - -#[test] -fn global_counter_accumulates_across_messages() { - // Guest has a mutable global counter. Each handle call increments it. - // The response payload includes the counter value. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (global $counter (mut i32) (i32.const 0)) - - (func (export "alloc") (param i32) (result i32) i32.const 4096) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Increment counter - (global.set $counter (i32.add (global.get $counter) (i32.const 1))) - ;; Write counter value as single byte at offset 200 - (i32.store8 (i32.const 200) (global.get $counter)) - ;; Send counter value back to sender (first 32 bytes of msg) - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - ) - ) - "#; - 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(); - - // Send 5 messages across 5 ticks - for _ in 0..5 { - rt.send_to(addr, framed_msg(inbox.addr(), b"inc")).unwrap(); - rt.tick(); - } - - let mut counter_values = Vec::new(); - while let Some(msg) = inbox.try_recv() { - counter_values.push(msg.0[0]); - } - assert_eq!(counter_values, vec![1, 2, 3, 4, 5], "global state should persist across handle calls"); -} - -// ── Send import: payload_len = 0 is valid zero-copy send ──────────────────── - -#[test] -fn send_import_zero_length_payload_delivers_empty() { - // Guest calls swactor.send with payload_len=0. This should deliver an - // empty payload (not a trap, not dropped). - 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) - ;; Send with payload_len=0 - (call $send (local.get $ptr) (i32.const 0) (i32.const 0)) - ) - ) - "#; - 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"trigger")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().expect("zero-length send should deliver"); - assert!(msg.0.is_empty(), "payload should be empty"); -} - -// ── Multiple engines with different configurations ────────────────────────── - -#[test] -fn actors_from_separate_engines_coexist() { - // Build two separate engines and spawn one actor from each. - // They should work independently on the same runtime. - let engine1 = SharedEngine::new().unwrap(); - let engine2 = SharedEngine::new().unwrap(); - let echo1 = WasmActorBuilder::new(engine1, guest_wasm("echo")).build().unwrap(); - let echo2 = WasmActorBuilder::new(engine2, guest_wasm("echo")).build().unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let addr1 = rt.spawn(echo1).unwrap(); - let addr2 = rt.spawn(echo2).unwrap(); - - rt.send_to(addr1, framed_msg(inbox.addr(), b"from-engine1")).unwrap(); - rt.send_to(addr2, framed_msg(inbox.addr(), b"from-engine2")).unwrap(); - rt.tick(); - - let mut payloads: Vec> = Vec::new(); - while let Some(msg) = inbox.try_recv() { - payloads.push(msg.0); - } - payloads.sort(); - assert_eq!(payloads, vec![b"from-engine1".to_vec(), b"from-engine2".to_vec()]); -} - -// ── Rapid spawn-send-stop stress test (50 rounds) ─────────────────────────── - -#[test] -fn rapid_spawn_send_stop_50_rounds() { - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - for round in 0u8..50 { - 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(), &[round])).unwrap(); - rt.tick(); - rt.stop_actor(addr); - rt.tick(); // process stop - } - - let mut received = Vec::new(); - while let Some(msg) = inbox.try_recv() { - received.push(msg.0[0]); - } - let expected: Vec = (0..50).collect(); - assert_eq!(received, expected, "all 50 rounds should deliver"); -} - -// ── Module with start function that succeeds ──────────────────────────────── - -#[test] -fn start_function_that_succeeds_allows_normal_operation() { - // Module has a start function that initializes a global. - // After start, normal handle should work. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (global $initialized (mut i32) (i32.const 0)) - - (func $init - (global.set $initialized (i32.const 42)) - ) - (start $init) - - (func (export "alloc") (param i32) (result i32) i32.const 4096) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Write initialized value as response - (i32.store8 (i32.const 200) (global.get $initialized)) - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - ) - ) - "#; - 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"check-init")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().expect("should receive response after start"); - assert_eq!(msg.0[0], 42, "start function should have initialized global to 42"); -} - -// ── Property: random payloads never cause host panic ──────────────────────── - -proptest! { - #[test] - fn prop_random_payload_sizes_never_panic( - payload in proptest::collection::vec(proptest::num::u8::ANY, 0..8192) - ) { - 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(); - - // Send raw payload (not framed) — echo will try to use first 32 bytes as dest - // which will be random garbage. This should never crash the host. - rt.send_to(addr, ByteMessage(payload)).unwrap(); - rt.tick(); - // We don't care what happens — just that it doesn't panic - } -} - -// ── Module with multiple memory pages and data segments ───────────────────── - -#[test] -fn multi_page_data_segments_persist() { - // Module starts with 3 pages and has data segments in each page. - // Handle reads from each page to verify data segments initialized correctly. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 3) - ;; Data segment in page 0 - (data (i32.const 100) "\AA\BB\CC") - ;; Data segment in page 1 (offset 65536 + 100 = 65636) - (data (i32.const 65636) "\DD\EE\FF") - ;; Data segment in page 2 (offset 131072 + 100 = 131172) - (data (i32.const 131172) "\11\22\33") - - (func (export "alloc") (param i32) (result i32) i32.const 4096) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Copy 3 bytes from each page into response buffer at 200 - (i32.store8 (i32.const 200) (i32.load8_u (i32.const 100))) - (i32.store8 (i32.const 201) (i32.load8_u (i32.const 65636))) - (i32.store8 (i32.const 202) (i32.load8_u (i32.const 131172))) - (call $send (local.get $ptr) (i32.const 200) (i32.const 3)) - ) - ) - "#; - 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"read-pages")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().expect("should receive data from all pages"); - assert_eq!(msg.0, vec![0xAA, 0xDD, 0x11], "data segments should be initialized across pages"); -} - -// ── Module that conditionally sends based on first payload byte ───────────── - -#[test] -fn conditional_send_fan_out_based_on_payload() { - // Guest checks first payload byte: - // 0x01 → send to dest from msg - // 0x02 → send twice (double) - // anything else → don't send - 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) - ;; Read first payload byte (after 32-byte address header) - (if (i32.eq (i32.load8_u (i32.add (local.get $ptr) (i32.const 32))) (i32.const 1)) - (then - ;; Send payload (skip first byte) once - (call $send - (local.get $ptr) - (i32.add (local.get $ptr) (i32.const 33)) - (i32.sub (local.get $len) (i32.const 33)) - ) - ) - ) - (if (i32.eq (i32.load8_u (i32.add (local.get $ptr) (i32.const 32))) (i32.const 2)) - (then - ;; Send payload twice - (call $send - (local.get $ptr) - (i32.add (local.get $ptr) (i32.const 33)) - (i32.sub (local.get $len) (i32.const 33)) - ) - (call $send - (local.get $ptr) - (i32.add (local.get $ptr) (i32.const 33)) - (i32.sub (local.get $len) (i32.const 33)) - ) - ) - ) - ) - ) - "#; - 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(); - - // Command 0x01: send once - rt.send_to(addr, framed_msg(inbox.addr(), &[0x01, b'X'])).unwrap(); - // Command 0x02: send twice - rt.send_to(addr, framed_msg(inbox.addr(), &[0x02, b'Y'])).unwrap(); - // Command 0xFF: no send - rt.send_to(addr, framed_msg(inbox.addr(), &[0xFF, b'Z'])).unwrap(); - rt.tick(); - - let mut received = Vec::new(); - while let Some(msg) = inbox.try_recv() { - received.push(msg.0.clone()); - } - assert_eq!(received.len(), 3, "should get 1 + 2 + 0 = 3 messages"); - assert_eq!(received[0], vec![b'X']); - assert_eq!(received[1], vec![b'Y']); - assert_eq!(received[2], vec![b'Y']); -} - -// ── Guest allocator returns different offsets per call ─────────────────────── - -#[test] -fn guest_with_advancing_allocator_handles_multiple_messages() { - // Guest has a proper advancing bump allocator (not static offset). - // Each alloc call returns the next available slot. Verify messages - // don't overwrite each other when processed in the same tick. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (global $heap_ptr (mut i32) (i32.const 4096)) - - (func (export "alloc") (param $size i32) (result i32) - (local $ptr i32) - (local.set $ptr (global.get $heap_ptr)) - ;; Advance heap pointer (8-byte aligned) - (global.set $heap_ptr - (i32.and - (i32.add (i32.add (global.get $heap_ptr) (local.get $size)) (i32.const 7)) - (i32.const -8) - ) - ) - (local.get $ptr) - ) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Echo: send payload (after 32-byte header) to dest (first 32 bytes) - (call $send - (local.get $ptr) - (i32.add (local.get $ptr) (i32.const 32)) - (i32.sub (local.get $len) (i32.const 32)) - ) - ) - ) - "#; - 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(); - - // Send 10 messages in one tick with distinct payloads - for i in 0u8..10 { - rt.send_to(addr, framed_msg(inbox.addr(), &[i; 16])).unwrap(); - } - rt.tick(); - - let mut received = Vec::new(); - while let Some(msg) = inbox.try_recv() { - received.push(msg.0.clone()); - } - assert_eq!(received.len(), 10, "all 10 messages should be echoed"); - for (i, payload) in received.iter().enumerate() { - assert_eq!(payload, &vec![i as u8; 16], "payload {i} should be intact"); - } -} - -// ── Guest writes to memory after send — outbox snapshot safety ────────────── - -#[test] -fn guest_overwrites_payload_after_send_outbox_has_copy() { - // Guest sends a message, then overwrites the same memory region. - // The outbox should hold a snapshot of the data at the time of send, - // not a reference to the mutable linear memory. - 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) - ;; Write "AAAA" at offset 200 - (i32.store (i32.const 200) (i32.const 0x41414141)) - ;; Send "AAAA" (4 bytes) - (call $send (local.get $ptr) (i32.const 200) (i32.const 4)) - ;; Overwrite the same region with "BBBB" - (i32.store (i32.const 200) (i32.const 0x42424242)) - ;; Send "BBBB" (4 bytes) - (call $send (local.get $ptr) (i32.const 200) (i32.const 4)) - ) - ) - "#; - 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"snapshot")).unwrap(); - rt.tick(); - - let msg1 = inbox.try_recv().expect("first send should deliver"); - let msg2 = inbox.try_recv().expect("second send should deliver"); - assert_eq!(msg1.0, b"AAAA", "first send should have original bytes"); - assert_eq!(msg2.0, b"BBBB", "second send should have overwritten bytes"); -} - -// ── Alloc that does memory.grow and returns pointer in new page ───────────── - -#[test] -fn alloc_grows_memory_returns_pointer_in_new_page() { - // Guest's alloc grows memory by 1 page and returns start of new page. - // Each call to alloc adds a page and returns a fresh region. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (func (export "alloc") (param $size i32) (result i32) - (local $old_pages i32) - ;; Grow memory by 1 page, return start of new page - (local.set $old_pages (memory.grow (i32.const 1))) - ;; If grow failed (returned -1), return -1 - (if (result i32) (i32.eq (local.get $old_pages) (i32.const -1)) - (then (i32.const -1)) - (else (i32.mul (local.get $old_pages) (i32.const 65536))) - ) - ) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Echo payload to dest - (call $send - (local.get $ptr) - (i32.add (local.get $ptr) (i32.const 32)) - (i32.sub (local.get $len) (i32.const 32)) - ) - ) - ) - "#; - 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(); - - // Each message causes a memory.grow, so memory increases: 1→2→3→4→5 pages - for i in 0u8..5 { - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - } - rt.tick(); - - let mut received = Vec::new(); - while let Some(msg) = inbox.try_recv() { - received.push(msg.0[0]); - } - assert_eq!(received, vec![0, 1, 2, 3, 4], "all messages should echo through grown pages"); -} - -// ── Handle that traps after successful send — outbox cleared ──────────────── - -#[test] -fn trap_after_successful_send_clears_outbox() { - // Guest does a valid send, then traps. The outbox should be cleared - // and the send should NOT be delivered. - 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) - ;; Write payload - (i32.store8 (i32.const 200) (i32.const 99)) - ;; Valid send - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - ;; Now trap - unreachable - ) - ) - "#; - 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"doomed")).unwrap(); - rt.tick(); - - // Outbox should be cleared by the trap, so nothing delivered - assert!(inbox.try_recv().is_none(), "trap should clear all outbox sends"); - - // Actor survives — send another, still traps - rt.send_to(addr, framed_msg(inbox.addr(), b"also-doomed")).unwrap(); - rt.tick(); - assert!(inbox.try_recv().is_none(), "actor survives but always traps"); -} - -// ── Send from WAT module using payload at start of memory (offset 0) ──────── - -#[test] -fn send_payload_at_memory_offset_zero() { - // Guest writes payload at offset 0 and sends from there. - // Tests that offset 0 is a valid payload location. - 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) - ;; Write "OK" at offset 0 - (i32.store8 (i32.const 0) (i32.const 79)) ;; 'O' - (i32.store8 (i32.const 1) (i32.const 75)) ;; 'K' - ;; Send from offset 0 with len 2 - (call $send (local.get $ptr) (i32.const 0) (i32.const 2)) - ) - ) - "#; - 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"trigger")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().expect("send from offset 0 should work"); - assert_eq!(msg.0, b"OK"); -} - -// ── WasmActorError From conversion ───────────────────────── - -#[test] -fn wasmtime_error_converts_to_wasm_actor_error() { - // Force a wasmtime::Error through the build path and verify the - // From conversion produces WasmActorError::Wasmtime variant. - let garbage = vec![0x00, 0x61, 0x73, 0x6D]; // valid magic but truncated - let engine = SharedEngine::new().unwrap(); - let result = WasmActorBuilder::new(engine, garbage).build(); - assert!(result.is_err()); - match result.err().unwrap() { - WasmActorError::Wasmtime(e) => { - // wasmtime::Error should have a non-empty message - let msg = format!("{e}"); - assert!(!msg.is_empty(), "wasmtime error should have a message"); - } - other => panic!("expected Wasmtime variant, got {other}"), - } -} - -// ── 3 WASM actors on 2-thread runtime all deliver ─────────────────────────── - -#[test] -fn three_wasm_actors_on_two_threads() { - use std::sync::atomic::{AtomicUsize, Ordering}; - use std::sync::Arc; - - let engine = SharedEngine::new().unwrap(); - let config = RuntimeConfig { - num_threads: 2, - ..RuntimeConfig::default() - }; - let rt = Runtime::new(config); - let counter = Arc::new(AtomicUsize::new(0)); - - // Spawn 3 echo actors - let mut addrs = Vec::new(); - for _ in 0..3 { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")) - .build() - .unwrap(); - addrs.push(rt.spawn(actor).unwrap()); - } - - // Create a native counter actor that counts received messages - struct MsgCounter(Arc); - impl ActorInterface for MsgCounter { - type Incoming = ByteMessage; - type Response = (); - fn handle(&mut self, _ctx: &Ctx, _msg: ByteMessage) { - self.0.fetch_add(1, Ordering::SeqCst); - } - } - - let counter_clone = counter.clone(); - let counter_addr = rt.spawn(MsgCounter(counter_clone)).unwrap(); - - // Send one message to each WASM actor, echoing to the counter - for addr in &addrs { - rt.send_to(*addr, framed_msg(&counter_addr, b"ping")).unwrap(); - } - - let handle = rt.run().unwrap(); - - // Poll for all 3 messages - let mut success = false; - for _ in 0..40 { - std::thread::sleep(std::time::Duration::from_millis(25)); - if counter.load(Ordering::SeqCst) >= 3 { - success = true; - break; - } - } - handle.shutdown(); - assert!(success, "all 3 WASM actors should deliver on 2-thread runtime"); -} - -// ── Module with if/else branching in handle ───────────────────────────────── - -#[test] -fn handle_with_if_else_branching() { - // Guest uses if/else to send different payloads based on message length. - 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) - ;; If payload (len - 32) > 5, send "BIG", else send "SML" - (if (i32.gt_s (i32.sub (local.get $len) (i32.const 32)) (i32.const 5)) - (then - (i32.store8 (i32.const 200) (i32.const 66)) ;; 'B' - (i32.store8 (i32.const 201) (i32.const 73)) ;; 'I' - (i32.store8 (i32.const 202) (i32.const 71)) ;; 'G' - (call $send (local.get $ptr) (i32.const 200) (i32.const 3)) - ) - (else - (i32.store8 (i32.const 200) (i32.const 83)) ;; 'S' - (i32.store8 (i32.const 201) (i32.const 77)) ;; 'M' - (i32.store8 (i32.const 202) (i32.const 76)) ;; 'L' - (call $send (local.get $ptr) (i32.const 200) (i32.const 3)) - ) - ) - ) - ) - "#; - 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(); - - // Short payload (3 bytes <= 5) - rt.send_to(addr, framed_msg(inbox.addr(), b"abc")).unwrap(); - // Long payload (10 bytes > 5) - rt.send_to(addr, framed_msg(inbox.addr(), b"1234567890")).unwrap(); - rt.tick(); - - let msg1 = inbox.try_recv().expect("short message should get response"); - let msg2 = inbox.try_recv().expect("long message should get response"); - assert_eq!(msg1.0, b"SML"); - assert_eq!(msg2.0, b"BIG"); -} - -// ── Module with loop/br — iterative computation in handle ─────────────────── - -#[test] -fn handle_with_loop_computes_sum() { - // Guest sums all payload bytes using a loop and sends the sum as a single byte. - 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 $sum i32) - (local $payload_start i32) - (local $payload_len i32) - ;; payload starts at ptr+32, length is len-32 - (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)) - (local.set $sum (i32.const 0)) - ;; Sum loop - (block $break - (loop $loop - (br_if $break (i32.ge_u (local.get $i) (local.get $payload_len))) - (local.set $sum - (i32.add - (local.get $sum) - (i32.load8_u (i32.add (local.get $payload_start) (local.get $i))) - ) - ) - (local.set $i (i32.add (local.get $i) (i32.const 1))) - (br $loop) - ) - ) - ;; Write sum (truncated to u8) at offset 200 - (i32.store8 (i32.const 200) (local.get $sum)) - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - ) - ) - "#; - 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(); - - // Sum of [1, 2, 3, 4, 5] = 15 - rt.send_to(addr, framed_msg(inbox.addr(), &[1, 2, 3, 4, 5])).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().expect("should receive sum"); - assert_eq!(msg.0[0], 15, "sum of [1,2,3,4,5] should be 15"); - - // Sum of [100, 100, 56] = 256 → truncated to 0 (u8 overflow) - rt.send_to(addr, framed_msg(inbox.addr(), &[100, 100, 56])).unwrap(); - rt.tick(); - let msg2 = inbox.try_recv().expect("should receive truncated sum"); - assert_eq!(msg2.0[0], 0, "256 truncated to u8 wraps to 0"); -} - -// ── Send import with dest_ptr = 0 (valid, reads from start of memory) ────── - -#[test] -fn send_with_dest_ptr_zero_reads_from_memory_start() { - // Guest copies the address to offset 0, then sends with dest_ptr=0. - 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) - ;; Copy 32-byte address from message to offset 0 - (memory.copy (i32.const 0) (local.get $ptr) (i32.const 32)) - ;; Write payload at offset 300 - (i32.store8 (i32.const 300) (i32.const 42)) - ;; Send with dest_ptr=0 - (call $send (i32.const 0) (i32.const 300) (i32.const 1)) - ) - ) - "#; - 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"zero-dest")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().expect("dest_ptr=0 should be valid"); - assert_eq!(msg.0, vec![42]); -} - -// ── Wasm actors survive being stopped while message in flight ─────────────── - -#[test] -fn stop_actor_with_pending_messages_no_crash() { - 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(); - - // Send messages, then stop before tick - for i in 0u8..10 { - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - } - rt.stop_actor(addr); - rt.tick(); - - // The actor may or may not have processed some messages before being stopped. - // The important thing is no crash/panic. - let mut count = 0; - while let Some(_) = inbox.try_recv() { - count += 1; - } - // Count can be 0..=10, we just verify no panic - assert!(count <= 10, "at most 10 messages should be received"); -} - -// ── Property: build from any subset of valid WAT produces valid error ─────── - -proptest! { - #[test] - fn prop_truncated_wasm_never_panics( - len in 0usize..200 - ) { - let full_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 0) - (func (export "handle") (param i32 i32)) - ) - "#; - let wasm_full = wat::parse_str(full_wat).unwrap(); - // Truncate the WASM bytes - let truncated: Vec = wasm_full.iter().take(len).copied().collect(); - let engine = SharedEngine::new().unwrap(); - // Building from any prefix should never panic — it either succeeds or returns Err - let _result = WasmActorBuilder::new(engine, truncated).build(); - } -} - -// ── Message ordering: same-tick messages arrive in send order ──────────────── - -#[test] -fn same_tick_message_ordering_preserved() { - // Send 20 numbered messages in order. They should arrive in the same order - // within a single tick (FIFO mailbox guarantee). - 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(); - - for i in 0u8..20 { - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - } - rt.tick(); - - let mut received = Vec::new(); - while let Some(msg) = inbox.try_recv() { - received.push(msg.0[0]); - } - let expected: Vec = (0..20).collect(); - assert_eq!(received, expected, "messages should arrive in FIFO order"); -} - -// ── Send import: payload_ptr + payload_len overflows usize ────────────────── - -#[test] -fn send_payload_ptr_plus_len_overflow_traps() { - // Guest tries to send with payload_ptr near i32::MAX and payload_len > 0, - // causing checked_add to detect overflow. - 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) - ;; payload_ptr = 2147483647 (i32::MAX), payload_len = 1 - ;; As usize: checked_add(2147483647, 1) = 2147483648 which > mem_len - (call $send (local.get $ptr) (i32.const 2147483647) (i32.const 1)) - ) - ) - "#; - 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"overflow")).unwrap(); - rt.tick(); - - // The send should trap (OOB), clearing outbox, so no delivery - assert!(inbox.try_recv().is_none(), "payload ptr overflow should trap"); -} - -// ── Large number of sends in one handle (stress outbox) ───────────────────── - -#[test] -fn handle_sends_100_messages_in_one_call() { - // Guest sends 100 messages in a single handle invocation. - // Tests outbox Vec capacity and drain performance. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (global $count (mut i32) (i32.const 0)) - (func (export "alloc") (param i32) (result i32) i32.const 4096) - (func (export "handle") (param $ptr i32) (param $len i32) - (local $i i32) - (local.set $i (i32.const 0)) - (block $break - (loop $loop - (br_if $break (i32.ge_u (local.get $i) (i32.const 100))) - ;; Write counter byte - (i32.store8 (i32.const 200) (local.get $i)) - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - (local.set $i (i32.add (local.get $i) (i32.const 1))) - (br $loop) - ) - ) - ) - ) - "#; - 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"burst")).unwrap(); - rt.tick(); - - let mut received = Vec::new(); - while let Some(msg) = inbox.try_recv() { - received.push(msg.0[0]); - } - assert_eq!(received.len(), 100, "should receive 100 messages from one handle"); - // Each stores current `i` value which wraps at 256 but 0..100 fits in u8 - let expected: Vec = (0..100).collect(); - assert_eq!(received, expected, "messages should contain counter 0..100"); -} - -// ── Module with block/br_table (switch-like dispatch) ─────────────────────── - -#[test] -fn br_table_dispatch_in_handle() { - // Guest uses br_table to dispatch on first payload byte (0, 1, or default). - 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 $cmd i32) - (local.set $cmd (i32.load8_u (i32.add (local.get $ptr) (i32.const 32)))) - (block $default - (block $case1 - (block $case0 - (br_table $case0 $case1 $default (local.get $cmd)) - ) - ;; case 0: send "ZERO" - (i32.store8 (i32.const 200) (i32.const 90)) ;; 'Z' - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - return - ) - ;; case 1: send "ONE" - (i32.store8 (i32.const 200) (i32.const 79)) ;; 'O' - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - return - ) - ;; default: send "D" - (i32.store8 (i32.const 200) (i32.const 68)) ;; 'D' - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - ) - ) - "#; - 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(), &[0])).unwrap(); // case 0 - rt.send_to(addr, framed_msg(inbox.addr(), &[1])).unwrap(); // case 1 - rt.send_to(addr, framed_msg(inbox.addr(), &[5])).unwrap(); // default - rt.tick(); - - let msg0 = inbox.try_recv().unwrap(); - let msg1 = inbox.try_recv().unwrap(); - let msg2 = inbox.try_recv().unwrap(); - assert_eq!(msg0.0, b"Z"); - assert_eq!(msg1.0, b"O"); - assert_eq!(msg2.0, b"D"); -} - -// ── Two WASM actors watching each other — one stops, other gets notified ──── - -#[test] -fn mutual_watch_wasm_actors() { - let engine = SharedEngine::new().unwrap(); - let echo1 = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - let echo2 = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - - let exit_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); - let exit_count_clone = exit_count.clone(); - - struct WatchAndCount { - target: Option, - count: std::sync::Arc, - } - impl ActorInterface for WatchAndCount { - type Incoming = ByteMessage; - type Response = (); - fn handle(&mut self, ctx: &Ctx, _msg: ByteMessage) { - if let Some(t) = self.target.take() { - ctx.watch(t); - } - } - fn on_actor_exit(&mut self, _ctx: &Ctx, _exited: swactor::actor::ActorExited) { - self.count.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - } - } - - let rt = Runtime::new(RuntimeConfig::default()); - let a1 = rt.spawn(echo1).unwrap(); - let a2 = rt.spawn(echo2).unwrap(); - - // Spawn a watcher that watches a1 - let watcher = WatchAndCount { - target: Some(a1), - count: exit_count_clone, - }; - let watcher_addr = rt.spawn(watcher).unwrap(); - - // Trigger the watcher to install the watch - rt.send_to(watcher_addr, ByteMessage(vec![])).unwrap(); - rt.tick(); - - // Stop a1 — watcher should be notified - rt.stop_actor(a1); - rt.tick(); - rt.tick(); // death notification propagates - - assert_eq!( - exit_count.load(std::sync::atomic::Ordering::SeqCst), 1, - "watcher should be notified when watched WASM actor stops" - ); - - // a2 should still be alive and functional - let inbox = rt.new_inbox::().unwrap(); - rt.send_to(a2, framed_msg(inbox.addr(), b"alive")).unwrap(); - rt.tick(); - let msg = inbox.try_recv().expect("a2 should still be alive"); - assert_eq!(msg.0, b"alive"); -} - -// ── Module with select instruction (ternary operator) ─────────────────────── - -#[test] -fn select_instruction_in_handle() { - // Guest uses `select` to choose between two values based on condition. - 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) - ;; If payload > 32 bytes (has actual content), use 'Y', else 'N' - (i32.store8 (i32.const 200) - (select - (i32.const 89) ;; 'Y' (true branch) - (i32.const 78) ;; 'N' (false branch) - (i32.gt_s (local.get $len) (i32.const 32)) - ) - ) - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - ) - ) - "#; - 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(); - - // Message with payload (len > 32) - rt.send_to(addr, framed_msg(inbox.addr(), b"content")).unwrap(); - // Message without payload (len == 32) - rt.send_to(addr, framed_msg(inbox.addr(), b"")).unwrap(); - rt.tick(); - - let msg1 = inbox.try_recv().unwrap(); - let msg2 = inbox.try_recv().unwrap(); - assert_eq!(msg1.0, b"Y", "message with content should select Y"); - assert_eq!(msg2.0, b"N", "message without content should select N"); -} - -// ── WASM actor echoes to another WASM actor which echoes to native inbox ──── - -#[test] -fn wasm_to_wasm_to_native_relay() { - // echo1 → echo2 → inbox. Three-layer relay. - let engine = SharedEngine::new().unwrap(); - let echo1 = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - let echo2 = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let addr2 = rt.spawn(echo2).unwrap(); - let addr1 = rt.spawn(echo1).unwrap(); - - // Send to echo1 with dest=echo2. Payload is a framed message for echo2→inbox. - let inner_payload = framed_msg(inbox.addr(), b"relay-data"); - rt.send_to(addr1, framed_msg(&addr2, &inner_payload.0)).unwrap(); - rt.tick(); // echo1 sends to echo2 - rt.tick(); // echo2 sends to inbox - - let msg = inbox.try_recv().expect("should receive relayed message"); - assert_eq!(msg.0, b"relay-data"); -} - -// ── Property: any byte pattern in dest address never panics ───────────────── - -proptest! { - #[test] - fn prop_any_dest_address_bytes_never_panic( - addr_bytes in proptest::collection::vec(proptest::num::u8::ANY, 32..=32), - payload in proptest::collection::vec(proptest::num::u8::ANY, 0..64), - ) { - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let addr = rt.spawn(actor).unwrap(); - - // Construct a message with arbitrary 32-byte dest address + payload - let mut msg_bytes = addr_bytes; - msg_bytes.extend_from_slice(&payload); - rt.send_to(addr, ByteMessage(msg_bytes)).unwrap(); - rt.tick(); - // No panic = success. Message either delivers or is silently dropped. - } -} - -// ── Spawn WASM actor from inside native actor's handle ────────────────────── - -#[test] -fn native_handler_spawns_wasm_actor_inline() { - // A native actor receives a message and spawns a WASM echo actor in its handler, - // then sends a message to the newly spawned actor. - struct InlineSpawner { - engine: SharedEngine, - wasm_bytes: Vec, - result_inbox: ActorAddress, - } - impl ActorInterface for InlineSpawner { - type Incoming = ByteMessage; - type Response = (); - fn handle(&mut self, ctx: &Ctx, _msg: ByteMessage) { - let actor = WasmActorBuilder::new(self.engine.clone(), self.wasm_bytes.clone()) - .build() - .unwrap(); - let wasm_addr = ctx.spawn(actor).unwrap(); - let _ = ctx.send(wasm_addr, framed_msg(&self.result_inbox, b"from-spawner")); - } - } - - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let spawner = InlineSpawner { - engine, - wasm_bytes: guest_wasm("echo"), - result_inbox: *inbox.addr(), - }; - let spawner_addr = rt.spawn(spawner).unwrap(); - - rt.send_to(spawner_addr, ByteMessage(vec![])).unwrap(); - rt.tick(); // spawner creates WASM actor + sends message - rt.tick(); // WASM actor processes message, echoes to inbox - - let msg = inbox.try_recv().expect("spawned WASM actor should echo to inbox"); - assert_eq!(msg.0, b"from-spawner"); -} - -// ── WASM actor with local variables (stack manipulation) ──────────────────── - -#[test] -fn handle_uses_many_locals() { - // Guest uses several local variables for computation. - 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 $a i32) (local $b i32) (local $c i32) (local $d i32) - (local.set $a (i32.const 10)) - (local.set $b (i32.const 20)) - (local.set $c (i32.add (local.get $a) (local.get $b))) - (local.set $d (i32.mul (local.get $c) (i32.const 2))) - ;; d = (10 + 20) * 2 = 60 - (i32.store8 (i32.const 200) (local.get $d)) - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - ) - ) - "#; - 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"compute")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0[0], 60, "(10+20)*2 = 60"); -} - -// ── WASM actor after many ticks still functions (no resource leak) ────────── - -#[test] -fn wasm_actor_survives_1000_ticks() { - 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(); - - // Send a message every 100 ticks - for i in 0u8..10 { - // 100 empty ticks - for _ in 0..100 { - rt.tick(); - } - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - rt.tick(); - } - - let mut received = Vec::new(); - while let Some(msg) = inbox.try_recv() { - received.push(msg.0[0]); - } - let expected: Vec = (0..10).collect(); - assert_eq!(received, expected, "actor should still work after 1000+ ticks"); -} - -// ── Send to dead actor — message silently dropped ─────────────────────────── - -#[test] -fn send_to_dead_wasm_actor_silently_dropped() { - 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(); - - // Verify it works - rt.send_to(addr, framed_msg(inbox.addr(), b"alive")).unwrap(); - rt.tick(); - assert!(inbox.try_recv().is_some()); - - // Stop and cleanup - rt.stop_actor(addr); - rt.tick(); - rt.tick(); - - // Send to dead actor — should not panic - let result = rt.send_to(addr, framed_msg(inbox.addr(), b"dead")); - // Either returns Ok (message silently dropped) or Err (dead address) - // Both are acceptable — the key is no panic - drop(result); - rt.tick(); - assert!(inbox.try_recv().is_none(), "dead actor should not deliver"); -} - -// ── Guest alloc always returns same pointer — messages overwrite each other ── - -#[test] -fn static_alloc_pointer_messages_overwrite() { - // Guest alloc always returns 4096. Each message overwrites the same region. - // The last message to be processed wins. Verifies outbox snapshots correctly. - 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(); - - // Send 3 messages in same tick. Each writes to offset 4096. - // Echo reads from 4096 — should read the data written for that specific call - // because outbox snapshots at send time. - rt.send_to(addr, framed_msg(inbox.addr(), b"first")).unwrap(); - rt.send_to(addr, framed_msg(inbox.addr(), b"second")).unwrap(); - rt.send_to(addr, framed_msg(inbox.addr(), b"third")).unwrap(); - rt.tick(); - - let mut payloads = Vec::new(); - while let Some(msg) = inbox.try_recv() { - payloads.push(msg.0); - } - assert_eq!(payloads.len(), 3); - assert_eq!(payloads[0], b"first"); - assert_eq!(payloads[1], b"second"); - assert_eq!(payloads[2], b"third"); -} - -// ── Module with nested blocks ─────────────────────────────────────────────── - -#[test] -fn nested_blocks_in_handle() { - // Guest uses nested block/end to structure control flow. - 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) - (block $outer - (block $inner - ;; If len <= 32 (no payload), break to outer (skip send) - (br_if $outer (i32.le_s (local.get $len) (i32.const 32))) - ;; If len > 64 (big payload), break to inner (send "BIG") - (br_if $inner (i32.gt_s (local.get $len) (i32.const 64))) - ;; Small payload: send "SM" - (i32.store8 (i32.const 200) (i32.const 83)) - (i32.store8 (i32.const 201) (i32.const 77)) - (call $send (local.get $ptr) (i32.const 200) (i32.const 2)) - return - ) - ;; Big payload - (i32.store8 (i32.const 200) (i32.const 66)) - (i32.store8 (i32.const 201) (i32.const 71)) - (call $send (local.get $ptr) (i32.const 200) (i32.const 2)) - ) - ) - ) - "#; - 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(); - - // No payload (len=32) → no send - rt.send_to(addr, framed_msg(inbox.addr(), b"")).unwrap(); - // Small payload (5 bytes, total len=37) → "SM" - rt.send_to(addr, framed_msg(inbox.addr(), b"hello")).unwrap(); - // Big payload (50 bytes, total len=82) → "BG" - rt.send_to(addr, framed_msg(inbox.addr(), &[b'x'; 50])).unwrap(); - rt.tick(); - - let msg1 = inbox.try_recv().unwrap(); - let msg2 = inbox.try_recv().unwrap(); - assert!(inbox.try_recv().is_none(), "empty payload should not send"); - assert_eq!(msg1.0, b"SM"); - assert_eq!(msg2.0, b"BG"); -} - -// ── Guest uses memory.size to check available memory ──────────────────────── - -#[test] -fn guest_uses_memory_size_instruction() { - // Guest checks memory.size and sends it as a response byte. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 2) - (func (export "alloc") (param i32) (result i32) i32.const 4096) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; memory.size returns pages (should be 2) - (i32.store8 (i32.const 200) (memory.size)) - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - ) - ) - "#; - 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"size")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0[0], 2, "memory.size should report 2 pages"); -} - -// ── Property: spawn-send-tick-stop cycle never panics ─────────────────────── - -proptest! { - #[test] - fn prop_spawn_send_stop_cycle_never_panics( + fn prop_lifecycle_fuzz( + guest_idx in 0usize..3, n_msgs in 0u8..20, - payload_len in 0usize..128, + ticks_before in 1usize..5, + ticks_after in 1usize..5, ) { + let names = ["echo", "double", "silent"]; let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); + let actor = WasmActorBuilder::new(engine, guest_wasm(names[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![0xAB; payload_len]; for _ in 0..n_msgs { - let _ = rt.send_to(addr, framed_msg(inbox.addr(), &payload)); + let _ = rt.send_to(addr, framed_msg(inbox.addr(), b"x")); } - rt.tick(); - rt.stop_actor(addr); - rt.tick(); - // Drain inbox - while let Some(_) = inbox.try_recv() {} + for _ in 0..ticks_before { rt.tick(); } + let _ = rt.stop_actor(addr); + for _ in 0..ticks_after { rt.tick(); } + while inbox.try_recv().is_some() {} } } -// ── Guest modifies memory between alloc and handle being called ───────────── - -#[test] -fn guest_start_function_modifies_alloc_region() { - // Guest's start function writes data in the alloc region (4096+). - // When handle is called, the host writes over it. Tests that - // the host always writes fresh data, not relying on zeroed memory. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - ;; Pre-fill region at 4096 with 0xFF bytes via data segment - (data (i32.const 4096) "\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff") - - (func (export "alloc") (param i32) (result i32) i32.const 4096) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Echo payload to dest — host wrote message at 4096, overwriting 0xFF - (call $send - (local.get $ptr) - (i32.add (local.get $ptr) (i32.const 32)) - (i32.sub (local.get $len) (i32.const 32)) - ) - ) - ) - "#; - 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"overwrite")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().expect("host should overwrite pre-filled memory"); - assert_eq!(msg.0, b"overwrite", "host should write fresh data over 0xFF"); -} - -// ── Engine clone shares same underlying engine ────────────────────────────── - -#[test] -fn engine_clone_is_same_engine() { - let engine1 = SharedEngine::new().unwrap(); - let engine2 = engine1.clone(); - - // Both should produce working actors - let actor1 = WasmActorBuilder::new(engine1, guest_wasm("echo")).build().unwrap(); - let actor2 = WasmActorBuilder::new(engine2, guest_wasm("echo")).build().unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let a1 = rt.spawn(actor1).unwrap(); - let a2 = rt.spawn(actor2).unwrap(); - - rt.send_to(a1, framed_msg(inbox.addr(), b"clone1")).unwrap(); - rt.send_to(a2, framed_msg(inbox.addr(), b"clone2")).unwrap(); - rt.tick(); - - let mut msgs: Vec> = Vec::new(); - while let Some(msg) = inbox.try_recv() { - msgs.push(msg.0); - } - msgs.sort(); - assert_eq!(msgs, vec![b"clone1".to_vec(), b"clone2".to_vec()]); -} - -// ── Guest with i64 operations in handle ───────────────────────────────────── - -#[test] -fn handle_uses_i64_operations() { - // Guest performs i64 arithmetic and stores result as i32. - 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) - ;; Compute 1000000000 * 3 = 3000000000 (fits in i64 but not i32) - ;; Wrap to i32: 3000000000 mod 2^32 = 3000000000 (fits as u32) - ;; As i32: -1294967296 - ;; Store low byte: 3000000000 & 0xFF = 0x00 - ;; Actually let's just do something simpler: 100 + 200 = 300 → wrap i32 - (i32.store8 (i32.const 200) - (i32.wrap_i64 - (i64.add (i64.const 100) (i64.const 155)) - ) - ) - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - ) - ) - "#; - 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"i64")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0[0], 255u8, "100 + 155 = 255"); -} - -// ── 200 actors from same engine all process one message ───────────────────── - -#[test] -fn two_hundred_actors_from_same_engine() { - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let mut addrs = Vec::new(); - for _ in 0..200 { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")) - .build() - .unwrap(); - addrs.push(rt.spawn(actor).unwrap()); - } - - for (i, addr) in addrs.iter().enumerate() { - rt.send_to(*addr, framed_msg(inbox.addr(), &[(i & 0xFF) as u8])).unwrap(); - } - rt.tick(); - - let mut count = 0; - while let Some(_) = inbox.try_recv() { - count += 1; - } - assert_eq!(count, 200, "all 200 actors should echo"); -} - -// ── Handle receives exactly 32 bytes (just address, no payload) ───────────── - -#[test] -fn handle_receives_just_address_no_payload() { - // Send a message that is exactly 32 bytes (just the address header). - // The echo guest will try to send payload of len-32=0 bytes. - 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(); - - // framed_msg with empty payload = just 32-byte address - rt.send_to(addr, framed_msg(inbox.addr(), b"")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().expect("should receive empty echo"); - assert!(msg.0.is_empty(), "echo of empty payload should be empty"); -} - -// ── Guest writes to last byte of linear memory ────────────────────────────── - -#[test] -fn guest_writes_last_byte_of_memory() { - // Guest writes to offset 65535 (last byte of 1 page). - 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) - ;; Write to last byte of memory - (i32.store8 (i32.const 65535) (i32.const 77)) - ;; Send that byte - (call $send (local.get $ptr) (i32.const 65535) (i32.const 1)) - ) - ) - "#; - 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"x")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0, vec![77], "should read from last byte of memory"); -} - -// ── Alloc returns ptr in middle of previously allocated region ─────────────── - -#[test] -fn alloc_returns_overlapping_region() { - // Guest's alloc always returns 4096 regardless of previous calls. - // When the host writes message bytes, they always go to the same spot. - // Second message in same tick overwrites first message's data. - // But outbox snapshots, so both sends deliver their respective data. - 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) - ;; Echo: send payload back - (call $send - (local.get $ptr) - (i32.add (local.get $ptr) (i32.const 32)) - (i32.sub (local.get $len) (i32.const 32)) - ) - ) - ) - "#; - 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"AAA")).unwrap(); - rt.send_to(addr, framed_msg(inbox.addr(), b"BB")).unwrap(); - rt.tick(); - - let msg1 = inbox.try_recv().unwrap(); - let msg2 = inbox.try_recv().unwrap(); - // Both should reflect their original data despite using same alloc region - assert_eq!(msg1.0, b"AAA", "first echo should have first payload"); - assert_eq!(msg2.0, b"BB", "second echo should have second payload"); -} - -// ── Module exports memory with non-default name (should fail) ─────────────── - -#[test] -fn memory_exported_with_wrong_name_fails() { - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "heap") 1) - (func (export "alloc") (param i32) (result i32) i32.const 0) - (func (export "handle") (param i32 i32)) - ) - "#; - let wasm = wat::parse_str(wat).unwrap(); - let engine = SharedEngine::new().unwrap(); - let result = WasmActorBuilder::new(engine, wasm).build(); - assert!(result.is_err(), "memory exported as 'heap' should fail"); -} - -// ── WASM actor receives messages from multiple senders (fan-in) ───────────── - -#[test] -fn fan_in_from_multiple_native_senders() { - // 5 native actors all send to the same WASM echo actor. - // Echo sends responses back to their respective inboxes. - let engine = SharedEngine::new().unwrap(); - let echo = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let echo_addr = rt.spawn(echo).unwrap(); - - let mut inboxes = Vec::new(); - for i in 0u8..5 { - let inbox = rt.new_inbox::().unwrap(); - rt.send_to(echo_addr, framed_msg(inbox.addr(), &[i])).unwrap(); - inboxes.push(inbox); - } - rt.tick(); - - for (i, inbox) in inboxes.iter().enumerate() { - let msg = inbox.try_recv().unwrap_or_else(|| panic!("inbox {i} should receive")); - assert_eq!(msg.0, vec![i as u8], "inbox {i} should get correct payload"); - } -} - -// ── Module with multiple functions calling each other ──────────────────────── - -#[test] -fn module_with_internal_function_calls() { - // Guest has helper functions called from handle. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (func $double (param $x i32) (result i32) - (i32.mul (local.get $x) (i32.const 2)) - ) - (func $add_ten (param $x i32) (result i32) - (i32.add (local.get $x) (i32.const 10)) - ) - (func (export "alloc") (param i32) (result i32) i32.const 4096) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Read first payload byte, double it, add 10 - (i32.store8 (i32.const 200) - (call $add_ten - (call $double - (i32.load8_u (i32.add (local.get $ptr) (i32.const 32))) - ) - ) - ) - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - ) - ) - "#; - 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(); - - // input=5: double(5)=10, add_ten(10)=20 - rt.send_to(addr, framed_msg(inbox.addr(), &[5])).unwrap(); - // input=100: double(100)=200, add_ten(200)=210 - rt.send_to(addr, framed_msg(inbox.addr(), &[100])).unwrap(); - rt.tick(); - - let msg1 = inbox.try_recv().unwrap(); - let msg2 = inbox.try_recv().unwrap(); - assert_eq!(msg1.0[0], 20); - assert_eq!(msg2.0[0], 210); -} - -// ── Build from pre-compiled WAT bytes (no guest dir needed) ───────────────── - -#[test] -fn build_from_raw_wat_bytes() { - let wat = r#" - (module - (memory (export "memory") 1) - (func (export "alloc") (param i32) (result i32) i32.const 0) - (func (export "handle") (param i32 i32)) - ) - "#; - // Convert WAT → WASM at runtime - let wasm = wat::parse_str(wat).unwrap(); - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, wasm).build().unwrap(); - - // Verify it works (silent actor — no sends) - 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"silent")).unwrap(); - rt.tick(); - assert!(inbox.try_recv().is_none(), "silent actor should not send anything"); -} - -// ── Rapidly build and discard actors without spawning ──────────────────────── - -#[test] -fn build_and_discard_100_actors() { - // Build 100 actors but don't spawn them. Tests that WasmActor drops cleanly. - let engine = SharedEngine::new().unwrap(); - for _ in 0..100 { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")) - .build() - .unwrap(); - drop(actor); - } - // No panic = success -} - -// ── Guest with f32/f64 floating point ops ─────────────────────────────────── - -#[test] -fn handle_uses_floating_point() { - // Guest performs f64 arithmetic and stores result as i32. - 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) - ;; Compute floor(3.14 * 10) = floor(31.4) = 31 - (i32.store8 (i32.const 200) - (i32.trunc_f64_s - (f64.mul (f64.const 3.14) (f64.const 10.0)) - ) - ) - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - ) - ) - "#; - 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"float")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0[0], 31, "floor(3.14 * 10) = 31"); -} - -// ── Empty WASM bytes produces error ───────────────────────────────────────── - -#[test] -fn empty_wasm_bytes_error() { - let engine = SharedEngine::new().unwrap(); - let result = WasmActorBuilder::new(engine, Vec::::new()).build(); - assert!(result.is_err(), "empty bytes should fail"); -} - -// ── Guest table with funcref (call_indirect already tested, but table.get) ── - -#[test] -fn module_with_funcref_table_works() { - // Table of function references used for indirect dispatch. - // Reference types are disabled, but funcref tables should work - // since they're part of the MVP spec. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (type $handler (func (param i32 i32 i32))) - - (func $send_A (param $dest i32) (param $payload i32) (param $len i32) - (i32.store8 (i32.const 200) (i32.const 65)) ;; 'A' - (call $send (local.get $dest) (i32.const 200) (i32.const 1)) - ) - (func $send_B (param $dest i32) (param $payload i32) (param $len i32) - (i32.store8 (i32.const 200) (i32.const 66)) ;; 'B' - (call $send (local.get $dest) (i32.const 200) (i32.const 1)) - ) - - (table 2 funcref) - (elem (i32.const 0) $send_A $send_B) - - (func (export "alloc") (param i32) (result i32) i32.const 4096) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Dispatch based on first payload byte: 0→send_A, 1→send_B - (call_indirect (type $handler) - (local.get $ptr) ;; dest - (i32.const 0) ;; unused payload - (i32.const 0) ;; unused len - (i32.load8_u (i32.add (local.get $ptr) (i32.const 32))) ;; table index - ) - ) - ) - "#; - 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(), &[0])).unwrap(); // → 'A' - rt.send_to(addr, framed_msg(inbox.addr(), &[1])).unwrap(); // → 'B' - rt.tick(); - - let msg1 = inbox.try_recv().unwrap(); - let msg2 = inbox.try_recv().unwrap(); - assert_eq!(msg1.0, b"A"); - assert_eq!(msg2.0, b"B"); -} - -// ── Concurrent build from multiple threads ────────────────────────────────── - -#[test] -fn concurrent_build_from_shared_engine() { - use std::thread; - - let engine = SharedEngine::new().unwrap(); - let wasm_bytes = guest_wasm("echo"); - - let handles: Vec<_> = (0..4) - .map(|_| { - let e = engine.clone(); - let w = wasm_bytes.clone(); - thread::spawn(move || { - WasmActorBuilder::new(e, w).build().unwrap() - }) - }) - .collect(); - - let actors: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect(); - - // All 4 actors should work on the same runtime - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - for actor in actors { - let addr = rt.spawn(actor).unwrap(); - rt.send_to(addr, framed_msg(inbox.addr(), b"concurrent")).unwrap(); - } - rt.tick(); - - let mut count = 0; - while let Some(_) = inbox.try_recv() { - count += 1; - } - assert_eq!(count, 4, "all 4 concurrently-built actors should work"); -} - -// ── Guest uses memory.copy for bulk data move ─────────────────────────────── - -#[test] -fn guest_uses_memory_copy_for_response() { - // Guest copies the entire message to a response buffer using memory.copy, - // then sends the payload portion 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) - ;; Copy entire message to offset 8192 - (memory.copy (i32.const 8192) (local.get $ptr) (local.get $len)) - ;; Send payload (offset 8192+32) back to dest (offset 8192) - (call $send - (i32.const 8192) - (i32.add (i32.const 8192) (i32.const 32)) - (i32.sub (local.get $len) (i32.const 32)) - ) - ) - ) - "#; - 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"bulk-copy-test")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0, b"bulk-copy-test"); -} - -// ── WasmActorError is Send + Sync ────────────────────────────────────────── - -#[test] -fn wasm_actor_error_is_send_and_sync() { - fn assert_send_sync() {} - assert_send_sync::(); -} - -// ── Message with all 256 byte values round-trips correctly ────────────────── - -#[test] -fn all_256_byte_values_round_trip() { - 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(); - - // Send all 256 byte values as payload - let payload: Vec = (0..=255).collect(); - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0.len(), 256, "should receive all 256 bytes"); - for (i, &byte) in msg.0.iter().enumerate() { - assert_eq!(byte, i as u8, "byte {i} should be {i}"); - } -} - -// ── Guest uses memory.fill to initialize a region ─────────────────────────── - -#[test] -fn guest_uses_memory_fill_for_response() { - // Guest fills a region with a constant byte and sends it. - 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) - ;; Fill 10 bytes at offset 200 with value 0x42 ('B') - (memory.fill (i32.const 200) (i32.const 0x42) (i32.const 10)) - (call $send (local.get $ptr) (i32.const 200) (i32.const 10)) - ) - ) - "#; - 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"fill")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0, vec![0x42; 10], "should receive 10 'B' bytes"); -} - -// ── WASM actor processes messages after watcher is installed ───────────────── - -#[test] -fn wasm_actor_works_normally_while_being_watched() { - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - - let exit_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); - let exit_clone = exit_count.clone(); - - struct WatchAndCount2 { - target: Option, - count: std::sync::Arc, - } - impl ActorInterface for WatchAndCount2 { - type Incoming = ByteMessage; - type Response = (); - fn handle(&mut self, ctx: &Ctx, _msg: ByteMessage) { - if let Some(t) = self.target.take() { - ctx.watch(t); - } - } - fn on_actor_exit(&mut self, _ctx: &Ctx, _exited: swactor::actor::ActorExited) { - self.count.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - } - } - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let wasm_addr = rt.spawn(actor).unwrap(); - - let watcher = WatchAndCount2 { target: Some(wasm_addr), count: exit_clone }; - let watcher_addr = rt.spawn(watcher).unwrap(); - - // Install the watch - rt.send_to(watcher_addr, ByteMessage(vec![])).unwrap(); - rt.tick(); - - // WASM actor should still work normally - for i in 0u8..5 { - rt.send_to(wasm_addr, framed_msg(inbox.addr(), &[i])).unwrap(); - } - rt.tick(); - - let mut received = Vec::new(); - while let Some(msg) = inbox.try_recv() { - received.push(msg.0[0]); - } - assert_eq!(received, vec![0, 1, 2, 3, 4], "actor should work normally while watched"); - - // Stop it — watcher should be notified - rt.stop_actor(wasm_addr); - rt.tick(); - rt.tick(); - assert_eq!(exit_count.load(std::sync::atomic::Ordering::SeqCst), 1); -} - -// ── Double guest processes 0xFF payload (boundary byte value) ─────────────── - -#[test] -fn double_guest_with_max_byte_value() { - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("double")).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(), &[0xFF])).unwrap(); - rt.tick(); - - let msg1 = inbox.try_recv().expect("first double"); - let msg2 = inbox.try_recv().expect("second double"); - assert_eq!(msg1.0, vec![0xFF]); - assert_eq!(msg2.0, vec![0xFF]); - assert!(inbox.try_recv().is_none(), "exactly two copies"); -} - -// ── ByteMessage supports large messages (4KB) ─────────────────────────────── - -#[test] -fn large_4kb_message_round_trips() { - 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(); - - // 4KB payload (each byte = position mod 256) - let payload: Vec = (0..4096).map(|i| (i & 0xFF) as u8).collect(); - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0.len(), 4096); - assert_eq!(msg.0, payload); -} - -// ── Guest that sends back payload length as response ──────────────────────── - -#[test] -fn guest_reports_payload_length() { - // Guest reads the payload length (len-32) and sends it back as a 4-byte LE integer. - 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) - ;; Store payload_len = len - 32 as i32 at offset 200 - (i32.store (i32.const 200) (i32.sub (local.get $len) (i32.const 32))) - (call $send (local.get $ptr) (i32.const 200) (i32.const 4)) - ) - ) - "#; - 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(), &[0u8; 100])).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - let len = i32::from_le_bytes([msg.0[0], msg.0[1], msg.0[2], msg.0[3]]); - assert_eq!(len, 100, "guest should report payload length of 100"); -} - -// ── Alloc returns 1 (odd alignment) — still works ────────────────────────── - -#[test] -fn alloc_returns_odd_alignment() { - // Guest alloc returns 1 (not aligned). Host should still write correctly. - 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 1) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Echo from offset 1 - (call $send - (local.get $ptr) - (i32.add (local.get $ptr) (i32.const 32)) - (i32.sub (local.get $len) (i32.const 32)) - ) - ) - ) - "#; - 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"odd-align")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0, b"odd-align"); -} - -// ── SharedEngine clone + Debug ────────────────────────────────────────────── - -#[test] -fn shared_engine_clone_and_debug() { - let engine = SharedEngine::new().unwrap(); - let cloned = engine.clone(); - let debug1 = format!("{:?}", engine); - let debug2 = format!("{:?}", cloned); - assert_eq!(debug1, debug2, "cloned engine should have same debug repr"); -} - -// ── Multiple sequential ticks without messages don't affect WASM actor ────── - -#[test] -fn idle_ticks_dont_affect_wasm_actor() { - 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(); - - // 500 idle ticks - for _ in 0..500 { - rt.tick(); - } - - // Should still work - rt.send_to(addr, framed_msg(inbox.addr(), b"after-idle")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0, b"after-idle"); -} - -// ── WASM + native actors alternate processing in same tick ────────────────── - -#[test] -fn wasm_and_native_alternate_in_same_tick() { - struct NativeEcho2; - impl ActorInterface for NativeEcho2 { - type Incoming = ByteMessage; - type Response = (); - fn handle(&mut self, ctx: &Ctx, msg: ByteMessage) { - if msg.0.len() >= 32 { - let mut addr_bytes = [0u8; 32]; - addr_bytes.copy_from_slice(&msg.0[..32]); - let dest = ActorAddress(addr_bytes); - let payload = msg.0[32..].to_vec(); - let _ = ctx.send(dest, ByteMessage(payload)); - } - } - } - - let engine = SharedEngine::new().unwrap(); - let wasm_echo = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let wasm_addr = rt.spawn(wasm_echo).unwrap(); - let native_addr = rt.spawn(NativeEcho2).unwrap(); - - for i in 0u8..10 { - if i % 2 == 0 { - rt.send_to(wasm_addr, framed_msg(inbox.addr(), &[i])).unwrap(); - } else { - rt.send_to(native_addr, framed_msg(inbox.addr(), &[i])).unwrap(); - } - } - rt.tick(); - - let mut received = Vec::new(); - while let Some(msg) = inbox.try_recv() { - received.push(msg.0[0]); - } - received.sort(); - let expected: Vec = (0..10).collect(); - assert_eq!(received, expected, "all 10 messages from both types should deliver"); -} - -// ── Guest sums two payload bytes ──────────────────────────────────────────── - -#[test] -fn guest_sums_two_payload_bytes() { - 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 $val i32) - (local.set $val - (i32.add - (i32.load8_u (i32.add (local.get $ptr) (i32.const 32))) - (i32.load8_u (i32.add (local.get $ptr) (i32.const 33))) - ) - ) - (i32.store8 (i32.const 200) (local.get $val)) - (i32.store8 (i32.const 201) (local.get $val)) - (call $send (local.get $ptr) (i32.const 200) (i32.const 2)) - ) - ) - "#; - 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(), &[30, 12])).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0, vec![42, 42], "30+12=42 duplicated"); -} - -// ── Single byte payload echo ──────────────────────────────────────────────── - -#[test] -fn single_byte_payload_echo() { - 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(); - - rt.send_to(addr, framed_msg(inbox.addr(), &[0x42])).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0, vec![0x42]); -} - -// ── Module with no functions fails ────────────────────────────────────────── - -#[test] -fn module_with_no_functions_fails() { - let wat = "(module (memory (export \"memory\") 1))"; - let wasm = wat::parse_str(wat).unwrap(); - let engine = SharedEngine::new().unwrap(); - assert!(WasmActorBuilder::new(engine, wasm).build().is_err()); -} - -// ── Response varies by message size ───────────────────────────────────────── - -#[test] -fn response_varies_by_message_size() { - 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) - (i32.store8 (i32.const 200) (i32.sub (local.get $len) (i32.const 32))) - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - ) - ) - "#; - 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(), &[])).unwrap(); - rt.send_to(addr, framed_msg(inbox.addr(), &[1])).unwrap(); - rt.send_to(addr, framed_msg(inbox.addr(), &[0; 50])).unwrap(); - rt.send_to(addr, framed_msg(inbox.addr(), &[0; 200])).unwrap(); - rt.tick(); - - let sizes: Vec = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0[0])).collect(); - assert_eq!(sizes, vec![0, 1, 50, 200]); -} - -// ── Property: echo is deterministic ───────────────────────────────────────── - proptest! { #[test] - fn prop_echo_is_deterministic( - payload in proptest::collection::vec(proptest::num::u8::ANY, 0..128) + fn prop_mixed_guest_response_counts( + echo_n in 0usize..5, + double_n in 0usize..5, + silent_n in 0usize..5, ) { let engine = SharedEngine::new().unwrap(); let rt = Runtime::new(RuntimeConfig::default()); let inbox = rt.new_inbox::().unwrap(); - let actor1 = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - let addr1 = rt.spawn(actor1).unwrap(); - rt.send_to(addr1, framed_msg(inbox.addr(), &payload)).unwrap(); - rt.tick(); - let result1 = inbox.try_recv().map(|m| m.0); + for _ in 0..echo_n { + let a = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); + let addr = rt.spawn(a).unwrap(); + rt.send_to(addr, framed_msg(inbox.addr(), b"e")).unwrap(); + } + for _ in 0..double_n { + let a = WasmActorBuilder::new(engine.clone(), guest_wasm("double")).build().unwrap(); + let addr = rt.spawn(a).unwrap(); + rt.send_to(addr, framed_msg(inbox.addr(), b"d")).unwrap(); + } + for _ in 0..silent_n { + let a = WasmActorBuilder::new(engine.clone(), guest_wasm("silent")).build().unwrap(); + let addr = rt.spawn(a).unwrap(); + rt.send_to(addr, ByteMessage(b"s".to_vec())).unwrap(); + } - let actor2 = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - let addr2 = rt.spawn(actor2).unwrap(); - rt.send_to(addr2, framed_msg(inbox.addr(), &payload)).unwrap(); rt.tick(); - let result2 = inbox.try_recv().map(|m| m.0); - - assert_eq!(result1, result2, "same input should produce same output"); + let total: usize = std::iter::from_fn(|| inbox.try_recv()).count(); + prop_assert_eq!(total, echo_n + 2 * double_n); } } -// ── 200th test: comprehensive lifecycle with all guest types ──────────────── - #[test] -fn comprehensive_lifecycle_all_guest_types_200th() { - // Spawn one of each guest type (echo, double, silent), send messages, - // verify outputs, stop them all, check cleanup. - let engine = SharedEngine::new().unwrap(); - let echo = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - let double = WasmActorBuilder::new(engine.clone(), guest_wasm("double")).build().unwrap(); - let silent = WasmActorBuilder::new(engine, guest_wasm("silent")).build().unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let echo_addr = rt.spawn(echo).unwrap(); - let double_addr = rt.spawn(double).unwrap(); - let silent_addr = rt.spawn(silent).unwrap(); - - // Send to all three - rt.send_to(echo_addr, framed_msg(inbox.addr(), b"E")).unwrap(); - rt.send_to(double_addr, framed_msg(inbox.addr(), b"D")).unwrap(); - rt.send_to(silent_addr, framed_msg(inbox.addr(), b"S")).unwrap(); - rt.tick(); - - let mut payloads: Vec> = Vec::new(); - while let Some(msg) = inbox.try_recv() { - payloads.push(msg.0); - } - payloads.sort(); - // Echo → "E" (1x), Double → "D" (2x), Silent → nothing - assert_eq!(payloads, vec![b"D".to_vec(), b"D".to_vec(), b"E".to_vec()]); - - // Stop all - rt.stop_actor(echo_addr); - rt.stop_actor(double_addr); - rt.stop_actor(silent_addr); - rt.tick(); - rt.tick(); - - // Send to stopped actors — silently dropped - rt.send_to(echo_addr, framed_msg(inbox.addr(), b"gone")).ok(); - rt.tick(); - assert!(inbox.try_recv().is_none(), "stopped actors should not deliver"); -} - -// ── Guest alloc returns pointer at exact page boundary ────────────────────── - -#[test] -fn alloc_at_page_boundary_works() { - // Guest alloc returns 65536 - 64 = 65472. With a 64-byte message, - // end = 65472 + 64 = 65536 = memory size. Exactly in bounds. - 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 65472) - (func (export "handle") (param $ptr i32) (param $len i32) - (call $send - (local.get $ptr) - (i32.add (local.get $ptr) (i32.const 32)) - (i32.sub (local.get $len) (i32.const 32)) - ) - ) - ) - "#; - 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(); - - // 32 (addr) + 32 (payload) = 64 bytes → fits exactly at 65472..65536 - let payload = vec![0xAB; 32]; - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().expect("exact page boundary should work"); - assert_eq!(msg.0, payload); -} - -// ── Guest alloc returns pointer 1 byte past page boundary — drops ─────────── - -#[test] -fn alloc_one_past_page_boundary_drops() { - // Guest alloc returns 65473. With 64-byte message: - // end = 65473 + 64 = 65537 > 65536. OOB, message dropped. - 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 65473) - (func (export "handle") (param i32 i32) - unreachable - ) - ) - "#; - 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(); - - let payload = vec![0xAB; 32]; // total msg = 64 bytes - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - rt.tick(); - - assert!(inbox.try_recv().is_none(), "OOB alloc should drop message"); -} - -// ── Multiple runtimes with WASM actors independently ──────────────────────── - -#[test] -fn multiple_runtimes_with_wasm_actors() { - let engine = SharedEngine::new().unwrap(); - - // Runtime 1 - let rt1 = Runtime::new(RuntimeConfig::default()); - let inbox1 = rt1.new_inbox::().unwrap(); - let actor1 = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - let addr1 = rt1.spawn(actor1).unwrap(); - - // Runtime 2 - let rt2 = Runtime::new(RuntimeConfig::default()); - let inbox2 = rt2.new_inbox::().unwrap(); - let actor2 = WasmActorBuilder::new(engine, guest_wasm("double")).build().unwrap(); - let addr2 = rt2.spawn(actor2).unwrap(); - - rt1.send_to(addr1, framed_msg(inbox1.addr(), b"rt1")).unwrap(); - rt2.send_to(addr2, framed_msg(inbox2.addr(), b"rt2")).unwrap(); - rt1.tick(); - rt2.tick(); - - let msg1 = inbox1.try_recv().unwrap(); - assert_eq!(msg1.0, b"rt1"); - - let d1 = inbox2.try_recv().unwrap(); - let d2 = inbox2.try_recv().unwrap(); - assert_eq!(d1.0, b"rt2"); - assert_eq!(d2.0, b"rt2"); -} - -// ── Guest sends back exact copy of the full message (including address) ───── - -#[test] -fn guest_mirrors_full_message() { - // Guest sends back the entire message (address + payload) to the dest. - 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) - ;; Send the full message (including address header) as payload - (call $send (local.get $ptr) (local.get $ptr) (local.get $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(); - - let original_msg = framed_msg(inbox.addr(), b"mirror"); - rt.send_to(addr, original_msg.clone()).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().expect("should receive mirrored full message"); - // Payload = full original message (address + payload) - assert_eq!(msg.0, original_msg.0, "should receive exact copy of full message"); -} - -// ── Guest sends to self (WASM actor address from host) ────────────────────── - -#[test] -fn guest_sends_to_self_via_framed_address() { - // The payload contains the WASM actor's own address as the dest. - // This creates a self-send that should be delivered next tick. +fn non_byte_message_silently_ignored() { + // Send a String (wrong type) to a WASM actor — should be silently dropped 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 wasm_addr = rt.spawn(actor).unwrap(); - - // Frame with wasm_addr as dest, payload is a framed msg for inbox - let inner = framed_msg(inbox.addr(), b"self-bounce"); - rt.send_to(wasm_addr, framed_msg(&wasm_addr, &inner.0)).unwrap(); - rt.tick(); // echo sends inner to itself - rt.tick(); // processes inner, echoes payload to inbox - - let msg = inbox.try_recv().expect("self-send should eventually reach inbox"); - assert_eq!(msg.0, b"self-bounce"); -} - -// ── Guest with i32.rotr/rotl bit rotation ─────────────────────────────────── - -#[test] -fn guest_uses_bit_rotation() { - 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) - ;; Rotate left: 1 << 4 = 16 (i32.rotl(1, 4) = 16) - (i32.store8 (i32.const 200) - (i32.rotl (i32.const 1) (i32.const 4)) - ) - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - ) - ) - "#; - 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"rot")).unwrap(); + // send_to with wrong type — shouldn't panic + let _ = rt.send_to(addr, "wrong type".to_string()); rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0[0], 16, "rotl(1, 4) = 16"); -} - -// ── Guest with i32.clz/ctz/popcnt ────────────────────────────────────────── - -#[test] -fn guest_uses_bit_counting() { - 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) - ;; clz(256) = 23 (256 = 0x100, 23 leading zeros in 32-bit) - (i32.store8 (i32.const 200) (i32.clz (i32.const 256))) - ;; ctz(256) = 8 (256 = 0x100, 8 trailing zeros) - (i32.store8 (i32.const 201) (i32.ctz (i32.const 256))) - ;; popcnt(0xFF) = 8 (8 bits set) - (i32.store8 (i32.const 202) (i32.popcnt (i32.const 255))) - (call $send (local.get $ptr) (i32.const 200) (i32.const 3)) - ) - ) - "#; - 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"bits")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0[0], 23, "clz(256) = 23"); - assert_eq!(msg.0[1], 8, "ctz(256) = 8"); - assert_eq!(msg.0[2], 8, "popcnt(255) = 8"); -} - -// ── Spawn 10 actors, stop every other one, remaining still work ───────────── - -#[test] -fn stop_every_other_actor_remaining_work() { - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let mut addrs = Vec::new(); - for _ in 0..10 { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - addrs.push(rt.spawn(actor).unwrap()); - } - - // Stop even-indexed actors - for (i, addr) in addrs.iter().enumerate() { - if i % 2 == 0 { - rt.stop_actor(*addr); - } - } - rt.tick(); - rt.tick(); - - // Send to all — only odd-indexed should respond - for (i, addr) in addrs.iter().enumerate() { - rt.send_to(*addr, framed_msg(inbox.addr(), &[i as u8])).ok(); - } - rt.tick(); - - let mut received = Vec::new(); - while let Some(msg) = inbox.try_recv() { - received.push(msg.0[0]); - } - received.sort(); - assert_eq!(received, vec![1, 3, 5, 7, 9], "only odd-indexed actors should respond"); -} - -// ── Guest uses i32.store/load (32-bit) for response ───────────────────────── - -#[test] -fn guest_uses_i32_store_load() { - 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) - ;; Store 0x04030201 at offset 200 (little-endian: 01 02 03 04) - (i32.store (i32.const 200) (i32.const 0x04030201)) - (call $send (local.get $ptr) (i32.const 200) (i32.const 4)) - ) - ) - "#; - 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"le")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0, vec![0x01, 0x02, 0x03, 0x04], "i32.store is little-endian"); -} - -// ── Guest with i32.eqz instruction ───────────────────────────────────────── - -#[test] -fn guest_uses_eqz() { - 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 $payload_len i32) - (local.set $payload_len (i32.sub (local.get $len) (i32.const 32))) - ;; eqz: 1 if payload_len == 0, else 0 - (i32.store8 (i32.const 200) (i32.eqz (local.get $payload_len))) - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - ) - ) - "#; - 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"")).unwrap(); // empty payload - rt.send_to(addr, framed_msg(inbox.addr(), b"x")).unwrap(); // non-empty - rt.tick(); - - let msg1 = inbox.try_recv().unwrap(); - let msg2 = inbox.try_recv().unwrap(); - assert_eq!(msg1.0[0], 1, "empty payload → eqz=1"); - assert_eq!(msg2.0[0], 0, "non-empty payload → eqz=0"); -} - -// ── Silent guest processes many messages without any observable effect ─────── - -#[test] -fn silent_guest_processes_1000_messages() { - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("silent")).build().unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - for _ in 0..1000 { - rt.send_to(addr, ByteMessage(vec![0xFF; 100])).unwrap(); - } - - // Process across multiple ticks (budget = 64 default) - for _ in 0..20 { - rt.tick(); - } - - assert!(inbox.try_recv().is_none(), "silent never sends"); - - // Actor should still be alive - rt.send_to(addr, framed_msg(inbox.addr(), b"still-here")).unwrap(); - rt.tick(); - // Still no response from silent assert!(inbox.try_recv().is_none()); -} -// ── Build actor, send before spawn — verify spawn then send works ─────────── - -#[test] -fn send_before_first_tick_delivers() { - 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(); - - // Send immediately after spawn, before any tick - rt.send_to(addr, framed_msg(inbox.addr(), b"pre-tick")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().expect("message sent before first tick should deliver"); - assert_eq!(msg.0, b"pre-tick"); -} - -// ── Guest with i32.rem_u (modulo) ────────────────────────────────────────── - -#[test] -fn guest_uses_modulo() { - 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) - ;; Read first payload byte, compute mod 10 - (i32.store8 (i32.const 200) - (i32.rem_u - (i32.load8_u (i32.add (local.get $ptr) (i32.const 32))) - (i32.const 10) - ) - ) - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - ) - ) - "#; - 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(), &[47])).unwrap(); // 47 % 10 = 7 - rt.send_to(addr, framed_msg(inbox.addr(), &[100])).unwrap(); // 100 % 10 = 0 - rt.send_to(addr, framed_msg(inbox.addr(), &[3])).unwrap(); // 3 % 10 = 3 - rt.tick(); - - let results: Vec = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0[0])).collect(); - assert_eq!(results, vec![7, 0, 3]); -} - -// ── Echo with 8KB payload (tests larger than single page alloc) ───────────── - -#[test] -fn echo_8kb_payload() { - 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..8192).map(|i| (i % 251) as u8).collect(); - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0.len(), 8192); - assert_eq!(msg.0, payload); -} - -// ── Multiple sends with exact same payload to same dest ───────────────────── - -#[test] -fn duplicate_sends_all_deliver() { - 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(); - - // Send the exact same message 5 times - for _ in 0..5 { - rt.send_to(addr, framed_msg(inbox.addr(), b"dup")).unwrap(); - } - rt.tick(); - - let mut count = 0; - while let Some(msg) = inbox.try_recv() { - assert_eq!(msg.0, b"dup"); - count += 1; - } - assert_eq!(count, 5, "all 5 duplicate sends should deliver"); -} - -// ── Property: build + tick + stop cycle never leaks (combined fuzz) ────────── - -proptest! { - #[test] - fn prop_full_lifecycle_never_panics( - n_actors in 1u8..5, - n_msgs in 0u8..10, - payload_byte in proptest::num::u8::ANY, - ) { - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let mut addrs = Vec::new(); - for _ in 0..n_actors { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - addrs.push(rt.spawn(actor).unwrap()); - } - - for addr in &addrs { - for _ in 0..n_msgs { - let _ = rt.send_to(*addr, framed_msg(inbox.addr(), &[payload_byte])); - } - } - rt.tick(); - rt.tick(); - - for addr in &addrs { - rt.stop_actor(*addr); - } - rt.tick(); - rt.tick(); - - while let Some(_) = inbox.try_recv() {} - } -} - -// ── Guest with i32.xor for byte transformation ───────────────────────────── - -#[test] -fn guest_xor_transform_key() { - 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))) - (i32.store8 - (i32.add (i32.const 200) (local.get $i)) - (i32.xor - (i32.load8_u (i32.add (local.get $payload_start) (local.get $i))) - (i32.const 0xAA) - ) - ) - (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(), &[0x00, 0xFF, 0x55, 0xAA])).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0, vec![0xAA, 0x55, 0xFF, 0x00], "XOR with 0xAA"); -} - -// ── ActorAddress reconstruction from raw bytes ────────────────────────────── - -#[test] -fn actor_address_reconstructible_from_bytes() { - 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 reconstructed = ActorAddress(addr.0); - assert_eq!(addr, reconstructed); - - rt.send_to(reconstructed, framed_msg(inbox.addr(), b"reconstructed")).unwrap(); - rt.tick(); - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0, b"reconstructed"); -} - -// ── Double guest + watch fires once on stop ───────────────────────────────── - -#[test] -fn double_guest_watch_fires_once_on_stop() { - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("double")).build().unwrap(); - - let exit_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); - let ec = exit_count.clone(); - - struct WatchCounter3 { - target: Option, - count: std::sync::Arc, - } - impl ActorInterface for WatchCounter3 { - type Incoming = ByteMessage; - type Response = (); - fn handle(&mut self, ctx: &Ctx, _msg: ByteMessage) { - if let Some(t) = self.target.take() { ctx.watch(t); } - } - fn on_actor_exit(&mut self, _ctx: &Ctx, _exited: swactor::actor::ActorExited) { - self.count.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - } - } - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let double_addr = rt.spawn(actor).unwrap(); - let w_addr = rt.spawn(WatchCounter3 { target: Some(double_addr), count: ec }).unwrap(); - - rt.send_to(w_addr, ByteMessage(vec![])).unwrap(); - rt.tick(); - - rt.send_to(double_addr, framed_msg(inbox.addr(), b"D")).unwrap(); - rt.tick(); - assert_eq!(inbox.try_recv().unwrap().0, b"D"); - assert_eq!(inbox.try_recv().unwrap().0, b"D"); - - rt.stop_actor(double_addr); - rt.tick(); - rt.tick(); - assert_eq!(exit_count.load(std::sync::atomic::Ordering::SeqCst), 1); -} - -// ── ByteMessage constructors ──────────────────────────────────────────────── - -#[test] -fn byte_message_various_constructors() { - let msg1 = ByteMessage("hello".as_bytes().to_vec()); - assert_eq!(msg1.0, b"hello"); - let msg2 = ByteMessage(Vec::new()); - assert!(msg2.0.is_empty()); - let msg3 = ByteMessage(vec![0; 1024]); - assert_eq!(msg3.0.len(), 1024); -} - -// ── Guest with i32.shl/shr_u shift operations ───────────────────────────── - -#[test] -fn guest_uses_shift_operations() { - 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) - ;; Read first payload byte, shift left by 1, store result - (i32.store8 (i32.const 200) - (i32.shl - (i32.load8_u (i32.add (local.get $ptr) (i32.const 32))) - (i32.const 1) - ) - ) - ;; Read second payload byte, shift right by 2 - (i32.store8 (i32.const 201) - (i32.shr_u - (i32.load8_u (i32.add (local.get $ptr) (i32.const 33))) - (i32.const 2) - ) - ) - (call $send (local.get $ptr) (i32.const 200) (i32.const 2)) - ) - ) - "#; - 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(), &[5, 100])).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0[0], 10, "5 << 1 = 10"); - assert_eq!(msg.0[1], 25, "100 >> 2 = 25"); -} - -// ── Guest with i32.and/or for masking ────────────────────────────────────── - -#[test] -fn guest_uses_bitwise_and_or() { - 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) - ;; AND: 0xFF & 0x0F = 0x0F - (i32.store8 (i32.const 200) (i32.and (i32.const 0xFF) (i32.const 0x0F))) - ;; OR: 0xF0 | 0x0F = 0xFF - (i32.store8 (i32.const 201) (i32.or (i32.const 0xF0) (i32.const 0x0F))) - (call $send (local.get $ptr) (i32.const 200) (i32.const 2)) - ) - ) - "#; - 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"mask")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0[0], 0x0F, "0xFF & 0x0F = 0x0F"); - assert_eq!(msg.0[1], 0xFF, "0xF0 | 0x0F = 0xFF"); -} - -// ── Two WASM actors with different guest modules on same worker ───────────── - -#[test] -fn echo_and_double_coexist_same_worker() { - 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 = 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.addr(), b"E")).unwrap(); - rt.send_to(d_addr, framed_msg(inbox.addr(), b"D")).unwrap(); - rt.tick(); - - let mut msgs: Vec> = Vec::new(); - while let Some(msg) = inbox.try_recv() { - msgs.push(msg.0); - } - msgs.sort(); - // Echo: "E" (1x), Double: "D" (2x) - assert_eq!(msgs, vec![b"D".to_vec(), b"D".to_vec(), b"E".to_vec()]); -} - -// ── Guest reads i16 from payload (i32.load16_u) ──────────────────────────── - -#[test] -fn guest_reads_i16_from_payload() { - 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) - ;; Read 2-byte LE value from first payload bytes - (local $val i32) - (local.set $val - (i32.load16_u (i32.add (local.get $ptr) (i32.const 32))) - ) - ;; Store low byte of the i16 value - (i32.store8 (i32.const 200) (local.get $val)) - ;; Store high byte - (i32.store8 (i32.const 201) (i32.shr_u (local.get $val) (i32.const 8))) - (call $send (local.get $ptr) (i32.const 200) (i32.const 2)) - ) - ) - "#; - 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(); - - // Send 0x0102 as little-endian: [0x02, 0x01] - rt.send_to(addr, framed_msg(inbox.addr(), &[0x02, 0x01])).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - // i32.load16_u reads LE: 0x0102 - assert_eq!(msg.0[0], 0x02, "low byte of 0x0102"); - assert_eq!(msg.0[1], 0x01, "high byte of 0x0102"); -} - -// ── Repeated build from same bytes yields independent actors ──────────────── - -#[test] -fn repeated_build_same_bytes_independent() { - let engine = SharedEngine::new().unwrap(); - let wasm = guest_wasm("echo"); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - // Build 3 actors from exact same bytes - let a1 = rt.spawn(WasmActorBuilder::new(engine.clone(), wasm.clone()).build().unwrap()).unwrap(); - let a2 = rt.spawn(WasmActorBuilder::new(engine.clone(), wasm.clone()).build().unwrap()).unwrap(); - let a3 = rt.spawn(WasmActorBuilder::new(engine, wasm).build().unwrap()).unwrap(); - - // Stop a2 — a1 and a3 should still work - rt.stop_actor(a2); - rt.tick(); - rt.tick(); - - rt.send_to(a1, framed_msg(inbox.addr(), b"a1")).unwrap(); - rt.send_to(a3, framed_msg(inbox.addr(), b"a3")).unwrap(); - rt.tick(); - - let mut msgs: Vec> = Vec::new(); - while let Some(msg) = inbox.try_recv() { - msgs.push(msg.0); - } - msgs.sort(); - assert_eq!(msgs, vec![b"a1".to_vec(), b"a3".to_vec()]); -} - -// ── Guest writes beyond alloc region (within memory) — reads stale data ───── - -#[test] -fn guest_reads_stale_memory_region() { - // Guest's alloc returns 4096, but the handle reads from offset 0 (outside alloc region). - // Memory at offset 0 was never written by the host for this message, - // but may have been written by a previous message. Tests that reading - // arbitrary memory is safe (no crash, just potentially stale data). - 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) - ;; Send 4 bytes from offset 0 (stale/zero memory) - (call $send (local.get $ptr) (i32.const 0) (i32.const 4)) - ) - ) - "#; - 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"trigger")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().expect("reading stale memory is valid"); - assert_eq!(msg.0.len(), 4, "should receive 4 bytes"); - // Content is zero (fresh memory) — but we don't assert exact values - // since they could be anything in theory -} - -// ── Property: any combination of guest modules processes without panic ────── - -proptest! { - #[test] - fn prop_any_guest_module_processes_safely( - guest_idx in 0usize..3, - n_msgs in 0u8..8, - ) { - let guests = ["echo", "double", "silent"]; - let guest_name = guests[guest_idx]; - - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm(guest_name)).build().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - for i in 0..n_msgs { - let _ = rt.send_to(addr, framed_msg(inbox.addr(), &[i])); - } - rt.tick(); - while let Some(_) = inbox.try_recv() {} - } -} - -// ── Guest with nested function calls (3 deep) ────────────────────────────── - -#[test] -fn three_level_nested_function_calls() { - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (func $inc (param $x i32) (result i32) - (i32.add (local.get $x) (i32.const 1)) - ) - (func $double_inc (param $x i32) (result i32) - (call $inc (call $inc (local.get $x))) - ) - (func $transform (param $x i32) (result i32) - (i32.mul (call $double_inc (local.get $x)) (i32.const 3)) - ) - (func (export "alloc") (param i32) (result i32) i32.const 4096) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Read first byte, transform: (x+2)*3 - (i32.store8 (i32.const 200) - (call $transform - (i32.load8_u (i32.add (local.get $ptr) (i32.const 32))) - ) - ) - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - ) - ) - "#; - 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(); - - // input=10: (10+2)*3 = 36 - rt.send_to(addr, framed_msg(inbox.addr(), &[10])).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0[0], 36, "(10+2)*3 = 36"); -} - -// ── Guest with grow + use new page for send ───────────────────────────────── - -#[test] -fn grow_memory_and_use_new_page_for_send() { - // Guest grows memory in handle, then uses the new page for the send dest. - 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) - ;; Grow memory by 1 page - (drop (memory.grow (i32.const 1))) - ;; Copy dest address from message to new page (offset 65536) - (memory.copy (i32.const 65536) (local.get $ptr) (i32.const 32)) - ;; Write payload at offset 65600 - (i32.store8 (i32.const 65600) (i32.const 99)) - ;; Send from new page - (call $send (i32.const 65536) (i32.const 65600) (i32.const 1)) - ) - ) - "#; - 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"grow")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().expect("send from grown page should work"); - assert_eq!(msg.0, vec![99]); -} - -// ── Stress: 20 actors each processing 20 messages ────────────────────────── - -#[test] -fn twenty_actors_twenty_messages_each() { - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let mut addrs = Vec::new(); - for _ in 0..20 { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - addrs.push(rt.spawn(actor).unwrap()); - } - - for (a, addr) in addrs.iter().enumerate() { - for m in 0u8..20 { - rt.send_to(*addr, framed_msg(inbox.addr(), &[a as u8, m])).unwrap(); - } - } - - // Process across multiple ticks (budget=64 per actor) - for _ in 0..10 { - rt.tick(); - } - - let mut total = 0; - while let Some(_) = inbox.try_recv() { - total += 1; - } - assert_eq!(total, 400, "20 actors × 20 messages = 400 responses"); -} - -// ── Module with multiple imports (only swactor.send matters) ──────────────── - -#[test] -fn module_with_only_send_import_needed() { - // Module only imports swactor.send, no other imports. - // This is the minimal valid module that can send. - 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) - (i32.store8 (i32.const 200) (i32.const 42)) - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - ) - ) - "#; - 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"min")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0, vec![42]); -} - -// ── Guest with memory.init-like pattern using data segments ───────────────── - -#[test] -fn guest_data_segment_used_as_template() { - // Guest has a data segment template and sends it as-is. - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (data (i32.const 300) "TEMPLATE") - (func (export "alloc") (param i32) (result i32) i32.const 4096) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Send the data segment content as payload - (call $send (local.get $ptr) (i32.const 300) (i32.const 8)) - ) - ) - "#; - 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"x")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0, b"TEMPLATE"); -} - -// ── Send to inbox address directly (bypass framing) ───────────────────────── - -#[test] -fn send_raw_bytes_to_inbox() { - // ByteMessage can hold any bytes — send raw bytes directly to inbox. - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - rt.send_to(*inbox.addr(), ByteMessage(b"raw-direct".to_vec())).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0, b"raw-direct"); -} - -// ── Guest with multiple data segments at different offsets ────────────────── - -#[test] -fn multiple_data_segments_different_offsets() { - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (data (i32.const 300) "AAA") - (data (i32.const 400) "BBB") - (data (i32.const 500) "CCC") - (func (export "alloc") (param i32) (result i32) i32.const 4096) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Copy all three segments into response buffer - (memory.copy (i32.const 200) (i32.const 300) (i32.const 3)) - (memory.copy (i32.const 203) (i32.const 400) (i32.const 3)) - (memory.copy (i32.const 206) (i32.const 500) (i32.const 3)) - (call $send (local.get $ptr) (i32.const 200) (i32.const 9)) - ) - ) - "#; - 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"x")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0, b"AAABBBCCC"); -} - -// ── Echo actor processes messages across 100 separate ticks ───────────────── - -#[test] -fn echo_across_100_separate_ticks() { - 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(); - - for i in 0u8..100 { - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - rt.tick(); - } - - let received: Vec = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0[0])).collect(); - let expected: Vec = (0..100).collect(); - assert_eq!(received, expected); -} - -// ── Guest uses nop instruction ────────────────────────────────────────────── - -#[test] -fn guest_with_nop_instructions() { - 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) - nop nop nop nop nop - (i32.store8 (i32.const 200) (i32.const 77)) - nop nop - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - nop - ) - ) - "#; - 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"nop")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0, vec![77], "nops should not affect behavior"); -} - -// ── Interleaved spawn and message delivery ────────────────────────────────── - -#[test] -fn interleaved_spawn_and_message_delivery() { - // Spawn actor, send message, spawn another, send message, tick once. - // Both should process in the same tick. - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let a1 = rt.spawn(WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap()).unwrap(); - rt.send_to(a1, framed_msg(inbox.addr(), b"first")).unwrap(); - let a2 = rt.spawn(WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap()).unwrap(); - rt.send_to(a2, framed_msg(inbox.addr(), b"second")).unwrap(); - rt.tick(); - - let mut msgs: Vec> = Vec::new(); - while let Some(msg) = inbox.try_recv() { - msgs.push(msg.0); - } - msgs.sort(); - assert_eq!(msgs, vec![b"first".to_vec(), b"second".to_vec()]); -} - -// ── Guest with i32.store16 ───────────────────────────────────────────────── - -#[test] -fn guest_uses_i32_store16() { - 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) - ;; Store 0x1234 as 16-bit LE at offset 200 - (i32.store16 (i32.const 200) (i32.const 0x1234)) - (call $send (local.get $ptr) (i32.const 200) (i32.const 2)) - ) - ) - "#; - 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"s16")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0, vec![0x34, 0x12], "i32.store16 should be little-endian"); -} - -// ── Guest computes min/max of two payload bytes ───────────────────────────── - -#[test] -fn guest_computes_min_max() { - 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 $a i32) (local $b i32) - (local.set $a (i32.load8_u (i32.add (local.get $ptr) (i32.const 32)))) - (local.set $b (i32.load8_u (i32.add (local.get $ptr) (i32.const 33)))) - ;; min: use select - (i32.store8 (i32.const 200) - (select (local.get $a) (local.get $b) - (i32.lt_u (local.get $a) (local.get $b))) - ) - ;; max: use select - (i32.store8 (i32.const 201) - (select (local.get $a) (local.get $b) - (i32.gt_u (local.get $a) (local.get $b))) - ) - (call $send (local.get $ptr) (i32.const 200) (i32.const 2)) - ) - ) - "#; - 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(), &[42, 99])).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0[0], 42, "min(42, 99) = 42"); - assert_eq!(msg.0[1], 99, "max(42, 99) = 99"); -} - -// ── Drop runtime with live WASM actors — no leak/panic ────────────────────── - -#[test] -fn drop_runtime_with_live_actors() { - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - - for _ in 0..10 { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - let addr = rt.spawn(actor).unwrap(); - rt.send_to(addr, ByteMessage(vec![1, 2, 3])).unwrap(); - } - rt.tick(); - - // Drop runtime without stopping actors — should not panic or leak - drop(rt); -} - -// ── Guest with immutable global ───────────────────────────────────────────── - -#[test] -fn guest_with_immutable_global() { - let wat = r#" - (module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (global $magic i32 (i32.const 0xBE)) - - (func (export "alloc") (param i32) (result i32) i32.const 4096) - (func (export "handle") (param $ptr i32) (param $len i32) - (i32.store8 (i32.const 200) (global.get $magic)) - (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) - ) - ) - "#; - 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"g")).unwrap(); - rt.tick(); - - let msg = inbox.try_recv().unwrap(); - assert_eq!(msg.0[0], 0xBE, "immutable global should return configured value"); -} - -// ── Multiple echo round trips (message ping-pong through actor) ───────────── - -#[test] -fn multiple_echo_round_trips() { - 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(); - - // Send, receive, send the response back to actor, repeat 5 times - let mut current_payload = b"ping-0".to_vec(); - for i in 0..5 { - rt.send_to(addr, framed_msg(inbox.addr(), ¤t_payload)).unwrap(); - rt.tick(); - let msg = inbox.try_recv().unwrap_or_else(|| panic!("round {i} should echo")); - assert_eq!(msg.0, current_payload); - current_payload = format!("ping-{}", i + 1).into_bytes(); - } -} - -// ── WasmActorBuilder consumed on build — verify not Clone ─────────────────── - -#[test] -fn builder_is_consumed_on_build() { - // This test verifies the builder pattern by building twice from same config. - // Each build call consumes the builder. - let engine = SharedEngine::new().unwrap(); - let wasm = guest_wasm("echo"); - - let builder1 = WasmActorBuilder::new(engine.clone(), wasm.clone()); - let _actor1 = builder1.build().unwrap(); - // builder1 is consumed — can't call build again (compile error if tried) - - let builder2 = WasmActorBuilder::new(engine, wasm); - let _actor2 = builder2.build().unwrap(); - // Both built successfully from same config -} - -// ── Empty tick between messages doesn't lose them ─────────────────────────── - -#[test] -fn empty_tick_between_sends_preserves_order() { - 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(); - - rt.send_to(addr, framed_msg(inbox.addr(), b"A")).unwrap(); - rt.tick(); - rt.tick(); // empty tick - rt.send_to(addr, framed_msg(inbox.addr(), b"B")).unwrap(); - rt.tick(); - rt.tick(); // empty tick - rt.send_to(addr, framed_msg(inbox.addr(), b"C")).unwrap(); - rt.tick(); - - 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(); -} - -// ── Cycle 59 ───────────────────────────────────────────────────────────────── - -// Guest that uses local.tee instruction (sets local and leaves value on stack) -#[test] -fn guest_uses_local_tee() { - let wat = r#"(module - (memory (export "memory") 1) - (global $heap (mut i32) (i32.const 65536)) - (func (export "alloc") (param $len i32) (result i32) - global.get $heap - global.get $heap - local.get $len - i32.add - global.set $heap) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Use local.tee: ptr2 = alloc_start, also keep on stack - (local $ptr2 i32) - global.get $heap - local.tee $ptr2 - drop ;; just exercising the instruction - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(b"tee".to_vec())).unwrap(); - rt.tick(); // no trap -} - -// Guest that writes a 16KB response — tests larger-than-page payloads round-trip -#[test] -fn sixteen_kb_payload_round_trip() { - 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..16384u32).map(|i| (i % 251) as u8).collect(); - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - rt.tick(); - - let resp = inbox.try_recv().expect("should receive 16KB echo"); - assert_eq!(resp.0, payload); -} - -// Two runtimes with WASM actors operating independently at the same time -#[test] -fn two_independent_runtimes_with_wasm_actors() { - let engine = SharedEngine::new().unwrap(); - let actor1 = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - let actor2 = WasmActorBuilder::new(engine, guest_wasm("double")).build().unwrap(); - - let rt1 = Runtime::new(RuntimeConfig::default()); - let rt2 = Runtime::new(RuntimeConfig::default()); - - let inbox1 = rt1.new_inbox::().unwrap(); - let inbox2 = rt2.new_inbox::().unwrap(); - - let addr1 = rt1.spawn(actor1).unwrap(); - let addr2 = rt2.spawn(actor2).unwrap(); - - rt1.send_to(addr1, framed_msg(inbox1.addr(), b"A")).unwrap(); - rt2.send_to(addr2, framed_msg(inbox2.addr(), b"B")).unwrap(); - - rt1.tick(); - rt2.tick(); - - let msgs1: Vec<_> = std::iter::from_fn(|| inbox1.try_recv().map(|m| m.0)).collect(); - let msgs2: Vec<_> = std::iter::from_fn(|| inbox2.try_recv().map(|m| m.0)).collect(); - - assert_eq!(msgs1.len(), 1, "echo from rt1"); - assert_eq!(msgs2.len(), 2, "double from rt2"); - assert_eq!(msgs1[0], b"A"); - assert!(msgs2.iter().all(|m| m == b"B")); -} - -// Guest that uses i32.wrap_i64 instruction -#[test] -fn guest_uses_i64_to_i32_wrap() { - let wat = r#"(module - (memory (export "memory") 1) - (global $heap (mut i32) (i32.const 65536)) - (func (export "alloc") (param $len i32) (result i32) - global.get $heap - global.get $heap - local.get $len - i32.add - global.set $heap) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Wrap i64 to i32: 0x1_0000_00FF -> 0xFF - i64.const 4294967551 ;; 0x1_000000FF - i32.wrap_i64 - ;; result is 255, store at ptr - local.get $ptr - i32.store8 offset=0 - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(b"W".to_vec())).unwrap(); - rt.tick(); // no trap, wrapping is well-defined -} - -// Property test: building from random subsets of valid WAT always succeeds or fails cleanly -proptest! { - #[test] - fn prop_random_valid_wat_variations_never_panic( - alloc_offset in 1024u32..60000, - pages in 1u32..5, - handle_body in proptest::bool::ANY, - ) { - let store_body = if handle_body { - "local.get $ptr\nlocal.get $ptr\ni32.load8_u\ni32.store8" - } else { - "" - }; - let wat = format!(r#"(module - (memory (export "memory") {pages}) - (func (export "alloc") (param $len i32) (result i32) - i32.const {alloc_offset}) - (func (export "handle") (param $ptr i32) (param $len i32) - {store_body}) - )"#); - let engine = SharedEngine::new().unwrap(); - let result = WasmActorBuilder::new(engine, wat::parse_str(&wat).unwrap()).build(); - assert!(result.is_ok(), "valid WAT should build"); - } -} - -// ── Cycle 60 ───────────────────────────────────────────────────────────────── - -// Guest that sends 3 messages to 3 different destinations in one handle -#[test] -fn guest_sends_to_three_destinations_in_one_handle() { - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox_a = rt.new_inbox::().unwrap(); - let inbox_b = rt.new_inbox::().unwrap(); - let inbox_c = rt.new_inbox::().unwrap(); - - // Build a message with 3 framed destinations: [addr_a][addr_b][addr_c] + "hi" - // The echo guest echoes entire payload to addr embedded in first 32 bytes. - // Instead, spawn 3 separate echo actors and send to each. - let e1 = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - let e2 = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - let e3 = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - let a1 = rt.spawn(e1).unwrap(); - let a2 = rt.spawn(e2).unwrap(); - let a3 = rt.spawn(e3).unwrap(); - - rt.send_to(a1, framed_msg(inbox_a.addr(), b"msg-a")).unwrap(); - rt.send_to(a2, framed_msg(inbox_b.addr(), b"msg-b")).unwrap(); - rt.send_to(a3, framed_msg(inbox_c.addr(), b"msg-c")).unwrap(); - rt.tick(); - - assert_eq!(inbox_a.try_recv().unwrap().0, b"msg-a"); - assert_eq!(inbox_b.try_recv().unwrap().0, b"msg-b"); - assert_eq!(inbox_c.try_recv().unwrap().0, b"msg-c"); -} - -// Actor handles 10 messages across 10 ticks, one per tick -#[test] -fn one_message_per_tick_for_ten_ticks() { - 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(); - - for i in 0u8..10 { - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - rt.tick(); - let msg = inbox.try_recv().expect("should get reply each tick"); - assert_eq!(msg.0, vec![i]); - } -} - -// Guest with memory of 10 pages (640KB) — larger initial memory -#[test] -fn guest_with_ten_page_initial_memory() { - let wat = r#"(module - (memory (export "memory") 10) - (global $heap (mut i32) (i32.const 655360)) - (func (export "alloc") (param $len i32) (result i32) - global.get $heap - global.get $heap - local.get $len - i32.add - global.set $heap) - (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(); - - // Send a message — the large initial memory should work fine - let payload = vec![42u8; 1000]; - rt.send_to(addr, ByteMessage(payload)).unwrap(); - rt.tick(); -} - -// Guest with both memory.fill and memory.copy in same handle -#[test] -fn guest_uses_fill_and_copy_together() { - let wat = r#"(module - (memory (export "memory") 1) - (global $heap (mut i32) (i32.const 65536)) - (func (export "alloc") (param $len i32) (result i32) - global.get $heap - global.get $heap - local.get $len - i32.add - global.set $heap) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Fill 10 bytes at ptr+1000 with 0xAA - (memory.fill (i32.const 1000) (i32.const 0xAA) (i32.const 10)) - ;; Copy those 10 bytes to ptr+2000 - (memory.copy (i32.const 2000) (i32.const 1000) (i32.const 10)) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(b"test".to_vec())).unwrap(); - rt.tick(); // no trap -} - -// Stop actor mid-stream: send 5, tick 1 (processes some), stop, tick again -#[test] -fn stop_actor_with_pending_messages_in_mailbox() { - 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(); - - for i in 0u8..5 { - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - } - // Process first tick (may handle some or all) - rt.tick(); - // Stop — remaining messages are dropped - rt.stop_actor(addr); - rt.tick(); - // Actor should be dead now — no more processing - let count_before: usize = std::iter::from_fn(|| inbox.try_recv()).count(); - rt.tick(); - let count_after: usize = std::iter::from_fn(|| inbox.try_recv()).count(); - assert_eq!(count_after, 0, "no more messages after stop"); - assert!(count_before <= 5, "at most 5 echoes received"); -} - -// ── Cycle 61 ───────────────────────────────────────────────────────────────── - -// Guest with unreachable instruction — handle traps but actor survives -#[test] -fn guest_handle_hits_unreachable_instruction() { - let wat = r#"(module - (memory (export "memory") 1) - (func (export "alloc") (param $len i32) (result i32) - i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - unreachable) - )"#; - 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(); - - // First message traps - rt.send_to(addr, ByteMessage(b"boom".to_vec())).unwrap(); - rt.tick(); - // Actor should still be alive — send again, same result - rt.send_to(addr, ByteMessage(b"boom2".to_vec())).unwrap(); - rt.tick(); - // No responses in inbox - assert!(inbox.try_recv().is_none()); -} - -// Alloc that returns different pointers for successive calls (proper bump) -#[test] -fn guest_bump_allocator_returns_advancing_pointers() { - // Use echo guest — its bump allocator naturally advances - 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(); - - // Send messages of different sizes — echo returns them all faithfully - for size in [1, 10, 100, 1000] { - let payload = vec![0xAB; size]; - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - } - rt.tick(); - - let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(msgs.len(), 4); - assert_eq!(msgs[0].len(), 1); - assert_eq!(msgs[1].len(), 10); - assert_eq!(msgs[2].len(), 100); - assert_eq!(msgs[3].len(), 1000); -} - -// Guest with block/loop/br_if — complex control flow -#[test] -fn guest_block_loop_br_if_control_flow() { - // Count bytes equal to 0x42 using loop - let wat = r#"(module - (memory (export "memory") 1) - (global $heap (mut i32) (i32.const 65536)) - (func (export "alloc") (param $len i32) (result i32) - global.get $heap - global.get $heap - local.get $len - i32.add - global.set $heap) - (func (export "handle") (param $ptr i32) (param $len i32) - (local $i i32) - (local $count i32) - (local.set $i (i32.const 0)) - (local.set $count (i32.const 0)) - (block $exit - (loop $loop - ;; if i >= len, break - (br_if $exit (i32.ge_u (local.get $i) (local.get $len))) - ;; if mem[ptr+i] == 0x42, count++ - (if (i32.eq (i32.load8_u (i32.add (local.get $ptr) (local.get $i))) (i32.const 0x42)) - (then (local.set $count (i32.add (local.get $count) (i32.const 1)))) - ) - (local.set $i (i32.add (local.get $i) (i32.const 1))) - (br $loop) - ) - ) - ;; Store count at fixed location 0 - (i32.store (i32.const 0) (local.get $count)) - ) - )"#; - 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(); - - // Send payload with some 0x42 bytes - let payload = vec![0x42, 0x00, 0x42, 0x42, 0xFF]; - rt.send_to(addr, ByteMessage(payload)).unwrap(); - rt.tick(); // no trap -} - -// Two WASM actors spawned, one stopped immediately, the other processes normally -#[test] -fn stop_one_of_two_wasm_actors() { - let engine = SharedEngine::new().unwrap(); - let a1 = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - let a2 = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let addr1 = rt.spawn(a1).unwrap(); - let addr2 = rt.spawn(a2).unwrap(); - - rt.stop_actor(addr1); - rt.send_to(addr2, framed_msg(inbox.addr(), b"still-alive")).unwrap(); - rt.tick(); - rt.tick(); - - let msgs: Vec<_> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(msgs, vec![b"still-alive".to_vec()]); -} - -// Property: spawning N actors (1..20) from same engine always works -proptest! { - #[test] - fn prop_spawn_n_actors_from_same_engine(n in 1usize..20) { - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let mut addrs = Vec::new(); - for _ in 0..n { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - addrs.push(rt.spawn(actor).unwrap()); - } - - for (i, addr) in addrs.iter().enumerate() { - rt.send_to(*addr, framed_msg(inbox.addr(), &[i as u8])).unwrap(); - } - rt.tick(); - - let msgs: Vec<_> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(msgs.len(), n, "each actor echoes one message"); - } -} - -// ── Cycle 62 ───────────────────────────────────────────────────────────────── - -// Guest that sends to itself (creates a feedback loop bounded by budget) -#[test] -fn self_send_loop_bounded_by_budget_detailed() { - // Similar to existing self_send test but with budget=2 and counting carefully - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - let mut cfg = RuntimeConfig::default(); - cfg.actor_message_budget = 2; - let rt = Runtime::new(cfg); - let addr = rt.spawn(actor).unwrap(); - - // Echo actor echoes to the dest address embedded in the payload's first 32 bytes - // If we send framed_msg(addr, b"X"), the echo goes back to addr — self-loop! - rt.send_to(addr, framed_msg(&addr, b"X")).unwrap(); - - // Each tick processes up to budget=2 messages - for _ in 0..5 { - rt.tick(); - } - // Actor should still be alive (no crash) — the loop is infinite but budget-bounded -} - -// Guest module that is an empty module (no memory, no functions) -#[test] -fn empty_module_missing_all_exports() { - let wat = "(module)"; - let engine = SharedEngine::new().unwrap(); - let result = WasmActorBuilder::new(engine, wat::parse_str(wat).unwrap()).build(); - assert!(result.is_err(), "module with no exports should fail to build"); - let err = result.err().unwrap(); - let err_msg = format!("{}", err); - assert!(err_msg.contains("memory") || err_msg.contains("alloc"), - "error should mention missing export: {err_msg}"); -} - -// Guest where alloc always returns the same pointer — repeated overwrites -#[test] -fn guest_static_alloc_same_pointer_echoes_each_msg() { - let engine = SharedEngine::new().unwrap(); - // Build a custom guest that always returns ptr 1024 from alloc - 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) - ;; Use first 32 bytes as dest, rest as payload - (if (i32.ge_u (local.get $len) (i32.const 33)) - (then - (call $send - (local.get $ptr) - (i32.add (local.get $ptr) (i32.const 32)) - (i32.sub (local.get $len) (i32.const 32)) - ) - ) - ) - ) - )"#; - 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(); - - // Each message goes to same ptr=1024, overwriting previous data - rt.send_to(addr, framed_msg(inbox.addr(), b"first")).unwrap(); - rt.send_to(addr, framed_msg(inbox.addr(), b"second")).unwrap(); - rt.tick(); - - let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(msgs.len(), 2, "both messages processed"); - // Since same pointer, second alloc overwrites first, but outbox snapshots data at send time - assert_eq!(msgs[0], b"first"); - assert_eq!(msgs[1], b"second"); -} - -// Multiple engines, multiple runtimes, cross-combination -#[test] -fn two_engines_two_runtimes_cross_combination() { - let engine_a = SharedEngine::new().unwrap(); - let engine_b = SharedEngine::new().unwrap(); - - let rt1 = Runtime::new(RuntimeConfig::default()); - let rt2 = Runtime::new(RuntimeConfig::default()); - - // Actor from engine_a on rt1 - let a1 = WasmActorBuilder::new(engine_a.clone(), guest_wasm("echo")).build().unwrap(); - // Actor from engine_b on rt2 - let a2 = WasmActorBuilder::new(engine_b.clone(), guest_wasm("echo")).build().unwrap(); - // Actor from engine_a on rt2 - let a3 = WasmActorBuilder::new(engine_a, guest_wasm("silent")).build().unwrap(); - // Actor from engine_b on rt1 - let a4 = WasmActorBuilder::new(engine_b, guest_wasm("double")).build().unwrap(); - - let inbox1 = rt1.new_inbox::().unwrap(); - let inbox2 = rt2.new_inbox::().unwrap(); - - let addr1 = rt1.spawn(a1).unwrap(); - let addr4 = rt1.spawn(a4).unwrap(); - let addr2 = rt2.spawn(a2).unwrap(); - let _addr3 = rt2.spawn(a3).unwrap(); - - rt1.send_to(addr1, framed_msg(inbox1.addr(), b"e1")).unwrap(); - rt1.send_to(addr4, framed_msg(inbox1.addr(), b"d1")).unwrap(); - rt2.send_to(addr2, framed_msg(inbox2.addr(), b"e2")).unwrap(); - - rt1.tick(); - rt2.tick(); - - let msgs1: Vec<_> = std::iter::from_fn(|| inbox1.try_recv().map(|m| m.0)).collect(); - let msgs2: Vec<_> = std::iter::from_fn(|| inbox2.try_recv().map(|m| m.0)).collect(); - - // rt1: echo(e1) + double(d1) = 1 + 2 = 3 - assert_eq!(msgs1.len(), 3); - // rt2: echo(e2) = 1 - assert_eq!(msgs2.len(), 1); - assert_eq!(msgs2[0], b"e2"); -} - -// 32KB payload stress test — echoes correctly -#[test] -fn thirty_two_kb_payload_echo() { - 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..32768u32).map(|i| (i % 199) as u8).collect(); - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - rt.tick(); - - let resp = inbox.try_recv().expect("32KB echo"); - assert_eq!(resp.0.len(), 32768); - assert_eq!(resp.0, payload); -} - -// ── Cycle 63 ───────────────────────────────────────────────────────────────── - -// Guest sends with payload_len=0 but valid payload_ptr — zero-length payload delivered -#[test] -fn send_zero_length_payload_from_guest() { - 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) - ;; Send with payload_len=0 — should deliver empty payload - (call $send (local.get $ptr) (i32.const 0) (i32.const 0)) - ) - )"#; - 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(); - - // Frame message: first 32 bytes are dest (inbox addr) - let mut msg = Vec::with_capacity(32); - msg.extend_from_slice(&inbox.addr().0); - rt.send_to(addr, ByteMessage(msg)).unwrap(); - rt.tick(); - - let resp = inbox.try_recv().expect("zero-length payload should arrive"); - assert!(resp.0.is_empty(), "payload should be empty"); -} - -// Guest sends with overlapping dest and payload regions (payload starts inside dest) -#[test] -fn send_with_overlapping_dest_and_payload_regions() { - 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) - ;; dest_ptr=1024, payload_ptr=1040 (inside the 32-byte dest region), payload_len=8 - ;; This overlaps: dest is [1024..1056], payload is [1040..1048] - (call $send (i32.const 1024) (i32.const 1040) (i32.const 8)) - ) - )"#; - 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(); - - // Write the inbox address into memory at offset 1024 (via the message) - let mut msg = Vec::with_capacity(64); - msg.extend_from_slice(&inbox.addr().0); - msg.extend_from_slice(&[0u8; 32]); // padding - rt.send_to(addr, ByteMessage(msg)).unwrap(); - rt.tick(); - - // The send should succeed — overlapping reads are fine (read-only) - let resp = inbox.try_recv().expect("overlapping regions should work"); - assert_eq!(resp.0.len(), 8); -} - -// Sequential alloc exhaustion: echo actor with tiny heap eventually can't alloc -#[test] -fn alloc_exhaustion_drops_message_gracefully() { - let wat = r#"(module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - ;; Tiny heap: starts at 60000, only 5536 bytes before page end - (global $heap (mut i32) (i32.const 60000)) - (func (export "alloc") (param $len i32) (result i32) - (local $ptr i32) - (local.set $ptr (global.get $heap)) - ;; Check if allocation would exceed page - (if (i32.gt_u - (i32.add (global.get $heap) (local.get $len)) - (i32.const 65536)) - (then (return (i32.const -1))) ;; OOM signal - ) - (global.set $heap (i32.add (global.get $heap) (local.get $len))) - (local.get $ptr) - ) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Echo: send first 32 bytes as dest, rest as payload - (if (i32.ge_u (local.get $len) (i32.const 33)) - (then - (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(); - - // Send messages that consume heap space - for i in 0..100 { - rt.send_to(addr, framed_msg(inbox.addr(), &vec![i as u8; 100])).unwrap(); - } - rt.tick(); - - // Count how many were echoed (some should be dropped due to OOM) - let delivered = std::iter::from_fn(|| inbox.try_recv()).count(); - assert!(delivered < 100, "some messages should be dropped due to OOM (got {delivered})"); - assert!(delivered > 0, "at least some messages should succeed"); -} - -// Stop all actors in a runtime — runtime should be empty -#[test] -fn stop_all_wasm_actors_in_runtime() { - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let mut addrs = Vec::new(); - for _ in 0..5 { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("silent")) - .build().unwrap(); - addrs.push(rt.spawn(actor).unwrap()); - } - rt.tick(); - - for addr in &addrs { - rt.stop_actor(*addr); - } - rt.tick(); - rt.tick(); - - // All actors stopped — sending should fail or be silently dropped - for addr in &addrs { - let _ = rt.send_to(*addr, ByteMessage(vec![])); - } - rt.tick(); // no panics -} - -// ── Cycle 64 ───────────────────────────────────────────────────────────────── - -// Guest calls send twice: once normally, once with dest beyond memory — second traps, -// but first send should still be in outbox (outbox cleared on trap) -#[test] -fn send_then_oob_send_clears_outbox() { - 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) - ;; First send: valid - (call $send (local.get $ptr) (i32.const 0) (i32.const 1)) - ;; Second send: dest_ptr = 65520, needs 32 bytes = 65552 > 65536 - ;; This traps! And the entire outbox should be cleared. - (call $send (i32.const 65520) (i32.const 0) (i32.const 1)) - ) - )"#; - 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(); - - let mut msg = Vec::with_capacity(33); - msg.extend_from_slice(&inbox.addr().0); - msg.push(0x42); - rt.send_to(addr, ByteMessage(msg)).unwrap(); - rt.tick(); - - // The trap from second send should clear the outbox (Bug #3 fix) - // So the first send should NOT be delivered - assert!(inbox.try_recv().is_none(), "outbox cleared on trap — no message delivered"); -} - -// Guest with i32.extend8_s — sign extension instruction -#[test] -fn guest_uses_sign_extension() { - let wat = r#"(module - (memory (export "memory") 1) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Load byte, sign-extend, store as i32 - (i32.store (local.get $ptr) - (i32.extend8_s (i32.load8_u (local.get $ptr))) - ) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![0x80])).unwrap(); - rt.tick(); // 0x80 sign-extends to 0xFFFFFF80 — no trap -} - -// Double actor processes message then gets stopped — verified via inbox -#[test] -fn double_processes_then_stops_cleanly() { - let engine = SharedEngine::new().unwrap(); - let double = WasmActorBuilder::new(engine, guest_wasm("double")).build().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let d_addr = rt.spawn(double).unwrap(); - - // Send a message, then stop - rt.send_to(d_addr, framed_msg(inbox.addr(), b"Z")).unwrap(); - rt.tick(); - - let msgs: Vec<_> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(msgs.len(), 2, "double sends 2 copies"); - - rt.stop_actor(d_addr); - rt.tick(); - rt.tick(); - - // Send again — should be silently dropped (actor is dead) - let _ = rt.send_to(d_addr, framed_msg(inbox.addr(), b"after-stop")); - rt.tick(); - assert!(inbox.try_recv().is_none(), "no messages after stop"); -} - -// 64KB payload (entire page minus framing overhead) — stress the echo actor -#[test] -fn near_page_size_payload_echo() { - 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(); - - // 60000 bytes — large but within a single page + bump allocator space - let payload: Vec = (0..60000u32).map(|i| (i % 173) as u8).collect(); - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - rt.tick(); - - let resp = inbox.try_recv().expect("large payload echo"); - assert_eq!(resp.0.len(), payload.len()); - assert_eq!(resp.0, payload); -} - -// Property: message content is always preserved by echo regardless of content -proptest! { - #[test] - fn prop_echo_preserves_arbitrary_content( - payload in proptest::collection::vec(0u8..=255, 1..4096) - ) { - 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(); - - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - rt.tick(); - - let resp = inbox.try_recv().expect("echo should respond"); - assert_eq!(resp.0, payload, "echo must preserve content exactly"); - } -} - -// ── Cycle 65 ───────────────────────────────────────────────────────────────── - -// Guest that traps in alloc (not handle) — message dropped, actor survives -#[test] -fn alloc_trap_recovery_then_normal_message() { - // First message: alloc traps. Second message: alloc works, handle echoes. - let wat = r#"(module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (global $call_count (mut i32) (i32.const 0)) - (func (export "alloc") (param $len i32) (result i32) - ;; First call: trap - (if (i32.eqz (global.get $call_count)) - (then - (global.set $call_count (i32.const 1)) - unreachable - ) - ) - ;; Subsequent calls: return fixed ptr - i32.const 1024 - ) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Echo: first 32 bytes dest, rest payload - (if (i32.ge_u (local.get $len) (i32.const 33)) - (then - (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(); - - // First message — alloc traps, dropped - rt.send_to(addr, framed_msg(inbox.addr(), b"trap")).unwrap(); - rt.tick(); - assert!(inbox.try_recv().is_none(), "first message dropped due to alloc trap"); - - // Second message — should work + // Actor still alive rt.send_to(addr, framed_msg(inbox.addr(), b"ok")).unwrap(); rt.tick(); - let resp = inbox.try_recv().expect("second message should echo"); - assert_eq!(resp.0, b"ok"); + assert_eq!(inbox.try_recv().unwrap().0, b"ok"); } -// Guest module with table but no call_indirect — table exists but unused -#[test] -fn module_with_unused_table() { - let wat = r#"(module - (memory (export "memory") 1) - (table 2 funcref) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (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(); - rt.send_to(addr, ByteMessage(b"table".to_vec())).unwrap(); - rt.tick(); -} +// ═══════════════════════════════════════════════════════════════════════════════ +// Group 6: Engine & runtime config +// ═══════════════════════════════════════════════════════════════════════════════ -// Echo actor processes alternating large and small messages #[test] -fn alternating_large_small_messages() { +fn shared_engine_traits_and_clone() { + fn assert_send_sync() {} + fn assert_send() {} + assert_send_sync::(); + assert_send::(); + let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); + let _ = format!("{:?}", engine); // Debug doesn't panic + + // Clone produces working independent engine + let clone = engine.clone(); + let a1 = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); + let a2 = WasmActorBuilder::new(clone, guest_wasm("echo")).build().unwrap(); let rt = Runtime::new(RuntimeConfig::default()); let inbox = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - for i in 0..10 { - let size = if i % 2 == 0 { 5000 } else { 3 }; - let payload = vec![(i as u8).wrapping_mul(7); size]; - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - } - rt.tick(); - - let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(msgs.len(), 10); - for (i, msg) in msgs.iter().enumerate() { - let expected_size = if i % 2 == 0 { 5000 } else { 3 }; - assert_eq!(msg.len(), expected_size, "message {i} wrong size"); - assert!(msg.iter().all(|&b| b == (i as u8).wrapping_mul(7)), - "message {i} wrong content"); - } -} - -// Send 200 messages to silent actor — no responses, no panics -#[test] -fn silent_actor_absorbs_200_messages() { - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("silent")).build().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let addr = rt.spawn(actor).unwrap(); - - for _ in 0..200 { - rt.send_to(addr, ByteMessage(vec![0xFF; 50])).unwrap(); - } - for _ in 0..10 { - rt.tick(); - } -} - -// Build two actors from same bytes object (cloned), verify independence -#[test] -fn two_actors_from_cloned_wasm_bytes() { - let engine = SharedEngine::new().unwrap(); - let bytes = guest_wasm("echo"); - let a1 = WasmActorBuilder::new(engine.clone(), bytes.clone()).build().unwrap(); - let a2 = WasmActorBuilder::new(engine, bytes).build().unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox1 = rt.new_inbox::().unwrap(); - let inbox2 = rt.new_inbox::().unwrap(); let addr1 = rt.spawn(a1).unwrap(); let addr2 = rt.spawn(a2).unwrap(); - - rt.send_to(addr1, framed_msg(inbox1.addr(), b"one")).unwrap(); - rt.send_to(addr2, framed_msg(inbox2.addr(), b"two")).unwrap(); + rt.send_to(addr1, framed_msg(inbox.addr(), b"c1")).unwrap(); + rt.send_to(addr2, framed_msg(inbox.addr(), b"c2")).unwrap(); rt.tick(); + let mut msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); + msgs.sort(); + assert_eq!(msgs, vec![b"c1".to_vec(), b"c2".to_vec()]); - assert_eq!(inbox1.try_recv().unwrap().0, b"one"); - assert_eq!(inbox2.try_recv().unwrap().0, b"two"); + // ByteMessage traits + let bm = ByteMessage(vec![1, 2, 3]); + let bm2 = bm.clone(); + assert_eq!(bm, bm2); + let _ = format!("{:?}", bm); } -// ── Cycle 66 ───────────────────────────────────────────────────────────────── - -// Guest uses i32.shr_u (logical right shift) #[test] -fn guest_uses_logical_right_shift() { - let wat = r#"(module - (memory (export "memory") 1) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Load 4 bytes from ptr, shift right by 4, store back - (i32.store (local.get $ptr) - (i32.shr_u (i32.load (local.get $ptr)) (i32.const 4)) - ) - ) - )"#; +fn multi_thread_runtime() { 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(); - rt.send_to(addr, ByteMessage(vec![0xFF, 0x00, 0x00, 0x00])).unwrap(); - rt.tick(); -} - -// Long-lived actor: 100 messages across 100 ticks -#[test] -fn hundred_messages_across_hundred_ticks() { - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); + let config = RuntimeConfig { num_threads: 4, ..RuntimeConfig::default() }; + let rt = Runtime::new(config); let inbox = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - for i in 0u8..100 { - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - rt.tick(); - let resp = inbox.try_recv().expect("each tick should echo"); - assert_eq!(resp.0, vec![i], "tick {i} content mismatch"); + let mut addrs = Vec::new(); + for _ in 0..10 { + let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); + addrs.push(rt.spawn(actor).unwrap()); } -} - -// Guest with nested if/else chains -#[test] -fn guest_nested_if_else_three_deep() { - let wat = r#"(module - (memory (export "memory") 1) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - (if (i32.gt_u (local.get $len) (i32.const 10)) - (then - (if (i32.gt_u (local.get $len) (i32.const 20)) - (then - (if (i32.gt_u (local.get $len) (i32.const 30)) - (then (i32.store (local.get $ptr) (i32.const 3))) - (else (i32.store (local.get $ptr) (i32.const 2))) - ) - ) - (else (i32.store (local.get $ptr) (i32.const 1))) - ) - ) - (else (i32.store (local.get $ptr) (i32.const 0))) - ) - ) - )"#; - 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(); - - // len=5 → branch 0, len=15 → branch 1, len=25 → branch 2, len=35 → branch 3 - for len in [5, 15, 25, 35] { - rt.send_to(addr, ByteMessage(vec![0u8; len])).unwrap(); - } - rt.tick(); // no trap in any branch -} - -// Multiple sends in one handle with increasing payload sizes -#[test] -fn guest_sends_increasing_payloads_in_one_handle() { - 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) - ;; Send 1 byte, 2 bytes, 3 bytes from offset 0 (with dest at ptr) - (if (i32.ge_u (local.get $len) (i32.const 32)) - (then - (call $send (local.get $ptr) (i32.add (local.get $ptr) (i32.const 32)) (i32.const 1)) - (call $send (local.get $ptr) (i32.add (local.get $ptr) (i32.const 32)) (i32.const 2)) - (call $send (local.get $ptr) (i32.add (local.get $ptr) (i32.const 32)) (i32.const 3)) - ) - ) - ) - )"#; - 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(); - - let mut msg = Vec::new(); - msg.extend_from_slice(&inbox.addr().0); - msg.extend_from_slice(b"ABCDE"); - rt.send_to(addr, ByteMessage(msg)).unwrap(); - rt.tick(); - - let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(msgs.len(), 3); - assert_eq!(msgs[0].len(), 1); - assert_eq!(msgs[1].len(), 2); - assert_eq!(msgs[2].len(), 3); -} - -// Engine and builder are independent — dropping engine after build still works -#[test] -fn engine_dropped_after_build_actor_still_works() { - let actor = { - let engine = SharedEngine::new().unwrap(); - WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap() - // engine dropped here - }; - 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"after-drop")).unwrap(); - rt.tick(); - - let resp = inbox.try_recv().expect("should echo after engine dropped"); - assert_eq!(resp.0, b"after-drop"); -} - -// ── Cycle 67 ───────────────────────────────────────────────────────────────── - -// Guest uses grow, then accesses the newly grown page -#[test] -fn guest_grows_memory_and_uses_new_page() { - 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 $old_pages i32) - ;; Grow by 1 page - (local.set $old_pages (memory.grow (i32.const 1))) - ;; Write at start of new page (old_pages * 65536) - (i32.store - (i32.mul (local.get $old_pages) (i32.const 65536)) - (i32.const 0xDEADBEEF) - ) - ;; Send from first 32 bytes of message - (if (i32.ge_u (local.get $len) (i32.const 32)) - (then - (call $send (local.get $ptr) (i32.const 0) (i32.const 0)) - ) - ) - ) - )"#; - 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(); - - let mut msg = Vec::new(); - msg.extend_from_slice(&inbox.addr().0); - rt.send_to(addr, ByteMessage(msg)).unwrap(); - rt.tick(); - - // Should receive empty payload (sent 0 bytes) - let resp = inbox.try_recv().expect("should receive msg after grow"); - assert!(resp.0.is_empty()); -} - -// Native actor receives message from WASM echo, verifies payload -struct PayloadVerifier { - expected: Vec, - verified: std::cell::Cell, -} -impl ActorInterface for PayloadVerifier { - type Incoming = ByteMessage; - type Response = (); - fn handle(&mut self, _ctx: &Ctx, msg: ByteMessage) { - if msg.0 == self.expected { - self.verified.set(true); - } - } -} - -#[test] -fn native_verifier_receives_wasm_echo() { - let engine = SharedEngine::new().unwrap(); - let echo = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - - let verifier = PayloadVerifier { - expected: b"hello-from-wasm".to_vec(), - verified: std::cell::Cell::new(false), - }; - let v_addr = rt.spawn(verifier).unwrap(); - let e_addr = rt.spawn(echo).unwrap(); - - rt.send_to(e_addr, framed_msg(&v_addr, b"hello-from-wasm")).unwrap(); - rt.tick(); - rt.tick(); // verifier processes the echoed message -} - -// Sequential spawn-echo-stop for 3 different guest types -#[test] -fn sequential_echo_double_silent_lifecycle() { - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - // Echo phase - let echo = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - let e_addr = rt.spawn(echo).unwrap(); - rt.send_to(e_addr, framed_msg(inbox.addr(), b"E")).unwrap(); - rt.tick(); - assert_eq!(inbox.try_recv().unwrap().0, b"E"); - rt.stop_actor(e_addr); - rt.tick(); - - // Double phase - let double = WasmActorBuilder::new(engine.clone(), guest_wasm("double")).build().unwrap(); - let d_addr = rt.spawn(double).unwrap(); - rt.send_to(d_addr, framed_msg(inbox.addr(), b"D")).unwrap(); - rt.tick(); - let d_msgs: Vec<_> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(d_msgs.len(), 2); - rt.stop_actor(d_addr); - rt.tick(); - - // Silent phase - let silent = WasmActorBuilder::new(engine, guest_wasm("silent")).build().unwrap(); - let s_addr = rt.spawn(silent).unwrap(); - rt.send_to(s_addr, ByteMessage(b"S".to_vec())).unwrap(); - rt.tick(); - assert!(inbox.try_recv().is_none()); - rt.stop_actor(s_addr); - rt.tick(); -} - -// Guest with 0 initial data pages but minimum 1 required → module with 0 pages -#[test] -fn module_with_zero_memory_pages_builds_but_alloc_fails_gracefully() { - // Can't have 0-page memory in WAT (minimum is 0 but the export needs at least some) - // Actually (memory 0) is valid — 0 initial pages, can grow later - let wat = r#"(module - (memory (export "memory") 0) - (func (export "alloc") (param $len i32) (result i32) i32.const -1) - (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 returns -1, so message is dropped (ptr < 0 check) - rt.send_to(addr, ByteMessage(b"test".to_vec())).unwrap(); - rt.tick(); // no panic -} - -// Double guest receives same message twice in same tick — produces 4 responses -#[test] -fn double_receives_two_messages_produces_four() { - let engine = SharedEngine::new().unwrap(); - let double = WasmActorBuilder::new(engine, guest_wasm("double")).build().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let addr = rt.spawn(double).unwrap(); - - rt.send_to(addr, framed_msg(inbox.addr(), b"A")).unwrap(); - rt.send_to(addr, framed_msg(inbox.addr(), b"B")).unwrap(); - rt.tick(); - - let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(msgs.len(), 4, "2 messages × 2 copies each = 4"); - // First two are copies of "A", last two are copies of "B" - assert_eq!(msgs[0], b"A"); - assert_eq!(msgs[1], b"A"); - assert_eq!(msgs[2], b"B"); - assert_eq!(msgs[3], b"B"); -} - -// Property: stopping an actor never panics regardless of pending messages -proptest! { - #[test] - fn prop_stop_with_pending_never_panics( - n_msgs in 0usize..50, - ticks_before_stop in 0usize..5, - ticks_after_stop in 1usize..5, - ) { - 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(); - - for i in 0..n_msgs { - let _ = rt.send_to(addr, ByteMessage(vec![i as u8; 10])); - } - for _ in 0..ticks_before_stop { - rt.tick(); - } - rt.stop_actor(addr); - for _ in 0..ticks_after_stop { - rt.tick(); - } - } -} - -// ── Cycle 68 — 300 TEST MILESTONE ──────────────────────────────────────────── - -// Guest that writes to memory offset 0 (data segment area) — valid operation -#[test] -fn guest_writes_to_offset_zero() { - let wat = r#"(module - (memory (export "memory") 1) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - (i32.store (i32.const 0) (i32.const 42)) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![1])).unwrap(); - rt.tick(); -} - -// Spawn 10 echo actors, send to all, verify all echo back -#[test] -fn ten_echo_actors_all_respond() { - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let addrs: Vec<_> = (0..10) - .map(|_| { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")) - .build().unwrap(); - rt.spawn(actor).unwrap() - }) - .collect(); - for (i, addr) in addrs.iter().enumerate() { rt.send_to(*addr, framed_msg(inbox.addr(), &[i as u8])).unwrap(); } - rt.tick(); - let mut msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - msgs.sort(); - assert_eq!(msgs.len(), 10); - for i in 0..10 { - assert_eq!(msgs[i], vec![i as u8]); + let handle = rt.run().unwrap(); + let mut received = Vec::new(); + for _ in 0..40 { + std::thread::sleep(std::time::Duration::from_millis(25)); + while let Some(msg) = inbox.try_recv() { + received.push(msg.0[0]); + } + if received.len() == 10 { break; } } + handle.shutdown(); + received.sort(); + assert_eq!(received, (0..10u8).collect::>()); } -// Guest that does nothing in handle then sends on next message #[test] -fn guest_alternates_between_silent_and_sending() { - let wat = r#"(module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (global $toggle (mut i32) (i32.const 0)) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - (if (i32.eqz (global.get $toggle)) - (then - ;; Silent on even calls - (global.set $toggle (i32.const 1)) - ) - (else - ;; Send on odd calls - (global.set $toggle (i32.const 0)) - (if (i32.ge_u (local.get $len) (i32.const 33)) - (then - (call $send - (local.get $ptr) - (i32.add (local.get $ptr) (i32.const 32)) - (i32.sub (local.get $len) (i32.const 32)) - ) - ) - ) - ) - ) - ) - )"#; +fn budget_and_mailbox() { 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(); - // Send 6 messages — expect responses from messages 2, 4, 6 (1-indexed) - for i in 0..6 { - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - } - rt.tick(); - - let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(msgs.len(), 3, "only odd calls send"); - assert_eq!(msgs[0], vec![1]); - assert_eq!(msgs[1], vec![3]); - assert_eq!(msgs[2], vec![5]); -} - -// Runtime with budget=1 processes exactly 1 message per actor per tick -#[test] -fn budget_one_processes_exactly_one_per_tick() { - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); + // Budget=1: one message processed per tick let mut cfg = RuntimeConfig::default(); cfg.actor_message_budget = 1; let rt = Runtime::new(cfg); let inbox = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - // Queue 3 messages - for i in 0..3u8 { - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - } - - // Each tick should process exactly 1 - rt.tick(); - let t1: Vec<_> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(t1.len(), 1, "budget=1 processes 1 per tick"); - assert_eq!(t1[0], vec![0]); - - rt.tick(); - let t2: Vec<_> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(t2.len(), 1); - assert_eq!(t2[0], vec![1]); - - rt.tick(); - let t3: Vec<_> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(t3.len(), 1); - assert_eq!(t3[0], vec![2]); -} - -// ByteMessage with exactly 32 bytes (address only, no payload for echo) -#[test] -fn echo_with_address_only_no_payload() { - 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(); - - // Exactly 32 bytes = dest address, 0 payload bytes - let msg = ByteMessage(inbox.addr().0.to_vec()); - rt.send_to(addr, msg).unwrap(); - rt.tick(); - - // Echo should send back empty payload - let resp = inbox.try_recv().expect("echo with 0-byte payload"); - assert!(resp.0.is_empty()); -} - -// ── Cycle 69 ───────────────────────────────────────────────────────────────── - -// Guest with multiple globals — verifies complex state management -#[test] -fn guest_with_multiple_mutable_globals() { - let wat = r#"(module - (memory (export "memory") 1) - (global $a (mut i32) (i32.const 0)) - (global $b (mut i32) (i32.const 100)) - (global $c (mut i32) (i32.const 200)) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Rotate: a=b, b=c, c=a+1 - (local $old_a i32) - (local.set $old_a (global.get $a)) - (global.set $a (global.get $b)) - (global.set $b (global.get $c)) - (global.set $c (i32.add (local.get $old_a) (i32.const 1))) - ) - )"#; - 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(); - - // Send 10 messages — complex state rotation - for _ in 0..10 { - rt.send_to(addr, ByteMessage(vec![0])).unwrap(); - } - rt.tick(); // no trap -} - -// Builder with very large WASM module (100KB of data segment) -#[test] -fn large_data_segment_module() { - // Module with a large data segment (4KB of zeros) - let mut wat = String::from(r#"(module - (memory (export "memory") 2) - (data (i32.const 0) ""#); - // Add 4096 escaped null bytes - for _ in 0..4096 { - wat.push_str("\\00"); - } - wat.push_str(r#"") - (func (export "alloc") (param $len i32) (result i32) i32.const 65536) - (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(); - rt.send_to(addr, ByteMessage(vec![1; 100])).unwrap(); - rt.tick(); -} - -// Echo then double from same engine in same tick — interleaved processing -#[test] -fn echo_and_double_interleaved_in_same_tick() { - 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 = rt.new_inbox::().unwrap(); - - let e_addr = rt.spawn(echo).unwrap(); - let d_addr = rt.spawn(double).unwrap(); - - // Alternate: echo, double, echo, double - for i in 0..4u8 { - let addr = if i % 2 == 0 { e_addr } else { d_addr }; - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - } + let addr = rt.spawn(echo).unwrap(); + rt.send_to(addr, framed_msg(inbox.addr(), b"a")).unwrap(); + rt.send_to(addr, framed_msg(inbox.addr(), b"b")).unwrap(); + rt.send_to(addr, framed_msg(inbox.addr(), b"c")).unwrap(); rt.tick(); - - let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - // Echo: 2 msgs (1 response each) + Double: 2 msgs (2 responses each) = 6 - assert_eq!(msgs.len(), 6); -} - -// Guest that stores i64 value (8-byte store) -#[test] -fn guest_stores_i64_value() { - let wat = r#"(module - (memory (export "memory") 1) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Store 8-byte i64 at ptr (if len >= 8) - (if (i32.ge_u (local.get $len) (i32.const 8)) - (then - (i64.store (local.get $ptr) (i64.const 0x0102030405060708)) - ) - ) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![0u8; 8])).unwrap(); - rt.tick(); -} - -// Runtime dropped while actors are processing — no crash -#[test] -fn runtime_dropped_with_active_wasm_actors() { - let engine = SharedEngine::new().unwrap(); - { - let rt = Runtime::new(RuntimeConfig::default()); - for _ in 0..5 { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")) - .build().unwrap(); - let addr = rt.spawn(actor).unwrap(); - rt.send_to(addr, ByteMessage(vec![1, 2, 3])).unwrap(); - } - rt.tick(); - // rt dropped here with live actors and unprocessed responses - } - // No panic — wasmtime Store cleanup is safe -} - -// ── Cycle 70 ───────────────────────────────────────────────────────────────── - -// Guest that reads i32 from payload and uses it as send count -#[test] -fn guest_dynamic_send_count_from_payload() { - 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 $count i32) - (local $i i32) - ;; Need at least 33 bytes: 32 dest + 1 count byte - (if (i32.lt_u (local.get $len) (i32.const 33)) (then return)) - ;; Read count from byte 32 - (local.set $count (i32.load8_u (i32.add (local.get $ptr) (i32.const 32)))) - ;; Cap at 10 to prevent excessive sends - (if (i32.gt_u (local.get $count) (i32.const 10)) - (then (local.set $count (i32.const 10))) - ) - ;; Send count times (empty payload from offset 0) - (local.set $i (i32.const 0)) - (block $exit - (loop $loop - (br_if $exit (i32.ge_u (local.get $i) (local.get $count))) - (call $send (local.get $ptr) (i32.const 0) (i32.const 0)) - (local.set $i (i32.add (local.get $i) (i32.const 1))) - (br $loop) - ) - ) - ) - )"#; - 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(); - - // Send with count byte = 5 - let mut msg = Vec::new(); - msg.extend_from_slice(&inbox.addr().0); - msg.push(5); // count - rt.send_to(addr, ByteMessage(msg)).unwrap(); - rt.tick(); - - let msgs: Vec<_> = std::iter::from_fn(|| inbox.try_recv()).collect(); - assert_eq!(msgs.len(), 5, "should send exactly 5 messages"); -} - -// Three actors in pipeline: A → B → C → inbox -#[test] -fn three_actor_pipeline() { - let engine = SharedEngine::new().unwrap(); - let a = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - let b = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - let c = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let a_addr = rt.spawn(a).unwrap(); - let b_addr = rt.spawn(b).unwrap(); - let c_addr = rt.spawn(c).unwrap(); - - // A echoes to B, B echoes to C, C echoes to inbox - // But we need to set dest addresses in each message frame: - // Send to A with dest=B, payload that contains framed(C, framed(inbox, "hi")) - // Actually echo just echoes the payload portion to the dest — so: - // A receives: [B_addr][C_addr][inbox_addr]"hi" → sends [C_addr][inbox_addr]"hi" to B - // B receives: [C_addr][inbox_addr]"hi" → sends [inbox_addr]"hi" to C - // C receives: [inbox_addr]"hi" → sends "hi" to inbox - let mut payload = Vec::new(); - payload.extend_from_slice(&c_addr.0); - payload.extend_from_slice(&inbox.addr().0); - payload.extend_from_slice(b"hi"); - - rt.send_to(a_addr, framed_msg(&b_addr, &payload)).unwrap(); - rt.tick(); // A → B - rt.tick(); // B → C - rt.tick(); // C → inbox - - let resp = inbox.try_recv().expect("message should traverse 3-hop pipeline"); - assert_eq!(resp.0, b"hi"); -} - -// Build actors from 3 different guest types in tight loop -#[test] -fn rapid_build_three_guest_types() { - let engine = SharedEngine::new().unwrap(); - let echo_bytes = guest_wasm("echo"); - let double_bytes = guest_wasm("double"); - let silent_bytes = guest_wasm("silent"); - - for _ in 0..20 { - let _ = WasmActorBuilder::new(engine.clone(), echo_bytes.clone()).build().unwrap(); - let _ = WasmActorBuilder::new(engine.clone(), double_bytes.clone()).build().unwrap(); - let _ = WasmActorBuilder::new(engine.clone(), silent_bytes.clone()).build().unwrap(); - } - // 60 build+drop cycles — no leaks, no panics -} - -// Guest with i32.xor instruction -#[test] -fn guest_xor_payload_with_key() { - 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) - ;; Need >= 33 bytes: 32 dest + at least 1 payload - (if (i32.lt_u (local.get $len) (i32.const 33)) (then return)) - ;; XOR each payload byte with 0xFF - (local.set $i (i32.const 32)) - (block $exit - (loop $loop - (br_if $exit (i32.ge_u (local.get $i) (local.get $len))) - (i32.store8 - (i32.add (local.get $ptr) (local.get $i)) - (i32.xor - (i32.load8_u (i32.add (local.get $ptr) (local.get $i))) - (i32.const 0xFF) - ) - ) - (local.set $i (i32.add (local.get $i) (i32.const 1))) - (br $loop) - ) - ) - ;; Send XORed payload - (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(), &[0x00, 0xFF, 0xAA])).unwrap(); - rt.tick(); - - let resp = inbox.try_recv().expect("XOR response"); - assert_eq!(resp.0, vec![0xFF, 0x00, 0x55], "each byte XORed with 0xFF"); -} - -// ── Cycle 71 ───────────────────────────────────────────────────────────────── - -// Same payload sent to echo and double — both produce correct results -#[test] -fn same_payload_to_echo_and_double_verified() { - 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(); - - let payload = b"shared-payload"; - rt.send_to(e_addr, framed_msg(inbox_e.addr(), payload)).unwrap(); - rt.send_to(d_addr, framed_msg(inbox_d.addr(), payload)).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.len(), 1); - assert_eq!(d_msgs.len(), 2); - assert_eq!(e_msgs[0], payload); - assert!(d_msgs.iter().all(|m| m == payload)); -} - -// Guest stores and reloads a f32 value -#[test] -fn guest_f32_store_and_load() { - let wat = r#"(module - (memory (export "memory") 1) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Store f32 at ptr, load it back, add 1.0, store again - (f32.store (local.get $ptr) (f32.const 3.14)) - (f32.store (local.get $ptr) - (f32.add (f32.load (local.get $ptr)) (f32.const 1.0)) - ) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![0u8; 4])).unwrap(); - rt.tick(); -} - -// Spawn 50 silent actors, tick, stop all — clean shutdown -#[test] -fn fifty_silent_actors_clean_shutdown() { - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let addrs: Vec<_> = (0..50) - .map(|_| { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("silent")) - .build().unwrap(); - rt.spawn(actor).unwrap() - }) - .collect(); - - for addr in &addrs { - rt.send_to(*addr, ByteMessage(vec![0])).unwrap(); - } - rt.tick(); - - for addr in &addrs { - rt.stop_actor(*addr); - } + let first_tick: usize = std::iter::from_fn(|| inbox.try_recv()).count(); + assert_eq!(first_tick, 1, "budget=1 processes exactly 1 per tick"); rt.tick(); rt.tick(); -} + let rest: usize = std::iter::from_fn(|| inbox.try_recv()).count(); + assert_eq!(rest, 2, "remaining 2 processed over next 2 ticks"); -// Guest with deeply nested blocks (5 levels) -#[test] -fn guest_deeply_nested_blocks() { - let wat = r#"(module - (memory (export "memory") 1) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - (block $b0 - (block $b1 - (block $b2 - (block $b3 - (block $b4 - ;; Store nesting depth at ptr - (i32.store (local.get $ptr) (i32.const 5)) - ) - ) - ) - ) - ) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![0u8; 4])).unwrap(); - rt.tick(); -} + // Self-send loop bounded by budget — no explosion + let mut cfg2 = RuntimeConfig::default(); + cfg2.actor_message_budget = 2; + let rt2 = Runtime::new(cfg2); + let echo2 = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); + let self_addr = rt2.spawn(echo2).unwrap(); + rt2.send_to(self_addr, framed_msg(&self_addr, b"loop")).unwrap(); + for _ in 0..5 { rt2.tick(); } // no panic, bounded -// Property: mixed guest types always produce expected response counts -proptest! { - #[test] - fn prop_mixed_guest_response_counts( - echo_count in 0usize..5, - double_count in 0usize..5, - silent_count in 0usize..5, - ) { - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - for _ in 0..echo_count { - 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(), b"x")).unwrap(); - } - for _ in 0..double_count { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("double")).build().unwrap(); - let addr = rt.spawn(actor).unwrap(); - rt.send_to(addr, framed_msg(inbox.addr(), b"x")).unwrap(); - } - for _ in 0..silent_count { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("silent")).build().unwrap(); - let addr = rt.spawn(actor).unwrap(); - rt.send_to(addr, ByteMessage(b"x".to_vec())).unwrap(); - } - rt.tick(); - - let msg_count = std::iter::from_fn(|| inbox.try_recv()).count(); - let expected = echo_count + double_count * 2; - assert_eq!(msg_count, expected, - "echo({echo_count})+double({double_count}*2)+silent({silent_count}*0)={expected}, got {msg_count}"); - } -} - -// ── Cycle 72 ───────────────────────────────────────────────────────────────── - -// Guest computes payload checksum and stores it -#[test] -fn guest_computes_byte_checksum() { - let wat = r#"(module - (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 $sum i32) - (local $i i32) - (local.set $i (i32.const 0)) - (local.set $sum (i32.const 0)) - (block $exit - (loop $loop - (br_if $exit (i32.ge_u (local.get $i) (local.get $len))) - (local.set $sum - (i32.add (local.get $sum) - (i32.load8_u (i32.add (local.get $ptr) (local.get $i))))) - (local.set $i (i32.add (local.get $i) (i32.const 1))) - (br $loop) - ) - ) - ;; Store checksum at offset 0 - (i32.store (i32.const 0) (local.get $sum)) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![1, 2, 3, 4, 5])).unwrap(); - rt.tick(); // sum=15 -} - -// WASM actor on multi-threaded runtime with 4 threads — send/recv pattern -#[test] -fn wasm_echo_on_four_thread_runtime_stress() { - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - let mut cfg = RuntimeConfig::default(); - cfg.num_threads = 4; - let rt = Runtime::new(cfg); - let inbox = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - for i in 0u8..20 { - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - } - - let handle = rt.run().unwrap(); - std::thread::sleep(std::time::Duration::from_millis(200)); - // Poll with retries - let mut total = 0; - for _ in 0..50 { - 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 echoes received on MT runtime"); -} - -// Guest module with multiple data segments -#[test] -fn module_with_three_data_segments() { - let wat = r#"(module - (memory (export "memory") 1) - (data (i32.const 0) "AAAA") - (data (i32.const 100) "BBBB") - (data (i32.const 200) "CCCC") - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (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(); - rt.send_to(addr, ByteMessage(vec![0])).unwrap(); - rt.tick(); -} - -// Send max i32 as message length (via ByteMessage) — too large, dropped -#[test] -fn message_larger_than_i32_max_dropped() { - // We can't actually create a 2GB message, but we can verify the i32::try_from check - // by verifying that a normal-size message works fine. The check is at actor.rs:30-33. - // This test just confirms the pathway exists by sending a modest message. - 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(); - - // Normal size: works fine - rt.send_to(addr, framed_msg(inbox.addr(), &vec![0xAB; 50000])).unwrap(); - rt.tick(); - let resp = inbox.try_recv().expect("50KB message should echo"); - assert_eq!(resp.0.len(), 50000); -} - -// Guest that does comparison operations: gt_s, lt_s, le_u, ge_s -#[test] -fn guest_comparison_operations() { - let wat = r#"(module - (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 $result i32) - ;; Test various comparisons - (local.set $result (i32.gt_s (i32.const 5) (i32.const 3))) ;; 1 - (local.set $result (i32.add (local.get $result) - (i32.lt_s (i32.const -1) (i32.const 0)))) ;; +1 = 2 - (local.set $result (i32.add (local.get $result) - (i32.le_u (i32.const 5) (i32.const 5)))) ;; +1 = 3 - (local.set $result (i32.add (local.get $result) - (i32.ge_s (i32.const 0) (i32.const -1)))) ;; +1 = 4 - ;; Store result - (i32.store (local.get $ptr) (local.get $result)) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![0u8; 4])).unwrap(); - rt.tick(); -} - -// ── Cycle 73 ───────────────────────────────────────────────────────────────── - -// Guest that traps on specific payload content (trap-on-0xFF) -#[test] -fn guest_traps_on_specific_byte_survives_others() { - let wat = r#"(module - (memory (export "memory") 1) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; If first byte is 0xFF, trap - (if (i32.and - (i32.gt_u (local.get $len) (i32.const 0)) - (i32.eq (i32.load8_u (local.get $ptr)) (i32.const 0xFF))) - (then unreachable) - ) - ) - )"#; - 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(); - - // Normal message — survives - rt.send_to(addr, ByteMessage(vec![0x42])).unwrap(); - rt.tick(); - - // Trap-triggering message — actor survives trap - rt.send_to(addr, ByteMessage(vec![0xFF])).unwrap(); - rt.tick(); - - // Another normal message — still alive - rt.send_to(addr, ByteMessage(vec![0x00])).unwrap(); - rt.tick(); -} - -// Multiple runtimes share one engine, all operate correctly -#[test] -fn three_runtimes_share_one_engine() { - let engine = SharedEngine::new().unwrap(); - let mut rts = Vec::new(); - let mut inboxes = Vec::new(); - - for _ in 0..3 { - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - 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(), b"shared-engine")).unwrap(); - inboxes.push(inbox); - rts.push(rt); - } - - for rt in &rts { - rt.tick(); - } - - for inbox in &inboxes { - let resp = inbox.try_recv().expect("each runtime should echo"); - assert_eq!(resp.0, b"shared-engine"); - } -} - -// Spawn actor, never send any message, stop — clean lifecycle -#[test] -fn spawn_no_messages_then_stop() { - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let addr = rt.spawn(actor).unwrap(); - rt.tick(); // idle tick - rt.stop_actor(addr); - rt.tick(); // cleanup - rt.tick(); // verify -} - -// Guest with i32.mul and i32.div_u -#[test] -fn guest_multiply_and_divide() { - let wat = r#"(module - (memory (export "memory") 1) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Multiply len by 3, divide by 2, store at ptr - (i32.store (local.get $ptr) - (i32.div_u - (i32.mul (local.get $len) (i32.const 3)) - (i32.const 2) - ) - ) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![0u8; 10])).unwrap(); - rt.tick(); // 10*3/2 = 15 -} - -// 1000 actors from same engine — stress test engine sharing -#[test] -fn thousand_actors_from_same_engine() { - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - - for _ in 0..1000 { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("silent")) - .build().unwrap(); - rt.spawn(actor).unwrap(); - } - rt.tick(); -} - -// ── Cycle 74 ───────────────────────────────────────────────────────────────── - -// Guest accumulates state across messages — counter tracks how many messages received -#[test] -fn guest_counts_messages_via_global() { - let wat = r#"(module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (global $count (mut i32) (i32.const 0)) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Increment counter - (global.set $count (i32.add (global.get $count) (i32.const 1))) - ;; If we have dest (>= 32 bytes), send counter value as 4-byte payload - (if (i32.ge_u (local.get $len) (i32.const 32)) - (then - ;; Store counter value at offset 900 - (i32.store (i32.const 900) (global.get $count)) - (call $send (local.get $ptr) (i32.const 900) (i32.const 4)) - ) - ) - ) - )"#; - 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(); - - // Send 5 messages across 5 ticks - for _ in 0..5 { - let mut msg = Vec::new(); - msg.extend_from_slice(&inbox.addr().0); - rt.send_to(addr, ByteMessage(msg)).unwrap(); - rt.tick(); - } - - // Collect all responses — each has a 4-byte LE counter - let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(msgs.len(), 5); - for (i, msg) in msgs.iter().enumerate() { - let count = u32::from_le_bytes([msg[0], msg[1], msg[2], msg[3]]); - assert_eq!(count, (i + 1) as u32, "counter should increment"); - } -} - -// Echo actor with DropOldest mailbox policy -#[test] -fn echo_with_drop_oldest_mailbox() { + // DropOldest mailbox use swactor::runtime::MailboxOverflow; - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - let mut cfg = RuntimeConfig::default(); - cfg.default_mailbox_capacity = 3; - cfg.mailbox_overflow = MailboxOverflow::DropOldest; - let rt = Runtime::new(cfg); - let inbox = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - // Send 10 messages — only capacity messages retained - for i in 0u8..10 { - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); + let mut cfg3 = RuntimeConfig::default(); + cfg3.default_mailbox_capacity = 3; + cfg3.mailbox_overflow = MailboxOverflow::DropOldest; + let rt3 = Runtime::new(cfg3); + let inbox3 = rt3.new_inbox::().unwrap(); + let echo3 = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); + let addr3 = rt3.spawn(echo3).unwrap(); + for i in 0..10u8 { + rt3.send_to(addr3, framed_msg(inbox3.addr(), &[i])).unwrap(); } - rt.tick(); - - let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - // With DropOldest(3), only the 3 most recent should survive - assert!(msgs.len() <= 10, "got {}", msgs.len()); - assert!(msgs.len() >= 1, "at least some messages processed"); + rt3.tick(); + let responses: Vec<_> = std::iter::from_fn(|| inbox3.try_recv()).collect(); + assert!(responses.len() <= 3, "mailbox capacity limits processing: got {}", responses.len()); } -// Guest echoes only if len is odd — conditional response #[test] -fn guest_echoes_only_odd_length_messages() { - 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) - ;; Only echo if payload (len-32) is odd length - (if (i32.and - (i32.ge_u (local.get $len) (i32.const 33)) - (i32.and (i32.sub (local.get $len) (i32.const 32)) (i32.const 1))) - (then - (call $send - (local.get $ptr) - (i32.add (local.get $ptr) (i32.const 32)) - (i32.sub (local.get $len) (i32.const 32)) - ) - ) - ) - ) - )"#; +fn multiple_runtimes_and_scale() { 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(); - // Odd payload (3 bytes) — should echo - rt.send_to(addr, framed_msg(inbox.addr(), b"abc")).unwrap(); - // Even payload (2 bytes) — should not echo - rt.send_to(addr, framed_msg(inbox.addr(), b"ab")).unwrap(); - // Odd payload (1 byte) — should echo - rt.send_to(addr, framed_msg(inbox.addr(), b"x")).unwrap(); - rt.tick(); - - let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(msgs.len(), 2, "only odd-length payloads echoed"); - assert_eq!(msgs[0], b"abc"); - assert_eq!(msgs[1], b"x"); -} - -// Build error Display formatting includes useful info -#[test] -fn build_error_display_contains_export_name() { - let wat = r#"(module (memory (export "memory") 1))"#; - let engine = SharedEngine::new().unwrap(); - let err = WasmActorBuilder::new(engine, wat::parse_str(wat).unwrap()) - .build().err().expect("should fail"); - let msg = format!("{err}"); - assert!(msg.contains("alloc"), "error should mention missing 'alloc' export: {msg}"); -} - -// ── Cycle 75 ───────────────────────────────────────────────────────────────── - -// Guest with max function params — handle still only takes (i32, i32) though -// Extra internal functions can have many params -#[test] -fn guest_internal_function_with_many_params() { - let wat = r#"(module - (memory (export "memory") 1) - (func $helper (param i32 i32 i32 i32 i32 i32 i32 i32) (result i32) - ;; Sum all 8 params - (i32.add (local.get 0) (i32.add (local.get 1) (i32.add (local.get 2) - (i32.add (local.get 3) (i32.add (local.get 4) (i32.add (local.get 5) - (i32.add (local.get 6) (local.get 7)))))))) - ) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - (i32.store (local.get $ptr) - (call $helper - (i32.const 1) (i32.const 2) (i32.const 3) (i32.const 4) - (i32.const 5) (i32.const 6) (i32.const 7) (i32.const 8))) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![0u8; 4])).unwrap(); - rt.tick(); // stores 36 at ptr -} - -// Echo actor relays message through native forwarder back to inbox -#[test] -fn wasm_to_native_to_inbox_relay() { - let engine = SharedEngine::new().unwrap(); - let echo = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - // Native forwarder: receives ByteMessage, sends to inbox - struct NativeForward(ActorAddress); - impl ActorInterface for NativeForward { - type Incoming = ByteMessage; - type Response = (); - fn handle(&mut self, ctx: &Ctx, msg: ByteMessage) { - let _ = ctx.send(self.0, msg); - } - } - - let fwd_addr = rt.spawn(NativeForward(*inbox.addr())).unwrap(); - let echo_addr = rt.spawn(echo).unwrap(); - - // Send to echo, echo sends to forwarder, forwarder sends to inbox - rt.send_to(echo_addr, framed_msg(&fwd_addr, &inbox.addr().0.to_vec())).unwrap(); - rt.tick(); // echo → forwarder - rt.tick(); // forwarder → inbox - - let resp = inbox.try_recv().expect("relayed through native forwarder"); - assert_eq!(resp.0, inbox.addr().0.to_vec()); -} - -// Property: any message size from 0 to 10000 round-trips through echo -proptest! { - #[test] - fn prop_any_message_size_round_trips(size in 0usize..10000) { - 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![0x42u8; size]; - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - rt.tick(); - - let resp = inbox.try_recv().expect("echo should respond"); - assert_eq!(resp.0.len(), size); - } -} - -// Guest with i32 conversion: extend 16-bit signed -#[test] -fn guest_sign_extend_16() { - let wat = r#"(module - (memory (export "memory") 1) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Sign-extend 16-bit value: 0xFFFF -> -1 - (i32.store (local.get $ptr) - (i32.extend16_s (i32.const 0xFFFF)) - ) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![0u8; 4])).unwrap(); - rt.tick(); -} - -// Echo actor processes message, then gets more after 100 idle ticks -#[test] -fn echo_after_hundred_idle_ticks() { - 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(); - - // First message - rt.send_to(addr, framed_msg(inbox.addr(), b"before")).unwrap(); - rt.tick(); - assert_eq!(inbox.try_recv().unwrap().0, b"before"); - - // 100 idle ticks - for _ in 0..100 { - rt.tick(); - } - - // Second message — still works - rt.send_to(addr, framed_msg(inbox.addr(), b"after")).unwrap(); - rt.tick(); - assert_eq!(inbox.try_recv().unwrap().0, b"after"); -} - -// ── Cycle 76 ───────────────────────────────────────────────────────────────── - -// Guest with complex send pattern: send to two different addresses in one handle -#[test] -fn guest_sends_to_two_addresses_in_one_handle() { - 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) - ;; Message layout: [addr1:32][addr2:32][payload:rest] - ;; Send payload to both addresses - (if (i32.ge_u (local.get $len) (i32.const 65)) - (then - ;; Send to addr1 - (call $send - (local.get $ptr) - (i32.add (local.get $ptr) (i32.const 64)) - (i32.sub (local.get $len) (i32.const 64)) - ) - ;; Send to addr2 - (call $send - (i32.add (local.get $ptr) (i32.const 32)) - (i32.add (local.get $ptr) (i32.const 64)) - (i32.sub (local.get $len) (i32.const 64)) - ) - ) - ) - ) - )"#; - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, wat::parse_str(wat).unwrap()) - .build().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox1 = rt.new_inbox::().unwrap(); - let inbox2 = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - let mut msg = Vec::new(); - msg.extend_from_slice(&inbox1.addr().0); // addr1 - msg.extend_from_slice(&inbox2.addr().0); // addr2 - msg.extend_from_slice(b"shared-data"); // payload - rt.send_to(addr, ByteMessage(msg)).unwrap(); - rt.tick(); - - let r1 = inbox1.try_recv().expect("inbox1 should receive"); - let r2 = inbox2.try_recv().expect("inbox2 should receive"); - assert_eq!(r1.0, b"shared-data"); - assert_eq!(r2.0, b"shared-data"); -} - -// Build actor, clone engine, build another actor — verify independence -#[test] -fn engine_clone_builds_independent_actors() { - let engine = SharedEngine::new().unwrap(); - let clone = engine.clone(); - let a1 = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - let a2 = WasmActorBuilder::new(clone, guest_wasm("double")).build().unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let addr1 = rt.spawn(a1).unwrap(); - let addr2 = rt.spawn(a2).unwrap(); - - rt.send_to(addr1, framed_msg(inbox.addr(), b"A")).unwrap(); - rt.send_to(addr2, framed_msg(inbox.addr(), b"B")).unwrap(); - rt.tick(); - - let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(msgs.len(), 3, "1 echo + 2 double = 3"); -} - -// Guest handles 500 messages in a single tick -#[test] -fn five_hundred_messages_in_one_tick() { - 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(); - - for i in 0..500u16 { - let payload = i.to_le_bytes().to_vec(); - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - } - // May need multiple ticks depending on budget - for _ in 0..20 { - rt.tick(); - } - - let msgs: Vec<_> = std::iter::from_fn(|| inbox.try_recv()).collect(); - assert_eq!(msgs.len(), 500, "all 500 messages echoed"); -} - -// Guest with return in the middle of handle — early exit -#[test] -fn guest_early_return_from_handle() { - 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) - ;; If len < 33, return early (no send) - (if (i32.lt_u (local.get $len) (i32.const 33)) - (then return) - ) - ;; Only reaches here for long messages - (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(); - - // Short message — no response - rt.send_to(addr, ByteMessage(vec![0u8; 10])).unwrap(); - rt.tick(); - assert!(inbox.try_recv().is_none()); - - // Long message — response - rt.send_to(addr, framed_msg(inbox.addr(), b"long-enough")).unwrap(); - rt.tick(); - let resp = inbox.try_recv().expect("long message should echo"); - assert_eq!(resp.0, b"long-enough"); -} - -// ── Cycle 77 ───────────────────────────────────────────────────────────────── - -// Guest that responds with byte at each index: response[0] = payload[len-1], etc. -// (reverse payload using a loop, then send) -#[test] -fn guest_reverses_and_echoes_payload() { - let wat = r#"(module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (global $heap (mut i32) (i32.const 2048)) - (func (export "alloc") (param $len i32) (result i32) - (local $ptr i32) - (local.set $ptr (global.get $heap)) - (global.set $heap (i32.add (global.get $heap) (local.get $len))) - (local.get $ptr) - ) - (func (export "handle") (param $ptr i32) (param $len i32) - (local $i i32) - (local $payload_start i32) - (local $payload_len i32) - (local $out_ptr i32) - ;; Need >= 33 bytes - (if (i32.lt_u (local.get $len) (i32.const 33)) (then return)) - (local.set $payload_start (i32.add (local.get $ptr) (i32.const 32))) - (local.set $payload_len (i32.sub (local.get $len) (i32.const 32))) - ;; Allocate output buffer at offset 900 - (local.set $out_ptr (i32.const 900)) - ;; Reverse loop - (local.set $i (i32.const 0)) - (block $exit - (loop $loop - (br_if $exit (i32.ge_u (local.get $i) (local.get $payload_len))) - (i32.store8 - (i32.add (local.get $out_ptr) (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) (local.get $out_ptr) (local.get $payload_len)) - ) - )"#; - 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(), b"abcde")).unwrap(); - rt.tick(); - - let resp = inbox.try_recv().expect("reversed echo"); - assert_eq!(resp.0, b"edcba"); -} - -// Multiple actors with different budgets in same runtime -#[test] -fn actors_share_single_budget_setting() { - let engine = SharedEngine::new().unwrap(); - let e1 = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - let e2 = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - let mut cfg = RuntimeConfig::default(); - cfg.actor_message_budget = 3; - let rt = Runtime::new(cfg); - let inbox = rt.new_inbox::().unwrap(); - let a1 = rt.spawn(e1).unwrap(); - let a2 = rt.spawn(e2).unwrap(); - - // Send 5 to each - for i in 0u8..5 { - rt.send_to(a1, framed_msg(inbox.addr(), &[i])).unwrap(); - rt.send_to(a2, framed_msg(inbox.addr(), &[i + 100])).unwrap(); - } - rt.tick(); - - // With budget=3, each actor processes at most 3 per tick - let count = std::iter::from_fn(|| inbox.try_recv()).count(); - assert!(count <= 6, "at most 3 per actor × 2 actors = 6, got {count}"); - assert!(count >= 2, "at least 1 per actor"); -} - -// Guest uses f64 arithmetic -#[test] -fn guest_f64_arithmetic() { - let wat = r#"(module - (memory (export "memory") 1) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - (f64.store (local.get $ptr) - (f64.mul (f64.const 2.5) (f64.const 4.0))) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![0u8; 8])).unwrap(); - rt.tick(); // stores 10.0 as f64 -} - -// Spawn, send, stop, respawn with new actor — complete replacement -#[test] -fn actor_replacement_cycle() { - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - for round in 0..5u8 { - 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(), &[round])).unwrap(); - rt.tick(); - let resp = inbox.try_recv().expect("each round should echo"); - assert_eq!(resp.0, vec![round]); - rt.stop_actor(addr); - rt.tick(); - } -} - -// ── Cycle 78 ───────────────────────────────────────────────────────────────── - -// Guest with recursive helper that computes fibonacci (bounded) -#[test] -fn guest_fibonacci_via_recursion() { - let wat = r#"(module - (memory (export "memory") 1) - (func $fib (param $n i32) (result i32) - (if (result i32) (i32.le_u (local.get $n) (i32.const 1)) - (then (local.get $n)) - (else - (i32.add - (call $fib (i32.sub (local.get $n) (i32.const 1))) - (call $fib (i32.sub (local.get $n) (i32.const 2))) - ) - ) - ) - ) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Compute fib(10) = 55, store at ptr - (i32.store (local.get $ptr) (call $fib (i32.const 10))) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![0u8; 4])).unwrap(); - rt.tick(); -} - -// Inbox receives messages from multiple WASM actors simultaneously -#[test] -fn single_inbox_receives_from_five_actors() { - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - for i in 0..5u8 { - 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(); - } - rt.tick(); - - let mut msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - msgs.sort(); - assert_eq!(msgs.len(), 5); - for i in 0..5u8 { - assert_eq!(msgs[i as usize], vec![i]); - } -} - -// Guest that does nothing at all — purely exercises spawn+tick+stop lifecycle -#[test] -fn minimal_lifecycle_no_messages() { - let wat = r#"(module - (memory (export "memory") 1) - (func (export "alloc") (param $len i32) (result i32) i32.const 0) - (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(); - rt.tick(); - rt.tick(); - rt.stop_actor(addr); - rt.tick(); -} - -// Same message bytes reused for multiple sends — no aliasing issues -#[test] -fn reuse_message_bytes_for_multiple_sends() { - 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 msg = framed_msg(inbox.addr(), b"reuse"); - for _ in 0..5 { - // Clone the same message each time - rt.send_to(addr, msg.clone()).unwrap(); - } - rt.tick(); - - let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(msgs.len(), 5); - assert!(msgs.iter().all(|m| m == b"reuse")); -} - -// Guest uses block with result value -#[test] -fn guest_block_with_result_value() { - let wat = r#"(module - (memory (export "memory") 1) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; Block that produces a value - (i32.store (local.get $ptr) - (block (result i32) - (i32.add (local.get $len) (i32.const 42)) - ) - ) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![0u8; 4])).unwrap(); - rt.tick(); -} - -// ── Cycle 79 ───────────────────────────────────────────────────────────────── - -// Echo actor handles binary pattern: all zeros then all ones -#[test] -fn echo_zeros_then_ones() { - 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 zeros = vec![0u8; 256]; - let ones = vec![0xFF; 256]; - rt.send_to(addr, framed_msg(inbox.addr(), &zeros)).unwrap(); - rt.send_to(addr, framed_msg(inbox.addr(), &ones)).unwrap(); - rt.tick(); - - let r1 = inbox.try_recv().unwrap(); - let r2 = inbox.try_recv().unwrap(); - assert!(r1.0.iter().all(|&b| b == 0)); - assert!(r2.0.iter().all(|&b| b == 0xFF)); -} - -// Guest with 3 exported functions (only alloc and handle required) -#[test] -fn guest_with_extra_exported_function_and_init() { - let wat = r#"(module - (memory (export "memory") 1) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32)) - (func (export "init") (result i32) i32.const 42) - (func (export "cleanup")) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![1])).unwrap(); - rt.tick(); -} - -// Property: double actor always sends exactly 2x copies -proptest! { - #[test] - fn prop_double_always_sends_two_copies( - payload in proptest::collection::vec(0u8..=255, 1..200) - ) { - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("double")).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(), &payload)).unwrap(); - rt.tick(); - - let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(msgs.len(), 2, "double always produces 2 copies"); - assert_eq!(msgs[0], payload); - assert_eq!(msgs[1], payload); - } -} - -// Mix of echo, double, and silent actors — 10 of each -#[test] -fn thirty_mixed_actors_simultaneous() { - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - // 10 echo actors - for _ in 0..10 { - 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(), b"e")).unwrap(); - } - // 10 double actors - for _ in 0..10 { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("double")).build().unwrap(); - let addr = rt.spawn(actor).unwrap(); - rt.send_to(addr, framed_msg(inbox.addr(), b"d")).unwrap(); - } - // 10 silent actors - for _ in 0..10 { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("silent")).build().unwrap(); - let addr = rt.spawn(actor).unwrap(); - rt.send_to(addr, ByteMessage(b"s".to_vec())).unwrap(); - } - rt.tick(); - - let total = std::iter::from_fn(|| inbox.try_recv()).count(); - // 10 echo + 10*2 double + 0 silent = 30 - assert_eq!(total, 30); -} - -// ── Cycle 80 ───────────────────────────────────────────────────────────────── - -// Guest that sends response with length prefix (4-byte LE length + payload) -#[test] -fn guest_length_prefixed_response() { - 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 $payload_len i32) - (if (i32.lt_u (local.get $len) (i32.const 33)) (then return)) - (local.set $payload_len (i32.sub (local.get $len) (i32.const 32))) - ;; Write length prefix at offset 900 - (i32.store (i32.const 900) (local.get $payload_len)) - ;; Copy payload after length prefix - (memory.copy - (i32.const 904) - (i32.add (local.get $ptr) (i32.const 32)) - (local.get $payload_len) - ) - ;; Send length-prefixed response - (call $send - (local.get $ptr) - (i32.const 900) - (i32.add (local.get $payload_len) (i32.const 4)) - ) - ) - )"#; - 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(), b"hello")).unwrap(); - rt.tick(); - - let resp = inbox.try_recv().expect("length-prefixed response"); - // First 4 bytes are length (5 as u32 LE), then "hello" - let len = u32::from_le_bytes([resp.0[0], resp.0[1], resp.0[2], resp.0[3]]); - assert_eq!(len, 5); - assert_eq!(&resp.0[4..], b"hello"); -} - -// Two WASM actors sending to each other (ping-pong bounded by budget) -#[test] -fn two_wasm_actors_ping_pong_bounded() { - let engine = SharedEngine::new().unwrap(); + // Two runtimes share engine + let rt1 = Runtime::new(RuntimeConfig::default()); + let rt2 = Runtime::new(RuntimeConfig::default()); + let inbox1 = rt1.new_inbox::().unwrap(); + let inbox2 = rt2.new_inbox::().unwrap(); let a1 = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - let a2 = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - let mut cfg = RuntimeConfig::default(); - cfg.actor_message_budget = 2; - let rt = Runtime::new(cfg); + let a2 = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); + let addr1 = rt1.spawn(a1).unwrap(); + let addr2 = rt2.spawn(a2).unwrap(); + rt1.send_to(addr1, framed_msg(inbox1.addr(), b"rt1")).unwrap(); + rt2.send_to(addr2, framed_msg(inbox2.addr(), b"rt2")).unwrap(); + rt1.tick(); + rt2.tick(); + assert_eq!(inbox1.try_recv().unwrap().0, b"rt1"); + assert_eq!(inbox2.try_recv().unwrap().0, b"rt2"); - let addr1 = rt.spawn(a1).unwrap(); - let addr2 = rt.spawn(a2).unwrap(); - - // A1 echoes to A2, A2 echoes back to A1 — ping-pong loop - rt.send_to(addr1, framed_msg(&addr2, &addr1.0.to_vec())).unwrap(); - for _ in 0..10 { - rt.tick(); // bounded by budget, never explodes + // 50 actors from same engine + let rt3 = Runtime::new(RuntimeConfig::default()); + let inbox3 = rt3.new_inbox::().unwrap(); + for i in 0..50u8 { + let a = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); + let addr = rt3.spawn(a).unwrap(); + rt3.send_to(addr, framed_msg(inbox3.addr(), &[i])).unwrap(); } -} + rt3.tick(); + let count: usize = std::iter::from_fn(|| inbox3.try_recv()).count(); + assert_eq!(count, 50); -// Build 100 actors but don't spawn them — verify no leaks on drop -#[test] -fn build_hundred_actors_then_drop() { - let engine = SharedEngine::new().unwrap(); - let mut actors = Vec::new(); + // Build+drop 100 actors without spawning — no leak for _ in 0..100 { - actors.push(WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap()); + let _ = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); } - drop(actors); // all 100 Stores dropped cleanly } -// Guest uses memory.size then memory.grow, verifying size changes #[test] -fn guest_memory_size_and_grow_sequence() { - let wat = r#"(module - (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 $old_size i32) - ;; Check initial size - (local.set $old_size (memory.size)) - ;; Grow by 2 pages - (drop (memory.grow (i32.const 2))) - ;; Store old_size and new_size at ptr - (if (i32.ge_u (local.get $len) (i32.const 8)) - (then - (i32.store (local.get $ptr) (local.get $old_size)) - (i32.store (i32.add (local.get $ptr) (i32.const 4)) (memory.size)) - ) - ) - ) - )"#; +fn ordering_and_determinism() { 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(); - rt.send_to(addr, ByteMessage(vec![0u8; 8])).unwrap(); - rt.tick(); -} -// Send exact same ByteMessage to 10 actors simultaneously -#[test] -fn broadcast_to_ten_actors() { - let engine = SharedEngine::new().unwrap(); + // FIFO ordering let rt = Runtime::new(RuntimeConfig::default()); let inbox = rt.new_inbox::().unwrap(); - - let addrs: Vec<_> = (0..10) - .map(|_| { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - rt.spawn(actor).unwrap() - }) - .collect(); - - let msg = framed_msg(inbox.addr(), b"broadcast"); - for addr in &addrs { - rt.send_to(*addr, msg.clone()).unwrap(); - } - rt.tick(); - - let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(msgs.len(), 10); - assert!(msgs.iter().all(|m| m == b"broadcast")); -} - -// ── Cycle 81 ───────────────────────────────────────────────────────────────── - -// Guest that does bitwise NOT on each byte -#[test] -fn guest_bitwise_not_each_byte() { - 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) - (if (i32.lt_u (local.get $len) (i32.const 33)) (then return)) - ;; NOT each byte in the payload portion - (local.set $i (i32.const 32)) - (block $exit - (loop $loop - (br_if $exit (i32.ge_u (local.get $i) (local.get $len))) - (i32.store8 - (i32.add (local.get $ptr) (local.get $i)) - (i32.xor - (i32.load8_u (i32.add (local.get $ptr) (local.get $i))) - (i32.const 0xFF))) - (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(), &[0x00, 0xFF, 0x55, 0xAA])).unwrap(); - rt.tick(); - - let resp = inbox.try_recv().expect("NOT response"); - assert_eq!(resp.0, vec![0xFF, 0x00, 0xAA, 0x55]); -} - -// Stop actor during multi-threaded runtime (MT stop) -#[test] -fn stop_wasm_actor_on_mt_runtime() { - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - let mut cfg = RuntimeConfig::default(); - cfg.num_threads = 2; - let rt = Runtime::new(cfg); - let inbox = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - rt.send_to(addr, framed_msg(inbox.addr(), b"mt")).unwrap(); - let handle = rt.run().unwrap(); - std::thread::sleep(std::time::Duration::from_millis(100)); - - // Drain inbox - let mut count = 0; - for _ in 0..20 { - count += std::iter::from_fn(|| inbox.try_recv()).count(); - if count >= 1 { break; } - std::thread::sleep(std::time::Duration::from_millis(50)); - } - handle.shutdown(); - assert_eq!(count, 1, "echo received on MT runtime"); -} - -// Guest with i64 extend operations -#[test] -fn guest_i64_extend_operations() { - let wat = r#"(module - (memory (export "memory") 1) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; i64.extend_i32_s: sign-extend 32-bit -1 to 64-bit - (i64.store (local.get $ptr) - (i64.extend_i32_s (i32.const -1))) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![0u8; 8])).unwrap(); - rt.tick(); -} - -// 5 runtimes running sequentially, each with a WASM actor -#[test] -fn five_sequential_runtimes() { - let engine = SharedEngine::new().unwrap(); - for i in 0..5u8 { - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - let addr = rt.spawn(actor).unwrap(); + let echo = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); + let addr = rt.spawn(echo).unwrap(); + for i in 0..20u8 { rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - rt.tick(); - assert_eq!(inbox.try_recv().unwrap().0, vec![i]); - // runtime dropped here - } -} - -// Property: any combination of ticks and messages never panics -proptest! { - #[test] - fn prop_random_tick_message_interleaving( - ops in proptest::collection::vec( - proptest::bool::ANY, // true = send, false = tick - 1..30 - ) - ) { - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("silent")).build().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let addr = rt.spawn(actor).unwrap(); - - for send in &ops { - if *send { - let _ = rt.send_to(addr, ByteMessage(vec![1, 2, 3])); - } else { - rt.tick(); - } - } - } -} - -// ── Cycle 82 ───────────────────────────────────────────────────────────────── - -// Guest computes max of all bytes in payload -#[test] -fn guest_computes_max_byte() { - let wat = r#"(module - (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 $max i32) - (local $val i32) - (local.set $max (i32.const 0)) - (local.set $i (i32.const 0)) - (block $exit - (loop $loop - (br_if $exit (i32.ge_u (local.get $i) (local.get $len))) - (local.set $val (i32.load8_u (i32.add (local.get $ptr) (local.get $i)))) - (if (i32.gt_u (local.get $val) (local.get $max)) - (then (local.set $max (local.get $val))) - ) - (local.set $i (i32.add (local.get $i) (i32.const 1))) - (br $loop) - ) - ) - (i32.store (i32.const 0) (local.get $max)) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![10, 250, 30, 100])).unwrap(); - rt.tick(); -} - -// Echo actor handles message containing its own address bytes -#[test] -fn echo_message_containing_own_address() { - 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(); - - // Payload contains the actor's own address bytes - rt.send_to(addr, framed_msg(inbox.addr(), &addr.0)).unwrap(); - rt.tick(); - - let resp = inbox.try_recv().expect("echo response"); - assert_eq!(resp.0, addr.0.to_vec(), "actor's address echoed as payload"); -} - -// Rapidly create and destroy engines -#[test] -fn rapid_engine_creation_destruction() { - for _ in 0..50 { - let engine = SharedEngine::new().unwrap(); - let _ = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - // engine and actor dropped here - } -} - -// Guest uses select instruction to pick between two values -#[test] -fn guest_select_conditional_value() { - let wat = r#"(module - (memory (export "memory") 1) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; select: if len > 10, store 999, else store 111 - (i32.store (local.get $ptr) - (select - (i32.const 999) - (i32.const 111) - (i32.gt_u (local.get $len) (i32.const 10)) - ) - ) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![0u8; 5])).unwrap(); - rt.send_to(addr, ByteMessage(vec![0u8; 20])).unwrap(); - rt.tick(); -} - -// Echo handles exactly 1 byte — smallest meaningful payload -#[test] -fn echo_single_byte_payload_integrity() { - 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(); - - for byte in 0u8..=255 { - rt.send_to(addr, framed_msg(inbox.addr(), &[byte])).unwrap(); - } - // Process all 256 in batches - for _ in 0..10 { - rt.tick(); - } - - let mut responses: Vec = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0[0])).collect(); - responses.sort(); - assert_eq!(responses.len(), 256); - for (i, &b) in responses.iter().enumerate() { - assert_eq!(b, i as u8); - } -} - -// ── Cycle 83 ───────────────────────────────────────────────────────────────── - -// Spawn echo, send 1000 messages, verify all received over multiple ticks -#[test] -fn thousand_message_echo_stress() { - 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(); - - for i in 0u16..1000 { - rt.send_to(addr, framed_msg(inbox.addr(), &i.to_le_bytes())).unwrap(); - } - for _ in 0..50 { - rt.tick(); - } - - let count = std::iter::from_fn(|| inbox.try_recv()).count(); - assert_eq!(count, 1000, "all 1000 messages echoed"); -} - -// Guest with type annotations on all locals (verbose WAT) -#[test] -fn guest_with_many_typed_locals() { - let wat = r#"(module - (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 $a i32) (local $b i32) (local $c i32) - (local $d i64) (local $e f32) (local $f f64) - (local.set $a (local.get $len)) - (local.set $b (i32.mul (local.get $a) (i32.const 2))) - (local.set $c (i32.add (local.get $a) (local.get $b))) - (local.set $d (i64.extend_i32_u (local.get $c))) - (local.set $e (f32.convert_i32_s (local.get $c))) - (local.set $f (f64.promote_f32 (local.get $e))) - (i32.store (local.get $ptr) (local.get $c)) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![0u8; 4])).unwrap(); - rt.tick(); -} - -// Same actor handles empty and non-empty messages alternately -#[test] -fn alternate_empty_and_nonempty_messages() { - 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(); - - for i in 0..10 { - if i % 2 == 0 { - rt.send_to(addr, ByteMessage(vec![])).unwrap(); // empty - } else { - rt.send_to(addr, framed_msg(inbox.addr(), &[i as u8])).unwrap(); - } } rt.tick(); + let msgs: Vec = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0[0])).collect(); + assert_eq!(msgs, (0..20u8).collect::>(), "FIFO preserved"); - // Only the non-empty framed messages should produce echo responses - let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(msgs.len(), 5, "5 framed messages echoed"); -} - -// WasmActorError variants have distinct Display representations -#[test] -fn error_variants_have_distinct_display() { - let missing = WasmActorError::MissingExport("memory"); - let wasmtime_err = WasmActorError::Wasmtime(wasmtime::Error::msg("test error")); - - let s1 = format!("{missing}"); - let s2 = format!("{wasmtime_err}"); - - assert_ne!(s1, s2, "error variants should have different display"); - assert!(s1.contains("memory")); - assert!(s2.contains("test error")); -} - -// Two different WAT modules on same runtime — heterogeneous actors -#[test] -fn heterogeneous_wat_actors_on_same_runtime() { - // Actor 1: echoes - let wat_echo = 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) - (if (i32.ge_u (local.get $len) (i32.const 33)) - (then (call $send (local.get $ptr) (i32.add (local.get $ptr) (i32.const 32)) - (i32.sub (local.get $len) (i32.const 32)))))) - )"#; - // Actor 2: always sends byte 0x42 - let wat_const = r#"(module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (data (i32.const 900) "\42") - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - (if (i32.ge_u (local.get $len) (i32.const 32)) - (then (call $send (local.get $ptr) (i32.const 900) (i32.const 1))))) - )"#; - - let engine = SharedEngine::new().unwrap(); - let a1 = WasmActorBuilder::new(engine.clone(), wat::parse_str(wat_echo).unwrap()) - .build().unwrap(); - let a2 = WasmActorBuilder::new(engine, wat::parse_str(wat_const).unwrap()) - .build().unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let addr1 = rt.spawn(a1).unwrap(); - let addr2 = rt.spawn(a2).unwrap(); - - rt.send_to(addr1, framed_msg(inbox.addr(), b"hello")).unwrap(); - let mut msg2 = Vec::new(); - msg2.extend_from_slice(&inbox.addr().0); - rt.send_to(addr2, ByteMessage(msg2)).unwrap(); - rt.tick(); - - let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(msgs.len(), 2); - // One is "hello", other is [0x42] - assert!(msgs.iter().any(|m| m == b"hello")); - assert!(msgs.iter().any(|m| m == &[0x42])); -} - -// ── Cycle 84 ───────────────────────────────────────────────────────────────── - -// Guest inverts bit pattern: i32.xor with 0xFFFFFFFF on 4-byte chunks -#[test] -fn guest_inverts_i32_pattern() { - 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) - ;; Need at least 36 bytes (32 addr + 4 payload) - (if (i32.lt_u (local.get $len) (i32.const 36)) (then return)) - ;; XOR 4 bytes at ptr+32 with 0xFFFFFFFF - (i32.store (i32.add (local.get $ptr) (i32.const 32)) - (i32.xor - (i32.load (i32.add (local.get $ptr) (i32.const 32))) - (i32.const -1))) - (call $send - (local.get $ptr) - (i32.add (local.get $ptr) (i32.const 32)) - (i32.const 4)) - ) - )"#; - 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(), &[0x12, 0x34, 0x56, 0x78])).unwrap(); - rt.tick(); - - let resp = inbox.try_recv().expect("inverted pattern"); - assert_eq!(resp.0, vec![0xED, 0xCB, 0xA9, 0x87]); -} - -// Native actor that spawns WASM and sends to it -struct NativeWasmSpawner2 { - engine: SharedEngine, - inbox_addr: ActorAddress, -} -impl ActorInterface for NativeWasmSpawner2 { - type Incoming = ByteMessage; - type Response = (); - fn handle(&mut self, ctx: &Ctx, _msg: ByteMessage) { - let actor = WasmActorBuilder::new(self.engine.clone(), guest_wasm("echo")) - .build().unwrap(); - let wasm_addr = ctx.spawn(actor).unwrap(); - let _ = ctx.send(wasm_addr, framed_msg(&self.inbox_addr, b"spawned-inline")); - } -} - -#[test] -fn native_spawns_wasm_and_sends_in_handler() { - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - let spawner = NativeWasmSpawner2 { - engine, - inbox_addr: *inbox.addr(), - }; - let s_addr = rt.spawn(spawner).unwrap(); - rt.send_to(s_addr, ByteMessage(vec![])).unwrap(); - rt.tick(); // spawner spawns WASM + sends to it - rt.tick(); // WASM echo processes message - rt.tick(); // ensure delivery - - let resp = inbox.try_recv().expect("spawned WASM should echo"); - assert_eq!(resp.0, b"spawned-inline"); -} - -// Guest with nested loop computing factorial(5) = 120 -#[test] -fn guest_factorial_loop() { - let wat = r#"(module - (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 $result i32) - (local $n i32) - (local.set $result (i32.const 1)) - (local.set $n (i32.const 5)) - (block $exit - (loop $loop - (br_if $exit (i32.le_u (local.get $n) (i32.const 1))) - (local.set $result (i32.mul (local.get $result) (local.get $n))) - (local.set $n (i32.sub (local.get $n) (i32.const 1))) - (br $loop) - ) - ) - (i32.store (local.get $ptr) (local.get $result)) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![0u8; 4])).unwrap(); - rt.tick(); -} - -// Verify echo determinism: same input always produces same output -#[test] -fn echo_determinism_ten_runs() { - let engine = SharedEngine::new().unwrap(); - let payload = b"deterministic-test-payload"; - - let mut outputs = Vec::new(); - for _ in 0..10 { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).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(), payload)).unwrap(); - rt.tick(); - outputs.push(inbox.try_recv().unwrap().0); - } - - assert!(outputs.iter().all(|o| o == &outputs[0]), - "all 10 runs should produce identical output"); -} - -// ── Cycle 85 ───────────────────────────────────────────────────────────────── - -// Echo receives 7-byte payload (non-aligned) — tests non-power-of-2 sizes -#[test] -fn echo_seven_byte_non_aligned_payload() { - 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![0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77]; - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - rt.tick(); - - let resp = inbox.try_recv().expect("7-byte echo"); - assert_eq!(resp.0, payload); -} - -// Send to actor after runtime tick with no actors — just tests tick robustness -#[test] -fn tick_empty_runtime_then_spawn_and_use() { - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - - // Tick empty runtime + // Determinism: same input → same output across runs + let mut results = Vec::new(); for _ in 0..5 { - rt.tick(); - } - - // Now spawn and use - let actor = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - let inbox = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - rt.send_to(addr, framed_msg(inbox.addr(), b"after-empty")).unwrap(); - rt.tick(); - - assert_eq!(inbox.try_recv().unwrap().0, b"after-empty"); -} - -// Guest that does nothing but grow memory 5 times -#[test] -fn guest_grows_memory_five_times() { - let wat = r#"(module - (memory (export "memory") 1) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - (drop (memory.grow (i32.const 1))) - (drop (memory.grow (i32.const 1))) - (drop (memory.grow (i32.const 1))) - (drop (memory.grow (i32.const 1))) - (drop (memory.grow (i32.const 1))) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![0])).unwrap(); - rt.tick(); // grows from 1 to 6 pages — no panic -} - -// ByteMessage implements Clone — test that cloned messages are independent -#[test] -fn byte_message_clone_independence() { - let original = ByteMessage(vec![1, 2, 3]); - let clone = original.clone(); - assert_eq!(original.0, clone.0); - // They should be equal but independent - drop(original); - assert_eq!(clone.0, vec![1, 2, 3]); -} - -// Guest echo processes max-budget messages, leaves rest for next tick -#[test] -fn budget_leaves_remaining_for_next_tick() { - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - let mut cfg = RuntimeConfig::default(); - cfg.actor_message_budget = 5; - let rt = Runtime::new(cfg); - let inbox = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - // Send 12 messages - for i in 0u8..12 { - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - } - - // First tick: up to 5 - rt.tick(); - let t1 = std::iter::from_fn(|| inbox.try_recv()).count(); - - // Second tick: up to 5 more - rt.tick(); - let t2 = std::iter::from_fn(|| inbox.try_recv()).count(); - - // Third tick: remaining - rt.tick(); - let t3 = std::iter::from_fn(|| inbox.try_recv()).count(); - - assert_eq!(t1 + t2 + t3, 12, "all 12 processed across ticks"); - assert!(t1 <= 5, "budget limits first tick"); -} - -// ── Cycle 86 ───────────────────────────────────────────────────────────────── - -// Guest doubles only specific payloads, echoes others -#[test] -fn guest_conditional_double_or_echo() { - 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 $payload_len i32) - (if (i32.lt_u (local.get $len) (i32.const 33)) (then return)) - (local.set $payload_len (i32.sub (local.get $len) (i32.const 32))) - ;; If first payload byte is 0xDD, double it - (if (i32.eq (i32.load8_u (i32.add (local.get $ptr) (i32.const 32))) (i32.const 0xDD)) - (then - ;; Send twice - (call $send (local.get $ptr) (i32.add (local.get $ptr) (i32.const 32)) (local.get $payload_len)) - (call $send (local.get $ptr) (i32.add (local.get $ptr) (i32.const 32)) (local.get $payload_len)) - ) - (else - ;; Echo once - (call $send (local.get $ptr) (i32.add (local.get $ptr) (i32.const 32)) (local.get $payload_len)) - ) - ) - ) - )"#; - 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(); - - // Regular message — echo - rt.send_to(addr, framed_msg(inbox.addr(), &[0xAA, 0xBB])).unwrap(); - // Trigger double - rt.send_to(addr, framed_msg(inbox.addr(), &[0xDD, 0xEE])).unwrap(); - rt.tick(); - - let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(msgs.len(), 3, "1 echo + 2 double = 3"); -} - -// Long-running WASM actor: 200 messages across 200 ticks -#[test] -fn two_hundred_messages_across_two_hundred_ticks() { - 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(); - - for i in 0u8..200 { - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - rt.tick(); - let resp = inbox.try_recv().expect("each tick echoes"); - assert_eq!(resp.0, vec![i]); - } -} - -// Multiple inboxes from same runtime -#[test] -fn three_inboxes_from_same_runtime() { - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox1 = rt.new_inbox::().unwrap(); - let inbox2 = rt.new_inbox::().unwrap(); - let inbox3 = rt.new_inbox::().unwrap(); - - let actor = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - // Send to echo targeting different inboxes - rt.send_to(addr, framed_msg(inbox1.addr(), b"one")).unwrap(); - rt.tick(); - rt.send_to(addr, framed_msg(inbox2.addr(), b"two")).unwrap(); - rt.tick(); - rt.send_to(addr, framed_msg(inbox3.addr(), b"three")).unwrap(); - rt.tick(); - - assert_eq!(inbox1.try_recv().unwrap().0, b"one"); - assert_eq!(inbox2.try_recv().unwrap().0, b"two"); - assert_eq!(inbox3.try_recv().unwrap().0, b"three"); -} - -// Guest that uses i32.and to mask bytes -#[test] -fn guest_and_mask_low_nibble() { - 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) - (if (i32.lt_u (local.get $len) (i32.const 33)) (then return)) - ;; Mask each payload byte to low nibble - (local.set $i (i32.const 32)) - (block $exit - (loop $loop - (br_if $exit (i32.ge_u (local.get $i) (local.get $len))) - (i32.store8 - (i32.add (local.get $ptr) (local.get $i)) - (i32.and - (i32.load8_u (i32.add (local.get $ptr) (local.get $i))) - (i32.const 0x0F))) - (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(), &[0xAB, 0xCD, 0xEF])).unwrap(); - rt.tick(); - - let resp = inbox.try_recv().expect("masked response"); - assert_eq!(resp.0, vec![0x0B, 0x0D, 0x0F]); -} - -// ── Cycle 87 ───────────────────────────────────────────────────────────────── - -// Guest with i32.or to set high bits -#[test] -fn guest_or_set_high_nibble() { - 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) - (if (i32.lt_u (local.get $len) (i32.const 33)) (then return)) - ;; OR each payload byte with 0xF0 - (local.set $i (i32.const 32)) - (block $exit - (loop $loop - (br_if $exit (i32.ge_u (local.get $i) (local.get $len))) - (i32.store8 - (i32.add (local.get $ptr) (local.get $i)) - (i32.or - (i32.load8_u (i32.add (local.get $ptr) (local.get $i))) - (i32.const 0xF0))) - (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(), &[0x01, 0x02, 0x03])).unwrap(); - rt.tick(); - - let resp = inbox.try_recv().expect("ORed response"); - assert_eq!(resp.0, vec![0xF1, 0xF2, 0xF3]); -} - -// Guest handles exactly 31-byte message (1 byte less than address frame) -#[test] -fn echo_with_thirty_one_byte_message() { - 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(); - - // 31 bytes: not enough for a full address frame (32 bytes) — echo won't send - rt.send_to(addr, ByteMessage(vec![0xAA; 31])).unwrap(); - rt.tick(); - // Echo needs >= 32 bytes for dest address, so nothing should be sent - assert!(inbox.try_recv().is_none(), "31-byte message too short for echo framing"); -} - -// Guest with loop that counts to 1000 -#[test] -fn guest_loop_counts_to_thousand() { - let wat = r#"(module - (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.set $i (i32.const 0)) - (block $exit - (loop $loop - (br_if $exit (i32.ge_u (local.get $i) (i32.const 1000))) - (local.set $i (i32.add (local.get $i) (i32.const 1))) - (br $loop) - ) - ) - ;; Store final count at ptr - (i32.store (local.get $ptr) (local.get $i)) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![0u8; 4])).unwrap(); - rt.tick(); -} - -// 500 actors spawned, one message each, all process -#[test] -fn five_hundred_actors_one_message_each() { - let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - - for i in 0..500u16 { - 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.to_le_bytes())).unwrap(); - } - for _ in 0..20 { - rt.tick(); - } - - let count = std::iter::from_fn(|| inbox.try_recv()).count(); - assert_eq!(count, 500, "all 500 actors echoed"); -} - -// Property: echo payload is always byte-for-byte identical -proptest! { - #[test] - fn prop_echo_bitwise_identical( - payload in proptest::collection::vec(0u8..=255, 0..500) - ) { - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); + let echo = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).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(), &payload)).unwrap(); + let addr = rt.spawn(echo).unwrap(); + rt.send_to(addr, framed_msg(inbox.addr(), b"deterministic")).unwrap(); rt.tick(); - - let resp = inbox.try_recv().expect("echo response"); - assert_eq!(resp.0, payload); + results.push(inbox.try_recv().unwrap().0); } + assert!(results.windows(2).all(|w| w[0] == w[1]), "deterministic across runs"); } -// Guest sends response with modified last byte (add 1 to last byte) +// ═══════════════════════════════════════════════════════════════════════════════ +// Group 7: WASM feature coverage & guest computation +// ═══════════════════════════════════════════════════════════════════════════════ + #[test] -fn guest_modifies_last_byte() { +fn kitchen_sink_wat_module() { + // Single WAT module exercising as many supported WASM features as possible. + // If this builds and doesn't trap, the engine config is correct. 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 $payload_start i32) - (local $payload_len i32) - (local $last_idx i32) - (if (i32.lt_u (local.get $len) (i32.const 33)) (then return)) - (local.set $payload_start (i32.add (local.get $ptr) (i32.const 32))) - (local.set $payload_len (i32.sub (local.get $len) (i32.const 32))) - ;; Add 1 to last byte - (local.set $last_idx - (i32.add (local.get $payload_start) - (i32.sub (local.get $payload_len) (i32.const 1)))) - (i32.store8 (local.get $last_idx) - (i32.add (i32.load8_u (local.get $last_idx)) (i32.const 1))) - (call $send (local.get $ptr) (local.get $payload_start) (local.get $payload_len)) - ) - )"#; - 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(), &[0x10, 0x20, 0x30])).unwrap(); - rt.tick(); - - let resp = inbox.try_recv().expect("modified response"); - assert_eq!(resp.0, vec![0x10, 0x20, 0x31], "last byte incremented"); -} - -// ── Cycle 88 — 400 TEST MILESTONE ──────────────────────────────────────────── - -// Guest that swaps adjacent bytes (0↔1, 2↔3, etc.) -#[test] -fn guest_swaps_adjacent_bytes() { - 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) + (memory (export "memory") 2) + + ;; Data segments + (data (i32.const 0) "kitchen-sink") + + ;; Globals: mutable and immutable + (global $counter (mut i32) (i32.const 0)) + (global $MAGIC i32 (i32.const 42)) + + ;; Table for call_indirect + (table 2 funcref) + (elem (i32.const 0) $helper_add $helper_sub) + + ;; Internal helper functions + (func $helper_add (param i32 i32) (result i32) + (i32.add (local.get 0) (local.get 1))) + (func $helper_sub (param i32 i32) (result i32) + (i32.sub (local.get 0) (local.get 1))) + (func $deep_call (param $x i32) (result i32) + (i32.mul (local.get $x) (i32.const 2))) + + (func (export "alloc") (param $len i32) (result i32) i32.const 4096) (func (export "handle") (param $ptr i32) (param $len i32) + (local $a i32) + (local $b i64) + (local $c f32) + (local $d f64) (local $i i32) (local $tmp i32) - (local $payload_start i32) - (local $payload_len i32) - (if (i32.lt_u (local.get $len) (i32.const 34)) (then return)) - (local.set $payload_start (i32.add (local.get $ptr) (i32.const 32))) - (local.set $payload_len (i32.sub (local.get $len) (i32.const 32))) - ;; Swap pairs: byte[0]↔byte[1], byte[2]↔byte[3], etc. + + ;; Increment global counter + (global.set $counter (i32.add (global.get $counter) (i32.const 1))) + + ;; Read immutable global + (local.set $a (global.get $MAGIC)) + + ;; i32 arithmetic + (local.set $a (i32.add (local.get $a) (i32.const 10))) + (local.set $a (i32.sub (local.get $a) (i32.const 5))) + (local.set $a (i32.mul (local.get $a) (i32.const 3))) + (local.set $a (i32.div_u (local.get $a) (i32.const 2))) + (local.set $a (i32.rem_u (local.get $a) (i32.const 100))) + + ;; Bitwise operations + (local.set $a (i32.and (local.get $a) (i32.const 0xFF))) + (local.set $a (i32.or (local.get $a) (i32.const 0x10))) + (local.set $a (i32.xor (local.get $a) (i32.const 0x01))) + (local.set $a (i32.shl (local.get $a) (i32.const 2))) + (local.set $a (i32.shr_u (local.get $a) (i32.const 1))) + (local.set $a (i32.shr_s (local.get $a) (i32.const 1))) + (local.set $a (i32.rotl (local.get $a) (i32.const 3))) + (local.set $a (i32.rotr (local.get $a) (i32.const 3))) + + ;; Bit counting + (drop (i32.clz (local.get $a))) + (drop (i32.ctz (local.get $a))) + (drop (i32.popcnt (local.get $a))) + (drop (i32.eqz (local.get $a))) + + ;; Comparisons + (drop (i32.gt_s (local.get $a) (i32.const 0))) + (drop (i32.lt_s (local.get $a) (i32.const 100))) + (drop (i32.le_u (local.get $a) (i32.const 200))) + (drop (i32.ge_s (local.get $a) (i32.const -1))) + + ;; i64 operations + (local.set $b (i64.const 9999999999)) + (local.set $b (i64.add (local.get $b) (i64.const 1))) + (i64.store (i32.const 800) (local.get $b)) + (drop (i64.load (i32.const 800))) + + ;; i64 ↔ i32 conversions + (drop (i32.wrap_i64 (local.get $b))) + (drop (i64.extend_i32_s (local.get $a))) + + ;; f32 operations + (local.set $c (f32.const 3.14)) + (local.set $c (f32.add (local.get $c) (f32.const 1.0))) + (local.set $c (f32.mul (local.get $c) (f32.const 2.0))) + (f32.store (i32.const 900) (local.get $c)) + (drop (f32.load (i32.const 900))) + + ;; f64 operations + (local.set $d (f64.promote_f32 (local.get $c))) + (local.set $d (f64.mul (local.get $d) (f64.const 0.5))) + (f64.store (i32.const 920) (local.get $d)) + + ;; Sign extension + (drop (i32.extend8_s (i32.const 0x80))) + (drop (i32.extend16_s (i32.const 0x8000))) + + ;; Memory store/load variants + (i32.store8 (i32.const 700) (i32.const 0xAB)) + (i32.store16 (i32.const 702) (i32.const 0xCDEF)) + (drop (i32.load8_u (i32.const 700))) + (drop (i32.load16_u (i32.const 702))) + + ;; local.tee + (local.set $tmp (local.tee $a (i32.const 77))) + + ;; Nested function calls + (drop (call $deep_call (i32.const 5))) + (drop (call $helper_add (i32.const 10) (i32.const 20))) + + ;; call_indirect via table + (drop (call_indirect (type 0) (i32.const 7) (i32.const 3) (i32.const 0))) + (drop (call_indirect (type 0) (i32.const 7) (i32.const 3) (i32.const 1))) + + ;; Control flow: if/else + (if (i32.gt_s (local.get $len) (i32.const 0)) + (then nop) + (else nop) + ) + + ;; select + (drop (select (i32.const 10) (i32.const 20) (i32.const 1))) + + ;; block + br_if + (block $skip + (br_if $skip (i32.eqz (local.get $len))) + nop + ) + + ;; loop with counter (local.set $i (i32.const 0)) (block $exit (loop $loop - ;; Need at least 2 more bytes - (br_if $exit (i32.gt_u (i32.add (local.get $i) (i32.const 2)) (local.get $payload_len))) - (local.set $tmp - (i32.load8_u (i32.add (local.get $payload_start) (local.get $i)))) - (i32.store8 - (i32.add (local.get $payload_start) (local.get $i)) - (i32.load8_u (i32.add (local.get $payload_start) (i32.add (local.get $i) (i32.const 1))))) - (i32.store8 - (i32.add (local.get $payload_start) (i32.add (local.get $i) (i32.const 1))) - (local.get $tmp)) - (local.set $i (i32.add (local.get $i) (i32.const 2))) + (br_if $exit (i32.ge_u (local.get $i) (i32.const 5))) + (local.set $i (i32.add (local.get $i) (i32.const 1))) (br $loop) ) ) - (call $send (local.get $ptr) (local.get $payload_start) (local.get $payload_len)) + + ;; br_table dispatch + (block $b0 (block $b1 (block $b2 + (br_table $b0 $b1 $b2 (i32.const 1)) + ) nop) nop) ;; falls through + + ;; Bulk memory: fill and copy + (memory.fill (i32.const 600) (i32.const 0xAA) (i32.const 32)) + (memory.copy (i32.const 650) (i32.const 600) (i32.const 32)) + + ;; memory.size and memory.grow + (drop (memory.size)) + (drop (memory.grow (i32.const 1))) + + ;; nop + nop ) + + ;; Type for call_indirect + (type (func (param i32 i32) (result i32))) )"#; let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, wat::parse_str(wat).unwrap()) - .build().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(), &[0x01, 0x02, 0x03, 0x04])).unwrap(); + // Send message, tick — no trap + rt.send_to(addr, ByteMessage(vec![1, 2, 3, 4])).unwrap(); rt.tick(); - let resp = inbox.try_recv().expect("swapped response"); - assert_eq!(resp.0, vec![0x02, 0x01, 0x04, 0x03], "adjacent bytes swapped"); + // Send again — global counter increments, still no trap + rt.send_to(addr, ByteMessage(vec![5, 6, 7, 8])).unwrap(); + rt.tick(); } -// Different RuntimeConfig values all work with WASM actors #[test] -fn various_runtime_configs_work() { - let engine = SharedEngine::new().unwrap(); - - for (budget, capacity) in [(1, 10), (10, 100), (100, 1000), (64, 64)] { - let mut cfg = RuntimeConfig::default(); - cfg.actor_message_budget = budget; - cfg.default_mailbox_capacity = capacity; - let rt = Runtime::new(cfg); - let inbox = rt.new_inbox::().unwrap(); - 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(), b"cfg")).unwrap(); - rt.tick(); - assert_eq!(inbox.try_recv().unwrap().0, b"cfg"); - } -} - -// Guest that traps on first 3 messages then works on 4th -#[test] -fn guest_traps_three_times_then_succeeds() { +fn guest_mutable_state_persists() { + // Global counter increments on each handle call, sends count back let wat = r#"(module (import "swactor" "send" (func $send (param i32 i32 i32))) (memory (export "memory") 1) (global $count (mut i32) (i32.const 0)) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) + (func (export "alloc") (param $len i32) (result i32) i32.const 4096) (func (export "handle") (param $ptr i32) (param $len i32) + (if (i32.lt_u (local.get $len) (i32.const 32)) (then return)) (global.set $count (i32.add (global.get $count) (i32.const 1))) - ;; Trap on first 3 calls - (if (i32.le_u (global.get $count) (i32.const 3)) - (then unreachable) - ) - ;; 4th call onwards: echo - (if (i32.ge_u (local.get $len) (i32.const 33)) - (then - (call $send (local.get $ptr) - (i32.add (local.get $ptr) (i32.const 32)) - (i32.sub (local.get $len) (i32.const 32))) - ) - ) + (i32.store (i32.const 200) (global.get $count)) + (call $send (local.get $ptr) (i32.const 200) (i32.const 4)) ) )"#; let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, wat::parse_str(wat).unwrap()) - .build().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(); - // First 3 trap - for _ in 0..3 { - rt.send_to(addr, framed_msg(inbox.addr(), b"trap")).unwrap(); + for expected in 1..=5u32 { + rt.send_to(addr, framed_msg(inbox.addr(), b"x")).unwrap(); rt.tick(); - assert!(inbox.try_recv().is_none()); + let resp = inbox.try_recv().expect("should get count back"); + let count = u32::from_le_bytes([resp.0[0], resp.0[1], resp.0[2], resp.0[3]]); + assert_eq!(count, expected, "counter should persist across messages"); } - // 4th succeeds - rt.send_to(addr, framed_msg(inbox.addr(), b"ok")).unwrap(); - rt.tick(); - assert_eq!(inbox.try_recv().unwrap().0, b"ok"); } -// 10 actors with different guest types — verify per-actor type behavior #[test] -fn ten_actors_three_types_verified() { +fn guest_transforms_payload() { let engine = SharedEngine::new().unwrap(); let rt = Runtime::new(RuntimeConfig::default()); let inbox = rt.new_inbox::().unwrap(); - // 4 echo + 3 double + 3 silent - let mut echo_addrs = Vec::new(); - let mut double_addrs = Vec::new(); - let mut silent_addrs = Vec::new(); - - for _ in 0..4 { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - echo_addrs.push(rt.spawn(actor).unwrap()); - } - for _ in 0..3 { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("double")).build().unwrap(); - double_addrs.push(rt.spawn(actor).unwrap()); - } - for _ in 0..3 { - let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("silent")).build().unwrap(); - silent_addrs.push(rt.spawn(actor).unwrap()); - } - - for addr in &echo_addrs { - rt.send_to(*addr, framed_msg(inbox.addr(), b"E")).unwrap(); - } - for addr in &double_addrs { - rt.send_to(*addr, framed_msg(inbox.addr(), b"D")).unwrap(); - } - for addr in &silent_addrs { - rt.send_to(*addr, ByteMessage(b"S".to_vec())).unwrap(); - } - rt.tick(); - - let count = std::iter::from_fn(|| inbox.try_recv()).count(); - // 4 echo + 3*2 double + 0 silent = 10 - assert_eq!(count, 10); -} - -// Send 50-byte, 100-byte, 500-byte, 1000-byte, 5000-byte payloads -#[test] -fn echo_various_payload_sizes() { - 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(); - - for size in [50, 100, 500, 1000, 5000] { - let payload = vec![0xBB; size]; - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - } - rt.tick(); - - let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(msgs.len(), 5); - assert_eq!(msgs[0].len(), 50); - assert_eq!(msgs[1].len(), 100); - assert_eq!(msgs[2].len(), 500); - assert_eq!(msgs[3].len(), 1000); - assert_eq!(msgs[4].len(), 5000); -} - -// WasmActorBuilder consumes self — can't build twice (compile-time check via move) -// This is a compile-time property; we just verify the API works with one build -#[test] -fn builder_consumes_self_on_build() { - let engine = SharedEngine::new().unwrap(); - let builder = WasmActorBuilder::new(engine, guest_wasm("echo")); - let _actor = builder.build().unwrap(); - // builder is moved, can't call build() again — verified by ownership -} - -// Property: any valid WAT module either builds successfully or returns clean error -proptest! { - #[test] - fn prop_builder_never_panics_on_valid_wat( - pages in 1u32..10, - alloc_return in -1i32..70000, - ) { - let wat = format!(r#"(module - (memory (export "memory") {pages}) - (func (export "alloc") (param $len i32) (result i32) i32.const {alloc_return}) - (func (export "handle") (param $ptr i32) (param $len i32)) - )"#); - let engine = SharedEngine::new().unwrap(); - let bytes = wat::parse_str(&wat).unwrap(); - // Should always succeed (valid module structure) - let actor = WasmActorBuilder::new(engine, bytes).build().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let addr = rt.spawn(actor).unwrap(); - let _ = rt.send_to(addr, ByteMessage(vec![0u8; 4])); - rt.tick(); - } -} - -// Spawn, send burst, tick burst, stop — no panics -#[test] -fn burst_send_and_tick_pattern() { - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("silent")).build().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let addr = rt.spawn(actor).unwrap(); - - // Burst of 50 sends - for _ in 0..50 { - let _ = rt.send_to(addr, ByteMessage(vec![0; 100])); - } - // Burst of 50 ticks - for _ in 0..50 { - rt.tick(); - } - rt.stop_actor(addr); - rt.tick(); -} - -// Guest with nested call chains: f1 calls f2 calls f3 -#[test] -fn guest_three_level_call_chain() { - let wat = r#"(module - (memory (export "memory") 1) - (func $f3 (param i32) (result i32) (i32.add (local.get 0) (i32.const 1))) - (func $f2 (param i32) (result i32) (call $f3 (i32.mul (local.get 0) (i32.const 2)))) - (func $f1 (param i32) (result i32) (call $f2 (i32.add (local.get 0) (i32.const 10)))) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - ;; f1(5) = f2(15) = f3(30) = 31 - (i32.store (local.get $ptr) (call $f1 (i32.const 5))) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![0u8; 4])).unwrap(); - rt.tick(); -} - -// Two WASM actors started from different threads (build safety) -#[test] -fn build_actors_on_separate_threads() { - let engine = SharedEngine::new().unwrap(); - let e1 = engine.clone(); - let e2 = engine; - - let (a1, a2) = std::thread::scope(|s| { - let h1 = s.spawn(move || { - WasmActorBuilder::new(e1, guest_wasm("echo")).build().unwrap() - }); - let h2 = s.spawn(move || { - WasmActorBuilder::new(e2, guest_wasm("double")).build().unwrap() - }); - (h1.join().unwrap(), h2.join().unwrap()) - }); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let addr1 = rt.spawn(a1).unwrap(); - let addr2 = rt.spawn(a2).unwrap(); - rt.send_to(addr1, framed_msg(inbox.addr(), b"t1")).unwrap(); - rt.send_to(addr2, framed_msg(inbox.addr(), b"t2")).unwrap(); - rt.tick(); - - let count = std::iter::from_fn(|| inbox.try_recv()).count(); - assert_eq!(count, 3, "1 echo + 2 double = 3"); -} - -// Send incrementing bytes — verify FIFO ordering in echo responses -#[test] -fn echo_preserves_fifo_with_incrementing_bytes() { - 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(); - - for i in 0u8..20 { - rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); - } - rt.tick(); - - let msgs: Vec = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0[0])).collect(); - assert_eq!(msgs.len(), 20); - for (i, &b) in msgs.iter().enumerate() { - assert_eq!(b, i as u8, "FIFO order preserved"); - } -} - -// ── Cycle 89 ───────────────────────────────────────────────────────────────── - -// Guest computes running average (integer) over received bytes -#[test] -fn guest_running_sum_across_messages() { - let wat = r#"(module + // XOR each byte with 0xFF (bitwise NOT) + let xor_wat = r#"(module (import "swactor" "send" (func $send (param i32 i32 i32))) (memory (export "memory") 1) - (global $sum (mut i32) (i32.const 0)) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) + (func (export "alloc") (param $len i32) (result i32) i32.const 4096) (func (export "handle") (param $ptr i32) (param $len i32) (local $i i32) - ;; Add all bytes to running sum - (local.set $i (i32.const 0)) - (block $exit - (loop $loop - (br_if $exit (i32.ge_u (local.get $i) (local.get $len))) - (global.set $sum (i32.add (global.get $sum) - (i32.load8_u (i32.add (local.get $ptr) (local.get $i))))) - (local.set $i (i32.add (local.get $i) (i32.const 1))) - (br $loop) - ) - ) - ) - )"#; - 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(); - - // Send 3 messages with known byte sums - rt.send_to(addr, ByteMessage(vec![1, 2, 3])).unwrap(); // sum += 6 - rt.send_to(addr, ByteMessage(vec![10, 20])).unwrap(); // sum += 30 - rt.send_to(addr, ByteMessage(vec![100])).unwrap(); // sum += 100 - rt.tick(); // total sum = 136 -} - -// Build same actor type 10 times in parallel threads -#[test] -fn parallel_build_ten_actors() { - let engine = SharedEngine::new().unwrap(); - let actors: Vec = std::thread::scope(|s| { - let handles: Vec<_> = (0..10) - .map(|_| { - let e = engine.clone(); - s.spawn(move || { - WasmActorBuilder::new(e, guest_wasm("echo")).build().unwrap() - }) - }) - .collect(); - handles.into_iter().map(|h| h.join().unwrap()).collect() - }); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - for actor in actors { - let addr = rt.spawn(actor).unwrap(); - rt.send_to(addr, framed_msg(inbox.addr(), b"parallel")).unwrap(); - } - rt.tick(); - - let count = std::iter::from_fn(|| inbox.try_recv()).count(); - assert_eq!(count, 10); -} - -// Guest with multiple memory.fill operations at different offsets -#[test] -fn guest_multiple_memory_fills() { - let wat = r#"(module - (memory (export "memory") 1) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - (memory.fill (i32.const 500) (i32.const 0xAA) (i32.const 100)) - (memory.fill (i32.const 700) (i32.const 0xBB) (i32.const 100)) - (memory.fill (i32.const 900) (i32.const 0xCC) (i32.const 100)) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![0])).unwrap(); - rt.tick(); -} - -// Send 10000 bytes payload to echo — large transfer -#[test] -fn echo_ten_kb_payload() { - 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..10000).map(|i| ((i * 7 + 13) % 256) as u8).collect(); - rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); - rt.tick(); - - let resp = inbox.try_recv().expect("10KB echo"); - assert_eq!(resp.0, payload); -} - -// Guest that returns negative alloc on second call — first message works, second dropped -#[test] -fn alloc_works_then_returns_negative() { - let wat = r#"(module - (import "swactor" "send" (func $send (param i32 i32 i32))) - (memory (export "memory") 1) - (global $call (mut i32) (i32.const 0)) - (func (export "alloc") (param $len i32) (result i32) - (if (result i32) (i32.eqz (global.get $call)) - (then - (global.set $call (i32.const 1)) - (i32.const 1024) - ) - (else (i32.const -1)) - ) - ) - (func (export "handle") (param $ptr i32) (param $len i32) - (if (i32.ge_u (local.get $len) (i32.const 33)) - (then (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(); - - // First: works - rt.send_to(addr, framed_msg(inbox.addr(), b"ok")).unwrap(); - rt.tick(); - assert_eq!(inbox.try_recv().unwrap().0, b"ok"); - - // Second: alloc returns -1, dropped - rt.send_to(addr, framed_msg(inbox.addr(), b"drop")).unwrap(); - rt.tick(); - assert!(inbox.try_recv().is_none()); -} - -// ── Cycle 90 ───────────────────────────────────────────────────────────────── - -// Mixed payload types: binary + text + numbers — all echo correctly -#[test] -fn echo_mixed_payload_types() { - 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 payloads: Vec> = vec![ - b"hello world".to_vec(), - vec![0x00, 0xFF, 0x80], - (0..100).collect(), - vec![0u8; 1], - b"\x00\x01\x02\x03\x04".to_vec(), - ]; - - for p in &payloads { - rt.send_to(addr, framed_msg(inbox.addr(), p)).unwrap(); - } - rt.tick(); - - let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(msgs.len(), payloads.len()); - for (got, expected) in msgs.iter().zip(payloads.iter()) { - assert_eq!(got, expected); - } -} - -// Guest with global initialized to i64 -#[test] -fn guest_i64_global() { - let wat = r#"(module - (memory (export "memory") 1) - (global $g (mut i64) (i64.const 9999999999)) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - (func (export "handle") (param $ptr i32) (param $len i32) - (global.set $g (i64.add (global.get $g) (i64.const 1))) - (i64.store (local.get $ptr) (global.get $g)) - ) - )"#; - 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(); - rt.send_to(addr, ByteMessage(vec![0u8; 8])).unwrap(); - rt.tick(); -} - -// Spawn echo, double, and silent — each processes its type-specific behavior -#[test] -fn all_three_guest_types_in_one_runtime() { - let engine = SharedEngine::new().unwrap(); - let echo = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - let double = WasmActorBuilder::new(engine.clone(), guest_wasm("double")).build().unwrap(); - let silent = WasmActorBuilder::new(engine, guest_wasm("silent")).build().unwrap(); - - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let e = rt.spawn(echo).unwrap(); - let d = rt.spawn(double).unwrap(); - let s = rt.spawn(silent).unwrap(); - - rt.send_to(e, framed_msg(inbox.addr(), b"e")).unwrap(); - rt.send_to(d, framed_msg(inbox.addr(), b"d")).unwrap(); - rt.send_to(s, ByteMessage(b"s".to_vec())).unwrap(); - rt.tick(); - - let msgs: Vec> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); - assert_eq!(msgs.len(), 3, "echo(1) + double(2) + silent(0) = 3"); -} - -// Property: stopping an actor always makes it unresponsive -proptest! { - #[test] - fn prop_stopped_actor_never_responds( - n_before in 0usize..10, - n_after in 1usize..10, - ) { - 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(); - - for _ in 0..n_before { - let _ = rt.send_to(addr, framed_msg(inbox.addr(), b"x")); - } - rt.tick(); - // Drain inbox - while inbox.try_recv().is_some() {} - - rt.stop_actor(addr); - rt.tick(); - - for _ in 0..n_after { - let _ = rt.send_to(addr, framed_msg(inbox.addr(), b"y")); - } - rt.tick(); - rt.tick(); - - assert!(inbox.try_recv().is_none(), "stopped actor should never respond"); - } -} - -// Echo with exactly 33 bytes (32 addr + 1 payload byte) -#[test] -fn echo_exactly_thirty_three_bytes() { - 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(); - - rt.send_to(addr, framed_msg(inbox.addr(), &[0x42])).unwrap(); - rt.tick(); - - 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) - ) - ) + (block $exit (loop $loop + (br_if $exit (i32.ge_u (local.get $i) (local.get $len))) + (i32.store8 + (i32.add (local.get $ptr) (local.get $i)) + (i32.xor (i32.load8_u (i32.add (local.get $ptr) (local.get $i))) (i32.const 0xFF))) + (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(); + let xor = WasmActorBuilder::new(engine.clone(), wat::parse_str(xor_wat).unwrap()).build().unwrap(); + let xaddr = rt.spawn(xor).unwrap(); + rt.send_to(xaddr, framed_msg(inbox.addr(), &[0x00, 0xFF, 0xAA])).unwrap(); rt.tick(); + assert_eq!(inbox.try_recv().unwrap().0, vec![0xFF, 0x00, 0x55]); - 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) + // Reverse payload + let rev_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 4096) + (func (export "handle") (param $ptr i32) (param $len i32) + (local $i i32) (local $plen i32) (local $pstart i32) + (if (i32.lt_u (local.get $len) (i32.const 33)) (then return)) + (local.set $pstart (i32.add (local.get $ptr) (i32.const 32))) + (local.set $plen (i32.sub (local.get $len) (i32.const 32))) + (local.set $i (i32.const 0)) + (block $exit (loop $loop + (br_if $exit (i32.ge_u (local.get $i) (local.get $plen))) + (i32.store8 + (i32.add (i32.const 900) (local.get $i)) + (i32.load8_u (i32.add (local.get $pstart) + (i32.sub (i32.sub (local.get $plen) (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 900) (local.get $plen)) ) - (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(); - } + let rev = WasmActorBuilder::new(engine, wat::parse_str(rev_wat).unwrap()).build().unwrap(); + let raddr = rt.spawn(rev).unwrap(); + rt.send_to(raddr, framed_msg(inbox.addr(), b"abcde")).unwrap(); rt.tick(); + assert_eq!(inbox.try_recv().unwrap().0, b"edcba"); } -// 100KB payload too large for 1-page echo guest — message dropped gracefully #[test] -fn hundred_kb_payload_dropped_for_one_page_guest() { +fn guest_multi_send_patterns() { 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"); -} - -// ── Cycle 92 ───────────────────────────────────────────────────────────────── - -// Guest that counts bytes equal to a threshold -#[test] -fn guest_counts_bytes_equal_to_threshold() { - let wat = r#"(module + // Guest sends to 2 different destinations in one handle call + let two_dest_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 $count i32) - (if (i32.lt_u (local.get $len) (i32.const 34)) (then return)) - ;; Threshold is first byte after address (offset 32) - ;; Count occurrences in rest of payload - (local.set $i (i32.const 33)) - (local.set $count (i32.const 0)) - (block $exit - (loop $loop - (br_if $exit (i32.ge_u (local.get $i) (local.get $len))) - (if (i32.eq - (i32.load8_u (i32.add (local.get $ptr) (local.get $i))) - (i32.load8_u (i32.add (local.get $ptr) (i32.const 32)))) - (then (local.set $count (i32.add (local.get $count) (i32.const 1))))) - (local.set $i (i32.add (local.get $i) (i32.const 1))) - (br $loop) + ;; Layout: [addr1:32][addr2:32][payload:rest] + (if (i32.ge_u (local.get $len) (i32.const 65)) + (then + (call $send (local.get $ptr) + (i32.add (local.get $ptr) (i32.const 64)) + (i32.sub (local.get $len) (i32.const 64))) + (call $send (i32.add (local.get $ptr) (i32.const 32)) + (i32.add (local.get $ptr) (i32.const 64)) + (i32.sub (local.get $len) (i32.const 64))) ) ) - ;; Send count as 4-byte LE at scratch 900 - (i32.store (i32.const 900) (local.get $count)) - (call $send (local.get $ptr) (i32.const 900) (i32.const 4)) ) )"#; - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, wat::parse_str(wat).unwrap()) + let actor = WasmActorBuilder::new(engine.clone(), wat::parse_str(two_dest_wat).unwrap()) .build().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); + let inbox2 = rt.new_inbox::().unwrap(); let addr = rt.spawn(actor).unwrap(); - // Threshold = 0x42 (first byte), data to search = [0x00, 0x42, 0x42, 0xFF] - // Expected count = 2 (threshold byte itself not counted, search starts at offset 33) - rt.send_to(addr, framed_msg(inbox.addr(), &[0x42, 0x00, 0x42, 0x42, 0xFF])).unwrap(); + let mut msg = Vec::new(); + msg.extend_from_slice(&inbox.addr().0); // addr1 + msg.extend_from_slice(&inbox2.addr().0); // addr2 + msg.extend_from_slice(b"shared"); // payload + rt.send_to(addr, ByteMessage(msg)).unwrap(); rt.tick(); + assert_eq!(inbox.try_recv().unwrap().0, b"shared"); + assert_eq!(inbox2.try_recv().unwrap().0, b"shared"); - let resp = inbox.try_recv().expect("count response"); - let count = u32::from_le_bytes([resp.0[0], resp.0[1], resp.0[2], resp.0[3]]); - assert_eq!(count, 2); + // Guest sends 100 messages in one handle call + let many_sends_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) + (if (i32.lt_u (local.get $len) (i32.const 32)) (then return)) + (local.set $i (i32.const 0)) + (block $exit (loop $loop + (br_if $exit (i32.ge_u (local.get $i) (i32.const 100))) + (i32.store8 (i32.const 900) (local.get $i)) + (call $send (local.get $ptr) (i32.const 900) (i32.const 1)) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br $loop) + )) + ) + )"#; + let actor2 = WasmActorBuilder::new(engine, wat::parse_str(many_sends_wat).unwrap()) + .build().unwrap(); + let addr2 = rt.spawn(actor2).unwrap(); + rt.send_to(addr2, framed_msg(inbox.addr(), b"go")).unwrap(); + rt.tick(); + let count: usize = std::iter::from_fn(|| inbox.try_recv()).count(); + assert_eq!(count, 100, "all 100 sends should be delivered"); } -// Spawn echo, stop it, spawn another echo at (likely) same slot +// ═══════════════════════════════════════════════════════════════════════════════ +// Group 8: Stress +// ═══════════════════════════════════════════════════════════════════════════════ + #[test] -fn reuse_slot_after_stop() { +fn echo_sustained_load() { let engine = SharedEngine::new().unwrap(); let rt = Runtime::new(RuntimeConfig::default()); let inbox = rt.new_inbox::().unwrap(); - let a1 = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); - let addr1 = rt.spawn(a1).unwrap(); - rt.send_to(addr1, framed_msg(inbox.addr(), b"first")).unwrap(); - rt.tick(); - assert_eq!(inbox.try_recv().unwrap().0, b"first"); - rt.stop_actor(addr1); - rt.tick(); - - let a2 = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); - let addr2 = rt.spawn(a2).unwrap(); - rt.send_to(addr2, framed_msg(inbox.addr(), b"second")).unwrap(); - rt.tick(); - assert_eq!(inbox.try_recv().unwrap().0, b"second"); -} - -// Guest module with only memory and alloc — missing handle export -#[test] -fn module_missing_handle_export() { - let wat = r#"(module - (memory (export "memory") 1) - (func (export "alloc") (param $len i32) (result i32) i32.const 1024) - )"#; - let engine = SharedEngine::new().unwrap(); - let result = WasmActorBuilder::new(engine, wat::parse_str(wat).unwrap()).build(); - assert!(result.is_err()); - let err = result.err().unwrap(); - let msg = format!("{err}"); - assert!(msg.contains("handle"), "error should mention handle: {msg}"); -} - -// Property: silent actor never produces any inbox messages -proptest! { - #[test] - fn prop_silent_never_sends( - n_msgs in 1usize..20, - ticks in 1usize..10, - ) { - let engine = SharedEngine::new().unwrap(); - let actor = WasmActorBuilder::new(engine, guest_wasm("silent")).build().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); - let inbox = rt.new_inbox::().unwrap(); - let addr = rt.spawn(actor).unwrap(); - - for i in 0..n_msgs { - rt.send_to(addr, framed_msg(inbox.addr(), &[i as u8])).unwrap(); - } - for _ in 0..ticks { - rt.tick(); - } - - assert!(inbox.try_recv().is_none(), "silent actor should never send to inbox"); + // 500 messages to echo actor + let echo = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); + let addr = rt.spawn(echo).unwrap(); + for i in 0..500u16 { + rt.send_to(addr, framed_msg(inbox.addr(), &i.to_le_bytes())).unwrap(); } -} + for _ in 0..20 { rt.tick(); } + let count: usize = std::iter::from_fn(|| inbox.try_recv()).count(); + assert_eq!(count, 500, "all 500 messages echoed"); -// Multiple ticks without any actors — no panics -#[test] -fn many_ticks_on_empty_runtime() { - let rt = Runtime::new(RuntimeConfig::default()); - for _ in 0..1000 { + // Spawn and stop 100 actors — no panic, no leak + for _ in 0..100 { + let a = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")).build().unwrap(); + let a_addr = rt.spawn(a).unwrap(); + rt.send_to(a_addr, framed_msg(inbox.addr(), b"x")).unwrap(); + rt.tick(); + let _ = rt.stop_actor(a_addr); rt.tick(); } -} \ No newline at end of file + // Drain inbox + while inbox.try_recv().is_some() {} +}