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}