1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
//! # Test Files
//!
//! `test_files` implements some convenient patterns for creating
//! temporary files with given paths (relative to a temporary root)
//! and content.
//!
//! A temporary directory is created on instantiation, and torn
//! down when the returned object falls out of scope.
//!
//! # Example
//!
//! ```
//! use test_files::TestFiles;
//!
//! let temp_dir = TestFiles::new();
//! temp_dir
//!     .file("a/b/c.txt", "ok")
//!     .file("b/c/d.txt", "fine");
//!
//! let file_path = temp_dir.path().join("a").join("b").join("c.txt");
//! let written_content = std::fs::read_to_string(file_path).unwrap();
//! assert_eq!(written_content, "ok");
//!
//! let file_path = temp_dir.path().join("b").join("c").join("d.txt");
//! let written_content = std::fs::read_to_string(file_path).unwrap();
//! assert_eq!(written_content, "fine");
//! ```
//!
//! The pain of creating intermediate directories is abstracted
//! away, so you can just write relative paths, content, and
//! use the created files in tests or otherwise.  The root of
//! the temporary directory is exposed by the `.path()` method.
use std::path::{Path, PathBuf};
use tempfile::{tempdir, TempDir};
use thiserror::Error;
use touch::file;

pub type Result<T, E = TestFilesError> = core::result::Result<T, E>;

#[derive(Error, Debug)]
pub enum TestFilesError {
    #[error("Path error `{path:?}`")]
    PathError { path: String },
    #[error(transparent)]
    FileWriteError(#[from] touch::Error),
    #[error(transparent)]
    TempDirError(#[from] std::io::Error),
}

pub struct TestFiles(TempDir);

impl TestFiles {
    /// Creates a plain file under temporary directory, with specified
    /// content.
    ///
    /// # Examples
    ///
    /// ```
    /// use indoc::indoc;
    /// use std::fs;
    ///
    /// let temp_dir = test_files::TestFiles::new();
    /// temp_dir.file("a/b/c.txt", indoc!{"
    ///     ok
    /// "})
    /// .file("b/c/d.txt", indoc!{"
    ///     fine
    /// "});
    ///
    /// let file_path = temp_dir.path().join("a").join("b").join("c.txt");
    /// let written_content = fs::read_to_string(file_path).unwrap();
    /// assert_eq!(written_content, "ok");
    ///
    /// let file_path = temp_dir.path().join("b").join("c").join("d.txt");
    /// let written_content = fs::read_to_string(file_path).unwrap();
    /// assert_eq!(written_content, "fine");
    /// ```
    pub fn file(&self, path: &str, content: &str) -> &Self {
        self.try_file(path, content).unwrap()
    }

    /// Creates a new temporary directory that is
    /// removed when it goes out of scope.
    ///
    /// Panics on failure
    ///
    /// # Examples
    ///
    /// ```
    /// let temp_dir = test_files::TestFiles::new();
    ///
    /// assert!(temp_dir.path().is_dir());
    /// ```
    pub fn new() -> Self {
        Self::try_new().unwrap()
    }

    /// Returns the path of the underlying temporary directory.
    ///
    /// # Examples
    ///
    /// ```
    /// let temp_dir = test_files::TestFiles::new();
    ///
    /// assert!(temp_dir.path().is_dir());
    /// ```
    pub fn path(&self) -> &Path {
        self.0.path()
    }

    fn slash(&self, relative_path: &str) -> PathBuf {
        self.path().join(relative_path)
    }

    /// Tries to create a plain file under temporary directory
    /// with specified content.
    ///
    /// # Examples
    ///
    /// ```
    /// use indoc::indoc;
    /// use std::fs;
    ///
    /// # fn main() -> test_files::Result<()> {
    /// let temp_dir = test_files::TestFiles::new();
    /// temp_dir.try_file("a/b/c.txt", indoc!{"
    ///     ok
    /// "})?
    /// .try_file("b/c/d.txt", indoc!{"
    ///     fine
    /// "})?;
    ///
    /// let file_path = temp_dir.path().join("a").join("b").join("c.txt");
    /// let written_content = fs::read_to_string(file_path).unwrap();
    /// assert_eq!(written_content, "ok");
    ///
    /// let file_path = temp_dir.path().join("b").join("c").join("d.txt");
    /// let written_content = fs::read_to_string(file_path).unwrap();
    /// assert_eq!(written_content, "fine");
    /// #   Ok(())
    /// # }
    /// ```
    pub fn try_file(&self, path: &str, content: &str) -> Result<&Self> {
        file::write(
            self.slash(path).to_str().ok_or(TestFilesError::PathError {
                path: path.to_string(),
            })?,
            content,
            true,
        )?;
        Ok(self)
    }

    /// Tries to create a new temporary directory that is
    /// removed when it goes out of scope.
    ///
    /// # Examples
    ///
    /// ```
    /// let temp_dir = test_files::TestFiles::try_new();
    ///
    /// assert!(temp_dir.is_ok());
    /// assert!(temp_dir.unwrap().path().is_dir());
    /// ```
    pub fn try_new() -> Result<Self> {
        Ok(Self(tempdir()?))
    }
}

impl Default for TestFiles {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use color_eyre::Result;
    use indoc::indoc;
    use std::fs;

    #[test]
    fn makes_deletes_files() -> Result<()> {
        let tmp_path: Option<PathBuf>;
        {
            let files = TestFiles::new();
            tmp_path = Some(files.path().to_owned());

            let content = indoc! {"
                ---
                version: 3
            "};

            files.file("a/b/index.yml", content);
            let file_path = tmp_path
                .as_ref()
                .unwrap()
                .join("a")
                .join("b")
                .join("index.yml");
            let written_content = fs::read_to_string(file_path).unwrap();
            assert_eq!(written_content, content);
        }
        // directory is cleaned up with TestFiles falls out of scope
        assert!(!tmp_path.unwrap().is_dir());
        Ok(())
    }
}