asmov_common_testing/
test.rs

1use std::path::{Path, PathBuf};
2use crate::*;
3
4/// Configuraiton for a single unit or integration test.
5///
6/// It has a parent [TestModule] from which it may inherit configuration.
7pub struct Test<'module,'func> {
8    pub(crate) module: &'module TestModule,
9    pub(crate) namepath: Namepath,
10    pub(crate) temp_dir: Option<PathBuf>,
11    pub(crate) fixture_dir: Option<PathBuf>,
12    pub(crate) teardown_func: Option<Box<dyn FnOnce(&mut Test) + 'func>>,
13}
14
15impl<'module,'func> Test<'module,'func> {
16    /// The parent [TestModule] of this test.
17    pub fn module(&self) -> &'module TestModule {
18        &self.module
19    }
20
21    fn teardown(&mut self) {
22        if let Some(teardown_fn) = self.teardown_func.take() {
23            teardown_fn(self);
24        }
25    }
26}
27
28impl<'module,'func> Testing for Test<'module,'func> {
29    fn namepath(&self) -> &Namepath {
30        &self.namepath
31    }
32
33    fn use_case(&self)-> UseCase {
34        self.module.use_case
35    }
36
37    fn fixture_dir(&self) -> &Path {
38        &self.fixture_dir.as_ref()
39            .context("Test `fixture dir` is not configured").unwrap()
40    }
41
42    fn temp_dir(&self) -> &Path {
43        self.temp_dir.as_ref()
44            .context("Test `temp dir` is not configured").unwrap()
45    }
46}
47
48impl<'module,'func> Drop for Test<'module,'func> {
49    fn drop(&mut self) {
50        self.teardown();
51    }
52}
53
54/// Constructs a [Test]
55pub struct TestBuilder<'module,'func> {
56    pub(crate) name: &'static str,
57    pub(crate) module: &'module TestModule,
58    pub(crate) using_temp_dir: bool,
59    pub(crate) inherit_temp_dir: bool,
60    pub(crate) using_fixture_dir: bool,
61    pub(crate) inherit_fixture_dir: bool,
62    pub(crate) setup_func: Option<Box<dyn FnOnce(&mut Test) + 'func>>,
63    pub(crate) teardown_func: Option<Box<dyn FnOnce(&mut Test) + 'func>>,
64}
65
66impl<'module,'func> TestBuilder<'module,'func> {
67    pub fn new(module: &'module TestModule, name: &'static str) -> Self{
68        debug_assert!(!name.contains("::") && !name.contains('/') && !name.contains('.'),
69            "Test name should be a single non-delimited token.");
70
71        Self {
72            name,
73            module,
74            using_temp_dir: false,
75            inherit_temp_dir: false,
76            using_fixture_dir: false,
77            inherit_fixture_dir: false,
78            setup_func: None,
79            teardown_func: None,
80        }
81    }
82
83    /// Builds the test and initializes it.
84    pub fn build(self) -> Test<'module,'func> {
85        let namepath = Namepath::new_test(
86            self.module.namepath.raw.package_name,
87            self.module.use_case,
88            self.module.namepath.raw().path,
89            self.name)
90            .expect("Invalid namepath for Test");
91
92        let temp_dir = if self.using_temp_dir {
93            Some(build_temp_dir(&namepath, &self.module.base_temp_dir()))
94        } else if self.inherit_temp_dir {
95            Some(self.module.temp_dir().to_owned())
96        } else {
97            None
98        };
99
100        let fixture_dir = if self.using_fixture_dir {
101            Some(build_fixture_dir(&namepath))
102        } else if self.inherit_fixture_dir {
103            Some(self.module.fixture_dir().to_owned())
104        } else {
105            None
106        };
107
108        let mut test = Test {
109            module: self.module,
110            namepath,
111            temp_dir,
112            fixture_dir,
113            teardown_func: self.teardown_func,
114        };
115
116        if let Some(setup_fn) = self.setup_func {
117            setup_fn(&mut test);
118        }
119
120        test
121    }
122
123    /// Configures this test to use an existing fixture directory.
124    /// The base path is defined by the parent Module or Group, with an existing subdirectory expected to be the name of this test.
125    pub fn using_fixture_dir(mut self) -> Self {
126        assert!(!self.inherit_fixture_dir, "Configuring both `inherit` and `using` for `fixture_dir` is ambiguous");
127        self.using_fixture_dir = true;
128        self
129    }
130
131    /// Configures the test to use a temporary directory.
132    /// The base path is defined by the parent Module or Group, with a subdirectory created just for this test (by its name).
133    pub fn using_temp_dir(mut self) -> Self {
134        assert!(!self.inherit_temp_dir);
135        if self.module.temp_dir.is_none() {
136            panic!("Test cannot use a temporary directory unless its parent Module uses one");
137        }
138
139        self.using_temp_dir = true;
140        self
141    }
142
143    /// Configures the test to use the exact same temporary directory as its parent Module or Group.
144    /// A separate subdirectory will not be created for this test.
145    pub fn inherit_temp_dir(mut self) -> Self {
146        assert!(!self.using_temp_dir);
147        if self.module.temp_dir.is_none() {
148            panic!("Test cannot use a temporary directory unless its parent Module uses one");
149        }
150
151        self.inherit_temp_dir = true;
152        self
153    }
154
155    /// Configures the test to use the exact same fixture directory as its parent Module or Group.
156    /// A separate subdirectory for this test is not expected to exist.
157    pub fn inherit_fixture_dir(mut self) -> Self {
158        assert!(!self.using_fixture_dir);
159        self.inherit_fixture_dir = true;
160        self
161    }
162
163    /// Calls the provided function once on construction of the test.
164    pub fn setup(mut self, func: impl FnOnce(&mut Test) + 'func) -> Self {
165        self.setup_func = Some(Box::new(func));
166        self
167    }
168
169    /// Calls the provided function once on destruction of the test.
170    pub fn teardown(mut self, func: impl FnOnce(&mut Test) + 'func) -> Self {
171        self.teardown_func = Some(Box::new(func));
172        self
173    }
174}
175
176/// Constructs a [Test] using with a parent [TestModule] in scope named, "TESTING".
177#[macro_export]
178macro_rules! test {
179    ({$($b:tt)+}) => {
180        $crate::TestBuilder::new(&TESTING, function_name!())
181        $($b)+
182            .build()
183    };
184    () => {
185        $crate::TestBuilder::new(&TESTING, function_name!()).build()
186    };
187}
188
189/// Constructs a [Test] using a custom ident for its parent [TestModule].
190#[macro_export]
191macro_rules! test_with {
192    ($m:ident, {$($b:tt)+}) => {
193        let builder = $crate::TestBuilder::new(&$m, function_name!());
194        builder$($b)+
195            .build()
196    };
197    ($m:ident) => {
198        $crate::TestBuilder::new(&$m, function_name!()).build()
199    };
200}
201
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use crate::prelude::*;
207
208    static MODULE_BASIC: Module = module!(Unit);
209
210    static MODULE_WITH_DIRS: Module = module!(Unit, {
211            .using_fixture_dir()
212            .using_temp_dir()
213    });
214
215    // Test parent Module should be bound.
216    #[test] #[named]
217    fn test_module() {
218        let test = MODULE_BASIC.test_builder(function_name!()).build();
219        assert_eq!(&*MODULE_BASIC.namepath(), test.module().namepath(),
220            "Test parent Module should be bound.");
221    }
222
223    // Test name should be set.
224    #[test] #[named]
225    fn test_name() {
226        let test = MODULE_BASIC.test_builder(function_name!()).build();
227        assert_eq!("test-name", test.namepath().name(),
228            "Test name should be set.");
229    }
230
231    // Test name should not contain namepath separator tokens: "::", '/', '.'
232    #[test] #[should_panic]
233    fn test_name_invalid() {
234        MODULE_BASIC.test_builder("foo.bar").build();  // should panic
235    }
236
237    // Test with only a parent Module should have a namepath of: `Test::module().namepath()` / `Test::name()`
238    // Test with a parent Group should have a namepath of: `Test::group().namepath()` / `Test::name()`
239    #[test] #[named]
240    fn test_namepath() {
241        const EXPECTED_TEST_NAMEPATH: &'static str = "unit/test/test-namepath";
242        let test = MODULE_BASIC.test_builder(function_name!()).build();
243        assert_eq!(EXPECTED_TEST_NAMEPATH, test.namepath().to_string());
244    }
245
246    // Test not configured with a temp dir should panic when attempting to access it
247    #[test] #[should_panic] #[named]
248    fn test_temp_dir_unconfigured_access() {
249        MODULE_BASIC.test_builder(function_name!())
250            .build()
251            .temp_dir();  // should panic
252    }
253
254    // Test should not allow configuration with `using_temp_dir()` if its parent Module is not using a temp dir.
255    #[test] #[should_panic] #[named]
256    fn test_temp_dir_using_unconfigured_module() {
257        MODULE_BASIC.test_builder(function_name!())
258            .using_temp_dir()  // should panic
259            .build();
260    }
261
262    // Test should not allow configuration with `inherit_temp_dir()` if its parent Module is not using a temp dir.
263    #[test] #[should_panic] #[named]
264    fn test_temp_dir_inherited_unconfigured_module() {
265        MODULE_BASIC.test_builder(function_name!())
266            .inherit_temp_dir()  // should panic
267            .build();
268    }
269
270    // Test configured with `using_tmp_dir()` should have a temp path of: `Module.tmp_dir()` + `Test.name()`
271    // Test configured with `using_temp_dir()` should create the directory on construction if it does not exist.
272    #[test] #[named]
273    fn test_temp_dir_using() {
274        let test = MODULE_WITH_DIRS.test_builder(function_name!())
275            .using_temp_dir()
276            .build();
277
278        assert!(test.temp_dir().exists());
279        assert_eq!(MODULE_WITH_DIRS.temp_dir().join("test-temp-dir-using"), test.temp_dir());
280    }
281
282    // Test configured to `inherit_temp_dir()` should have the same temp path as its parent.
283    #[test] #[named]
284    fn test_temp_dir_inherited() {
285        let test = MODULE_WITH_DIRS.test_builder(function_name!())
286            .inherit_temp_dir()
287            .build();
288
289        assert_eq!(MODULE_WITH_DIRS.temp_dir(), test.temp_dir(),
290            "Test configured to `inherit_temp_dir()` should have the same temp path as its parent.");
291    }
292
293    // Test not configured with a fixture dir should panic when attempting to access it
294    #[test] #[should_panic] #[named]
295    fn test_fixture_dir_unconfigured_access() {
296        MODULE_WITH_DIRS.test_builder(function_name!())
297            .build()
298            .fixture_dir(); // should panic
299    }
300
301    // Test should not allow configuration with `using_fixture_dir()` if its parent Module is not using a fixture dir.
302    #[test] #[should_panic] #[named]
303    fn test_fixture_dir_using_unconfigured_module() {
304        MODULE_BASIC.test_builder(function_name!())
305            .using_fixture_dir()  // should panic
306            .build();
307    }
308
309    // Test should not allow configuration with `inherit_fixture_dir()` if its parent Module is not using a fixture dir.
310    #[test] #[should_panic] #[named]
311    fn test_fixture_dir_inherited_unconfigured_module() {
312        MODULE_BASIC.test_builder(function_name!())
313            .inherit_fixture_dir()  // should panic
314            .build();
315    }
316
317    // Test configured with `using_fixture_dir()` should have a path of: `Module::fixture_dir()` + `Test::name()`
318    // Fixture path should exist for Test configured as `using_fixture_dir()` with a parent Module.
319    // Test configured with `using_fixture_dir()` should have a path of: `Group::fixture_dir()` + `Test::name()`
320    // Fixture path should exist for Test configured as `using_fixture_dir()` with a parent Module.
321     #[test] #[named]
322    fn test_fixture_dir_using() {
323        let test = MODULE_WITH_DIRS.test_builder(function_name!())
324            .using_fixture_dir()
325            .build();
326
327        assert_eq!(MODULE_WITH_DIRS.fixture_dir().join("test-fixture-dir-using"), test.fixture_dir(),
328            "Test configured with `using_fixture_dir()` should have a path of: `Module::fixture_dir()` + `Test::name()`");
329        assert!(test.fixture_dir().exists(),
330            "Fixture path should exist for Test configured as `using_fixture_dir()`");
331    }
332
333    // Test configured to `inherit_fixture_dir()` should have a fixture path that is the same as its Module.
334    // Fixture path should exist for Test configured to `inherit_fixture_dir()` from Module
335    // Test configured to `inherit_fixture_dir()` should have a fixture path that is the same as its Group.
336    // Fixture path should exist for Test configured to `inherit_fixture_dir()` from Group
337    #[test] #[named]
338    fn test_fixture_dir_inherited() {
339        let test = MODULE_WITH_DIRS.test_builder(function_name!())
340            .inherit_fixture_dir()
341            .build();
342
343        assert_eq!(MODULE_WITH_DIRS.fixture_dir(), test.fixture_dir(),
344            "Test configured to `inherit_fixture_dir()` should have a fixture path that is the same as its Module.");
345        assert!(test.fixture_dir().exists(),
346            "Fixture path should exist for Test configured to `inherit_fixture_dir()` from Module");
347    }
348
349    // SAFETY: This can only be called once, by `test_setup_function()`. Not thread safe.
350    static mut SETUP_FUNC_CALLED: bool = false;
351    fn setup_func(_test: &mut Test) {
352        unsafe {
353            SETUP_FUNC_CALLED = true;
354        }
355    }
356
357    // Test setup function should be ran on construction.
358    #[test] #[named]
359    fn test_setup_function() {
360        let _testgroup = MODULE_BASIC.test_builder(function_name!())
361            .setup(setup_func)
362            .build();
363
364        unsafe {
365            assert!(SETUP_FUNC_CALLED,
366                "Test setup function should be ran on construction.");
367        }
368    }
369
370    // Test setup closure should be ran on construction.
371    #[test] #[named]
372    fn test_setup_closure() {
373        let mut setup_closure_called = false;
374        MODULE_BASIC.test_builder(function_name!())
375            .setup(|_| {
376                setup_closure_called = true;
377            })
378            .build();
379
380        assert!(setup_closure_called,
381            "Test setup closure should be ran on construction.");
382    }
383
384    // unsafe: This can only be called once, by `test_setup_function()`. Not thread safe.
385    static mut TEARDOWN_FUNC_CALLED: bool = false;
386    fn teardown_func(_group: &mut Test) {
387        unsafe {
388            TEARDOWN_FUNC_CALLED = true;
389        }
390    }
391
392    // Test teardown function should be ran on destruction.
393    #[test] #[named]
394    fn test_teardown_function() {
395        {
396            MODULE_BASIC.test_builder(function_name!())
397            .teardown(teardown_func)
398            .build();
399        }
400
401        unsafe {
402            assert!(TEARDOWN_FUNC_CALLED,
403                "Test teardown function should be ran on destruction.");
404        }
405    }
406
407    // Test teardown closure should be ran on destruction.
408    #[test] #[named]
409    fn test_teardown_closure() {
410        let mut teardown_closure_called = false;
411        {
412            MODULE_BASIC.test_builder(function_name!())
413                .teardown(|_| {
414                    teardown_closure_called = true;
415                })
416                .build();
417        }
418
419        assert!(teardown_closure_called,
420            "Test teardown closure should be ran on destruction.");
421    }
422}