netsuke-build 0.1.0-beta3

A YAML-powered Ninja/Jinja hybrid build system.
//! Tests for the match set [`glob_paths`] returns.
#[cfg(unix)]
use super::super::GlobPattern;
#[cfg(unix)]
use super::super::walk::{GlobRoot, process_glob_entry};
use super::super::{glob_paths, is_shell_inert_path};
#[cfg(unix)]
use anyhow::{Context, anyhow};
use anyhow::{Result, ensure};
#[cfg(unix)]
use cap_std::{ambient_authority, fs::Dir};
use minijinja::ErrorKind;
use rstest::rstest;
use tempfile::tempdir;
use test_support::fs as test_fs;

#[test]
fn glob_paths_filters_directories() -> Result<()> {
    let temp = tempdir()?;
    let dir = temp.path().join("dir");
    test_fs::create_dir(&dir)?;
    let file = temp.path().join("dir").join("file.txt");
    test_fs::write(&file, "data")?;

    let pattern = format!("{}/dir/*", temp.path().display());
    let results = glob_paths(&pattern, None)?;
    ensure!(
        results.iter().any(|p| p.ends_with("file.txt")),
        "expected file match"
    );
    ensure!(
        results.iter().all(|p| !p.ends_with("/dir")),
        "directories should be filtered out"
    );
    Ok(())
}

#[test]
fn glob_paths_rejects_unmatched_brace() {
    let err = glob_paths("foo{bar", None).expect_err("brace mismatch should error");
    assert_eq!(err.kind(), ErrorKind::SyntaxError);
}

#[rstest]
fn glob_paths_rejects_an_invalid_pattern_before_a_missing_prefix() {
    let err = glob_paths("missing/[", None).expect_err("an invalid pattern should error");
    assert_eq!(err.kind(), ErrorKind::SyntaxError);
}

#[rstest]
#[case("src/file.txt")]
#[case("build/generated-file_1.txt")]
#[case("C:/workspace/file.txt")]
#[case("data/version,1.txt")]
fn shell_inert_template_paths_accept_portable_words(#[case] path: &str) {
    assert!(
        is_shell_inert_path(path),
        "expected a shell-inert path: {path}"
    );
}

#[rstest]
#[case("")]
#[case("src/two files.txt")]
#[case("src/a.txt;touch PWNED")]
#[case("src/$(touch PWNED).txt")]
#[case("src/`touch PWNED`.txt")]
#[case("src/a.txt#comment")]
#[case("src/line\nbreak.txt")]
#[case("src/quoted'name.txt")]
fn shell_inert_template_paths_reject_shell_syntax(#[case] path: &str) {
    assert!(
        !is_shell_inert_path(path),
        "expected an unsafe template path: {path:?}"
    );
}
#[cfg(unix)]
#[test]
fn glob_paths_accepts_escaped_braces_and_matches_files() -> Result<()> {
    let temp = tempdir()?;
    let file = temp.path().join("{file}.txt");
    test_fs::write(&file, "data")?;

    let pattern = format!("{}/\\{{file\\}}.txt", temp.path().display());
    let normalized = GlobPattern::new(&pattern)?;
    ensure!(
        normalized.normalized().contains("[{]file[}]"),
        "unexpected normalized pattern: {}",
        normalized.normalized()
    );
    let results = glob_paths(&pattern, None)?;
    ensure!(
        results.iter().any(|p| p.ends_with("{file}.txt")),
        "escaped brace pattern should match literal braces"
    );
    Ok(())
}

#[cfg(unix)]
#[test]
fn process_glob_entry_rejects_non_utf8_paths() -> Result<()> {
    use std::ffi::OsString;
    use std::os::unix::ffi::OsStringExt;

    let dir = Dir::open_ambient_dir("/", ambient_authority()).context("open ambient root dir")?;
    let root = GlobRoot::new(dir, camino::Utf8PathBuf::from("/"));
    let path = std::path::PathBuf::from(OsString::from_vec(b"bad\xFF".to_vec()));
    let pattern = GlobPattern::new("pattern")?;
    match process_glob_entry(Ok(path), &pattern, &root) {
        Ok(value) => Err(anyhow!("expected non-UTF-8 error but received {value:?}")),
        Err(err) => {
            ensure!(
                err.kind() == ErrorKind::InvalidOperation,
                "unexpected error kind {kind:?}",
                kind = err.kind()
            );
            Ok(())
        }
    }
}