Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
radicle/git/canonical: Introduce property tests for glob matching behaviour
Adrian Duke committed 7 days ago
commit 1cf0223564219fb3a702cdaa820e5f2cfdee2048
parent da44aacca1c154755acac28633337b9f729f6abb
1 file changed +161 -0
modified crates/radicle/src/git/canonical/rules.rs
@@ -1195,4 +1195,165 @@ mod tests {
            &git::fmt::qualified!("refs/heads/quarterly/hardened/15-stable/main/2026q2")
        ));
    }
+

+
    #[test]
+
    fn test_matches_properties() {
+
        /*
+
         * See: https://git-scm.com/docs/git-check-ref-format
+
         * They can include slash / for hierarchical (directory) grouping, but no slash-separated component can begin with a dot . or end with the sequence .lock.
+
         * They must contain at least one /. This enforces the presence of a category like heads/, tags/ etc. but the actual names are not restricted. If the --allow-onelevel option is used, this rule is waived.
+
         * They cannot have two consecutive dots .. anywhere.
+
         * They cannot have ASCII control characters (i.e. bytes whose values are lower than \040, or \177 DEL), space, tilde ~, caret ^, or colon : anywhere.
+
         * They cannot have question-mark ?, asterisk *, or open bracket [ anywhere. See the --refspec-pattern option below for an exception to this rule.
+
         * They cannot begin or end with a slash / or contain multiple consecutive slashes (see the --normalize option below for an exception to this rule).
+
         * They cannot end with a dot ..
+
         * They cannot contain a sequence @{.
+
         * They cannot be the single character @.
+
         * They cannot contain a \.
+
         */
+
        fn make_valid_comp(s: String) -> String {
+
            let mut comp: String = s
+
                .chars()
+
                .filter(|&c| {
+
                    let is_control_or_space = c <= ' ' || c == '\x7F';
+
                    let is_forbidden_sym = ['~', '^', ':', '?', '*', '[', '\\', '/'].contains(&c);
+
                    !is_control_or_space && !is_forbidden_sym
+
                })
+
                .collect();
+

+
            while comp.contains("..") {
+
                comp = comp.replace("..", ".");
+
            }
+
            while comp.contains("@{") {
+
                comp = comp.replace("@{", "@");
+
            }
+

+
            if comp.starts_with('.') {
+
                comp.insert(0, 'a');
+
            }
+
            if comp.ends_with(".lock") {
+
                comp.push('a');
+
            }
+
            if comp.ends_with('.') {
+
                comp.push('a');
+
            }
+
            if comp == "@" {
+
                comp.push('a');
+
            }
+

+
            if comp.is_empty() {
+
                comp.push('a');
+
            }
+

+
            comp
+
        }
+

+
        fn to_refname(name: &str) -> git::fmt::Qualified<'_> {
+
            let refstr = git::fmt::RefStr::try_from_str(name).unwrap();
+
            git::fmt::Qualified::from_refstr(refstr).unwrap()
+
        }
+
        fn prop_identity(c1: String, c2: String, c3: String) -> bool {
+
            let c1 = make_valid_comp(c1);
+
            let c2 = make_valid_comp(c2);
+
            let c3 = make_valid_comp(c3);
+

+
            let name = format!("refs/{c1}/{c2}/{c3}");
+

+
            let pattern: RawPattern =
+
                serde_json::from_value(serde_json::Value::String(name.clone())).unwrap();
+

+
            println!("refs/{c1}/{c2}/{c3}");
+
            matches(&pattern, &to_refname(&name))
+
        }
+

+
        fn prop_prefix(c1: String, c2: String, c3: String) -> bool {
+
            let c1 = make_valid_comp(c1);
+
            let c2 = make_valid_comp(c2);
+
            let c3 = make_valid_comp(c3);
+

+
            let pat_str = format!("refs/{c1}/*");
+
            let pattern: RawPattern =
+
                serde_json::from_value(serde_json::Value::String(pat_str)).unwrap();
+

+
            matches(&pattern, &to_refname(&format!("refs/{c1}/{c2}/{c3}")))
+
                && matches(&pattern, &to_refname(&format!("refs/{c1}/{c2}")))
+
                && matches(&pattern, &to_refname(&format!("refs/{c1}/{c3}")))
+
                && matches(&pattern, &to_refname(&format!("refs/{c1}/{c3}/{c2}")))
+
        }
+

+
        fn prop_suffix(c1: String, c2: String, c3: String) -> bool {
+
            let c1 = make_valid_comp(c1);
+
            let c2 = make_valid_comp(c2);
+
            let c3 = make_valid_comp(c3);
+

+
            let pat_str = format!("refs/*/{c3}");
+
            let pattern: RawPattern =
+
                serde_json::from_value(serde_json::Value::String(pat_str)).unwrap();
+

+
            matches(&pattern, &to_refname(&format!("refs/{c1}/{c2}/{c3}")))
+
                && matches(&pattern, &to_refname(&format!("refs/{c2}/{c3}")))
+
                && matches(&pattern, &to_refname(&format!("refs/{c1}/{c3}")))
+
                && matches(&pattern, &to_refname(&format!("refs/{c2}/{c1}/{c3}")))
+
        }
+

+
        fn prop_trailing_asterisk_partial_component(c1: String, c2: String, c3: String) -> bool {
+
            let c1 = make_valid_comp(c1);
+
            let c2 = make_valid_comp(c2);
+
            let c3 = make_valid_comp(c3);
+

+
            let pat_str = format!("refs/{c1}/{c2}*");
+
            let pattern: RawPattern =
+
                serde_json::from_value(serde_json::Value::String(pat_str)).unwrap();
+

+
            matches(&pattern, &to_refname(&format!("refs/{c1}/{c2}{c3}")))
+
                && matches(&pattern, &to_refname(&format!("refs/{c1}/{c2}-{c3}")))
+
                && matches(&pattern, &to_refname(&format!("refs/{c1}/{c2}/{c3}")))
+
        }
+
        fn prop_prefix_negative(c1: String, c2: String, c3: String) -> bool {
+
            let (c1, c2, c3) = (
+
                make_valid_comp(c1),
+
                make_valid_comp(c2),
+
                make_valid_comp(c3),
+
            );
+
            if c1 == c2 {
+
                return true;
+
            }
+

+
            let pat_str = format!("refs/{c1}/*");
+
            let pattern: RawPattern =
+
                serde_json::from_value(serde_json::Value::String(pat_str)).unwrap();
+

+
            !matches(&pattern, &to_refname(&format!("refs/{c2}/{c3}")))
+
                && !matches(&pattern, &to_refname(&format!("refs/{c3}/{c2}")))
+
        }
+

+
        fn prop_suffix_negative(c1: String, c2: String, c3: String) -> bool {
+
            let (c1, c2, c3) = (
+
                make_valid_comp(c1),
+
                make_valid_comp(c2),
+
                make_valid_comp(c3),
+
            );
+
            if c2 == c3 {
+
                return true;
+
            }
+

+
            let pat_str = format!("refs/*/{c3}");
+
            let pattern: RawPattern =
+
                serde_json::from_value(serde_json::Value::String(pat_str)).unwrap();
+

+
            !matches(&pattern, &to_refname(&format!("refs/{c1}/{c2}")))
+
                && !matches(&pattern, &to_refname(&format!("refs/{c2}/{c1}")))
+
        }
+

+
        qcheck::QuickCheck::new().quickcheck(prop_identity as fn(String, String, String) -> bool);
+
        qcheck::QuickCheck::new().quickcheck(prop_prefix as fn(String, String, String) -> bool);
+
        qcheck::QuickCheck::new().quickcheck(prop_suffix as fn(String, String, String) -> bool);
+
        qcheck::QuickCheck::new().quickcheck(
+
            prop_trailing_asterisk_partial_component as fn(String, String, String) -> bool,
+
        );
+
        qcheck::QuickCheck::new()
+
            .quickcheck(prop_prefix_negative as fn(String, String, String) -> bool);
+
        qcheck::QuickCheck::new()
+
            .quickcheck(prop_suffix_negative as fn(String, String, String) -> bool);
+
    }
}