//! Integration tests for EngineBridge command/event routing use libmarathon::engine::{EngineBridge, EngineCommand, EngineCore, EngineEvent}; use libmarathon::networking::SessionId; use std::time::Duration; use tokio::time::timeout; /// Get appropriate timeout for engine operations /// - With fast_tests: short timeout (networking is mocked) /// - Without fast_tests: long timeout (real networking with DHT discovery) fn engine_timeout() -> Duration { #[cfg(feature = "fast_tests")] { Duration::from_millis(200) } #[cfg(not(feature = "fast_tests"))] { Duration::from_secs(30) } } /// Get appropriate wait time for command processing fn processing_delay() -> Duration { #[cfg(feature = "fast_tests")] { Duration::from_millis(50) } #[cfg(not(feature = "fast_tests"))] { // Real networking needs more time for initialization Duration::from_secs(20) } } /// Test that commands sent from "Bevy side" reach the engine #[tokio::test] async fn test_command_routing() { let (bridge, handle) = EngineBridge::new(); // Spawn engine in background let engine_handle = tokio::spawn(async move { // Run engine for a short time let core = EngineCore::new(handle, ":memory:"); timeout(engine_timeout(), core.run()) .await .ok(); }); // Give engine time to start tokio::time::sleep(Duration::from_millis(10)).await; // Send a command from "Bevy side" let session_id = SessionId::new(); bridge.send_command(EngineCommand::StartNetworking { session_id: session_id.clone(), }); // Give engine time to process tokio::time::sleep(processing_delay()).await; // Poll events let events = bridge.poll_events(); // Verify we got a NetworkingStarted event assert!(!events.is_empty(), "Should receive at least one event"); let has_networking_started = events.iter().any(|e| { matches!( e, EngineEvent::NetworkingStarted { session_id: sid, .. } if sid == &session_id ) }); assert!( has_networking_started, "Should receive NetworkingStarted event" ); // Cleanup drop(bridge); let _ = engine_handle.await; } /// Test that events from engine reach "Bevy side" #[tokio::test] async fn test_event_routing() { let (bridge, handle) = EngineBridge::new(); // Spawn engine let engine_handle = tokio::spawn(async move { let core = EngineCore::new(handle, ":memory:"); timeout(engine_timeout(), core.run()) .await .ok(); }); tokio::time::sleep(Duration::from_millis(10)).await; // Send StartNetworking command let session_id = SessionId::new(); bridge.send_command(EngineCommand::StartNetworking { session_id: session_id.clone(), }); tokio::time::sleep(processing_delay()).await; // Poll events multiple times to verify queue works let events1 = bridge.poll_events(); let events2 = bridge.poll_events(); assert!(!events1.is_empty(), "First poll should return events"); assert!( events2.is_empty(), "Second poll should be empty (events already drained)" ); // Cleanup drop(bridge); let _ = engine_handle.await; } /// Test full lifecycle: Start → Stop networking #[tokio::test] async fn test_networking_lifecycle() { let (bridge, handle) = EngineBridge::new(); let engine_handle = tokio::spawn(async move { let core = EngineCore::new(handle, ":memory:"); timeout(engine_timeout(), core.run()) .await .ok(); }); tokio::time::sleep(Duration::from_millis(10)).await; // Start networking let session_id = SessionId::new(); bridge.send_command(EngineCommand::StartNetworking { session_id: session_id.clone(), }); tokio::time::sleep(processing_delay()).await; let events = bridge.poll_events(); assert!( events .iter() .any(|e| matches!(e, EngineEvent::NetworkingStarted { .. })), "Should receive NetworkingStarted" ); // Stop networking bridge.send_command(EngineCommand::StopNetworking); tokio::time::sleep(processing_delay()).await; let events = bridge.poll_events(); assert!( events .iter() .any(|e| matches!(e, EngineEvent::NetworkingStopped)), "Should receive NetworkingStopped" ); // Cleanup drop(bridge); let _ = engine_handle.await; } /// Test JoinSession command routing #[tokio::test] async fn test_join_session_routing() { let (bridge, handle) = EngineBridge::new(); let engine_handle = tokio::spawn(async move { let core = EngineCore::new(handle, ":memory:"); timeout(engine_timeout(), core.run()) .await .ok(); }); tokio::time::sleep(Duration::from_millis(10)).await; // Join a new session (should start networking) let session_id = SessionId::new(); bridge.send_command(EngineCommand::JoinSession { session_id: session_id.clone(), }); tokio::time::sleep(processing_delay()).await; let events = bridge.poll_events(); assert!( events.iter().any(|e| { matches!( e, EngineEvent::NetworkingStarted { session_id: sid, .. } if sid == &session_id ) }), "JoinSession should start networking" ); // Cleanup drop(bridge); let _ = engine_handle.await; } /// Test that multiple commands are processed in order #[tokio::test] async fn test_command_ordering() { let (bridge, handle) = EngineBridge::new(); let engine_handle = tokio::spawn(async move { let core = EngineCore::new(handle, ":memory:"); timeout(engine_timeout(), core.run()) .await .ok(); }); tokio::time::sleep(Duration::from_millis(10)).await; // Send first command and wait for it to complete let session1 = SessionId::new(); bridge.send_command(EngineCommand::StartNetworking { session_id: session1.clone(), }); // Wait for first networking to start tokio::time::sleep(processing_delay()).await; let events1 = bridge.poll_events(); assert!( events1.iter().any(|e| matches!(e, EngineEvent::NetworkingStarted { .. })), "Should receive first NetworkingStarted" ); // Now send stop and start second session let session2 = SessionId::new(); bridge.send_command(EngineCommand::StopNetworking); bridge.send_command(EngineCommand::JoinSession { session_id: session2.clone(), }); // Wait for second networking to start tokio::time::sleep(processing_delay()).await; let events2 = bridge.poll_events(); // Should see: NetworkingStopped, NetworkingStarted(session2) let started_events: Vec<_> = events2 .iter() .filter(|e| matches!(e, EngineEvent::NetworkingStarted { .. })) .collect(); let stopped_events: Vec<_> = events2 .iter() .filter(|e| matches!(e, EngineEvent::NetworkingStopped)) .collect(); assert_eq!(started_events.len(), 1, "Should have 1 NetworkingStarted event in second batch"); assert_eq!(stopped_events.len(), 1, "Should have 1 NetworkingStopped event"); // Cleanup drop(bridge); let _ = engine_handle.await; } /// Test: Shutdown command causes EngineCore to exit gracefully #[tokio::test] async fn test_shutdown_command() { let (bridge, handle) = EngineBridge::new(); let engine_handle = tokio::spawn(async move { let core = EngineCore::new(handle, ":memory:"); core.run().await; }); tokio::time::sleep(Duration::from_millis(10)).await; // Send Shutdown command bridge.send_command(EngineCommand::Shutdown); // Wait for engine to exit (should be quick since it's just processing the command) let result = timeout(Duration::from_millis(100), engine_handle).await; assert!( result.is_ok(), "Engine should exit within 100ms after receiving Shutdown command" ); // Verify that the engine actually exited (not errored) assert!( result.unwrap().is_ok(), "Engine should exit cleanly without panic" ); }