Sebastian Thielthanks clippybac57dd3 years agoHistory
325 lines · 11.2 KB · Rust
RawDownload
use std::collections::BTreeSet; use bstr::{BStr, ByteSlice};use git_glob::{pattern, pattern::Case}; #[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Copy, Clone)]pub struct GitMatch<'a> {    pattern: &'a BStr,    value: &'a BStr,    /// True if git could match `value` with `pattern`    is_match: bool,} pub struct Baseline<'a> {    inner: bstr::Lines<'a>,} impl<'a> Iterator for Baseline<'a> {    type Item = GitMatch<'a>;     fn next(&mut self) -> Option<Self::Item> {        let mut tokens = self.inner.next()?.splitn(2, |b| *b == b' ');        let pattern = tokens.next().expect("pattern").as_bstr();        let value = tokens.next().expect("value").as_bstr().trim_start().as_bstr();         let git_match = self.inner.next()?;        let is_match = !git_match.starts_with(b"::\t");        Some(GitMatch {            pattern,            value,            is_match,        })    }} impl<'a> Baseline<'a> {    fn new(input: &'a [u8]) -> Self {        Baseline {            inner: input.as_bstr().lines(),        }    }} #[test]fn compare_baseline_with_ours() {    let dir = git_testtools::scripted_fixture_read_only("make_baseline.sh").unwrap();    let (mut total_matches, mut total_correct, mut panics) = (0, 0, 0);    let mut mismatches = Vec::new();    for (input_file, expected_matches, case) in &[        ("git-baseline.match", true, pattern::Case::Sensitive),        ("git-baseline.nmatch", false, pattern::Case::Sensitive),        ("git-baseline.match-icase", true, pattern::Case::Fold),    ] {        let input = std::fs::read(dir.join(*input_file)).unwrap();        let mut seen = BTreeSet::default();         for m @ GitMatch {            pattern,            value,            is_match,        } in Baseline::new(&input)        {            total_matches += 1;            assert!(seen.insert(m), "duplicate match entry: {m:?}");            assert_eq!(                is_match, *expected_matches,                "baseline for matches must be {expected_matches} - check baseline and git version: {m:?}"            );            match std::panic::catch_unwind(|| {                let pattern = pat(pattern);                pattern.matches_repo_relative_path(value, basename_start_pos(value), None, *case)            }) {                Ok(actual_match) => {                    if actual_match == is_match {                        total_correct += 1;                    } else {                        mismatches.push((pattern.to_owned(), value.to_owned(), is_match, expected_matches));                    }                }                Err(_) => {                    panics += 1;                    continue;                }            };        }    }     dbg!(mismatches);    assert_eq!(        total_correct,        total_matches - panics,        "We perfectly agree with git here"    );    assert_eq!(panics, 0);} #[test]fn non_dirs_for_must_be_dir_patterns_are_ignored() {    let pattern = pat("hello/");     assert!(pattern.mode.contains(pattern::Mode::MUST_BE_DIR));    assert_eq!(        pattern.text, "hello",        "a dir pattern doesn't actually end with the trailing slash"    );    let path = "hello";    assert!(        !pattern.matches_repo_relative_path(path, None, false.into() /* is-dir */, Case::Sensitive),        "non-dirs never match a dir pattern"    );    assert!(        pattern.matches_repo_relative_path(path, None, true.into() /* is-dir */, Case::Sensitive),        "dirs can match a dir pattern with the normal rules"    );} #[test]fn matches_of_absolute_paths_work() {    let pattern = "/hello/git";    assert!(        git_glob::wildmatch(pattern.into(), pattern.into(), git_glob::wildmatch::Mode::empty()),        "patterns always match themselves"    );    assert!(        git_glob::wildmatch(            pattern.into(),            pattern.into(),            git_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL        ),        "patterns always match themselves, path mode doesn't change that"    );} #[test]fn basename_matches_from_end() {    let pat = &pat("foo");    assert!(match_file(pat, "FoO", Case::Fold));    assert!(!match_file(pat, "FoOo", Case::Fold));    assert!(!match_file(pat, "Foo", Case::Sensitive));    assert!(match_file(pat, "foo", Case::Sensitive));    assert!(!match_file(pat, "Foo", Case::Sensitive));    assert!(!match_file(pat, "barfoo", Case::Sensitive));} #[test]fn absolute_basename_matches_only_from_beginning() {    let pat = &pat("/foo");    assert!(match_file(pat, "FoO", Case::Fold));    assert!(!match_file(pat, "bar/Foo", Case::Fold));    assert!(match_file(pat, "foo", Case::Sensitive));    assert!(!match_file(pat, "Foo", Case::Sensitive));    assert!(!match_file(pat, "bar/foo", Case::Sensitive));} #[test]fn absolute_path_matches_only_from_beginning() {    let pat = &pat("/bar/foo");    assert!(!match_file(pat, "FoO", Case::Fold));    assert!(match_file(pat, "bar/Foo", Case::Fold));    assert!(!match_file(pat, "foo", Case::Sensitive));    assert!(match_file(pat, "bar/foo", Case::Sensitive));    assert!(!match_file(pat, "bar/Foo", Case::Sensitive));} #[test]fn absolute_path_with_recursive_glob_detects_mismatches_quickly() {    let pat = &pat("/bar/foo/**");    assert!(!match_file(pat, "FoO", Case::Fold));    assert!(!match_file(pat, "bar/Fooo", Case::Fold));    assert!(!match_file(pat, "baz/bar/Foo", Case::Fold));} #[test]fn absolute_path_with_recursive_glob_can_do_case_insensitive_prefix_search() {    let pat = &pat("/bar/foo/**");    assert!(!match_file(pat, "bar/Foo/match", Case::Sensitive));    assert!(match_file(pat, "bar/Foo/match", Case::Fold));} #[test]fn relative_path_does_not_match_from_end() {    for pattern in &["bar/foo", "/bar/foo"] {        let pattern = &pat(*pattern);        assert!(!match_file(pattern, "FoO", Case::Fold));        assert!(match_file(pattern, "bar/Foo", Case::Fold));        assert!(!match_file(pattern, "baz/bar/Foo", Case::Fold));        assert!(!match_file(pattern, "foo", Case::Sensitive));        assert!(match_file(pattern, "bar/foo", Case::Sensitive));        assert!(!match_file(pattern, "baz/bar/foo", Case::Sensitive));        assert!(!match_file(pattern, "Baz/bar/Foo", Case::Sensitive));    }} #[test]fn basename_glob_and_literal_is_ends_with() {    let pattern = &pat("*foo");    assert!(match_file(pattern, "FoO", Case::Fold));    assert!(match_file(pattern, "BarFoO", Case::Fold));    assert!(!match_file(pattern, "BarFoOo", Case::Fold));    assert!(!match_file(pattern, "Foo", Case::Sensitive));    assert!(!match_file(pattern, "BarFoo", Case::Sensitive));    assert!(match_file(pattern, "barfoo", Case::Sensitive));    assert!(!match_file(pattern, "barfooo", Case::Sensitive));     assert!(match_file(pattern, "bar/foo", Case::Sensitive));    assert!(match_file(pattern, "bar/bazfoo", Case::Sensitive));} #[test]fn special_cases_from_corpus() {    let pattern = &pat("foo*bar");    assert!(        !match_file(pattern, "foo/baz/bar", Case::Sensitive),        "asterisk does not match path separators"    );    let pattern = &pat("*some/path/to/hello.txt");    assert!(        !match_file(pattern, "a/bigger/some/path/to/hello.txt", Case::Sensitive),        "asterisk doesn't match path separators"    );     let pattern = &pat("/*foo.txt");    assert!(match_file(pattern, "hello-foo.txt", Case::Sensitive));    assert!(        !match_file(pattern, "hello/foo.txt", Case::Sensitive),        "absolute single asterisk doesn't match paths"    );} #[test]fn absolute_basename_glob_and_literal_is_ends_with_in_basenames() {    let pattern = &pat("/*foo");     assert!(match_file(pattern, "FoO", Case::Fold));    assert!(match_file(pattern, "BarFoO", Case::Fold));    assert!(!match_file(pattern, "BarFoOo", Case::Fold));    assert!(!match_file(pattern, "Foo", Case::Sensitive));    assert!(!match_file(pattern, "BarFoo", Case::Sensitive));    assert!(match_file(pattern, "barfoo", Case::Sensitive));    assert!(!match_file(pattern, "barfooo", Case::Sensitive));} #[test]fn absolute_basename_glob_and_literal_is_glob_in_paths() {    let pattern = &pat("/*foo");     assert!(!match_file(pattern, "bar/foo", Case::Sensitive), "* does not match /");    assert!(!match_file(pattern, "bar/bazfoo", Case::Sensitive));} #[test]fn negated_patterns_are_handled_by_caller() {    let pattern = &pat("!foo");    assert!(        match_file(pattern, "foo", Case::Sensitive),        "negative patterns match like any other"    );    assert!(        pattern.is_negative(),        "the caller checks for the negative flag and acts accordingly"    );}#[test]fn names_do_not_automatically_match_entire_directories() {    // this feature is implemented with the directory stack.    let pattern = &pat("foo");    assert!(!match_file(pattern, "foobar", Case::Sensitive));    assert!(!match_file(pattern, "foo/bar", Case::Sensitive));    assert!(!match_file(pattern, "foo/bar/baz", Case::Sensitive));} #[test]fn directory_patterns_do_not_match_files_within_a_directory_as_well_like_slash_star_star() {    // this feature is implemented with the directory stack, which excludes entire directories    let pattern = &pat("dir/");    assert!(!match_path(pattern, "dir/file", None, Case::Sensitive));    assert!(!match_path(pattern, "base/dir/file", None, Case::Sensitive));    assert!(!match_path(pattern, "base/ndir/file", None, Case::Sensitive));    assert!(!match_path(pattern, "Dir/File", None, Case::Fold));    assert!(!match_path(pattern, "Base/Dir/File", None, Case::Fold));    assert!(!match_path(pattern, "dir2/file", None, Case::Sensitive));     let pattern = &pat("dir/sub-dir/");    assert!(!match_path(pattern, "dir/sub-dir/file", None, Case::Sensitive));    assert!(!match_path(pattern, "dir/Sub-dir/File", None, Case::Fold));    assert!(!match_path(pattern, "dir/Sub-dir2/File", None, Case::Fold));} #[test]fn single_paths_match_anywhere() {    let pattern = &pat("target");    assert!(match_file(pattern, "dir/target", Case::Sensitive));    assert!(!match_file(pattern, "dir/atarget", Case::Sensitive));    assert!(!match_file(pattern, "dir/targeta", Case::Sensitive));    assert!(match_path(pattern, "dir/target", Some(true), Case::Sensitive));     let pattern = &pat("target/");    assert!(!match_file(pattern, "dir/target", Case::Sensitive));    assert!(        !match_path(pattern, "dir/target", None, Case::Sensitive),        "it assumes unknown to not be a directory"    );    assert!(match_path(pattern, "dir/target", Some(true), Case::Sensitive));    assert!(        !match_path(pattern, "dir/target/", Some(true), Case::Sensitive),        "we need sanitized paths that don't have trailing slashes"    );} fn pat<'a>(pattern: impl Into<&'a BStr>) -> git_glob::Pattern {    git_glob::Pattern::from_bytes(pattern.into()).expect("parsing works")} fn match_file<'a>(pattern: &git_glob::Pattern, path: impl Into<&'a BStr>, case: Case) -> bool {    match_path(pattern, path, false.into(), case)} fn match_path<'a>(pattern: &git_glob::Pattern, path: impl Into<&'a BStr>, is_dir: Option<bool>, case: Case) -> bool {    let path = path.into();    pattern.matches_repo_relative_path(path, basename_start_pos(path), is_dir, case)} fn basename_start_pos(value: &BStr) -> Option<usize> {    value.rfind_byte(b'/').map(|pos| pos + 1)}