cli_integration_test/
lib.rs

1use assert_cmd::Command;
2use fs_extra::dir::create_all;
3use fs_extra::file::read_to_string;
4use fs_extra::file::write_all;
5use std::collections::HashMap;
6use std::fmt::{Display, Formatter};
7use std::fs::{set_permissions, OpenOptions, Permissions};
8use std::io;
9use std::io::Write as IoWrite;
10use std::os::unix::fs::PermissionsExt;
11use std::path::{Path, PathBuf};
12use tempdir::TempDir;
13use walkdir::WalkDir;
14
15pub struct IntegrationTestEnvironment {
16    tmp_dir: TempDir,
17    entries: HashMap<PathBuf, Option<String>>,
18    cfg_command_callback: Box<dyn Fn(PathBuf, Command) -> Command>,
19}
20
21impl IntegrationTestEnvironment {
22    pub fn new<L>(label: L) -> Self
23    where
24        L: AsRef<str>,
25    {
26        let label = label.as_ref().to_string();
27        let tmp_dir = TempDir::new(&label).expect("fail to create tmp directory");
28        Self {
29            tmp_dir,
30            entries: HashMap::new(),
31            cfg_command_callback: Box::new(|_, c| c),
32        }
33    }
34
35    pub fn set_cfg_command_callback(
36        &mut self,
37        callback: impl Fn(PathBuf, Command) -> Command + 'static,
38    ) {
39        self.cfg_command_callback = Box::new(callback);
40    }
41
42    pub fn add_file<P, C>(&mut self, path: P, content: C)
43    where
44        P: AsRef<Path>,
45        C: AsRef<str>,
46    {
47        self.entries.insert(
48            path.as_ref().to_path_buf(),
49            Some(content.as_ref().to_string()),
50        );
51    }
52
53    pub fn read_file<P>(&self, path: P) -> String
54    where
55        P: AsRef<Path>,
56    {
57        let path = self.tmp_dir.path().join(path.as_ref());
58        read_to_string(&path).expect(format!("fail to read file {:?}", path).as_str())
59    }
60
61    pub fn file_exists<P: AsRef<Path>>(&self, path: P) -> bool {
62        let path = self.tmp_dir.path().join(path.as_ref());
63        path.exists()
64    }
65
66    pub fn add_dir<P>(&mut self, path: P)
67    where
68        P: AsRef<Path>,
69    {
70        self.entries.insert(path.as_ref().to_path_buf(), None);
71    }
72
73    pub fn setup(&self) {
74        for (path, content) in self.entries.iter() {
75            let path = self.tmp_dir.path().join(path);
76            if let Some(content) = content {
77                if let Some(path) = path.parent() {
78                    create_all(path, false)
79                        .expect(format!("fail to create directory {:?}", path).as_str())
80                }
81                write_all(path, content).expect("fail to create file");
82            } else {
83                create_all(&path, false)
84                    .expect(format!("fail to create directory {:?}", path).as_str())
85            }
86        }
87    }
88
89    pub fn set_exec_permission<P: AsRef<Path>>(&self, file: P) -> io::Result<()> {
90        let file = self.tmp_dir.path().join(file.as_ref());
91        let permissions = Permissions::from_mode(0o755);
92        set_permissions(file, permissions)?;
93        Ok(())
94    }
95
96    pub fn set_update_file_time<P: AsRef<Path>>(&self, file: P) -> io::Result<()> {
97        let content = self.read_file(file.as_ref());
98        let file = self.tmp_dir.path().join(file.as_ref());
99        let mut file = OpenOptions::new().write(true).truncate(true).open(file)?;
100        write!(file, "{}", content)?;
101        Ok(())
102    }
103
104    pub fn tree(&self) -> Vec<PathBuf> {
105        let mut tree: Vec<PathBuf> = WalkDir::new(self.tmp_dir.path())
106            .into_iter()
107            .filter_map(|dir_entry| {
108                if let Ok(dir_entry) = dir_entry {
109                    if let Ok(dir_entry) = dir_entry.path().strip_prefix(self.tmp_dir.path()) {
110                        return Some(dir_entry.to_path_buf());
111                    }
112                }
113                None
114            })
115            .collect();
116        tree.sort();
117        tree
118    }
119
120    pub fn command<C>(&self, crate_name: C) -> io::Result<Command>
121    where
122        C: AsRef<str>,
123    {
124        let mut command = Command::cargo_bin(crate_name).unwrap();
125        command.current_dir(&self.tmp_dir.path());
126        let command = (self.cfg_command_callback)(self.path()?.clone().to_path_buf(), command);
127        Ok(command)
128    }
129
130    pub fn path(&self) -> io::Result<PathBuf> {
131        self.tmp_dir.path().canonicalize()
132    }
133}
134
135impl Display for IntegrationTestEnvironment {
136    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
137        for e in self.tree() {
138            writeln!(f, "{}", e.to_string_lossy())?;
139        }
140        Ok(())
141    }
142}
143
144#[cfg(test)]
145mod test {
146    use crate::IntegrationTestEnvironment;
147    use predicates::prelude::Predicate;
148    use predicates::str::contains;
149
150    #[test]
151    fn integration_test_environment() {
152        let mut e = IntegrationTestEnvironment::new("test");
153        e.add_file("file1", "test 1");
154        e.add_file("dir/file2", "test 2");
155        e.add_dir("emptry_dir");
156        e.setup();
157        e.set_exec_permission("dir/file2").unwrap();
158        let display = e.to_string();
159        assert!(contains("file1").eval(display.as_str()));
160        assert!(contains("dir/file2").eval(display.as_str()));
161        assert!(contains("emptry_dir").eval(display.as_str()));
162        assert!(contains("test 1").eval(e.read_file("file1").as_str()));
163    }
164}
165
166#[macro_export]
167macro_rules! println_output {
168    ($v:ident) => {
169        println!(
170            "{}",
171            String::from_utf8($v.stderr.clone()).expect("fail to read stderr")
172        );
173        println!("---------------------------");
174        println!(
175            "{}",
176            String::from_utf8($v.stdout.clone()).expect("fail to read stdout")
177        );
178        println!("---------------------------");
179        println!("{}", $v.status);
180    };
181}
182
183#[macro_export]
184macro_rules! println_result_output {
185    ($v:ident) => {
186        match $v {
187            Ok(output) => {
188                println_output!(output);
189            }
190            Err(outputError) => {
191                println!("output error !!");
192                println!("{}", outputError.to_string());
193            }
194        }
195    };
196}