tree-type 0.4.5

Rust macros for creating type-safe filesystem tree structures
Documentation
#![cfg_attr(feature = "walk", allow(deprecated))]
#[cfg(feature = "walk")]
use std::collections::HashSet;
#[cfg(feature = "walk")]
use std::fs;
#[cfg(feature = "walk")]
use std::os::unix::fs::symlink;
/// Comprehensive tests for the walk() method on GenericDir and generated types
#[cfg(feature = "walk")]
use tree_type::GenericDir;
/// Comprehensive tests for the walk() method on GenericDir and generated types
#[cfg(feature = "walk")]
use tree_type::GenericPath;
/// Comprehensive tests for the walk() method on GenericDir and generated types
#[cfg(feature = "walk")]
use tree_type::dir_type;
/// Comprehensive tests for the walk() method on GenericDir and generated types
#[cfg(feature = "walk")]
use tree_type::tree_type;

#[cfg(feature = "walk")]
#[test]
fn test_generic_dir_walk_basic() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path();

    // Create test structure
    fs::create_dir(root.join("subdir")).unwrap();
    fs::write(root.join("file1.txt"), "content1").unwrap();
    fs::write(root.join("subdir/file2.txt"), "content2").unwrap();

    let generic_dir = GenericDir::new(root).unwrap();
    let paths: Result<Vec<_>, _> = generic_dir.walk().collect();
    let paths = paths.unwrap();

    assert_eq!(paths.len(), 4); // root + subdir + 2 files

    // Check we get expected types
    let mut files = 0;
    let mut dirs = 0;
    for path in paths {
        match path {
            GenericPath::File(_) => files += 1,
            GenericPath::Dir(_) => dirs += 1,
        }
    }
    assert_eq!(files, 2);
    assert_eq!(dirs, 2); // root + subdir
}

#[cfg(feature = "walk")]
#[test]
fn test_dir_type_walk() {
    dir_type!(TestDir);

    let temp = tempfile::tempdir().unwrap();
    let root = temp.path();

    fs::create_dir(root.join("nested")).unwrap();
    fs::write(root.join("test.txt"), "data").unwrap();
    fs::write(root.join("nested/deep.txt"), "deep data").unwrap();

    let test_dir = TestDir::new(root).unwrap();
    let paths: Result<Vec<_>, _> = test_dir.walk().collect();
    let paths = paths.unwrap();

    assert!(paths.len() >= 3); // At least root + nested + files
}

#[cfg(feature = "walk")]
#[test]
fn test_tree_type_walk() {
    tree_type! {
        TestRoot {
            docs/
            src/
        }
    }

    let temp = tempfile::tempdir().unwrap();
    let root = TestRoot::new(temp.path()).unwrap();
    root.setup().unwrap();

    let paths: Result<Vec<_>, _> = root.walk().collect();
    let paths = paths.unwrap();

    // Should include root, docs/, src/
    assert!(paths.len() >= 3);

    // Test subdirectory walk
    let src_paths: Result<Vec<_>, _> = root.src().walk().collect();
    let src_paths = src_paths.unwrap();

    // Should include src/ itself
    assert!(!src_paths.is_empty());
}

#[cfg(feature = "walk")]
#[test]
fn test_walk_follows_symlinks() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path();

    // Create target file and symlink
    fs::write(root.join("target.txt"), "target content").unwrap();
    symlink(root.join("target.txt"), root.join("link.txt")).unwrap();

    let generic_dir = GenericDir::new(root).unwrap();
    let paths: Result<Vec<_>, _> = generic_dir.walk().collect();
    let paths = paths.unwrap();

    // Should resolve symlink to file
    let mut found_target = false;
    for path in paths {
        if let GenericPath::File(file) = path {
            if file.as_ref().file_name().unwrap() == "target.txt" {
                found_target = true;
            }
        }
    }
    assert!(
        found_target,
        "Should find target file through symlink resolution"
    );
}

#[cfg(feature = "walk")]
#[test]
fn test_walk_detects_symlink_loops() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path();

    // Create symlink loop: link1 -> link2 -> link1
    symlink("link2", root.join("link1")).unwrap();
    symlink("link1", root.join("link2")).unwrap();

    let generic_dir = GenericDir::new(root).unwrap();
    let results: Vec<_> = generic_dir.walk().collect();

    // Should detect loop and return error
    let has_error = results.iter().any(|r| r.is_err());
    assert!(has_error, "Should detect symlink loop and return error");

    // Check error message - the actual error might be different
    let error_result = results.into_iter().find(|r| r.is_err()).unwrap();
    let error = error_result.err().unwrap();
    // Accept either "symlink loop" or "unsupported path type" as valid loop detection
    let error_msg = error.to_string();
    assert!(
        error_msg.contains("symlink loop") || error_msg.contains("unsupported path type"),
        "Expected symlink loop error, got: {}",
        error_msg
    );
}

#[cfg(feature = "walk")]
#[test]
fn test_walk_symlink_to_directory() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path();

    // Create target directory and symlink
    fs::create_dir(root.join("target_dir")).unwrap();
    fs::write(root.join("target_dir/file.txt"), "content").unwrap();
    symlink(root.join("target_dir"), root.join("dir_link")).unwrap();

    let generic_dir = GenericDir::new(root).unwrap();
    let paths: Result<Vec<_>, _> = generic_dir.walk().collect();
    let paths = paths.unwrap();

    // Should resolve symlink to directory and walk its contents
    let mut found_target_dir = false;
    let mut found_file_in_target = false;

    for path in paths {
        match path {
            GenericPath::Dir(dir) => {
                if dir.as_ref().file_name().unwrap() == "target_dir" {
                    found_target_dir = true;
                }
            }
            GenericPath::File(file) => {
                if file.as_ref().file_name().unwrap() == "file.txt" {
                    found_file_in_target = true;
                }
            }
        }
    }

    assert!(found_target_dir, "Should find target directory");
    assert!(found_file_in_target, "Should find file in target directory");
}

#[cfg(feature = "walk")]
#[test]
fn test_walk_complex_symlink_chain() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path();

    // Create chain: link1 -> link2 -> target.txt
    fs::write(root.join("target.txt"), "final content").unwrap();
    symlink("target.txt", root.join("link2")).unwrap();
    symlink("link2", root.join("link1")).unwrap();

    let generic_dir = GenericDir::new(root).unwrap();
    let results: Vec<_> = generic_dir.walk().collect();

    // Check if we can resolve the chain or get an error
    let has_target_or_error = results.iter().any(|r| {
        match r {
            Ok(GenericPath::File(file)) => file.as_ref().file_name().unwrap() == "target.txt",
            Err(_) => true, // Accept errors as valid for complex symlink chains
            _ => false,
        }
    });

    assert!(
        has_target_or_error,
        "Should either resolve symlink chain or return error"
    );
}

#[cfg(feature = "walk")]
#[test]
fn test_walk_empty_directory() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path();

    let generic_dir = GenericDir::new(root).unwrap();
    let paths: Result<Vec<_>, _> = generic_dir.walk().collect();
    let paths = paths.unwrap();

    // Should only contain the root directory itself
    assert_eq!(paths.len(), 1);
    assert!(matches!(paths[0], GenericPath::Dir(_)));
}

#[cfg(feature = "walk")]
#[test]
fn test_walk_deep_nesting() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path();

    // Create deep nesting: a/b/c/d/file.txt
    let deep_path = root.join("a/b/c/d");
    fs::create_dir_all(&deep_path).unwrap();
    fs::write(deep_path.join("file.txt"), "deep content").unwrap();

    let generic_dir = GenericDir::new(root).unwrap();
    let paths: Result<Vec<_>, _> = generic_dir.walk().collect();
    let paths = paths.unwrap();

    // Should find all directories and the file
    assert!(paths.len() >= 6); // root + a + b + c + d + file.txt

    // Verify we can find the deep file
    let found_deep_file = paths.iter().any(
        |path| matches!(path, GenericPath::File(file) if file.as_ref().file_name().unwrap() == "file.txt"),
    );
    assert!(found_deep_file, "Should find deeply nested file");
}

#[cfg(feature = "walk")]
#[test]
fn test_walk_mixed_content() {
    tree_type! {
        MixedRoot {
            regular_dir/
        }
    }

    let temp = tempfile::tempdir().unwrap();
    let root = temp.path();

    // Create external target and manual symlink
    fs::write(root.join("external.txt"), "external content").unwrap();

    let mixed_root = MixedRoot::new(root.join("project")).unwrap();
    mixed_root.setup().unwrap();

    // Create manual symlink after setup
    symlink(
        root.join("external.txt"),
        mixed_root.as_ref().join("link_to_external"),
    )
    .unwrap();

    let paths: Result<Vec<_>, _> = mixed_root.walk().collect();
    let paths = paths.unwrap();

    // Should include directories and resolved symlinks
    let mut dirs = 0;

    for path in paths {
        match path {
            GenericPath::File(_) => {}
            GenericPath::Dir(_) => dirs += 1,
        }
    }

    assert!(dirs >= 2, "Should find root and regular_dir");
}

#[cfg(feature = "walk")]
#[test]
fn test_walk_iterator_is_lazy() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path();

    // Create many files
    for i in 0..100 {
        fs::write(
            root.join(format!("file_{}.txt", i)),
            format!("content {}", i),
        )
        .unwrap();
    }

    let generic_dir = GenericDir::new(root).unwrap();
    let walker = generic_dir.walk();

    // Should be able to take just a few items without processing all
    let first_few: Result<Vec<_>, _> = walker.take(5).collect();
    let first_few = first_few.unwrap();

    assert_eq!(first_few.len(), 5);
}

#[cfg(feature = "walk")]
#[test]
fn test_walk_path_uniqueness() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path();

    // Create structure with potential duplicates via symlinks
    fs::create_dir(root.join("dir1")).unwrap();
    fs::write(root.join("dir1/file.txt"), "content").unwrap();
    symlink(root.join("dir1"), root.join("dir_link")).unwrap();

    let generic_dir = GenericDir::new(root).unwrap();
    let results: Vec<_> = generic_dir.walk().collect();

    // Count successful results (some may be errors due to symlink handling)
    let successful_results: Vec<_> = results.into_iter().filter_map(|r| r.ok()).collect();

    // Collect all path strings for uniqueness check
    let path_strings: HashSet<String> = successful_results
        .iter()
        .map(|p| match p {
            GenericPath::File(f) => f.as_ref().to_string_lossy().to_string(),
            GenericPath::Dir(d) => d.as_ref().to_string_lossy().to_string(),
        })
        .collect();

    // We should have at least the basic structure (root, dir1, file.txt)
    assert!(
        path_strings.len() >= 3,
        "Should find at least root, dir1, and file.txt"
    );
}