blob: 13eec8a73c962791d59e94159d9ca3c75a4111d4 [file] [log] [blame]
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::env;
use std::error::Error;
use std::fmt;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::str;
use std::sync::Mutex;
use std::thread;
use console::style;
use difference::{Changeset, Difference};
use lazy_static::lazy_static;
use serde::Deserialize;
use crate::settings::Settings;
use crate::snapshot::{MetaData, PendingInlineSnapshot, Snapshot, SnapshotContents};
use crate::utils::is_ci;
lazy_static! {
static ref WORKSPACES: Mutex<BTreeMap<String, &'static Path>> = Mutex::new(BTreeMap::new());
static ref TEST_NAME_COUNTERS: Mutex<BTreeMap<String, usize>> = Mutex::new(BTreeMap::new());
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum UpdateBehavior {
InPlace,
NewFile,
NoUpdate,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum OutputBehavior {
Diff,
Summary,
Minimal,
Nothing,
}
#[cfg(windows)]
fn path_to_storage<P: AsRef<Path>>(path: P) -> String {
path.as_ref().to_str().unwrap().replace('\\', "/")
}
#[cfg(not(windows))]
fn path_to_storage<P: AsRef<Path>>(path: P) -> String {
path.as_ref().to_string_lossy().into()
}
fn format_rust_expression(value: &str) -> Cow<'_, str> {
const PREFIX: &str = "const x:() = ";
const SUFFIX: &str = ";\n";
if let Ok(mut proc) = Command::new("rustfmt")
.arg("--emit=stdout")
.arg("--edition=2018")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
{
{
let stdin = proc.stdin.as_mut().unwrap();
stdin.write_all(PREFIX.as_bytes()).unwrap();
stdin.write_all(value.as_bytes()).unwrap();
stdin.write_all(SUFFIX.as_bytes()).unwrap();
}
if let Ok(output) = proc.wait_with_output() {
if output.status.success() {
// slice between after the prefix and before the suffix
// (currently 14 from the start and 2 before the end, respectively)
let start = PREFIX.len() + 1;
let end = output.stdout.len() - SUFFIX.len();
return str::from_utf8(&output.stdout[start..end])
.unwrap()
.to_owned()
.into();
}
}
}
Cow::Borrowed(value)
}
#[test]
fn test_format_rust_expression() {
use crate::assert_snapshot;
assert_snapshot!(format_rust_expression("vec![1,2,3]"), @"vec![1, 2, 3]");
assert_snapshot!(format_rust_expression("vec![1,2,3].iter()"), @"vec![1, 2, 3].iter()");
assert_snapshot!(format_rust_expression(r#" "aoeu""#), @r###""aoeu""###);
assert_snapshot!(format_rust_expression(r#" "aoe😄""#), @r###""aoe😄""###);
assert_snapshot!(format_rust_expression("😄😄😄😄😄"), @"😄😄😄😄😄")
}
fn update_snapshot_behavior(unseen: bool) -> UpdateBehavior {
match env::var("INSTA_UPDATE").ok().as_deref() {
None | Some("") | Some("auto") => {
if is_ci() {
UpdateBehavior::NoUpdate
} else {
UpdateBehavior::NewFile
}
}
Some("always") | Some("1") => UpdateBehavior::InPlace,
Some("new") => UpdateBehavior::NewFile,
Some("unseen") => {
if unseen {
UpdateBehavior::NewFile
} else {
UpdateBehavior::InPlace
}
}
Some("no") => UpdateBehavior::NoUpdate,
_ => panic!("invalid value for INSTA_UPDATE"),
}
}
fn output_snapshot_behavior() -> OutputBehavior {
match env::var("INSTA_OUTPUT").ok().as_deref() {
None | Some("") | Some("diff") => OutputBehavior::Diff,
Some("summary") => OutputBehavior::Summary,
Some("minimal") => OutputBehavior::Minimal,
Some("none") => OutputBehavior::Nothing,
_ => panic!("invalid value for INSTA_OUTPUT"),
}
}
fn force_update_snapshots() -> bool {
match env::var("INSTA_FORCE_UPDATE_SNAPSHOTS").ok().as_deref() {
None | Some("") | Some("0") => false,
Some("1") => true,
_ => panic!("invalid value for INSTA_FORCE_UPDATE_SNAPSHOTS"),
}
}
fn should_fail_in_tests() -> bool {
match env::var("INSTA_FORCE_PASS").ok().as_deref() {
None | Some("") | Some("0") => true,
Some("1") => false,
_ => panic!("invalid value for INSTA_FORCE_PASS"),
}
}
fn get_cargo() -> String {
env::var("CARGO")
.ok()
.unwrap_or_else(|| "cargo".to_string())
}
pub fn get_cargo_workspace(manifest_dir: &str) -> &Path {
// we really do not care about poisoning here.
let mut workspaces = WORKSPACES.lock().unwrap_or_else(|x| x.into_inner());
if let Some(rv) = workspaces.get(manifest_dir) {
rv
} else {
#[derive(Deserialize)]
struct Manifest {
workspace_root: String,
}
let output = std::process::Command::new(get_cargo())
.arg("metadata")
.arg("--format-version=1")
.arg("--no-deps")
.current_dir(manifest_dir)
.output()
.unwrap();
let manifest: Manifest = serde_json::from_slice(&output.stdout).unwrap();
let path = Box::leak(Box::new(PathBuf::from(manifest.workspace_root)));
workspaces.insert(manifest_dir.to_string(), path.as_path());
workspaces.get(manifest_dir).unwrap()
}
}
fn print_changeset(changeset: &Changeset, expr: Option<&str>) {
let Changeset { ref diffs, .. } = *changeset;
#[derive(PartialEq, Debug)]
enum Mode {
Same,
Add,
Rem,
}
#[derive(PartialEq, Debug)]
enum Lineno {
NotPresent,
Present(usize),
}
impl fmt::Display for Lineno {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Lineno::NotPresent => f.pad(""),
Lineno::Present(lineno) => fmt::Display::fmt(&lineno, f),
}
}
}
let mut lines = vec![];
let mut lineno_a = 1;
let mut lineno_b = 1;
for diff in diffs.iter() {
match *diff {
Difference::Same(ref x) => {
for line in x.split('\n') {
lines.push((
Mode::Same,
Lineno::Present(lineno_a),
Lineno::Present(lineno_b),
line.trim_end(),
));
lineno_a += 1;
lineno_b += 1;
}
}
Difference::Add(ref x) => {
for line in x.split('\n') {
lines.push((
Mode::Add,
Lineno::NotPresent,
Lineno::Present(lineno_b),
line.trim_end(),
));
lineno_b += 1;
}
}
Difference::Rem(ref x) => {
for line in x.split('\n') {
lines.push((
Mode::Rem,
Lineno::Present(lineno_a),
Lineno::NotPresent,
line.trim_end(),
));
lineno_a += 1;
}
}
}
}
let width = console::Term::stdout().size().1 as usize;
if let Some(expr) = expr {
println!("{:─^1$}", "", width,);
println!("{}", style(format_rust_expression(expr)));
}
println!("────────────┬{:─^1$}", "", width.saturating_sub(13),);
let mut has_changes = false;
for (i, (mode, lineno_a, lineno_b, line)) in lines.iter().enumerate() {
match mode {
Mode::Add => {
has_changes = true;
println!(
"{:>5} {:>5} │{}{}",
style(lineno_a).dim(),
style(lineno_b).dim().bold(),
style("+").green(),
style(line).green()
);
}
Mode::Rem => {
has_changes = true;
println!(
"{:>5} {:>5} │{}{}",
style(lineno_a).dim(),
style(lineno_b).dim().bold(),
style("-").red(),
style(line).red()
);
}
Mode::Same => {
if lines[i.saturating_sub(5)..(i + 5).min(lines.len())]
.iter()
.any(|x| x.0 != Mode::Same)
{
println!(
"{:>5} {:>5} │ {}",
style(lineno_a).dim(),
style(lineno_b).dim().bold(),
style(line).dim()
);
}
}
}
}
if !has_changes {
println!(
"{:>5} {:>5} │{}",
"",
style("-").dim(),
style(" snapshots are matching").cyan(),
);
}
println!("────────────┴{:─^1$}", "", width.saturating_sub(13),);
}
pub fn get_snapshot_filename(
module_path: &str,
snapshot_name: &str,
cargo_workspace: &Path,
base: &str,
) -> PathBuf {
let root = Path::new(cargo_workspace);
let base = Path::new(base);
Settings::with(|settings| {
root.join(base.parent().unwrap())
.join(settings.snapshot_path())
.join(format!(
"{}__{}.snap",
module_path.replace("::", "__"),
snapshot_name.replace("/", "__").replace("\\", "__")
))
})
}
/// Prints the summary of a snapshot
pub fn print_snapshot_summary(
workspace_root: &Path,
snapshot: &Snapshot,
snapshot_file: Option<&Path>,
line: Option<u32>,
) {
if let Some(snapshot_file) = snapshot_file {
let snapshot_file = workspace_root
.join(snapshot_file)
.strip_prefix(workspace_root)
.ok()
.map(|x| x.to_path_buf())
.unwrap_or_else(|| snapshot_file.to_path_buf());
println!(
"Snapshot file: {}",
style(snapshot_file.display()).cyan().underlined()
);
}
if let Some(name) = snapshot.snapshot_name() {
println!("Snapshot: {}", style(name).yellow());
} else {
println!("Snapshot: {}", style("<inline>").dim());
}
if let Some(ref value) = snapshot.metadata().get_relative_source(workspace_root) {
println!(
"Source: {}{}",
style(value.display()).cyan(),
if let Some(line) = line {
format!(":{}", style(line).bold())
} else {
"".to_string()
}
);
}
if let Some(ref value) = snapshot.metadata().input_file() {
println!("Input file: {}", style(value).cyan());
}
}
/// Prints a diff against an old snapshot.
pub fn print_snapshot_diff(
workspace_root: &Path,
new: &Snapshot,
old_snapshot: Option<&Snapshot>,
snapshot_file: Option<&Path>,
line: Option<u32>,
) {
print_snapshot_summary(workspace_root, new, snapshot_file, line);
let changeset = Changeset::new(
old_snapshot.as_ref().map_or("", |x| x.contents_str()),
&new.contents_str(),
"\n",
);
if old_snapshot.is_some() {
println!("{}", style("-old snapshot").red());
println!("{}", style("+new results").green());
} else {
println!("{}", style("+new results").green());
}
print_changeset(&changeset, new.metadata().expression.as_deref());
}
fn print_snapshot_diff_with_title(
workspace_root: &Path,
new_snapshot: &Snapshot,
old_snapshot: Option<&Snapshot>,
line: u32,
snapshot_file: Option<&Path>,
) {
let width = console::Term::stdout().size().1 as usize;
println!(
"{title:━^width$}",
title = style(" Snapshot Differences ").bold(),
width = width
);
print_snapshot_diff(
workspace_root,
new_snapshot,
old_snapshot,
snapshot_file,
Some(line),
);
}
fn print_snapshot_summary_with_title(
workspace_root: &Path,
new_snapshot: &Snapshot,
old_snapshot: Option<&Snapshot>,
line: u32,
snapshot_file: Option<&Path>,
) {
let _old_snapshot = old_snapshot;
let width = console::Term::stdout().size().1 as usize;
println!(
"{title:━^width$}",
title = style(" Snapshot Summary ").bold(),
width = width
);
print_snapshot_summary(workspace_root, new_snapshot, snapshot_file, Some(line));
println!("{title:━^width$}", title = "", width = width);
}
/// Special marker to use an automatic name.
///
/// This can be passed as a snapshot name in a macro to explicitly tell
/// insta to use the automatic name. This is useful in ambiguous syntax
/// situations.
#[derive(Debug)]
pub struct AutoName;
impl From<AutoName> for ReferenceValue<'static> {
fn from(_value: AutoName) -> ReferenceValue<'static> {
ReferenceValue::Named(None)
}
}
impl From<Option<String>> for ReferenceValue<'static> {
fn from(value: Option<String>) -> ReferenceValue<'static> {
ReferenceValue::Named(value.map(Cow::Owned))
}
}
impl From<String> for ReferenceValue<'static> {
fn from(value: String) -> ReferenceValue<'static> {
ReferenceValue::Named(Some(Cow::Owned(value)))
}
}
impl<'a> From<Option<&'a str>> for ReferenceValue<'a> {
fn from(value: Option<&'a str>) -> ReferenceValue<'a> {
ReferenceValue::Named(value.map(Cow::Borrowed))
}
}
impl<'a> From<&'a str> for ReferenceValue<'a> {
fn from(value: &'a str) -> ReferenceValue<'a> {
ReferenceValue::Named(Some(Cow::Borrowed(value)))
}
}
pub enum ReferenceValue<'a> {
Named(Option<Cow<'a, str>>),
Inline(&'a str),
}
#[cfg(feature = "backtrace")]
fn test_name_from_backtrace(module_path: &str) -> Result<String, &'static str> {
let backtrace = backtrace::Backtrace::new();
let frames = backtrace.frames();
let mut found_run_wrapper = false;
for symbol in frames
.iter()
.rev()
.flat_map(|x| x.symbols())
.filter_map(|x| x.name())
.map(|x| format!("{}", x))
{
if !found_run_wrapper {
if symbol.starts_with("test::run_test::") {
found_run_wrapper = true;
}
} else if symbol.starts_with(module_path) {
let mut rv = &symbol[..symbol.len() - 19];
if rv.ends_with("::{{closure}}") {
rv = &rv[..rv.len() - 13];
}
return Ok(rv.to_string());
}
}
Err(
"Cannot determine test name from backtrace, no snapshot name \
can be generated. Did you forget to enable debug info?",
)
}
fn generate_snapshot_name_for_thread(module_path: &str) -> Result<String, &'static str> {
let thread = thread::current();
#[allow(unused_mut)]
let mut name = Cow::Borrowed(
thread
.name()
.ok_or("test thread is unnamed, no snapshot name can be generated.")?,
);
if name == "main" {
#[cfg(feature = "backtrace")]
{
name = Cow::Owned(test_name_from_backtrace(module_path)?);
}
#[cfg(not(feature = "backtrace"))]
{
return Err("tests run with disabled concurrency, automatic snapshot \
name generation is not supported. Consider using the \
\"backtrace\" feature of insta which tries to recover test \
names from the call stack.");
}
}
// clean test name first
let mut name = name.rsplit("::").next().unwrap();
if name.starts_with("test_") {
name = &name[5..];
}
// next check if we need to add a suffix
let name = add_suffix_to_snapshot_name(Cow::Borrowed(name));
let key = format!("{}::{}", module_path.replace("::", "__"), name);
// if the snapshot name clashes we need to increment a counter.
// we really do not care about poisoning here.
let mut counters = TEST_NAME_COUNTERS.lock().unwrap_or_else(|x| x.into_inner());
let test_idx = counters.get(&key).cloned().unwrap_or(0) + 1;
let rv = if test_idx == 1 {
name.to_string()
} else {
format!("{}-{}", name, test_idx)
};
counters.insert(key, test_idx);
Ok(rv)
}
/// Helper function that returns the real inline snapshot value from a given
/// frozen value string. If the string starts with the '⋮' character
/// (optionally prefixed by whitespace) the alternative serialization format
/// is picked which has slightly improved indentation semantics.
pub(super) fn get_inline_snapshot_value(frozen_value: &str) -> String {
// TODO: could move this into the SnapshotContents `from_inline` method
// (the only call site)
if frozen_value.trim_start().starts_with('⋮') {
// legacy format - retain so old snapshots still work
let mut buf = String::new();
let mut line_iter = frozen_value.lines();
let mut indentation = 0;
for line in &mut line_iter {
let line_trimmed = line.trim_start();
if line_trimmed.is_empty() {
continue;
}
indentation = line.len() - line_trimmed.len();
// 3 because '⋮' is three utf-8 bytes long
buf.push_str(&line_trimmed[3..]);
buf.push('\n');
break;
}
for line in &mut line_iter {
if let Some(prefix) = line.get(..indentation) {
if !prefix.trim().is_empty() {
return "".to_string();
}
}
if let Some(remainder) = line.get(indentation..) {
if remainder.starts_with('⋮') {
// 3 because '⋮' is three utf-8 bytes long
buf.push_str(&remainder[3..]);
buf.push('\n');
} else if remainder.trim().is_empty() {
continue;
} else {
return "".to_string();
}
}
}
buf.trim_end().to_string()
} else {
normalize_inline_snapshot(frozen_value)
}
}
#[test]
fn test_inline_snapshot_value_newline() {
// https://github.com/mitsuhiko/insta/issues/39
assert_eq!(get_inline_snapshot_value("\n"), "");
}
fn count_leading_spaces(value: &str) -> usize {
value.chars().take_while(|x| x.is_whitespace()).count()
}
fn min_indentation(snapshot: &str) -> usize {
let lines = snapshot.trim_end().lines();
if lines.clone().count() <= 1 {
// not a multi-line string
return 0;
}
lines
.skip_while(|l| l.is_empty())
.map(count_leading_spaces)
.min()
.unwrap_or(0)
}
#[test]
fn test_min_indentation() {
let t = r#"
1
2
"#;
assert_eq!(min_indentation(t), 3);
let t = r#"
1
2"#;
assert_eq!(min_indentation(t), 4);
let t = r#"
1
2
"#;
assert_eq!(min_indentation(t), 12);
let t = r#"
1
2
"#;
assert_eq!(min_indentation(t), 3);
let t = r#"
a
"#;
assert_eq!(min_indentation(t), 8);
let t = "";
assert_eq!(min_indentation(t), 0);
let t = r#"
a
b
c
"#;
assert_eq!(min_indentation(t), 0);
let t = r#"
a
"#;
assert_eq!(min_indentation(t), 0);
let t = "
a";
assert_eq!(min_indentation(t), 4);
let t = r#"a
a"#;
assert_eq!(min_indentation(t), 0);
}
// Removes excess indentation, removes excess whitespace at start & end
fn normalize_inline_snapshot(snapshot: &str) -> String {
let indentation = min_indentation(snapshot);
snapshot
.trim_end()
.lines()
.skip_while(|l| l.is_empty())
.map(|l| &l[indentation..])
.collect::<Vec<&str>>()
.join("\n")
}
#[test]
fn test_normalize_inline_snapshot() {
// here we do exact matching (rather than `assert_snapshot`)
// to ensure we're not incorporating the modifications this library makes
let t = r#"
1
2
"#;
assert_eq!(
normalize_inline_snapshot(t),
r###"
1
2"###[1..]
);
let t = r#"
1
2"#;
assert_eq!(
normalize_inline_snapshot(t),
r###"
1
2"###[1..]
);
let t = r#"
1
2
"#;
assert_eq!(
normalize_inline_snapshot(t),
r###"
1
2"###[1..]
);
let t = r#"
1
2
"#;
assert_eq!(
normalize_inline_snapshot(t),
r###"
1
2"###[1..]
);
let t = r#"
a
"#;
assert_eq!(normalize_inline_snapshot(t), "a");
let t = "";
assert_eq!(normalize_inline_snapshot(t), "");
let t = r#"
a
b
c
"#;
assert_eq!(
normalize_inline_snapshot(t),
r###"
a
b
c"###[1..]
);
let t = r#"
a
"#;
assert_eq!(normalize_inline_snapshot(t), "a");
let t = "
a";
assert_eq!(normalize_inline_snapshot(t), "a");
let t = r#"a
a"#;
assert_eq!(
normalize_inline_snapshot(t),
r###"
a
a"###[1..]
);
}
fn update_snapshots(
snapshot_file: Option<&Path>,
new: Snapshot,
old: Option<Snapshot>,
line: u32,
pending_snapshots: Option<PathBuf>,
output_behavior: OutputBehavior,
) -> Result<(), Box<dyn Error>> {
let unseen = snapshot_file.map_or(false, |x| fs::metadata(x).is_ok());
let should_print = output_behavior != OutputBehavior::Nothing;
match update_snapshot_behavior(unseen) {
UpdateBehavior::InPlace => {
if let Some(ref snapshot_file) = snapshot_file {
new.save(snapshot_file)?;
if should_print {
eprintln!(
"{} {}",
if unseen {
style("created previously unseen snapshot").green()
} else {
style("updated snapshot").green()
},
style(snapshot_file.display()).cyan().underlined(),
);
}
return Ok(());
} else if should_print {
eprintln!(
"{}",
style("error: cannot update inline snapshots in-place")
.red()
.bold(),
);
}
}
UpdateBehavior::NewFile => {
if let Some(ref snapshot_file) = snapshot_file {
let mut new_path = snapshot_file.to_path_buf();
new_path.set_extension("snap.new");
new.save(&new_path)?;
if should_print {
eprintln!(
"{} {}",
style("stored new snapshot").green(),
style(new_path.display()).cyan().underlined(),
);
}
} else {
PendingInlineSnapshot::new(Some(new), old, line)
.save(pending_snapshots.unwrap())?;
}
}
UpdateBehavior::NoUpdate => {}
}
Ok(())
}
/// If there is a suffix on the settings, append it to the snapshot name.
fn add_suffix_to_snapshot_name(name: Cow<'_, str>) -> Cow<'_, str> {
Settings::with(|settings| {
settings
.snapshot_suffix()
.map(|suffix| Cow::Owned(format!("{}@{}", name, suffix)))
.unwrap_or_else(|| name)
})
}
#[allow(clippy::too_many_arguments)]
pub fn assert_snapshot(
refval: ReferenceValue<'_>,
new_snapshot: &str,
manifest_dir: &str,
module_path: &str,
file: &str,
line: u32,
expr: &str,
) -> Result<(), Box<dyn Error>> {
let cargo_workspace = get_cargo_workspace(manifest_dir);
let output_behavior = output_snapshot_behavior();
let (snapshot_name, snapshot_file, old, pending_snapshots) = match refval {
ReferenceValue::Named(snapshot_name) => {
let snapshot_name = match snapshot_name {
Some(snapshot_name) => add_suffix_to_snapshot_name(snapshot_name),
None => generate_snapshot_name_for_thread(module_path)
.unwrap()
.into(),
};
let snapshot_file =
get_snapshot_filename(module_path, &snapshot_name, &cargo_workspace, file);
let old = if fs::metadata(&snapshot_file).is_ok() {
Some(Snapshot::from_file(&snapshot_file)?)
} else {
None
};
(Some(snapshot_name), Some(snapshot_file), old, None)
}
ReferenceValue::Inline(contents) => {
let snapshot_name = generate_snapshot_name_for_thread(module_path)
.ok()
.map(Cow::Owned);
let mut filename = cargo_workspace.join(file);
filename.set_file_name(format!(
".{}.pending-snap",
filename
.file_name()
.expect("no filename")
.to_str()
.expect("non unicode filename")
));
(
snapshot_name,
None,
Some(Snapshot::from_components(
module_path.replace("::", "__"),
None,
MetaData::default(),
SnapshotContents::from_inline(contents),
)),
Some(filename),
)
}
};
let new_snapshot_contents: SnapshotContents = new_snapshot.into();
let new = Snapshot::from_components(
module_path.replace("::", "__"),
snapshot_name.as_ref().map(|x| x.to_string()),
MetaData {
source: Some(path_to_storage(file)),
expression: Some(expr.to_string()),
input_file: Settings::with(|settings| {
settings
.input_file()
.and_then(|x| cargo_workspace.join(x).canonicalize().ok())
.and_then(|s| {
s.strip_prefix(cargo_workspace)
.ok()
.map(|x| x.to_path_buf())
})
.map(path_to_storage)
}),
},
new_snapshot_contents,
);
// if the snapshot matches we're done.
if let Some(ref old_snapshot) = old {
if old_snapshot.contents() == new.contents() {
// let's just make sure there are no more pending files lingering
// around.
if let Some(ref snapshot_file) = snapshot_file {
let mut snapshot_file = snapshot_file.clone();
snapshot_file.set_extension("snap.new");
fs::remove_file(snapshot_file).ok();
}
// and add a null pending snapshot to a pending snapshot file if needed
if let Some(ref pending_snapshots) = pending_snapshots {
if fs::metadata(pending_snapshots).is_ok() {
PendingInlineSnapshot::new(None, None, line).save(pending_snapshots)?;
}
}
if force_update_snapshots() {
update_snapshots(
snapshot_file.as_deref(),
new,
old,
line,
pending_snapshots,
output_behavior,
)?;
}
return Ok(());
}
}
match output_behavior {
OutputBehavior::Summary => {
print_snapshot_summary_with_title(
cargo_workspace,
&new,
old.as_ref(),
line,
snapshot_file.as_deref(),
);
}
OutputBehavior::Diff => {
print_snapshot_diff_with_title(
cargo_workspace,
&new,
old.as_ref(),
line,
snapshot_file.as_deref(),
);
}
_ => {}
}
update_snapshots(
snapshot_file.as_deref(),
new,
old,
line,
pending_snapshots,
output_behavior,
)?;
if output_behavior != OutputBehavior::Nothing {
println!(
"{hint}",
hint = style("To update snapshots run `cargo insta review`").dim(),
);
}
if should_fail_in_tests() {
panic!(
"snapshot assertion for '{}' failed in line {}",
snapshot_name.as_ref().map_or("unnamed snapshot", |x| &*x),
line
);
}
Ok(())
}