cli_integration_test 0.2.3

cli integration test
Documentation
use assert_cmd::Command;
use fs_extra::dir::create_all;
use fs_extra::file::read_to_string;
use fs_extra::file::write_all;
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::fs::{set_permissions, OpenOptions, Permissions};
use std::io;
use std::io::Write as IoWrite;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use tempdir::TempDir;
use walkdir::WalkDir;

pub struct IntegrationTestEnvironment {
    tmp_dir: TempDir,
    entries: HashMap<PathBuf, Option<String>>,
    cfg_command_callback: Box<dyn Fn(PathBuf, Command) -> Command>,
}

impl IntegrationTestEnvironment {
    pub fn new<L>(label: L) -> Self
    where
        L: AsRef<str>,
    {
        let label = label.as_ref().to_string();
        let tmp_dir = TempDir::new(&label).expect("fail to create tmp directory");
        Self {
            tmp_dir,
            entries: HashMap::new(),
            cfg_command_callback: Box::new(|_, c| c),
        }
    }

    pub fn set_cfg_command_callback(
        &mut self,
        callback: impl Fn(PathBuf, Command) -> Command + 'static,
    ) {
        self.cfg_command_callback = Box::new(callback);
    }

    pub fn add_file<P, C>(&mut self, path: P, content: C)
    where
        P: AsRef<Path>,
        C: AsRef<str>,
    {
        self.entries.insert(
            path.as_ref().to_path_buf(),
            Some(content.as_ref().to_string()),
        );
    }

    pub fn read_file<P>(&self, path: P) -> String
    where
        P: AsRef<Path>,
    {
        let path = self.tmp_dir.path().join(path.as_ref());
        read_to_string(&path).expect(format!("fail to read file {:?}", path).as_str())
    }

    pub fn file_exists<P: AsRef<Path>>(&self, path: P) -> bool {
        let path = self.tmp_dir.path().join(path.as_ref());
        path.exists()
    }

    pub fn add_dir<P>(&mut self, path: P)
    where
        P: AsRef<Path>,
    {
        self.entries.insert(path.as_ref().to_path_buf(), None);
    }

    pub fn setup(&self) {
        for (path, content) in self.entries.iter() {
            let path = self.tmp_dir.path().join(path);
            if let Some(content) = content {
                if let Some(path) = path.parent() {
                    create_all(path, false)
                        .expect(format!("fail to create directory {:?}", path).as_str())
                }
                write_all(path, content).expect("fail to create file");
            } else {
                create_all(&path, false)
                    .expect(format!("fail to create directory {:?}", path).as_str())
            }
        }
    }

    pub fn set_exec_permission<P: AsRef<Path>>(&self, file: P) -> io::Result<()> {
        let file = self.tmp_dir.path().join(file.as_ref());
        let permissions = Permissions::from_mode(0o755);
        set_permissions(file, permissions)?;
        Ok(())
    }

    pub fn set_update_file_time<P: AsRef<Path>>(&self, file: P) -> io::Result<()> {
        let content = self.read_file(file.as_ref());
        let file = self.tmp_dir.path().join(file.as_ref());
        let mut file = OpenOptions::new().write(true).truncate(true).open(file)?;
        write!(file, "{}", content)?;
        Ok(())
    }

    pub fn tree(&self) -> Vec<PathBuf> {
        let mut tree: Vec<PathBuf> = WalkDir::new(self.tmp_dir.path())
            .into_iter()
            .filter_map(|dir_entry| {
                if let Ok(dir_entry) = dir_entry {
                    if let Ok(dir_entry) = dir_entry.path().strip_prefix(self.tmp_dir.path()) {
                        return Some(dir_entry.to_path_buf());
                    }
                }
                None
            })
            .collect();
        tree.sort();
        tree
    }

    pub fn command<C>(&self, crate_name: C) -> io::Result<Command>
    where
        C: AsRef<str>,
    {
        let mut command = Command::cargo_bin(crate_name).unwrap();
        command.current_dir(&self.tmp_dir.path());
        let command = (self.cfg_command_callback)(self.path()?.clone().to_path_buf(), command);
        Ok(command)
    }

    pub fn path(&self) -> io::Result<PathBuf> {
        self.tmp_dir.path().canonicalize()
    }
}

impl Display for IntegrationTestEnvironment {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        for e in self.tree() {
            writeln!(f, "{}", e.to_string_lossy())?;
        }
        Ok(())
    }
}

#[cfg(test)]
mod test {
    use crate::IntegrationTestEnvironment;
    use predicates::prelude::Predicate;
    use predicates::str::contains;

    #[test]
    fn integration_test_environment() {
        let mut e = IntegrationTestEnvironment::new("test");
        e.add_file("file1", "test 1");
        e.add_file("dir/file2", "test 2");
        e.add_dir("emptry_dir");
        e.setup();
        e.set_exec_permission("dir/file2").unwrap();
        let display = e.to_string();
        assert!(contains("file1").eval(display.as_str()));
        assert!(contains("dir/file2").eval(display.as_str()));
        assert!(contains("emptry_dir").eval(display.as_str()));
        assert!(contains("test 1").eval(e.read_file("file1").as_str()));
    }
}

#[macro_export]
macro_rules! println_output {
    ($v:ident) => {
        println!(
            "{}",
            String::from_utf8($v.stderr.clone()).expect("fail to read stderr")
        );
        println!("---------------------------");
        println!(
            "{}",
            String::from_utf8($v.stdout.clone()).expect("fail to read stdout")
        );
        println!("---------------------------");
        println!("{}", $v.status);
    };
}

#[macro_export]
macro_rules! println_result_output {
    ($v:ident) => {
        match $v {
            Ok(output) => {
                println_output!(output);
            }
            Err(outputError) => {
                println!("output error !!");
                println!("{}", outputError.to_string());
            }
        }
    };
}