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}