use termcolor::{Color, ColorSpec, NoColor, WriteColor};
use std::{
fs::File,
io::{self, BufReader, Write},
path::Path,
str,
};
use super::{
color_diff::{ColorDiff, ColorSpan},
parser::Parsed,
utils::{ColorPrintlnWriter, IndentingWriter},
MatchKind, TestConfig, TestOutputConfig, TestStats,
};
use crate::{traits::SpawnShell, Interaction, TermError, Transcript, UserInput};
impl<Cmd: SpawnShell> TestConfig<Cmd> {
pub fn test<I: Into<UserInput>>(
&mut self,
snapshot_path: impl AsRef<Path>,
inputs: impl IntoIterator<Item = I>,
) {
let inputs = inputs.into_iter().map(Into::into);
let snapshot_path = snapshot_path.as_ref();
if snapshot_path.is_file() {
let snapshot = File::open(snapshot_path).unwrap_or_else(|err| {
panic!("Cannot open `{:?}`: {}", snapshot_path, err);
});
let snapshot = BufReader::new(snapshot);
let transcript = Transcript::from_svg(snapshot).unwrap_or_else(|err| {
panic!("Cannot parse snapshot from `{:?}`: {}", snapshot_path, err);
});
self.compare_and_test_transcript(
snapshot_path,
&transcript,
&inputs.collect::<Vec<_>>(),
);
} else if snapshot_path.exists() {
panic!(
"Snapshot path `{:?}` exists, but is not a file",
snapshot_path
);
} else {
let new_snapshot_message = self.create_and_write_new_snapshot(snapshot_path, inputs);
panic!(
"Snapshot `{:?}` is missing\n{}",
snapshot_path, new_snapshot_message
);
}
}
fn compare_and_test_transcript(
&mut self,
snapshot_path: &Path,
transcript: &Transcript<Parsed>,
expected_inputs: &[UserInput],
) {
let actual_inputs: Vec<_> = transcript
.interactions()
.iter()
.map(Interaction::input)
.collect();
if !actual_inputs.iter().copied().eq(expected_inputs) {
let new_snapshot_message =
self.create_and_write_new_snapshot(snapshot_path, expected_inputs.iter().cloned());
panic!(
"Unexpected user inputs in parsed snapshot: expected {exp:?}, got {act:?}\n{msg}",
exp = expected_inputs,
act = actual_inputs,
msg = new_snapshot_message
);
}
let (stats, reproduced) = self
.test_transcript_for_stats(transcript)
.unwrap_or_else(|err| panic!("{}", err));
if stats.errors(self.match_kind) > 0 {
let new_snapshot_message = self.write_new_snapshot(snapshot_path, &reproduced);
panic!("There were test failures\n{}", new_snapshot_message);
}
}
#[cfg(feature = "svg")]
fn create_and_write_new_snapshot(
&mut self,
path: &Path,
inputs: impl Iterator<Item = UserInput>,
) -> String {
let reproduced =
Transcript::from_inputs(&mut self.shell_options, inputs).unwrap_or_else(|err| {
panic!("Cannot create a snapshot `{:?}`: {}", path, err);
});
self.write_new_snapshot(path, &reproduced)
}
#[cfg(feature = "svg")]
fn write_new_snapshot(&self, path: &Path, transcript: &Transcript) -> String {
if !self.update_mode.should_create_snapshot() {
return format!("Skipped writing new snapshot `{:?}` per test config", path);
}
let mut new_path = path.to_owned();
new_path.set_extension("new.svg");
let new_snapshot = File::create(&new_path).unwrap_or_else(|err| {
panic!(
"Cannot create file for new snapshot `{:?}`: {}",
new_path, err
);
});
self.template
.render(transcript, &mut io::BufWriter::new(new_snapshot))
.unwrap_or_else(|err| {
panic!("Cannot render snapshot `{:?}`: {}", new_path, err);
});
format!("A new snapshot was saved to `{:?}`", new_path)
}
#[cfg(not(feature = "svg"))]
#[allow(clippy::unused_self)] fn write_new_snapshot(&self, _: &Path, _: &Transcript) -> String {
format!(
"Not writing a new snapshot since `{}/svg` feature is not enabled",
env!("CARGO_CRATE_NAME")
)
}
#[cfg(not(feature = "svg"))]
#[allow(clippy::unused_self)] fn create_and_write_new_snapshot(
&mut self,
_: &Path,
_: impl Iterator<Item = UserInput>,
) -> String {
format!(
"Not writing a new snapshot since `{}/svg` feature is not enabled",
env!("CARGO_CRATE_NAME")
)
}
pub fn test_transcript(&mut self, transcript: &Transcript<Parsed>) {
let (stats, _) = self
.test_transcript_for_stats(transcript)
.unwrap_or_else(|err| panic!("{}", err));
stats.assert_no_errors(self.match_kind);
}
pub fn test_transcript_for_stats(
&mut self,
transcript: &Transcript<Parsed>,
) -> io::Result<(TestStats, Transcript)> {
if self.output == TestOutputConfig::Quiet {
let mut out = NoColor::new(io::sink());
self.test_transcript_inner(&mut out, transcript)
} else {
let mut out = ColorPrintlnWriter::new(self.color_choice);
self.test_transcript_inner(&mut out, transcript)
}
}
pub(super) fn test_transcript_inner(
&mut self,
out: &mut impl WriteColor,
transcript: &Transcript<Parsed>,
) -> io::Result<(TestStats, Transcript)> {
let inputs = transcript
.interactions()
.iter()
.map(|interaction| interaction.input().clone());
let reproduced = Transcript::from_inputs(&mut self.shell_options, inputs)?;
let stats = self.compare_transcripts(out, transcript, &reproduced)?;
Ok((stats, reproduced))
}
pub(super) fn compare_transcripts(
&self,
out: &mut impl WriteColor,
parsed: &Transcript<Parsed>,
reproduced: &Transcript,
) -> io::Result<TestStats> {
let it = parsed
.interactions()
.iter()
.zip(reproduced.interactions().iter().map(Interaction::output));
let mut stats = TestStats {
matches: Vec::with_capacity(parsed.interactions().len()),
};
for (original, reproduced) in it {
write!(out, " ")?;
out.set_color(ColorSpec::new().set_intense(true))?;
write!(out, "[")?;
let original_text = original.output().plaintext();
let reproduced_text = reproduced
.to_plaintext()
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
let mut actual_match = if original_text == reproduced_text {
Some(MatchKind::TextOnly)
} else {
None
};
let color_diff = if self.match_kind == MatchKind::Precise && actual_match.is_some() {
let original_spans = &original.output().color_spans;
let reproduced_spans =
ColorSpan::parse(reproduced.as_ref()).map_err(|err| match err {
TermError::Io(err) => err,
other => io::Error::new(io::ErrorKind::InvalidInput, other),
})?;
let diff = ColorDiff::new(original_spans, &reproduced_spans);
if diff.is_empty() {
actual_match = Some(MatchKind::Precise);
None
} else {
Some(diff)
}
} else {
None
};
stats.matches.push(actual_match);
if actual_match >= Some(self.match_kind) {
out.set_color(ColorSpec::new().set_reset(false).set_fg(Some(Color::Green)))?;
write!(out, "+")?;
} else {
out.set_color(ColorSpec::new().set_reset(false).set_fg(Some(Color::Red)))?;
if color_diff.is_some() {
write!(out, "#")?;
} else {
write!(out, "-")?;
}
}
out.set_color(ColorSpec::new().set_intense(true))?;
write!(out, "]")?;
out.reset()?;
writeln!(out, " Input: {}", original.input().as_ref())?;
if let Some(diff) = color_diff {
let original_spans = &original.output().color_spans;
diff.highlight_text(out, original_text, original_spans)?;
diff.write_as_table(out)?;
} else if actual_match.is_none() {
Self::write_diff(out, original_text, &reproduced_text)?;
} else if self.output == TestOutputConfig::Verbose {
out.set_color(ColorSpec::new().set_fg(Some(Color::Ansi256(244))))?;
let mut out_with_indents = IndentingWriter::new(&mut *out, b" ");
writeln!(out_with_indents, "{}", original.output().plaintext())?;
out.reset()?;
}
}
Ok(stats)
}
#[cfg(feature = "pretty_assertions")]
fn write_diff(out: &mut impl Write, original: &str, reproduced: &str) -> io::Result<()> {
use pretty_assertions::Comparison;
use std::fmt;
struct DebugStr<'a>(&'a str);
impl fmt::Debug for DebugStr<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
for line in self.0.lines() {
writeln!(formatter, " {}", line)?;
}
Ok(())
}
}
write!(
out,
" {}",
Comparison::new(&DebugStr(original), &DebugStr(reproduced))
)
}
#[cfg(not(feature = "pretty_assertions"))]
fn write_diff(out: &mut impl Write, original: &str, reproduced: &str) -> io::Result<()> {
writeln!(out, " Original:")?;
for line in original.lines() {
writeln!(out, " {}", line)?;
}
writeln!(out, " Reproduced:")?;
for line in reproduced.lines() {
writeln!(out, " {}", line)?;
}
Ok(())
}
}