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}