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::{NumberedDir, KEEP_DEFAULT, 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!("{}-of-{}", ROOT_DEFAULT, whoami::username());
84 Self {
85 parent: std::env::temp_dir().join(root),
86 base,
87 count: KEEP_DEFAULT.unwrap(),
88 reuse_fn: None,
89 }
90 }
91
92 /// Resets the *base*-name of the [`NumberedDir`].
93 pub fn base(&mut self, base: String) -> &mut Self {
94 self.base = base;
95 self
96 }
97
98 /// Sets a *root* in the system's temporary directory location.
99 ///
100 /// The [`NumberedDir`]'s parent will be the `root` subdirectory of the system's
101 /// default temporary directory location.
102 pub fn root(&mut self, root: impl Into<String>) -> &mut Self {
103 self.parent.set_file_name(root.into());
104 self
105 }
106
107 /// Sets a *root* with the username affixed.
108 ///
109 /// Like [`NumberedDirBuilder::root`] this sets a subdirectory of the system's default
110 /// temporary directory location as the parent direcotry for the [`NumberedDir`].
111 /// However it suffixes the username to the given `prefix` to use as *root*.
112 pub fn user_root(&mut self, prefix: &str) -> &mut Self {
113 let root = format!("{}{}", prefix, whoami::username());
114 self.parent.set_file_name(root);
115 self
116 }
117
118 /// Uses a different temporary direcotry to place the *root* into.
119 ///
120 /// By default [`std::env::temp_dir`] is used to get the system's temporary directory
121 /// location to place the *root* into. This allows you to provide an alternate function
122 /// which will be called to get the location of the directory where *root* will be
123 /// placed. You provider should probably return an absolute path but this is not
124 /// enforced.
125 pub fn tmpdir_provider(&mut self, provider: impl FnOnce() -> PathBuf) -> &mut Self {
126 let default_root = OsString::from_str(ROOT_DEFAULT).unwrap();
127 let root = self.parent.file_name().unwrap_or(&default_root);
128 self.parent = provider().join(root);
129 self
130 }
131
132 /// Sets the parent directory for the [`NumberedDir`].
133 ///
134 /// This does not follow the *root* concept anymore, instead it directly sets the full
135 /// path for the parent directory in which the [`NumberedDir`] will be created. You
136 /// probably want this to be an absolute path but this is not enforced.
137 ///
138 /// Be aware that it is a requirement that the last component of the parent directory is
139 /// valid UTF-8.
140 pub fn set_parent(&mut self, path: PathBuf) -> &mut Self {
141 if path.file_name().and_then(|name| name.to_str()).is_none() {
142 panic!("Last component of parent is not UTF-8");
143 }
144 self.parent = path;
145 self
146 }
147
148 /// Sets the total number of [`NumberedDir`] directories to keep.
149 ///
150 /// If creating the new [`NumberedDir`] would exceed this number, older directories will
151 /// be removed.
152 pub fn count(&mut self, count: NonZeroU8) -> &mut Self {
153 self.count = count;
154 self
155 }
156
157 /// Enables [`NumberedDir`] re-use if `f` returns `true`.
158 ///
159 /// The provided function will be called with each existing numbered directory and if it
160 /// returns `true` this directory will be re-used instead of a new one being created.
161 pub fn reusefn<F>(&mut self, f: F) -> &mut Self
162 where
163 F: Fn(&Path) -> bool + Send + Sync + 'static,
164 {
165 self.reuse_fn = Some(Arc::new(Box::new(f)));
166 self
167 }
168
169 /// Disables any previous call to [`NumberedDirBuilder::reusefn`].
170 pub fn disable_reuse(&mut self) -> &mut Self {
171 self.reuse_fn = None;
172 self
173 }
174
175 /// Creates a new [`NumberedDir`] as configured.
176 pub fn create(&self) -> Result<NumberedDir> {
177 if !self.parent.exists() {
178 fs::create_dir_all(&self.parent).context("Failed to create root directory")?;
179 }
180 if !self.parent.is_dir() {
181 return Err(Error::msg("Path for root is not a directory"));
182 }
183 if let Some(ref reuse_fn) = self.reuse_fn {
184 for numdir in NumberedDir::iterate(&self.parent, &self.base)? {
185 if reuse_fn(numdir.path()) {
186 return Ok(numdir);
187 }
188 }
189 }
190 NumberedDir::create(&self.parent, &self.base, self.count)
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn test_builder_create() {
200 let parent = tempfile::tempdir().unwrap();
201 let dir = NumberedDirBuilder::new(String::from("base"))
202 .tmpdir_provider(|| parent.path().to_path_buf())
203 .create()
204 .unwrap();
205 assert!(dir.path().is_dir());
206 let root = dir
207 .path()
208 .parent()
209 .unwrap()
210 .file_name()
211 .unwrap()
212 .to_string_lossy();
213 assert!(root.starts_with("testdir-of-"));
214 }
215
216 #[test]
217 fn test_builder_root() {
218 let parent = tempfile::tempdir().unwrap();
219 let dir = NumberedDirBuilder::new(String::from("base"))
220 .tmpdir_provider(|| parent.path().to_path_buf())
221 .root("myroot")
222 .create()
223 .unwrap();
224 assert!(dir.path().is_dir());
225 let root = parent.path().join("myroot");
226 assert_eq!(dir.path(), root.join("base-0"));
227 }
228
229 #[test]
230 fn test_builder_user_root() {
231 let parent = tempfile::tempdir().unwrap();
232 let dir = NumberedDirBuilder::new(String::from("base"))
233 .tmpdir_provider(|| parent.path().to_path_buf())
234 .root("myroot-")
235 .create()
236 .unwrap();
237 assert!(dir.path().is_dir());
238 let root = dir
239 .path()
240 .parent()
241 .unwrap()
242 .file_name()
243 .unwrap()
244 .to_string_lossy();
245 assert!(root.starts_with("myroot-"));
246 }
247
248 #[test]
249 fn test_builder_set_parent() {
250 let temp = tempfile::tempdir().unwrap();
251 let parent = temp.path().join("myparent");
252 let dir = NumberedDirBuilder::new(String::from("base"))
253 .set_parent(parent.clone())
254 .create()
255 .unwrap();
256 assert!(dir.path().is_dir());
257 assert_eq!(dir.path(), parent.join("base-0"));
258 }
259
260 #[test]
261 fn test_builder_count() {
262 let temp = tempfile::tempdir().unwrap();
263 let parent = temp.path();
264 let mut builder = NumberedDirBuilder::new(String::from("base"));
265 builder.tmpdir_provider(|| parent.to_path_buf());
266 builder.count(NonZeroU8::new(1).unwrap());
267
268 let dir0 = builder.create().unwrap();
269 assert!(dir0.path().is_dir());
270
271 let dir1 = builder.create().unwrap();
272 assert!(!dir0.path().is_dir());
273 assert!(dir1.path().is_dir());
274 }
275}