asmov_common_testing/
module.rs1use std::{ffi::OsStr, ops::Deref, path::{Path, PathBuf}, sync::LazyLock};
2use crate::*;
3
4#[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 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
52pub 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) using_fixture_dir: bool,
63 pub(crate) setup_func: Option<Box<dyn FnOnce(&mut TestModule) + 'func>>,
64 pub(crate) static_teardown_func: Option<extern "C" fn()>,
65}
66
67impl<'func> ModuleBuilder<'func> {
68 pub fn new(package_name: &'static str, use_case: UseCase, module_path: &'static str) -> Self {
69 ModuleBuilder {
70 package_name,
71 use_case,
72 module_path,
73 base_temp_dir: std::env::temp_dir(),
74 using_temp_dir: false,
75 using_fixture_dir: false,
76 setup_func: None,
77 static_teardown_func: None,
78 }
79 }
80
81 pub fn build(mut self) -> TestModule {
88 let namepath = Namepath::new_module(self.package_name, self.use_case, self.module_path)
89 .expect("Invalid namepath for testing module");
90
91 let base_temp_dir;
92 let temp_dir = if self.using_temp_dir {
93 let dirname = namepath.full_path_to_squashed_slug();
94 base_temp_dir = Some(create_random_subdir(&self.base_temp_dir, &dirname) .context(format!("Unable to create temporary directory in base: {}", &self.base_temp_dir.to_str().unwrap()))
96 .unwrap() );
97
98 Some(build_temp_dir(&namepath, &base_temp_dir.as_ref().unwrap()) )
99 } else {
100 base_temp_dir = None;
101 None
102 };
103
104 let fixture_dir = if self.using_fixture_dir {
105 Some(build_fixture_dir(&namepath) )
106 } else {
107 None
108 };
109
110 let mut module = TestModule {
111 namepath,
112 use_case: self.use_case,
113 base_temp_dir,
114 temp_dir,
115 fixture_dir,
116 };
117
118 if let Some(setup_fn) = self.setup_func {
119 setup_fn(&mut module);
120 }
121
122 let teardown = Teardown {
123 base_temp_dir: module.base_temp_dir.clone(),
124 func: self.static_teardown_func.take()
125 };
126
127 teardown_queue_push(teardown);
128
129 module
130 }
131
132 pub fn using_fixture_dir(mut self) -> Self {
133 self.using_fixture_dir = true;
134 self
135 }
136
137 pub fn base_temp_dir<P>(mut self, dir: &P) -> Self
138 where
139 P: ?Sized + AsRef<OsStr>
140 {
141 let dir = PathBuf::from(dir);
142 let dir = dir.canonicalize()
143 .context(format!("Base temporary directory does not exist: {}", &dir.to_str().unwrap()))
144 .unwrap();
145
146 self.base_temp_dir = dir;
147 self
148 }
149
150 pub fn using_temp_dir(mut self) -> Self {
151 self.using_temp_dir = true;
152 self
153 }
154
155 pub fn setup(mut self, func: impl FnOnce(&mut TestModule) + 'func) -> Self {
156 self.setup_func = Some(Box::new(func));
157 self
158 }
159
160 pub fn teardown_static(mut self, func: extern "C" fn()) -> Self {
161 self.static_teardown_func = Some(func);
162 self
163 }
164}
165
166pub struct Module(LazyLock<TestModule>);
171
172impl Deref for Module {
173 type Target = LazyLock<TestModule>;
174
175 fn deref(&self) -> &Self::Target {
176 &self.0
177 }
178}
179
180impl Module {
181 pub const fn new(func: fn() -> TestModule) -> Self {
183 Self(LazyLock::new(func))
184 }
185}
186
187#[macro_export]
201macro_rules! module {
202 ($u:tt, {$($b:tt)+}) => {
203 $crate::Module::new(|| {
204 $crate::ModuleBuilder::new(env!("CARGO_PKG_NAME"), $crate::UseCase::$u, module_path!())
205 $($b)+
206 .build()
207 })
208 };
209 ($u:tt) => {
210 $crate::Module::new(|| {
211 $crate::ModuleBuilder::new(env!("CARGO_PKG_NAME"), $crate::UseCase::$u, module_path!()).build()
212 })
213 };
214}
215
216#[cfg(test)]
217mod tests {
218 use std::path::PathBuf;
219 use std::sync::LazyLock;
220 use crate::*;
221
222 #[test] #[should_panic]
223 fn test_temp_dir_unconfigured() {
225 let module = module!(Unit);
226 module.temp_dir(); }
228
229 #[test] #[should_panic]
231 fn test_fixture_dir_unconfigured() {
232 let module = module!(Unit);
233 module.fixture_dir(); }
235
236 #[test] #[should_panic]
238 fn test_base_temp_dir_unconfigured_temp_dir() {
239 module!(Unit, {
240 .base_temp_dir(&std::env::temp_dir())
241 }).base_temp_dir(); }
243
244 #[test]
246 fn test_base_temp_dir() {
247 static EXPECTED_BASE_TEMP_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
248 let base_temp_dir = std::env::temp_dir()
249 .join("asmov-common-testing-unit-module");
250
251 if !base_temp_dir.exists() {
252 std::fs::create_dir(&base_temp_dir).unwrap(); }
254
255 base_temp_dir.canonicalize().unwrap() });
257
258 let module = module!(Unit, {
259 .base_temp_dir(EXPECTED_BASE_TEMP_DIR.as_path())
260 .using_temp_dir()
261 });
262
263 assert_eq!(EXPECTED_BASE_TEMP_DIR.as_path(), module.base_temp_dir().parent().unwrap(),
264 "Module base temp dir should accept paths of type `Path`." );
265
266 let module = module!(Unit, {
267 .base_temp_dir(EXPECTED_BASE_TEMP_DIR.to_str().unwrap())
268 .using_temp_dir()
269 });
270
271 assert_eq!(EXPECTED_BASE_TEMP_DIR.as_path(), module.base_temp_dir().parent().unwrap(),
272 "Module base temp dir should accept paths of type `String`." );
273
274
275 std::fs::remove_dir_all(EXPECTED_BASE_TEMP_DIR.as_path()).unwrap(); }
277
278 #[test] #[should_panic]
281 fn test_base_temp_dir_relative() {
282 let module = module!(Unit, {
283 .base_temp_dir("tmp")
284 });
285
286 let _ = module.namepath(); }
288
289 #[test] #[should_panic]
291 fn test_base_temp_dir_nonexistant() {
292 let module = module!(Unit, {
293 .base_temp_dir(&std::env::temp_dir().join("asmovtestingnoandthen"))
294 });
295
296 let _ = module.namepath(); }
298
299 #[test]
301 fn test_use_case() {
302 let unit = module!(Unit);
303 let integration = module!(Integration);
304
305 assert_eq!(UseCase::Unit, unit.use_case(),
306 "Module use-case should match the fascade helper function (Unit) that was used to create it.");
307 assert_eq!(UseCase::Integration, integration.use_case(),
308 "Module use-case should match the fascade helper function (Integration) that was used to create it.");
309 }
310
311 #[test]
315 fn test_temp_dir_using() {
316 const MODULE_PATH: &'static str = "asmov_common_testing::module::test_temp_dir_using";
317 const EXPECTED_DIRNAME: &'static str = "unit/module/test-temp-dir-using";
318 let unit = ModuleBuilder::new(env!("CARGO_PKG_NAME"), UseCase::Unit, MODULE_PATH)
319 .using_temp_dir().build();
320 let expected_tmp_dir = PathBuf::from(&unit.base_temp_dir()).join(EXPECTED_DIRNAME);
321
322 assert_eq!(expected_tmp_dir, unit.temp_dir(),
323 "Module configured with `using_temp_dir()` should have a temp path: `Module.base_temp_dir() + `Module.namepath().path()`");
324 assert!(unit.temp_dir().exists(),
325 "Module configured with `using_temp_dir()` should create the temp directory on construction.");
326 }
327
328 fn expected_unit_module_fixture_dir() -> PathBuf {
329 PathBuf::from(strings::TESTING).join(strings::FIXTURES)
330 .join(UseCase::Unit.to_string())
331 .join("module")
332 .canonicalize()
333 .unwrap()
334 }
335
336 #[test]
340 fn test_fixture_dir_using() {
341 let unit = module!(Unit, {
342 .using_fixture_dir()
343 });
344
345 assert_eq!(expected_unit_module_fixture_dir(), unit.fixture_dir(),
346 "Module configured with `using_fixture_dir` should have a fixture path: testing / fixtures / `Module.use_case()` / `Module.namepath().dir()`");
347 assert!(unit.fixture_dir().exists(),
348 "Module configured with `using_fixture_dir` should have a pre-existing fixture dir");
349 }
350
351 static mut SETUP_FUNC_CALLED: bool = false;
356 fn setup_func(_module: &mut TestModule) {
357 unsafe {
358 SETUP_FUNC_CALLED = true;
359 }
360 }
361
362 #[test]
363 fn test_setup_function() {
365 let module = module!(Unit, {
366 .setup(setup_func)
367 });
368
369 let _ = module.namepath(); unsafe {
372 assert!(SETUP_FUNC_CALLED);
373 }
374 }
375
376 #[test]
377 fn test_setup_closure() {
379 let mut setup_closure_called = false;
380
381 let _module = ModuleBuilder::new(env!("CARGO_PKG_NAME"), UseCase::Unit, module_path!())
382 .setup(|_| {
383 setup_closure_called = true;
384 })
385 .build();
386
387 assert!(setup_closure_called);
388 }
389
390 extern "C" fn static_teardown_func() {
391 println!("STATIC_MODULE: teardown_static() ran");
392 }
393
394 #[test]
395 fn test_teardown_static() {
397 let _module = module!(Unit, {
398 .teardown_static(static_teardown_func)
399 });
400 }
401}