Skip to main content

testing_conventions/
tiers.rs

1//! The standard suite-tier layout, derived from the package root.
2//!
3//! The standard places a package's test suites at fixed locations relative to
4//! its package root: colocated unit tests beside the sources, the integration
5//! suite in `tests/integration/`, and the e2e suite in `tests/e2e/` (Rust's
6//! cargo layout keeps both out-of-crate suites in the crate root's `tests/`).
7//! The scans derive those locations from the scanned `path` and the package's
8//! own manifest — `integration lint` takes its subjects from the derived suite
9//! directories, and the unit-tier scans leave `<package root>/tests/` to them.
10
11use std::path::{Path, PathBuf};
12
13/// The package root for `scan_root`: the nearest directory at or above it
14/// holding `manifest` (`pyproject.toml`, `package.json`, or `Cargo.toml`).
15/// The walk stops at a `.git` boundary so it cannot escape the repository into
16/// an unrelated manifest. `None` when no manifest is found — a loose-script
17/// tree, scanned at `scan_root` directly.
18pub fn package_root(scan_root: &Path, manifest: &str) -> Option<PathBuf> {
19    for dir in scan_root.ancestors() {
20        if dir.join(manifest).is_file() {
21            return Some(dir.to_path_buf());
22        }
23        if dir.join(".git").exists() {
24            break;
25        }
26    }
27    None
28}
29
30/// The `<package root>/tests/` directory `scan_root` belongs to, or `None` for
31/// a loose-script tree. The unit-tier scans skip every file under it — that
32/// subtree belongs to the suite tiers.
33pub fn suite_tests_dir(scan_root: &Path, manifest: &str) -> Option<PathBuf> {
34    package_root(scan_root, manifest).map(|root| root.join("tests"))
35}
36
37#[cfg(test)]
38mod tests {
39    use std::path::PathBuf;
40    use std::sync::atomic::{AtomicU64, Ordering};
41
42    use super::{package_root, suite_tests_dir};
43
44    /// A throwaway directory tree, removed on drop.
45    struct TempTree(PathBuf);
46
47    impl TempTree {
48        fn new() -> Self {
49            static COUNTER: AtomicU64 = AtomicU64::new(0);
50            let dir = std::env::temp_dir().join(format!(
51                "tc-tiers-{}-{}",
52                std::process::id(),
53                COUNTER.fetch_add(1, Ordering::Relaxed),
54            ));
55            std::fs::create_dir_all(&dir).unwrap();
56            TempTree(dir)
57        }
58
59        fn touch(&self, name: &str) {
60            let path = self.0.join(name);
61            std::fs::create_dir_all(path.parent().unwrap()).unwrap();
62            std::fs::write(path, "").unwrap();
63        }
64    }
65
66    impl Drop for TempTree {
67        fn drop(&mut self) {
68            let _ = std::fs::remove_dir_all(&self.0);
69        }
70    }
71
72    #[test]
73    fn finds_the_nearest_manifest_above_the_scan_root() {
74        let tree = TempTree::new();
75        tree.touch("pkg/pyproject.toml");
76        tree.touch("pkg/src/widget.py");
77        assert_eq!(
78            package_root(&tree.0.join("pkg/src"), "pyproject.toml"),
79            Some(tree.0.join("pkg")),
80        );
81    }
82
83    #[test]
84    fn the_scan_root_itself_can_be_the_package_root() {
85        let tree = TempTree::new();
86        tree.touch("pkg/package.json");
87        assert_eq!(
88            package_root(&tree.0.join("pkg"), "package.json"),
89            Some(tree.0.join("pkg")),
90        );
91    }
92
93    #[test]
94    fn the_walk_stops_at_a_git_boundary() {
95        let tree = TempTree::new();
96        tree.touch("Cargo.toml");
97        tree.touch("repo/.git/HEAD");
98        tree.touch("repo/src/lib.rs");
99        assert_eq!(package_root(&tree.0.join("repo/src"), "Cargo.toml"), None);
100    }
101
102    #[test]
103    fn a_manifest_at_the_git_boundary_is_still_found() {
104        let tree = TempTree::new();
105        tree.touch("repo/.git/HEAD");
106        tree.touch("repo/pyproject.toml");
107        assert_eq!(
108            package_root(&tree.0.join("repo"), "pyproject.toml"),
109            Some(tree.0.join("repo")),
110        );
111    }
112
113    #[test]
114    fn suite_tests_dir_is_the_package_roots_tests() {
115        let tree = TempTree::new();
116        tree.touch(".git/HEAD");
117        tree.touch("pkg/pyproject.toml");
118        assert_eq!(
119            suite_tests_dir(&tree.0.join("pkg"), "pyproject.toml"),
120            Some(tree.0.join("pkg/tests")),
121        );
122        assert_eq!(suite_tests_dir(&tree.0, "pyproject.toml"), None);
123    }
124}