runt 0.4.1

A lightweight and parallel snapshot testing framework.
Documentation
//! Structures to the track the results generated by running a test.
use crate::{cli, errors::RuntError, printer};
use std::path::PathBuf;
use tokio::fs;

use super::suite;

/// Track the state of TestResult.
#[derive(Debug, PartialEq)]
pub enum State {
    /// The test timed out.
    Timeout,
    /// The comparison succeeded.
    Correct,
    /// The test was skipped because of a .skip file
    Skip,
    /// The .expect file is missing. Contains the generated expectation string.
    Missing(String),
    /// The comparison failed. Contains the the generated expectation string
    /// and the contents of the expect file.
    Mismatch(
        String, // Generated expect string.
        String, // Contents of the expect file.
    ),
}

/// Store information related to a test.
#[derive(Debug)]
pub struct Test {
    /// Path of the test
    pub path: PathBuf,
    /// Location of the expect string.
    pub expect_path: PathBuf,
    /// Result of comparison
    pub state: State,
    /// The results of this structure were saved.
    pub saved: bool,
    /// Id for the test suite that owns this test.
    pub test_suite: suite::Id,
}

impl Test {
    /// Save the results of the test suite into the expect file.
    pub async fn save_results(&mut self) -> Result<(), RuntError> {
        match &self.state {
            State::Skip | State::Correct | State::Timeout => Ok(()),
            State::Missing(expect) | State::Mismatch(expect, _) => {
                self.saved = true;
                fs::write(&self.expect_path, expect).await.map_err(|err| {
                    RuntError(format!(
                        "{}: {}.",
                        self.expect_path.to_str().unwrap(),
                        err
                    ))
                })
            }
        }
    }

    // Helper method to select if this test should be accepted with
    // opt.
    fn with_only_opt(&self, only: &cli::OnlyOpt) -> bool {
        use cli::OnlyOpt as O;
        match (only, &self.state) {
            (O::Fail, State::Mismatch(..)) => true,
            (O::Fail, State::Timeout) => true,
            (O::Pass, State::Correct) => true,
            (O::Missing, State::Missing(..)) => true,
            (O::Fail, _) | (O::Pass, _) | (O::Missing, _) => false,
        }
    }

    /// Returns true if the current options require this test to be saved.
    pub fn should_save(&self, opts: &cli::Opts) -> bool {
        if !opts.save {
            return false;
        }

        if let Some(only) = &opts.post_filter {
            return self.with_only_opt(only);
        }

        true
    }

    /// Returns true if this test should be printed with the current options.
    pub fn should_print(&self, opts: &cli::Opts) -> bool {
        // Print everything if verbose mode is enabled
        if opts.verbose {
            return true;
        }

        // Selectively print things if post_filter is enabled.
        if let Some(only) = &opts.post_filter {
            return self.with_only_opt(only);
        }
        // Otherwise just print failing and missing tests
        !matches!(self.state, State::Correct)
    }

    /// Generate colorized string to report the results of this test.
    pub fn report_str(
        &self,
        suite: Option<&String>,
        show_diff: bool,
    ) -> String {
        use colored::*;

        let mut buf = String::new();
        let path_str = self.path.to_str().unwrap();
        match &self.state {
            State::Skip => {
                assert!(!self.saved, "Skipped files cannot be saved");
                buf.push_str(&"- ".yellow().dimmed());
                suite.into_iter().for_each(|suite_name| {
                    buf.push_str(&suite_name.bold().yellow().dimmed());
                    buf.push_str(&":".yellow().dimmed())
                });
                buf.push_str(&path_str.yellow().dimmed());
            }
            State::Missing(expect_string) => {
                buf.push_str(&"? ".yellow());
                suite.into_iter().for_each(|suite_name| {
                    buf.push_str(&suite_name.bold().yellow());
                    buf.push_str(&":".yellow())
                });
                buf.push_str(&path_str.yellow());
                if self.saved {
                    buf.push_str(&" (saved)".dimmed());
                }
                if show_diff {
                    let diff = printer::gen_diff("", expect_string);
                    buf.push('\n');
                    buf.push_str(&diff);
                }
            }
            State::Timeout => {
                buf.push_str(&"".red());
                suite.into_iter().for_each(|suite_name| {
                    buf.push_str(&suite_name.bold().red());
                    buf.push_str(&":".red())
                });
                buf.push_str(&path_str.red());
                buf.push_str(&" (timeout)".dimmed());
            }
            State::Correct => {
                buf.push_str(&"".green());
                suite.into_iter().for_each(|suite_name| {
                    buf.push_str(&suite_name.bold().green());
                    buf.push_str(&":".green())
                });
                buf.push_str(&path_str.green());
            }
            State::Mismatch(expect_string, contents) => {
                buf.push_str(&"".red());
                suite.into_iter().for_each(|suite_name| {
                    buf.push_str(&suite_name.bold().red());
                    buf.push_str(&":".red())
                });
                buf.push_str(&path_str.red());
                if self.saved {
                    buf.push_str(&" (saved)".dimmed());
                }
                if show_diff {
                    let diff = printer::gen_diff(contents, expect_string);
                    buf.push('\n');
                    buf.push_str(&diff);
                }
            }
        };
        buf
    }
}