use std::process::Command;
#[cfg(feature = "svg")]
use std::{env, ffi::OsStr};
use termcolor::ColorChoice;
mod color_diff;
mod config_impl;
mod parser;
#[cfg(test)]
mod tests;
mod utils;
pub use self::parser::Parsed;
#[cfg(feature = "svg")]
use crate::svg::Template;
use crate::{traits::SpawnShell, ShellOptions, Transcript};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum TestOutputConfig {
Quiet,
Normal,
Verbose,
}
impl Default for TestOutputConfig {
fn default() -> Self {
Self::Normal
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[cfg(feature = "svg")]
#[cfg_attr(docsrs, doc(cfg(feature = "svg")))]
pub enum UpdateMode {
Never,
Always,
}
#[cfg(feature = "svg")]
impl UpdateMode {
pub fn from_env() -> Self {
const ENV_VAR: &str = "TERM_TRANSCRIPT_UPDATE";
match env::var_os(ENV_VAR) {
Some(s) => Self::from_os_str(&s).unwrap_or_else(|| {
panic!(
"Cannot read update mode from env variable {ENV_VAR}: `{}` is not a valid value \
(use one of `never` or `always`)",
s.to_string_lossy()
);
}),
None => {
if env::var_os("CI").is_some() {
Self::Never
} else {
Self::Always
}
}
}
}
fn from_os_str(s: &OsStr) -> Option<Self> {
match s {
s if s == "never" => Some(Self::Never),
s if s == "always" => Some(Self::Always),
_ => None,
}
}
fn should_create_snapshot(self) -> bool {
match self {
Self::Always => true,
Self::Never => false,
}
}
}
#[derive(Debug)]
pub struct TestConfig<Cmd = Command, F = fn(&mut Transcript)> {
shell_options: ShellOptions<Cmd>,
match_kind: MatchKind,
output: TestOutputConfig,
color_choice: ColorChoice,
#[cfg(feature = "svg")]
update_mode: UpdateMode,
#[cfg(feature = "svg")]
template: Template,
transform: F,
}
impl<Cmd: SpawnShell> TestConfig<Cmd> {
pub fn new(shell_options: ShellOptions<Cmd>) -> Self {
Self {
shell_options,
match_kind: MatchKind::TextOnly,
output: TestOutputConfig::Normal,
color_choice: ColorChoice::Auto,
#[cfg(feature = "svg")]
update_mode: UpdateMode::from_env(),
#[cfg(feature = "svg")]
template: Template::default(),
transform: |_| { },
}
}
#[must_use]
pub fn with_transform<F>(self, transform: F) -> TestConfig<Cmd, F>
where
F: FnMut(&mut Transcript),
{
TestConfig {
shell_options: self.shell_options,
match_kind: self.match_kind,
output: self.output,
color_choice: self.color_choice,
#[cfg(feature = "svg")]
update_mode: self.update_mode,
#[cfg(feature = "svg")]
template: self.template,
transform,
}
}
}
impl<Cmd: SpawnShell, F: FnMut(&mut Transcript)> TestConfig<Cmd, F> {
#[must_use]
pub fn with_match_kind(mut self, kind: MatchKind) -> Self {
self.match_kind = kind;
self
}
#[must_use]
pub fn with_color_choice(mut self, color_choice: ColorChoice) -> Self {
self.color_choice = color_choice;
self
}
#[must_use]
pub fn with_output(mut self, output: TestOutputConfig) -> Self {
self.output = output;
self
}
#[cfg(feature = "svg")]
#[cfg_attr(docsrs, doc(cfg(feature = "svg")))]
#[must_use]
pub fn with_template(mut self, template: Template) -> Self {
self.template = template;
self
}
#[cfg(feature = "svg")]
#[cfg_attr(docsrs, doc(cfg(feature = "svg")))]
#[must_use]
pub fn with_update_mode(mut self, update_mode: UpdateMode) -> Self {
self.update_mode = update_mode;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum MatchKind {
TextOnly,
Precise,
}
#[derive(Debug, Clone)]
pub struct TestStats {
matches: Vec<Option<MatchKind>>,
}
impl TestStats {
pub fn passed(&self, match_level: MatchKind) -> usize {
self.matches
.iter()
.filter(|&&kind| kind >= Some(match_level))
.count()
}
pub fn errors(&self, match_level: MatchKind) -> usize {
self.matches.len() - self.passed(match_level)
}
pub fn matches(&self) -> &[Option<MatchKind>] {
&self.matches
}
#[allow(clippy::missing_panics_doc)]
pub fn assert_no_errors(&self, match_level: MatchKind) {
assert_eq!(self.errors(match_level), 0, "There were test errors");
}
}