Files
marathon/crates/libmarathon/tests/bridge_integration.rs
2025-12-28 17:39:27 +00:00

303 lines
8.3 KiB
Rust

//! 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"
);
}