assert_fs/fixture/
file.rs

1use std::ffi;
2use std::path;
3
4use super::errors::FixtureError;
5use super::errors::FixtureKind;
6use super::errors::ResultChainExt;
7
8/// A potential file in the filesystem that is automatically deleted when
9/// it goes out of scope.
10///
11/// The [`NamedTempFile`] type creates a directory on the file system that
12/// is deleted once it goes out of scope. At construction, the
13/// `NamedTempFile` creates a new directory with a randomly generated name.
14///
15/// The constructor, [`NamedTempFile::new(name)`], creates directories in
16/// the location returned by [`std::env::temp_dir()`].
17///
18/// After creating a `NamedTempFile`, work with the file system by doing
19/// standard [`std::fs`] file system operations on its [`Path`],
20/// which can be retrieved with [`NamedTempFile::path()`]. Once the `NamedTempFile`
21/// value is dropped, the parent directory will be deleted, along with the file. It is your
22/// responsibility to ensure that no further file system operations are attempted inside the
23/// temporary directory once it has been deleted.
24///
25/// # Resource Leaking
26///
27/// Various platform-specific conditions may cause `NamedTempFile` to fail
28/// to delete the underlying directory. It's important to ensure that
29/// handles (like [`File`] and [`ReadDir`]) to the file inside the
30/// directory is dropped before the `NamedTempFile` goes out of scope. The
31/// `NamedTempFile` destructor will silently ignore any errors in deleting
32/// the directory; to instead handle errors call [`NamedTempFile::close()`].
33///
34/// Note that if the program exits before the `NamedTempFile` destructor is
35/// run, such as via [`std::process::exit()`], by segfaulting, or by
36/// receiving a signal like `SIGINT`, then the temporary directory
37/// will not be deleted.
38///
39/// # Examples
40///
41/// Create a temporary file.
42///
43/// ```
44/// use assert_fs::fixture::NamedTempFile;
45///
46/// let tmp_file = NamedTempFile::new("foo.rs").unwrap();
47///
48/// // Ensure deletion happens.
49/// tmp_file.close().unwrap();
50/// ```
51///
52/// [`File`]: std::fs::File
53/// [`Path`]: std::path::Path
54/// [`ReadDir`]: std::fs::ReadDir
55#[derive(Debug)]
56pub struct NamedTempFile {
57    temp: Inner,
58    path: path::PathBuf,
59}
60
61#[derive(Debug)]
62enum Inner {
63    Temp(tempfile::TempDir),
64    Persisted,
65}
66
67impl NamedTempFile {
68    /// Attempts to make a temporary file inside of `env::temp_dir()`.
69    ///
70    /// The file and parent directory will be automatically deleted once the returned
71    /// `NamedTempFile` is destroyed.
72    ///
73    /// # Errors
74    ///
75    /// If the parent directory can not be created, `Err` is returned.
76    ///
77    /// # Examples
78    ///
79    /// ```
80    /// use assert_fs::fixture::NamedTempFile;
81    ///
82    /// let tmp_file = NamedTempFile::new("foo.rs").unwrap();
83    ///
84    /// // Ensure deletion happens.
85    /// tmp_file.close().unwrap();
86    /// ```
87    pub fn new<S>(name: S) -> Result<Self, FixtureError>
88    where
89        S: AsRef<ffi::OsStr>,
90    {
91        let temp = tempfile::TempDir::new().chain(FixtureError::new(FixtureKind::CreateDir))?;
92        let path = temp.path().join(name.as_ref());
93        let temp = Inner::Temp(temp);
94        Ok(Self { temp, path })
95    }
96
97    /// Conditionally persist the temporary file for debug purposes.
98    ///
99    /// Note: this operation is not reversible, i.e. `into_persistent_if(false)` is a no-op.
100    ///
101    /// # Examples
102    ///
103    /// ```no_run
104    /// use assert_fs::fixture::NamedTempFile;
105    ///
106    /// let tmp_file = NamedTempFile::new("foo.rs")
107    ///     .unwrap()
108    ///     .into_persistent_if(std::env::var_os("TEST_PERSIST_FILES").is_some());
109    ///
110    /// // Ensure deletion happens.
111    /// tmp_file.close().unwrap();
112    /// ```
113    pub fn into_persistent_if(self, yes: bool) -> Self {
114        if !yes {
115            return self;
116        }
117
118        self.into_persistent()
119    }
120
121    /// Persist the temporary file for debug purposes.
122    ///
123    /// Note: this operation is not reversible, i.e. `into_persistent_if(false)` is a no-op.
124    ///
125    /// # Examples
126    ///
127    /// ```no_run
128    /// use assert_fs::fixture::NamedTempFile;
129    ///
130    /// let tmp_file = NamedTempFile::new("foo.rs")
131    ///     .unwrap()
132    ///     .into_persistent();
133    ///
134    /// // Ensure deletion happens.
135    /// tmp_file.close().unwrap();
136    /// ```
137    pub fn into_persistent(mut self) -> Self {
138        let mut temp = Inner::Persisted;
139        ::std::mem::swap(&mut self.temp, &mut temp);
140        if let Inner::Temp(temp) = temp {
141            _ = temp.into_path();
142        }
143
144        self
145    }
146
147    /// Accesses the [`Path`] to the temporary file.
148    ///
149    /// [`Path`]: std::path::Path
150    ///
151    /// # Examples
152    ///
153    /// ```
154    /// use assert_fs::fixture::NamedTempFile;
155    ///
156    /// let tmp_file = NamedTempFile::new("foo.rs").unwrap();
157    ///
158    /// println!("{}", tmp_file.path().display());
159    ///
160    /// // Ensure deletion happens.
161    /// tmp_file.close().unwrap();
162    /// ```
163    pub fn path(&self) -> &path::Path {
164        &self.path
165    }
166
167    /// Closes and removes the temporary file and parent directory, returning a `Result`.
168    ///
169    /// Although `NamedTempFile` removes the directory on drop, in the destructor
170    /// any errors are ignored. To detect errors cleaning up the temporary
171    /// directory, call `close` instead.
172    ///
173    /// # Errors
174    ///
175    /// This function may return a variety of [`std::io::Error`]s that result from deleting the
176    /// temporary file and parent directory, These errors may be platform specific.
177    ///
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// use assert_fs::fixture::NamedTempFile;
183    ///
184    /// let tmp_file = NamedTempFile::new("foo.rs").unwrap();
185    ///
186    /// // Ensure deletion happens.
187    /// tmp_file.close().unwrap();
188    /// ```
189    pub fn close(self) -> Result<(), FixtureError> {
190        match self.temp {
191            Inner::Temp(temp) => temp
192                .close()
193                .chain(FixtureError::new(FixtureKind::Cleanup))?,
194            Inner::Persisted => (),
195        }
196        Ok(())
197    }
198}
199
200impl AsRef<path::Path> for NamedTempFile {
201    fn as_ref(&self) -> &path::Path {
202        self.path()
203    }
204}
205
206impl std::ops::Deref for NamedTempFile {
207    type Target = path::Path;
208    #[inline]
209    fn deref(&self) -> &path::Path {
210        self.path()
211    }
212}