Testing Guide
Comprehensive testing strategy for AnyFS
Overview
AnyFS uses a layered testing approach:
| Layer | What it tests | Run with |
|---|---|---|
| Unit tests | Individual components | cargo test |
| Conformance tests | Backend trait compliance | cargo test --features conformance |
| Integration tests | Full stack behavior | cargo test --test integration |
| Stress tests | Concurrency & limits | cargo test --release -- --ignored |
| Platform tests | Cross-platform behavior | CI matrix |
1. Backend Conformance Tests
Every backend must pass the same conformance suite. This ensures backends are interchangeable.
Running Conformance Tests
#![allow(unused)]
fn main() {
use anyfs_test::{run_conformance_suite, ConformanceLevel};
#[test]
fn memory_backend_conformance() {
run_conformance_suite(
MemoryBackend::new(),
ConformanceLevel::Fs, // or FsFull, FsFuse, FsPosix
);
}
#[test]
fn vrootfs_backend_conformance() {
let temp = tempfile::tempdir().unwrap();
run_conformance_suite(
VRootFsBackend::new(temp.path()).unwrap(),
ConformanceLevel::FsFull,
);
}
}
Conformance Levels
FsPosix ──▶ FsHandles, FsLock, FsXattr tests
│
FsFuse ──▶ FsInode tests (path_to_inode, lookup, etc.)
│
FsFull ──▶ FsLink, FsPermissions, FsSync, FsStats tests
│
Fs ──▶ FsRead, FsWrite, FsDir tests (REQUIRED for all)
Core Tests (Fs level)
#![allow(unused)]
fn main() {
#[test]
fn test_write_and_read() {
let backend = create_backend();
backend.write(std::path::Path::new("/file.txt"), b"hello world").unwrap();
let content = backend.read(std::path::Path::new("/file.txt")).unwrap();
assert_eq!(content, b"hello world");
}
#[test]
fn test_read_nonexistent_returns_not_found() {
let backend = create_backend();
let result = backend.read(std::path::Path::new("/nonexistent.txt"));
assert!(matches!(result, Err(FsError::NotFound { .. })));
}
#[test]
fn test_create_dir_and_list() {
let backend = create_backend();
backend.create_dir(std::path::Path::new("/mydir")).unwrap();
backend.write(std::path::Path::new("/mydir/file.txt"), b"data").unwrap();
let entries: Vec<_> = backend.read_dir(std::path::Path::new("/mydir")).unwrap()
.collect::<Result<Vec<_>, _>>().unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "file.txt");
}
#[test]
fn test_create_dir_all() {
let backend = create_backend();
backend.create_dir_all(std::path::Path::new("/a/b/c/d")).unwrap();
assert!(backend.exists(std::path::Path::new("/a/b/c/d")).unwrap());
}
#[test]
fn test_remove_file() {
let backend = create_backend();
backend.write(std::path::Path::new("/file.txt"), b"data").unwrap();
backend.remove_file(std::path::Path::new("/file.txt")).unwrap();
assert!(!backend.exists(std::path::Path::new("/file.txt")).unwrap());
}
#[test]
fn test_remove_dir_all() {
let backend = create_backend();
backend.create_dir_all(std::path::Path::new("/a/b/c")).unwrap();
backend.write(std::path::Path::new("/a/b/c/file.txt"), b"data").unwrap();
backend.remove_dir_all(std::path::Path::new("/a")).unwrap();
assert!(!backend.exists(std::path::Path::new("/a")).unwrap());
}
#[test]
fn test_rename() {
let backend = create_backend();
backend.write(std::path::Path::new("/old.txt"), b"data").unwrap();
backend.rename(std::path::Path::new("/old.txt"), std::path::Path::new("/new.txt")).unwrap();
assert!(!backend.exists(std::path::Path::new("/old.txt")).unwrap());
assert_eq!(backend.read(std::path::Path::new("/new.txt")).unwrap(), b"data");
}
#[test]
fn test_copy() {
let backend = create_backend();
backend.write(std::path::Path::new("/original.txt"), b"data").unwrap();
backend.copy(std::path::Path::new("/original.txt"), std::path::Path::new("/copy.txt")).unwrap();
assert_eq!(backend.read(std::path::Path::new("/original.txt")).unwrap(), b"data");
assert_eq!(backend.read(std::path::Path::new("/copy.txt")).unwrap(), b"data");
}
#[test]
fn test_metadata() {
let backend = create_backend();
backend.write(std::path::Path::new("/file.txt"), b"hello").unwrap();
let meta = backend.metadata(std::path::Path::new("/file.txt")).unwrap();
assert_eq!(meta.size, 5);
assert!(meta.file_type.is_file());
}
#[test]
fn test_append() {
let backend = create_backend();
backend.write(std::path::Path::new("/file.txt"), b"hello").unwrap();
backend.append(std::path::Path::new("/file.txt"), b" world").unwrap();
assert_eq!(backend.read(std::path::Path::new("/file.txt")).unwrap(), b"hello world");
}
#[test]
fn test_truncate() {
let backend = create_backend();
backend.write(std::path::Path::new("/file.txt"), b"hello world").unwrap();
backend.truncate(std::path::Path::new("/file.txt"), 5).unwrap();
assert_eq!(backend.read(std::path::Path::new("/file.txt")).unwrap(), b"hello");
}
#[test]
fn test_read_range() {
let backend = create_backend();
backend.write(std::path::Path::new("/file.txt"), b"hello world").unwrap();
let partial = backend.read_range(std::path::Path::new("/file.txt"), 6, 5).unwrap();
assert_eq!(partial, b"world");
}
}
Extended Tests (FsFull level)
#![allow(unused)]
fn main() {
#[test]
fn test_symlink() {
let backend = create_backend();
backend.write(std::path::Path::new("/target.txt"), b"data").unwrap();
backend.symlink(std::path::Path::new("/target.txt"), std::path::Path::new("/link.txt")).unwrap();
// read_link returns the target
assert_eq!(backend.read_link(std::path::Path::new("/link.txt")).unwrap(), Path::new("/target.txt"));
// reading the symlink follows it
assert_eq!(backend.read(std::path::Path::new("/link.txt")).unwrap(), b"data");
}
#[test]
fn test_hard_link() {
let backend = create_backend();
backend.write(std::path::Path::new("/original.txt"), b"data").unwrap();
backend.hard_link(std::path::Path::new("/original.txt"), std::path::Path::new("/hardlink.txt")).unwrap();
// Both point to same data
assert_eq!(backend.read(std::path::Path::new("/hardlink.txt")).unwrap(), b"data");
// Metadata shows nlink > 1
let meta = backend.metadata(std::path::Path::new("/original.txt")).unwrap();
assert!(meta.nlink >= 2);
}
#[test]
fn test_symlink_metadata() {
let backend = create_backend();
backend.write(std::path::Path::new("/target.txt"), b"data").unwrap();
backend.symlink(std::path::Path::new("/target.txt"), std::path::Path::new("/link.txt")).unwrap();
// symlink_metadata returns metadata of the symlink itself
let meta = backend.symlink_metadata(std::path::Path::new("/link.txt")).unwrap();
assert!(meta.file_type.is_symlink());
}
#[test]
fn test_set_permissions() {
let backend = create_backend();
backend.write(std::path::Path::new("/file.txt"), b"data").unwrap();
backend.set_permissions(std::path::Path::new("/file.txt"), Permissions::from_mode(0o644)).unwrap();
let meta = backend.metadata(std::path::Path::new("/file.txt")).unwrap();
assert_eq!(meta.permissions.mode() & 0o777, 0o644);
}
#[test]
fn test_sync() {
let backend = create_backend();
backend.write(std::path::Path::new("/file.txt"), b"data").unwrap();
// Should not error
backend.sync().unwrap();
backend.fsync(std::path::Path::new("/file.txt")).unwrap();
}
#[test]
fn test_statfs() {
let backend = create_backend();
let stats = backend.statfs().unwrap();
assert!(stats.total_bytes > 0 || stats.total_bytes == 0); // Memory may report 0
}
}
2. Middleware Tests
Each middleware is tested in isolation and in combination.
Quota Tests
#![allow(unused)]
fn main() {
#[test]
fn test_quota_blocks_when_exceeded() {
let backend = MemoryBackend::new()
.layer(QuotaLayer::builder().max_total_size(100).build());
let fs = FileStorage::new(backend);
let result = fs.write("/big.txt", &[0u8; 200]);
assert!(matches!(result, Err(FsError::QuotaExceeded { .. })));
}
#[test]
fn test_quota_allows_within_limit() {
let backend = MemoryBackend::new()
.layer(QuotaLayer::builder().max_total_size(1000).build());
let fs = FileStorage::new(backend);
fs.write("/small.txt", &[0u8; 100]).unwrap();
assert!(fs.exists("/small.txt").unwrap());
}
#[test]
fn test_quota_tracks_deletes() {
let backend = MemoryBackend::new()
.layer(QuotaLayer::builder().max_total_size(100).build());
let fs = FileStorage::new(backend);
fs.write("/file.txt", &[0u8; 50]).unwrap();
fs.remove_file("/file.txt").unwrap();
// Should be able to write again after delete
fs.write("/file2.txt", &[0u8; 50]).unwrap();
}
#[test]
fn test_quota_max_file_size() {
let backend = MemoryBackend::new()
.layer(QuotaLayer::builder().max_file_size(50).build());
let fs = FileStorage::new(backend);
let result = fs.write("/big.txt", &[0u8; 100]);
assert!(matches!(result, Err(FsError::QuotaExceeded { .. })));
}
#[test]
fn test_quota_streaming_write() {
let backend = MemoryBackend::new()
.layer(QuotaLayer::builder().max_total_size(100).build());
let fs = FileStorage::new(backend);
let mut writer = fs.open_write("/file.txt").unwrap();
writer.write_all(&[0u8; 50]).unwrap();
writer.write_all(&[0u8; 50]).unwrap();
drop(writer);
// Next write should fail
let result = fs.write("/file2.txt", &[0u8; 10]);
assert!(matches!(result, Err(FsError::QuotaExceeded { .. })));
}
}
Restrictions Tests
#![allow(unused)]
fn main() {
#[test]
fn test_restrictions_blocks_permissions() {
let backend = MemoryBackend::new()
.layer(RestrictionsLayer::builder().deny_permissions().build());
let fs = FileStorage::new(backend);
fs.write("/file.txt", b"data").unwrap();
let result = fs.set_permissions("/file.txt", Permissions::from_mode(0o644));
assert!(matches!(result, Err(FsError::FeatureNotEnabled { .. })));
}
#[test]
fn test_restrictions_allows_links() {
// Restrictions doesn't block FsLink - capability is via trait bounds
let backend = MemoryBackend::new()
.layer(RestrictionsLayer::builder().deny_permissions().build());
let fs = FileStorage::new(backend);
fs.write("/target.txt", b"data").unwrap();
fs.symlink("/target.txt", "/link.txt").unwrap(); // Works - MemoryBackend: FsLink
fs.hard_link("/target.txt", "/hardlink.txt").unwrap(); // Works too
}
#[test]
fn test_restrictions_blocks_permissions() {
let backend = MemoryBackend::new()
.layer(RestrictionsLayer::builder().deny_permissions().build());
let fs = FileStorage::new(backend);
fs.write("/file.txt", b"data").unwrap();
let result = fs.set_permissions("/file.txt", Permissions::from_mode(0o777));
assert!(matches!(result, Err(FsError::FeatureNotEnabled { .. })));
}
}
PathFilter Tests
#![allow(unused)]
fn main() {
#[test]
fn test_pathfilter_allows_matching() {
let backend = MemoryBackend::new()
.layer(PathFilterLayer::builder().allow("/workspace/**").build());
let fs = FileStorage::new(backend);
fs.create_dir_all("/workspace/project").unwrap();
fs.write("/workspace/project/file.txt", b"data").unwrap();
}
#[test]
fn test_pathfilter_blocks_non_matching() {
let backend = MemoryBackend::new()
.layer(PathFilterLayer::builder().allow("/workspace/**").build());
let fs = FileStorage::new(backend);
let result = fs.write("/etc/passwd", b"data");
assert!(matches!(result, Err(FsError::AccessDenied { .. })));
}
#[test]
fn test_pathfilter_deny_overrides_allow() {
let backend = MemoryBackend::new()
.layer(PathFilterLayer::builder()
.allow("/workspace/**")
.deny("**/.env")
.build());
let fs = FileStorage::new(backend);
let result = fs.write("/workspace/.env", b"SECRET=xxx");
assert!(matches!(result, Err(FsError::AccessDenied { .. })));
}
#[test]
fn test_pathfilter_read_dir_filters() {
let mut inner = MemoryBackend::new();
inner.write(std::path::Path::new("/workspace/allowed.txt"), b"data").unwrap();
inner.write(std::path::Path::new("/workspace/.env"), b"secret").unwrap();
let backend = inner
.layer(PathFilterLayer::builder()
.allow("/workspace/**")
.deny("**/.env")
.build());
let fs = FileStorage::new(backend);
let entries: Vec<_> = fs.read_dir("/workspace").unwrap()
.collect::<Result<Vec<_>, _>>().unwrap();
// .env should be filtered out
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "allowed.txt");
}
}
ReadOnly Tests
#![allow(unused)]
fn main() {
#[test]
fn test_readonly_blocks_writes() {
let mut inner = MemoryBackend::new();
inner.write(std::path::Path::new("/file.txt"), b"original").unwrap();
let backend = ReadOnly::new(inner);
let fs = FileStorage::new(backend);
let result = fs.write("/file.txt", b"modified");
assert!(matches!(result, Err(FsError::ReadOnly { .. })));
let result = fs.remove_file("/file.txt");
assert!(matches!(result, Err(FsError::ReadOnly { .. })));
}
#[test]
fn test_readonly_allows_reads() {
let mut inner = MemoryBackend::new();
inner.write(std::path::Path::new("/file.txt"), b"data").unwrap();
let backend = ReadOnly::new(inner);
let fs = FileStorage::new(backend);
assert_eq!(fs.read("/file.txt").unwrap(), b"data");
}
}
Middleware Composition Tests
#![allow(unused)]
fn main() {
#[test]
fn test_middleware_composition_order() {
// Quota inside, Restrictions outside
let backend = MemoryBackend::new()
.layer(QuotaLayer::builder().max_total_size(100).build())
.layer(RestrictionsLayer::builder().deny_permissions().build());
let fs = FileStorage::new(backend);
// Write should hit quota
let result = fs.write("/big.txt", &[0u8; 200]);
assert!(matches!(result, Err(FsError::QuotaExceeded { .. })));
}
#[test]
fn test_layer_syntax() {
// All configurable middleware use builder pattern (per ADR-022)
let backend = MemoryBackend::new()
.layer(QuotaLayer::builder().max_total_size(1000).build())
.layer(RestrictionsLayer::builder().deny_permissions().build())
.layer(TracingLayer::new()); // TracingLayer has sensible defaults
let fs = FileStorage::new(backend);
fs.write("/test.txt", b"data").unwrap();
}
}
3. FileStorage Tests
#![allow(unused)]
fn main() {
#[test]
fn test_filestorage_type_inference() {
// Type should be inferred
let fs = FileStorage::new(MemoryBackend::new());
// No explicit type needed
}
#[test]
fn test_filestorage_wrapper_types() {
// Users who need type-safe domain separation create wrapper types
struct SandboxFs(FileStorage<MemoryBackend>);
struct ProductionFs(FileStorage<MemoryBackend>);
let sandbox = SandboxFs(FileStorage::new(MemoryBackend::new()));
let prod = ProductionFs(FileStorage::new(MemoryBackend::new()));
fn only_sandbox(_fs: &SandboxFs) {}
only_sandbox(&sandbox); // Compiles
// only_sandbox(&prod); // Would not compile - different type
}
}
4. Integration Tests (Real Filesystem)
Tests that use real filesystem backends (VRootFsBackend, tempfile) are integration tests, not unit tests.
#![allow(unused)]
fn main() {
#[test]
fn test_filestorage_boxed_with_real_fs() {
// This is an INTEGRATION test - uses real filesystem via tempfile
let fs1 = FileStorage::new(MemoryBackend::new()).boxed();
let temp = tempfile::tempdir().unwrap();
let fs2 = FileStorage::with_resolver(
VRootFsBackend::new(temp.path()).unwrap(),
NoOpResolver
).boxed();
// Both can be stored in same collection
let _filesystems: Vec<FileStorage<Box<dyn Fs>>> = vec![fs1, fs2];
}
}
5. Error Handling Tests
#![allow(unused)]
fn main() {
#[test]
fn test_error_not_found() {
let fs = FileStorage::new(MemoryBackend::new());
match fs.read("/nonexistent") {
Err(FsError::NotFound { path, operation }) => {
assert_eq!(path, Path::new("/nonexistent"));
assert_eq!(operation, "read");
}
_ => panic!("Expected NotFound error"),
}
}
#[test]
fn test_error_already_exists() {
let fs = FileStorage::new(MemoryBackend::new());
fs.create_dir("/mydir").unwrap();
match fs.create_dir("/mydir") {
Err(FsError::AlreadyExists { path, .. }) => {
assert_eq!(path, Path::new("/mydir"));
}
_ => panic!("Expected AlreadyExists error"),
}
}
#[test]
fn test_error_not_a_directory() {
let fs = FileStorage::new(MemoryBackend::new());
fs.write("/file.txt", b"data").unwrap();
match fs.read_dir("/file.txt") {
Err(FsError::NotADirectory { path }) => {
assert_eq!(path, Path::new("/file.txt"));
}
_ => panic!("Expected NotADirectory error"),
}
}
#[test]
fn test_error_directory_not_empty() {
let fs = FileStorage::new(MemoryBackend::new());
fs.create_dir("/mydir").unwrap();
fs.write("/mydir/file.txt", b"data").unwrap();
match fs.remove_dir("/mydir") {
Err(FsError::DirectoryNotEmpty { path }) => {
assert_eq!(path, Path::new("/mydir"));
}
_ => panic!("Expected DirectoryNotEmpty error"),
}
}
}
5. Concurrency Tests
#![allow(unused)]
fn main() {
#[test]
fn test_concurrent_reads() {
let backend = MemoryBackend::new();
backend.write(std::path::Path::new("/file.txt"), b"data").unwrap();
let backend = Arc::new(RwLock::new(backend));
let handles: Vec<_> = (0..10).map(|_| {
let backend = Arc::clone(&backend);
thread::spawn(move || {
let guard = backend.read().unwrap();
guard.read(std::path::Path::new("/file.txt")).unwrap()
})
}).collect();
for handle in handles {
assert_eq!(handle.join().unwrap(), b"data");
}
}
#[test]
fn test_concurrent_create_dir_all() {
let backend = Arc::new(Mutex::new(MemoryBackend::new()));
let handles: Vec<_> = (0..10).map(|_| {
let backend = Arc::clone(&backend);
thread::spawn(move || {
let mut guard = backend.lock().unwrap();
// Multiple threads creating same path should not race
guard.create_dir_all(std::path::Path::new("/a/b/c/d")).unwrap();
})
}).collect();
for handle in handles {
handle.join().unwrap();
}
assert!(backend.lock().unwrap().exists(std::path::Path::new("/a/b/c/d")).unwrap());
}
#[test]
#[ignore] // Run with: cargo test --release -- --ignored
fn stress_test_concurrent_operations() {
let backend = Arc::new(Mutex::new(MemoryBackend::new()));
let handles: Vec<_> = (0..100).map(|i| {
let backend = Arc::clone(&backend);
thread::spawn(move || {
for j in 0..100 {
let path = format!("/thread_{}/file_{}.txt", i, j);
let mut guard = backend.lock().unwrap();
guard.create_dir_all(std::path::Path::new(&format!("/thread_{}", i))).ok();
guard.write(std::path::Path::new(&path), b"data").unwrap();
drop(guard);
let guard = backend.lock().unwrap();
let _ = guard.read(std::path::Path::new(&path));
}
})
}).collect();
for handle in handles {
handle.join().unwrap();
}
}
}
6. Path Edge Case Tests
#![allow(unused)]
fn main() {
#[test]
fn test_path_normalization() {
let fs = FileStorage::new(MemoryBackend::new());
fs.write("/a/b/../c/file.txt", b"data").unwrap();
// Should be accessible via normalized path
assert_eq!(fs.read("/a/c/file.txt").unwrap(), b"data");
}
#[test]
fn test_double_slashes() {
let fs = FileStorage::new(MemoryBackend::new());
fs.write("//a//b//file.txt", b"data").unwrap();
assert_eq!(fs.read("/a/b/file.txt").unwrap(), b"data");
}
#[test]
fn test_root_path() {
let fs = FileStorage::new(MemoryBackend::new());
// ReadDirIter is an iterator, use collect_all() to check contents
let entries = fs.read_dir("/").unwrap().collect_all().unwrap();
assert!(entries.is_empty());
}
#[test]
fn test_empty_path_returns_error() {
let fs = FileStorage::new(MemoryBackend::new());
let result = fs.read("");
assert!(result.is_err());
}
#[test]
fn test_unicode_paths() {
let fs = FileStorage::new(MemoryBackend::new());
fs.write("/文件/データ.txt", b"data").unwrap();
assert_eq!(fs.read("/文件/データ.txt").unwrap(), b"data");
}
#[test]
fn test_paths_with_spaces() {
let fs = FileStorage::new(MemoryBackend::new());
fs.write("/my folder/my file.txt", b"data").unwrap();
assert_eq!(fs.read("/my folder/my file.txt").unwrap(), b"data");
}
}
7. No-Panic Guarantee Tests
#![allow(unused)]
fn main() {
#[test]
fn no_panic_missing_file() {
let fs = FileStorage::new(MemoryBackend::new());
let _ = fs.read("/missing"); // Should return Err, not panic
}
#[test]
fn no_panic_missing_parent() {
let fs = FileStorage::new(MemoryBackend::new());
let _ = fs.write("/missing/parent/file.txt", b"data"); // Should return Err
}
#[test]
fn no_panic_read_dir_on_file() {
let fs = FileStorage::new(MemoryBackend::new());
fs.write("/file.txt", b"data").unwrap();
let _ = fs.read_dir("/file.txt"); // Should return Err, not panic
}
#[test]
fn no_panic_remove_nonempty_dir() {
let fs = FileStorage::new(MemoryBackend::new());
fs.create_dir("/dir").unwrap();
fs.write("/dir/file.txt", b"data").unwrap();
let _ = fs.remove_dir("/dir"); // Should return Err, not panic
}
}
8. Symlink Security Tests
#![allow(unused)]
fn main() {
// Virtual backend symlink resolution (always follows for FsLink backends)
#[test]
fn test_virtual_backend_symlink_following() {
let backend = MemoryBackend::new();
backend.write(std::path::Path::new("/target.txt"), b"secret").unwrap();
backend.symlink(std::path::Path::new("/target.txt"), std::path::Path::new("/link.txt")).unwrap();
assert_eq!(backend.read(std::path::Path::new("/link.txt")).unwrap(), b"secret");
}
#[test]
fn test_symlink_chain_resolution() {
let backend = MemoryBackend::new();
backend.write(std::path::Path::new("/target.txt"), b"data").unwrap();
backend.symlink(std::path::Path::new("/target.txt"), std::path::Path::new("/link1.txt")).unwrap();
backend.symlink(std::path::Path::new("/link1.txt"), std::path::Path::new("/link2.txt")).unwrap();
// Should follow chain
assert_eq!(backend.read(std::path::Path::new("/link2.txt")).unwrap(), b"data");
}
#[test]
fn test_symlink_loop_detection() {
let backend = MemoryBackend::new();
backend.symlink(std::path::Path::new("/link2.txt"), std::path::Path::new("/link1.txt")).unwrap();
backend.symlink(std::path::Path::new("/link1.txt"), std::path::Path::new("/link2.txt")).unwrap();
let result = backend.read(std::path::Path::new("/link1.txt"));
assert!(matches!(result, Err(FsError::SymlinkLoop { .. })));
}
#[test]
fn test_virtual_symlink_cannot_escape() {
let backend = MemoryBackend::new();
// Create a symlink pointing "outside" - but in virtual backend, paths are just keys
backend.symlink(std::path::Path::new("../../../etc/passwd"), std::path::Path::new("/link.txt")).unwrap();
// Reading should fail (target doesn't exist), not read real /etc/passwd
let result = backend.read(std::path::Path::new("/link.txt"));
assert!(matches!(result, Err(FsError::NotFound { .. })));
}
}
VRootFsBackend Containment Tests
#![allow(unused)]
fn main() {
#[test]
fn test_vroot_prevents_path_traversal() {
let temp = tempfile::tempdir().unwrap();
let backend = VRootFsBackend::new(temp.path()).unwrap();
let fs = FileStorage::new(backend);
// Attempt to escape via ..
let result = fs.read("/../../../etc/passwd");
assert!(matches!(result, Err(FsError::AccessDenied { .. })));
}
#[test]
fn test_vroot_prevents_symlink_escape() {
let temp = tempfile::tempdir().unwrap();
std::fs::write(temp.path().join("file.txt"), b"data").unwrap();
// Create symlink pointing outside the jail
#[cfg(unix)]
std::os::unix::fs::symlink("/etc/passwd", temp.path().join("escape")).unwrap();
let backend = VRootFsBackend::new(temp.path()).unwrap();
let fs = FileStorage::new(backend);
// Reading should be blocked by strict-path
let result = fs.read("/escape");
assert!(matches!(result, Err(FsError::AccessDenied { .. })));
}
#[test]
fn test_vroot_allows_internal_symlinks() {
let temp = tempfile::tempdir().unwrap();
std::fs::write(temp.path().join("target.txt"), b"data").unwrap();
#[cfg(unix)]
std::os::unix::fs::symlink("target.txt", temp.path().join("link.txt")).unwrap();
let backend = VRootFsBackend::new(temp.path()).unwrap();
let fs = FileStorage::new(backend);
// Internal symlinks should work
assert_eq!(fs.read("/link.txt").unwrap(), b"data");
}
#[test]
fn test_vroot_canonicalizes_paths() {
let temp = tempfile::tempdir().unwrap();
let backend = VRootFsBackend::new(temp.path()).unwrap();
let fs = FileStorage::new(backend);
fs.create_dir("/a").unwrap();
fs.write("/a/file.txt", b"data").unwrap();
// Access via normalized path
assert_eq!(fs.read("/a/../a/./file.txt").unwrap(), b"data");
}
}
9. RateLimit Middleware Tests
#![allow(unused)]
fn main() {
#[test]
fn test_ratelimit_allows_within_limit() {
let backend = MemoryBackend::new()
.layer(RateLimitLayer::builder().max_ops(10).per_second().build());
let fs = FileStorage::new(backend);
// Should succeed within limit
for i in 0..5 {
fs.write(format!("/file{}.txt", i), b"data").unwrap();
}
}
#[test]
fn test_ratelimit_blocks_when_exceeded() {
let backend = MemoryBackend::new()
.layer(RateLimitLayer::builder().max_ops(3).per_second().build());
let fs = FileStorage::new(backend);
fs.write("/file1.txt", b"data").unwrap();
fs.write("/file2.txt", b"data").unwrap();
fs.write("/file3.txt", b"data").unwrap();
let result = fs.write("/file4.txt", b"data");
assert!(matches!(result, Err(FsError::RateLimitExceeded { .. })));
}
#[test]
fn test_ratelimit_resets_after_window() {
let backend = MemoryBackend::new()
.layer(RateLimitLayer::builder().max_ops(2).per(Duration::from_millis(100)).build());
let fs = FileStorage::new(backend);
fs.write("/file1.txt", b"data").unwrap();
fs.write("/file2.txt", b"data").unwrap();
// Wait for window to reset
std::thread::sleep(Duration::from_millis(150));
// Should succeed again
fs.write("/file3.txt", b"data").unwrap();
}
#[test]
fn test_ratelimit_counts_all_operations() {
let backend = MemoryBackend::new()
.layer(RateLimitLayer::builder().max_ops(3).per_second().build());
let fs = FileStorage::new(backend);
fs.write("/file.txt", b"data").unwrap(); // 1
let _ = fs.read("/file.txt"); // 2
let _ = fs.exists("/file.txt"); // 3
let result = fs.metadata("/file.txt");
assert!(matches!(result, Err(FsError::RateLimitExceeded { .. })));
}
}
10. Tracing Middleware Tests
#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};
#[derive(Default)]
struct TestLogger {
logs: Arc<Mutex<Vec<String>>>,
}
impl TestLogger {
fn entries(&self) -> Vec<String> {
self.logs.lock().unwrap().clone()
}
}
#[test]
fn test_tracing_logs_operations() {
let logger = TestLogger::default();
let logs = Arc::clone(&logger.logs);
let backend = MemoryBackend::new()
.layer(TracingLayer::new()
.with_logger(move |op| {
logs.lock().unwrap().push(op.to_string());
}));
let fs = FileStorage::new(backend);
fs.write("/file.txt", b"data").unwrap();
fs.read("/file.txt").unwrap();
let entries = logger.entries();
assert!(entries.iter().any(|e| e.contains("write")));
assert!(entries.iter().any(|e| e.contains("read")));
}
#[test]
fn test_tracing_includes_path() {
let logger = TestLogger::default();
let logs = Arc::clone(&logger.logs);
let backend = MemoryBackend::new()
.layer(TracingLayer::new()
.with_logger(move |op| {
logs.lock().unwrap().push(op.to_string());
}));
let fs = FileStorage::new(backend);
fs.write("/important/secret.txt", b"data").unwrap();
let entries = logger.entries();
assert!(entries.iter().any(|e| e.contains("/important/secret.txt")));
}
#[test]
fn test_tracing_logs_errors() {
let logger = TestLogger::default();
let logs = Arc::clone(&logger.logs);
let backend = MemoryBackend::new()
.layer(TracingLayer::new()
.with_logger(move |op| {
logs.lock().unwrap().push(op.to_string());
}));
let fs = FileStorage::new(backend);
let _ = fs.read("/nonexistent.txt");
let entries = logger.entries();
assert!(entries.iter().any(|e| e.contains("NotFound") || e.contains("error")));
}
#[test]
fn test_tracing_with_span_context() {
use tracing::{info_span, Instrument};
let backend = MemoryBackend::new().layer(TracingLayer::new());
let fs = FileStorage::new(backend);
async {
fs.write("/async.txt", b"data").unwrap();
}
.instrument(info_span!("test_operation"))
.now_or_never();
}
}
11. Backend Interchangeability Tests
#![allow(unused)]
fn main() {
/// Ensure all backends can be used interchangeably
fn generic_filesystem_test<B: Fs>(mut backend: B) {
backend.create_dir(std::path::Path::new("/test")).unwrap();
backend.write(std::path::Path::new("/test/file.txt"), b"hello").unwrap();
assert_eq!(backend.read(std::path::Path::new("/test/file.txt")).unwrap(), b"hello");
backend.remove_dir_all(std::path::Path::new("/test")).unwrap();
assert!(!backend.exists(std::path::Path::new("/test")).unwrap());
}
#[test]
fn test_memory_backend_interchangeable() {
generic_filesystem_test(MemoryBackend::new());
}
#[test]
fn test_sqlite_backend_interchangeable() {
let (backend, _temp) = temp_sqlite_backend();
generic_filesystem_test(backend);
}
#[test]
fn test_vroot_backend_interchangeable() {
let temp = tempfile::tempdir().unwrap();
let backend = VRootFsBackend::new(temp.path()).unwrap();
generic_filesystem_test(backend);
}
#[test]
fn test_middleware_stack_interchangeable() {
let backend = MemoryBackend::new()
.layer(QuotaLayer::builder()
.max_total_size(1024 * 1024)
.build())
.layer(TracingLayer::new());
generic_filesystem_test(backend);
}
}
12. Property-Based Tests
#![allow(unused)]
fn main() {
use proptest::prelude::*;
proptest! {
#[test]
fn prop_write_read_roundtrip(data: Vec<u8>) {
let backend = MemoryBackend::new();
backend.write(std::path::Path::new("/file.bin"), &data).unwrap();
let read_data = backend.read(std::path::Path::new("/file.bin")).unwrap();
prop_assert_eq!(data, read_data);
}
#[test]
fn prop_path_normalization_idempotent(path in "[a-z/]{1,50}") {
let backend = MemoryBackend::new();
if let Ok(()) = backend.create_dir_all(std::path::Path::new(&path)) {
// Creating again should either succeed or return AlreadyExists
let result = backend.create_dir_all(std::path::Path::new(&path));
prop_assert!(result.is_ok() || matches!(result, Err(FsError::AlreadyExists { .. })));
}
}
#[test]
fn prop_quota_never_exceeds_limit(
file_count in 1..10usize,
file_sizes in prop::collection::vec(1..100usize, 1..10)
) {
let limit = 500usize;
let backend = MemoryBackend::new()
.layer(QuotaLayer::builder().max_total_size(limit as u64).build());
let fs = FileStorage::new(backend);
let mut total_written = 0usize;
for (i, size) in file_sizes.into_iter().take(file_count).enumerate() {
let data = vec![0u8; size];
match fs.write(format!("/file{}.txt", i), &data) {
Ok(()) => total_written += size,
Err(FsError::QuotaExceeded { .. }) => break,
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
prop_assert!(total_written <= limit);
}
}
}
13. Snapshot & Restore Tests
#![allow(unused)]
fn main() {
// MemoryBackend implements Clone - that's the snapshot mechanism
#[test]
fn test_clone_creates_independent_copy() {
let mut original = MemoryBackend::new();
original.write(std::path::Path::new("/file.txt"), b"original").unwrap();
// Clone = snapshot
let mut snapshot = original.clone();
// Modify original
original.write(std::path::Path::new("/file.txt"), b"modified").unwrap();
original.write(std::path::Path::new("/new.txt"), b"new").unwrap();
// Snapshot is unchanged
assert_eq!(snapshot.read(std::path::Path::new("/file.txt")).unwrap(), b"original");
assert!(!snapshot.exists(std::path::Path::new("/new.txt")).unwrap());
}
#[test]
fn test_checkpoint_and_rollback() {
let fs = MemoryBackend::new();
fs.write(std::path::Path::new("/important.txt"), b"original").unwrap();
// Checkpoint = clone
let checkpoint = fs.clone();
// Do risky work
fs.write(std::path::Path::new("/important.txt"), b"corrupted").unwrap();
// Rollback = replace with checkpoint
fs = checkpoint;
assert_eq!(fs.read(std::path::Path::new("/important.txt")).unwrap(), b"original");
}
#[test]
fn test_persistence_roundtrip() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("state.bin");
let fs = MemoryBackend::new();
fs.write(std::path::Path::new("/data.txt"), b"persisted").unwrap();
// Save
fs.save_to(&path).unwrap();
// Load
let restored = MemoryBackend::load_from(&path).unwrap();
assert_eq!(restored.read(std::path::Path::new("/data.txt")).unwrap(), b"persisted");
}
#[test]
fn test_to_bytes_from_bytes() {
let fs = MemoryBackend::new();
fs.create_dir_all(std::path::Path::new("/a/b/c")).unwrap();
fs.write(std::path::Path::new("/a/b/c/file.txt"), b"nested").unwrap();
let bytes = fs.to_bytes().unwrap();
let restored = MemoryBackend::from_bytes(&bytes).unwrap();
assert_eq!(restored.read(std::path::Path::new("/a/b/c/file.txt")).unwrap(), b"nested");
}
#[test]
fn test_from_bytes_invalid_data() {
let result = MemoryBackend::from_bytes(b"garbage");
assert!(result.is_err());
}
}
14. Running Tests
# All tests
cargo test
# Specific backend conformance
cargo test memory_backend_conformance
cargo test sqlite_backend_conformance
# Middleware tests
cargo test quota
cargo test restrictions
cargo test pathfilter
# Stress tests (release mode)
cargo test --release -- --ignored
# With coverage
cargo tarpaulin --out Html
# Cross-platform (CI)
cargo test --target x86_64-unknown-linux-gnu
cargo test --target x86_64-pc-windows-msvc
cargo test --target x86_64-apple-darwin
# WASM
cargo test --target wasm32-unknown-unknown
9. Test Utilities
#![allow(unused)]
fn main() {
// In anyfs-test crate
/// Create a temporary backend for testing
pub fn temp_vrootfs_backend() -> (VRootFsBackend, tempfile::TempDir) {
let temp = tempfile::tempdir().unwrap();
let backend = VRootFsBackend::new(temp.path()).unwrap();
(backend, temp)
}
/// Run a test against multiple backends
pub fn test_all_backends<F>(test: F)
where
F: Fn(&mut dyn Fs),
{
// Memory
let backend = MemoryBackend::new();
test(&mut backend);
// VRootFs (real filesystem with containment)
let (mut backend, _temp) = temp_vrootfs_backend();
test(&mut backend);
}
}