trycmd 0.8.1

Snapshot testing for a herd of CLI tests
Documentation
use std::collections::BTreeMap;

#[derive(Debug)]
pub(crate) struct RunnerSpec {
    cases: Vec<CaseSpec>,
    include: Option<Vec<String>>,
    default_bin: Option<crate::schema::Bin>,
    timeout: Option<std::time::Duration>,
    env: crate::schema::Env,
}

impl RunnerSpec {
    pub(crate) fn new() -> Self {
        Self {
            cases: Default::default(),
            include: None,
            default_bin: None,
            timeout: Default::default(),
            env: Default::default(),
        }
    }

    pub(crate) fn case(
        &mut self,
        glob: &std::path::Path,
        expected: Option<crate::schema::CommandStatus>,
    ) {
        self.cases.push(CaseSpec {
            glob: glob.into(),
            expected,
        });
    }

    pub(crate) fn include(&mut self, include: Option<Vec<String>>) {
        self.include = include;
    }

    pub(crate) fn default_bin(&mut self, bin: Option<crate::schema::Bin>) {
        self.default_bin = bin;
    }

    pub(crate) fn timeout(&mut self, time: Option<std::time::Duration>) {
        self.timeout = time;
    }

    pub(crate) fn env(&mut self, key: impl Into<String>, value: impl Into<String>) {
        self.env.add.insert(key.into(), value.into());
    }

    pub(crate) fn prepare(&mut self) -> crate::Runner {
        let mut runner = crate::Runner::new();

        // Both sort and let the last writer win to allow overriding specific cases within a glob
        let mut cases: BTreeMap<std::path::PathBuf, crate::Case> = BTreeMap::new();

        for spec in &self.cases {
            if let Some(glob) = get_glob(&spec.glob) {
                match ::glob::glob(glob) {
                    Ok(paths) => {
                        for path in paths {
                            match path {
                                Ok(path) => {
                                    cases.insert(
                                        path.clone(),
                                        crate::Case {
                                            path,
                                            expected: spec.expected,
                                            default_bin: self.default_bin.clone(),
                                            timeout: self.timeout,
                                            env: self.env.clone(),
                                            error: None,
                                        },
                                    );
                                }
                                Err(err) => {
                                    let path = err.path().to_owned();
                                    let err = crate::Error::new(err.into_error().to_string());
                                    cases.insert(path.clone(), crate::Case::with_error(path, err));
                                }
                            }
                        }
                    }
                    Err(err) => {
                        let err = crate::Error::new(err.to_string());
                        cases.insert(
                            spec.glob.clone(),
                            crate::Case::with_error(spec.glob.clone(), err),
                        );
                    }
                }
            } else {
                let path = spec.glob.as_path();
                cases.insert(
                    path.into(),
                    crate::Case {
                        path: path.into(),
                        expected: spec.expected,
                        default_bin: self.default_bin.clone(),
                        timeout: self.timeout,
                        env: self.env.clone(),
                        error: None,
                    },
                );
            }
        }

        for case in cases.into_values() {
            if self.is_included(&case) {
                runner.case(case);
            }
        }

        runner
    }

    fn is_included(&self, case: &crate::Case) -> bool {
        if let Some(include) = self.include.as_deref() {
            include
                .iter()
                .any(|i| case.path.to_string_lossy().contains(i))
        } else {
            true
        }
    }
}

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

#[derive(Debug)]
struct CaseSpec {
    glob: std::path::PathBuf,
    expected: Option<crate::schema::CommandStatus>,
}

fn get_glob(path: &std::path::Path) -> Option<&str> {
    if let Some(utf8) = path.to_str() {
        if utf8.contains('*') {
            return Some(utf8);
        }
    }

    None
}