use moonpool_core::{OpenOptions, StorageFile, StorageProvider};
use moonpool_sim::{SimWorld, StorageConfiguration};
use std::net::IpAddr;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
const TEST_IP_STR: &str = "127.0.0.1";
fn test_ip() -> IpAddr {
TEST_IP_STR.parse().expect("valid IP")
}
async fn run_and_measure_time<F, Fut>(mut sim: SimWorld, f: F) -> Duration
where
F: FnOnce(moonpool_sim::SimStorageProvider) -> Fut,
Fut: std::future::Future<Output = std::io::Result<()>> + 'static,
{
let provider = sim.storage_provider(test_ip());
let handle = tokio::task::spawn_local(f(provider));
while !handle.is_finished() {
while sim.pending_event_count() > 0 {
sim.step();
}
tokio::task::yield_now().await;
}
handle.await.expect("task panicked").expect("io error");
sim.current_time()
}
fn local_runtime() -> tokio::runtime::LocalRuntime {
tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build_local(Default::default())
.expect("Failed to build local runtime")
}
#[test]
fn test_fast_storage_config() {
local_runtime().block_on(async {
let mut sim = SimWorld::new();
sim.set_storage_config(StorageConfiguration::fast_local());
let time = run_and_measure_time(sim, |provider| async move {
let mut file = provider
.open("fast.txt", OpenOptions::create_write())
.await?;
file.write_all(b"quick test").await?;
file.sync_all().await?;
Ok(())
})
.await;
assert!(
time < Duration::from_millis(1),
"Fast config should complete in microseconds, got {:?}",
time
);
println!("Fast config completed in {:?}", time);
});
}
#[test]
fn test_default_storage_config() {
local_runtime().block_on(async {
let sim = SimWorld::new();
let time = run_and_measure_time(sim, |provider| async move {
let mut file = provider
.open("default.txt", OpenOptions::create_write())
.await?;
file.write_all(b"test").await?;
file.sync_all().await?;
Ok(())
})
.await;
assert!(
time >= Duration::from_millis(1),
"Default config should take at least 1ms (sync latency), got {:?}",
time
);
println!("Default config completed in {:?}", time);
});
}
#[test]
fn test_custom_latency_ranges() {
local_runtime().block_on(async {
let ten_ms = Duration::from_millis(10);
let config = StorageConfiguration {
iops: 1_000_000, bandwidth: 1_000_000_000, read_latency: ten_ms..ten_ms,
write_latency: ten_ms..ten_ms,
sync_latency: ten_ms..ten_ms,
read_fault_probability: 0.0,
write_fault_probability: 0.0,
crash_fault_probability: 0.0,
misdirect_write_probability: 0.0,
misdirect_read_probability: 0.0,
phantom_write_probability: 0.0,
sync_failure_probability: 0.0,
};
let mut sim = SimWorld::new();
sim.set_storage_config(config);
let time = run_and_measure_time(sim, |provider| async move {
let mut file = provider
.open("custom.txt", OpenOptions::create_write())
.await?;
file.write_all(b"test").await?;
file.sync_all().await?;
Ok(())
})
.await;
assert!(
time >= Duration::from_millis(20),
"Custom config should respect configured latencies, got {:?}",
time
);
assert!(
time <= Duration::from_millis(50),
"Latency should not exceed expected range, got {:?}",
time
);
println!("Custom config completed in {:?}", time);
});
}
#[test]
fn test_latency_formula() {
local_runtime().block_on(async {
let base_write = Duration::from_millis(1);
let config = StorageConfiguration {
iops: 1000, bandwidth: 1_000_000, read_latency: Duration::from_micros(100)..Duration::from_micros(100),
write_latency: base_write..base_write,
sync_latency: Duration::from_micros(100)..Duration::from_micros(100),
read_fault_probability: 0.0,
write_fault_probability: 0.0,
crash_fault_probability: 0.0,
misdirect_write_probability: 0.0,
misdirect_read_probability: 0.0,
phantom_write_probability: 0.0,
sync_failure_probability: 0.0,
};
let mut sim = SimWorld::new();
sim.set_storage_config(config);
let data_size = 10 * 1024;
let data = vec![0u8; data_size];
let time = run_and_measure_time(sim, |provider| async move {
let mut file = provider
.open("formula.txt", OpenOptions::create_write())
.await?;
file.write_all(&data).await?;
file.sync_all().await?;
Ok(())
})
.await;
println!("Latency formula test: wrote {}B in {:?}", data_size, time);
assert!(
time >= Duration::from_millis(10),
"Large write should take noticeable time, got {:?}",
time
);
});
}
#[test]
fn test_read_latency_scales_with_size() {
local_runtime().block_on(async {
let base = Duration::from_micros(100);
let config = StorageConfiguration {
iops: 1_000_000,
bandwidth: 100_000, read_latency: base..base,
write_latency: base..base,
sync_latency: base..base,
read_fault_probability: 0.0,
write_fault_probability: 0.0,
crash_fault_probability: 0.0,
misdirect_write_probability: 0.0,
misdirect_read_probability: 0.0,
phantom_write_probability: 0.0,
sync_failure_probability: 0.0,
};
let mut sim1 = SimWorld::new();
sim1.set_storage_config(config.clone());
let _ = run_and_measure_time(sim1, |provider| async move {
let mut file = provider
.open("read_scale.txt", OpenOptions::create_write())
.await?;
file.write_all(&vec![0u8; 10_000]).await?; file.sync_all().await?;
Ok(())
})
.await;
let sizes = [100, 1000, 5000];
let mut read_times = Vec::new();
for &size in &sizes {
let mut sim = SimWorld::new();
sim.set_storage_config(config.clone());
let provider = sim.storage_provider(test_ip());
let handle = tokio::task::spawn_local(async move {
let mut file = provider
.open("read_scale.txt", OpenOptions::create_write())
.await?;
file.write_all(&vec![b'x'; 10_000]).await?;
file.sync_all().await?;
Ok::<_, std::io::Error>(())
});
while !handle.is_finished() {
while sim.pending_event_count() > 0 {
sim.step();
}
tokio::task::yield_now().await;
}
handle.await.expect("task panicked").expect("io error");
let start_time = sim.current_time();
let provider2 = sim.storage_provider(test_ip());
let handle2 = tokio::task::spawn_local(async move {
let mut file = provider2
.open("read_scale.txt", OpenOptions::read_only())
.await?;
let mut buf = vec![0u8; size];
file.read_exact(&mut buf).await?;
Ok::<_, std::io::Error>(())
});
while !handle2.is_finished() {
while sim.pending_event_count() > 0 {
sim.step();
}
tokio::task::yield_now().await;
}
handle2.await.expect("task panicked").expect("io error");
let read_time = sim.current_time() - start_time;
read_times.push((size, read_time));
}
println!("Read latency scaling:");
for (size, time) in &read_times {
println!(" {} bytes: {:?}", size, time);
}
assert!(
read_times[1].1 > read_times[0].1,
"1KB read should take longer than 100B read"
);
assert!(
read_times[2].1 > read_times[1].1,
"5KB read should take longer than 1KB read"
);
});
}
#[test]
fn test_write_latency_scales_with_size() {
local_runtime().block_on(async {
let base = Duration::from_micros(100);
let config = StorageConfiguration {
iops: 1_000_000,
bandwidth: 100_000, read_latency: base..base,
write_latency: base..base,
sync_latency: base..base,
read_fault_probability: 0.0,
write_fault_probability: 0.0,
crash_fault_probability: 0.0,
misdirect_write_probability: 0.0,
misdirect_read_probability: 0.0,
phantom_write_probability: 0.0,
sync_failure_probability: 0.0,
};
let sizes = [100, 1000, 5000];
let mut write_times = Vec::new();
for &size in &sizes {
let mut sim = SimWorld::new();
sim.set_storage_config(config.clone());
let data = vec![b'x'; size];
let time = run_and_measure_time(sim, |provider| async move {
let mut file = provider
.open("write_scale.txt", OpenOptions::create_write())
.await?;
file.write_all(&data).await?;
Ok(())
})
.await;
write_times.push((size, time));
}
println!("Write latency scaling:");
for (size, time) in &write_times {
println!(" {} bytes: {:?}", size, time);
}
assert!(
write_times[1].1 > write_times[0].1,
"1KB write should take longer than 100B write"
);
assert!(
write_times[2].1 > write_times[1].1,
"5KB write should take longer than 1KB write"
);
});
}