fast-fs 0.2.1

High-speed async file system traversal library with batteries-included file browser component
Documentation
// <FILE>tests/reader/test_fnc_read_dir_recursive.rs</FILE> - <DESC>Tests for enhanced read_dir_recursive with TraversalOptions</DESC>
// <VERS>VERSION: 1.0.0</VERS>
// <WCTX>Implementing enhanced read_dir_recursive with filtering support</WCTX>
// <CLOG>Initial creation with comprehensive test suite per work package 204</CLOG>

use fast_fs::{read_dir_recursive, TraversalOptions};
use std::fs::{self, File};
use std::io::Write;
use tempfile::tempdir;

#[tokio::test]
async fn test_with_default_options() {
    let dir = tempdir().unwrap();

    // Create .git directory (required for WalkBuilder to respect .gitignore)
    fs::create_dir(dir.path().join(".git")).unwrap();

    // Create structure
    File::create(dir.path().join("main.rs")).unwrap();
    File::create(dir.path().join("lib.rs")).unwrap();
    fs::create_dir(dir.path().join("src")).unwrap();
    File::create(dir.path().join("src/mod.rs")).unwrap();

    // Create .gitignore
    let mut gitignore = File::create(dir.path().join(".gitignore")).unwrap();
    writeln!(gitignore, "target/").unwrap();

    // Create target directory (should be ignored)
    fs::create_dir(dir.path().join("target")).unwrap();
    File::create(dir.path().join("target/debug.txt")).unwrap();

    let options = TraversalOptions::default();
    let entries = read_dir_recursive(dir.path(), options).await.unwrap();

    // Should include main.rs, lib.rs, src/, src/mod.rs, .gitignore
    // Should NOT include target/ or target/debug.txt
    let paths: Vec<String> = entries
        .iter()
        .map(|e| e.name.clone())
        .collect();

    assert!(paths.contains(&"main.rs".to_string()));
    assert!(paths.contains(&"lib.rs".to_string()));
    assert!(paths.contains(&"src".to_string()));
    assert!(!paths.contains(&"target".to_string()));
    assert!(!paths.contains(&"debug.txt".to_string()));
}

#[tokio::test]
async fn test_depth_limit() {
    let dir = tempdir().unwrap();

    // Create nested structure
    File::create(dir.path().join("root.txt")).unwrap();
    fs::create_dir(dir.path().join("level1")).unwrap();
    File::create(dir.path().join("level1/file1.txt")).unwrap();
    fs::create_dir(dir.path().join("level1/level2")).unwrap();
    File::create(dir.path().join("level1/level2/file2.txt")).unwrap();
    fs::create_dir(dir.path().join("level1/level2/level3")).unwrap();
    File::create(dir.path().join("level1/level2/level3/file3.txt")).unwrap();

    // Test depth 0 (root only)
    let options = TraversalOptions::default().with_max_depth(0);
    let entries = read_dir_recursive(dir.path(), options).await.unwrap();
    assert_eq!(entries.len(), 2); // root.txt + level1/

    // Test depth 1 (root + immediate children)
    let options = TraversalOptions::default().with_max_depth(1);
    let entries = read_dir_recursive(dir.path(), options).await.unwrap();
    assert_eq!(entries.len(), 4); // root.txt, level1/, level1/file1.txt, level1/level2/

    // Test depth 2 (stops at level2)
    let options = TraversalOptions::default().with_max_depth(2);
    let entries = read_dir_recursive(dir.path(), options).await.unwrap();
    assert_eq!(entries.len(), 6); // Everything except level3 contents

    let paths: Vec<String> = entries.iter().map(|e| e.name.clone()).collect();
    assert!(!paths.contains(&"file3.txt".to_string()));
}

#[tokio::test]
async fn test_gitignore_respected() {
    let dir = tempdir().unwrap();

    // Create .git directory (required for WalkBuilder to respect .gitignore)
    fs::create_dir(dir.path().join(".git")).unwrap();

    // Create structure
    File::create(dir.path().join("main.rs")).unwrap();
    fs::create_dir(dir.path().join("target")).unwrap();
    File::create(dir.path().join("target/output.bin")).unwrap();
    fs::create_dir(dir.path().join("build")).unwrap();
    File::create(dir.path().join("build/temp.o")).unwrap();

    // Create .gitignore
    let mut gitignore = File::create(dir.path().join(".gitignore")).unwrap();
    writeln!(gitignore, "target/").unwrap();
    writeln!(gitignore, "build/").unwrap();

    let options = TraversalOptions::default();
    let entries = read_dir_recursive(dir.path(), options).await.unwrap();

    let paths: Vec<String> = entries.iter().map(|e| e.name.clone()).collect();

    assert!(paths.contains(&"main.rs".to_string()));
    assert!(!paths.contains(&"target".to_string()));
    assert!(!paths.contains(&"build".to_string()));
    assert!(!paths.contains(&"output.bin".to_string()));
}

#[tokio::test]
async fn test_node_modules_skipped() {
    let dir = tempdir().unwrap();

    // Create .git directory (required for WalkBuilder to respect .gitignore)
    fs::create_dir(dir.path().join(".git")).unwrap();

    // Create Node.js structure
    File::create(dir.path().join("package.json")).unwrap();
    File::create(dir.path().join("index.js")).unwrap();
    fs::create_dir(dir.path().join("node_modules")).unwrap();
    File::create(dir.path().join("node_modules/package.js")).unwrap();

    // Create .gitignore with node_modules
    let mut gitignore = File::create(dir.path().join(".gitignore")).unwrap();
    writeln!(gitignore, "node_modules/").unwrap();

    let options = TraversalOptions::default();
    let entries = read_dir_recursive(dir.path(), options).await.unwrap();

    let paths: Vec<String> = entries.iter().map(|e| e.name.clone()).collect();

    assert!(paths.contains(&"package.json".to_string()));
    assert!(paths.contains(&"index.js".to_string()));
    assert!(!paths.contains(&"node_modules".to_string()));
}

#[tokio::test]
async fn test_rust_target_skipped() {
    let dir = tempdir().unwrap();

    // Create .git directory (required for WalkBuilder to respect .gitignore)
    fs::create_dir(dir.path().join(".git")).unwrap();

    // Create Rust project structure
    File::create(dir.path().join("Cargo.toml")).unwrap();
    fs::create_dir(dir.path().join("src")).unwrap();
    File::create(dir.path().join("src/main.rs")).unwrap();
    fs::create_dir(dir.path().join("target")).unwrap();
    fs::create_dir(dir.path().join("target/debug")).unwrap();
    File::create(dir.path().join("target/debug/binary")).unwrap();

    // Create .gitignore with target
    let mut gitignore = File::create(dir.path().join(".gitignore")).unwrap();
    writeln!(gitignore, "target/").unwrap();

    let options = TraversalOptions::default();
    let entries = read_dir_recursive(dir.path(), options).await.unwrap();

    let paths: Vec<String> = entries.iter().map(|e| e.name.clone()).collect();

    assert!(paths.contains(&"Cargo.toml".to_string()));
    assert!(paths.contains(&"src".to_string()));
    assert!(!paths.contains(&"target".to_string()));
    assert!(!paths.contains(&"binary".to_string()));
}

#[tokio::test]
async fn test_extension_filter() {
    let dir = tempdir().unwrap();

    // Create mixed file types
    File::create(dir.path().join("main.rs")).unwrap();
    File::create(dir.path().join("lib.rs")).unwrap();
    File::create(dir.path().join("README.md")).unwrap();
    File::create(dir.path().join("build.py")).unwrap();
    fs::create_dir(dir.path().join("src")).unwrap();
    File::create(dir.path().join("src/module.rs")).unwrap();
    File::create(dir.path().join("src/script.sh")).unwrap();

    let options = TraversalOptions::default()
        .with_extensions(&["rs"])
        .with_gitignore(false); // Disable gitignore for this test

    let entries = read_dir_recursive(dir.path(), options).await.unwrap();

    let file_names: Vec<String> = entries
        .iter()
        .filter(|e| !e.is_dir)
        .map(|e| e.name.clone())
        .collect();

    // Should only include .rs files
    assert_eq!(file_names.len(), 3);
    assert!(file_names.contains(&"main.rs".to_string()));
    assert!(file_names.contains(&"lib.rs".to_string()));
    assert!(file_names.contains(&"module.rs".to_string()));

    // Should still include directories
    let dir_names: Vec<String> = entries
        .iter()
        .filter(|e| e.is_dir)
        .map(|e| e.name.clone())
        .collect();
    assert!(dir_names.contains(&"src".to_string()));
}

#[tokio::test]
async fn test_include_hidden() {
    let dir = tempdir().unwrap();

    // Create files including hidden ones
    File::create(dir.path().join("visible.txt")).unwrap();
    File::create(dir.path().join(".hidden")).unwrap();
    fs::create_dir(dir.path().join(".config")).unwrap();
    File::create(dir.path().join(".config/settings.json")).unwrap();

    // Test without including hidden (default)
    let options = TraversalOptions::default().with_gitignore(false);
    let entries = read_dir_recursive(dir.path(), options).await.unwrap();

    let paths: Vec<String> = entries.iter().map(|e| e.name.clone()).collect();
    assert!(paths.contains(&"visible.txt".to_string()));
    assert!(!paths.contains(&".hidden".to_string()));
    assert!(!paths.contains(&".config".to_string()));

    // Test with including hidden
    let options = TraversalOptions::default()
        .include_hidden()
        .with_gitignore(false);
    let entries = read_dir_recursive(dir.path(), options).await.unwrap();

    let paths: Vec<String> = entries.iter().map(|e| e.name.clone()).collect();
    assert!(paths.contains(&"visible.txt".to_string()));
    assert!(paths.contains(&".hidden".to_string()));
    assert!(paths.contains(&".config".to_string()));
    assert!(paths.contains(&"settings.json".to_string()));
}

#[tokio::test]
async fn test_symlink_loop_detection() {
    let dir = tempdir().unwrap();

    // Create a directory and a symlink pointing back to parent
    fs::create_dir(dir.path().join("subdir")).unwrap();
    File::create(dir.path().join("subdir/file.txt")).unwrap();

    // Create symlink (only on Unix-like systems)
    #[cfg(unix)]
    {
        use std::os::unix::fs::symlink;
        let _ = symlink(dir.path(), dir.path().join("subdir/loop_link"));
    }

    let options = TraversalOptions::default().with_gitignore(false);
    let result = read_dir_recursive(dir.path(), options).await;

    // Should complete without infinite loop
    assert!(result.is_ok());
    let entries = result.unwrap();

    // Should have collected files without following the loop
    let paths: Vec<String> = entries.iter().map(|e| e.name.clone()).collect();
    assert!(paths.contains(&"subdir".to_string()));
    assert!(paths.contains(&"file.txt".to_string()));
}

#[tokio::test]
async fn test_empty_directory() {
    let dir = tempdir().unwrap();

    let options = TraversalOptions::default().with_gitignore(false);
    let entries = read_dir_recursive(dir.path(), options).await.unwrap();

    assert_eq!(entries.len(), 0);
}

#[tokio::test]
async fn test_combined_filters() {
    let dir = tempdir().unwrap();

    // Create .git directory (required for WalkBuilder to respect .gitignore)
    fs::create_dir(dir.path().join(".git")).unwrap();

    // Create complex structure
    File::create(dir.path().join("main.rs")).unwrap();
    File::create(dir.path().join(".hidden.rs")).unwrap();
    File::create(dir.path().join("script.py")).unwrap();
    fs::create_dir(dir.path().join("src")).unwrap();
    File::create(dir.path().join("src/lib.rs")).unwrap();
    fs::create_dir(dir.path().join("target")).unwrap();
    File::create(dir.path().join("target/output.rs")).unwrap();

    // Create .gitignore
    let mut gitignore = File::create(dir.path().join(".gitignore")).unwrap();
    writeln!(gitignore, "target/").unwrap();

    // Combine filters: only .rs files, no hidden, respect gitignore
    let options = TraversalOptions::default()
        .with_extensions(&["rs"]);

    let entries = read_dir_recursive(dir.path(), options).await.unwrap();

    let file_names: Vec<String> = entries
        .iter()
        .filter(|e| !e.is_dir)
        .map(|e| e.name.clone())
        .collect();

    // Should have main.rs and lib.rs (not .hidden.rs, not script.py, not target/output.rs)
    assert_eq!(file_names.len(), 2);
    assert!(file_names.contains(&"main.rs".to_string()));
    assert!(file_names.contains(&"lib.rs".to_string()));
}

// <FILE>tests/reader/test_fnc_read_dir_recursive.rs</FILE>
// <VERS>END OF VERSION: 1.0.0</VERS>