From fc3f35a5591f3933429f285e37a92437a9953528 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 28 Jan 2019 16:31:42 +0100 Subject: [PATCH 01/18] Added inline snapshot support --- Cargo.toml | 9 +- cargo-insta/Cargo.toml | 5 +- cargo-insta/src/cargo.rs | 197 +++++++++++++++++---- cargo-insta/src/cli.rs | 111 ++++++------ cargo-insta/src/inline.rs | 170 ++++++++++++++++++ cargo-insta/src/main.rs | 1 + src/lib.rs | 9 +- src/macros.rs | 81 ++++++++- src/runtime.rs | 361 +++++++++++++++----------------------- src/snapshot.rs | 237 +++++++++++++++++++++++++ tests/test_inline.rs | 45 +++++ 11 files changed, 915 insertions(+), 311 deletions(-) create mode 100644 cargo-insta/src/inline.rs create mode 100644 src/snapshot.rs create mode 100644 tests/test_inline.rs diff --git a/Cargo.toml b/Cargo.toml index 71b12820..3ca5e92c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "insta" -version = "0.5.2" +version = "0.6.0" license = "Apache-2.0" authors = ["Armin Ronacher "] description = "A snapshot testing library for Rust" @@ -29,13 +29,14 @@ serde = { version = "1.0.85", features = ["derive"] } failure = "0.1.5" serde_yaml = "0.8.8" console = "0.7.5" -chrono = "0.4.6" +chrono = { version = "0.4.6", features = ["serde"] } serde_json = "1.0.36" lazy_static = "1.2.0" ci_info = "0.3.1" pest = "2.1.0" pest_derive = "2.1.0" ron = "0.4.1" - -[dev-dependencies] uuid = { version = "0.7.1", features = ["serde", "v4"] } + +[patch.crates-io] +proc-macro2 = { git = "https://github.com/mitsuhiko/proc-macro2", rev = "e09dfd48f2ac12a38898aa7812bd06935298d23f" } diff --git a/cargo-insta/Cargo.toml b/cargo-insta/Cargo.toml index 0fc63381..ec639b62 100644 --- a/cargo-insta/Cargo.toml +++ b/cargo-insta/Cargo.toml @@ -11,7 +11,7 @@ edition = "2018" readme = "README.md" [dependencies] -insta = { version = "0.5.0", path = ".." } +insta = { version = "0.6.0", path = ".." } console = "0.7.5" clap = "2.32.0" difference = "2.0.0" @@ -21,3 +21,6 @@ serde_json = "1.0.36" failure = "0.1.5" glob = "0.2.11" walkdir = "2.2.7" +proc-macro2 = { version = "0.4.26", features = ["span-location-info"] } +syn = { version = "0.15.26", features = ["full"] } +quote = { version = "0.6.11" } diff --git a/cargo-insta/src/cargo.rs b/cargo-insta/src/cargo.rs index 19c65ad1..3c32b88c 100644 --- a/cargo-insta/src/cargo.rs +++ b/cargo-insta/src/cargo.rs @@ -5,10 +5,12 @@ use std::path::{Component, Path, PathBuf}; use std::process; use failure::{err_msg, Error}; -pub use insta::Snapshot; +use insta::{PendingInlineSnapshot, Snapshot}; use serde::Deserialize; use walkdir::{DirEntry, WalkDir}; +use crate::inline::FilePatcher; + #[derive(Deserialize, Clone, Debug)] pub struct Target { src_path: PathBuf, @@ -30,6 +32,13 @@ pub struct Metadata { workspace_root: String, } +#[derive(Clone, Copy, Debug)] +pub enum Operation { + Accept, + Reject, + Skip, +} + impl Metadata { pub fn workspace_root(&self) -> &Path { Path::new(&self.workspace_root) @@ -42,41 +51,147 @@ struct ProjectLocation { } #[derive(Debug)] -pub struct SnapshotRef { - old_path: PathBuf, - new_path: PathBuf, +pub enum SnapshotContainerKind { + Inline, + External, } -impl SnapshotRef { - fn new(new_path: PathBuf) -> SnapshotRef { - let mut old_path = new_path.clone(); - old_path.set_extension(""); - SnapshotRef { old_path, new_path } - } - - pub fn path(&self) -> &Path { - &self.old_path - } +#[derive(Debug)] +pub struct PendingSnapshot { + pub id: usize, + pub old: Option, + pub new: Snapshot, + pub op: Operation, + pub line: Option, +} - pub fn load_old(&self) -> Result, Error> { - if fs::metadata(&self.old_path).is_err() { - Ok(None) +impl PendingSnapshot { + pub fn id(&self) -> String { + if let Some(name) = self.new.snapshot_name() { + format!("{}#{}", self.new.module_name(), name) } else { - Snapshot::from_file(&self.old_path).map(Some) + format!( + "{}#inline+{}", + self.new.module_name(), + self.line.unwrap_or(0) + ) } } +} - pub fn load_new(&self) -> Result { - Snapshot::from_file(&self.new_path) +#[derive(Debug)] +pub struct SnapshotContainer { + snapshot_path: PathBuf, + target_path: PathBuf, + kind: SnapshotContainerKind, + snapshots: Vec, + patcher: Option, +} + +impl SnapshotContainer { + fn load( + snapshot_path: PathBuf, + target_path: PathBuf, + kind: SnapshotContainerKind, + ) -> Result { + let mut snapshots = Vec::new(); + let patcher = match kind { + SnapshotContainerKind::External => { + let old = if fs::metadata(&target_path).is_err() { + None + } else { + Some(Snapshot::from_file(&target_path)?) + }; + let new = Snapshot::from_file(&snapshot_path)?; + snapshots.push(PendingSnapshot { + id: 0, + old, + new, + op: Operation::Skip, + line: None, + }); + None + } + SnapshotContainerKind::Inline => { + let pending_vec = PendingInlineSnapshot::load_batch(&snapshot_path)?; + let mut patcher = FilePatcher::open(&target_path)?; + for (id, pending) in pending_vec.into_iter().enumerate() { + snapshots.push(PendingSnapshot { + id, + old: pending.old, + new: pending.new, + op: Operation::Skip, + line: Some(pending.line), + }); + patcher.add_snapshot_macro(pending.line as usize); + } + Some(patcher) + } + }; + + Ok(SnapshotContainer { + snapshot_path, + target_path, + kind, + snapshots, + patcher, + }) } - pub fn accept(&self) -> Result<(), Error> { - fs::rename(&self.new_path, &self.old_path)?; - Ok(()) + pub fn len(&self) -> usize { + self.snapshots.len() } - pub fn reject(&self) -> Result<(), Error> { - fs::remove_file(&self.new_path)?; + pub fn iter_snapshots(&mut self) -> impl Iterator { + self.snapshots.iter_mut() + } + + pub fn commit(&mut self) -> Result<(), Error> { + if let Some(ref mut patcher) = self.patcher { + let mut new_pending = vec![]; + let mut did_accept = false; + let mut did_skip = false; + + for (idx, snapshot) in self.snapshots.iter().enumerate() { + match snapshot.op { + Operation::Accept => { + patcher.set_new_content(idx, snapshot.new.contents()); + did_accept = true; + } + Operation::Reject => {} + Operation::Skip => { + new_pending.push(PendingInlineSnapshot::new( + snapshot.new.clone(), + snapshot.old.clone(), + patcher.get_new_line(idx) as u32, + )); + did_skip = true; + } + } + } + + if did_accept { + patcher.save()?; + } + if did_skip { + PendingInlineSnapshot::save_batch(&self.snapshot_path, &new_pending)?; + } else { + fs::remove_file(&self.snapshot_path)?; + } + } else { + // should only be one or this is weird + for snapshot in self.snapshots.iter() { + match snapshot.op { + Operation::Accept => { + fs::rename(&self.snapshot_path, &self.target_path)?; + } + Operation::Reject => { + fs::remove_file(&self.snapshot_path)?; + } + Operation::Skip => {} + } + } + } Ok(()) } } @@ -98,7 +213,9 @@ impl Package { &self.version } - pub fn iter_snapshots(&self) -> impl Iterator { + pub fn iter_snapshot_containers( + &self, + ) -> impl Iterator> { let mut roots = HashSet::new(); for target in &self.targets { let root = target.src_path.parent().unwrap(); @@ -109,10 +226,11 @@ impl Package { roots.into_iter().flat_map(|root| { WalkDir::new(root.clone()) .into_iter() - .filter_entry(|e| !is_hidden(e)) + .filter_entry(|e| e.file_type().is_file() || !is_hidden(e)) .filter_map(|e| e.ok()) - .filter(move |e| { - e.file_name().to_string_lossy().ends_with(".snap.new") + .filter_map(move |e| { + let fname = e.file_name().to_string_lossy(); + if fname.ends_with(".snap.new") && e.path() .strip_prefix(&root) .unwrap() @@ -121,8 +239,27 @@ impl Package { Component::Normal(dir) => dir.to_str() == Some("snapshots"), _ => false, }) + { + let new_path = e.into_path(); + let mut old_path = new_path.clone(); + old_path.set_extension(""); + Some(SnapshotContainer::load( + new_path, + old_path, + SnapshotContainerKind::External, + )) + } else if fname.starts_with(".") && fname.ends_with(".pending-snap") { + let mut target_path = e.path().to_path_buf(); + target_path.set_file_name(&fname[1..fname.len() - 13]); + Some(SnapshotContainer::load( + e.path().to_path_buf(), + target_path, + SnapshotContainerKind::Inline, + )) + } else { + None + } }) - .map(|e| SnapshotRef::new(e.into_path())) }) } } diff --git a/cargo-insta/src/cli.rs b/cargo-insta/src/cli.rs index 535396cf..954e4333 100644 --- a/cargo-insta/src/cli.rs +++ b/cargo-insta/src/cli.rs @@ -3,10 +3,11 @@ use std::path::{Path, PathBuf}; use console::{set_colors_enabled, style, Key, Term}; use failure::{err_msg, Error}; +use insta::{print_snapshot_diff, Snapshot}; use structopt::clap::AppSettings; use structopt::StructOpt; -use crate::cargo::{find_packages, get_package_metadata, Metadata, Package, SnapshotRef}; +use crate::cargo::{find_packages, get_package_metadata, Metadata, Operation, Package}; /// A helper utility to work with insta snapshots. #[derive(StructOpt, Debug)] @@ -61,43 +62,30 @@ pub struct ProcessCommand { pub pkg_args: PackageArgs, } -#[derive(Clone, Copy, Debug)] -enum Operation { - Accept, - Reject, - Skip, -} - -fn process_snapshot( +fn query_snapshot( + workspace_root: &Path, term: &Term, - cargo_workspace: &Path, - snapshot_ref: &SnapshotRef, + new: &Snapshot, + old: Option<&Snapshot>, pkg: &Package, + line: Option, i: usize, n: usize, ) -> Result { - let old = snapshot_ref.load_old()?; - let new = snapshot_ref.load_new()?; - - let path = snapshot_ref - .path() - .strip_prefix(cargo_workspace) - .ok() - .unwrap_or_else(|| snapshot_ref.path()); - term.clear_screen()?; println!( - "{}{}{} {} in {} ({})", + "{}{}{} {} ({})", style("Reviewing [").bold(), style(format!("{}/{}", i, n)).yellow().bold(), style("]:").bold(), - style(path.display()).cyan().underlined().bold(), style(pkg.name()).dim(), pkg.version() ); - println!("Snapshot: {}", style(new.snapshot_name()).yellow()); - new.print_changes(old.as_ref()); + if let Some(snapshot_name) = new.snapshot_name() { + println!("Snapshot: {}", style(snapshot_name).yellow()); + } + print_snapshot_diff(workspace_root, new, old, line); println!(""); println!( @@ -145,12 +133,15 @@ fn handle_pkg_args(pkg_args: &PackageArgs) -> Result<(Metadata, Vec), E fn process_snapshots(cmd: &ProcessCommand, op: Option) -> Result<(), Error> { let term = Term::stdout(); let (metadata, packages) = handle_pkg_args(&cmd.pkg_args)?; - let snapshots: Vec<_> = packages - .iter() - .flat_map(|p| p.iter_snapshots().map(move |s| (s, p))) - .collect(); + let mut snapshot_containers = vec![]; + for package in &packages { + for snapshot_container in package.iter_snapshot_containers() { + snapshot_containers.push((snapshot_container?, package)); + } + } + let snapshot_count = snapshot_containers.iter().map(|x| x.0.len()).sum(); - if snapshots.is_empty() { + if snapshot_count == 0 { println!("{}: no snapshots to review", style("done").bold()); return Ok(()); } @@ -158,33 +149,39 @@ fn process_snapshots(cmd: &ProcessCommand, op: Option) -> Result<(), let mut accepted = vec![]; let mut rejected = vec![]; let mut skipped = vec![]; - - for (idx, (snapshot, package)) in snapshots.iter().enumerate() { - let op = match op { - Some(op) => op, - None => process_snapshot( - &term, - metadata.workspace_root(), - snapshot, - package, - idx + 1, - snapshots.len(), - )?, - }; - let path = snapshot.path().to_path_buf(); - match op { - Operation::Accept => { - snapshot.accept()?; - accepted.push(path); - } - Operation::Reject => { - snapshot.reject()?; - rejected.push(path); - } - Operation::Skip => { - skipped.push(path); + let mut num = 0; + + for (snapshot_container, package) in snapshot_containers.iter_mut() { + for snapshot_ref in snapshot_container.iter_snapshots() { + num += 1; + let op = match op { + Some(op) => op, + None => query_snapshot( + metadata.workspace_root(), + &term, + &snapshot_ref.new, + snapshot_ref.old.as_ref(), + package, + snapshot_ref.line, + num, + snapshot_count, + )?, + }; + match op { + Operation::Accept => { + snapshot_ref.op = Operation::Accept; + accepted.push(snapshot_ref.id()); + } + Operation::Reject => { + snapshot_ref.op = Operation::Reject; + rejected.push(snapshot_ref.id()); + } + Operation::Skip => { + skipped.push(snapshot_ref.id()); + } } } + snapshot_container.commit()?; } if op.is_none() { @@ -195,19 +192,19 @@ fn process_snapshots(cmd: &ProcessCommand, op: Option) -> Result<(), if !accepted.is_empty() { println!("{}:", style("accepted").green()); for item in accepted { - println!(" {}", item.display()); + println!(" {}", item); } } if !rejected.is_empty() { println!("{}:", style("rejected").red()); for item in rejected { - println!(" {}", item.display()); + println!(" {}", item); } } if !skipped.is_empty() { println!("{}:", style("skipped").yellow()); for item in skipped { - println!(" {}", item.display()); + println!(" {}", item); } } diff --git a/cargo-insta/src/inline.rs b/cargo-insta/src/inline.rs new file mode 100644 index 00000000..ce0341e7 --- /dev/null +++ b/cargo-insta/src/inline.rs @@ -0,0 +1,170 @@ +use std::fs; +use std::io::Write; +use std::path::{Path, PathBuf}; + +use failure::Error; +use proc_macro2::TokenTree; +use syn; +use syn::spanned::Spanned; + +#[derive(Debug)] +pub struct InlineSnapshot { + start: (usize, usize), + end: (usize, usize), +} + +#[derive(Debug)] +pub struct FilePatcher { + filename: PathBuf, + lines: Vec, + newline: &'static str, + source: syn::File, + inline_snapshots: Vec, +} + +impl FilePatcher { + pub fn open>(p: P) -> Result { + let filename = p.as_ref().to_path_buf(); + let contents = fs::read_to_string(p)?; + let source = syn::parse_file(&contents)?; + let mut line_iter = contents.lines().peekable(); + let newline = if let Some(line) = line_iter.peek() { + match contents.as_bytes().get(line.len() + 1) { + Some(b'\r') => &"\r\n", + _ => &"\n", + } + } else { + &"\n" + }; + let lines: Vec = line_iter.map(|x| x.into()).collect(); + Ok(FilePatcher { + filename, + source, + newline, + lines, + inline_snapshots: vec![], + }) + } + + pub fn save(&self) -> Result<(), Error> { + let mut f = fs::File::create(&self.filename)?; + for line in &self.lines { + writeln!(&mut f, "{}", line)?; + } + Ok(()) + } + + pub fn add_snapshot_macro(&mut self, line: usize) { + match self.find_snapshot_macro(line) { + Some(snapshot) => self.inline_snapshots.push(snapshot), + None => panic!("Could not find snapshot in line {}", line), + } + } + + pub fn get_new_line(&self, id: usize) -> usize { + self.inline_snapshots[id].start.0 + 1 + } + + pub fn set_new_content(&mut self, id: usize, snapshot: &str) { + let inline = &mut self.inline_snapshots[id]; + let old_lines = inline.end.0 - inline.start.0 + 1; + + // find prefix and suffix + let prefix: String = self.lines[inline.start.0] + .chars() + .take(inline.start.1) + .collect(); + let suffix: String = self.lines[inline.end.0] + .chars() + .skip(inline.end.1) + .collect(); + + // replace lines + let mut new_lines: Vec<_> = snapshot.lines().collect(); + if new_lines.is_empty() { + new_lines.push(""); + } + let (quote_start, quote_end) = + if new_lines.len() > 1 || new_lines[0].contains(&['\\', '"'][..]) { + ("r###\"", "\"###") + } else { + ("\"", "\"") + }; + let line_count_diff = new_lines.len() as i64 - old_lines as i64; + + self.lines.splice( + inline.start.0..=inline.end.0, + new_lines.iter().enumerate().map(|(idx, line)| { + let mut rv = String::new(); + if idx == 0 { + rv.push_str(&prefix); + rv.push_str(quote_start); + } + rv.push_str(&line); + if idx + 1 == new_lines.len() { + rv.push_str(quote_end); + rv.push_str(&suffix); + } + rv + }), + ); + + for inl in &mut self.inline_snapshots[id..] { + inl.start.0 = (inl.start.0 as i64 + line_count_diff) as usize; + inl.end.0 = (inl.end.0 as i64 + line_count_diff) as usize; + } + } + + fn find_snapshot_macro(&self, line: usize) -> Option { + struct Visitor(usize, Option); + + impl<'ast> syn::visit::Visit<'ast> for Visitor { + fn visit_macro(&mut self, i: &'ast syn::Macro) { + if i.span().start().line != self.0 || i.path.segments.is_empty() { + return; + } + + let last = i.path.segments[i.path.segments.len() - 1].ident.to_string(); + if !last.starts_with("assert_") || !last.ends_with("_snapshot_matches") { + return; + } + + let tokens: Vec<_> = i.tts.clone().into_iter().collect(); + if tokens.len() < 2 { + return; + } + + match &tokens[tokens.len() - 2] { + TokenTree::Punct(ref punct) if punct.as_char() == '@' => {} + _ => return, + } + + let (start, end) = match &tokens[tokens.len() - 1] { + TokenTree::Literal(lit) => { + let span = lit.span(); + ( + (span.start().line - 1, span.start().column), + (span.end().line - 1, span.end().column), + ) + } + _ => return, + }; + + self.1 = Some(InlineSnapshot { start, end }); + } + } + + let mut visitor = Visitor(line, None); + syn::visit::visit_file(&mut visitor, &self.source); + visitor.1 + } +} + +#[test] +fn test_patcher() { + let mut patcher = FilePatcher::open("tests/test_inline.rs").unwrap(); + patcher.set_new_content(0, "this is the new content\nsecond line\nMOAR!"); + patcher.set_new_content(1, "almost empty"); + println!("{:#?}", &patcher.lines); + panic!(); +} diff --git a/cargo-insta/src/main.rs b/cargo-insta/src/main.rs index f54272b4..fb951371 100644 --- a/cargo-insta/src/main.rs +++ b/cargo-insta/src/main.rs @@ -11,6 +11,7 @@ //! ``` mod cargo; mod cli; +mod inline; fn main() { if let Err(err) = cli::run() { diff --git a/src/lib.rs b/src/lib.rs index d3400acb..9442d1b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -162,18 +162,23 @@ mod content; mod redaction; mod runtime; mod serialization; +mod snapshot; #[cfg(test)] mod test; -pub use crate::runtime::Snapshot; +pub use crate::snapshot::Snapshot; + +// exported for cargo-insta only +#[doc(hidden)] +pub use crate::{runtime::print_snapshot_diff, snapshot::PendingInlineSnapshot}; // these are here to make the macros work #[doc(hidden)] pub mod _macro_support { pub use crate::content::Content; pub use crate::redaction::Selector; - pub use crate::runtime::assert_snapshot; + pub use crate::runtime::{assert_snapshot, ReferenceValue}; pub use crate::serialization::{ serialize_value, serialize_value_redacted, SerializationFormat, }; diff --git a/src/macros.rs b/src/macros.rs index 7514d466..bbd6c6f7 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -32,6 +32,12 @@ /// The replacement value can be a string, integer or any other primitive value. #[macro_export] macro_rules! assert_serialized_snapshot_matches { + ($value:expr, @$snapshot:literal) => {{ + $crate::_assert_serialized_snapshot_matches!($value, Yaml, @$snapshot); + }}; + ($value:expr, {$($k:expr => $v:expr),*}, @$snapshot:literal) => {{ + $crate::_assert_serialized_snapshot_matches!($value, {$($k => $v),*}, Yaml, @$snapshot); + }}; ($name:expr, $value:expr) => {{ $crate::_assert_serialized_snapshot_matches!($name, $value, Yaml); }}; @@ -57,6 +63,12 @@ macro_rules! assert_serialized_snapshot_matches { /// about redactions see [redactions](index.html#redactions). #[macro_export] macro_rules! assert_ron_snapshot_matches { + ($value:expr, @$snapshot:literal) => {{ + $crate::_assert_serialized_snapshot_matches!($value, Ron, @$snapshot); + }}; + ($value:expr, {$($k:expr => $v:expr),*}, @$snapshot:literal) => {{ + $crate::_assert_serialized_snapshot_matches!($value, {$($k => $v),*}, Ron, @$snapshot); + }}; ($name:expr, $value:expr) => {{ $crate::_assert_serialized_snapshot_matches!($name, $value, Ron); }}; @@ -82,6 +94,12 @@ macro_rules! assert_ron_snapshot_matches { /// about redactions see [redactions](index.html#redactions). #[macro_export] macro_rules! assert_json_snapshot_matches { + ($value:expr, @$snapshot:literal) => {{ + $crate::_assert_serialized_snapshot_matches!($value, Json, @$snapshot); + }}; + ($value:expr, {$($k:expr => $v:expr),*}, @$snapshot:literal) => {{ + $crate::_assert_serialized_snapshot_matches!($value, {$($k => $v),*}, Json, @$snapshot); + }}; ($name:expr, $value:expr) => {{ $crate::_assert_serialized_snapshot_matches!($name, $value, Json); }}; @@ -93,6 +111,31 @@ macro_rules! assert_json_snapshot_matches { #[doc(hidden)] #[macro_export] macro_rules! _assert_serialized_snapshot_matches { + ($value:expr, $format:ident, @$snapshot:literal) => {{ + let value = $crate::_macro_support::serialize_value( + &$value, + $crate::_macro_support::SerializationFormat::$format + ); + $crate::assert_snapshot_matches!( + value, + stringify!($value), + @$snapshot + ); + }}; + ($value:expr, {$($k:expr => $v:expr),*}, $format:ident, @$snapshot:literal) => {{ + let vec = vec![ + $(( + $crate::_macro_support::Selector::parse($k).unwrap(), + $crate::_macro_support::Content::from($v) + ),)* + ]; + let value = $crate::_macro_support::serialize_value_redacted( + &$value, + &vec, + $crate::_macro_support::SerializationFormat::$format + ); + $crate::assert_snapshot_matches!(value, stringify!($value), @snapshot); + }}; ($name:expr, $value:expr, $format:ident) => {{ let value = $crate::_macro_support::serialize_value( &$value, @@ -127,6 +170,10 @@ macro_rules! _assert_serialized_snapshot_matches { /// permit redactions. #[macro_export] macro_rules! assert_debug_snapshot_matches { + ($value:expr, @$snapshot:literal) => {{ + let value = format!("{:#?}", $value); + $crate::assert_snapshot_matches!(value, stringify!($value), @$snapshot); + }}; ($name:expr, $value:expr) => {{ let value = format!("{:#?}", $value); $crate::assert_snapshot_matches!($name, value, stringify!($value)); @@ -148,14 +195,44 @@ macro_rules! assert_debug_snapshot_matches { /// source of this macro and other assertion macros. #[macro_export] macro_rules! assert_snapshot_matches { + ($value:expr, @$snapshot:literal) => { + $crate::_assert_snapshot_matches!( + $crate::_macro_support::ReferenceValue::Inline($snapshot), + $value, + stringify!($value) + ) + }; + ($value:expr, $debug_expr:expr, @$snapshot:literal) => { + $crate::_assert_snapshot_matches!( + $crate::_macro_support::ReferenceValue::Inline($snapshot), + $value, + $debug_expr + ) + }; ($name:expr, $value:expr) => { - $crate::assert_snapshot_matches!($name, $value, stringify!($value)) + $crate::_assert_snapshot_matches!( + $crate::_macro_support::ReferenceValue::Named($name), + $value, + stringify!($value) + ) }; ($name:expr, $value:expr, $debug_expr:expr) => { + $crate::_assert_snapshot_matches!( + $crate::_macro_support::ReferenceValue::Named($name), + $value, + $debug_expr + ) + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! _assert_snapshot_matches { + ($refval:expr, $value:expr, $debug_expr:expr) => { match &$value { value => { $crate::_macro_support::assert_snapshot( - &$name, + $refval, value, env!("CARGO_MANIFEST_DIR"), module_path!(), diff --git a/src/runtime.rs b/src/runtime.rs index ebb4106e..c906c813 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -8,7 +8,7 @@ use std::process::{Command, Stdio}; use std::sync::Mutex; use chrono::Utc; -use console::{style, Color}; +use console::style; use difference::{Changeset, Difference}; use failure::Error; use lazy_static::lazy_static; @@ -17,6 +17,8 @@ use ci_info::is_ci; use serde::Deserialize; use serde_json; +use crate::snapshot::{MetaData, PendingInlineSnapshot, Snapshot}; + lazy_static! { static ref WORKSPACES: Mutex> = Mutex::new(BTreeMap::new()); } @@ -133,7 +135,7 @@ fn get_cargo_workspace(manifest_dir: &str) -> &Path { } } -fn print_changeset_diff(changeset: &Changeset, expr: Option<&str>) { +fn print_changeset(changeset: &Changeset, expr: Option<&str>) { let Changeset { ref diffs, .. } = *changeset; #[derive(PartialEq)] enum Mode { @@ -214,218 +216,91 @@ fn print_changeset_diff(changeset: &Changeset, expr: Option<&str>) { } pub fn get_snapshot_filename( - name: &str, + module_name: &str, + snapshot_name: &str, cargo_workspace: &Path, - module_path: &str, base: &str, ) -> PathBuf { let root = Path::new(cargo_workspace); let base = Path::new(base); root.join(base.parent().unwrap()) .join("snapshots") - .join(format!( - "{}__{}.snap", - module_path.rsplit("::").next().unwrap(), - name - )) + .join(format!("{}__{}.snap", module_name, snapshot_name)) } -/// A helper to work with stored snapshots. -#[derive(Debug)] -pub struct Snapshot { - path: PathBuf, - metadata: BTreeMap, - snapshot: String, -} - -impl Snapshot { - /// Loads a snapshot from a file. - pub fn from_file>(p: P) -> Result { - let mut f = BufReader::new(fs::File::open(p.as_ref())?); - let mut buf = String::new(); - - f.read_line(&mut buf)?; - - // yaml format - let metadata = if buf.trim_end() == "---" { - loop { - let read = f.read_line(&mut buf)?; - if read == 0 { - break; - } - if buf[buf.len() - read..].trim_end() == "---" { - buf.truncate(buf.len() - read); - break; - } - } - serde_yaml::from_str(&buf)? - // legacy format - } else { - let mut rv = BTreeMap::new(); - loop { - buf.clear(); - let read = f.read_line(&mut buf)?; - if read == 0 || buf.trim_end().is_empty() { - buf.truncate(buf.len() - read); - break; - } - let mut iter = buf.splitn(2, ':'); - if let Some(key) = iter.next() { - if let Some(value) = iter.next() { - rv.insert(key.to_lowercase(), value.to_string()); - } - } - } - rv - }; - - buf.clear(); - for (idx, line) in f.lines().enumerate() { - let line = line?; - if idx > 0 { - buf.push('\n'); - } - buf.push_str(&line); - } - - Ok(Snapshot { - path: p.as_ref().to_path_buf(), - metadata, - snapshot: buf, - }) - } - - /// The path of the snapshot - pub fn path(&self) -> &Path { - &self.path - } - - /// Relative path to the workspace root. - pub fn relative_path(&self, root: &Path) -> &Path { - self.path.strip_prefix(root).ok().unwrap_or(&self.path) - } - - /// Returns the module name. - pub fn module_name(&self) -> &str { - self.path - .file_name() - .unwrap() - .to_str() - .unwrap_or("") - .split("__") - .next() - .unwrap() - } - - /// Returns the snapshot name. - pub fn snapshot_name(&self) -> &str { - self.path - .file_name() - .unwrap() - .to_str() - .unwrap_or("") - .split('.') - .next() - .unwrap_or("") - .splitn(2, "__") - .nth(1) - .unwrap_or("unknown") - } - - /// The metadata in the snapshot. - pub fn metadata(&self) -> &BTreeMap { - &self.metadata - } - - /// The snapshot contents - pub fn contents(&self) -> &str { - &self.snapshot - } - - /// Prints a diff against an old snapshot. - pub fn print_changes(&self, old_snapshot: Option<&Snapshot>) { - if let Some(value) = self.metadata.get("source") { - println!("Source: {}", style(value).cyan()); - } - if let Some(value) = self.metadata.get("created") { - println!("New: {}", style(value).cyan()); - } - let changeset = Changeset::new( - old_snapshot.as_ref().map_or("", |x| x.contents()), - &self.snapshot, - "\n", - ); - if let Some(old_snapshot) = old_snapshot { - if let Some(value) = old_snapshot.metadata.get("created") { - println!("Old: {}", style(value).cyan()); +/// Prints a diff against an old snapshot. +pub fn print_snapshot_diff( + workspace_root: &Path, + new: &Snapshot, + old_snapshot: Option<&Snapshot>, + line: Option, +) { + if let Some(ref value) = new.metadata().get_relative_source(workspace_root) { + println!( + "Source: {}{}", + style(value.display()).cyan(), + if let Some(line) = line { + format!(":{}", style(line).bold()) + } else { + "".to_string() } - println!(); - println!("{}", style("-old snapshot").red()); - println!("{}", style("+new results").green()); - } else { - println!("Old: {}", style("n.a.").red()); - println!(); - println!("{}", style("+new results").green()); - } - print_changeset_diff( - &changeset, - self.metadata.get("expression").map(|x| x.as_str()), ); } - - fn save(&self) -> Result<(), Error> { - self.save_impl(&self.path) + if let Some(ref value) = new.metadata().created { + println!("New: {}", style(value.to_rfc3339()).cyan()); } - - fn save_new(&self) -> Result { - let mut path = self.path.to_path_buf(); - path.set_extension("snap.new"); - self.save_impl(&path)?; - Ok(path) - } - - fn save_impl(&self, path: &Path) -> Result<(), Error> { - if let Some(folder) = path.parent() { - fs::create_dir_all(&folder)?; + let changeset = Changeset::new( + old_snapshot.as_ref().map_or("", |x| x.contents()), + &new.contents(), + "\n", + ); + if let Some(old_snapshot) = old_snapshot { + if let Some(ref value) = old_snapshot.metadata().created { + println!("Old: {}", style(value.to_rfc3339()).cyan()); } - let mut f = fs::File::create(&path)?; - serde_yaml::to_writer(&mut f, &self.metadata)?; - f.write_all(b"\n---\n")?; - f.write_all(self.snapshot.as_bytes())?; - f.write_all(b"\n")?; - Ok(()) + println!(); + println!("{}", style("-old snapshot").red()); + println!("{}", style("+new results").green()); + } else { + println!("Old: {}", style("n.a.").red()); + println!(); + println!("{}", style("+new results").green()); } + print_changeset( + &changeset, + new.metadata().expression.as_ref().map(|x| x.as_str()), + ); } -fn print_snapshot_diff( - cargo_workspace: &Path, - name: &str, - old_snapshot: Option<&Snapshot>, +fn print_snapshot_diff_with_title( + workspace_root: &Path, new_snapshot: &Snapshot, + old_snapshot: Option<&Snapshot>, + line: u32, ) { let width = console::Term::stdout().size().1 as usize; - let file = style(new_snapshot.relative_path(&cargo_workspace).display()) - .underlined() - .fg(if fs::metadata(&new_snapshot.path).is_ok() { - Color::Cyan - } else { - Color::Red - }); - println!( - "{title:━^width$}\nFile: {file}\nSnapshot: {name}", - name = style(name).yellow(), - file = file, + "{title:━^width$}", title = style(" Snapshot Differences ").bold(), width = width ); - new_snapshot.print_changes(old_snapshot); + if let Some(name) = new_snapshot.snapshot_name() { + println!("Snapshot: {}", style(name).yellow()); + } + + print_snapshot_diff(workspace_root, new_snapshot, old_snapshot, Some(line)); } +pub enum ReferenceValue<'a> { + Named(&'a str), + Inline(&'a str), +} + +#[allow(clippy::too_many_arguments)] pub fn assert_snapshot( - name: &str, + refval: ReferenceValue<'_>, new_snapshot: &str, manifest_dir: &str, module_path: &str, @@ -433,30 +308,70 @@ pub fn assert_snapshot( line: u32, expr: &str, ) -> Result<(), Error> { + let module_name = module_path.rsplit("::").next().unwrap(); let cargo_workspace = get_cargo_workspace(manifest_dir); - let snapshot_file = get_snapshot_filename(name, &cargo_workspace, module_path, file); - let old = Snapshot::from_file(&snapshot_file).ok(); + + let (snapshot_name, snapshot_file, old, pending_snapshots) = match refval { + ReferenceValue::Named(snapshot_name) => { + let snapshot_file = + get_snapshot_filename(module_name, 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 mut filename = cargo_workspace.join(file); + let created = fs::metadata(&filename)?.created()?.into(); + filename.set_file_name(format!( + ".{}.pending-snap", + filename + .file_name() + .expect("no filename") + .to_str() + .expect("non unicode filename") + )); + ( + None, + None, + Some(Snapshot::from_components( + module_name.to_string(), + None, + MetaData { + created: Some(created), + ..MetaData::default() + }, + contents.to_string(), + )), + Some(filename), + ) + } + }; // if the snapshot matches we're done. - if old.as_ref().map_or(false, |x| x.snapshot == new_snapshot) { + if old.as_ref().map_or(false, |x| x.contents() == new_snapshot) { return Ok(()); } - let mut metadata = BTreeMap::new(); - metadata.insert("created".to_string(), Utc::now().to_rfc3339()); - metadata.insert( - "creator".to_string(), - format!("{}@{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")), + let new = Snapshot::from_components( + module_name.to_string(), + snapshot_name.map(|x| x.to_string()), + MetaData { + created: Some(Utc::now()), + creator: Some(format!( + "{}@{}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + )), + source: Some(path_to_storage(file)), + expression: Some(expr.to_string()), + }, + new_snapshot.to_string(), ); - metadata.insert("source".to_string(), path_to_storage(file)); - metadata.insert("expression".to_string(), expr.to_string()); - let new = Snapshot { - path: snapshot_file.to_path_buf(), - metadata, - snapshot: new_snapshot.to_string(), - }; - print_snapshot_diff(cargo_workspace, name, old.as_ref(), &new); + print_snapshot_diff_with_title(cargo_workspace, &new, old.as_ref(), line); println!( "{hint}", hint = style("To update snapshots re-run the tests with INSTA_UPDATE=yes or use `cargo insta review`").dim(), @@ -464,21 +379,36 @@ pub fn assert_snapshot( match update_snapshot_behavior() { UpdateBehavior::InPlace => { - new.save()?; - eprintln!( - " {} {}\n", - style("updated snapshot").green(), - style(snapshot_file.display()).cyan().underlined(), - ); - return Ok(()); + if let Some(ref snapshot_file) = snapshot_file { + new.save(snapshot_file)?; + eprintln!( + " {} {}\n", + style("updated snapshot").green(), + style(snapshot_file.display()).cyan().underlined(), + ); + return Ok(()); + } else { + eprintln!( + " {}", + style("error: cannot update inline snapshots in-place") + .red() + .bold(), + ); + } } UpdateBehavior::NewFile => { - let new_path = new.save_new()?; - eprintln!( - " {} {}\n", - style("stored new snapshot").green(), - style(new_path.display()).cyan().underlined(), - ); + 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)?; + eprintln!( + " {} {}\n", + style("stored new snapshot").green(), + style(new_path.display()).cyan().underlined(), + ); + } else { + PendingInlineSnapshot::new(new, old, line).save(pending_snapshots.unwrap())?; + } } UpdateBehavior::NoUpdate => {} } @@ -487,7 +417,8 @@ pub fn assert_snapshot( assert!( false, "snapshot assertion for '{}' failed in line {}", - name, line + snapshot_name.unwrap_or("inline snapshot"), + line ); } diff --git a/src/snapshot.rs b/src/snapshot.rs new file mode 100644 index 00000000..9d29bb62 --- /dev/null +++ b/src/snapshot.rs @@ -0,0 +1,237 @@ +use std::fs; +use std::io::{BufRead, BufReader, Write}; +use std::path::{Path, PathBuf}; +use uuid::Uuid; + +use chrono::{DateTime, Utc}; +use failure::Error; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use serde_json; + +lazy_static! { + static ref RUN_ID: Uuid = Uuid::new_v4(); +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PendingInlineSnapshot { + pub run_id: Uuid, + pub line: u32, + pub new: Snapshot, + pub old: Option, +} + +impl PendingInlineSnapshot { + pub fn new(new: Snapshot, old: Option, line: u32) -> PendingInlineSnapshot { + PendingInlineSnapshot { + new, + old, + line, + run_id: *RUN_ID, + } + } + + pub fn load_batch>(p: P) -> Result, Error> { + let f = BufReader::new(fs::File::open(p)?); + let iter = serde_json::Deserializer::from_reader(f).into_iter::(); + let mut rv = iter.collect::, _>>()?; + + // remove all but the last run + if let Some(last_run_id) = rv.last().map(|x| x.run_id) { + rv.retain(|x| x.run_id == last_run_id); + } + + Ok(rv) + } + + pub fn save_batch>(p: P, batch: &[PendingInlineSnapshot]) -> Result<(), Error> { + fs::remove_file(&p).ok(); + for snap in batch { + snap.save(&p)?; + } + Ok(()) + } + + pub fn save>(&self, p: P) -> Result<(), Error> { + let mut f = fs::OpenOptions::new().create(true).append(true).open(p)?; + let mut s = serde_json::to_string(self)?; + s.push('\n'); + f.write_all(s.as_bytes())?; + Ok(()) + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct MetaData { + #[serde(skip_serializing_if = "Option::is_none")] + pub created: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub creator: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub expression: Option, +} + +impl MetaData { + pub fn get_relative_source(&self, base: &Path) -> Option { + self.source.as_ref().map(|source| { + base.join(source) + .canonicalize() + .ok() + .and_then(|s| s.strip_prefix(base).ok().map(|x| x.to_path_buf())) + .unwrap_or_else(|| base.to_path_buf()) + }) + } +} + +/// A helper to work with stored snapshots. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Snapshot { + module_name: String, + #[serde(skip_serializing_if = "Option::is_none")] + snapshot_name: Option, + metadata: MetaData, + snapshot: String, +} + +impl Snapshot { + /// Loads a snapshot from a file. + pub fn from_file>(p: P) -> Result { + let mut f = BufReader::new(fs::File::open(p.as_ref())?); + let mut buf = String::new(); + + f.read_line(&mut buf)?; + + // yaml format + let metadata = if buf.trim_end() == "---" { + loop { + let read = f.read_line(&mut buf)?; + if read == 0 { + break; + } + if buf[buf.len() - read..].trim_end() == "---" { + buf.truncate(buf.len() - read); + break; + } + } + serde_yaml::from_str(&buf)? + // legacy format + } else { + let mut rv = MetaData::default(); + loop { + buf.clear(); + let read = f.read_line(&mut buf)?; + if read == 0 || buf.trim_end().is_empty() { + buf.truncate(buf.len() - read); + break; + } + let mut iter = buf.splitn(2, ':'); + if let Some(key) = iter.next() { + if let Some(value) = iter.next() { + let value = value.trim(); + match key.to_lowercase().as_str() { + "created" => { + rv.created = + Some(DateTime::parse_from_rfc3339(value)?.with_timezone(&Utc)) + } + "creator" => rv.creator = Some(value.to_string()), + "expression" => rv.expression = Some(value.to_string()), + "source" => rv.source = Some(value.into()), + _ => {} + } + } + } + } + rv + }; + + buf.clear(); + for (idx, line) in f.lines().enumerate() { + let line = line?; + if idx > 0 { + buf.push('\n'); + } + buf.push_str(&line); + } + + let module_name = p + .as_ref() + .file_name() + .unwrap() + .to_str() + .unwrap_or("") + .split("__") + .next() + .unwrap_or("") + .to_string(); + + let snapshot_name = p + .as_ref() + .file_name() + .unwrap() + .to_str() + .unwrap_or("") + .split('.') + .next() + .unwrap_or("") + .splitn(2, "__") + .nth(1) + .map(|x| x.to_string()); + + Ok(Snapshot::from_components( + module_name, + snapshot_name, + metadata, + buf, + )) + } + + /// Creates an empty snapshot. + pub(crate) fn from_components( + module_name: String, + snapshot_name: Option, + metadata: MetaData, + snapshot: String, + ) -> Snapshot { + Snapshot { + module_name, + snapshot_name, + metadata, + snapshot, + } + } + + /// Returns the module name. + pub fn module_name(&self) -> &str { + &self.module_name + } + + /// Returns the snapshot name. + pub fn snapshot_name(&self) -> Option<&str> { + self.snapshot_name.as_ref().map(|x| x.as_str()) + } + + /// The metadata in the snapshot. + pub fn metadata(&self) -> &MetaData { + &self.metadata + } + + /// The snapshot contents + pub fn contents(&self) -> &str { + &self.snapshot + } + + pub(crate) fn save>(&self, path: P) -> Result<(), Error> { + let path = path.as_ref(); + if let Some(folder) = path.parent() { + fs::create_dir_all(&folder)?; + } + let mut f = fs::File::create(&path)?; + serde_yaml::to_writer(&mut f, &self.metadata)?; + f.write_all(b"\n---\n")?; + f.write_all(self.snapshot.as_bytes())?; + f.write_all(b"\n")?; + Ok(()) + } +} diff --git a/tests/test_inline.rs b/tests/test_inline.rs new file mode 100644 index 00000000..3cf992fd --- /dev/null +++ b/tests/test_inline.rs @@ -0,0 +1,45 @@ +use insta::{assert_debug_snapshot_matches, assert_ron_snapshot_matches}; +use serde::Serialize; + +#[test] +fn test_simple() { + assert_debug_snapshot_matches!(vec![1, 2, 3], @r###"[ + 1, + 2, + 3 +]"###); +} + +#[test] +fn test_complex() { + assert_debug_snapshot_matches!(vec![1, 2, 3, 4, 5], @r###"[ + 1, + 2, + 3, + 4, + 5 +]"###); +} + +#[test] +fn test_ron_inline() { + #[derive(Serialize)] + pub struct Email(String); + + #[derive(Serialize)] + pub struct User { + id: u32, + username: String, + email: Email, + } + + assert_ron_snapshot_matches!(User { + id: 42, + username: "peter-doe".into(), + email: Email("peter@doe.invalid".into()), + }, @r###"User( + id: 42, + username: "peter-doe", + email: Email("peter@doe.invalid"), +)"###); +} From e3a27c15d9b0c787608e1f0a52ea76c3eda4990c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 28 Jan 2019 20:22:53 +0100 Subject: [PATCH 02/18] Handle poison errors and platforms without created timestamp --- src/runtime.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/runtime.rs b/src/runtime.rs index 44fe8574..a57b4ac7 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -114,7 +114,8 @@ fn get_cargo() -> String { } fn get_cargo_workspace(manifest_dir: &str) -> &Path { - let mut workspaces = WORKSPACES.lock().unwrap(); + // we really do not care about locking here. + let mut workspaces = WORKSPACES.lock().unwrap_or_else(|x| x.into_inner()); if let Some(rv) = workspaces.get(manifest_dir) { rv } else { @@ -324,7 +325,7 @@ pub fn assert_snapshot( } ReferenceValue::Inline(contents) => { let mut filename = cargo_workspace.join(file); - let created = fs::metadata(&filename)?.created()?.into(); + let created = fs::metadata(&filename)?.created().ok().map(|x| x.into()); filename.set_file_name(format!( ".{}.pending-snap", filename @@ -340,7 +341,7 @@ pub fn assert_snapshot( module_name.to_string(), None, MetaData { - created: Some(created), + created: created, ..MetaData::default() }, contents.to_string(), From de0b78382716fc5dcac1eb4ff9fe886a7a7830b8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 28 Jan 2019 21:47:37 +0100 Subject: [PATCH 03/18] Switch to dtolnay's branch for proc-macro2 --- Cargo.toml | 2 +- cargo-insta/Cargo.toml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3ca5e92c..73a95d66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,4 +39,4 @@ ron = "0.4.1" uuid = { version = "0.7.1", features = ["serde", "v4"] } [patch.crates-io] -proc-macro2 = { git = "https://github.com/mitsuhiko/proc-macro2", rev = "e09dfd48f2ac12a38898aa7812bd06935298d23f" } +proc-macro2 = { git = "https://github.com/dtolnay/proc-macro2", rev = "383649a8fa8754adf9da2a4dd8473b494ccfd2df" } diff --git a/cargo-insta/Cargo.toml b/cargo-insta/Cargo.toml index ec639b62..2dee9c30 100644 --- a/cargo-insta/Cargo.toml +++ b/cargo-insta/Cargo.toml @@ -21,6 +21,5 @@ serde_json = "1.0.36" failure = "0.1.5" glob = "0.2.11" walkdir = "2.2.7" -proc-macro2 = { version = "0.4.26", features = ["span-location-info"] } +proc-macro2 = { version = "0.4.26", features = ["span-locations"] } syn = { version = "0.15.26", features = ["full"] } -quote = { version = "0.6.11" } From e0d76d672f24e1911ee8c9c20738e4659375feba Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 28 Jan 2019 22:57:06 +0100 Subject: [PATCH 04/18] Bumped to newer proc macro and added json test --- Cargo.toml | 2 +- cargo-insta/src/inline.rs | 11 +---------- tests/test_inline.rs | 12 +++++++++++- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 73a95d66..91d04013 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,4 +39,4 @@ ron = "0.4.1" uuid = { version = "0.7.1", features = ["serde", "v4"] } [patch.crates-io] -proc-macro2 = { git = "https://github.com/dtolnay/proc-macro2", rev = "383649a8fa8754adf9da2a4dd8473b494ccfd2df" } +proc-macro2 = { git = "https://github.com/dtolnay/proc-macro2", rev = "3b1f7d28c11a0f88ab52504c4b8e1dc9e7789883" } diff --git a/cargo-insta/src/inline.rs b/cargo-insta/src/inline.rs index ce0341e7..c752c07f 100644 --- a/cargo-insta/src/inline.rs +++ b/cargo-insta/src/inline.rs @@ -158,13 +158,4 @@ impl FilePatcher { syn::visit::visit_file(&mut visitor, &self.source); visitor.1 } -} - -#[test] -fn test_patcher() { - let mut patcher = FilePatcher::open("tests/test_inline.rs").unwrap(); - patcher.set_new_content(0, "this is the new content\nsecond line\nMOAR!"); - patcher.set_new_content(1, "almost empty"); - println!("{:#?}", &patcher.lines); - panic!(); -} +} \ No newline at end of file diff --git a/tests/test_inline.rs b/tests/test_inline.rs index 3cf992fd..9614ea78 100644 --- a/tests/test_inline.rs +++ b/tests/test_inline.rs @@ -1,4 +1,6 @@ -use insta::{assert_debug_snapshot_matches, assert_ron_snapshot_matches}; +use insta::{ + assert_debug_snapshot_matches, assert_json_snapshot_matches, assert_ron_snapshot_matches, +}; use serde::Serialize; #[test] @@ -43,3 +45,11 @@ fn test_ron_inline() { email: Email("peter@doe.invalid"), )"###); } + +#[test] +fn test_json_inline() { + assert_json_snapshot_matches!(vec!["foo", "bar"], @r###"[ + "foo", + "bar" +]"###); +} From 680dae3b8efd7f45dc27576fe98f1c1f4c6583c7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 28 Jan 2019 23:21:54 +0100 Subject: [PATCH 05/18] Fixed a serialization macro bug --- src/macros.rs | 2 +- tests/test_inline.rs | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/macros.rs b/src/macros.rs index bbd6c6f7..b81a57eb 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -134,7 +134,7 @@ macro_rules! _assert_serialized_snapshot_matches { &vec, $crate::_macro_support::SerializationFormat::$format ); - $crate::assert_snapshot_matches!(value, stringify!($value), @snapshot); + $crate::assert_snapshot_matches!(value, stringify!($value), @$snapshot); }}; ($name:expr, $value:expr, $format:ident) => {{ let value = $crate::_macro_support::serialize_value( diff --git a/tests/test_inline.rs b/tests/test_inline.rs index 9614ea78..32eefcd6 100644 --- a/tests/test_inline.rs +++ b/tests/test_inline.rs @@ -1,5 +1,6 @@ use insta::{ assert_debug_snapshot_matches, assert_json_snapshot_matches, assert_ron_snapshot_matches, + assert_serialized_snapshot_matches, }; use serde::Serialize; @@ -53,3 +54,23 @@ fn test_json_inline() { "bar" ]"###); } + +#[test] +fn test_serialize_inline_redacted() { + #[derive(Serialize)] + pub struct User { + id: u32, + username: String, + email: String, + } + + assert_serialized_snapshot_matches!(User { + id: 42, + username: "peter-pan".into(), + email: "peterpan@wonderland.invalid".into() + }, { + ".id" => "[user-id]" + }, @r###"id: "[user-id]" +username: peter-pan +email: peterpan@wonderland.invalid"###); +} From f2bb4d1d4af54d276ed96467995cee8e0a711296 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 28 Jan 2019 23:34:37 +0100 Subject: [PATCH 06/18] Documentation update --- src/lib.rs | 34 +++++++++++++++++++++++++++++++--- src/macros.rs | 28 +++++++++++++++++++++++++--- src/snapshot.rs | 5 +++++ tests/test_inline.rs | 22 +++++++++++++++++++++- 4 files changed, 82 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9442d1b1..cc52d6e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ //! //! - `assert_snapshot_matches!` for comparing basic string snapshots. //! - `assert_debug_snapshot_matches!` for comparing `Debug` outputs of values. -//! - `assert_serialized_snapshot_matches!` for comparing YAML serialized +//! - `assert_yaml_snapshot_matches!` for comparing YAML serialized //! output of types implementing `serde::Serialize`. //! - `assert_ron_snapshot_matches!` for comparing RON serialized output of //! types implementing `serde::Serialize`. @@ -20,6 +20,10 @@ //! where this is used. The name of the file is `__.snap` where //! the `name` of the snapshot has to be provided to the assertion macro. //! +//! Additionally snapshots can also be stored inline. In that case the +//! `cargo-insta` tool is necessary. See [inline snapshots](#inline-snapshots) +//! for more information. +//! //! For macros that work with `serde::Serialize` this crate also permits //! redacting of partial values. See [redactions](#redactions) for more //! information. @@ -149,13 +153,37 @@ //! username: String, //! } //! -//! assert_serialized_snapshot_matches!("user", &User { +//! assert_yaml_snapshot_matches!("user", &User { //! id: Uuid::new_v4(), //! username: "john_doe".to_string(), //! }, { //! ".id" => "[uuid]" //! }); //! ``` +//! +//! # Inline Snapshots +//! +//! Additionally snapshots can also be stored inline. In that case the format +//! for the snapshot macros is `assert_snapshot_matches!(reference_value, @"snapshot")`. +//! The leading at sign (`@`) indicates that the following string is the +//! reference value. `cargo-insta` will then update that string with the new +//! value on review. +//! +//! Example: +//! +//! ```rust,ignore +//! #[derive(Serialize)] +//! pub struct User { +//! username: String, +//! } +//! +//! assert_yaml_snapshot_matches!(User { +//! username: "john_doe".to_string(), +//! }, @""); +//! ``` +//! +//! After the initial test failure you can run `cargo insta review` to +//! accept the change. The file will then be updated automatically. #[macro_use] mod macros; mod content; @@ -167,7 +195,7 @@ mod snapshot; #[cfg(test)] mod test; -pub use crate::snapshot::Snapshot; +pub use crate::snapshot::{MetaData, Snapshot}; // exported for cargo-insta only #[doc(hidden)] diff --git a/src/macros.rs b/src/macros.rs index b81a57eb..fab18189 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,3 +1,20 @@ +/// Alias for `assert_yaml_snapshot_matches`. +#[macro_export] +macro_rules! assert_serialized_snapshot_matches { + ($value:expr, @$snapshot:literal) => {{ + $crate::_assert_serialized_snapshot_matches!($value, Yaml, @$snapshot); + }}; + ($value:expr, {$($k:expr => $v:expr),*}, @$snapshot:literal) => {{ + $crate::_assert_serialized_snapshot_matches!($value, {$($k => $v),*}, Yaml, @$snapshot); + }}; + ($name:expr, $value:expr) => {{ + $crate::_assert_serialized_snapshot_matches!($name, $value, Yaml); + }}; + ($name:expr, $value:expr, {$($k:expr => $v:expr),*}) => {{ + $crate::_assert_serialized_snapshot_matches!($name, $value, {$($k => $v),*}, Yaml); + }} +} + /// Assets a `Serialize` snapshot in YAML format. /// /// The value needs to implement the `serde::Serialize` trait and the snapshot @@ -10,7 +27,7 @@ /// Example: /// /// ```no_run,ignore -/// assert_serialized_snapshot_matches!("snapshot_name", vec[1, 2, 3]); +/// assert_yaml_snapshot_matches!("snapshot_name", vec[1, 2, 3]); /// ``` /// /// Unlike the `assert_debug_snapshot_matches` macro, this one has a secondary @@ -23,15 +40,20 @@ /// Example: /// /// ```no_run,ignore -/// assert_serialized_snapshot_matches!("name", value, { +/// assert_yaml_snapshot_matches!("name", value, { /// ".key.to.redact" => "[replacement value]", /// ".another.key.*.to.redact" => 42 /// }); /// ``` /// /// The replacement value can be a string, integer or any other primitive value. +/// +/// For inline usage the format is `(expression, @reference_value)` where the +/// reference value must be a string literal. If you make the initial snapshot +/// just use an empty string (`@""`). For more information see +/// [inline snapshots](index.html#inline-snapshots). #[macro_export] -macro_rules! assert_serialized_snapshot_matches { +macro_rules! assert_yaml_snapshot_matches { ($value:expr, @$snapshot:literal) => {{ $crate::_assert_serialized_snapshot_matches!($value, Yaml, @$snapshot); }}; diff --git a/src/snapshot.rs b/src/snapshot.rs index 9d29bb62..6ec9a915 100644 --- a/src/snapshot.rs +++ b/src/snapshot.rs @@ -61,14 +61,19 @@ impl PendingInlineSnapshot { } } +/// Snapshot metadata information. #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct MetaData { + /// The timestamp of when the snapshot was created. #[serde(skip_serializing_if = "Option::is_none")] pub created: Option>, + /// The creator of the snapshot. #[serde(skip_serializing_if = "Option::is_none")] pub creator: Option, + /// The source file (relative to workspace root). #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, + /// Optionally the expression that created the snapshot. #[serde(skip_serializing_if = "Option::is_none")] pub expression: Option, } diff --git a/tests/test_inline.rs b/tests/test_inline.rs index 32eefcd6..bc26c222 100644 --- a/tests/test_inline.rs +++ b/tests/test_inline.rs @@ -1,6 +1,6 @@ use insta::{ assert_debug_snapshot_matches, assert_json_snapshot_matches, assert_ron_snapshot_matches, - assert_serialized_snapshot_matches, + assert_serialized_snapshot_matches, assert_yaml_snapshot_matches, }; use serde::Serialize; @@ -74,3 +74,23 @@ fn test_serialize_inline_redacted() { username: peter-pan email: peterpan@wonderland.invalid"###); } + +#[test] +fn test_yaml_inline_redacted() { + #[derive(Serialize)] + pub struct User { + id: u32, + username: String, + email: String, + } + + assert_yaml_snapshot_matches!(User { + id: 42, + username: "peter-pan".into(), + email: "peterpan@wonderland.invalid".into() + }, { + ".id" => "[user-id]" + }, @r###"id: "[user-id]" +username: peter-pan +email: peterpan@wonderland.invalid"###); +} From c0cad1acd7396c789eab90495c093d9292e0c13c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 28 Jan 2019 23:35:07 +0100 Subject: [PATCH 07/18] Readme update --- README.md | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3f61c2fe..e86eef7b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This crate exports multiple macros for snapshot testing: - `assert_snapshot_matches!` for comparing basic string snapshots. - `assert_debug_snapshot_matches!` for comparing `Debug` outputs of values. -- `assert_serialized_snapshot_matches!` for comparing YAML serialized +- `assert_yaml_snapshot_matches!` for comparing YAML serialized output of types implementing `serde::Serialize`. - `assert_ron_snapshot_matches!` for comparing RON serialized output of types implementing `serde::Serialize`. @@ -22,6 +22,10 @@ Snapshots are stored in the `snapshots` folder right next to the test file where this is used. The name of the file is `__.snap` where the `name` of the snapshot has to be provided to the assertion macro. +Additionally snapshots can also be stored inline. In that case the +`cargo-insta` tool is necessary. See [inline snapshots](#inline-snapshots) +for more information. + For macros that work with `serde::Serialize` this crate also permits redacting of partial values. See [redactions](#redactions) for more information. @@ -151,7 +155,7 @@ pub struct User { username: String, } -assert_serialized_snapshot_matches!("user", &User { +assert_yaml_snapshot_matches!("user", &User { id: Uuid::new_v4(), username: "john_doe".to_string(), }, { @@ -159,4 +163,28 @@ assert_serialized_snapshot_matches!("user", &User { }); ``` +## Inline Snapshots + +Additionally snapshots can also be stored inline. In that case the format +for the snapshot macros is `assert_snapshot_matches!(reference_value, @"snapshot")`. +The leading at sign (`@`) indicates that the following string is the +reference value. `cargo-insta` will then update that string with the new +value on review. + +Example: + +```rust +#[derive(Serialize)] +pub struct User { + username: String, +} + +assert_yaml_snapshot_matches!(User { + username: "john_doe".to_string(), +}, @""); +``` + +After the initial test failure you can run `cargo insta review` to +accept the change. The file will then be updated automatically. + License: Apache-2.0 From 5048aadb3b62983f6dc344f8462a9f60482ea877 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 29 Jan 2019 00:14:49 +0100 Subject: [PATCH 08/18] Improved test coverage and review tool output --- cargo-insta/src/cargo.rs | 26 ++++++++---- cargo-insta/src/cli.rs | 15 +++---- src/runtime.rs | 44 ++++++++++++++++---- tests/snapshots/test_basic__yaml_vector.snap | 9 ++++ tests/test_basic.rs | 9 +++- tests/test_inline.rs | 17 +++----- 6 files changed, 84 insertions(+), 36 deletions(-) create mode 100644 tests/snapshots/test_basic__yaml_vector.snap diff --git a/cargo-insta/src/cargo.rs b/cargo-insta/src/cargo.rs index 3c32b88c..adbf648f 100644 --- a/cargo-insta/src/cargo.rs +++ b/cargo-insta/src/cargo.rs @@ -66,16 +66,19 @@ pub struct PendingSnapshot { } impl PendingSnapshot { - pub fn id(&self) -> String { + pub fn summary(&self) -> String { + use std::fmt::Write; + let mut rv = String::new(); + if let Some(ref source) = self.new.metadata().source { + write!(&mut rv, "{}", source).unwrap(); + } + if let Some(line) = self.line { + write!(&mut rv, ":{}", line).unwrap(); + } if let Some(name) = self.new.snapshot_name() { - format!("{}#{}", self.new.module_name(), name) - } else { - format!( - "{}#inline+{}", - self.new.module_name(), - self.line.unwrap_or(0) - ) + write!(&mut rv, " ({})", name).unwrap(); } + rv } } @@ -138,6 +141,13 @@ impl SnapshotContainer { }) } + pub fn snapshot_file(&self) -> Option<&Path> { + match self.kind { + SnapshotContainerKind::External => Some(&self.target_path), + SnapshotContainerKind::Inline => None, + } + } + pub fn len(&self) -> usize { self.snapshots.len() } diff --git a/cargo-insta/src/cli.rs b/cargo-insta/src/cli.rs index 954e4333..1e0f1a89 100644 --- a/cargo-insta/src/cli.rs +++ b/cargo-insta/src/cli.rs @@ -62,6 +62,7 @@ pub struct ProcessCommand { pub pkg_args: PackageArgs, } +#[allow(clippy::too_many_arguments)] fn query_snapshot( workspace_root: &Path, term: &Term, @@ -71,6 +72,7 @@ fn query_snapshot( line: Option, i: usize, n: usize, + snapshot_file: Option<&Path>, ) -> Result { term.clear_screen()?; println!( @@ -82,10 +84,7 @@ fn query_snapshot( pkg.version() ); - if let Some(snapshot_name) = new.snapshot_name() { - println!("Snapshot: {}", style(snapshot_name).yellow()); - } - print_snapshot_diff(workspace_root, new, old, line); + print_snapshot_diff(workspace_root, new, old, snapshot_file, line); println!(""); println!( @@ -152,6 +151,7 @@ fn process_snapshots(cmd: &ProcessCommand, op: Option) -> Result<(), let mut num = 0; for (snapshot_container, package) in snapshot_containers.iter_mut() { + let snapshot_file = snapshot_container.snapshot_file().map(|x| x.to_path_buf()); for snapshot_ref in snapshot_container.iter_snapshots() { num += 1; let op = match op { @@ -165,19 +165,20 @@ fn process_snapshots(cmd: &ProcessCommand, op: Option) -> Result<(), snapshot_ref.line, num, snapshot_count, + snapshot_file.as_ref().map(|x| x.as_path()), )?, }; match op { Operation::Accept => { snapshot_ref.op = Operation::Accept; - accepted.push(snapshot_ref.id()); + accepted.push(snapshot_ref.summary()); } Operation::Reject => { snapshot_ref.op = Operation::Reject; - rejected.push(snapshot_ref.id()); + rejected.push(snapshot_ref.summary()); } Operation::Skip => { - skipped.push(snapshot_ref.id()); + skipped.push(snapshot_ref.summary()); } } } diff --git a/src/runtime.rs b/src/runtime.rs index a57b4ac7..914db41b 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -234,8 +234,27 @@ pub fn print_snapshot_diff( workspace_root: &Path, new: &Snapshot, old_snapshot: Option<&Snapshot>, + snapshot_file: Option<&Path>, line: Option, ) { + 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) = new.snapshot_name() { + println!("Snapshot: {}", style(name).yellow()); + } else { + println!("Snapshot: {}", style("").dim()); + } + if let Some(ref value) = new.metadata().get_relative_source(workspace_root) { println!( "Source: {}{}", @@ -278,20 +297,21 @@ fn print_snapshot_diff_with_title( 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 ); - - if let Some(name) = new_snapshot.snapshot_name() { - println!("Snapshot: {}", style(name).yellow()); - } - - print_snapshot_diff(workspace_root, new_snapshot, old_snapshot, Some(line)); + print_snapshot_diff( + workspace_root, + new_snapshot, + old_snapshot, + snapshot_file, + Some(line), + ); } pub enum ReferenceValue<'a> { @@ -372,10 +392,16 @@ pub fn assert_snapshot( new_snapshot.to_string(), ); - print_snapshot_diff_with_title(cargo_workspace, &new, old.as_ref(), line); + print_snapshot_diff_with_title( + cargo_workspace, + &new, + old.as_ref(), + line, + snapshot_file.as_ref().map(|x| x.as_path()), + ); println!( "{hint}", - hint = style("To update snapshots re-run the tests with INSTA_UPDATE=yes or use `cargo insta review`").dim(), + hint = style("To update snapshots run `cargo insta review`").dim(), ); match update_snapshot_behavior() { diff --git a/tests/snapshots/test_basic__yaml_vector.snap b/tests/snapshots/test_basic__yaml_vector.snap new file mode 100644 index 00000000..609ce70d --- /dev/null +++ b/tests/snapshots/test_basic__yaml_vector.snap @@ -0,0 +1,9 @@ +--- +created: "2019-01-28T23:09:07.547790Z" +creator: insta@0.6.0 +source: tests/test_basic.rs +expression: "vec![1 , 2 , 3]" +--- +- 1 +- 2 +- 3 diff --git a/tests/test_basic.rs b/tests/test_basic.rs index 52ef39c1..add3a4a7 100644 --- a/tests/test_basic.rs +++ b/tests/test_basic.rs @@ -1,6 +1,8 @@ extern crate insta; -use insta::{assert_debug_snapshot_matches, assert_serialized_snapshot_matches}; +use insta::{ + assert_debug_snapshot_matches, assert_serialized_snapshot_matches, assert_yaml_snapshot_matches, +}; #[test] fn test_vector() { @@ -11,3 +13,8 @@ fn test_vector() { fn test_serialized_vector() { assert_serialized_snapshot_matches!("serialized_vector", vec![1, 2, 3]); } + +#[test] +fn test_yaml_vector() { + assert_yaml_snapshot_matches!("yaml_vector", vec![1, 2, 3]); +} diff --git a/tests/test_inline.rs b/tests/test_inline.rs index bc26c222..442471bc 100644 --- a/tests/test_inline.rs +++ b/tests/test_inline.rs @@ -1,27 +1,22 @@ use insta::{ assert_debug_snapshot_matches, assert_json_snapshot_matches, assert_ron_snapshot_matches, - assert_serialized_snapshot_matches, assert_yaml_snapshot_matches, + assert_serialized_snapshot_matches, assert_snapshot_matches, assert_yaml_snapshot_matches, }; use serde::Serialize; #[test] fn test_simple() { - assert_debug_snapshot_matches!(vec![1, 2, 3], @r###"[ + assert_debug_snapshot_matches!(vec![1, 2, 3, 4], @r###"[ 1, 2, - 3 + 3, + 4 ]"###); } #[test] -fn test_complex() { - assert_debug_snapshot_matches!(vec![1, 2, 3, 4, 5], @r###"[ - 1, - 2, - 3, - 4, - 5 -]"###); +fn test_single_line() { + assert_snapshot_matches!("Testing", @"Testing"); } #[test] From c290d9e18a5cf704f673a3122b7c7118a7460fe9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 29 Jan 2019 00:41:43 +0100 Subject: [PATCH 09/18] Format timestamps in local timezone --- src/runtime.rs | 22 ++++++++++++++----- .../test_basic__yaml_vector.snap.new | 11 ++++++++++ tests/test_basic.rs | 8 ++++++- 3 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 tests/snapshots/test_basic__yaml_vector.snap.new diff --git a/src/runtime.rs b/src/runtime.rs index 914db41b..f3af93a2 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -7,7 +7,7 @@ use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::sync::Mutex; -use chrono::Utc; +use chrono::{Local, Utc}; use console::style; use difference::{Changeset, Difference}; use failure::Error; @@ -266,9 +266,6 @@ pub fn print_snapshot_diff( } ); } - if let Some(ref value) = new.metadata().created { - println!("New: {}", style(value.to_rfc3339()).cyan()); - } let changeset = Changeset::new( old_snapshot.as_ref().map_or("", |x| x.contents()), &new.contents(), @@ -276,13 +273,28 @@ pub fn print_snapshot_diff( ); if let Some(old_snapshot) = old_snapshot { if let Some(ref value) = old_snapshot.metadata().created { - println!("Old: {}", style(value.to_rfc3339()).cyan()); + println!( + "Old: {}", + style(value.with_timezone(&Local).to_rfc2822()).cyan() + ); + } + if let Some(ref value) = new.metadata().created { + println!( + "New: {}", + style(value.with_timezone(&Local).to_rfc2822()).cyan() + ); } println!(); println!("{}", style("-old snapshot").red()); println!("{}", style("+new results").green()); } else { println!("Old: {}", style("n.a.").red()); + if let Some(ref value) = new.metadata().created { + println!( + "New: {}", + style(value.with_timezone(&Local).to_rfc2822()).cyan() + ); + } println!(); println!("{}", style("+new results").green()); } diff --git a/tests/snapshots/test_basic__yaml_vector.snap.new b/tests/snapshots/test_basic__yaml_vector.snap.new new file mode 100644 index 00000000..c4bac5c6 --- /dev/null +++ b/tests/snapshots/test_basic__yaml_vector.snap.new @@ -0,0 +1,11 @@ +--- +created: "2019-01-28T23:41:01.161964Z" +creator: insta@0.6.0 +source: tests/test_basic.rs +expression: "vec![1 , 2 , 3]" +--- +[ + 1, + 2, + 3 +] diff --git a/tests/test_basic.rs b/tests/test_basic.rs index add3a4a7..b881d9ac 100644 --- a/tests/test_basic.rs +++ b/tests/test_basic.rs @@ -1,7 +1,8 @@ extern crate insta; use insta::{ - assert_debug_snapshot_matches, assert_serialized_snapshot_matches, assert_yaml_snapshot_matches, + assert_debug_snapshot_matches, assert_json_snapshot_matches, + assert_serialized_snapshot_matches, assert_yaml_snapshot_matches, }; #[test] @@ -18,3 +19,8 @@ fn test_serialized_vector() { fn test_yaml_vector() { assert_yaml_snapshot_matches!("yaml_vector", vec![1, 2, 3]); } + +#[test] +fn test_json_vector() { + assert_json_snapshot_matches!("yaml_vector", vec![1, 2, 3]); +} From 90fcad6503b63540c79a5230366dd062773d9203 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 29 Jan 2019 00:42:02 +0100 Subject: [PATCH 10/18] Fixed a broken test --- tests/snapshots/test_basic__yaml_vector.snap | 10 ++++++---- tests/snapshots/test_basic__yaml_vector.snap.new | 11 ----------- 2 files changed, 6 insertions(+), 15 deletions(-) delete mode 100644 tests/snapshots/test_basic__yaml_vector.snap.new diff --git a/tests/snapshots/test_basic__yaml_vector.snap b/tests/snapshots/test_basic__yaml_vector.snap index 609ce70d..c4bac5c6 100644 --- a/tests/snapshots/test_basic__yaml_vector.snap +++ b/tests/snapshots/test_basic__yaml_vector.snap @@ -1,9 +1,11 @@ --- -created: "2019-01-28T23:09:07.547790Z" +created: "2019-01-28T23:41:01.161964Z" creator: insta@0.6.0 source: tests/test_basic.rs expression: "vec![1 , 2 , 3]" --- -- 1 -- 2 -- 3 +[ + 1, + 2, + 3 +] diff --git a/tests/snapshots/test_basic__yaml_vector.snap.new b/tests/snapshots/test_basic__yaml_vector.snap.new deleted file mode 100644 index c4bac5c6..00000000 --- a/tests/snapshots/test_basic__yaml_vector.snap.new +++ /dev/null @@ -1,11 +0,0 @@ ---- -created: "2019-01-28T23:41:01.161964Z" -creator: insta@0.6.0 -source: tests/test_basic.rs -expression: "vec![1 , 2 , 3]" ---- -[ - 1, - 2, - 3 -] From e2b5f0269d1b32ed58a71fd467d1dc9209fb3b89 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 29 Jan 2019 01:10:13 +0100 Subject: [PATCH 11/18] Updated snapshot for yaml --- tests/snapshots/test_basic__yaml_vector.snap | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/snapshots/test_basic__yaml_vector.snap b/tests/snapshots/test_basic__yaml_vector.snap index c4bac5c6..8d3a1812 100644 --- a/tests/snapshots/test_basic__yaml_vector.snap +++ b/tests/snapshots/test_basic__yaml_vector.snap @@ -1,11 +1,9 @@ --- -created: "2019-01-28T23:41:01.161964Z" +created: "2019-01-29T00:09:49.830483Z" creator: insta@0.6.0 source: tests/test_basic.rs expression: "vec![1 , 2 , 3]" --- -[ - 1, - 2, - 3 -] +- 1 +- 2 +- 3 From 8725b7fe28c24f82f7f2b5001d159e19ac839933 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 29 Jan 2019 23:39:02 +0100 Subject: [PATCH 12/18] Resolved some lint issues --- .vscode/settings.json | 1 + cargo-insta/src/cargo.rs | 2 +- cargo-insta/src/cli.rs | 2 +- src/content.rs | 16 ++++++++-------- src/runtime.rs | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 3a71ab9a..cbb7229a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "editor.formatOnSave": true, + "rust.clippy_preference": "on", "rust.cfg_test": true, "rust.all_features": true } \ No newline at end of file diff --git a/cargo-insta/src/cargo.rs b/cargo-insta/src/cargo.rs index adbf648f..a86bd131 100644 --- a/cargo-insta/src/cargo.rs +++ b/cargo-insta/src/cargo.rs @@ -258,7 +258,7 @@ impl Package { old_path, SnapshotContainerKind::External, )) - } else if fname.starts_with(".") && fname.ends_with(".pending-snap") { + } else if fname.starts_with('.') && fname.ends_with(".pending-snap") { let mut target_path = e.path().to_path_buf(); target_path.set_file_name(&fname[1..fname.len() - 13]); Some(SnapshotContainer::load( diff --git a/cargo-insta/src/cli.rs b/cargo-insta/src/cli.rs index 1e0f1a89..3c53df34 100644 --- a/cargo-insta/src/cli.rs +++ b/cargo-insta/src/cli.rs @@ -86,7 +86,7 @@ fn query_snapshot( print_snapshot_diff(workspace_root, new, old, snapshot_file, line); - println!(""); + println!(); println!( " {} accept {}", style("A").green().bold(), diff --git a/src/content.rs b/src/content.rs index 829d58b8..a3af6f3c 100644 --- a/src/content.rs +++ b/src/content.rs @@ -355,7 +355,7 @@ where len: usize, ) -> Result { Ok(SerializeTupleStruct { - name: name, + name, fields: Vec::with_capacity(len), error: PhantomData, }) @@ -369,9 +369,9 @@ where len: usize, ) -> Result { Ok(SerializeTupleVariant { - name: name, - variant_index: variant_index, - variant: variant, + name, + variant_index, + variant, fields: Vec::with_capacity(len), error: PhantomData, }) @@ -387,7 +387,7 @@ where fn serialize_struct(self, name: &'static str, len: usize) -> Result { Ok(SerializeStruct { - name: name, + name, fields: Vec::with_capacity(len), error: PhantomData, }) @@ -401,9 +401,9 @@ where len: usize, ) -> Result { Ok(SerializeStructVariant { - name: name, - variant_index: variant_index, - variant: variant, + name, + variant_index, + variant, fields: Vec::with_capacity(len), error: PhantomData, }) diff --git a/src/runtime.rs b/src/runtime.rs index f3af93a2..b5db356b 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -373,7 +373,7 @@ pub fn assert_snapshot( module_name.to_string(), None, MetaData { - created: created, + created, ..MetaData::default() }, contents.to_string(), From ef80a27edc0cd47f47dc954d9f6faf070224cb46 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 30 Jan 2019 00:35:37 +0100 Subject: [PATCH 13/18] Added cargo insta test command and fixed test --- cargo-insta/src/cargo.rs | 2 +- cargo-insta/src/cli.rs | 94 ++++++++++++++++++-- tests/snapshots/test_basic__json_vector.snap | 11 +++ tests/snapshots/test_basic__yaml_vector.snap | 10 ++- tests/test_basic.rs | 2 +- 5 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 tests/snapshots/test_basic__json_vector.snap diff --git a/cargo-insta/src/cargo.rs b/cargo-insta/src/cargo.rs index a86bd131..0ddcbfac 100644 --- a/cargo-insta/src/cargo.rs +++ b/cargo-insta/src/cargo.rs @@ -274,7 +274,7 @@ impl Package { } } -fn get_cargo() -> String { +pub fn get_cargo() -> String { env::var("CARGO") .ok() .unwrap_or_else(|| "cargo".to_string()) diff --git a/cargo-insta/src/cli.rs b/cargo-insta/src/cli.rs index 3c53df34..955f3297 100644 --- a/cargo-insta/src/cli.rs +++ b/cargo-insta/src/cli.rs @@ -1,5 +1,6 @@ use std::env; use std::path::{Path, PathBuf}; +use std::process; use console::{set_colors_enabled, style, Key, Term}; use failure::{err_msg, Error}; @@ -7,7 +8,7 @@ use insta::{print_snapshot_diff, Snapshot}; use structopt::clap::AppSettings; use structopt::StructOpt; -use crate::cargo::{find_packages, get_package_metadata, Metadata, Operation, Package}; +use crate::cargo::{find_packages, get_cargo, get_package_metadata, Metadata, Operation, Package}; /// A helper utility to work with insta snapshots. #[derive(StructOpt, Debug)] @@ -41,18 +42,21 @@ pub enum Command { /// Accept all snapshots #[structopt(name = "accept")] Accept(ProcessCommand), + /// Run tests and then reviews + #[structopt(name = "test")] + Test(TestCommand), } -#[derive(StructOpt, Debug)] +#[derive(StructOpt, Debug, Clone)] #[structopt(rename_all = "kebab-case")] pub struct PackageArgs { - /// Review all packages - #[structopt(long)] - pub all: bool, - /// Path to Cargo.toml #[structopt(long, value_name = "PATH", parse(from_os_str))] pub manifest_path: Option, + + /// Work on all packages in the workspace + #[structopt(long)] + pub all: bool, } #[derive(StructOpt, Debug)] @@ -62,6 +66,34 @@ pub struct ProcessCommand { pub pkg_args: PackageArgs, } +#[derive(StructOpt, Debug)] +#[structopt(rename_all = "kebab-case")] +pub struct TestCommand { + #[structopt(flatten)] + pub pkg_args: PackageArgs, + /// Package to run tests for + #[structopt(short = "p", long)] + pub package: Option, + /// Disable force-passing of snapshot tests + #[structopt(long)] + pub no_force_pass: bool, + /// Prevent running all tests regardless of failure + #[structopt(long)] + pub fail_fast: bool, + /// Space-separated list of features to activate + #[structopt(long, value_name = "FEATURES")] + pub features: Option, + /// Activate all available features + #[structopt(long)] + pub all_features: bool, + /// Do not activate the `default` feature + #[structopt(long)] + pub no_default_features: bool, + /// Follow up with review. + #[structopt(long)] + pub review: bool, +} + #[allow(clippy::too_many_arguments)] fn query_snapshot( workspace_root: &Path, @@ -212,6 +244,55 @@ fn process_snapshots(cmd: &ProcessCommand, op: Option) -> Result<(), Ok(()) } +fn test_run(cmd: &TestCommand) -> Result<(), Error> { + let mut proc = process::Command::new(get_cargo()); + proc.arg("test"); + if cmd.pkg_args.all { + proc.arg("--all"); + } + if let Some(ref pkg) = cmd.package { + proc.arg("--package"); + proc.arg(pkg); + } + if let Some(ref manifest_path) = cmd.pkg_args.manifest_path { + proc.arg("--manifest-path"); + proc.arg(manifest_path); + } + if !cmd.fail_fast { + proc.arg("--no-fail-fast"); + } + if !cmd.no_force_pass { + proc.env("INSTA_FORCE_PASS", "1"); + } + if let Some(ref features) = cmd.features { + proc.arg("--features"); + proc.arg(features); + } + if cmd.all_features { + proc.arg("--all-features"); + } + if cmd.no_default_features { + proc.arg("--no-default-features"); + } + proc.arg("--"); + proc.arg("-q"); + let status = proc.status()?; + if !status.success() { + eprintln!("error: some tests failed"); + } + + if cmd.review { + process_snapshots( + &ProcessCommand { + pkg_args: cmd.pkg_args.clone(), + }, + None, + )? + } + + Ok(()) +} + pub fn run() -> Result<(), Error> { // chop off cargo let mut args = env::args_os(); @@ -223,5 +304,6 @@ pub fn run() -> Result<(), Error> { Command::Review(cmd) => process_snapshots(&cmd, None), Command::Accept(cmd) => process_snapshots(&cmd, Some(Operation::Accept)), Command::Reject(cmd) => process_snapshots(&cmd, Some(Operation::Reject)), + Command::Test(cmd) => test_run(&cmd), } } diff --git a/tests/snapshots/test_basic__json_vector.snap b/tests/snapshots/test_basic__json_vector.snap new file mode 100644 index 00000000..107d3346 --- /dev/null +++ b/tests/snapshots/test_basic__json_vector.snap @@ -0,0 +1,11 @@ +--- +created: "2019-01-29T23:35:18.809083Z" +creator: insta@0.6.0 +source: tests/test_basic.rs +expression: "vec![1 , 2 , 3]" +--- +[ + 1, + 2, + 3 +] diff --git a/tests/snapshots/test_basic__yaml_vector.snap b/tests/snapshots/test_basic__yaml_vector.snap index 8d3a1812..df83a3a8 100644 --- a/tests/snapshots/test_basic__yaml_vector.snap +++ b/tests/snapshots/test_basic__yaml_vector.snap @@ -1,9 +1,11 @@ --- -created: "2019-01-29T00:09:49.830483Z" +created: "2019-01-29T23:34:54.062133Z" creator: insta@0.6.0 source: tests/test_basic.rs expression: "vec![1 , 2 , 3]" --- -- 1 -- 2 -- 3 +[ + 1, + 2, + 3 +] diff --git a/tests/test_basic.rs b/tests/test_basic.rs index b881d9ac..95d3b276 100644 --- a/tests/test_basic.rs +++ b/tests/test_basic.rs @@ -22,5 +22,5 @@ fn test_yaml_vector() { #[test] fn test_json_vector() { - assert_json_snapshot_matches!("yaml_vector", vec![1, 2, 3]); + assert_json_snapshot_matches!("json_vector", vec![1, 2, 3]); } From 4dd98164f0c72567030ba6c1d1a9d3870fcdde33 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 30 Jan 2019 00:37:34 +0100 Subject: [PATCH 14/18] Updated docs --- README.md | 7 +++++++ cargo-insta/README.md | 2 ++ cargo-insta/src/main.rs | 2 ++ src/lib.rs | 7 +++++++ 4 files changed, 18 insertions(+) diff --git a/README.md b/README.md index e86eef7b..0ad4f84f 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,13 @@ This can be enabled by setting `INSTA_FORCE_PASS` to `1`: $ INSTA_FORCE_PASS=1 cargo test --no-fail-fast ``` +A better way to do this is to run `cargo insta test --review` which will +run all tests with force pass and then bring up the review tool: + +```rust +$ cargo insta test --review +``` + ## Redactions For all snapshots created based on `serde::Serialize` output `insta` diff --git a/cargo-insta/README.md b/cargo-insta/README.md index d6c3ada8..188b5187 100644 --- a/cargo-insta/README.md +++ b/cargo-insta/README.md @@ -12,4 +12,6 @@ $ cargo install cargo-insta $ cargo insta --help ``` +For more information see [the insta crate documentation](https://docs.rs/insta). + License: Apache-2.0 diff --git a/cargo-insta/src/main.rs b/cargo-insta/src/main.rs index fb951371..90265aec 100644 --- a/cargo-insta/src/main.rs +++ b/cargo-insta/src/main.rs @@ -9,6 +9,8 @@ //! $ cargo install cargo-insta //! $ cargo insta --help //! ``` +//! +//! For more information see [the insta crate documentation](https://docs.rs/insta). mod cargo; mod cli; mod inline; diff --git a/src/lib.rs b/src/lib.rs index cc52d6e3..f44fb10f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -122,6 +122,13 @@ //! $ INSTA_FORCE_PASS=1 cargo test --no-fail-fast //! ``` //! +//! A better way to do this is to run `cargo insta test --review` which will +//! run all tests with force pass and then bring up the review tool: +//! +//! ```ignore +//! $ cargo insta test --review +//! ``` +//! //! # Redactions //! //! For all snapshots created based on `serde::Serialize` output `insta` From bb1411060dd365160fdccb25027314ae5955479f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 30 Jan 2019 00:39:02 +0100 Subject: [PATCH 15/18] Make sure INSTA_UPDATE is set with review usage --- cargo-insta/src/cli.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cargo-insta/src/cli.rs b/cargo-insta/src/cli.rs index 955f3297..a4597510 100644 --- a/cargo-insta/src/cli.rs +++ b/cargo-insta/src/cli.rs @@ -264,6 +264,9 @@ fn test_run(cmd: &TestCommand) -> Result<(), Error> { if !cmd.no_force_pass { proc.env("INSTA_FORCE_PASS", "1"); } + if cmd.review { + proc.env("INSTA_UPDATE", "new"); + } if let Some(ref features) = cmd.features { proc.arg("--features"); proc.arg(features); From c5ae3288c4a8dcbd92156006d54ebc4b75b14eb3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 30 Jan 2019 00:40:19 +0100 Subject: [PATCH 16/18] Fixed tests --- tests/snapshots/test_basic__json_json.snap | 11 +++++++++++ tests/snapshots/test_basic__yaml_vector.snap | 10 ++++------ tests/test_basic.rs | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 tests/snapshots/test_basic__json_json.snap diff --git a/tests/snapshots/test_basic__json_json.snap b/tests/snapshots/test_basic__json_json.snap new file mode 100644 index 00000000..fc8ecf2a --- /dev/null +++ b/tests/snapshots/test_basic__json_json.snap @@ -0,0 +1,11 @@ +--- +created: "2019-01-29T23:40:11.451426Z" +creator: insta@0.6.0 +source: tests/test_basic.rs +expression: "vec![1 , 2 , 3]" +--- +[ + 1, + 2, + 3 +] diff --git a/tests/snapshots/test_basic__yaml_vector.snap b/tests/snapshots/test_basic__yaml_vector.snap index df83a3a8..bd018900 100644 --- a/tests/snapshots/test_basic__yaml_vector.snap +++ b/tests/snapshots/test_basic__yaml_vector.snap @@ -1,11 +1,9 @@ --- -created: "2019-01-29T23:34:54.062133Z" +created: "2019-01-29T23:40:11.452071Z" creator: insta@0.6.0 source: tests/test_basic.rs expression: "vec![1 , 2 , 3]" --- -[ - 1, - 2, - 3 -] +- 1 +- 2 +- 3 diff --git a/tests/test_basic.rs b/tests/test_basic.rs index 95d3b276..013c9248 100644 --- a/tests/test_basic.rs +++ b/tests/test_basic.rs @@ -22,5 +22,5 @@ fn test_yaml_vector() { #[test] fn test_json_vector() { - assert_json_snapshot_matches!("json_vector", vec![1, 2, 3]); + assert_json_snapshot_matches!("json_json", vec![1, 2, 3]); } From 4ba507206ebe28a8db8cb4d72c1cdc4232d94c6e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 30 Jan 2019 00:47:49 +0100 Subject: [PATCH 17/18] Do not review on non snapshot failures --- cargo-insta/src/cli.rs | 16 ++++++++++++++-- cargo-insta/src/main.rs | 11 +++++++++-- tests/snapshots/test_basic__json_json.snap | 2 +- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/cargo-insta/src/cli.rs b/cargo-insta/src/cli.rs index a4597510..7886b471 100644 --- a/cargo-insta/src/cli.rs +++ b/cargo-insta/src/cli.rs @@ -3,13 +3,18 @@ use std::path::{Path, PathBuf}; use std::process; use console::{set_colors_enabled, style, Key, Term}; -use failure::{err_msg, Error}; +use failure::{err_msg, Error, Fail}; use insta::{print_snapshot_diff, Snapshot}; use structopt::clap::AppSettings; use structopt::StructOpt; use crate::cargo::{find_packages, get_cargo, get_package_metadata, Metadata, Operation, Package}; +/// Close without message but exit code. +#[derive(Fail, Debug)] +#[fail(display = "exit with {}", _0)] +pub struct QuietExit(pub i32); + /// A helper utility to work with insta snapshots. #[derive(StructOpt, Debug)] #[structopt( @@ -280,8 +285,15 @@ fn test_run(cmd: &TestCommand) -> Result<(), Error> { proc.arg("--"); proc.arg("-q"); let status = proc.status()?; + if !status.success() { - eprintln!("error: some tests failed"); + if cmd.review { + eprintln!( + "{} non snapshot tests failed, skipping review", + style("warning:").bold().yellow() + ); + } + return Err(QuietExit(1).into()); } if cmd.review { diff --git a/cargo-insta/src/main.rs b/cargo-insta/src/main.rs index 90265aec..4d4d02ab 100644 --- a/cargo-insta/src/main.rs +++ b/cargo-insta/src/main.rs @@ -15,9 +15,16 @@ mod cargo; mod cli; mod inline; +use console::style; + fn main() { if let Err(err) = cli::run() { - println!("error: {}", err); - std::process::exit(1); + let exit_code = if let Some(ref exit) = err.downcast_ref::() { + exit.0 + } else { + println!("{} {}", style("error:").red().bold(), err); + 1 + }; + std::process::exit(exit_code); } } diff --git a/tests/snapshots/test_basic__json_json.snap b/tests/snapshots/test_basic__json_json.snap index fc8ecf2a..58593dd7 100644 --- a/tests/snapshots/test_basic__json_json.snap +++ b/tests/snapshots/test_basic__json_json.snap @@ -1,5 +1,5 @@ --- -created: "2019-01-29T23:40:11.451426Z" +created: "2019-01-29T23:47:38.350858Z" creator: insta@0.6.0 source: tests/test_basic.rs expression: "vec![1 , 2 , 3]" From d4f555e60863de74186bbdafe9d9e8dad26bb3cc Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 1 Feb 2019 14:43:45 +0100 Subject: [PATCH 18/18] Update to release version of proc-macro2 --- Cargo.toml | 3 --- cargo-insta/Cargo.toml | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 91d04013..11a488c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,3 @@ pest = "2.1.0" pest_derive = "2.1.0" ron = "0.4.1" uuid = { version = "0.7.1", features = ["serde", "v4"] } - -[patch.crates-io] -proc-macro2 = { git = "https://github.com/dtolnay/proc-macro2", rev = "3b1f7d28c11a0f88ab52504c4b8e1dc9e7789883" } diff --git a/cargo-insta/Cargo.toml b/cargo-insta/Cargo.toml index 2dee9c30..c5836252 100644 --- a/cargo-insta/Cargo.toml +++ b/cargo-insta/Cargo.toml @@ -21,5 +21,5 @@ serde_json = "1.0.36" failure = "0.1.5" glob = "0.2.11" walkdir = "2.2.7" -proc-macro2 = { version = "0.4.26", features = ["span-locations"] } +proc-macro2 = { version = "0.4.27", features = ["span-locations"] } syn = { version = "0.15.26", features = ["full"] }