testdir/builder.rs
1//! The [`NumberedDirBuilder`].
2
3use std::ffi::OsString;
4use std::fmt;
5use std::fs;
6use std::num::NonZeroU8;
7use std::path::{Path, PathBuf};
8use std::str::FromStr;
9use std::sync::Arc;
10
11use anyhow::{Context, Error, Result};
12
13use crate::{KEEP_DEFAULT, NumberedDir, ROOT_DEFAULT};
14
15/// Builder to create a [`NumberedDir`].
16///
17/// While you can use [`NumberedDir::create`] directly this provides functionality to
18/// specific ways of constructing and re-using the [`NumberedDir`].
19///
20/// Primarily this builder adds the concept of a **root**, a directory in which to create
21/// the [`NumberedDir`]. The concept of the **base** is the same as for [`NumberedDir`] and
22/// is the prefix of the name of the [`NumberedDir`], thus a prefix of `myprefix` would
23/// create directories numbered `myprefix-0`, `myprefix-1` etc. Likewise the **count** is
24/// also the same concept as for [`NumberedDir`] and specifies the maximum number of
25/// numbered directories, older directories will be cleaned up.
26///
27/// # Configuring the builder
28///
29/// The basic constructor uses a *root* of `testdir-of-$USER` placed in the system's default
30/// temporary director location as per [`std::env::temp_dir`]. To customise the root you
31/// can use [`NumberedDirBuilder::root`] or [`NumberedDirBuilder::user_root]. The temporary
32/// directory provider can also be changed using [`NumberedDirBuilder::tmpdir_provider`].
33///
34/// If you simply want an absolute path as parent directory for the numbered directory use
35/// the [`NumberedDirBuilder::set_parent`] function.
36///
37/// Sometimes you may have some external condition which signals that an existing numbered
38/// directory should be re-used. The [`NumberedDirBuilder::reusefn] can be used for this.
39/// This is useful for example when running tests using `cargo test` and you want to use the
40/// same numbered directory for the unit, integration and doc tests even though they all run
41/// in different processes. The [`testdir`] macro does this by storing the process ID of
42/// the `cargo test` process in the numbered directory and comparing that to the parent
43/// process ID of the current process.
44///
45/// # Creating the [`NumberedDir`]
46///
47/// The [`NumberedDirBuilder::create`] method will create a new [`NumberedDir`].
48#[derive(Clone)]
49pub struct NumberedDirBuilder {
50 /// The current absolute path of the parent directory. The last component is the
51 /// current root. This is the parent directory in which we should create the
52 /// NumberedDir.
53 parent: PathBuf,
54 /// The base of the numbered dir, its name without the number suffix.
55 base: String,
56 /// The number of numbered dirs to keep around **after** the new directory is created.
57 count: NonZeroU8,
58 /// Function to determine whether to re-use a numbered dir.
59 #[allow(clippy::type_complexity)]
60 reuse_fn: Option<Arc<Box<dyn Fn(&Path) -> bool + Send + Sync>>>,
61}
62
63impl fmt::Debug for NumberedDirBuilder {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 f.debug_struct("NumberedDirBuilder")
66 .field("parent", &self.parent)
67 .field("base", &self.base)
68 .field("count", &self.count)
69 .field("reusefn", &"<Fn(&Path) -> bool>")
70 .finish()
71 }
72}
73
74impl NumberedDirBuilder {
75 /// Create a new builder for [`NumberedDir`].
76 ///
77 /// By default the *root* will be set to `testdir-of-$USER`. (using [`ROOT_DEFAULT`])
78 /// and the count will be set to `8` ([`KEEP_DEFAULT`]).
79 pub fn new(base: String) -> Self {
80 if base.contains('/') || base.contains('\\') {
81 panic!("base must not contain path separators");
82 }
83 let root = format!(
84 "{}-of-{}",
85 ROOT_DEFAULT,
86 whoami::username().unwrap_or("unknown".to_string())
87 );
88 Self {
89 parent: std::env::temp_dir().join(root),
90 base,
91 count: KEEP_DEFAULT.unwrap(),
92 reuse_fn: None,
93 }
94 }
95
96 /// Resets the *base*-name of the [`NumberedDir`].
97 pub fn base(&mut self, base: String) -> &mut Self {
98 self.base = base;
99 self
100 }
101
102 /// Sets a *root* in the system's temporary directory location.
103 ///
104 /// The [`NumberedDir`]'s parent will be the `root` subdirectory of the system's
105 /// default temporary directory location.
106 pub fn root(&mut self, root: impl Into<String>) -> &mut Self {
107 self.parent.set_file_name(root.into());
108 self
109 }
110
111 /// Sets a *root* with the username affixed.
112 ///
113 /// Like [`NumberedDirBuilder::root`] this sets a subdirectory of the system's default
114 /// temporary directory location as the parent direcotry for the [`NumberedDir`].
115 /// However it suffixes the username to the given `prefix` to use as *root*.
116 pub fn user_root(&mut self, prefix: &str) -> &mut Self {
117 let root = format!(
118 "{}{}",
119 prefix,
120 whoami::username().unwrap_or("unknown".to_string())
121 );
122 self.parent.set_file_name(root);
123 self
124 }
125
126 /// Uses a different temporary direcotry to place the *root* into.
127 ///
128 /// By default [`std::env::temp_dir`] is used to get the system's temporary directory
129 /// location to place the *root* into. This allows you to provide an alternate function
130 /// which will be called to get the location of the directory where *root* will be
131 /// placed. You provider should probably return an absolute path but this is not
132 /// enforced.
133 pub fn tmpdir_provider(&mut self, provider: impl FnOnce() -> PathBuf) -> &mut Self {
134 let default_root = OsString::from_str(ROOT_DEFAULT).unwrap();
135 let root = self.parent.file_name().unwrap_or(&default_root);
136 self.parent = provider().join(root);
137 self
138 }
139
140 /// Sets the parent directory for the [`NumberedDir`].
141 ///
142 /// This does not follow the *root* concept anymore, instead it directly sets the full
143 /// path for the parent directory in which the [`NumberedDir`] will be created. You
144 /// probably want this to be an absolute path but this is not enforced.
145 ///
146 /// Be aware that it is a requirement that the last component of the parent directory is
147 /// valid UTF-8.
148 pub fn set_parent(&mut self, path: PathBuf) -> &mut Self {
149 if path.file_name().and_then(|name| name.to_str()).is_none() {
150 panic!("Last component of parent is not UTF-8");
151 }
152 self.parent = path;
153 self
154 }
155
156 /// Sets the total number of [`NumberedDir`] directories to keep.
157 ///
158 /// If creating the new [`NumberedDir`] would exceed this number, older directories will
159 /// be removed.
160 pub fn count(&mut self, count: NonZeroU8) -> &mut Self {
161 self.count = count;
162 self
163 }
164
165 /// Enables [`NumberedDir`] re-use if `f` returns `true`.
166 ///
167 /// The provided function will be called with each existing numbered directory and if it
168 /// returns `true` this directory will be re-used instead of a new one being created.
169 pub fn reusefn<F>(&mut self, f: F) -> &mut Self
170 where
171 F: Fn(&Path) -> bool + Send + Sync + 'static,
172 {
173 self.reuse_fn = Some(Arc::new(Box::new(f)));
174 self
175 }
176
177 /// Disables any previous call to [`NumberedDirBuilder::reusefn`].
178 pub fn disable_reuse(&mut self) -> &mut Self {
179 self.reuse_fn = None;
180 self
181 }
182
183 /// Creates a new [`NumberedDir`] as configured.
184 pub fn create(&self) -> Result<NumberedDir> {
185 if !self.parent.exists() {
186 fs::create_dir_all(&self.parent).context("Failed to create root directory")?;
187 }
188 if !self.parent.is_dir() {
189 return Err(Error::msg("Path for root is not a directory"));
190 }
191 if let Some(ref reuse_fn) = self.reuse_fn {
192 for numdir in NumberedDir::iterate(&self.parent, &self.base)? {
193 if reuse_fn(numdir.path()) {
194 return Ok(numdir);
195 }
196 }
197 }
198 NumberedDir::create(&self.parent, &self.base, self.count)
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_builder_create() {
208 let parent = tempfile::tempdir().unwrap();
209 let dir = NumberedDirBuilder::new(String::from("base"))
210 .tmpdir_provider(|| parent.path().to_path_buf())
211 .create()
212 .unwrap();
213 assert!(dir.path().is_dir());
214 let root = dir
215 .path()
216 .parent()
217 .unwrap()
218 .file_name()
219 .unwrap()
220 .to_string_lossy();
221 assert!(root.starts_with("testdir-of-"));
222 }
223
224 #[test]
225 fn test_builder_root() {
226 let parent = tempfile::tempdir().unwrap();
227 let dir = NumberedDirBuilder::new(String::from("base"))
228 .tmpdir_provider(|| parent.path().to_path_buf())
229 .root("myroot")
230 .create()
231 .unwrap();
232 assert!(dir.path().is_dir());
233 let root = parent.path().join("myroot");
234 assert_eq!(dir.path(), root.join("base-0"));
235 }
236
237 #[test]
238 fn test_builder_user_root() {
239 let parent = tempfile::tempdir().unwrap();
240 let dir = NumberedDirBuilder::new(String::from("base"))
241 .tmpdir_provider(|| parent.path().to_path_buf())
242 .root("myroot-")
243 .create()
244 .unwrap();
245 assert!(dir.path().is_dir());
246 let root = dir
247 .path()
248 .parent()
249 .unwrap()
250 .file_name()
251 .unwrap()
252 .to_string_lossy();
253 assert!(root.starts_with("myroot-"));
254 }
255
256 #[test]
257 fn test_builder_set_parent() {
258 let temp = tempfile::tempdir().unwrap();
259 let parent = temp.path().join("myparent");
260 let dir = NumberedDirBuilder::new(String::from("base"))
261 .set_parent(parent.clone())
262 .create()
263 .unwrap();
264 assert!(dir.path().is_dir());
265 assert_eq!(dir.path(), parent.join("base-0"));
266 }
267
268 #[test]
269 fn test_builder_count() {
270 let temp = tempfile::tempdir().unwrap();
271 let parent = temp.path();
272 let mut builder = NumberedDirBuilder::new(String::from("base"));
273 builder.tmpdir_provider(|| parent.to_path_buf());
274 builder.count(NonZeroU8::new(1).unwrap());
275
276 let dir0 = builder.create().unwrap();
277 assert!(dir0.path().is_dir());
278
279 let dir1 = builder.create().unwrap();
280 assert!(!dir0.path().is_dir());
281 assert!(dir1.path().is_dir());
282 }
283}