srctrait_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) 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 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) .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 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
187pub 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 pub const fn new(func: fn() -> TestModule) -> Self {
204 Self(LazyLock::new(func))
205 }
206}
207
208#[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 fn test_temp_dir_unconfigured() {
246 let module = module!(Unit);
247 module.temp_dir(); }
249
250 #[test] #[should_panic]
252 fn test_fixture_dir_unconfigured() {
253 let module = module!(Unit);
254 module.fixture_dir(); }
256
257 #[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(); }
264
265 #[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(); }
275
276 base_temp_dir.canonicalize().unwrap() });
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(); }
298
299 #[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(); }
309
310 #[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(); }
319
320 #[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 #[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 #[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 fn test_setup_function() {
382 let module = module!(Unit, {
383 .setup(setup_func)
384 });
385
386 let _ = module.namepath(); unsafe {
389 assert!(SETUP_FUNC_CALLED);
390 }
391 }
392
393 #[test]
394 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 fn test_teardown_static() {
414 let _module = module!(Unit, {
415 .teardown_static(static_teardown_func)
416 });
417 }
418}