use std::ops::ControlFlow;
use std::time::Duration;
use smb2::client::{list_shares, ClientConfig, Connection, Session, SmbClient, Tree};
fn load_dotenv() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(".env");
if let Ok(contents) = std::fs::read_to_string(path) {
for line in contents.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
if std::env::var(key.trim()).is_err() {
let value = value.trim();
let value = value
.strip_prefix('"')
.and_then(|v| v.strip_suffix('"'))
.unwrap_or(value);
std::env::set_var(key.trim(), value);
}
}
}
}
}
fn nas_password() -> String {
load_dotenv();
std::env::var("SMB2_TEST_NAS_PASSWORD").expect(
"SMB2_TEST_NAS_PASSWORD not set. Copy .env.example to .env and fill in your password.",
)
}
#[tokio::test]
#[ignore]
async fn connect_and_list_directory_on_real_nas() {
let _ = env_logger::try_init();
let mut conn = Connection::connect("192.168.1.111:445", Duration::from_secs(5))
.await
.expect("failed to connect to NAS");
conn.negotiate().await.expect("negotiate failed");
let params = conn.params().unwrap();
println!("Negotiated dialect: {}", params.dialect);
println!("Max read size: {}", params.max_read_size);
println!("Signing required: {}", params.signing_required);
let session = Session::setup(&mut conn, "david", &nas_password(), "")
.await
.expect("session setup failed");
println!("Session ID: {}", session.session_id);
let tree = Tree::connect(&mut conn, "naspi")
.await
.expect("tree connect failed");
println!("Tree ID: {}", tree.tree_id);
let entries = tree
.list_directory(&mut conn, "")
.await
.expect("list directory failed");
println!("Directory entries ({} total):", entries.len());
for entry in entries.iter().take(10) {
let kind = if entry.is_directory { "dir " } else { "file" };
println!(" {} {} ({} bytes)", kind, entry.name, entry.size);
}
if entries.len() > 10 {
println!(" ... and {} more", entries.len() - 10);
}
assert!(!entries.is_empty(), "expected at least one entry in root");
tree.disconnect(&mut conn)
.await
.expect("tree disconnect failed");
}
#[tokio::test]
#[ignore]
async fn connect_and_list_directory_on_raspberry_pi() {
let _ = env_logger::try_init();
let mut conn = Connection::connect("192.168.1.150:445", Duration::from_secs(5))
.await
.expect("failed to connect to Raspberry Pi");
conn.negotiate().await.expect("negotiate failed");
let params = conn.params().unwrap();
println!("Negotiated dialect: {}", params.dialect);
println!("Max read size: {}", params.max_read_size);
println!("Max transact size: {}", params.max_transact_size);
println!("Signing required: {}", params.signing_required);
let session = Session::setup(&mut conn, "", "", "")
.await
.expect("session setup failed");
println!("Session ID: {}", session.session_id);
let tree = Tree::connect(&mut conn, "PiHDD")
.await
.expect("tree connect failed");
println!("Tree ID: {}", tree.tree_id);
let entries = tree
.list_directory(&mut conn, "")
.await
.expect("list directory failed");
println!("Directory entries ({} total):", entries.len());
for entry in entries.iter().take(10) {
let kind = if entry.is_directory { "dir " } else { "file" };
println!(" {} {} ({} bytes)", kind, entry.name, entry.size);
}
if entries.len() > 10 {
println!(" ... and {} more", entries.len() - 10);
}
assert!(!entries.is_empty(), "expected at least one entry in root");
tree.disconnect(&mut conn)
.await
.expect("tree disconnect failed");
}
async fn connect_to_nas() -> (Connection, Tree) {
let mut conn = Connection::connect("192.168.1.111:445", Duration::from_secs(5))
.await
.expect("failed to connect to NAS");
conn.negotiate().await.expect("negotiate failed");
let _session = Session::setup(&mut conn, "david", &nas_password(), "")
.await
.expect("session setup failed");
let tree = Tree::connect(&mut conn, "naspi")
.await
.expect("tree connect failed");
(conn, tree)
}
#[tokio::test]
#[ignore]
async fn write_and_read_file_on_nas() {
let _ = env_logger::try_init();
let (mut conn, tree) = connect_to_nas().await;
let test_data = b"Hello from smb2-rs integration test!";
let test_path = "smb2_test_write_read.tmp";
let written = tree
.write_file(&mut conn, test_path, test_data)
.await
.expect("write_file failed");
println!("Wrote {} bytes to {}", written, test_path);
assert_eq!(written, test_data.len() as u64);
let data = tree
.read_file(&mut conn, test_path)
.await
.expect("read_file failed");
println!("Read {} bytes from {}", data.len(), test_path);
assert_eq!(data, test_data);
tree.delete_file(&mut conn, test_path)
.await
.expect("delete_file failed");
println!("Deleted {}", test_path);
tree.disconnect(&mut conn).await.expect("disconnect failed");
}
#[tokio::test]
#[ignore]
async fn read_file_compound_on_nas() {
let _ = env_logger::try_init();
let (mut conn, tree) = connect_to_nas().await;
let test_path = "smb2_test_compound.tmp";
let test_data = b"compound read test data 1234567890";
tree.write_file(&mut conn, test_path, test_data)
.await
.expect("write_file failed");
let start = std::time::Instant::now();
let compound_data = tree
.read_file_compound(&mut conn, test_path)
.await
.expect("read_file_compound failed");
let compound_elapsed = start.elapsed();
assert_eq!(compound_data, test_data);
println!(
"Compound read: {} bytes in {:?}",
compound_data.len(),
compound_elapsed
);
let start = std::time::Instant::now();
let sequential_data = tree
.read_file(&mut conn, test_path)
.await
.expect("read_file (sequential) failed");
let sequential_elapsed = start.elapsed();
assert_eq!(sequential_data, test_data);
println!(
"Sequential read: {} bytes in {:?}",
sequential_data.len(),
sequential_elapsed
);
if compound_elapsed < sequential_elapsed {
println!(
"Compound was {:.1}x faster",
sequential_elapsed.as_secs_f64() / compound_elapsed.as_secs_f64()
);
}
tree.delete_file(&mut conn, test_path)
.await
.expect("delete_file failed");
tree.disconnect(&mut conn).await.expect("disconnect failed");
}
#[tokio::test]
#[ignore]
async fn stat_file_on_nas() {
let _ = env_logger::try_init();
let (mut conn, tree) = connect_to_nas().await;
let test_path = "smb2_test_stat.tmp";
let test_data = b"stat test content";
tree.write_file(&mut conn, test_path, test_data)
.await
.expect("write_file failed");
let info = tree.stat(&mut conn, test_path).await.expect("stat failed");
println!(
"Stat {}: size={}, is_dir={}",
test_path, info.size, info.is_directory
);
assert_eq!(info.size, test_data.len() as u64);
assert!(!info.is_directory);
tree.delete_file(&mut conn, test_path)
.await
.expect("delete_file failed");
tree.disconnect(&mut conn).await.expect("disconnect failed");
}
#[tokio::test]
#[ignore]
async fn create_and_delete_directory_on_nas() {
let _ = env_logger::try_init();
let (mut conn, tree) = connect_to_nas().await;
let dir_path = "smb2_test_dir_tmp";
tree.create_directory(&mut conn, dir_path)
.await
.expect("create_directory failed");
println!("Created directory {}", dir_path);
let info = tree.stat(&mut conn, dir_path).await.expect("stat failed");
assert!(info.is_directory, "expected a directory");
tree.delete_directory(&mut conn, dir_path)
.await
.expect("delete_directory failed");
println!("Deleted directory {}", dir_path);
tree.disconnect(&mut conn).await.expect("disconnect failed");
}
#[tokio::test]
#[ignore]
async fn list_shares_on_nas() {
let _ = env_logger::try_init();
let mut conn = Connection::connect("192.168.1.111:445", Duration::from_secs(5))
.await
.expect("failed to connect to NAS");
conn.negotiate().await.expect("negotiate failed");
let _session = Session::setup(&mut conn, "david", &nas_password(), "")
.await
.expect("session setup failed");
let shares = list_shares(&mut conn).await.expect("list_shares failed");
println!("Shares ({} total):", shares.len());
for share in &shares {
println!(
" {} (type=0x{:08X}) - {}",
share.name, share.share_type, share.comment
);
}
assert!(
shares.iter().any(|s| s.name == "naspi"),
"expected 'naspi' share in the list"
);
}
#[tokio::test]
#[ignore]
async fn list_shares_on_raspberry_pi() {
let _ = env_logger::try_init();
let mut conn = Connection::connect("192.168.1.150:445", Duration::from_secs(5))
.await
.expect("failed to connect to Raspberry Pi");
conn.negotiate().await.expect("negotiate failed");
let _session = Session::setup(&mut conn, "", "", "")
.await
.expect("session setup failed");
let shares = list_shares(&mut conn).await.expect("list_shares failed");
println!("Shares on Pi ({} total):", shares.len());
for share in &shares {
println!(
" {} (type=0x{:08X}) - {}",
share.name, share.share_type, share.comment
);
}
assert!(!shares.is_empty(), "expected at least one share");
}
async fn connect_client_to_nas() -> SmbClient {
SmbClient::connect(ClientConfig {
addr: "192.168.1.111:445".to_string(),
timeout: Duration::from_secs(5),
username: "david".to_string(),
password: nas_password(),
domain: String::new(),
auto_reconnect: false,
compression: true,
dfs_enabled: true,
dfs_target_overrides: std::collections::HashMap::new(),
})
.await
.expect("SmbClient::connect failed")
}
#[tokio::test]
#[ignore]
async fn smb_client_connect_and_list_directory() {
let _ = env_logger::try_init();
let mut client = connect_client_to_nas().await;
let params = client.params().unwrap();
println!("Dialect: {}", params.dialect);
println!("Session ID: {}", client.session().session_id);
let tree = client
.connect_share("naspi")
.await
.expect("connect_share failed");
println!("Tree ID: {}", tree.tree_id);
let entries = tree
.list_directory(client.connection_mut(), "")
.await
.expect("list_directory failed");
println!("Directory entries ({} total):", entries.len());
for entry in entries.iter().take(10) {
let kind = if entry.is_directory { "dir " } else { "file" };
println!(" {} {} ({} bytes)", kind, entry.name, entry.size);
}
assert!(!entries.is_empty(), "expected at least one entry");
tree.disconnect(client.connection_mut())
.await
.expect("disconnect failed");
}
#[tokio::test]
#[ignore]
async fn smb_client_list_shares() {
let _ = env_logger::try_init();
let mut client = connect_client_to_nas().await;
let shares = client.list_shares().await.expect("list_shares failed");
println!("Shares ({} total):", shares.len());
for share in &shares {
println!(" {} - {}", share.name, share.comment);
}
assert!(
shares.iter().any(|s| s.name == "naspi"),
"expected 'naspi' share in the list"
);
}
#[tokio::test]
#[ignore]
async fn pipelined_read_large_file() {
let _ = env_logger::try_init();
let (mut conn, tree) = connect_to_nas().await;
let test_path = "smb2_test_pipelined_read.tmp";
let test_data: Vec<u8> = (0..1_048_576).map(|i| (i % 251) as u8).collect();
let written = tree
.write_file(&mut conn, test_path, &test_data)
.await
.expect("write_file failed");
println!("Setup: wrote {} bytes to {}", written, test_path);
let start = std::time::Instant::now();
let data = tree
.read_file_pipelined(&mut conn, test_path)
.await
.expect("read_file_pipelined failed");
let elapsed = start.elapsed();
println!(
"Pipelined read: {} bytes in {:.2?} ({:.1} MB/s)",
data.len(),
elapsed,
data.len() as f64 / (1024.0 * 1024.0) / elapsed.as_secs_f64()
);
assert_eq!(data.len(), test_data.len(), "size mismatch");
assert_eq!(data, test_data, "content mismatch");
let start_seq = std::time::Instant::now();
let data_seq = tree
.read_file(&mut conn, test_path)
.await
.expect("read_file (sequential) failed");
let elapsed_seq = start_seq.elapsed();
println!(
"Sequential read: {} bytes in {:.2?} ({:.1} MB/s)",
data_seq.len(),
elapsed_seq,
data_seq.len() as f64 / (1024.0 * 1024.0) / elapsed_seq.as_secs_f64()
);
tree.delete_file(&mut conn, test_path)
.await
.expect("delete_file failed");
tree.disconnect(&mut conn).await.expect("disconnect failed");
}
#[tokio::test]
#[ignore]
async fn pipelined_write_large_file() {
let _ = env_logger::try_init();
let (mut conn, tree) = connect_to_nas().await;
let test_path = "smb2_test_pipelined_write.tmp";
let test_data: Vec<u8> = (0..1_048_576).map(|i| (i % 199) as u8).collect();
let start = std::time::Instant::now();
let written = tree
.write_file_pipelined(&mut conn, test_path, &test_data)
.await
.expect("write_file_pipelined failed");
let elapsed = start.elapsed();
println!(
"Pipelined write: {} bytes in {:.2?} ({:.1} MB/s)",
written,
elapsed,
written as f64 / (1024.0 * 1024.0) / elapsed.as_secs_f64()
);
assert_eq!(written, test_data.len() as u64);
let data = tree
.read_file(&mut conn, test_path)
.await
.expect("read_file failed");
assert_eq!(data.len(), test_data.len(), "size mismatch");
assert_eq!(data, test_data, "content mismatch");
tree.delete_file(&mut conn, test_path)
.await
.expect("delete_file failed");
tree.disconnect(&mut conn).await.expect("disconnect failed");
}
#[tokio::test]
#[ignore]
async fn reconnect_after_disconnect() {
let _ = env_logger::try_init();
let mut client = connect_client_to_nas().await;
let tree = client
.connect_share("naspi")
.await
.expect("connect_share failed");
let entries = tree
.list_directory(client.connection_mut(), "")
.await
.expect("list_directory failed");
println!("Before reconnect: {} entries", entries.len());
assert!(!entries.is_empty());
tree.disconnect(client.connection_mut())
.await
.expect("disconnect failed");
client.reconnect().await.expect("reconnect failed");
println!(
"Reconnected, new session_id={}",
client.session().session_id
);
let tree2 = client
.connect_share("naspi")
.await
.expect("connect_share failed after reconnect");
let entries2 = tree2
.list_directory(client.connection_mut(), "")
.await
.expect("list_directory failed after reconnect");
println!("After reconnect: {} entries", entries2.len());
assert!(!entries2.is_empty());
tree2
.disconnect(client.connection_mut())
.await
.expect("disconnect failed");
}
#[tokio::test]
#[ignore]
async fn streaming_download_large_file() {
let _ = env_logger::try_init();
let mut client = connect_client_to_nas().await;
let mut tree = client
.connect_share("naspi")
.await
.expect("connect_share failed");
let test_path = "smb2_test_streaming_download.tmp";
let test_data: Vec<u8> = (0..1_048_576).map(|i| (i % 251) as u8).collect();
client
.write_file(&mut tree, test_path, &test_data)
.await
.expect("write_file failed");
println!("Setup: wrote {} bytes", test_data.len());
let mut download = client
.download(&tree, test_path)
.await
.expect("download failed");
assert_eq!(download.size(), test_data.len() as u64);
println!("Downloading {} bytes...", download.size());
let mut received = Vec::new();
let mut chunk_count = 0u32;
while let Some(chunk) = download.next_chunk().await {
let bytes = chunk.expect("next_chunk failed");
assert!(!bytes.is_empty(), "received empty chunk");
received.extend_from_slice(&bytes);
chunk_count += 1;
println!(
" chunk {}: {} bytes, progress={:.1}%",
chunk_count,
bytes.len(),
download.progress().percent()
);
}
assert!(
(download.progress().fraction() - 1.0).abs() < f64::EPSILON,
"expected progress to reach 1.0, got {}",
download.progress().fraction()
);
assert!(
chunk_count >= 1,
"expected at least one chunk, got {}",
chunk_count
);
assert_eq!(received.len(), test_data.len(), "size mismatch");
assert_eq!(received, test_data, "content mismatch");
drop(download);
client
.delete_file(&mut tree, test_path)
.await
.expect("delete_file failed");
client
.disconnect_share(&tree)
.await
.expect("disconnect failed");
}
#[tokio::test]
#[ignore]
async fn write_with_progress_and_cancel() {
let _ = env_logger::try_init();
let mut client = connect_client_to_nas().await;
let mut tree = client
.connect_share("naspi")
.await
.expect("connect_share failed");
let test_path = "smb2_test_write_progress.tmp";
let test_data: Vec<u8> = (0..1_048_576).map(|i| (i % 199) as u8).collect();
let mut progress_updates = Vec::new();
let written = client
.write_file_with_progress(&mut tree, test_path, &test_data, |progress| {
println!(
" progress: {}/{} ({:.1}%)",
progress.bytes_transferred,
progress.total_bytes.unwrap_or(0),
progress.percent()
);
progress_updates.push(progress.bytes_transferred);
ControlFlow::Continue(())
})
.await
.expect("write_file_with_progress failed");
assert_eq!(written, test_data.len() as u64);
assert!(
!progress_updates.is_empty(),
"expected at least one progress update"
);
let readback = client
.read_file(&mut tree, test_path)
.await
.expect("read_file failed");
assert_eq!(
readback, test_data,
"content mismatch after write_with_progress"
);
client
.delete_file(&mut tree, test_path)
.await
.expect("delete_file failed");
let cancel_path = "smb2_test_write_cancel.tmp";
let half = test_data.len() as u64 / 2;
let result = client
.write_file_with_progress(&mut tree, cancel_path, &test_data, |progress| {
if progress.bytes_transferred >= half {
println!(" cancelling at {:.1}%", progress.percent());
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
})
.await;
match result {
Err(smb2::Error::Cancelled) => println!(" correctly received Error::Cancelled"),
other => panic!("expected Error::Cancelled, got {:?}", other),
}
let _ = client.delete_file(&mut tree, cancel_path).await;
client
.disconnect_share(&tree)
.await
.expect("disconnect failed");
}
#[tokio::test]
#[ignore]
async fn write_file_compound_on_nas() {
let _ = env_logger::try_init();
let (mut conn, tree) = connect_to_nas().await;
let test_path = "smb2_test_compound_write.tmp";
let test_data = b"compound write test data 1234567890";
let start = std::time::Instant::now();
let written = tree
.write_file_compound(&mut conn, test_path, test_data)
.await
.expect("write_file_compound failed");
let compound_elapsed = start.elapsed();
assert_eq!(written, test_data.len() as u64);
println!(
"Compound write: {} bytes in {:?}",
written, compound_elapsed
);
let data = tree
.read_file(&mut conn, test_path)
.await
.expect("read_file failed");
assert_eq!(data, test_data);
println!("Read back verified: {} bytes match", data.len());
let empty_path = "smb2_test_compound_write_empty.tmp";
let empty_written = tree
.write_file_compound(&mut conn, empty_path, b"")
.await
.expect("write_file_compound (empty) failed");
assert_eq!(empty_written, 0);
let empty_data = tree
.read_file(&mut conn, empty_path)
.await
.expect("read empty file failed");
assert!(empty_data.is_empty());
tree.delete_file(&mut conn, test_path)
.await
.expect("delete_file failed");
tree.delete_file(&mut conn, empty_path)
.await
.expect("delete empty file failed");
tree.disconnect(&mut conn).await.expect("disconnect failed");
}
#[tokio::test]
#[ignore]
async fn debug_rapid_pipelined_writes() {
let _ = env_logger::try_init();
let config = ClientConfig {
addr: "192.168.1.111:445".into(),
timeout: Duration::from_secs(5),
username: "david".into(),
password: nas_password(),
domain: String::new(),
auto_reconnect: false,
compression: true,
dfs_enabled: true,
dfs_target_overrides: std::collections::HashMap::new(),
};
let mut client = SmbClient::connect(config).await.expect("connect failed");
let mut share = client.connect_share("naspi").await.expect("tree failed");
eprintln!("Connected. Credits: {}", client.credits());
let data = vec![0xAA; 100 * 1024];
for i in 0..20 {
let path = format!("_test/smb2_diag_{}.tmp", i);
let start = std::time::Instant::now();
let result = tokio::time::timeout(
Duration::from_secs(10),
client.write_file_pipelined(&mut share, &path, &data),
)
.await;
match result {
Ok(Ok(n)) => eprintln!(
" [{}] wrote {} bytes in {:?}, credits={}",
i,
n,
start.elapsed(),
client.credits()
),
Ok(Err(e)) => {
eprintln!(" [{}] WRITE ERROR: {}", i, e);
break;
}
Err(_) => {
eprintln!(" [{}] TIMEOUT after 10s, credits={}", i, client.credits());
break;
}
}
}
for i in 0..20 {
let path = format!("_test/smb2_diag_{}.tmp", i);
let _ = client.delete_file(&mut share, &path).await;
}
let _ = client.disconnect_share(&share).await;
}
#[tokio::test]
#[ignore]
async fn micro_benchmark_smb2_vs_native() {
let _ = env_logger::try_init();
let config = ClientConfig {
addr: "192.168.1.111:445".into(),
timeout: Duration::from_secs(5),
username: "david".into(),
password: nas_password(),
domain: String::new(),
auto_reconnect: false,
compression: true,
dfs_enabled: true,
dfs_target_overrides: std::collections::HashMap::new(),
};
let mut client = SmbClient::connect(config).await.expect("connect");
let mut share = client.connect_share("naspi").await.expect("tree");
let _ = client
.create_directory(&mut share, "_test/smb2_bench")
.await;
let file_count = 50;
let file_size = 100 * 1024; let data = vec![0x42u8; file_size];
let start = std::time::Instant::now();
for i in 0..file_count {
let path = format!("_test/smb2_bench/f_{}.bin", i);
client
.write_file_pipelined(&mut share, &path, &data)
.await
.expect("write");
}
let smb2_upload = start.elapsed();
let start = std::time::Instant::now();
let entries = client
.list_directory(&mut share, "_test/smb2_bench")
.await
.expect("list");
let smb2_list = start.elapsed();
assert!(
entries.len() >= file_count,
"expected {} entries, got {}",
file_count,
entries.len()
);
let max_read = client
.params()
.map(|p| p.max_read_size as usize)
.unwrap_or(65536);
let start = std::time::Instant::now();
for i in 0..file_count {
let path = format!("_test/smb2_bench/f_{}.bin", i);
let d = if file_size <= max_read {
client.read_file(&mut share, &path).await.expect("read")
} else {
client
.read_file_pipelined(&mut share, &path)
.await
.expect("read")
};
assert_eq!(d.len(), file_size);
}
let smb2_download = start.elapsed();
let start = std::time::Instant::now();
for i in 0..file_count {
let path = format!("_test/smb2_bench/f_{}.bin", i);
client.delete_file(&mut share, &path).await.expect("delete");
}
let smb2_delete = start.elapsed();
let mount_path = std::path::Path::new("/Volumes/naspi/_test/smb2_bench_native");
let native_available = std::path::Path::new("/Volumes/naspi").exists();
let (nat_upload, nat_list, nat_download, nat_delete) = if native_available {
let _ = std::fs::create_dir_all(mount_path);
let start = std::time::Instant::now();
for i in 0..file_count {
let p = mount_path.join(format!("f_{}.bin", i));
std::fs::write(&p, &data).expect("native write");
}
let nu = start.elapsed();
let start = std::time::Instant::now();
let n = std::fs::read_dir(mount_path).unwrap().count();
let nl = start.elapsed();
assert!(n >= file_count);
let start = std::time::Instant::now();
for i in 0..file_count {
let p = mount_path.join(format!("f_{}.bin", i));
let d = std::fs::read(&p).expect("native read");
assert_eq!(d.len(), file_size);
}
let nd = start.elapsed();
let start = std::time::Instant::now();
for i in 0..file_count {
let p = mount_path.join(format!("f_{}.bin", i));
std::fs::remove_file(&p).expect("native delete");
}
let ndel = start.elapsed();
let _ = std::fs::remove_dir(mount_path);
(nu, nl, nd, ndel)
} else {
eprintln!("WARNING: /Volumes/naspi not mounted, skipping native comparison");
(
Duration::ZERO,
Duration::ZERO,
Duration::ZERO,
Duration::ZERO,
)
};
let _ = client
.delete_directory(&mut share, "_test/smb2_bench")
.await;
let _ = client.disconnect_share(&share).await;
let total_mb = (file_count * file_size) as f64 / (1024.0 * 1024.0);
println!("\n╔══════════════════════════════════════════════════════════╗");
println!(
"║ MICRO BENCHMARK: {} × {} KB = {:.1} MB ║",
file_count,
file_size / 1024,
total_mb
);
println!("╚══════════════════════════════════════════════════════════╝\n");
println!("┌────────────┬────────────┬────────────┬──────────┐");
println!("│ operation │ native │ smb2 │ ratio │");
println!("├────────────┼────────────┼────────────┼──────────┤");
let ops = [
("upload", nat_upload, smb2_upload),
("list", nat_list, smb2_list),
("download", nat_download, smb2_download),
("delete", nat_delete, smb2_delete),
];
for (name, nat, smb2) in &ops {
let ratio = if nat.as_secs_f64() > 0.0 {
format!("{:.2}x", smb2.as_secs_f64() / nat.as_secs_f64())
} else {
"N/A".to_string()
};
println!(
"│ {:<10} │ {:>8.0?} │ {:>8.0?} │ {:>8} │",
name, nat, smb2, ratio
);
}
println!("└────────────┴────────────┴────────────┴──────────┘");
println!("\nRatio < 1.0 means smb2 is faster than native.");
}
#[tokio::test]
#[ignore]
async fn compound_read_and_write_on_raspberry_pi() {
let _ = env_logger::try_init();
let mut conn = Connection::connect("192.168.1.150:445", Duration::from_secs(5))
.await
.expect("failed to connect to Pi");
conn.negotiate().await.expect("negotiate failed");
let session = Session::setup(&mut conn, "", "", "")
.await
.expect("session setup failed");
println!(
"Pi: dialect={}, sign={}",
conn.params().unwrap().dialect,
session.should_sign
);
let tree = smb2::Tree::connect(&mut conn, "PiHDD")
.await
.expect("tree connect failed");
let test_data = b"Pi compound test 1234567890";
let start = std::time::Instant::now();
tree.write_file_compound(&mut conn, "smb2_pi_compound.tmp", test_data)
.await
.expect("compound write failed");
println!("Pi compound write: {:?}", start.elapsed());
let start = std::time::Instant::now();
let read_back = tree
.read_file_compound(&mut conn, "smb2_pi_compound.tmp")
.await
.expect("compound read failed");
println!("Pi compound read: {:?}", start.elapsed());
assert_eq!(read_back, test_data, "data mismatch on Pi");
println!("Pi compound read/write verified!");
let _ = tree.delete_file(&mut conn, "smb2_pi_compound.tmp").await;
let _ = tree.disconnect(&mut conn).await;
}
#[tokio::test]
#[ignore]
async fn streaming_upload_large_file() {
let _ = env_logger::try_init();
let mut client = connect_client_to_nas().await;
let mut tree = client
.connect_share("naspi")
.await
.expect("connect_share failed");
let test_path = "smb2_test_streaming_upload.tmp";
let test_data: Vec<u8> = (0..2_097_152).map(|i| (i % 251) as u8).collect();
let mut upload = client
.upload(&tree, test_path, &test_data)
.await
.expect("upload failed");
assert_eq!(upload.total_bytes(), test_data.len() as u64);
println!("Uploading {} bytes...", upload.total_bytes());
let mut chunk_count = 0u32;
while upload
.write_next_chunk()
.await
.expect("write_next_chunk failed")
{
chunk_count += 1;
println!(
" chunk {}: progress={:.1}%",
chunk_count,
upload.progress().percent()
);
}
assert!(
(upload.progress().fraction() - 1.0).abs() < f64::EPSILON,
"expected progress to reach 1.0, got {}",
upload.progress().fraction()
);
assert!(
chunk_count >= 1,
"expected at least one progress update for large file, got {}",
chunk_count
);
println!("Upload complete in {} chunks", chunk_count);
drop(upload);
let readback = client
.read_file(&mut tree, test_path)
.await
.expect("read_file failed");
assert_eq!(readback.len(), test_data.len(), "size mismatch");
assert_eq!(readback, test_data, "content mismatch");
println!("Read back verified: {} bytes match", readback.len());
client
.delete_file(&mut tree, test_path)
.await
.expect("delete_file failed");
client
.disconnect_share(&tree)
.await
.expect("disconnect failed");
}
#[tokio::test]
#[ignore]
async fn streaming_upload_small_file_uses_compound() {
let _ = env_logger::try_init();
let mut client = connect_client_to_nas().await;
let mut tree = client
.connect_share("naspi")
.await
.expect("connect_share failed");
let test_path = "smb2_test_streaming_upload_small.tmp";
let test_data = b"small file via streaming upload API";
let mut upload = client
.upload(&tree, test_path, test_data)
.await
.expect("upload failed");
assert_eq!(upload.total_bytes(), test_data.len() as u64);
let has_more = upload
.write_next_chunk()
.await
.expect("write_next_chunk failed");
assert!(
!has_more,
"expected write_next_chunk to return false for small file"
);
assert!(
(upload.progress().fraction() - 1.0).abs() < f64::EPSILON,
"expected progress to reach 1.0, got {}",
upload.progress().fraction()
);
println!("Small file upload complete (compound, no chunks needed)");
drop(upload);
let readback = client
.read_file(&mut tree, test_path)
.await
.expect("read_file failed");
assert_eq!(readback, test_data.as_slice(), "content mismatch");
println!("Read back verified: {} bytes match", readback.len());
client
.delete_file(&mut tree, test_path)
.await
.expect("delete_file failed");
client
.disconnect_share(&tree)
.await
.expect("disconnect failed");
}
async fn connect_client_to_pi() -> SmbClient {
SmbClient::connect(ClientConfig {
addr: "192.168.1.150:445".to_string(),
timeout: Duration::from_secs(5),
username: String::new(),
password: String::new(),
domain: String::new(),
auto_reconnect: false,
compression: true,
dfs_enabled: true,
dfs_target_overrides: std::collections::HashMap::new(),
})
.await
.expect("SmbClient::connect to Pi failed")
}
#[tokio::test]
#[ignore]
async fn streaming_upload_and_download_on_pi() {
let _ = env_logger::try_init();
let mut client = connect_client_to_pi().await;
let mut tree = client
.connect_share("PiHDD")
.await
.expect("connect_share failed");
let test_path = "smb2_test_stream_roundtrip.tmp";
let test_data = b"streaming roundtrip test on Pi 1234567890";
let mut upload = client
.upload(&tree, test_path, test_data)
.await
.expect("upload failed");
while upload
.write_next_chunk()
.await
.expect("write_next_chunk failed")
{
println!(" upload progress: {:.1}%", upload.progress().percent());
}
println!(
"Upload complete: {} bytes, progress={:.1}%",
upload.total_bytes(),
upload.progress().percent()
);
drop(upload);
let received = client
.read_file(&mut tree, test_path)
.await
.expect("read_file failed");
assert_eq!(
received,
test_data.as_slice(),
"content mismatch after roundtrip"
);
println!("Roundtrip verified: {} bytes match", received.len());
let _ = client.delete_file(&mut tree, test_path).await;
let _ = client.disconnect_share(&tree).await;
}
#[tokio::test]
#[ignore]
async fn fs_info_on_nas() {
let _ = env_logger::try_init();
let mut client = connect_client_to_nas().await;
let mut tree = client
.connect_share("naspi")
.await
.expect("connect_share failed");
let info = client.fs_info(&mut tree).await.expect("fs_info failed");
assert!(info.total_bytes > 0, "total_bytes should be positive");
assert!(info.free_bytes > 0, "free_bytes should be positive");
assert!(
info.free_bytes <= info.total_bytes,
"free_bytes should not exceed total_bytes"
);
assert!(
info.total_free_bytes > 0,
"total_free_bytes should be positive"
);
assert!(
info.bytes_per_sector > 0,
"bytes_per_sector should be positive"
);
assert!(
info.sectors_per_unit > 0,
"sectors_per_unit should be positive"
);
let total_gb = info.total_bytes as f64 / 1_000_000_000.0;
let free_gb = info.free_bytes as f64 / 1_000_000_000.0;
println!(
"QNAP: {:.1} GB total, {:.1} GB free ({:.1}% used)",
total_gb,
free_gb,
(1.0 - free_gb / total_gb) * 100.0
);
println!(
" bytes_per_sector={}, sectors_per_unit={}",
info.bytes_per_sector, info.sectors_per_unit
);
let _ = client.disconnect_share(&tree).await;
}
#[tokio::test]
#[ignore]
async fn fs_info_on_pi() {
let _ = env_logger::try_init();
let mut client = connect_client_to_pi().await;
let mut tree = client
.connect_share("PiHDD")
.await
.expect("connect_share failed");
let info = client.fs_info(&mut tree).await.expect("fs_info failed");
assert!(info.total_bytes > 0, "total_bytes should be positive");
assert!(info.free_bytes > 0, "free_bytes should be positive");
assert!(
info.free_bytes <= info.total_bytes,
"free_bytes should not exceed total_bytes"
);
assert!(
info.total_free_bytes > 0,
"total_free_bytes should be positive"
);
assert!(
info.bytes_per_sector > 0,
"bytes_per_sector should be positive"
);
assert!(
info.sectors_per_unit > 0,
"sectors_per_unit should be positive"
);
let total_gb = info.total_bytes as f64 / 1_000_000_000.0;
let free_gb = info.free_bytes as f64 / 1_000_000_000.0;
println!(
"Pi: {:.1} GB total, {:.1} GB free ({:.1}% used)",
total_gb,
free_gb,
(1.0 - free_gb / total_gb) * 100.0
);
println!(
" bytes_per_sector={}, sectors_per_unit={}",
info.bytes_per_sector, info.sectors_per_unit
);
let _ = client.disconnect_share(&tree).await;
}
#[tokio::test]
#[ignore]
async fn watch_directory_on_nas() {
use smb2::FileNotifyAction;
let _ = env_logger::try_init();
let local = tokio::task::LocalSet::new();
local
.run_until(async {
let mut watcher_client = connect_client_to_nas().await;
let mut watcher_share = watcher_client
.connect_share("naspi")
.await
.expect("tree connect failed (watcher)");
let _ = watcher_client
.create_directory(&mut watcher_share, "_test")
.await;
let mut watcher = watcher_client
.watch(&watcher_share, "_test/", false)
.await
.expect("watch failed");
let test_file_path = "_test/smb2_watch_test.tmp";
let writer_task = tokio::task::spawn_local(async move {
let mut writer_client = connect_client_to_nas().await;
let mut writer_share = writer_client
.connect_share("naspi")
.await
.expect("tree connect failed (writer)");
tokio::time::sleep(Duration::from_millis(500)).await;
writer_client
.write_file(&mut writer_share, test_file_path, b"watch test")
.await
.expect("write_file failed");
println!("Writer: created {}", test_file_path);
(writer_client, writer_share)
});
let events = tokio::time::timeout(Duration::from_secs(10), watcher.next_events())
.await
.expect("timed out waiting for change notification")
.expect("next_events failed");
println!("Received {} event(s):", events.len());
for event in &events {
println!(" {} {}", event.action, event.filename);
}
assert!(!events.is_empty(), "expected at least one event");
let added = events.iter().find(|e| e.action == FileNotifyAction::Added);
assert!(
added.is_some(),
"expected an Added event, got: {:?}",
events
.iter()
.map(|e| format!("{}: {}", e.action, e.filename))
.collect::<Vec<_>>()
);
watcher.close().await.expect("watcher close failed");
let (mut writer_client, mut writer_share) = writer_task.await.unwrap();
writer_client
.delete_file(&mut writer_share, test_file_path)
.await
.expect("delete_file failed");
println!("Cleaned up {}", test_file_path);
let _ = writer_client.disconnect_share(&writer_share).await;
})
.await;
}
#[tokio::test]
#[ignore]
async fn nas_accepts_stacked_change_notify() {
use smb2::FileNotifyAction;
let _ = env_logger::try_init();
const N_CYCLES: usize = 5;
const DIR: &str = "_test_stacked_notify";
let local = tokio::task::LocalSet::new();
local
.run_until(async {
let mut watcher_client = connect_client_to_nas().await;
let mut watcher_share = watcher_client
.connect_share("naspi")
.await
.expect("tree connect (watcher)");
for i in 0..N_CYCLES {
let _ = watcher_client
.delete_file(&mut watcher_share, &format!("{DIR}/file_{i:02}.tmp"))
.await;
}
let _ = watcher_client
.delete_directory(&mut watcher_share, DIR)
.await;
watcher_client
.create_directory(&mut watcher_share, DIR)
.await
.expect("create test dir");
let mut watcher = watcher_client
.watch(&watcher_share, &format!("{DIR}/"), false)
.await
.expect("watch");
let writer_task = tokio::task::spawn_local(async move {
let mut writer_client = connect_client_to_nas().await;
let mut writer_share = writer_client
.connect_share("naspi")
.await
.expect("tree connect (writer)");
for i in 0..N_CYCLES {
tokio::time::sleep(Duration::from_millis(300)).await;
writer_client
.write_file(&mut writer_share, &format!("{DIR}/file_{i:02}.tmp"), b"x")
.await
.unwrap_or_else(|e| panic!("write file_{i:02}.tmp: {e}"));
}
(writer_client, writer_share)
});
let mut seen = std::collections::HashSet::new();
let mut cycle = 0;
let overall_deadline = tokio::time::Instant::now() + Duration::from_secs(20);
while seen.len() < N_CYCLES && tokio::time::Instant::now() < overall_deadline {
let res =
tokio::time::timeout(Duration::from_secs(5), watcher.next_events()).await;
let events = match res {
Ok(Ok(events)) => events,
Ok(Err(e)) => panic!(
"next_events cycle #{cycle} returned error — likely the QNAP \
rejected a stacked CHANGE_NOTIFY on the same FileId. Error: {e:?}"
),
Err(_) => {
println!(
"cycle {cycle}: 5 s timeout waiting for next event (seen so far: {seen:?})"
);
cycle += 1;
continue;
}
};
if events.is_empty() {
println!("cycle {cycle}: <empty response>");
}
for e in events {
println!("cycle {cycle}: {} {}", e.action, e.filename);
if e.action == FileNotifyAction::Added && e.filename.starts_with("file_") {
seen.insert(e.filename);
}
}
cycle += 1;
}
watcher.close().await.expect("watcher close");
let (mut writer_client, mut writer_share) = writer_task.await.unwrap();
for i in 0..N_CYCLES {
let _ = writer_client
.delete_file(&mut writer_share, &format!("{DIR}/file_{i:02}.tmp"))
.await;
}
let _ = writer_client.delete_directory(&mut writer_share, DIR).await;
let _ = writer_client.disconnect_share(&writer_share).await;
assert_eq!(
seen.len(),
N_CYCLES,
"expected all {N_CYCLES} files' Added events; got {seen:?}. \
Missing events suggest either loss-window regressions or \
server-side rejection of stacked CHANGE_NOTIFY."
);
})
.await;
}
#[tokio::test]
#[ignore]
async fn kerberos_auth_against_docker_kdc() {
let _ = env_logger::try_init();
let mut conn = Connection::connect("127.0.0.1:10462", Duration::from_secs(5))
.await
.expect("failed to connect to Docker Samba");
conn.negotiate().await.expect("negotiate failed");
let params = conn.params().unwrap();
println!("Negotiated dialect: {}", params.dialect);
let credentials = smb2::KerberosCredentials {
username: "testuser".to_string(),
password: "testpass".to_string(),
realm: "TEST.LOCAL".to_string(),
kdc_address: "127.0.0.1:10088".to_string(),
};
let session = Session::setup_kerberos(&mut conn, &credentials, "localhost")
.await
.expect("Kerberos session setup failed");
println!("Kerberos session established: {}", session.session_id);
println!("Signing: {}", session.should_sign);
let tree = smb2::Tree::connect(&mut conn, "public")
.await
.expect("tree connect failed");
println!("Tree connected: {}", tree.tree_id);
let test_data = b"Kerberos auth works!";
tree.write_file(&mut conn, "krb_test.txt", test_data)
.await
.expect("write failed");
let read_back = tree
.read_file(&mut conn, "krb_test.txt")
.await
.expect("read failed");
assert_eq!(read_back, test_data);
println!("Kerberos auth verified: wrote and read file successfully");
let _ = tree.delete_file(&mut conn, "krb_test.txt").await;
let _ = tree.disconnect(&mut conn).await;
}
#[tokio::test]
#[ignore]
async fn kerberos_auth_against_aws_windows_ad() {
let _ = env_logger::try_init();
load_dotenv();
let server_ip = std::env::var("SMB2_TEST_AWS_AD_IP")
.expect("SMB2_TEST_AWS_AD_IP not set (public IP of the Windows AD DC)");
let server_hostname = std::env::var("SMB2_TEST_AWS_AD_HOSTNAME").expect(
"SMB2_TEST_AWS_AD_HOSTNAME not set (computer name of the DC, e.g. EC2AMAZ-XXXXXXX)",
);
println!("Connecting to AWS Windows AD at {}...", server_ip);
let mut conn = Connection::connect(&format!("{}:445", server_ip), Duration::from_secs(10))
.await
.expect("failed to connect to AWS Windows AD");
conn.negotiate().await.expect("negotiate failed");
let params = conn.params().unwrap();
println!("Negotiated dialect: {}", params.dialect);
println!("Max read size: {}", params.max_read_size);
println!("Signing required: {}", params.signing_required);
println!("Server GUID: {:?}", params.server_guid);
let credentials = smb2::KerberosCredentials {
username: "smbtest".to_string(),
password: "Kerberos!Test1".to_string(),
realm: "TEST.LOCAL".to_string(),
kdc_address: format!("{}:88", server_ip),
};
let spn_hostname = std::env::var("SMB2_TEST_AWS_AD_SPN")
.unwrap_or_else(|_| format!("{}.test.local", server_hostname.to_lowercase()));
println!("Using SPN: cifs/{}", spn_hostname);
let session = Session::setup_kerberos(&mut conn, &credentials, &spn_hostname)
.await
.expect("Kerberos session setup failed");
println!("Kerberos session established: {}", session.session_id);
println!("Signing: {}", session.should_sign);
let tree = smb2::Tree::connect(&mut conn, "testshare")
.await
.expect("tree connect to testshare failed");
println!("Tree connected: {}", tree.tree_id);
let test_data = b"Kerberos auth works on real Windows AD!";
tree.write_file(&mut conn, "krb_test.txt", test_data)
.await
.expect("write failed");
let read_back = tree
.read_file(&mut conn, "krb_test.txt")
.await
.expect("read failed");
assert_eq!(read_back, test_data);
println!("Kerberos auth verified: wrote and read file on Windows AD");
let _ = tree.delete_file(&mut conn, "krb_test.txt").await;
let _ = tree.disconnect(&mut conn).await;
}
#[tokio::test]
#[ignore]
async fn kerberos_auth_from_ccache() {
let _ = env_logger::try_init();
load_dotenv();
let server_ip = std::env::var("SMB2_TEST_AWS_AD_IP").expect("SMB2_TEST_AWS_AD_IP not set");
let server_hostname =
std::env::var("SMB2_TEST_AWS_AD_HOSTNAME").expect("SMB2_TEST_AWS_AD_HOSTNAME not set");
let ccache_path = std::env::var("SMB2_TEST_CCACHE")
.expect("SMB2_TEST_CCACHE not set (path to a file-based Kerberos ccache)");
let spn_hostname = std::env::var("SMB2_TEST_AWS_AD_SPN")
.unwrap_or_else(|_| format!("{}.test.local", server_hostname.to_lowercase()));
println!("Loading ccache from {}...", ccache_path);
let ccache_data = std::fs::read(&ccache_path).expect("failed to read ccache file");
let ccache =
smb2::auth::kerberos::ccache::parse_ccache(&ccache_data).expect("failed to parse ccache");
println!(
"Ccache: principal={}@{}, {} credentials",
ccache.default_principal.components.join("/"),
ccache.default_principal.realm,
ccache.credentials.len()
);
let mut conn = Connection::connect(&format!("{}:445", server_ip), Duration::from_secs(10))
.await
.expect("failed to connect");
conn.negotiate().await.expect("negotiate failed");
println!("Negotiated dialect: {}", conn.params().unwrap().dialect);
let credentials = smb2::KerberosCredentials {
username: ccache.default_principal.components[0].clone(),
password: String::new(), realm: ccache.default_principal.realm.clone(),
kdc_address: format!("{}:88", server_ip),
};
let session =
Session::setup_kerberos_from_ccache(&mut conn, &credentials, &spn_hostname, &ccache)
.await
.expect("Kerberos session setup from ccache failed");
println!("Kerberos session from ccache: {}", session.session_id);
let tree = smb2::Tree::connect(&mut conn, "testshare")
.await
.expect("tree connect failed");
let test_data = b"Kerberos from ccache works!";
tree.write_file(&mut conn, "ccache_test.txt", test_data)
.await
.expect("write failed");
let read_back = tree
.read_file(&mut conn, "ccache_test.txt")
.await
.expect("read failed");
assert_eq!(read_back, test_data);
println!("Kerberos ccache auth verified: wrote and read file");
let _ = tree.delete_file(&mut conn, "ccache_test.txt").await;
let _ = tree.disconnect(&mut conn).await;
}
#[tokio::test]
#[ignore]
async fn streamed_write_on_nas() {
let _ = env_logger::try_init();
let (mut conn, tree) = connect_to_nas().await;
let test_path = "smb2_test_streamed_write.tmp";
let total_size = 10 * 1024 * 1024usize; let chunk_size = 256 * 1024;
let test_data: Vec<u8> = (0..total_size).map(|i| (i % 199) as u8).collect();
let mut offset = 0usize;
let data_ref = &test_data;
let mut next_chunk = move || -> Option<Result<Vec<u8>, std::io::Error>> {
if offset >= data_ref.len() {
return None;
}
let end = (offset + chunk_size).min(data_ref.len());
let chunk = data_ref[offset..end].to_vec();
offset = end;
Some(Ok(chunk))
};
let start = std::time::Instant::now();
let written = tree
.write_file_streamed(&mut conn, test_path, &mut next_chunk)
.await
.expect("write_file_streamed failed");
let elapsed = start.elapsed();
assert_eq!(written, total_size as u64);
println!(
"Streamed write: {} bytes in {:.2?} ({:.1} MB/s)",
written,
elapsed,
written as f64 / (1024.0 * 1024.0) / elapsed.as_secs_f64()
);
let data = tree
.read_file_pipelined(&mut conn, test_path)
.await
.expect("read_file_pipelined failed");
assert_eq!(data.len(), test_data.len(), "size mismatch");
assert_eq!(data, test_data, "content mismatch");
tree.delete_file(&mut conn, test_path)
.await
.expect("delete_file failed");
tree.disconnect(&mut conn).await.expect("disconnect failed");
}
#[tokio::test]
#[ignore]
async fn streamed_write_performance_vs_pipelined() {
let _ = env_logger::try_init();
let total_size = 10 * 1024 * 1024usize; let chunk_size = 256 * 1024;
let test_data: Vec<u8> = (0..total_size).map(|i| (i % 199) as u8).collect();
let (mut conn1, tree1) = connect_to_nas().await;
let streamed_path = "smb2_test_streamed_perf.tmp";
let mut offset = 0usize;
let data_ref = &test_data;
let mut next_chunk = move || -> Option<Result<Vec<u8>, std::io::Error>> {
if offset >= data_ref.len() {
return None;
}
let end = (offset + chunk_size).min(data_ref.len());
let chunk = data_ref[offset..end].to_vec();
offset = end;
Some(Ok(chunk))
};
let start_streamed = std::time::Instant::now();
let written_streamed = tree1
.write_file_streamed(&mut conn1, streamed_path, &mut next_chunk)
.await
.expect("write_file_streamed failed");
let elapsed_streamed = start_streamed.elapsed();
tree1
.delete_file(&mut conn1, streamed_path)
.await
.expect("delete_file failed");
tree1
.disconnect(&mut conn1)
.await
.expect("disconnect failed");
let (mut conn2, tree2) = connect_to_nas().await;
let pipelined_path = "smb2_test_pipelined_perf.tmp";
let start_pipelined = std::time::Instant::now();
let written_pipelined = tree2
.write_file_pipelined(&mut conn2, pipelined_path, &test_data)
.await
.expect("write_file_pipelined failed");
let elapsed_pipelined = start_pipelined.elapsed();
tree2
.delete_file(&mut conn2, pipelined_path)
.await
.expect("delete_file failed");
tree2
.disconnect(&mut conn2)
.await
.expect("disconnect failed");
let streamed_mbps =
written_streamed as f64 / (1024.0 * 1024.0) / elapsed_streamed.as_secs_f64();
let pipelined_mbps =
written_pipelined as f64 / (1024.0 * 1024.0) / elapsed_pipelined.as_secs_f64();
println!(
"Streamed: {} bytes in {:.2?} ({:.1} MB/s)",
written_streamed, elapsed_streamed, streamed_mbps
);
println!(
"Pipelined: {} bytes in {:.2?} ({:.1} MB/s)",
written_pipelined, elapsed_pipelined, pipelined_mbps
);
println!(
"Ratio: streamed is {:.1}x vs pipelined",
streamed_mbps / pipelined_mbps
);
}
#[tokio::test]
#[ignore]
async fn bench_100_tiny_files_seq_vs_parallel() {
let _ = env_logger::try_init();
const FILE_COUNT: usize = 100;
const FILE_SIZE: usize = 10 * 1024; const PARALLEL_CONNS: usize = 10;
const BENCH_DIR: &str = "_test/bench_100tiny";
{
let mut client = connect_client_to_nas().await;
let mut share = client
.connect_share("naspi")
.await
.expect("connect_share setup");
let _ = client.create_directory(&mut share, BENCH_DIR).await;
let data = vec![0x42u8; FILE_SIZE];
let setup_start = std::time::Instant::now();
let mut uploaded = 0usize;
for i in 0..FILE_COUNT {
let path = format!("{}/f_{:03}.bin", BENCH_DIR, i);
let already_there = match client.stat(&mut share, &path).await {
Ok(info) => info.size as usize == FILE_SIZE,
Err(_) => false,
};
if !already_there {
client
.write_file(&mut share, &path, &data)
.await
.expect("upload");
uploaded += 1;
}
}
println!(
"Setup: {}/{} files uploaded ({} already present), took {:.2?}",
uploaded,
FILE_COUNT,
FILE_COUNT - uploaded,
setup_start.elapsed()
);
let _ = client.disconnect_share(&share).await;
}
let seq_elapsed = {
let mut client = connect_client_to_nas().await;
let mut share = client
.connect_share("naspi")
.await
.expect("connect_share seq");
let start = std::time::Instant::now();
let mut total_bytes = 0usize;
for i in 0..FILE_COUNT {
let path = format!("{}/f_{:03}.bin", BENCH_DIR, i);
let d = client.read_file(&mut share, &path).await.expect("seq read");
assert_eq!(d.len(), FILE_SIZE);
total_bytes += d.len();
}
let elapsed = start.elapsed();
let _ = client.disconnect_share(&share).await;
assert_eq!(total_bytes, FILE_COUNT * FILE_SIZE);
elapsed
};
let seq_fps = FILE_COUNT as f64 / seq_elapsed.as_secs_f64();
let conn_setup_start = std::time::Instant::now();
let mut clients: Vec<(SmbClient, _)> = Vec::with_capacity(PARALLEL_CONNS);
for _ in 0..PARALLEL_CONNS {
let mut c = connect_client_to_nas().await;
let s = c.connect_share("naspi").await.expect("connect_share par");
clients.push((c, s));
}
let conn_setup = conn_setup_start.elapsed();
let par_start = std::time::Instant::now();
let mut tasks = Vec::with_capacity(PARALLEL_CONNS);
let per_conn = FILE_COUNT / PARALLEL_CONNS; for (idx, (mut client, mut share)) in clients.into_iter().enumerate() {
let task = tokio::spawn(async move {
let mut bytes = 0usize;
for j in 0..per_conn {
let file_idx = idx * per_conn + j;
let path = format!("{}/f_{:03}.bin", BENCH_DIR, file_idx);
let d = client.read_file(&mut share, &path).await.expect("par read");
bytes += d.len();
}
let _ = client.disconnect_share(&share).await;
bytes
});
tasks.push(task);
}
let mut par_bytes = 0usize;
for t in tasks {
par_bytes += t.await.expect("task join");
}
let par_elapsed = par_start.elapsed();
assert_eq!(par_bytes, FILE_COUNT * FILE_SIZE);
let par_fps = FILE_COUNT as f64 / par_elapsed.as_secs_f64();
let p3_conn_setup_start = std::time::Instant::now();
let mut p3_conn = Connection::connect("192.168.1.111:445", Duration::from_secs(5))
.await
.expect("p3 Connection::connect");
p3_conn.negotiate().await.expect("p3 negotiate");
let _p3_session = Session::setup(&mut p3_conn, "david", &nas_password(), "")
.await
.expect("p3 session setup");
let p3_tree = std::sync::Arc::new(
Tree::connect(&mut p3_conn, "naspi")
.await
.expect("p3 tree connect"),
);
let p3_conn_setup = p3_conn_setup_start.elapsed();
let p3_start = std::time::Instant::now();
let mut p3_tasks = Vec::with_capacity(PARALLEL_CONNS);
for task_idx in 0..PARALLEL_CONNS {
let mut conn_clone = p3_conn.clone();
let tree = std::sync::Arc::clone(&p3_tree);
let task = tokio::spawn(async move {
let mut bytes = 0usize;
for j in 0..per_conn {
let file_idx = task_idx * per_conn + j;
let path = format!("{}/f_{:03}.bin", BENCH_DIR, file_idx);
let d = tree
.read_file_compound(&mut conn_clone, &path)
.await
.expect("p3 read");
bytes += d.len();
}
bytes
});
p3_tasks.push(task);
}
let mut p3_bytes = 0usize;
for t in p3_tasks {
p3_bytes += t.await.expect("p3 task join");
}
let p3_elapsed = p3_start.elapsed();
assert_eq!(p3_bytes, FILE_COUNT * FILE_SIZE);
let p3_fps = FILE_COUNT as f64 / p3_elapsed.as_secs_f64();
let speedup = seq_elapsed.as_secs_f64() / par_elapsed.as_secs_f64();
let p3_speedup = seq_elapsed.as_secs_f64() / p3_elapsed.as_secs_f64();
println!();
println!("─────────────────────────────────────────────────────────");
println!("100 × 10 KB file read benchmark against QNAP");
println!("─────────────────────────────────────────────────────────");
println!(
"Sequential (1 conn): {:>8.2?} = {:>6.1} files/sec",
seq_elapsed, seq_fps
);
println!(
"Phase 3 (1 conn, {} clones): {:>8.2?} = {:>6.1} files/sec (setup {:.2?} extra)",
PARALLEL_CONNS, p3_elapsed, p3_fps, p3_conn_setup
);
println!(
"Parallel ({} conns): {:>8.2?} = {:>6.1} files/sec (setup {:.2?} extra)",
PARALLEL_CONNS, par_elapsed, par_fps, conn_setup
);
println!(
"Phase 3 speedup: {:>8.1}x (single-session concurrent execute)",
p3_speedup
);
println!(
"Parallel speedup: {:>8.1}x (upper bound — multi-session, Phase 4 cmdr-side)",
speedup
);
println!("─────────────────────────────────────────────────────────");
}