asmov_common_testing/
group.rs1use std::{ffi::OsStr, ops::Deref, path::{Path, PathBuf}, sync::LazyLock};
2use crate::*;
3
4pub 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
41pub 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) .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
147pub 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#[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 #[test] #[should_panic]
211 fn test_temp_dir_unconfigured_access() {
212 GROUP_BASIC.temp_dir(); }
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 #[test] #[should_panic]
223 fn test_fixture_dir_unconfigured_access() {
224 GROUP_BASIC.fixture_dir(); }
226
227 #[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 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 #[test]
248 fn test_setup_function() {
249 let _ = GROUP_WITH_SETUP.use_case(); unsafe {
252 assert!(SETUP_FUNC_CALLED,
253 "Group setup function should be ran on construction.");
254 }
255 }
256
257 #[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 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 #[test]
282 fn test_teardown_function() {
283 let _ = GROUP_WITH_TEARDOWN.use_case(); }
285}