Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Testing Guide

Comprehensive testing strategy for AnyFS


Overview

AnyFS uses a layered testing approach:

LayerWhat it testsRun with
Unit testsIndividual componentscargo test
Conformance testsBackend trait compliancecargo test --features conformance
Integration testsFull stack behaviorcargo test --test integration
Stress testsConcurrency & limitscargo test --release -- --ignored
Platform testsCross-platform behaviorCI 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
}
}

#![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);
}
}