main-spex 0.25.0

AI-first development framework that turns specifications into deterministic, verifiable code
#[cfg(test)]
mod tests {
    use super::super::{Exclude, PackageFiles};
    use crate::error::describe;
    use std::collections::HashSet;
    use std::path::{Path, PathBuf};
    use tempfile::tempdir;

    fn exclude(root: &Path, patterns: &[&str]) -> Exclude {
        let patterns: Vec<String> = patterns.iter().map(ToString::to_string).collect();
        Exclude::new(root, &patterns).expect("valid exclude")
    }

    fn walked_files(root: &Path) -> HashSet<PathBuf> {
        walked_files_excluding(root, &[])
    }

    fn walked_files_excluding(root: &Path, patterns: &[&str]) -> HashSet<PathBuf> {
        PackageFiles::new(root, &exclude(root, patterns))
            .filter_map(Result::ok)
            .map(|path| path.strip_prefix(root).unwrap_or(&path).to_path_buf())
            .collect()
    }

    fn walked_in_order(root: &Path) -> Vec<PathBuf> {
        PackageFiles::new(root, &exclude(root, &[]))
            .filter_map(Result::ok)
            .map(|path| path.strip_prefix(root).unwrap_or(&path).to_path_buf())
            .collect()
    }

    fn write_files(root: &Path, files: &[&str]) {
        for file in files {
            let path = root.join(file);
            std::fs::create_dir_all(path.parent().expect("parent")).expect("parent dir");
            std::fs::write(path, "").expect("file");
        }
    }

    #[test]
    fn yields_files_sorted_by_name_within_each_directory() {
        let dir = tempdir().expect("temp dir");
        for file in [
            "zeta.txt",
            "nested/beta.txt",
            "nested/alpha.txt",
            "a.rs",
            "a/x.rs",
        ] {
            let path = dir.path().join(file);
            std::fs::create_dir_all(path.parent().expect("parent")).expect("parent dir");
            std::fs::write(path, "").expect("file");
        }

        let files = walked_in_order(dir.path());

        assert_eq!(
            files,
            [
                "a/x.rs",
                "a.rs",
                "nested/alpha.txt",
                "nested/beta.txt",
                "zeta.txt"
            ]
            .map(PathBuf::from)
        );
    }

    #[test]
    fn honors_gitignore_without_a_git_directory() {
        let dir = tempdir().expect("temp dir");
        std::fs::write(dir.path().join(".gitignore"), "ignored.txt\n").expect("gitignore");
        std::fs::write(dir.path().join("ignored.txt"), "").expect("ignored");
        std::fs::write(dir.path().join("kept.txt"), "").expect("kept");

        let files = walked_files(dir.path());

        assert!(files.contains(&PathBuf::from("kept.txt")));
        assert!(!files.contains(&PathBuf::from("ignored.txt")));
    }

    #[test]
    fn skips_dependency_lock_files() {
        let dir = tempdir().expect("temp dir");
        std::fs::write(dir.path().join("Cargo.lock"), "").expect("cargo lock");
        std::fs::write(dir.path().join("package-lock.json"), "").expect("npm lock");
        std::fs::write(dir.path().join("Cargo.toml"), "").expect("cargo manifest");

        let files = walked_files(dir.path());

        assert!(files.contains(&PathBuf::from("Cargo.toml")));
        assert!(!files.contains(&PathBuf::from("Cargo.lock")));
        assert!(!files.contains(&PathBuf::from("package-lock.json")));
    }

    #[test]
    fn surfaces_dotfiles_and_skips_git_directory() {
        let dir = tempdir().expect("temp dir");
        std::fs::create_dir_all(dir.path().join(".git")).expect("git dir");
        std::fs::write(dir.path().join(".git/config"), "").expect("git config");
        std::fs::write(dir.path().join(".env"), "").expect("env");

        let files = walked_files(dir.path());

        assert!(files.contains(&PathBuf::from(".env")));
        assert!(!files.iter().any(|path| path.starts_with(".git")));
    }

    #[test]
    fn yields_files_but_not_directories() {
        let dir = tempdir().expect("temp dir");
        std::fs::create_dir_all(dir.path().join("nested")).expect("nested dir");
        std::fs::write(dir.path().join("nested/file.txt"), "").expect("file");

        let files = walked_files(dir.path());

        assert_eq!(files, HashSet::from([PathBuf::from("nested/file.txt")]));
    }

    #[test]
    fn skips_a_folder_whose_path_matches_an_exclude_with_everything_inside_it() {
        let dir = tempdir().expect("temp dir");
        write_files(
            dir.path(),
            &["vendor/lib.js", "vendor/deep/more.js", "src/vendor/b.js"],
        );

        let files = walked_files_excluding(dir.path(), &["vendor"]);

        assert_eq!(files, HashSet::from([PathBuf::from("src/vendor/b.js")]));
    }

    #[test]
    fn skips_the_files_inside_a_folder_matched_by_a_slash_double_star_exclude() {
        let dir = tempdir().expect("temp dir");
        write_files(
            dir.path(),
            &["vendor/lib.js", "vendor/deep/more.js", "src/vendor/b.js"],
        );

        let files = walked_files_excluding(dir.path(), &["vendor/**"]);

        assert_eq!(files, HashSet::from([PathBuf::from("src/vendor/b.js")]));
    }

    #[test]
    fn skips_a_folder_at_any_depth_with_a_leading_double_star_exclude() {
        let dir = tempdir().expect("temp dir");
        write_files(
            dir.path(),
            &["vendor/lib.js", "src/vendor/b.js", "src/main.rs"],
        );

        let files = walked_files_excluding(dir.path(), &["**/vendor"]);

        assert_eq!(files, HashSet::from([PathBuf::from("src/main.rs")]));
    }

    #[test]
    fn skips_files_matched_by_an_exclude_glob_at_any_depth() {
        let dir = tempdir().expect("temp dir");
        write_files(
            dir.path(),
            &[
                "api.generated.ts",
                "src/models.generated.ts",
                "src/models.ts",
            ],
        );

        let files = walked_files_excluding(dir.path(), &["**/*.generated.ts"]);

        assert_eq!(files, HashSet::from([PathBuf::from("src/models.ts")]));
    }

    #[test]
    fn skips_files_matched_by_any_of_several_exclude_patterns() {
        let dir = tempdir().expect("temp dir");
        write_files(dir.path(), &["a.md", "b.txt", "c.rs"]);

        let files = walked_files_excluding(dir.path(), &["*.md", "*.txt"]);

        assert_eq!(files, HashSet::from([PathBuf::from("c.rs")]));
    }

    #[test]
    fn excludes_nothing_without_patterns() {
        let dir = tempdir().expect("temp dir");
        write_files(dir.path(), &["a.txt", "nested/b.txt"]);

        let files = walked_files_excluding(dir.path(), &[]);

        assert_eq!(files.len(), 2);
    }

    #[test]
    fn matches_a_path_relative_to_the_package_or_under_its_root() {
        let dir = tempdir().expect("temp dir");
        let exclude = exclude(dir.path(), &["vendor/**", "**/*.generated.ts"]);

        assert!(exclude.matches(Path::new("vendor/lib/deep.js")));
        assert!(exclude.matches(&dir.path().join("vendor/lib/deep.js")));
        assert!(exclude.matches(Path::new("src/api.generated.ts")));
        assert!(!exclude.matches(Path::new("src/api.ts")));
        assert!(!exclude.matches(&dir.path().join("src/api.ts")));
    }

    #[test]
    fn rejects_an_exclude_pattern_that_is_not_a_valid_glob() {
        let dir = tempdir().expect("temp dir");

        let error = Exclude::new(dir.path(), &["src/{a,b".to_string()]).expect_err("invalid glob");

        assert!(
            describe(&error).contains("src/{a,b"),
            "{}",
            describe(&error)
        );
    }
}