Skip to main content

skipper_core/
testid.rs

1use std::path::{Path, PathBuf};
2use std::sync::OnceLock;
3
4/// Canonical form used for every lookup: trim, lowercase, collapse whitespace.
5pub fn normalize_test_id(id: &str) -> String {
6    id.split_whitespace()
7        .collect::<Vec<_>>()
8        .join(" ")
9        .to_lowercase()
10}
11
12/// `relative/path.rs > part1 > part2`. The path is made relative to the
13/// project root (see [`find_project_root`]) and always uses `/`.
14pub fn build_test_id<S: AsRef<str>>(file: &str, parts: &[S]) -> String {
15    let mut id = relative_to_root(file);
16    for p in parts {
17        id.push_str(" > ");
18        id.push_str(p.as_ref());
19    }
20    id
21}
22
23/// Make `file` relative to the project root. Relative paths (what `file!()`
24/// yields for workspace members) are kept as they are.
25pub fn relative_to_root(file: &str) -> String {
26    let path = Path::new(file);
27    let rel = if path.is_absolute() {
28        path.strip_prefix(project_root()).unwrap_or(path)
29    } else {
30        path
31    };
32    rel.to_string_lossy().replace('\\', "/")
33}
34
35fn project_root() -> &'static Path {
36    static ROOT: OnceLock<PathBuf> = OnceLock::new();
37    ROOT.get_or_init(|| {
38        let start = std::env::var_os("CARGO_MANIFEST_DIR")
39            .map(PathBuf::from)
40            .or_else(|| std::env::current_dir().ok())
41            .unwrap_or_default();
42        find_project_root(&start)
43    })
44}
45
46/// Outermost ancestor whose `Cargo.toml` declares `[workspace]`, else the
47/// nearest directory with a `Cargo.toml`, else `start` itself.
48pub fn find_project_root(start: &Path) -> PathBuf {
49    let mut nearest = None;
50    let mut workspace = None;
51    for dir in start.ancestors() {
52        let Ok(manifest) = std::fs::read_to_string(dir.join("Cargo.toml")) else {
53            continue;
54        };
55        nearest.get_or_insert_with(|| dir.to_path_buf());
56        if manifest.lines().any(|l| l.trim() == "[workspace]") {
57            workspace = Some(dir.to_path_buf());
58        }
59    }
60    workspace.or(nearest).unwrap_or_else(|| start.to_path_buf())
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn normalizes() {
69        assert_eq!(
70            normalize_test_id("  Tests/Auth.rs  >\tLogin   >  Works "),
71            "tests/auth.rs > login > works"
72        );
73    }
74
75    #[test]
76    fn builds_with_forward_slashes() {
77        assert_eq!(
78            build_test_id("tests\\auth.rs", &["mod_a", "it works"]),
79            "tests/auth.rs > mod_a > it works"
80        );
81    }
82
83    #[test]
84    fn finds_outermost_workspace() {
85        let dir = tempfile::tempdir().unwrap();
86        let ws = dir.path();
87        let member = ws.join("crates/a");
88        std::fs::create_dir_all(member.join("src")).unwrap();
89        std::fs::write(ws.join("Cargo.toml"), "[workspace]\nmembers=[]\n").unwrap();
90        std::fs::write(member.join("Cargo.toml"), "[package]\nname=\"a\"\n").unwrap();
91        assert_eq!(find_project_root(&member.join("src")), ws);
92
93        std::fs::write(ws.join("Cargo.toml"), "# not a workspace\n").unwrap();
94        assert_eq!(find_project_root(&member.join("src")), member);
95    }
96}