asmov_common_testing/
group.rs

1use std::{ffi::OsStr, ops::Deref, path::{Path, PathBuf}, sync::LazyLock};
2use crate::*;
3
4/// Standalone top-level testing group.
5///
6/// Lives outside of the Module -> Test heirarchy.
7///
8/// Useful for grouping common fixtures, setup/teardown, and temp file operations.
9pub struct TestGroup {
10    pub(crate) use_case: UseCase,
11    pub(crate) namepath: Namepath,
12    pub(crate) temp_dir: Option<PathBuf>,
13    pub(crate) base_temp_dir: Option<PathBuf>,
14    pub(crate) fixture_dir: Option<PathBuf>,
15}
16
17impl TestGroup {
18    pub fn base_temp_dir(&self) -> &Path {
19        &self.base_temp_dir.as_ref().context("Module `base temp dir` is not configured").unwrap()
20    }
21}
22
23impl Testing for TestGroup {
24    fn use_case(&self) -> UseCase {
25         self.use_case
26    }
27
28    fn namepath(&self) -> &Namepath {
29        &self.namepath
30    }
31
32    fn fixture_dir(&self) -> &Path {
33        &self.fixture_dir.as_ref().context("Group `fixture dir` is not configured").unwrap()
34    }
35
36    fn temp_dir(&self) -> &Path {
37        self.temp_dir.as_ref().context("Group `temp dir` is not configured").unwrap()
38    }
39}
40
41/// Constructs a [TestGroup]
42pub struct GroupBuilder<'func> {
43    pub(crate) package_name: &'static str,
44    pub(crate) use_case: UseCase,
45    pub(crate) group_path: &'static str,
46    pub(crate) base_temp_dir: PathBuf,
47    pub(crate) using_temp_dir: bool,
48    pub(crate) using_fixture_dir: bool,
49    pub(crate) setup_func: Option<Box<dyn FnOnce(&mut TestGroup) + 'func>>,
50    pub(crate) static_teardown_func: Option<extern "C" fn()>,
51}
52
53impl<'func> GroupBuilder<'func> {
54    pub fn new(package_name: &'static str, use_case: UseCase, group_path: &'static str) -> Self {
55        Self {
56            package_name,
57            use_case,
58            group_path,
59            base_temp_dir: std::env::temp_dir(),
60            using_temp_dir: false,
61            using_fixture_dir: false,
62            setup_func: None,
63            static_teardown_func: None,
64        }
65    }
66
67    pub fn build(mut self) -> TestGroup {
68        let namepath = Namepath::new_group(self.package_name, self.use_case, self.group_path)
69            .expect("Invalid namepath");
70
71        let base_temp_dir;
72        let temp_dir = if self.using_temp_dir {
73            let dirname = namepath.full_path_to_squashed_slug();
74            base_temp_dir = Some(create_random_subdir(&self.base_temp_dir, &dirname) // todo: use squashed prefix
75                .context(format!("Unable to create temporary directory in base: {}", &self.base_temp_dir.to_str().unwrap()))
76                .unwrap() );
77
78            Some(build_temp_dir(&namepath, &base_temp_dir.as_ref().unwrap()) )
79        } else {
80            base_temp_dir = None;
81            None
82        };
83
84        let fixture_dir = if self.using_fixture_dir {
85            Some(build_fixture_dir(&namepath))
86        } else {
87            None
88        };
89
90        let mut group = TestGroup {
91            namepath,
92            use_case: self.use_case,
93            base_temp_dir,
94            temp_dir,
95            fixture_dir,
96        };
97
98        if let Some(setup_func) = self.setup_func {
99            setup_func(&mut group);
100        }
101
102        let teardown = Teardown {
103            base_temp_dir: group.base_temp_dir.clone(),
104            func: self.static_teardown_func.take()
105        };
106
107        teardown_queue_push(teardown);
108
109        group
110    }
111
112    pub fn base_temp_dir<P>(mut self, dir: &P) -> Self
113    where
114        P: ?Sized + AsRef<OsStr>
115    {
116        let dir = PathBuf::from(dir);
117        let dir = dir.canonicalize()
118            .context(format!("Base temporary directory does not exist: {}", &dir.to_str().unwrap()))
119            .unwrap();
120
121        self.base_temp_dir = dir;
122        self
123    }
124
125
126    pub fn using_temp_dir(mut self) -> Self {
127        self.using_temp_dir = true;
128        self
129    }
130
131    pub fn using_fixture_dir(mut self) -> Self {
132        self.using_fixture_dir = true;
133        self
134    }
135
136    pub fn setup(mut self, func: impl FnOnce(&mut TestGroup) + 'func) -> Self {
137        self.setup_func = Some(Box::new(func));
138        self
139    }
140
141    pub fn teardown_static(mut self, func: extern "C" fn()) -> Self {
142        self.static_teardown_func = Some(func);
143        self
144    }
145}
146
147/// Lazy-locked wrapper for [TestGroup]
148///
149/// Typically constructed using the [group!()] macro.
150///
151/// Statically associated with a Rust module.
152pub struct Group(LazyLock<TestGroup>);
153
154impl Deref for Group {
155    type Target = LazyLock<TestGroup>;
156
157    fn deref(&self) -> &Self::Target {
158        &self.0
159    }
160}
161
162impl Group {
163    pub const fn new(func: fn() -> TestGroup) -> Self {
164        Self(LazyLock::new(func))
165    }
166}
167
168/// Constructs a [TestGroup] and wraps it in [Group]
169#[macro_export]
170macro_rules! group {
171    ($n:expr, $u:tt, {$($b:tt)+}) => {
172        $crate::Group::new(|| {
173            $crate::GroupBuilder::new(env!("CARGO_PKG_NAME"), $crate::UseCase::$u, $n)
174            $($b)+
175                .build()
176        })
177    };
178    ($n:expr, $u:tt) => {
179        $crate::Group::new(|| {
180            $crate::GroupBuilder::new(env!("CARGO_PKG_NAME"), $crate::UseCase::$u, $n).build()
181        })
182    };
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    static _GROUP_NEW: Group = Group::new(|| {
190        GroupBuilder::new(env!("CARGO_PKG_NAME"), UseCase::Unit, "group/builder")
191            .setup(|_| {
192                println!("setup called");
193            })
194            .build()
195    });
196
197    static _GROUP_MACRO: Group = group!("group/macro", Unit, {
198        .using_temp_dir()
199        .setup(|_| {})
200    });
201
202    static GROUP_BASIC: Group = group!("group/basic", Unit);
203
204    static GROUP_WITH_DIRS: Group = group!("group/with-dirs", Unit, {
205        .using_fixture_dir()
206        .using_temp_dir()
207    });
208
209    // Group not configured with a temp dir should panic when attempting to access it
210    #[test] #[should_panic]
211    fn test_temp_dir_unconfigured_access() {
212        GROUP_BASIC.temp_dir();  // should panic
213    }
214
215    #[test]
216    fn test_temp_dir_using() {
217        assert!(GROUP_WITH_DIRS.temp_dir().exists(),
218            "Group configured with `using_temp_dir()` should create the directory on construction if it does not exist.");
219    }
220
221    // Group not configured with a fixture dir should panic when attempting to access it
222    #[test] #[should_panic]
223    fn test_fixture_dir_unconfigured_access() {
224        GROUP_BASIC.fixture_dir(); // should panic
225    }
226
227    // Fixture path should exist for Group configured with `using_fixture_dir()`
228     #[test]
229    fn test_fixture_dir_using() {
230        assert!(GROUP_WITH_DIRS.fixture_dir().exists(),
231            "Fixture path should exist for Group configured with `using_fixture_dir()`");
232    }
233
234    // SAFETY: This can only be called once, by `test_setup_function()`. Not thread safe.
235    static mut SETUP_FUNC_CALLED: bool = false;
236    fn setup_func(_group: &mut TestGroup) {
237        unsafe {
238            SETUP_FUNC_CALLED = true;
239        }
240    }
241
242    static GROUP_WITH_SETUP: Group = group!("group/with-setup", Unit, {
243        .setup(setup_func)
244    });
245
246    // Group setup function should be ran on construction.
247    #[test]
248    fn test_setup_function() {
249        let _ = GROUP_WITH_SETUP.use_case(); // lazy initialize
250
251        unsafe {
252            assert!(SETUP_FUNC_CALLED,
253                "Group setup function should be ran on construction.");
254        }
255    }
256
257    // Group setup closure should be ran on construction.
258    #[test]
259    fn test_setup_closure() {
260        let mut setup_closure_called = false;
261        let _group: TestGroup = GroupBuilder::new(env!("CARGO_PKG_NAME"), UseCase::Unit, "group/with-closure")
262            .setup(|_| {
263                setup_closure_called = true;
264            })
265            .build();
266
267        assert!(setup_closure_called,
268            "Group setup closure should be ran on construction.");
269    }
270
271    // only way to test this is using `cargo test -- --show-output`
272    extern "C" fn teardown_func() {
273        println!("STATIC_GROUP: teardown_static() ran");
274    }
275
276    static GROUP_WITH_TEARDOWN: Group = group!("group/with-teardown", Unit, {
277        .teardown_static(teardown_func)
278    });
279
280    // Group teardown function should be ran on destruction.
281    #[test]
282    fn test_teardown_function() {
283        let _ = GROUP_WITH_TEARDOWN.use_case(); // force lazy init
284    }
285}