Skip to content

Added inline snapshot support #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Feb 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"editor.formatOnSave": true,
"rust.clippy_preference": "on",
"rust.cfg_test": true,
"rust.all_features": true
}
6 changes: 2 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "insta"
version = "0.5.4"
version = "0.6.0"
license = "Apache-2.0"
authors = ["Armin Ronacher <[email protected]>"]
description = "A snapshot testing library for Rust"
Expand Down Expand Up @@ -29,13 +29,11 @@ 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"] }
39 changes: 37 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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 `<module>__<name>.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.
Expand Down Expand Up @@ -120,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`
Expand Down Expand Up @@ -151,12 +162,36 @@ 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(),
}, {
".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
#[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
4 changes: 3 additions & 1 deletion cargo-insta/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -21,3 +21,5 @@ serde_json = "1.0.36"
failure = "0.1.5"
glob = "0.2.11"
walkdir = "2.2.7"
proc-macro2 = { version = "0.4.27", features = ["span-locations"] }
syn = { version = "0.15.26", features = ["full"] }
2 changes: 2 additions & 0 deletions cargo-insta/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
205 changes: 176 additions & 29 deletions cargo-insta/src/cargo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand All @@ -42,41 +51,157 @@ 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 }
#[derive(Debug)]
pub struct PendingSnapshot {
pub id: usize,
pub old: Option<Snapshot>,
pub new: Snapshot,
pub op: Operation,
pub line: Option<u32>,
}

impl PendingSnapshot {
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() {
write!(&mut rv, " ({})", name).unwrap();
}
rv
}
}

pub fn path(&self) -> &Path {
&self.old_path
#[derive(Debug)]
pub struct SnapshotContainer {
snapshot_path: PathBuf,
target_path: PathBuf,
kind: SnapshotContainerKind,
snapshots: Vec<PendingSnapshot>,
patcher: Option<FilePatcher>,
}

impl SnapshotContainer {
fn load(
snapshot_path: PathBuf,
target_path: PathBuf,
kind: SnapshotContainerKind,
) -> Result<SnapshotContainer, Error> {
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 load_old(&self) -> Result<Option<Snapshot>, Error> {
if fs::metadata(&self.old_path).is_err() {
Ok(None)
} else {
Snapshot::from_file(&self.old_path).map(Some)
pub fn snapshot_file(&self) -> Option<&Path> {
match self.kind {
SnapshotContainerKind::External => Some(&self.target_path),
SnapshotContainerKind::Inline => None,
}
}

pub fn load_new(&self) -> Result<Snapshot, Error> {
Snapshot::from_file(&self.new_path)
pub fn len(&self) -> usize {
self.snapshots.len()
}

pub fn accept(&self) -> Result<(), Error> {
fs::rename(&self.new_path, &self.old_path)?;
Ok(())
pub fn iter_snapshots(&mut self) -> impl Iterator<Item = &'_ mut PendingSnapshot> {
self.snapshots.iter_mut()
}

pub fn reject(&self) -> Result<(), Error> {
fs::remove_file(&self.new_path)?;
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(())
}
}
Expand All @@ -98,7 +223,9 @@ impl Package {
&self.version
}

pub fn iter_snapshots(&self) -> impl Iterator<Item = SnapshotRef> {
pub fn iter_snapshot_containers(
&self,
) -> impl Iterator<Item = Result<SnapshotContainer, Error>> {
let mut roots = HashSet::new();
for target in &self.targets {
let root = target.src_path.parent().unwrap();
Expand All @@ -109,10 +236,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()
Expand All @@ -121,13 +249,32 @@ 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()))
})
}
}

fn get_cargo() -> String {
pub fn get_cargo() -> String {
env::var("CARGO")
.ok()
.unwrap_or_else(|| "cargo".to_string())
Expand Down
Loading