#![cfg(feature = "automerge-backend")]
use iroh::TransportAddr;
use peat_protocol::network::{IrohTransport, PeerInfo};
use peat_protocol::storage::capabilities::SyncCapable;
use peat_protocol::storage::{AutomergeBackend, AutomergeStore};
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tempfile::TempDir;
#[tokio::test]
async fn test_fast_transport_constructor_creates_functional_transport() {
let seed = "test-fast-constructor/node-1";
let bind_addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
let transport = IrohTransport::from_seed_at_addr(seed, bind_addr)
.await
.expect("Fast constructor should succeed");
assert!(
!transport.has_discovery(),
"Fast constructor should NOT enable mDNS"
);
let endpoint_id = transport.endpoint_id();
assert!(
!endpoint_id.as_bytes().is_empty(),
"Should have valid endpoint ID"
);
let addr = transport.endpoint_addr();
assert!(!addr.addrs.is_empty(), "Should have bound addresses");
let expected_id = IrohTransport::endpoint_id_from_seed(seed);
assert_eq!(
endpoint_id, expected_id,
"Fast constructor should produce deterministic endpoint ID"
);
}
#[tokio::test]
async fn test_deferred_mdns_discovery_enablement() {
let seed = "test-deferred-mdns/node-1";
let bind_addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
let transport = IrohTransport::from_seed_at_addr(seed, bind_addr)
.await
.expect("Fast constructor should succeed");
assert!(!transport.has_discovery(), "Should start without mDNS");
transport
.enable_mdns_discovery()
.await
.expect("Deferred mDNS enablement should succeed");
assert!(
transport.has_discovery(),
"Should have mDNS after enablement"
);
let mdns = transport.mdns_discovery();
assert!(mdns.is_some(), "Should be able to access mDNS discovery");
}
#[tokio::test]
async fn test_double_mdns_enablement_fails() {
let seed = "test-double-mdns/node-1";
let bind_addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
let transport = IrohTransport::from_seed_at_addr(seed, bind_addr)
.await
.expect("Fast constructor should succeed");
transport
.enable_mdns_discovery()
.await
.expect("First mDNS enablement should succeed");
let result = transport.enable_mdns_discovery().await;
assert!(result.is_err(), "Double mDNS enablement should fail");
assert!(
result.unwrap_err().to_string().contains("already enabled"),
"Error should indicate mDNS is already enabled"
);
}
#[tokio::test]
async fn test_fast_constructor_is_faster_than_mdns_constructor() {
let bind_addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
let _ = IrohTransport::from_seed_at_addr("warmup", bind_addr).await;
let mut fast_times = Vec::new();
for i in 0..3 {
let start = Instant::now();
let seed = format!("fast-timing-test/node-{}", i);
let _ = IrohTransport::from_seed_at_addr(&seed, bind_addr)
.await
.unwrap();
fast_times.push(start.elapsed().as_millis());
}
let mut mdns_times = Vec::new();
for i in 0..3 {
let start = Instant::now();
let seed = format!("mdns-timing-test/node-{}", i);
let _ = IrohTransport::from_seed_with_discovery_at_addr(&seed, bind_addr)
.await
.unwrap();
mdns_times.push(start.elapsed().as_millis());
}
let avg_fast: u128 = fast_times.iter().sum::<u128>() / fast_times.len() as u128;
let avg_mdns: u128 = mdns_times.iter().sum::<u128>() / mdns_times.len() as u128;
eprintln!(
"[STARTUP TIMING] Fast constructor avg: {}ms, mDNS constructor avg: {}ms",
avg_fast, avg_mdns
);
}
#[tokio::test]
async fn test_parallel_store_and_transport_initialization() {
let temp_dir = TempDir::new().unwrap();
let seed = "parallel-init-test/node-1";
let bind_addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
let storage_path = temp_dir.path().to_path_buf();
let start = Instant::now();
let (store_result, transport_result) = tokio::join!(
tokio::task::spawn_blocking({
let path = storage_path.clone();
move || AutomergeStore::open(&path)
}),
IrohTransport::from_seed_at_addr(seed, bind_addr)
);
let parallel_time = start.elapsed();
let store = Arc::new(store_result.unwrap().unwrap());
let transport = Arc::new(transport_result.unwrap());
eprintln!(
"[STARTUP TIMING] Parallel store+transport init: {}ms",
parallel_time.as_millis()
);
assert!(!store.is_in_memory());
assert!(!transport.endpoint_id().as_bytes().is_empty());
let backend = AutomergeBackend::with_transport(store, transport);
assert!(backend.sync_stats().is_ok());
}
#[tokio::test]
async fn test_fast_transport_can_connect_to_peers() {
let transport1 = Arc::new(
IrohTransport::from_seed_at_addr("connect-test/node-1", "127.0.0.1:0".parse().unwrap())
.await
.unwrap(),
);
let transport2 = Arc::new(
IrohTransport::from_seed_at_addr("connect-test/node-2", "127.0.0.1:0".parse().unwrap())
.await
.unwrap(),
);
transport1.start_accept_loop().unwrap();
transport2.start_accept_loop().unwrap();
let addr2 = get_first_ip_addr(&transport2);
let peer2_info = PeerInfo {
name: "node-2".to_string(),
node_id: hex::encode(transport2.endpoint_id().as_bytes()),
addresses: vec![addr2.to_string()],
relay_url: None,
};
transport1.connect_peer(&peer2_info).await.unwrap();
let connect_deadline = Duration::from_secs(5);
let poll_interval = Duration::from_millis(25);
let connected = tokio::time::timeout(connect_deadline, async {
loop {
if transport1.peer_count() > 0 || transport2.peer_count() > 0 {
return;
}
tokio::time::sleep(poll_interval).await;
}
})
.await
.is_ok();
let peer_count_1 = transport1.peer_count();
let peer_count_2 = transport2.peer_count();
eprintln!(
"[FAST TRANSPORT] Connection test - transport1 peers: {}, transport2 peers: {}",
peer_count_1, peer_count_2
);
assert!(
connected,
"Peers should connect using fast-created transports (no mDNS required); \
timed out after {:?} with peer counts {}/{}",
connect_deadline, peer_count_1, peer_count_2
);
let _ = transport1.stop_accept_loop();
let _ = transport2.stop_accept_loop();
}
#[tokio::test]
async fn test_sequential_vs_parallel_initialization_timing() {
let bind_addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
let sequential_time = {
let temp_dir = TempDir::new().unwrap();
let start = Instant::now();
let store = AutomergeStore::open(temp_dir.path()).unwrap();
let _transport = IrohTransport::from_seed_at_addr("sequential/node", bind_addr)
.await
.unwrap();
drop(store);
start.elapsed()
};
let parallel_time = {
let temp_dir = TempDir::new().unwrap();
let storage_path = temp_dir.path().to_path_buf();
let start = Instant::now();
let (store_result, transport_result) = tokio::join!(
tokio::task::spawn_blocking({
let path = storage_path.clone();
move || AutomergeStore::open(&path)
}),
IrohTransport::from_seed_at_addr("parallel/node", bind_addr)
);
let _ = store_result.unwrap().unwrap();
let _ = transport_result.unwrap();
start.elapsed()
};
eprintln!(
"[STARTUP TIMING] Sequential: {}ms, Parallel: {}ms, Improvement: {:.1}%",
sequential_time.as_millis(),
parallel_time.as_millis(),
(1.0 - parallel_time.as_secs_f64() / sequential_time.as_secs_f64()) * 100.0
);
}
#[tokio::test]
async fn test_full_startup_timing_like_ffi() {
use std::time::Instant;
let temp_dir = TempDir::new().unwrap();
let storage_path = temp_dir.path().to_path_buf();
let bind_addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
let seed = "timing-test/full-startup";
let total_start = Instant::now();
let phase_start = Instant::now();
let storage_path_for_store = storage_path.clone();
let (store_result, transport_result) = tokio::join!(
tokio::task::spawn_blocking(move || {
let start = Instant::now();
let result = AutomergeStore::open(&storage_path_for_store);
(result, start.elapsed().as_millis())
}),
async {
let start = Instant::now();
let result = IrohTransport::from_seed_at_addr(seed, bind_addr).await;
(result, start.elapsed().as_millis())
}
);
let (store, store_ms) = store_result.unwrap();
let store = Arc::new(store.unwrap());
let (transport, transport_ms) = transport_result;
let transport = Arc::new(transport.unwrap());
let parallel_ms = phase_start.elapsed().as_millis();
let phase_start = Instant::now();
let backend = AutomergeBackend::with_transport(Arc::clone(&store), Arc::clone(&transport));
let backend_ms = phase_start.elapsed().as_millis();
let phase_start = Instant::now();
backend.start_sync().unwrap();
let sync_init_ms = phase_start.elapsed().as_millis();
let total_ms = total_start.elapsed().as_millis();
eprintln!("\n=== FFI-EQUIVALENT STARTUP TIMING ===");
eprintln!("[Peat TIMING] Store open: {}ms", store_ms);
eprintln!(
"[Peat TIMING] Transport create (no mDNS): {}ms",
transport_ms
);
eprintln!(
"[Peat TIMING] Parallel total (max of above): {}ms",
parallel_ms
);
eprintln!("[Peat TIMING] Backend creation: {}ms", backend_ms);
eprintln!("[Peat TIMING] Sync init: {}ms", sync_init_ms);
eprintln!("[Peat TIMING] === TOTAL: {}ms ===\n", total_ms);
backend.stop_sync().unwrap();
let _ = total_ms;
}
fn get_first_ip_addr(transport: &IrohTransport) -> SocketAddr {
let addr = transport.endpoint_addr();
addr.addrs
.iter()
.find_map(|a| {
if let TransportAddr::Ip(socket_addr) = a {
Some(*socket_addr)
} else {
None
}
})
.expect("Transport should have at least one IP address")
}