mithril_common/test_utils/
temp_dir.rs

1use sha2::{Digest, Sha256};
2use std::path::{Path, PathBuf};
3
4/// A builder of temp directory for tests purpose.
5#[derive(Clone)]
6pub struct TempDir {
7    module_name: String,
8    name: String,
9    enable_short_path: bool,
10    short_path_max_len: usize,
11}
12
13const TEMP_DIR_ROOT_NAME: &str = "mithril_test";
14// 90 to have some room for the folder content (in case of restrained length like socket path)
15const DEFAULT_SHORT_PATH_MAX_LEN: usize = 90;
16
17impl TempDir {
18    /// `TempDir` builder factory
19    pub fn new<M: Into<String>, N: Into<String>>(module: M, name: N) -> Self {
20        Self {
21            module_name: module.into(),
22            name: name.into(),
23            enable_short_path: false,
24            short_path_max_len: DEFAULT_SHORT_PATH_MAX_LEN,
25        }
26    }
27
28    /// Change path generation in order to guarantee a path that have at maximum 90 characters.
29    ///
30    /// Typically used for cases when the generated folder will include a socket.
31    pub fn generate_shorter_path(mut self) -> Self {
32        self.enable_short_path = true;
33        self
34    }
35
36    /// Set the max len that a short path can have
37    pub fn set_short_path_max_len(mut self, max_len: usize) -> Self {
38        self.short_path_max_len = max_len;
39        self
40    }
41
42    /// Generate the path of the temp directory (no IO operation will be executed)
43    pub fn build_path(&self) -> PathBuf {
44        const SHA_LENGTH: usize = 10;
45        let base_dir = std::env::temp_dir().join(TEMP_DIR_ROOT_NAME);
46
47        // Short path only:
48        // Combined max len should be lower than `self.short_path_max_len` to have some room for
49        // the folder content.
50        // MacOS temp folders are not in the `/tmp` folder but in a dynamic path adding 45 chars.
51        // ie: /var/folders/_k/7j0m5c_n4g94vgx9gxknp4tm0000gn/T/
52        if self.enable_short_path {
53            // In order to discriminate two tests with the same name but within different modules
54            // we append the short sha of the module+name to the path.
55            let mut hasher = Sha256::new();
56            hasher.update(&self.module_name);
57            hasher.update(&self.name);
58            let sha = hex::encode(hasher.finalize());
59            let short_sha = &sha[0..SHA_LENGTH];
60
61            // `-2` since when joining a path this adds a `/` and we also add a `_` to join the sha
62            let max_path_len =
63                self.short_path_max_len - SHA_LENGTH - 2 - base_dir.to_string_lossy().len();
64
65            let max_len = self.name.len().min(max_path_len);
66
67            base_dir.join([&self.name[0..max_len], "_", short_sha].concat())
68        } else {
69            base_dir.join(&self.module_name).join(&self.name)
70        }
71    }
72
73    /// Create a directory based on the builder configuration in the system temp folder.
74    pub fn build(&self) -> PathBuf {
75        let path = self.build_path();
76        self.create_dir(&path);
77
78        path
79    }
80
81    /// Create on disk a temp directory based on the given module & name.
82    ///
83    /// Equivalent to:
84    /// ```
85    /// # use crate::mithril_common::test_utils::TempDir;
86    /// TempDir::new("module", "name").build();
87    /// ```
88    pub fn create<M: Into<String>, N: Into<String>>(module: M, name: N) -> PathBuf {
89        Self::new(module, name).build()
90    }
91
92    /// Create on disk a temp directory based on the given module & name, the generated path
93    /// is guaranteed to be at most 90 characters long.
94    ///
95    /// Equivalent to:
96    /// ```
97    /// # use crate::mithril_common::test_utils::TempDir;
98    /// TempDir::new("module", "name").generate_shorter_path().build();
99    /// ```
100    pub fn create_with_short_path<M: Into<String>, N: Into<String>>(module: M, name: N) -> PathBuf {
101        Self::new(module, name).generate_shorter_path().build()
102    }
103
104    fn create_dir(&self, path: &Path) {
105        if path.exists() {
106            std::fs::remove_dir_all(path)
107                .unwrap_or_else(|e| panic!("Could not remove dir {path:?}: {e}"));
108        }
109
110        std::fs::create_dir_all(path)
111            .unwrap_or_else(|e| panic!("Could not create dir {path:?}: {e}"));
112    }
113}
114
115/// Return a temporary directory based on the current function name.
116#[macro_export]
117macro_rules! temp_dir {
118    () => {{
119        fn f() {}
120        let current_function_path = $crate::test_utils::format_current_function_path(f);
121        let current_function_path = current_function_path.replace("/tests/", "/");
122
123        $crate::test_utils::TempDir::new(current_function_path, "").build_path()
124    }};
125}
126pub use temp_dir;
127
128/// Create and return a temporary directory based on the current function name.
129#[macro_export]
130macro_rules! temp_dir_create {
131    () => {{
132        fn f() {}
133        let current_function_path = $crate::test_utils::format_current_function_path(f);
134        let current_function_path = current_function_path.replace("/tests/", "/");
135
136        $crate::test_utils::TempDir::new(current_function_path, "").build()
137    }};
138}
139pub use temp_dir_create;
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use std::{fs, io::Write, ops::Not};
145
146    #[test]
147    fn non_short_path_are_in_a_mithril_test_slash_module_folder_structure() {
148        let path = TempDir::new("temp_dir", "basic").build();
149
150        assert_eq!(
151            Some(
152                std::env::temp_dir()
153                    .join(TEMP_DIR_ROOT_NAME)
154                    .join("temp_dir")
155                    .as_path()
156            ),
157            path.parent()
158        );
159        assert!(path.exists());
160    }
161
162    #[test]
163    fn short_path_are_in_a_mithril_test_folder_structure() {
164        let path = TempDir::new("temp_dir", "basic_short_path")
165            .generate_shorter_path()
166            .build();
167
168        assert_eq!(
169            Some(std::env::temp_dir().join(TEMP_DIR_ROOT_NAME).as_path()),
170            path.parent()
171        );
172        assert!(path.exists());
173    }
174
175    #[test]
176    fn shorter_path_have_a_length_lower_than_90_chars_even_when_given_module_longer_than_that() {
177        let path = TempDir::new(
178            "module_longer_than_a_string_of_90_characters_so_this_test_can_fail_if_the_builder_is_a_bad_builder_that_do_nothing",
179            "name",
180        )
181            .generate_shorter_path()
182            .set_short_path_max_len(90)
183            .build_path();
184        let path_len = path.to_string_lossy().len();
185
186        assert!(
187            path_len <= 90,
188            "path with `short` option enabled was longer than 90 characters:\n\
189            path_len: `{path_len}`\n\
190            path: `{}`",
191            path.display()
192        );
193    }
194
195    #[test]
196    fn shorter_path_have_a_length_lower_than_90_chars_even_when_given_name_longer_than_that() {
197        let path = TempDir::new(
198            "mod",
199            "name_longer_than_a_string_of_90_characters_so_this_test_can_fail_if_the_builder_is_a_bad_builder_that_do_nothing",
200        )
201            .generate_shorter_path()
202            .set_short_path_max_len(90)
203            .build_path();
204        let path_len = path.to_string_lossy().len();
205
206        assert!(
207            path_len <= 90,
208            "path with `short` option enabled was longer than 90 characters:\n\
209            path_len: `{path_len}`\n\
210            path: `{}`",
211            path.display()
212        );
213    }
214
215    #[test]
216    fn same_name_but_two_different_module_generate_different_path() {
217        let path1 = TempDir::new("module_a", "test").build_path();
218        let path2 = TempDir::new("module_b", "test").build_path();
219
220        assert_ne!(path1, path2);
221    }
222
223    #[test]
224    fn same_name_but_two_different_module_generate_different_path_even_with_short_path_enabled() {
225        let path1 = TempDir::new("module_a", "test")
226            .generate_shorter_path()
227            .build_path();
228        let path2 = TempDir::new("module_b", "test")
229            .generate_shorter_path()
230            .build_path();
231
232        assert_ne!(path1, path2);
233    }
234
235    #[test]
236    fn creating_temp_dir_remove_existing_content() {
237        let builder = TempDir::new("temp_dir", "creating_temp_dir_remove_existing_content");
238        let (existing_dir, existing_file) = {
239            let path = builder.build_path();
240            (path.join("existing_subdir"), path.join("existing_file.md"))
241        };
242
243        fs::create_dir_all(&existing_dir).unwrap();
244        let mut file = fs::File::create(&existing_file).unwrap();
245        file.write_all(b"file content").unwrap();
246
247        builder.build();
248
249        assert!(
250            existing_file.exists().not(),
251            "should have cleaned up existing files"
252        );
253        assert!(
254            existing_dir.exists().not(),
255            "should have cleaned up existing subdirectory"
256        );
257    }
258
259    #[test]
260    fn creating_temp_dir_base_on_current_function() {
261        assert_eq!(
262            std::env::temp_dir()
263                .join(TEMP_DIR_ROOT_NAME)
264                .join("mithril_common")
265                .join("test_utils")
266                .join("temp_dir")
267                .join("creating_temp_dir_base_on_current_function"),
268            temp_dir!(),
269        );
270    }
271
272    #[tokio::test]
273    async fn creating_temp_dir_base_on_current_async_function() {
274        assert_eq!(
275            std::env::temp_dir()
276                .join(TEMP_DIR_ROOT_NAME)
277                .join("mithril_common")
278                .join("test_utils")
279                .join("temp_dir")
280                .join("creating_temp_dir_base_on_current_async_function"),
281            temp_dir!(),
282        );
283    }
284}