asmov_common_testing/
namepath.rs

1use std::{fmt::Display, hash::Hash, path::{Path, PathBuf}, sync::LazyLock};
2use crate::*;
3
4/// Describes the normalized namepath of a [TestModule], [TestGroup], or [Test].
5///
6/// Module and Test reflect their [Rust path](https://doc.rust-lang.org/reference/paths.html).
7///
8/// Group uses an arbitrary path.
9///
10/// Components such as '::tests' are removed.
11///
12/// Module: Created using [module_path!()]
13///
14/// Test: Created using [module_path!()] and [function_name]
15///
16/// Group: Created using an arbitrary slash-separated slug (e.g., "foo/hat-cat")
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Namepath {
19    pub(crate) full_path: PathBuf,
20    pub(crate) raw: RawNamepath
21}
22
23impl Namepath {
24    pub fn new_module(package_name: &'static str, use_case: UseCase, module_path: &'static str) -> anyhow::Result<Self> {
25        let raw = RawNamepath {
26            kind: TestingKind::Module,
27            use_case,
28            package_name,
29            path: module_path,
30            name: None,
31        };
32
33        let full_path = normalize_path(&raw)?;
34
35        Ok(Self {
36            full_path,
37            raw,
38        })
39    }
40
41    pub fn new_group(package_name: &'static str, use_case: UseCase, path: &'static str) -> anyhow::Result<Self> {
42        let raw = RawNamepath {
43            kind: TestingKind::Group,
44            use_case,
45            package_name,
46            path,
47            name: None,
48        };
49
50        let full_path = normalize_path(&raw)?;
51
52        Ok(Self {
53            full_path,
54            raw,
55        })
56    }
57
58    pub fn new_test(package_name: &'static str, use_case: UseCase, module_path: &'static str, function_name: &'static str) -> anyhow::Result<Self> {
59        let raw = RawNamepath {
60            kind: TestingKind::Test,
61            use_case,
62            package_name,
63            path: module_path,
64            name: Some(function_name),
65        };
66
67        let full_path = normalize_path(&raw)?;
68
69        Ok(Self {
70            full_path,
71            raw,
72        })
73    }
74
75    /// The normalized path.
76    /// Eg., `use-case/module-path../function-name`
77    pub fn path(&self) -> &Path {
78        let mut components = self.full_path.components();
79        components.next();
80        components.as_path()
81    }
82
83    /// The normalized path, including its package name.
84    /// Eg., `package-name/use-case/module-path../function-name`
85    pub fn full_path(&self) -> &Path {
86        &self.full_path
87    }
88
89    pub fn full_path_to_squashed_slug(&self) -> String {
90        self.full_path
91            .to_str().expect("Invalid namepath")
92            .replace("/", "-")
93    }
94
95    /// The crate name or equivalent
96    pub fn package_name(&self) -> &str {
97        self.full_path.components()
98            .next().expect("Invalid path")
99            .as_os_str().to_str().expect("Invalid path")
100    }
101
102    /// The kind of testing model this namepath refers to
103    pub fn kind(&self) -> TestingKind {
104        self.raw.kind
105    }
106
107    // The testing use-case
108    pub fn use_case(&self) -> UseCase {
109        self.raw.use_case
110    }
111
112    /// The base name
113    /// Eg., a path of `unit/mymod/foo/bar` has the name: `bar`
114    pub fn name(&self) -> &str {
115        self.full_path.components()
116            .last().expect("Invalid namepath")
117            .as_os_str().to_str().expect("Invalid path")
118    }
119
120    pub fn raw(&self) -> &RawNamepath {
121        &self.raw
122    }
123}
124
125impl Display for Namepath {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        self.path().display().fmt(f)
128    }
129}
130
131impl Hash for Namepath {
132    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
133        self.full_path().hash(state);
134    }
135}
136
137/// Retains the original elements used to construct a [Namepath]
138///
139/// Primarily used for debugging namepath problems.
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct RawNamepath {
142    pub kind: TestingKind,
143    pub use_case: UseCase,
144    pub package_name: &'static str,
145    pub path: &'static str,
146    pub name: Option<&'static str>
147}
148
149impl Display for RawNamepath {
150    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151        write!(f, "{};{};{};{}", self.kind, self.use_case, self.package_name, self.path)?;
152
153        if let Some(name) = &self.name {
154            write!(f, ";{}", name)?;
155        }
156
157        Ok(())
158    }
159}
160
161/// Sanitizes paths if they've been created using module_path!() and function_name!().
162/// Strips the crate name prefix and the test/tests suffix.
163/// If the path is from lib.rs, the crate name is returned.
164/// Primarily for module and Test. Group uses plain strings.
165fn normalize_path(raw: &RawNamepath) -> anyhow::Result<PathBuf> {
166    static REGEX_NAMESPACE_INTEGRATION: LazyLock<regex::Regex> = LazyLock::new(|| {
167        regex::Regex::new(r"^(.+?)(?:::tests)?$").unwrap()
168    });
169
170    static REGEX_NAMESPACE_UNIT: LazyLock<regex::Regex> = LazyLock::new(|| {
171        regex::Regex::new(r"^\w+::(.+?)(?:::tests)?$").unwrap()
172    });
173
174    // Group doesn't use a module_path!() / function_name!()
175    let path = if raw.kind == TestingKind::Group {
176        &raw.path
177    } else {
178        let captures = match raw.use_case {
179            UseCase::Integration => REGEX_NAMESPACE_INTEGRATION.captures(&raw.path),
180            UseCase::Unit => REGEX_NAMESPACE_UNIT.captures(&raw.path)
181        };
182
183        match captures {
184            Some(captures) => captures.get(1).unwrap().as_str(),
185            None => ""
186        }
187    };
188
189    let full_path = format!("{package}/{use_case}/{path}{slash_name}",
190        package = &raw.package_name,
191        use_case = &raw.use_case,
192        path = &path
193            .replace("::", "/")
194            .replace("_", "-"),
195        slash_name = raw.name.as_ref()
196            .map(|name| format!("/{}", name.replace("_", "-")))
197            .unwrap_or_default()
198    );
199
200    Ok(PathBuf::from(full_path))
201}
202
203#[cfg(test)]
204mod tests {
205    use crate::prelude::*;
206
207    // NAMEPATH TESTING
208    // See the namepaths.rs integration test for the master copy
209    static TESTING: testing::Module = testing::module!(Unit);
210
211    const GROUP_NAME: &'static str = "namepath-group/uno/dos";
212    static GROUP: testing::Group = testing::group!(GROUP_NAME, Unit);
213
214    #[named]
215    #[test]
216    fn test_unit_namepath() {
217        const EXPECTED_PACKAGE_NAME: &'static str = "asmov-common-testing";
218        const EXPECTED_USE_CASE: testing::UseCase = testing::UseCase::Unit;
219
220        const EXPECTED_MODULE_KIND: testing::TestingKind = testing::TestingKind::Module;
221        const EXPECTED_MODULE_FULL_PATH: &'static str = "asmov-common-testing/unit/namepath";
222        const EXPECTED_MODULE_PATH: &'static str = "unit/namepath";
223        const EXPECTED_MODULE_NAME: &'static str = "namepath";
224        const EXPECTED_MODULE_RAW: &'static str = "module;unit;asmov-common-testing;asmov_common_testing::namepath::tests";
225
226        const EXPECTED_GROUP_KIND: testing::TestingKind = testing::TestingKind::Group;
227        const EXPECTED_GROUP_FULL_PATH: &'static str = "asmov-common-testing/unit/namepath-group/uno/dos";
228        const EXPECTED_GROUP_PATH: &'static str = "unit/namepath-group/uno/dos";
229        const EXPECTED_GROUP_NAME: &'static str = "dos";
230        const EXPECTED_GROUP_RAW: &'static str = "group;unit;asmov-common-testing;namepath-group/uno/dos";
231
232        const EXPECTED_TEST_KIND: testing::TestingKind = testing::TestingKind::Test;
233        const EXPECTED_TEST_FULL_PATH: &'static str = "asmov-common-testing/unit/namepath/test-unit-namepath";
234        const EXPECTED_TEST_PATH: &'static str = "unit/namepath/test-unit-namepath";
235        const EXPECTED_TEST_NAME: &'static str = "test-unit-namepath";
236        const EXPECTED_TEST_RAW: &'static str = "test;unit;asmov-common-testing;asmov_common_testing::namepath::tests;test_unit_namepath";
237
238        // Module
239        assert_eq!(EXPECTED_PACKAGE_NAME, TESTING.namepath().package_name());
240        assert_eq!(EXPECTED_MODULE_KIND, TESTING.namepath().kind());
241        assert_eq!(EXPECTED_USE_CASE, TESTING.namepath().use_case());
242        assert_eq!(EXPECTED_MODULE_FULL_PATH, TESTING.namepath().full_path().to_string_lossy());
243        assert_eq!(EXPECTED_MODULE_PATH, TESTING.namepath().path().to_string_lossy());
244        assert_eq!(EXPECTED_MODULE_NAME, TESTING.namepath().name());
245        assert_eq!(EXPECTED_MODULE_RAW, TESTING.namepath().raw().to_string());
246
247        // Group
248        assert_eq!(EXPECTED_PACKAGE_NAME, GROUP.namepath().package_name());
249        assert_eq!(EXPECTED_GROUP_KIND, GROUP.namepath().kind());
250        assert_eq!(EXPECTED_USE_CASE, GROUP.namepath().use_case());
251        assert_eq!(EXPECTED_GROUP_FULL_PATH, GROUP.namepath().full_path().to_string_lossy());
252        assert_eq!(EXPECTED_GROUP_PATH, GROUP.namepath().path().to_string_lossy());
253        assert_eq!(EXPECTED_GROUP_NAME, GROUP.namepath().name());
254        assert_eq!(EXPECTED_GROUP_RAW, GROUP.namepath().raw().to_string());
255
256        // Test
257        let test = testing::test!();
258        assert_eq!(EXPECTED_PACKAGE_NAME, test.namepath().package_name());
259        assert_eq!(EXPECTED_TEST_KIND, test.namepath().kind());
260        assert_eq!(EXPECTED_USE_CASE, test.namepath().use_case());
261        assert_eq!(EXPECTED_TEST_FULL_PATH, test.namepath().full_path().to_string_lossy());
262        assert_eq!(EXPECTED_TEST_PATH, test.namepath().path().to_string_lossy());
263        assert_eq!(EXPECTED_TEST_NAME, test.namepath().name());
264        assert_eq!(EXPECTED_TEST_RAW, test.namepath().raw().to_string());
265    }
266}