Skip to main content

testing_conventions/
tiers.rs

1//! The standard suite-tier layout, derived from the package root.
2
3use std::path::{Path, PathBuf};
4
5/// The nearest directory at or above `scan_root` holding `manifest`, or `None`.
6/// The walk stops at a `.git` boundary so it cannot escape the repository.
7pub fn package_root(scan_root: &Path, manifest: &str) -> Option<PathBuf> {
8    for dir in scan_root.ancestors() {
9        if dir.join(manifest).is_file() {
10            return Some(dir.to_path_buf());
11        }
12        if dir.join(".git").exists() {
13            break;
14        }
15    }
16    None
17}
18
19/// The `<package root>/tests/` directory `scan_root` belongs to, or `None`.
20pub fn suite_tests_dir(scan_root: &Path, manifest: &str) -> Option<PathBuf> {
21    package_root(scan_root, manifest).map(|root| root.join("tests"))
22}
23
24#[cfg(test)]
25mod tests {
26    use std::path::PathBuf;
27    use std::sync::atomic::{AtomicU64, Ordering};
28
29    use super::{package_root, suite_tests_dir};
30
31    struct TempTree(PathBuf);
32
33    impl TempTree {
34        fn new() -> Self {
35            static COUNTER: AtomicU64 = AtomicU64::new(0);
36            let dir = std::env::temp_dir().join(format!(
37                "tc-tiers-{}-{}",
38                std::process::id(),
39                COUNTER.fetch_add(1, Ordering::Relaxed),
40            ));
41            std::fs::create_dir_all(&dir).unwrap();
42            TempTree(dir)
43        }
44
45        fn touch(&self, name: &str) {
46            let path = self.0.join(name);
47            std::fs::create_dir_all(path.parent().unwrap()).unwrap();
48            std::fs::write(path, "").unwrap();
49        }
50    }
51
52    impl Drop for TempTree {
53        fn drop(&mut self) {
54            let _ = std::fs::remove_dir_all(&self.0);
55        }
56    }
57
58    #[test]
59    fn finds_the_nearest_manifest_above_the_scan_root() {
60        let tree = TempTree::new();
61        tree.touch("pkg/pyproject.toml");
62        tree.touch("pkg/src/widget.py");
63        assert_eq!(
64            package_root(&tree.0.join("pkg/src"), "pyproject.toml"),
65            Some(tree.0.join("pkg")),
66        );
67    }
68
69    #[test]
70    fn the_scan_root_itself_can_be_the_package_root() {
71        let tree = TempTree::new();
72        tree.touch("pkg/package.json");
73        assert_eq!(
74            package_root(&tree.0.join("pkg"), "package.json"),
75            Some(tree.0.join("pkg")),
76        );
77    }
78
79    #[test]
80    fn the_walk_stops_at_a_git_boundary() {
81        let tree = TempTree::new();
82        tree.touch("Cargo.toml");
83        tree.touch("repo/.git/HEAD");
84        tree.touch("repo/src/lib.rs");
85        assert_eq!(package_root(&tree.0.join("repo/src"), "Cargo.toml"), None);
86    }
87
88    #[test]
89    fn a_manifest_at_the_git_boundary_is_still_found() {
90        let tree = TempTree::new();
91        tree.touch("repo/.git/HEAD");
92        tree.touch("repo/pyproject.toml");
93        assert_eq!(
94            package_root(&tree.0.join("repo"), "pyproject.toml"),
95            Some(tree.0.join("repo")),
96        );
97    }
98
99    #[test]
100    fn suite_tests_dir_is_the_package_roots_tests() {
101        let tree = TempTree::new();
102        tree.touch(".git/HEAD");
103        tree.touch("pkg/pyproject.toml");
104        assert_eq!(
105            suite_tests_dir(&tree.0.join("pkg"), "pyproject.toml"),
106            Some(tree.0.join("pkg/tests")),
107        );
108        assert_eq!(suite_tests_dir(&tree.0, "pyproject.toml"), None);
109    }
110}