use ipfrs::{Node, NodeConfig};
use std::path::PathBuf;
use std::time::Duration;
use tokio::time::sleep;
fn unique_tmp_dir(label: &str) -> PathBuf {
let pid = std::process::id();
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
let nonce = {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.subsec_nanos()
};
std::env::temp_dir().join(format!("ipfrs_test_{}_{}_{}_{}", label, pid, nonce, seq))
}
fn alloc_free_port() -> u16 {
let listener =
std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind ephemeral port");
listener
.local_addr()
.expect("Failed to get local addr")
.port()
}
fn make_config(dir: PathBuf, tcp_port: Option<u16>) -> NodeConfig {
let mut config = NodeConfig::default();
config.storage.path = dir.join("blocks");
config.network.data_dir = dir.join("network");
config.enable_semantic = false;
config.enable_tensorlogic = false;
config.network.enable_mdns = false;
config.network.enable_nat_traversal = false;
if let Some(port) = tcp_port {
config.network.listen_addrs = vec![format!("/ip4/127.0.0.1/tcp/{}", port)];
}
config
}
async fn wait_until<F, Fut>(
deadline: Duration,
poll_interval: Duration,
max_interval: Duration,
mut condition: F,
) -> bool
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = bool>,
{
let start = tokio::time::Instant::now();
let mut interval = poll_interval;
loop {
if condition().await {
return true;
}
if start.elapsed() >= deadline {
return false;
}
let remaining = deadline.saturating_sub(start.elapsed());
let wait = interval.min(remaining);
sleep(wait).await;
interval = (interval * 2).min(max_interval);
}
}
async fn wait_for_tcp_port(port: u16, deadline: Duration) -> bool {
use std::net::SocketAddr;
let addr: SocketAddr = format!("127.0.0.1:{}", port).parse().expect("valid addr");
let start = tokio::time::Instant::now();
let mut interval = Duration::from_millis(50);
loop {
match tokio::net::TcpStream::connect(addr).await {
Ok(_) => return true,
Err(_) => {
if start.elapsed() >= deadline {
return false;
}
let remaining = deadline.saturating_sub(start.elapsed());
sleep(interval.min(remaining)).await;
interval = (interval * 2).min(Duration::from_millis(500));
}
}
}
}
#[tokio::test]
async fn test_single_node_add_get() {
let dir = unique_tmp_dir("single_add_get");
let config = make_config(dir, None);
let mut node = Node::new(config).expect("Failed to create node");
node.start().await.expect("Failed to start node");
let content = b"hello ipfrs 0.2.0";
let cid = node
.add_bytes(content.as_slice())
.await
.expect("add_bytes failed");
let retrieved = node
.get(&cid)
.await
.expect("get failed")
.expect("content not found");
assert_eq!(
retrieved.as_ref(),
content,
"Retrieved content should match what was added"
);
node.stop().await.expect("Failed to stop node");
}
#[tokio::test]
async fn test_single_node_block_stat() {
let dir = unique_tmp_dir("single_block_stat");
let config = make_config(dir, None);
let mut node = Node::new(config).expect("Failed to create node");
node.start().await.expect("Failed to start node");
let content = b"block stat content for ipfrs 0.2.0";
let cid = node
.add_bytes(content.as_slice())
.await
.expect("add_bytes failed");
let stat = node
.block_stat(&cid)
.await
.expect("block_stat failed")
.expect("block stat not found");
assert_eq!(stat.size, content.len(), "block_stat size mismatch");
assert_eq!(stat.cid, cid, "block_stat CID mismatch");
node.stop().await.expect("Failed to stop node");
}
#[tokio::test]
async fn test_node_provides_to_dht() {
let dir = unique_tmp_dir("provides_dht");
let config = make_config(dir, None);
let mut node = Node::new(config).expect("Failed to create node");
node.start().await.expect("Failed to start node");
let local_peer_id = node.peer_id().expect("Failed to get peer ID");
assert!(!local_peer_id.is_empty(), "Peer ID must not be empty");
let content = b"tensor data provided to dht";
let cid = node
.add_bytes(content.as_slice())
.await
.expect("add_bytes failed");
let stored = node.has_block(&cid).await.expect("has_block failed");
assert!(stored, "Block should be in local storage after add_bytes");
assert!(
node.is_running(),
"Node must still be running after add_bytes"
);
let data = node
.get(&cid)
.await
.expect("get failed")
.expect("block not found locally");
assert_eq!(data.as_ref(), content);
node.stop().await.expect("Failed to stop node");
}
#[tokio::test]
async fn test_two_nodes_connect() {
let port_a = alloc_free_port();
let port_b = alloc_free_port();
let dir_a = unique_tmp_dir("connect_a");
let dir_b = unique_tmp_dir("connect_b");
let mut node_a = Node::new(make_config(dir_a, Some(port_a))).expect("Failed to create node A");
let mut node_b = Node::new(make_config(dir_b, Some(port_b))).expect("Failed to create node B");
node_a.start().await.expect("Failed to start node A");
node_b.start().await.expect("Failed to start node B");
assert!(
wait_for_tcp_port(port_a, Duration::from_secs(15)).await,
"Node A's TCP port {} did not become reachable within 15 s",
port_a
);
let peer_id_a = node_a.peer_id().expect("Failed to get peer ID of A");
let peer_id_b = node_b.peer_id().expect("Failed to get peer ID of B");
let addr_a = format!("/ip4/127.0.0.1/tcp/{}/p2p/{}", port_a, peer_id_a);
node_b
.connect(&addr_a)
.await
.expect("Node B failed to dial Node A");
let peers_a_ok = wait_until(
Duration::from_secs(15),
Duration::from_millis(100),
Duration::from_millis(1000),
|| async {
node_a
.peers()
.await
.map(|peers| peers.contains(&peer_id_b))
.unwrap_or(false)
},
)
.await;
let peers_b_ok = wait_until(
Duration::from_secs(15),
Duration::from_millis(100),
Duration::from_millis(1000),
|| async {
node_b
.peers()
.await
.map(|peers| peers.contains(&peer_id_a))
.unwrap_or(false)
},
)
.await;
let peers_a = node_a.peers().await.expect("Failed to get peers of A");
let peers_b = node_b.peers().await.expect("Failed to get peers of B");
assert!(
peers_a_ok,
"Node A should see Node B in its peer list (A's peers: {:?})",
peers_a
);
assert!(
peers_b_ok,
"Node B should see Node A in its peer list (B's peers: {:?})",
peers_b
);
node_a.stop().await.expect("Failed to stop node A");
node_b.stop().await.expect("Failed to stop node B");
}
#[tokio::test]
async fn test_two_nodes_block_exchange() {
let port_a = alloc_free_port();
let port_b = alloc_free_port();
let dir_a = unique_tmp_dir("exchange_a");
let dir_b = unique_tmp_dir("exchange_b");
let mut node_a = Node::new(make_config(dir_a, Some(port_a))).expect("Failed to create node A");
let mut node_b = Node::new(make_config(dir_b, Some(port_b))).expect("Failed to create node B");
node_a.start().await.expect("Failed to start node A");
node_b.start().await.expect("Failed to start node B");
assert!(
wait_for_tcp_port(port_a, Duration::from_secs(15)).await,
"Node A's TCP port {} did not become reachable within 15 s",
port_a
);
let peer_id_a = node_a.peer_id().expect("Failed to get peer ID of A");
let addr_a = format!("/ip4/127.0.0.1/tcp/{}/p2p/{}", port_a, peer_id_a);
node_b
.connect(&addr_a)
.await
.expect("Node B failed to dial Node A");
let peer_id_b = node_b.peer_id().expect("Failed to get peer ID of B");
let connected = wait_until(
Duration::from_secs(15),
Duration::from_millis(100),
Duration::from_millis(500),
|| async {
let a_sees_b = node_a
.peers()
.await
.map(|ps| ps.contains(&peer_id_b))
.unwrap_or(false);
let b_sees_a = node_b
.peers()
.await
.map(|ps| ps.contains(&peer_id_a))
.unwrap_or(false);
a_sees_b && b_sees_a
},
)
.await;
assert!(
connected,
"Nodes did not see each other in peer lists within 15 s"
);
let content = b"tensor data from node A";
let cid = node_a
.add_bytes(content.as_slice())
.await
.expect("Node A: add_bytes failed");
use std::str::FromStr;
let expected_peer_id =
ipfrs_network::libp2p::PeerId::from_str(&peer_id_a).expect("Failed to parse peer ID A");
let found = tokio::time::timeout(Duration::from_secs(30), async {
let mut poll_interval = Duration::from_millis(500);
let max_poll_interval = Duration::from_secs(3);
loop {
let providers = node_b
.find_providers_timeout(&cid, Duration::from_secs(8))
.await
.unwrap_or_default();
if providers.contains(&expected_peer_id) {
return providers;
}
sleep(poll_interval).await;
poll_interval = (poll_interval * 2).min(max_poll_interval);
}
})
.await;
let providers = match found {
Ok(p) => p,
Err(_) => panic!(
"Node B did not find Node A as provider for {} within 30 s",
cid
),
};
assert!(
providers.contains(&expected_peer_id),
"Node A's peer ID should appear in providers (got: {:?})",
providers
);
node_a.stop().await.expect("Failed to stop node A");
node_b.stop().await.expect("Failed to stop node B");
}
#[tokio::test]
async fn test_single_node_bulk_add_get() {
let dir = unique_tmp_dir("bulk_add_get");
let config = make_config(dir, None);
let mut node = Node::new(config).expect("Failed to create node");
node.start().await.expect("Failed to start node");
let count = 50usize;
let mut cids = Vec::with_capacity(count);
for i in 0..count {
let content = format!("ipfrs 0.2.0 bulk block {:04}", i);
let content_bytes = content.into_bytes();
let cid = node
.add_bytes(content_bytes.clone())
.await
.unwrap_or_else(|e| panic!("add_bytes failed for block {}: {}", i, e));
cids.push((cid, content_bytes));
}
for (cid, expected_bytes) in &cids {
let data = node
.get(cid)
.await
.unwrap_or_else(|e| panic!("get failed for {}: {}", cid, e))
.unwrap_or_else(|| panic!("block not found: {}", cid));
assert_eq!(
data.as_ref(),
expected_bytes.as_slice(),
"Content mismatch for {}",
cid
);
}
let stats = node.storage_stats().expect("storage_stats failed");
assert!(
stats.num_blocks >= count,
"Expected at least {} blocks, got {}",
count,
stats.num_blocks
);
node.stop().await.expect("Failed to stop node");
sleep(Duration::from_millis(50)).await;
}
#[tokio::test]
async fn test_node_restart_durability() {
let dir = unique_tmp_dir("restart_durability");
let content = b"durable content across restarts";
let cid = {
let config = make_config(dir.clone(), None);
let mut node = Node::new(config).expect("Failed to create node (session 1)");
node.start().await.expect("Failed to start (session 1)");
let cid = node
.add_bytes(content.as_slice())
.await
.expect("add_bytes failed (session 1)");
node.stop().await.expect("Failed to stop (session 1)");
cid
};
{
let config = make_config(dir, None);
let mut node = Node::new(config).expect("Failed to create node (session 2)");
node.start().await.expect("Failed to start (session 2)");
let exists = node
.has_block(&cid)
.await
.expect("has_block failed (session 2)");
assert!(exists, "Block must survive across node restarts");
let retrieved = node
.get(&cid)
.await
.expect("get failed (session 2)")
.expect("block not found after restart");
assert_eq!(
retrieved.as_ref(),
content,
"Content must be intact after restart"
);
node.stop().await.expect("Failed to stop (session 2)");
}
}
fn make_semantic_config(dir: std::path::PathBuf) -> NodeConfig {
let mut config = NodeConfig::default();
config.storage.path = dir.join("blocks");
config.network.data_dir = dir.join("network");
config.enable_semantic = true;
config.enable_tensorlogic = false;
config.network.enable_mdns = false;
config.network.enable_nat_traversal = false;
config
}
#[tokio::test]
async fn test_semantic_index_persists_across_restart() {
let dir = unique_tmp_dir("semantic_persist");
let snap_path = dir.join("blocks").join("hnsw_index.snap");
{
let config = make_semantic_config(dir.clone());
let mut node = Node::new(config).expect("create node (session 1)");
node.start().await.expect("start (session 1)");
let cid = node
.add_bytes(b"semantic test content" as &[u8])
.await
.expect("add_bytes (session 1)");
let stats = node.semantic_stats().expect("semantic_stats (session 1)");
let dim = stats.dimension;
let embedding_padded: Vec<f32> = {
let mut v = vec![0.0f32; dim];
if dim > 0 {
v[0] = 1.0;
}
v
};
node.index_content(&cid, &embedding_padded)
.await
.expect("index_content (session 1)");
let stats_after = node.semantic_stats().expect("semantic_stats after index");
assert!(
stats_after.num_vectors >= 1,
"Expected at least 1 vector in index before stop, got {}",
stats_after.num_vectors
);
node.stop().await.expect("stop (session 1)");
}
assert!(
snap_path.exists(),
"HNSW snapshot must exist at {} after node stop",
snap_path.display()
);
{
let config = make_semantic_config(dir.clone());
let mut node = Node::new(config).expect("create node (session 2)");
node.start().await.expect("start (session 2)");
let stats = node.semantic_stats().expect("semantic_stats (session 2)");
assert!(
stats.num_vectors >= 1,
"Expected at least 1 vector restored from snapshot, got {}",
stats.num_vectors
);
node.stop().await.expect("stop (session 2)");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn test_distributed_infer_local_fast_path() {
use ipfrs::{Constant, Predicate, Term};
let dir = unique_tmp_dir("dinfer_local");
let mut config = make_config(dir.clone(), None);
config.enable_tensorlogic = true;
let mut node = Node::new(config).expect("Failed to create node");
node.start().await.expect("Failed to start node");
let fact = Predicate::new(
"greeting".to_string(),
vec![Term::Const(Constant::String("hello".to_string()))],
);
node.add_fact(fact).expect("Failed to add fact");
let result = node
.distributed_infer("greeting(hello)", 5, Duration::from_secs(1))
.await;
assert!(
result.is_ok(),
"distributed_infer should succeed: {:?}",
result.err()
);
let dinfer = result.expect("already checked above");
assert_eq!(
dinfer.peers_queried, 0,
"Expected 0 peers queried on local fast-path, got {}",
dinfer.peers_queried
);
assert!(
!dinfer.session_id.is_empty(),
"session_id must not be empty"
);
assert!(
dinfer.elapsed_ms < 10_000,
"elapsed_ms suspiciously large: {}",
dinfer.elapsed_ms
);
node.stop().await.expect("Failed to stop node");
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn test_distributed_infer_no_network_graceful() {
let dir = unique_tmp_dir("dinfer_nonet");
let mut config = NodeConfig::default();
config.storage.path = dir.join("blocks");
config.network.data_dir = dir.clone();
config.enable_tensorlogic = true;
let mut node = Node::new(config).expect("Failed to create node");
node.start().await.expect("Failed to start node");
let result = node
.distributed_infer("nonexistent_predicate(X)", 3, Duration::from_millis(200))
.await;
assert!(
result.is_ok(),
"distributed_infer should not error even without network peers: {:?}",
result.err()
);
let dinfer = result.expect("already checked above");
assert!(
dinfer.local_bindings.is_empty(),
"Expected no local bindings"
);
assert!(
dinfer.remote_bindings.is_empty(),
"Expected no remote bindings"
);
node.stop().await.expect("Failed to stop node");
let _ = std::fs::remove_dir_all(&dir);
}