tokmd-path 1.9.0

Single-responsibility path normalization helpers for deterministic cross-platform matching.
Documentation
1
//! W57 depth tests for tokmd-path: exhaustive edge-case, ordering,//! unicode, Windows, and proptest coverage for normalize_slashes//! and normalize_rel_path.use tokmd_path::{normalize_rel_path, normalize_slashes};// ═══════════════════════════════════════════════════════════════════// normalize_slashes — edge cases// ═══════════════════════════════════════════════════════════════════#[test]fn slashes_empty_input() {    assert_eq!(normalize_slashes(""), "");}#[test]fn slashes_single_dot() {    assert_eq!(normalize_slashes("."), ".");}#[test]fn slashes_double_dot() {    assert_eq!(normalize_slashes(".."), "..");}#[test]fn slashes_mixed_separators_complex() {    assert_eq!(normalize_slashes(r"a/b\c/d\e/f"), "a/b/c/d/e/f");}#[test]fn slashes_trailing_backslash_preserved_as_forward() {    assert_eq!(normalize_slashes(r"dir\sub\"), "dir/sub/");}#[test]fn slashes_trailing_forward_slash_unchanged() {    assert_eq!(normalize_slashes("dir/sub/"), "dir/sub/");}#[test]fn slashes_leading_dot_slash() {    assert_eq!(normalize_slashes(r".\foo\bar"), "./foo/bar");}#[test]fn slashes_multiple_consecutive_backslashes() {    assert_eq!(normalize_slashes(r"a\\\b"), "a///b");}#[test]fn slashes_multiple_consecutive_forward_slashes_unchanged() {    assert_eq!(normalize_slashes("a///b"), "a///b");}#[test]fn slashes_unicode_cjk_path() {    assert_eq!(normalize_slashes(r"项目\源码\main.rs"), "项目/源码/main.rs");}#[test]fn slashes_unicode_emoji_path() {    assert_eq!(normalize_slashes(r"📁\📄"), "📁/📄");}#[test]fn slashes_unicode_arabic_path() {    assert_eq!(normalize_slashes(r"مشروع\ملف.rs"), "مشروع/ملف.rs");}#[test]fn slashes_windows_drive_letter_uppercase() {    assert_eq!(normalize_slashes(r"C:\Users\me\code"), "C:/Users/me/code");}#[test]fn slashes_windows_drive_letter_lowercase() {    assert_eq!(normalize_slashes(r"d:\data\file.txt"), "d:/data/file.txt");}#[test]fn slashes_unc_path_double_backslash() {    assert_eq!(        normalize_slashes(r"\\server\share\dir\file"),        "//server/share/dir/file"    );}#[test]fn slashes_unc_path_long() {    assert_eq!(        normalize_slashes(r"\\?\C:\very\long\path"),        "//?/C:/very/long/path"    );}#[test]fn slashes_only_backslashes() {    assert_eq!(normalize_slashes(r"\\\"), "///");}#[test]fn slashes_only_forward_slashes() {    assert_eq!(normalize_slashes("///"), "///");}// ═══════════════════════════════════════════════════════════════════// normalize_rel_path — edge cases// ═══════════════════════════════════════════════════════════════════#[test]fn rel_empty_string_returns_empty() {    assert_eq!(normalize_rel_path(""), "");}#[test]fn rel_single_dot_returns_dot() {    // "." doesn't start with "./" so stays    assert_eq!(normalize_rel_path("."), ".");}#[test]fn rel_double_dot_preserved() {    assert_eq!(normalize_rel_path(".."), "..");}#[test]fn rel_dot_slash_dot_dot() {    // "./../foo" -> strip "./" -> "../foo"    assert_eq!(normalize_rel_path("./../foo"), "../foo");}#[test]fn rel_multiple_dot_slash_prefix() {    assert_eq!(normalize_rel_path("./././file.txt"), "file.txt");}#[test]fn rel_dot_slash_then_dot_file() {    assert_eq!(normalize_rel_path("./.gitignore"), ".gitignore");}#[test]fn rel_mixed_separators_with_dot_prefix() {    assert_eq!(normalize_rel_path(r".\foo/bar\baz"), "foo/bar/baz");}#[test]fn rel_unicode_with_dot_prefix() {    assert_eq!(        normalize_rel_path(r".\données\résumé.md"),        "données/résumé.md"    );}#[test]fn rel_windows_drive_not_stripped() {    // "C:\foo" doesn't start with "./" so stays (just normalized)    assert_eq!(normalize_rel_path(r"C:\foo\bar.rs"), "C:/foo/bar.rs");}#[test]fn rel_consecutive_slashes_after_dot() {    // ".//foo" -> strip "./" -> "/foo"    assert_eq!(normalize_rel_path(".//foo"), "/foo");}#[test]fn rel_deeply_nested_with_dot_prefix() {    assert_eq!(        normalize_rel_path("./a/b/c/d/e/f/g/h.rs"),        "a/b/c/d/e/f/g/h.rs"    );}// ═══════════════════════════════════════════════════════════════════// Forward-slash normalization — exhaustive// ═══════════════════════════════════════════════════════════════════#[test]fn forward_slash_all_common_patterns() {    let cases = [        (r"a\b\c", "a/b/c"),        (r"\a\b\c", "/a/b/c"),        (r"a\b\c\", "a/b/c/"),        ("a/b/c", "a/b/c"),        (r"a/b\c/d", "a/b/c/d"),        (r"\\a\\b", "//a//b"),        ("", ""),        ("no_sep", "no_sep"),    ];    for (input, expected) in &cases {        assert_eq!(            normalize_slashes(input),            *expected,            "Failed for input: {input:?}"        );    }}#[test]fn forward_slash_never_appears_as_backslash_in_output() {    let inputs = [        r"a\b\c\d\e",        r"\\server\share",        r"C:\Program Files\app",        r".\relative\path",        r"..\parent\path",        r"\\\triple",        r"mixed/and\back",        r"trailing\",    ];    for input in &inputs {        let result = normalize_slashes(input);        assert!(            !result.contains('\\'),            "Backslash found in output for {input:?}: {result}"        );    }}// ═══════════════════════════════════════════════════════════════════// Path comparison / ordering determinism// ═══════════════════════════════════════════════════════════════════#[test]fn ordering_normalized_paths_are_lexicographically_sorted() {    let mut paths: Vec<String> = vec![        r"src\zzz.rs",        r"src\aaa.rs",        r"lib\bbb.rs",        r"crates\foo\bar.rs",    ]    .into_iter()    .map(normalize_slashes)    .collect();    paths.sort();    assert_eq!(        paths,        vec![            "crates/foo/bar.rs",            "lib/bbb.rs",            "src/aaa.rs",            "src/zzz.rs",        ]    );}#[test]fn ordering_equivalent_paths_sort_identically() {    let a = normalize_slashes(r"src\main.rs");    let b = normalize_slashes("src/main.rs");    assert_eq!(a.cmp(&b), std::cmp::Ordering::Equal);}#[test]fn ordering_rel_paths_deterministic_sort() {    let mut paths: Vec<String> = vec![r".\src\b.rs", r".\src\a.rs", "./lib/c.rs", r".\tools\d.rs"]        .into_iter()        .map(normalize_rel_path)        .collect();    paths.sort();    assert_eq!(        paths,        vec!["lib/c.rs", "src/a.rs", "src/b.rs", "tools/d.rs",]    );}#[test]fn ordering_repeated_normalization_preserves_order() {    let inputs = [r"b\2.rs", r"a\1.rs", r"c\3.rs"];    let mut first: Vec<String> = inputs.iter().map(|p| normalize_slashes(p)).collect();    let mut second: Vec<String> = inputs.iter().map(|p| normalize_slashes(p)).collect();    first.sort();    second.sort();    assert_eq!(first, second);}// ═══════════════════════════════════════════════════════════════════// Cross-function consistency// ═══════════════════════════════════════════════════════════════════#[test]fn rel_path_always_subset_of_slash_normalization() {    let inputs = [        "./foo/bar",        r".\foo\bar",        "plain/path",        r"a\b",        "././nested",        "./",        "",    ];    for input in &inputs {        let slash = normalize_slashes(input);        let rel = normalize_rel_path(input);        assert!(            rel.len() <= slash.len(),            "rel ({rel:?}) longer than slashes ({slash:?}) for {input:?}"        );    }}#[test]fn rel_output_is_suffix_of_slashes_output() {    let inputs = [        "./foo/bar.rs",        r".\crates\foo\lib.rs",        "src/lib.rs",        "././nested/deep/file.rs",    ];    for input in &inputs {        let slash = normalize_slashes(input);        let rel = normalize_rel_path(input);        assert!(            slash.ends_with(&rel),            "rel {rel:?} not suffix of slashes {slash:?} for {input:?}"        );    }}#[test]fn idempotency_both_functions() {    let inputs = [        r".\foo\bar",        r"a\b\c",        "./x/y/z",        "plain",        "",        ".",        "..",        r"C:\drive",        r"\\unc\path",    ];    for input in &inputs {        let s1 = normalize_slashes(input);        let s2 = normalize_slashes(&s1);        assert_eq!(s1, s2, "normalize_slashes not idempotent for {input:?}");        let r1 = normalize_rel_path(input);        let r2 = normalize_rel_path(&r1);        assert_eq!(r1, r2, "normalize_rel_path not idempotent for {input:?}");    }}// ═══════════════════════════════════════════════════════════════════// proptest// ═══════════════════════════════════════════════════════════════════mod proptests {    use super::*;    use proptest::prelude::*;    proptest! {        #[test]        fn slashes_output_never_contains_backslash(path in "\\PC{0,200}") {            let result = normalize_slashes(&path);            prop_assert!(!result.contains('\\'),                "backslash in output for {:?}: {}", path, result);        }        #[test]        fn slashes_idempotent(path in "\\PC{0,200}") {            let once = normalize_slashes(&path);            let twice = normalize_slashes(&once);            prop_assert_eq!(&once, &twice);        }        #[test]        fn slashes_preserves_length(path in "\\PC{0,200}") {            let result = normalize_slashes(&path);            prop_assert_eq!(result.len(), path.len(),                "length changed for {:?}", path);        }        #[test]        fn rel_output_never_contains_backslash(path in "\\PC{0,200}") {            let result = normalize_rel_path(&path);            prop_assert!(!result.contains('\\'),                "backslash in output for {:?}: {}", path, result);        }        #[test]        fn rel_idempotent(path in "\\PC{0,200}") {            let once = normalize_rel_path(&path);            let twice = normalize_rel_path(&once);            prop_assert_eq!(&once, &twice);        }        #[test]        fn rel_output_no_leading_dot_slash(path in "\\PC{0,200}") {            let result = normalize_rel_path(&path);            prop_assert!(!result.starts_with("./"),                "leading ./ in output for {:?}: {}", path, result);        }        #[test]        fn slashes_forward_slash_count_gte_input(path in "\\PC{0,200}") {            let input_fwd = path.chars().filter(|c| *c == '/').count();            let input_back = path.chars().filter(|c| *c == '\\').count();            let output_fwd = normalize_slashes(&path).chars().filter(|c| *c == '/').count();            prop_assert!(output_fwd >= input_fwd,                "forward slashes decreased for {:?}", path);            prop_assert_eq!(output_fwd, input_fwd + input_back,                "forward slashes != fwd + back for {:?}", path);        }    }}