Skip to main content

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}