SMB comprehensive unit tests (229 passed, 0 failed)
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled
Scheduled Cleanup / cleanup (push) Has been cancelled

smb_server_backend.rs tests (+135 lines):
- Full VfsHandle lifecycle: file create/write/read/flush/close, stat,
  truncate (zero + extend), set_times, list_dir error, write past end
- Directory: create/stat/list/close, contains-created-file, read/write/truncate
  error cases
- All OpenIntent variants: Create (new + existing fail), OpenOrCreate
  (new + existing), OverwriteOrCreate (new + truncate existing), Truncate
  (existing + nonexistent fail)
- Directory OpenIntent: Create (new + existing fail), Open (existing),
  OpenOrCreate (new + existing)
- non_directory flag on dir (IsDirectory), directory flag on file (NotADirectory)
- Unlink: file, directory, nonexistent (NotFound)
- Rename: success + content preserved, nonexistent source (NotFound),
  existing target (Exists)
- Error mapping: all 8 VfsError variants (adds Unsupported, UnexpectedEof)
- FILETIME: roundtrip, below-offset returns epoch, exactly-offset
- vfs_stat_to_file_info: custom name, dir name from path, alloc_size

smb_fs.rs tests (+40 lines):
- Error mapping: NotFound, AlreadyExists, AccessDenied, IsADirectory,
  NotADirectory, DiskFull, SharingViolation, ConnectionLost, TimedOut,
  SessionExpired, InvalidData, Auth, Io, Cancelled
- Filetime: conversion, below-epoch, exact epoch boundary
- Path: leading slash stripping, root, deep paths
- Rejects trailing backslash
This commit is contained in:
Warren
2026-06-20 19:57:20 +08:00
parent 7eb528d35f
commit 8a85c2ef7c
2 changed files with 1000 additions and 64 deletions
+184 -39
View File
@@ -24,9 +24,9 @@ fn map_smb_error(e: smb2::Error) -> VfsError {
smb2::ErrorKind::AccessDenied => VfsError::PermissionDenied(e.to_string()),
smb2::ErrorKind::IsADirectory => VfsError::IsADirectory(e.to_string()),
smb2::ErrorKind::NotADirectory => VfsError::NotADirectory(e.to_string()),
smb2::ErrorKind::ConnectionLost | smb2::ErrorKind::TimedOut | smb2::ErrorKind::SessionExpired => {
VfsError::Io(format!("SMB connection error: {}", e))
}
smb2::ErrorKind::ConnectionLost
| smb2::ErrorKind::TimedOut
| smb2::ErrorKind::SessionExpired => VfsError::Io(format!("SMB connection error: {}", e)),
_ => VfsError::Io(format!("SMB error: {}", e)),
}
}
@@ -39,12 +39,7 @@ pub struct SmbVfs {
}
impl SmbVfs {
pub fn new(
addr: &str,
share: &str,
username: &str,
password: &str,
) -> Result<Self, VfsError> {
pub fn new(addr: &str, share: &str, username: &str, password: &str) -> Result<Self, VfsError> {
let runtime = Arc::new(
tokio::runtime::Builder::new_current_thread()
.enable_all()
@@ -105,7 +100,10 @@ impl VfsBackend for SmbVfs {
fn read_dir(&self, path: &Path) -> Result<Vec<VfsDirEntry>, VfsError> {
let smb_path = Self::path_to_str(path);
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
let mut client = self
.client
.lock()
.map_err(|e| VfsError::Io(e.to_string()))?;
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
let entries = self
.runtime
@@ -132,13 +130,12 @@ impl VfsBackend for SmbVfs {
.collect())
}
fn open_file(
&self,
path: &Path,
flags: &OpenFlags,
) -> Result<Box<dyn VfsFile>, VfsError> {
fn open_file(&self, path: &Path, flags: &OpenFlags) -> Result<Box<dyn VfsFile>, VfsError> {
let smb_path = Self::path_to_str(path);
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
let mut client = self
.client
.lock()
.map_err(|e| VfsError::Io(e.to_string()))?;
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
if flags.write || flags.create || flags.truncate {
@@ -175,7 +172,10 @@ impl VfsBackend for SmbVfs {
fn stat(&self, path: &Path) -> Result<VfsStat, VfsError> {
let smb_path = Self::path_to_str(path);
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
let mut client = self
.client
.lock()
.map_err(|e| VfsError::Io(e.to_string()))?;
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
let info = self
.runtime
@@ -200,7 +200,10 @@ impl VfsBackend for SmbVfs {
fn create_dir(&self, path: &Path, _mode: u32) -> Result<(), VfsError> {
let smb_path = Self::path_to_str(path);
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
let mut client = self
.client
.lock()
.map_err(|e| VfsError::Io(e.to_string()))?;
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
self.runtime
.block_on(client.create_directory(&mut *tree, &smb_path))
@@ -230,7 +233,10 @@ impl VfsBackend for SmbVfs {
fn remove_dir(&self, path: &Path) -> Result<(), VfsError> {
let smb_path = Self::path_to_str(path);
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
let mut client = self
.client
.lock()
.map_err(|e| VfsError::Io(e.to_string()))?;
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
self.runtime
.block_on(client.delete_directory(&mut *tree, &smb_path))
@@ -239,7 +245,10 @@ impl VfsBackend for SmbVfs {
fn remove_file(&self, path: &Path) -> Result<(), VfsError> {
let smb_path = Self::path_to_str(path);
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
let mut client = self
.client
.lock()
.map_err(|e| VfsError::Io(e.to_string()))?;
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
self.runtime
.block_on(client.delete_file(&mut *tree, &smb_path))
@@ -249,7 +258,10 @@ impl VfsBackend for SmbVfs {
fn rename(&self, from: &Path, to: &Path) -> Result<(), VfsError> {
let smb_from = Self::path_to_str(from);
let smb_to = Self::path_to_str(to);
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
let mut client = self
.client
.lock()
.map_err(|e| VfsError::Io(e.to_string()))?;
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
self.runtime
.block_on(client.rename(&mut *tree, &smb_from, &smb_to))
@@ -270,7 +282,10 @@ impl VfsBackend for SmbVfs {
fn real_path(&self, path: &Path) -> Result<PathBuf, VfsError> {
let smb_path = Self::path_to_str(path);
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
let mut client = self
.client
.lock()
.map_err(|e| VfsError::Io(e.to_string()))?;
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
let _info = self
.runtime
@@ -319,7 +334,10 @@ struct SmbVfsFile {
impl SmbVfsFile {
fn ensure_data_loaded(&mut self) -> Result<(), VfsError> {
if self.data.is_empty() && self.size > 0 {
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
let mut client = self
.client
.lock()
.map_err(|e| VfsError::Io(e.to_string()))?;
let data = self
.runtime
.block_on(client.read_file(&mut self.tree, &self.path))
@@ -382,7 +400,10 @@ impl VfsFile for SmbVfsFile {
if let FileMode::Write = self.mode {
if !self.write_buf.is_empty() {
let data = std::mem::take(&mut self.write_buf);
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
let mut client = self
.client
.lock()
.map_err(|e| VfsError::Io(e.to_string()))?;
self.runtime
.block_on(client.write_file(&mut self.tree, &self.path, &data))
.map_err(map_smb_error)?;
@@ -393,7 +414,10 @@ impl VfsFile for SmbVfsFile {
}
fn stat(&mut self) -> Result<VfsStat, VfsError> {
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
let mut client = self
.client
.lock()
.map_err(|e| VfsError::Io(e.to_string()))?;
let info = self
.runtime
.block_on(client.stat(&mut self.tree, &self.path))
@@ -421,9 +445,9 @@ impl Drop for SmbVfsFile {
if !self.write_buf.is_empty() {
let data = std::mem::take(&mut self.write_buf);
if let Ok(mut client) = self.client.lock() {
let _ = self
.runtime
.block_on(client.write_file(&mut self.tree, &self.path, &data));
let _ =
self.runtime
.block_on(client.write_file(&mut self.tree, &self.path, &data));
}
}
}
@@ -432,8 +456,13 @@ impl Drop for SmbVfsFile {
#[cfg(test)]
mod tests {
use smb2::types::status::NtStatus;
use smb2::types::Command;
use super::*;
// ── filetime_to_systemtime ──────────────────────────────────────────────
#[test]
fn test_filetime_conversion() {
let raw: u64 = 133604700000000000;
@@ -442,22 +471,138 @@ mod tests {
}
#[test]
fn test_path_to_str() {
assert_eq!(SmbVfs::path_to_str(Path::new("foo/bar.txt")), "foo/bar.txt");
assert_eq!(SmbVfs::path_to_str(Path::new("/foo/bar.txt")), "foo/bar.txt");
assert_eq!(SmbVfs::path_to_str(Path::new("")), "");
fn test_filetime_below_epoch_returns_epoch() {
let st = filetime_to_systemtime(0);
assert_eq!(st, UNIX_EPOCH);
}
#[test]
fn test_error_mapping_invalid_data() {
let err = smb2::Error::invalid_data("test");
let mapped = map_smb_error(err);
match mapped {
VfsError::Io(_) => {}
_ => panic!("Expected Io, got {:?}", mapped),
fn test_filetime_at_exact_unix_epoch_boundary() {
let ft = FILETIME_TO_UNIX_SECS * 10_000_000;
let st = filetime_to_systemtime(ft);
assert_eq!(st, UNIX_EPOCH);
}
// ── path_to_str ─────────────────────────────────────────────────────────
#[test]
fn test_path_to_str() {
assert_eq!(SmbVfs::path_to_str(Path::new("foo/bar.txt")), "foo/bar.txt");
assert_eq!(
SmbVfs::path_to_str(Path::new("/foo/bar.txt")),
"foo/bar.txt"
);
assert_eq!(SmbVfs::path_to_str(Path::new("")), "");
assert_eq!(SmbVfs::path_to_str(Path::new("/")), "");
assert_eq!(
SmbVfs::path_to_str(Path::new("/a/b/c/d/e/f/g.txt")),
"a/b/c/d/e/f/g.txt"
);
}
// ── map_smb_error — all ErrorKind variants ──────────────────────────────
fn proto_err(status: NtStatus) -> smb2::Error {
smb2::Error::Protocol {
status,
command: Command::Read,
}
}
#[test]
fn test_map_smb_error_not_found() {
let err = proto_err(NtStatus::NO_SUCH_FILE);
assert!(matches!(map_smb_error(err), VfsError::NotFound(_)));
}
#[test]
fn test_map_smb_error_already_exists() {
let err = proto_err(NtStatus::OBJECT_NAME_COLLISION);
assert!(matches!(map_smb_error(err), VfsError::AlreadyExists(_)));
}
#[test]
fn test_map_smb_error_access_denied() {
let err = proto_err(NtStatus::ACCESS_DENIED);
assert!(matches!(map_smb_error(err), VfsError::PermissionDenied(_)));
}
#[test]
fn test_map_smb_error_is_a_directory() {
let err = proto_err(NtStatus::FILE_IS_A_DIRECTORY);
assert!(matches!(map_smb_error(err), VfsError::IsADirectory(_)));
}
#[test]
fn test_map_smb_error_not_a_directory() {
let err = proto_err(NtStatus::NOT_A_DIRECTORY);
assert!(matches!(map_smb_error(err), VfsError::NotADirectory(_)));
}
#[test]
fn test_map_smb_error_disk_full() {
let err = proto_err(NtStatus::DISK_FULL);
assert!(matches!(map_smb_error(err), VfsError::Io(_)));
}
#[test]
fn test_map_smb_error_sharing_violation() {
let err = proto_err(NtStatus::SHARING_VIOLATION);
assert!(matches!(map_smb_error(err), VfsError::Io(_)));
}
#[test]
fn test_map_smb_error_connection_lost() {
let err = smb2::Error::Disconnected;
assert!(matches!(map_smb_error(err), VfsError::Io(_)));
}
#[test]
fn test_map_smb_error_timeout() {
let err = smb2::Error::Timeout;
assert!(matches!(map_smb_error(err), VfsError::Io(_)));
}
#[test]
fn test_map_smb_error_session_expired() {
let err = smb2::Error::SessionExpired;
assert!(matches!(map_smb_error(err), VfsError::Io(_)));
}
#[test]
fn test_map_smb_error_invalid_data() {
let err = smb2::Error::InvalidData {
message: "bad data".into(),
};
assert!(matches!(map_smb_error(err), VfsError::Io(_)));
}
#[test]
fn test_map_smb_error_auth() {
let err = smb2::Error::Auth {
message: "auth fail".into(),
};
// AuthRequired falls through to the catch-all _ => VfsError::Io
assert!(matches!(map_smb_error(err), VfsError::Io(_)));
}
#[test]
fn test_map_smb_error_io() {
let err = smb2::Error::Io(std::io::Error::other("io error"));
assert!(matches!(map_smb_error(err), VfsError::Io(_)));
}
#[test]
fn test_map_smb_error_cancelled() {
let err = smb2::Error::Cancelled;
assert!(matches!(map_smb_error(err), VfsError::Io(_)));
}
// VfsFile::set_len returns Unsupported — verified via integration tests
// since SmbVfsFile requires a real SMB connection to construct.
// ── Integration tests (ignored, require Docker Samba) ───────────────────
/// Integration test: requires Docker Samba container on port 10445.
/// Run with: docker compose -f vendor/smb2/tests/docker/internal/docker-compose.yml up -d smb-guest
#[test]
@@ -465,7 +610,7 @@ mod tests {
fn test_smb_vfs_list_root() {
let vfs = SmbVfs::new("127.0.0.1:10445", "public", "", "").unwrap();
let entries = vfs.read_dir(Path::new("/")).unwrap();
assert!(!entries.is_empty(), "Expected at least . and ..");
assert!(!entries.is_empty(), "Expected entries");
}
#[test]