test_files/lib.rs
1//! # Test Files
2//!
3//! `test_files` implements some convenient patterns for creating
4//! temporary files with given paths (relative to a temporary root)
5//! and content.
6//!
7//! A temporary directory is created on instantiation, and torn
8//! down when the returned object falls out of scope.
9//!
10//! # Example
11//!
12//! ```
13//! use test_files::TestFiles;
14//!
15//! let temp_dir = TestFiles::new();
16//! temp_dir
17//! .file("a/b/c.txt", "ok")
18//! .file("b/c/d.txt", "fine");
19//!
20//! let file_path = temp_dir.path().join("a").join("b").join("c.txt");
21//! let written_content = std::fs::read_to_string(file_path).unwrap();
22//! assert_eq!(written_content, "ok");
23//!
24//! let file_path = temp_dir.path().join("b").join("c").join("d.txt");
25//! let written_content = std::fs::read_to_string(file_path).unwrap();
26//! assert_eq!(written_content, "fine");
27//! ```
28//!
29//! The pain of creating intermediate directories is abstracted
30//! away, so you can just write relative paths, content, and
31//! use the created files in tests or otherwise. The root of
32//! the temporary directory is exposed by the `.path()` method.
33use std::path::{Path, PathBuf};
34use tempfile::{tempdir, TempDir};
35use thiserror::Error;
36use touch::file;
37
38pub type Result<T, E = TestFilesError> = core::result::Result<T, E>;
39
40#[derive(Error, Debug)]
41pub enum TestFilesError {
42 #[error("Path error `{path:?}`")]
43 PathError { path: String },
44 #[error(transparent)]
45 FileWriteError(#[from] touch::Error),
46 #[error(transparent)]
47 TempDirError(#[from] std::io::Error),
48}
49
50pub struct TestFiles(TempDir);
51
52impl TestFiles {
53 /// Creates a plain file under temporary directory, with specified
54 /// content.
55 ///
56 /// # Examples
57 ///
58 /// ```
59 /// use indoc::indoc;
60 /// use std::fs;
61 ///
62 /// let temp_dir = test_files::TestFiles::new();
63 /// temp_dir.file("a/b/c.txt", "ok")
64 /// .file("b/c/d.txt", "fine");
65 ///
66 /// let file_path = temp_dir.path().join("a").join("b").join("c.txt");
67 /// let written_content = fs::read_to_string(file_path).unwrap();
68 /// assert_eq!(written_content, "ok");
69 ///
70 /// let file_path = temp_dir.path().join("b").join("c").join("d.txt");
71 /// let written_content = fs::read_to_string(file_path).unwrap();
72 /// assert_eq!(written_content, "fine");
73 /// ```
74 pub fn file(&self, path: &str, content: &str) -> &Self {
75 self.try_file(path, content).unwrap()
76 }
77
78 /// Creates a new temporary directory that is
79 /// removed when it goes out of scope.
80 ///
81 /// Panics on failure
82 ///
83 /// # Examples
84 ///
85 /// ```
86 /// let temp_dir = test_files::TestFiles::new();
87 ///
88 /// assert!(temp_dir.path().is_dir());
89 /// ```
90 pub fn new() -> Self {
91 Self::try_new().unwrap()
92 }
93
94 /// Returns the path of the underlying temporary directory.
95 ///
96 /// # Examples
97 ///
98 /// ```
99 /// let temp_dir = test_files::TestFiles::new();
100 ///
101 /// assert!(temp_dir.path().is_dir());
102 /// ```
103 pub fn path(&self) -> &Path {
104 self.0.path()
105 }
106
107 fn slash(&self, relative_path: &str) -> PathBuf {
108 self.path().join(relative_path)
109 }
110
111 /// Tries to create a plain file under temporary directory
112 /// with specified content.
113 ///
114 /// # Examples
115 ///
116 /// ```
117 /// use indoc::indoc;
118 /// use std::fs;
119 ///
120 /// # fn main() -> test_files::Result<()> {
121 /// let temp_dir = test_files::TestFiles::new();
122 /// temp_dir.try_file("a/b/c.txt", "ok")?
123 /// .try_file("b/c/d.txt", "fine")?;
124 ///
125 /// let file_path = temp_dir.path().join("a").join("b").join("c.txt");
126 /// let written_content = fs::read_to_string(file_path).unwrap();
127 /// assert_eq!(written_content, "ok");
128 ///
129 /// let file_path = temp_dir.path().join("b").join("c").join("d.txt");
130 /// let written_content = fs::read_to_string(file_path).unwrap();
131 /// assert_eq!(written_content, "fine");
132 /// # Ok(())
133 /// # }
134 /// ```
135 pub fn try_file(&self, path: &str, content: &str) -> Result<&Self> {
136 file::write(
137 self.slash(path).to_str().ok_or(TestFilesError::PathError {
138 path: path.to_string(),
139 })?,
140 content,
141 true,
142 )?;
143 Ok(self)
144 }
145
146 /// Tries to create a new temporary directory that is
147 /// removed when it goes out of scope.
148 ///
149 /// # Examples
150 ///
151 /// ```
152 /// let temp_dir = test_files::TestFiles::try_new();
153 ///
154 /// assert!(temp_dir.is_ok());
155 /// assert!(temp_dir.unwrap().path().is_dir());
156 /// ```
157 pub fn try_new() -> Result<Self> {
158 Ok(Self(tempdir()?))
159 }
160}
161
162impl Default for TestFiles {
163 fn default() -> Self {
164 Self::new()
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use color_eyre::Result;
172 use indoc::indoc;
173 use std::fs;
174
175 #[test]
176 fn makes_deletes_files() -> Result<()> {
177 let tmp_path: Option<PathBuf>;
178 {
179 let files = TestFiles::new();
180 tmp_path = Some(files.path().to_owned());
181
182 let content = indoc! {"
183 ---
184 version: 3
185 "};
186
187 files.file("a/b/index.yml", content);
188 let file_path = tmp_path
189 .as_ref()
190 .unwrap()
191 .join("a")
192 .join("b")
193 .join("index.yml");
194 let written_content = fs::read_to_string(file_path).unwrap();
195 assert_eq!(written_content, content);
196 }
197 // directory is cleaned up with TestFiles falls out of scope
198 assert!(!tmp_path.unwrap().is_dir());
199 Ok(())
200 }
201}