2026-01-05 19:41:38 +00:00
|
|
|
//! Shared test utilities for integration tests
|
|
|
|
|
//!
|
|
|
|
|
//! This module provides common test infrastructure that all integration tests use:
|
|
|
|
|
//! - Real iroh-gossip setup with localhost connections
|
|
|
|
|
//! - Test app creation with networking + persistence
|
|
|
|
|
//! - Wait helpers for async sync verification
|
|
|
|
|
|
|
|
|
|
pub mod gossip;
|
|
|
|
|
|
|
|
|
|
pub use gossip::{init_gossip_node, setup_gossip_pair, setup_gossip_trio, spawn_gossip_bridge_tasks};
|
|
|
|
|
|
|
|
|
|
use anyhow::Result;
|
|
|
|
|
use bevy::{
|
|
|
|
|
MinimalPlugins,
|
|
|
|
|
app::{App, ScheduleRunnerPlugin},
|
|
|
|
|
prelude::*,
|
|
|
|
|
};
|
|
|
|
|
use libmarathon::{
|
|
|
|
|
networking::{
|
|
|
|
|
GossipBridge,
|
|
|
|
|
NetworkingConfig,
|
|
|
|
|
NetworkingPlugin,
|
|
|
|
|
},
|
|
|
|
|
persistence::{
|
|
|
|
|
PersistenceConfig,
|
|
|
|
|
PersistencePlugin,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
use std::{
|
|
|
|
|
path::PathBuf,
|
|
|
|
|
time::Duration,
|
|
|
|
|
};
|
|
|
|
|
use tempfile::TempDir;
|
|
|
|
|
use tokio::time::Instant;
|
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
|
|
/// Test context that manages temporary directories with RAII cleanup
|
|
|
|
|
pub struct TestContext {
|
|
|
|
|
temp_dir: TempDir,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TestContext {
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
temp_dir: TempDir::new().expect("Failed to create temp directory"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn db_path(&self) -> PathBuf {
|
|
|
|
|
self.temp_dir.path().join("test.db")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create a test app with networking and persistence
|
|
|
|
|
pub fn create_test_app(node_id: Uuid, db_path: PathBuf, bridge: GossipBridge) -> App {
|
|
|
|
|
create_test_app_maybe_offline(node_id, db_path, Some(bridge))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create a test app with optional bridge (for testing offline scenarios)
|
|
|
|
|
pub fn create_test_app_maybe_offline(node_id: Uuid, db_path: PathBuf, bridge: Option<GossipBridge>) -> App {
|
|
|
|
|
let mut app = App::new();
|
|
|
|
|
|
|
|
|
|
app.add_plugins(MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(
|
|
|
|
|
Duration::from_secs_f64(1.0 / 60.0),
|
|
|
|
|
)))
|
|
|
|
|
.add_plugins(NetworkingPlugin::new(NetworkingConfig {
|
|
|
|
|
node_id,
|
|
|
|
|
sync_interval_secs: 0.5,
|
|
|
|
|
prune_interval_secs: 10.0,
|
|
|
|
|
tombstone_gc_interval_secs: 30.0,
|
|
|
|
|
}))
|
|
|
|
|
.add_plugins(PersistencePlugin::with_config(
|
|
|
|
|
db_path,
|
|
|
|
|
PersistenceConfig {
|
|
|
|
|
flush_interval_secs: 1,
|
|
|
|
|
checkpoint_interval_secs: 5,
|
|
|
|
|
battery_adaptive: false,
|
|
|
|
|
..Default::default()
|
|
|
|
|
},
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
// Insert bridge if provided (online mode)
|
|
|
|
|
if let Some(bridge) = bridge {
|
|
|
|
|
app.insert_resource(bridge);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
app
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 20:10:51 +00:00
|
|
|
/// Helper to ensure FixedUpdate and FixedPostUpdate run (since they're on a fixed timestep)
|
|
|
|
|
fn update_with_fixed(app: &mut App) {
|
|
|
|
|
use bevy::prelude::{FixedUpdate, FixedPostUpdate};
|
|
|
|
|
// Run Main schedule (which includes Update)
|
|
|
|
|
app.update();
|
|
|
|
|
// Explicitly run FixedUpdate to ensure systems there execute
|
|
|
|
|
app.world_mut().run_schedule(FixedUpdate);
|
|
|
|
|
// Explicitly run FixedPostUpdate to ensure delta generation executes
|
|
|
|
|
app.world_mut().run_schedule(FixedPostUpdate);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-05 19:41:38 +00:00
|
|
|
/// Wait for sync condition to be met, polling both apps
|
|
|
|
|
pub async fn wait_for_sync<F>(
|
|
|
|
|
app1: &mut App,
|
|
|
|
|
app2: &mut App,
|
|
|
|
|
timeout: Duration,
|
|
|
|
|
check_fn: F,
|
|
|
|
|
) -> Result<()>
|
|
|
|
|
where
|
|
|
|
|
F: Fn(&mut World, &mut World) -> bool,
|
|
|
|
|
{
|
|
|
|
|
let start = Instant::now();
|
|
|
|
|
let mut tick_count = 0;
|
|
|
|
|
|
|
|
|
|
while start.elapsed() < timeout {
|
|
|
|
|
// Tick both apps
|
2026-02-06 20:10:51 +00:00
|
|
|
update_with_fixed(app1);
|
|
|
|
|
update_with_fixed(app2);
|
2026-01-05 19:41:38 +00:00
|
|
|
tick_count += 1;
|
|
|
|
|
|
|
|
|
|
if tick_count % 50 == 0 {
|
|
|
|
|
println!(
|
|
|
|
|
"Waiting for sync... tick {} ({:.1}s elapsed)",
|
|
|
|
|
tick_count,
|
|
|
|
|
start.elapsed().as_secs_f32()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check condition
|
|
|
|
|
if check_fn(app1.world_mut(), app2.world_mut()) {
|
|
|
|
|
println!(
|
|
|
|
|
"Sync completed after {} ticks ({:.3}s)",
|
|
|
|
|
tick_count,
|
|
|
|
|
start.elapsed().as_secs_f32()
|
|
|
|
|
);
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Small delay to avoid spinning
|
|
|
|
|
tokio::time::sleep(Duration::from_millis(16)).await;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
println!("Sync timeout after {} ticks", tick_count);
|
|
|
|
|
anyhow::bail!("Sync timeout after {:?}. Condition not met.", timeout)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Count entities with a specific network_id
|
|
|
|
|
pub fn count_entities_with_id(world: &mut World, network_id: Uuid) -> usize {
|
|
|
|
|
use libmarathon::networking::NetworkedEntity;
|
|
|
|
|
|
|
|
|
|
let mut query = world.query::<&NetworkedEntity>();
|
|
|
|
|
query
|
|
|
|
|
.iter(world)
|
|
|
|
|
.filter(|ne| ne.network_id == network_id)
|
|
|
|
|
.count()
|
|
|
|
|
}
|