use std::{borrow::Cow, fmt, io, process::Command};
#[cfg(feature = "svg")]
use std::{env, ffi::OsStr};
use anstream::ColorChoice;
pub use self::{
config_impl::compare_transcripts,
parser::{LocatedParseError, ParseError},
};
#[cfg(feature = "svg")]
use crate::svg::Template;
use crate::{ShellOptions, Transcript, UserInput, traits::SpawnShell};
mod config_impl;
mod parser;
#[cfg(test)]
mod tests;
mod utils;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum TestOutputConfig {
Quiet,
#[default]
Normal,
Verbose,
}
#[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,
}
}
}
pub trait TestCommand {
#[doc(hidden)] type Inputs: fmt::Debug;
#[doc(hidden)] fn extract_inputs(inputs: &Self::Inputs) -> Cow<'_, [UserInput]>;
#[doc(hidden)] fn reproduce(&mut self, inputs: Self::Inputs) -> io::Result<Transcript>;
}
impl<Cmd: SpawnShell> TestCommand for ShellOptions<Cmd> {
type Inputs = Vec<UserInput>;
fn extract_inputs(inputs: &Self::Inputs) -> Cow<'_, [UserInput]> {
Cow::Borrowed(inputs)
}
fn reproduce(&mut self, inputs: Self::Inputs) -> io::Result<Transcript> {
Transcript::from_inputs(self, inputs)
}
}
impl TestCommand for () {
type Inputs = Transcript;
fn extract_inputs(inputs: &Self::Inputs) -> Cow<'_, [UserInput]> {
let inputs = inputs
.interactions()
.iter()
.map(|interaction| interaction.input().clone());
Cow::Owned(inputs.collect())
}
fn reproduce(&mut self, inputs: Self::Inputs) -> io::Result<Transcript> {
Ok(inputs)
}
}
#[derive(Debug)]
pub struct TestConfig<Cmd = ShellOptions<Command>, F = fn(&mut Transcript)> {
command: Cmd,
match_kind: MatchKind,
output: TestOutputConfig,
color_choice: ColorChoice,
#[cfg(feature = "svg")]
update_mode: UpdateMode,
#[cfg(feature = "svg")]
template: Template,
transform: F,
}
impl<Cmd: TestCommand> TestConfig<Cmd> {
pub fn new(command: Cmd) -> Self {
Self {
command,
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 {
command: self.command,
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: TestCommand, 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");
}
}