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(())
}
}