Skip to content
Permalink
Browse files

Initial commit

  • Loading branch information
mitsuhiko committed Jan 13, 2019
0 parents commit 929eed8acdee8ff4534dbbfb74753ccd99ad157b
@@ -0,0 +1,3 @@
/target
**/*.rs.bk
Cargo.lock
@@ -0,0 +1,16 @@
[package]
name = "insta"
version = "0.1.0"
authors = ["Armin Ronacher <armin.ronacher@active-4.com>"]
edition = "2018"

[features]
serialization = ["serde", "serde_yaml"]

[dependencies]
difference = "2.0.0"
serde = { version = "1.0.84", optional = true }
failure = "0.1.5"
serde_yaml = { version = "0.8.8", optional = true }
console = "0.7.2"
chrono = "0.4.6"
@@ -0,0 +1,66 @@
//! "insta" is a simple snapshot testing library for Rust.
//!
//! # How it Operates
//!
//! This crate exports two basic macros for snapshot testing:
//! `assert_snapshot_matches!` for comparing basic string snapshots and
//! `assert_debug_snapshot_matches!` for snapshotting the debug print output of
//! a type. Additionally if the `serialization` feature is enabled the
//! `assert_serialized_snapshot_matches!` macro becomes available which
//! serializes an object with `serde` to yaml before snapshotting.
//!
//! 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.
//!
//! To update the snapshots export the `INSTA_UPDATE` environment variable
//! and set it to `1`. The snapshots can then be committed.
//!
//! # Example
//!
//! ```rust,ignore
//! use insta::assert_debug_snapshot_matches;
//!
//! #[test]
//! fn test_snapshots() {
//! let value = vec![1, 2, 3];
//! assert_debug_snapshot_matches!("snapshot_name", value);
//! }
//! ```
//!
//! The recommended flow is to run the tests once, have them fail and check
//! if the result is okay. Once you are satisifed run the tests again with
//! `INSTA_UPDATE` set to `1` and updates will be stored:
//!
//! ```ignore
//! $ INSTA_UPDATE=1 cargo test
//! ```
//!
//! # Snapshot files
//!
//! The committed snapshot files will have a header with some meta information
//! that can make debugging easier and the snapshot:
//!
//! ```ignore
//! Created: 2019-01-13T22:16:48.669496+00:00
//! Creator: insta@0.1.0
//! Source: tests/test_snapshots.rs
//!
//! [
//! 1,
//! 2,
//! 3
//! ]
//! ```
#[macro_use]
mod macros;
mod runtime;
#[cfg(test)]
mod test;

#[doc(hidden)]
pub mod _macro_support {
pub use crate::runtime::assert_snapshot;
#[cfg(feature = "serialization")]
pub use crate::runtime::serialize_value;
}
@@ -0,0 +1,43 @@
/// Assets a `Serialize` snapshot.
///
/// The value needs to implement the `serde::Serialize` trait.
///
/// This requires the `serialization` feature to be enabled.
#[cfg(feature = "serialization")]
#[macro_export]
macro_rules! assert_serialized_snapshot_matches {
($name:expr, $value:expr) => {{
let value = $crate::_macro_support::serialize_value(&$value);
$crate::assert_snapshot_matches!($name, value);
}};
}

/// Assets a `Debug` snapshot.
///
/// The value needs to implement the `fmt::Debug` trait.
#[macro_export]
macro_rules! assert_debug_snapshot_matches {
($name:expr, $value:expr) => {{
let value = format!("{:#?}", $value);
$crate::assert_snapshot_matches!($name, value);
}};
}

/// Assets a string snapshot.
#[macro_export]
macro_rules! assert_snapshot_matches {
($name:expr, $value:expr) => {
match &$value {
value => {
$crate::_macro_support::assert_snapshot(
&$name,
value,
module_path!(),
file!(),
line!(),
)
.unwrap();
}
}
};
}
@@ -0,0 +1,197 @@
use std::collections::BTreeMap;
use std::env;
use std::fmt;
use std::fs;
use std::io::Write;
use std::io::{BufRead, BufReader, Read};
use std::path::{Path, PathBuf};

use chrono::Utc;
use console::style;
use difference::Changeset;
use failure::Error;

#[cfg(feature = "serialization")]
use {serde::Serialize, serde_yaml};

struct RunHint<'a>(&'a Path, Option<&'a Snapshot>);

impl<'a> fmt::Display for RunHint<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"\n{title:-^width$}\nSnapshot: {file}\n",
file = style(self.0.display()).cyan().underlined(),
title = style(" Snapshot Information ").bold(),
width = 74
)?;

if let Some(ref old) = self.1 {
for (key, value) in old.metadata.iter() {
write!(f, "{}: {}\n", key, style(value).cyan())?;
}
}

write!(
f,
"\n{hint}\n",
hint = style("To update the snapshots re-run the tests with INSTA_UPDATE=1.").dim(),
)?;
Ok(())
}
}

fn should_update_snapshot() -> bool {
match env::var("INSTA_UPDATE").ok().as_ref().map(|x| x.as_str()) {
None | Some("") => false,
Some("1") => true,
_ => panic!("invalid value for INSTA_UPDATE"),
}
}

pub fn get_snapshot_filename(name: &str, module_path: &str, base: &str) -> PathBuf {
let path = Path::new(base);
path.parent()
.unwrap()
.join("snapshots")
.join(format!("{}__{}.snap", module_path.rsplit("::").next().unwrap(), name))
}

#[derive(Debug)]
pub struct Snapshot {
path: PathBuf,
metadata: BTreeMap<String, String>,
snapshot: String,
}

impl Snapshot {
pub fn from_file<P: AsRef<Path>>(p: &P) -> Result<Snapshot, Error> {
let mut f = BufReader::new(fs::File::open(p)?);
let mut buf = String::new();
let mut metadata = BTreeMap::new();

loop {
buf.clear();
f.read_line(&mut buf)?;
if buf.trim().is_empty() {
break;
}
let mut iter = buf.splitn(2, ':');
if let Some(key) = iter.next() {
if let Some(value) = iter.next() {
metadata.insert(key.to_string(), value.trim().to_string());
}
}
}

buf.clear();
f.read_to_string(&mut buf)?;
if buf.ends_with('\n') {
buf.truncate(buf.len() - 1);
}

Ok(Snapshot {
path: p.as_ref().to_path_buf(),
metadata,
snapshot: buf,
})
}

pub fn save(&self) -> Result<(), Error> {
if let Some(folder) = self.path.parent() {
fs::create_dir_all(&folder)?;
}
let mut f = fs::File::create(&self.path)?;
for (key, value) in self.metadata.iter() {
write!(f, "{}: {}\n", key, value)?;
}
f.write_all(b"\n")?;
f.write_all(self.snapshot.as_bytes())?;
f.write_all(b"\n")?;
Ok(())
}
}

pub fn assert_snapshot(
name: &str,
new_snapshot: &str,
module_path: &str,
file: &str,
line: u32,
) -> Result<(), Error> {
let snapshot_file = get_snapshot_filename(name, module_path, file);
let old = Snapshot::from_file(&snapshot_file).ok();

// if the snapshot matches we're done.
if old.as_ref().map_or(false, |x| x.snapshot == new_snapshot) {
return Ok(());
}

if should_update_snapshot() {
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")),
);
metadata.insert("Source".to_string(), file.to_string());
let snapshot = Snapshot {
path: snapshot_file.to_path_buf(),
metadata: metadata,
snapshot: new_snapshot.to_string(),
};
snapshot.save()?;

match old {
Some(ref old) => {
let title = Changeset::new("- old snapshot", "+ new snapshot", "\n");
let changeset = Changeset::new(&old.snapshot, new_snapshot, "\n");
writeln!(
std::io::stderr(),
" {} {}\n{}\n{}",
style("updated snapshot").green(),
style(snapshot_file.display()).cyan().underlined(),
title,
changeset,
)?;
}
None => {
writeln!(
std::io::stderr(),
" {} {}",
style("created snapshot").green(),
style(snapshot_file.display()).cyan().underlined()
)?;
}
}
} else {
match old.as_ref().map(|x| &x.snapshot) {
None => panic!(
"Missing snapshot '{}' in line {}{}",
name,
line,
RunHint(&snapshot_file, old.as_ref()),
),
Some(ref old_snapshot) => {
let title = Changeset::new("- got this run", "+ expected snapshot", "\n");
let changeset = Changeset::new(new_snapshot, old_snapshot, "\n");
assert!(
false,
"snapshot '{}' mismatched in line {}:\n{}\n{}{}",
name,
line,
title,
changeset,
RunHint(&snapshot_file, old.as_ref()),
);
}
}
}

Ok(())
}

#[cfg(feature = "serialization")]
pub fn serialize_value<S: Serialize>(s: &S) -> String {
serde_yaml::to_string(s).unwrap()
}
@@ -0,0 +1,5 @@
Created: 2019-01-13T22:16:48.630931+00:00
Creator: insta@0.1.0
Source: src/test.rs

Just a string
@@ -0,0 +1,4 @@
#[test]
fn test_embedded_test() {
assert_snapshot_matches!("embedded", "Just a string");
}
@@ -0,0 +1,9 @@
Created: 2019-01-13T22:16:48.669496+00:00
Creator: insta@0.1.0
Source: tests/test_basic.rs

---
- 1
- 2
- 3
- 4
@@ -0,0 +1,10 @@
Created: 2019-01-13T22:16:48.669496+00:00
Creator: insta@0.1.0
Source: tests/test_basic.rs

[
1,
2,
3,
4
]
@@ -0,0 +1,13 @@
extern crate insta;

use insta::{assert_debug_snapshot_matches, assert_serialized_snapshot_matches};

#[test]
fn test_vector() {
assert_debug_snapshot_matches!("vector", vec![1, 2, 3, 4]);
}

#[test]
fn test_serialized_vector() {
assert_serialized_snapshot_matches!("serialized_vector", vec![1, 2, 3, 4]);
}

0 comments on commit 929eed8

Please sign in to comment.
You can’t perform that action at this time.