srctrait_common_testing/
module.rs

1use std::{ffi::OsStr, ops::Deref, path::{Path, PathBuf}, sync::LazyLock};
2use crate::*;
3
4/// Represents a Rust module that contains tests.
5///
6/// Provides parent configuration for each [Test]:
7/// - Base [namepaths](Namepath)
8/// - Base fixture and tmp directories
9/// - The intended [UseCase] for each test
10///
11/// Each [Test] has one parent [TestModule]
12///
13/// This is statically associated with a module using a [Module] wrapper.
14#[derive(PartialEq, Eq, Debug)]
15pub struct TestModule {
16    pub(crate) namepath: Namepath,
17    pub(crate) use_case: UseCase,
18    pub(crate) base_temp_dir: Option<PathBuf>,
19    pub(crate) temp_dir: Option<PathBuf>,
20    pub(crate) fixture_dir: Option<PathBuf>,
21}
22
23impl TestModule {
24    pub fn base_temp_dir(&self) -> &Path {
25        &self.base_temp_dir.as_ref().context("Module `base temp dir` is not configured").unwrap()
26    }
27
28    /// Creates a [TestBuilder].
29    pub fn test_builder(&self, name: &'static str) -> TestBuilder {
30        TestBuilder::new(&self, name)
31    }
32}
33
34impl Testing for TestModule {
35    fn use_case(&self) -> UseCase {
36        self.use_case
37    }
38
39    fn namepath(&self) -> &Namepath {
40        &self.namepath
41    }
42
43    fn fixture_dir(&self) -> &Path {
44        self.fixture_dir.as_ref().context("Module `fixture dir` is not configured").unwrap()
45    }
46
47    fn temp_dir(&self) -> &Path {
48        self.temp_dir.as_ref().context("Module `temp dir` is not configured").unwrap()
49    }
50}
51
52/// Builds a new [TestModule]
53///
54/// The helper macro [module!()] is typically preferred over using this
55/// directly.
56pub struct ModuleBuilder<'func> {
57    pub(crate) use_case: UseCase,
58    pub(crate) package_name: &'static str,
59    pub(crate) module_path: &'static str,
60    pub(crate) base_temp_dir: PathBuf,
61    pub(crate) using_temp_dir: bool,
62    pub(crate) skip_temp_dir_teardown: bool,
63    pub(crate) using_fixture_dir: bool,
64    pub(crate) setup_func: Option<Box<dyn FnOnce(&mut TestModule) + 'func>>,
65    pub(crate) static_teardown_func: Option<extern "C" fn()>,
66}
67
68impl<'func> ModuleBuilder<'func> {
69    pub fn new(package_name: &'static str, use_case: UseCase, module_path: &'static str) -> Self {
70        ModuleBuilder {
71            package_name,
72            use_case,
73            module_path,
74            base_temp_dir: std::env::temp_dir(),
75            using_temp_dir: false,
76            skip_temp_dir_teardown: false,
77            using_fixture_dir: false,
78            setup_func: None,
79            static_teardown_func: None,
80        }
81    }
82
83    /// Builds the [TestModule]
84    ///
85    /// Creates the temp directory, if requested.
86    /// Runs the setup function, if provided.
87    /// Registers a shutdown hook to handle internal cleanup (temp directories)
88    /// Register a shutdown hook for the custom teardown function, if provided
89    pub fn build(mut self) -> TestModule {
90        let namepath = Namepath::new_module(self.package_name, self.use_case, self.module_path)
91            .expect("Invalid namepath for testing module");
92
93        let base_temp_dir;
94        let temp_dir = if self.using_temp_dir {
95            let dirname = namepath.full_path_to_squashed_slug();
96            base_temp_dir = Some(create_random_subdir(&self.base_temp_dir, &dirname) // todo: use squashed prefix
97                .context(format!("Unable to create temporary directory in base: {}", &self.base_temp_dir.to_str().unwrap()))
98                .unwrap() );
99
100            let tmpdir = build_temp_dir(&namepath, &base_temp_dir.as_ref().unwrap());
101            Some(tmpdir)
102        } else {
103            base_temp_dir = None;
104            None
105        };
106
107        let fixture_dir = if self.using_fixture_dir {
108            Some(build_fixture_dir(&namepath) )
109        } else {
110            None
111        };
112
113        let mut module = TestModule {
114            namepath,
115            use_case: self.use_case,
116            base_temp_dir,
117            temp_dir,
118            fixture_dir,
119        };
120
121        if let Some(setup_fn) = self.setup_func {
122            setup_fn(&mut module);
123        }
124
125        let teardown_temp_dir = if self.skip_temp_dir_teardown {
126            if let Some(tmpdir) = &module.base_temp_dir {
127                // Log that we're skipping teardown of the temp dir
128                println!("TESTING: {} :: Skipped teardown of temp_dir:\n  {}",
129                    module.namepath,
130                    tmpdir.display());
131            }
132
133            None
134        } else {
135            module.base_temp_dir.clone()
136        };
137
138        let teardown = Teardown {
139            base_temp_dir: teardown_temp_dir,
140            func: self.static_teardown_func.take()
141        };
142
143        teardown_queue_push(teardown);
144
145        module
146    }
147
148    pub fn using_fixture_dir(mut self) -> Self {
149        self.using_fixture_dir = true;
150        self
151    }
152
153    pub fn base_temp_dir<P>(mut self, dir: &P) -> Self
154    where
155        P: ?Sized + AsRef<OsStr>
156    {
157        let dir = PathBuf::from(dir);
158        let dir = dir.canonicalize()
159            .context(format!("Base temporary directory does not exist: {}", &dir.to_str().unwrap()))
160            .unwrap();
161
162        self.base_temp_dir = dir;
163        self
164    }
165
166    pub fn using_temp_dir(mut self) -> Self {
167        self.using_temp_dir = true;
168        self
169    }
170
171    pub fn skip_temp_dir_teardown(mut self, skip: bool) -> Self {
172        self.skip_temp_dir_teardown = skip;
173        self
174    }
175
176    pub fn setup(mut self, func: impl FnOnce(&mut TestModule) + 'func) -> Self {
177        self.setup_func = Some(Box::new(func));
178        self
179    }
180
181    pub fn teardown_static(mut self, func: extern "C" fn()) -> Self {
182        self.static_teardown_func = Some(func);
183        self
184    }
185}
186
187/// Lazy-locked wrapper for [TestModule].
188///
189/// Typically, it's constructed using the [module!()] macro. It can also be
190/// manually created by passing the result of [ModuleBuilder] to it.
191pub struct Module(LazyLock<TestModule>);
192
193impl Deref for Module {
194    type Target = LazyLock<TestModule>;
195
196    fn deref(&self) -> &Self::Target {
197        &self.0
198    }
199}
200
201impl Module {
202    /// Creates a lazy-locked wrapper over [TestModule]
203    pub const fn new(func: fn() -> TestModule) -> Self {
204        Self(LazyLock::new(func))
205    }
206}
207
208/// Constructs a [TestModule] and wraps it inside a lazy-locked [Module]
209///
210/// Simple usage: `module!(Unit)`
211///
212/// Complex usage:
213/// ```rust,ignore
214/// module!(Integration, {
215///     .using_fixture_dir()
216///     .using_tmp_dir()
217///     .setup(|module| {
218///         /* ... */
219///     })
220/// })
221#[macro_export]
222macro_rules! module {
223    ($u:tt, {$($b:tt)+}) => {
224        $crate::Module::new(|| {
225            $crate::ModuleBuilder::new(env!("CARGO_PKG_NAME"), $crate::UseCase::$u, module_path!())
226            $($b)+
227                .build()
228        })
229    };
230    ($u:tt) => {
231        $crate::Module::new(|| {
232            $crate::ModuleBuilder::new(env!("CARGO_PKG_NAME"), $crate::UseCase::$u, module_path!()).build()
233        })
234    };
235}
236
237#[cfg(test)]
238mod tests {
239    use std::path::PathBuf;
240    use std::sync::LazyLock;
241    use crate::*;
242
243    #[test] #[should_panic]
244    // Should panic if attempting to retrieve the temp_dir() without having configured one manually or by calling ensure_temp_dir().
245    fn test_temp_dir_unconfigured() {
246        let module = module!(Unit);
247        module.temp_dir();  // should panic
248    }
249
250    // Should panic if attempting to retrieve the fixture_dir() without having configured one manually or by calling ensure_fixture_dir().
251    #[test] #[should_panic]
252    fn test_fixture_dir_unconfigured() {
253        let module = module!(Unit);
254        module.fixture_dir(); // should panic
255    }
256
257    // Module base temp dir should be inaccessible if not using a temp dir.
258    #[test] #[should_panic]
259    fn test_base_temp_dir_unconfigured_temp_dir() {
260        module!(Unit, {
261            .base_temp_dir(&std::env::temp_dir())
262        }).base_temp_dir();  // should panic
263    }
264
265    // Module base temp dir should accept paths of types `Path` and `String`.
266    #[test]
267    fn test_base_temp_dir() {
268        static EXPECTED_BASE_TEMP_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
269            let base_temp_dir = std::env::temp_dir()
270                .join("srctrait-common-testing-unit-module");
271
272            if !base_temp_dir.exists() {
273                std::fs::create_dir(&base_temp_dir).unwrap(); // needs manual teardown
274            }
275
276            base_temp_dir.canonicalize().unwrap() // for posterity
277        });
278
279        let module = module!(Unit, {
280            .base_temp_dir(EXPECTED_BASE_TEMP_DIR.as_path())
281            .using_temp_dir()
282        });
283
284        assert_eq!(EXPECTED_BASE_TEMP_DIR.as_path(), module.base_temp_dir().parent().unwrap(),
285            "Module base temp dir should accept paths of type `Path`." );
286
287        let module = module!(Unit, {
288            .base_temp_dir(EXPECTED_BASE_TEMP_DIR.to_str().unwrap())
289            .using_temp_dir()
290        });
291
292        assert_eq!(EXPECTED_BASE_TEMP_DIR.as_path(), module.base_temp_dir().parent().unwrap(),
293            "Module base temp dir should accept paths of type `String`." );
294
295
296        std::fs::remove_dir_all(EXPECTED_BASE_TEMP_DIR.as_path()).unwrap(); // testing cleanup
297    }
298
299    // Module should not allow configuration of base temp dir with a relative path.
300    // Only canonical paths are allowed.
301    #[test] #[should_panic]
302    fn test_base_temp_dir_relative() {
303        let module = module!(Unit, {
304            .base_temp_dir("tmp")
305        });
306
307        let _ = module.namepath(); // force lazy init
308    }
309
310    // Module should not allow configuration of a base temp dir with a non-existing path.
311    #[test] #[should_panic]
312    fn test_base_temp_dir_nonexistant() {
313        let module = module!(Unit, {
314            .base_temp_dir(&std::env::temp_dir().join("srctraittestingnoandthen"))
315        });
316
317        let _ = module.namepath(); // force lazy init
318    }
319
320    // Module use-case should match the fascade helper function that was used to create it.
321    #[test]
322    fn test_use_case() {
323        let unit = module!(Unit);
324        let integration = module!(Integration);
325
326        assert_eq!(UseCase::Unit, unit.use_case(),
327            "Module use-case should match the fascade helper function (Unit) that was used to create it.");
328        assert_eq!(UseCase::Integration, integration.use_case(),
329            "Module use-case should match the fascade helper function (Integration) that was used to create it.");
330    }
331
332    // Module configured with `using_temp_dir()` should have a temp path:
333    //     `Module.base_temp_dir() + `Module.namepath().path()`
334    // Module configured with `using_temp_dir()` should create the temp directory on construction.
335    #[test]
336    fn test_temp_dir_using() {
337        const MODULE_PATH: &'static str = "srctrait_common_testing::module::test_temp_dir_using";
338        const EXPECTED_DIRNAME: &'static str = "unit/module/test-temp-dir-using";
339        let unit = ModuleBuilder::new(env!("CARGO_PKG_NAME"), UseCase::Unit, MODULE_PATH)
340            .using_temp_dir().build();
341        let expected_tmp_dir = PathBuf::from(&unit.base_temp_dir()).join(EXPECTED_DIRNAME);
342
343        assert_eq!(expected_tmp_dir, unit.temp_dir(),
344            "Module configured with `using_temp_dir()` should have a temp path: `Module.base_temp_dir() + `Module.namepath().path()`");
345        assert!(unit.temp_dir().exists(),
346            "Module configured with `using_temp_dir()` should create the temp directory on construction.");
347    }
348
349    fn expected_unit_module_fixture_dir() -> PathBuf {
350        PathBuf::from(strings::TESTING).join(strings::FIXTURES)
351            .join(UseCase::Unit.to_string())
352            .join("module")
353            .canonicalize()
354            .unwrap()
355    }
356
357    // Module configured with `using_fixture_dir()` should have a fixture path:
358    //     testing / fixtures / `Module.use_case()` / `Module::namepath().dir()`
359    // Module configured with `using_fixture_dir()` should have a pre-existing fixture dir
360    #[test]
361    fn test_fixture_dir_using() {
362        let unit = module!(Unit, {
363            .using_fixture_dir()
364        });
365
366        assert_eq!(expected_unit_module_fixture_dir(), unit.fixture_dir(),
367            "Module configured with `using_fixture_dir` should have a fixture path: testing / fixtures / `Module.use_case()` / `Module.namepath().dir()`");
368         assert!(unit.fixture_dir().exists(),
369            "Module configured with `using_fixture_dir` should have a pre-existing fixture dir");
370    }
371
372    static mut SETUP_FUNC_CALLED: bool = false;
373    fn setup_func(_module: &mut TestModule) {
374        unsafe {
375            SETUP_FUNC_CALLED = true;
376        }
377    }
378
379    #[test]
380    // Should run a setup function
381    fn test_setup_function() {
382        let module = module!(Unit, {
383            .setup(setup_func)
384        });
385
386        let _ = module.namepath(); // lazy init
387
388        unsafe {
389            assert!(SETUP_FUNC_CALLED);
390        }
391    }
392
393    #[test]
394    // Should run a setup closure
395    fn test_setup_closure() {
396        let mut setup_closure_called = false;
397
398        let _module = ModuleBuilder::new(env!("CARGO_PKG_NAME"), UseCase::Unit, module_path!())
399            .setup(|_| {
400                setup_closure_called = true;
401            })
402            .build();
403
404        assert!(setup_closure_called);
405    }
406
407    extern "C" fn static_teardown_func() {
408        println!("STATIC_MODULE: teardown_static() ran");
409    }
410
411    #[test]
412    // Should set a teardown hook. Not testing the actual atexit call here.
413    fn test_teardown_static() {
414        let _module = module!(Unit, {
415            .teardown_static(static_teardown_func)
416        });
417    }
418}