iron_test/
project_builder.rs

1use std::fs;
2use std::io::{self, Write};
3use std::env;
4use std::path::{Path, PathBuf};
5use std::fmt::Debug;
6use uuid::Uuid;
7use std::sync::{Mutex, Arc};
8
9static IRON_INTEGRATION_TEST_DIR : &'static str = "iron-integration-tests";
10
11#[derive(Debug, PartialEq, Clone)]
12struct FileBuilder {
13    path: PathBuf,
14    body: Vec<u8>
15}
16
17impl FileBuilder {
18    /// creates new instance of FileBuilder
19    pub fn new<P: AsRef<Path>, B: Into<Vec<u8>>>(path: P, body: B) -> FileBuilder {
20        FileBuilder { path: path.as_ref().to_path_buf(), body: body.into() }
21    }
22
23    fn mk(&self) -> Result<(), String> {
24        try!(mkdir_recursive(&self.dirname()));
25
26        let mut file = try!(
27            fs::File::create(&self.path)
28                .with_err_msg(format!("Could not create file; path={}",
29                                      self.path.display())));
30
31        file.write_all(&self.body)
32            .with_err_msg(format!("Could not write to file; path={}",
33                                  self.path.display()))
34    }
35
36    // FIXME: Panics if there's no parent
37    fn dirname(&self) -> &Path {
38        &self.path.parent().expect("parent directory")
39    }
40}
41
42
43/// An RAII guard that controls a temporary directory of test files.
44///
45/// It is also a builder and is used to build up the temporary files,
46/// which are then deleted on drop.
47#[derive(Debug, Clone)]
48pub struct ProjectBuilder {
49    name: String,
50    root: PathBuf,
51    files: Vec<FileBuilder>,
52    lock: Arc<Mutex<()>>,
53}
54
55impl ProjectBuilder {
56    /// Create a ProjectBuilder that will manage a new temporary directory
57    /// making use of the current name.
58    pub fn new(name: &str) -> ProjectBuilder {
59        let id = Uuid::new_v4();
60        let path = root(id);
61
62        // Clear out the temp directory.
63        path.rm_rf().unwrap();
64
65        ProjectBuilder {
66            name: name.to_string(),
67            root: path.join(name),
68            files: vec!(),
69            lock: Arc::new(Mutex::new(())),
70        }
71    }
72
73    /// Get the root path of the temporary directory.
74    pub fn root(&self) -> &Path {
75        &self.root
76    }
77
78    /// Add a new file to the temporary directory with the given contents.
79    pub fn file<P, B>(mut self, path: P, body: B) -> ProjectBuilder
80    where P: AsRef<Path>, B: Into<Vec<u8>> {
81        self.files.push(FileBuilder::new(self.root.join(&path), body));
82        self
83    }
84
85    /// Creates the project layout, based on current state of the builder
86    pub fn build(&self) -> &ProjectBuilder {
87        self.build_with_result().map(|_| self).unwrap()
88    }
89
90    /// Creates the project layout, based on current state of the builder
91    pub fn build_with_result(&self) -> Result<(), String> {
92        let _lock = self.lock.lock().unwrap();
93        for file in self.files.iter() {
94            try!(file.mk());
95        }
96
97        Ok(())
98    }
99}
100
101impl PartialEq for ProjectBuilder {
102    fn eq(&self, other: &ProjectBuilder) -> bool {
103        self.name.eq(&other.name) &&
104            self.root.eq(&other.root) &&
105            self.files.eq(&other.files)
106    }
107}
108
109impl Drop for ProjectBuilder {
110    fn drop(&mut self) {
111        let _lock = self.lock.lock().unwrap();
112        match self.root().parent().map(BuilderPathExt::rm_rf) {
113            Some(Ok(_)) => debug!("Successfully cleaned up the test directory; path = {:?}", self.root().parent().unwrap()),
114            Some(Err(e)) => debug!("Failed to cleanup the test directory; path = {:?}; {}", self.root().parent().unwrap(), e),
115            None => debug!("Failed to cleanup the test directory; no parent")
116        }
117    }
118}
119
120// Recursively creates the directory with all subdirectories
121fn mkdir_recursive(path: &Path) -> Result<(), String> {
122    fs::create_dir_all(path)
123        .with_err_msg(format!("could not create directory; path={}",
124                              path.display()))
125}
126
127/// Convenience methods to show errors
128trait ErrMsg<T> {
129    /// Convenience method on Result to to return either value on Ok,
130    /// or value + error on Err
131    fn with_err_msg(self, val: String) -> Result<T, String>;
132}
133
134impl<T, E: Debug> ErrMsg<T> for Result<T, E> {
135    fn with_err_msg(self, val: String) -> Result<T, String> {
136        match self {
137            Ok(val) => Ok(val),
138            Err(err) => Err(format!("{}; original={:?}", val, err))
139        }
140    }
141}
142
143// Current test root path.
144// Will be located in target/iron-integration-tests/test-<uuid>
145fn root(id: Uuid) -> PathBuf {
146    integration_tests_dir().join(&format!("test-{}", id))
147}
148
149fn integration_tests_dir() -> PathBuf {
150    env::current_exe()
151        .map(|mut p| { p.pop(); p.join(IRON_INTEGRATION_TEST_DIR) })
152        .unwrap()
153}
154
155
156/// Convenience methods on Path
157pub trait BuilderPathExt {
158    /// Deletes directory in Path recursively
159    fn rm_rf(&self) -> io::Result<()>;
160}
161
162impl BuilderPathExt for Path {
163    fn rm_rf(&self) -> io::Result<()> {
164        if let Ok(_) = fs::metadata(self) {
165            fs::remove_dir_all(self)
166        } else {
167            Ok(())
168        }
169    }
170}