use std::path::Path; use std::time::Duration; /// Helper to create an S3Vfs instance pointing to local MinIO. fn make_s3_vfs() -> markbase_core::vfs::s3_fs::S3Vfs { markbase_core::vfs::s3_fs::S3Vfs::new( "http://127.0.0.1:9000", "us-east-1", "test-bucket", "minioadmin", "minioadmin", ) .expect("Failed to create S3Vfs") } /// Helper to check if MinIO is reachable (skip test if not). fn minio_reachable() -> bool { use std::net::TcpStream; TcpStream::connect_timeout( &"127.0.0.1:9000".parse().unwrap(), Duration::from_secs(2), ) .is_ok() } fn cleanup(vfs: &impl markbase_core::vfs::VfsBackend, path: &Path) { if vfs.exists(path) { let _ = vfs.remove_file(path); } // Try parent dirs let mut p = path.parent(); while let Some(dir) = p { if dir == Path::new("/") { break; } let dk = format!("{}/", dir.display()); let _ = vfs.remove_dir(Path::new(&dk)); p = dir.parent(); } } #[test] fn test_create_dir() { if !minio_reachable() { eprintln!("MinIO not reachable, skipping test_create_dir"); return; } let vfs = make_s3_vfs(); let dir = Path::new("/test-create-dir/"); if vfs.exists(dir) { vfs.remove_dir(dir).ok(); } assert!(!vfs.exists(dir)); vfs.create_dir(dir, 0o755).expect("create_dir failed"); assert!(vfs.exists(dir)); let stat = vfs.stat(dir).expect("stat failed"); assert!(stat.is_dir); vfs.remove_dir(dir).expect("remove_dir failed"); assert!(!vfs.exists(dir)); } #[test] fn test_create_dir_all() { if !minio_reachable() { eprintln!("MinIO not reachable, skipping test_create_dir_all"); return; } let vfs = make_s3_vfs(); let nested = Path::new("/test-nested/a/b/c/"); // Clean up from previous runs for d in [Path::new("/test-nested/a/b/c/"), Path::new("/test-nested/a/b/"), Path::new("/test-nested/a/"), Path::new("/test-nested/")] { if vfs.exists(d) { vfs.remove_dir(d).ok(); } } vfs.create_dir_all(nested, 0o755).expect("create_dir_all failed"); assert!(vfs.exists(Path::new("/test-nested/"))); assert!(vfs.exists(Path::new("/test-nested/a/"))); assert!(vfs.exists(Path::new("/test-nested/a/b/"))); assert!(vfs.exists(Path::new("/test-nested/a/b/c/"))); for d in [Path::new("/test-nested/a/b/c/"), Path::new("/test-nested/a/b/"), Path::new("/test-nested/a/"), Path::new("/test-nested/")] { vfs.remove_dir(d).expect("cleanup remove_dir failed"); } } #[test] fn test_write_and_read_file() { if !minio_reachable() { eprintln!("MinIO not reachable, skipping test_write_and_read_file"); return; } let vfs = make_s3_vfs(); let file_path = Path::new("/test-write-read.txt"); cleanup(&vfs, file_path); use markbase_core::vfs::open_flags::OpenFlags; // Write let mut f = vfs .open_file(file_path, &OpenFlags::write()) .expect("open for write failed"); let content = b"Hello S3Vfs! This is a test."; f.write_all(content).expect("write_all failed"); f.flush().expect("flush failed"); // Stat let stat = vfs.stat(file_path).expect("stat failed"); assert_eq!(stat.size, content.len() as u64); assert!(!stat.is_dir); // Read let mut f = vfs .open_file(file_path, &OpenFlags::read()) .expect("open for read failed"); let mut buf = vec![0u8; content.len()]; f.read_exact(&mut buf).expect("read_exact failed"); assert_eq!(buf, content); // Exists assert!(vfs.exists(file_path)); // Cleanup vfs.remove_file(file_path).expect("remove_file failed"); assert!(!vfs.exists(file_path)); } #[test] fn test_read_dir() { if !minio_reachable() { eprintln!("MinIO not reachable, skipping test_read_dir"); return; } let vfs = make_s3_vfs(); let dir = Path::new("/test-readdir/"); let file1 = Path::new("/test-readdir/file1.txt"); let file2 = Path::new("/test-readdir/file2.txt"); let subdir = Path::new("/test-readdir/sub/"); // Clean for p in &[file1, file2] { cleanup(&vfs, p); } if vfs.exists(subdir) { vfs.remove_dir(subdir).ok(); } if vfs.exists(dir) { vfs.remove_dir(dir).ok(); } // Create structure vfs.create_dir(dir, 0o755).expect("create_dir failed"); let content = b"data"; let flags = markbase_core::vfs::open_flags::OpenFlags::write(); for p in &[file1, file2] { let mut f = vfs.open_file(p, &flags).expect("open for write failed"); f.write_all(content).expect("write_all failed"); f.flush().expect("flush failed"); } vfs.create_dir(subdir, 0o755).expect("create subdir failed"); // Read directory let entries = vfs.read_dir(dir).expect("read_dir failed"); let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect(); assert!(names.contains(&"file1.txt"), "missing file1.txt: {:?}", names); assert!(names.contains(&"file2.txt"), "missing file2.txt: {:?}", names); assert!(names.contains(&"sub"), "missing sub: {:?}", names); // Check entry types for entry in &entries { if entry.name == "file1.txt" || entry.name == "file2.txt" { assert!(!entry.stat.is_dir, "{} should be a file", entry.name); } if entry.name == "sub" { assert!(entry.stat.is_dir, "{} should be a dir", entry.name); } } // Cleanup vfs.remove_file(file1).ok(); vfs.remove_file(file2).ok(); vfs.remove_dir(subdir).ok(); vfs.remove_dir(dir).ok(); } #[test] fn test_rename() { if !minio_reachable() { eprintln!("MinIO not reachable, skipping test_rename"); return; } let vfs = make_s3_vfs(); let from = Path::new("/test-rename-src.txt"); let to = Path::new("/test-rename-dst.txt"); cleanup(&vfs, from); cleanup(&vfs, to); let content = b"rename test data"; let flags = markbase_core::vfs::open_flags::OpenFlags::write(); { let mut f = vfs.open_file(from, &flags).expect("open for write"); f.write_all(content).expect("write_all"); f.flush().expect("flush"); } assert!(vfs.exists(from)); vfs.rename(from, to).expect("rename failed"); assert!(!vfs.exists(from)); assert!(vfs.exists(to)); // Verify content let mut f = vfs .open_file(to, &markbase_core::vfs::open_flags::OpenFlags::read()) .expect("open for read"); let mut buf = vec![0u8; content.len()]; f.read_exact(&mut buf).expect("read_exact"); assert_eq!(buf, content); vfs.remove_file(to).ok(); } #[test] fn test_hard_link() { if !minio_reachable() { eprintln!("MinIO not reachable, skipping test_hard_link"); return; } let vfs = make_s3_vfs(); let original = Path::new("/test-link-orig.txt"); let link = Path::new("/test-link-copy.txt"); cleanup(&vfs, original); cleanup(&vfs, link); let content = b"hard link test"; let flags = markbase_core::vfs::open_flags::OpenFlags::write(); { let mut f = vfs.open_file(original, &flags).expect("open for write"); f.write_all(content).expect("write_all"); f.flush().expect("flush"); } vfs.hard_link(original, link).expect("hard_link failed"); assert!(vfs.exists(original)); assert!(vfs.exists(link)); // Verify link content let mut f = vfs .open_file(link, &markbase_core::vfs::open_flags::OpenFlags::read()) .expect("open for read"); let mut buf = vec![0u8; content.len()]; f.read_exact(&mut buf).expect("read_exact"); assert_eq!(buf, content); vfs.remove_file(original).ok(); vfs.remove_file(link).ok(); } #[test] fn test_remove_dir_not_empty() { if !minio_reachable() { eprintln!("MinIO not reachable, skipping test_remove_dir_not_empty"); return; } let vfs = make_s3_vfs(); let dir = Path::new("/test-nonempty/"); let file = Path::new("/test-nonempty/file.txt"); cleanup(&vfs, file); if vfs.exists(dir) { vfs.remove_dir(dir).ok(); } vfs.create_dir(dir, 0o755).expect("create_dir"); let flags = markbase_core::vfs::open_flags::OpenFlags::write(); { let mut f = vfs.open_file(file, &flags).expect("open for write"); f.write_all(b"x").expect("write"); f.flush().expect("flush"); } match vfs.remove_dir(dir) { Err(markbase_core::vfs::VfsError::NotEmpty(_)) => { /* expected */ } other => panic!("Expected NotEmpty, got {:?}", other), } vfs.remove_file(file).ok(); vfs.remove_dir(dir).ok(); } #[test] fn test_real_path() { if !minio_reachable() { eprintln!("MinIO not reachable, skipping test_real_path"); return; } let vfs = make_s3_vfs(); let dir = Path::new("/test-realpath/"); let file = Path::new("/test-realpath/foo.txt"); cleanup(&vfs, file); if vfs.exists(dir) { vfs.remove_dir(dir).ok(); } vfs.create_dir(dir, 0o755).expect("create_dir"); let flags = markbase_core::vfs::open_flags::OpenFlags::write(); { let mut f = vfs.open_file(file, &flags).expect("open for write"); f.write_all(b"test").expect("write"); f.flush().expect("flush"); } let rp = vfs.real_path(dir).expect("real_path dir"); assert!( rp.to_string_lossy().ends_with("test-realpath"), "real_path dir: {:?}", rp ); let rp = vfs.real_path(file).expect("real_path file"); assert!( rp.to_string_lossy().ends_with("test-realpath/foo.txt"), "real_path file: {:?}", rp ); vfs.remove_file(file).ok(); vfs.remove_dir(dir).ok(); }