Skip to content

Commit

Permalink
Add new --unreferenced option to control unreferenced snapshot behavi…
Browse files Browse the repository at this point in the history
…or (#328)
  • Loading branch information
mitsuhiko committed Jan 3, 2023
1 parent 7d43b71 commit c1947bc
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 64 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -9,6 +9,10 @@ All notable changes to insta and cargo-insta are documented here.
- Renamed `--no-ignore` to `--include-ignored`.
- Added `--include-hidden` to instruct insta to also walk into
hidden paths.
- Added new `--unreferenced` option to `cargo-insta test` which allows
fine tuning of what should happen with unreferenced files. It's now
possible to ignore (default), warn, reject or delete unreferenced
snapshots. (#328)

## 1.23.0

Expand Down
183 changes: 124 additions & 59 deletions cargo-insta/src/cli.rs
Expand Up @@ -10,7 +10,8 @@ use console::{set_colors_enabled, style, Key, Term};
use ignore::{Walk, WalkBuilder};
use insta::Snapshot;
use insta::_cargo_insta_support::{
print_snapshot, print_snapshot_diff, SnapshotUpdate, TestRunner, ToolConfig,
is_ci, print_snapshot, print_snapshot_diff, SnapshotUpdate, TestRunner, ToolConfig,
UnreferencedSnapshots,
};
use serde::Serialize;
use structopt::clap::AppSettings;
Expand Down Expand Up @@ -172,8 +173,11 @@ pub struct TestCommand {
/// Update all snapshots even if they are still matching.
#[structopt(long)]
pub force_update_snapshots: bool,
/// Delete unreferenced snapshots after the test run.
/// Controls what happens with unreferenced snapshots.
#[structopt(long)]
pub unreferenced: Option<String>,
/// Delete unreferenced snapshots after the test run.
#[structopt(long, hidden = true)]
pub delete_unreferenced_snapshots: bool,
/// Filters to apply to the insta glob feature.
#[structopt(long)]
Expand Down Expand Up @@ -402,7 +406,7 @@ fn load_snapshot_containers<'a>(
fn process_snapshots(
quiet: bool,
snapshot_filter: Option<&[String]>,
loc: LocationInfo<'_>,
loc: &LocationInfo<'_>,
op: Option<Operation>,
) -> Result<(), Box<dyn Error>> {
let term = Term::stdout();
Expand Down Expand Up @@ -590,17 +594,30 @@ fn test_run(mut cmd: TestCommand, color: &str) -> Result<(), Box<dyn Error>> {
cmd.review = true;
}

// Legacy command
if cmd.delete_unreferenced_snapshots {
cmd.unreferenced = Some("delete".into());
}

let test_runner = match cmd.test_runner {
Some(ref test_runner) => test_runner
.parse()
.map_err(|_| err_msg("invalid test runner preference"))?,
None => loc.tool_config.test_runner(),
};

let (mut proc, snapshot_ref_file) = prepare_test_runner(test_runner, &cmd, color, &[], None)?;
let unreferenced = match cmd.unreferenced {
Some(ref value) => value
.parse()
.map_err(|_| err_msg("invalid value for --unreferenced"))?,
None => loc.tool_config.test_unreferenced(),
};

let (mut proc, snapshot_ref_file) =
prepare_test_runner(test_runner, unreferenced, &cmd, color, &[], None)?;

if !cmd.keep_pending {
process_snapshots(true, None, loc, Some(Operation::Reject))?;
process_snapshots(true, None, &loc, Some(Operation::Reject))?;
}

let status = proc.status()?;
Expand All @@ -610,6 +627,7 @@ fn test_run(mut cmd: TestCommand, color: &str) -> Result<(), Box<dyn Error>> {
if matches!(test_runner, TestRunner::Nextest) {
let (mut proc, _) = prepare_test_runner(
TestRunner::CargoTest,
unreferenced,
&cmd,
color,
&["--doc"],
Expand All @@ -633,65 +651,16 @@ fn test_run(mut cmd: TestCommand, color: &str) -> Result<(), Box<dyn Error>> {
return Err(QuietExit(1).into());
}

// delete unreferenced snapshots if we were instructed to do so
// handle unreferenced snapshots if we were instructed to do so
if let Some(ref path) = snapshot_ref_file {
let mut files = HashSet::new();
match fs::read_to_string(path) {
Ok(s) => {
for line in s.lines() {
if let Ok(path) = fs::canonicalize(line) {
files.insert(path);
}
}
}
Err(err) => {
// if the file was not created, no test referenced
// snapshots.
if err.kind() != io::ErrorKind::NotFound {
return Err(err.into());
}
}
}

if let Ok(loc) = handle_target_args(&cmd.target_args) {
let mut deleted_any = false;
for entry in make_deletion_walker(&loc, cmd.package.as_deref()) {
let rel_path = match entry {
Ok(ref entry) => entry.path(),
_ => continue,
};
if !rel_path.is_file()
|| !rel_path
.file_name()
.map_or(false, |x| x.to_str().unwrap_or("").ends_with(".snap"))
{
continue;
}

if let Ok(path) = fs::canonicalize(rel_path) {
if !files.contains(&path) {
if !deleted_any {
eprintln!("{}: deleted unreferenced snapshots:", style("info").bold());
deleted_any = true;
}
eprintln!(" {}", rel_path.display());
fs::remove_file(path).ok();
}
}
}
if !deleted_any {
eprintln!("{}: no unreferenced snapshots found", style("info").bold());
}
}

fs::remove_file(&path).ok();
handle_unreferenced_snapshots(path, &loc, unreferenced, cmd.package.as_deref())?;
}

if cmd.review || cmd.accept {
process_snapshots(
false,
None,
handle_target_args(&cmd.target_args)?,
&handle_target_args(&cmd.target_args)?,
if cmd.accept {
Some(Operation::Accept)
} else {
Expand Down Expand Up @@ -720,8 +689,103 @@ fn test_run(mut cmd: TestCommand, color: &str) -> Result<(), Box<dyn Error>> {
Ok(())
}

fn handle_unreferenced_snapshots(
path: &Cow<Path>,
loc: &LocationInfo<'_>,
unreferenced: UnreferencedSnapshots,
package: Option<&str>,
) -> Result<(), Box<dyn Error>> {
enum Action {
Delete,
Reject,
Warn,
}

let action = match unreferenced {
UnreferencedSnapshots::Auto => {
if is_ci() {
Action::Reject
} else {
Action::Delete
}
}
UnreferencedSnapshots::Reject => Action::Reject,
UnreferencedSnapshots::Delete => Action::Delete,
UnreferencedSnapshots::Warn => Action::Warn,
UnreferencedSnapshots::Ignore => return Ok(()),
};

let mut files = HashSet::new();
match fs::read_to_string(path) {
Ok(s) => {
for line in s.lines() {
if let Ok(path) = fs::canonicalize(line) {
files.insert(path);
}
}
}
Err(err) => {
// if the file was not created, no test referenced
// snapshots.
if err.kind() != io::ErrorKind::NotFound {
return Err(err.into());
}
}
}

let mut encountered_any = false;
for entry in make_deletion_walker(&loc, package) {
let rel_path = match entry {
Ok(ref entry) => entry.path(),
_ => continue,
};
if !rel_path.is_file()
|| !rel_path
.file_name()
.map_or(false, |x| x.to_str().unwrap_or("").ends_with(".snap"))
{
continue;
}

if let Ok(path) = fs::canonicalize(rel_path) {
if files.contains(&path) {
continue;
}
if !encountered_any {
match action {
Action::Delete => {
eprintln!("{}: deleted unreferenced snapshots:", style("info").bold());
}
_ => {
eprintln!(
"{}: encountered unreferenced snapshots:",
style("warning").bold()
);
}
}
encountered_any = true;
}
eprintln!(" {}", rel_path.display());
if matches!(action, Action::Delete) {
fs::remove_file(path).ok();
}
}
}

fs::remove_file(&path).ok();

if !encountered_any {
eprintln!("{}: no unreferenced snapshots found", style("info").bold());
} else if matches!(action, Action::Reject) {
return Err(err_msg("aborting because of unreferenced snapshots"));
}

Ok(())
}

fn prepare_test_runner<'snapshot_ref>(
test_runner: TestRunner,
unreferenced: UnreferencedSnapshots,
cmd: &TestCommand,
color: &str,
extra_args: &[&str],
Expand All @@ -740,7 +804,8 @@ fn prepare_test_runner<'snapshot_ref>(
proc
}
};
let snapshot_ref_file = if cmd.delete_unreferenced_snapshots {

let snapshot_ref_file = if unreferenced != UnreferencedSnapshots::Ignore {
match snapshot_ref_file {
Some(path) => Some(Cow::Borrowed(path)),
None => {
Expand Down Expand Up @@ -933,7 +998,7 @@ pub fn run() -> Result<(), Box<dyn Error>> {
process_snapshots(
cmd.quiet,
cmd.snapshot_filter.as_deref(),
handle_target_args(&cmd.target_args)?,
&handle_target_args(&cmd.target_args)?,
match opts.command {
Command::Review(_) => None,
Command::Accept(_) => Some(Operation::Accept),
Expand Down
49 changes: 47 additions & 2 deletions src/env.rs
Expand Up @@ -45,6 +45,18 @@ pub enum OutputBehavior {
Nothing,
}

/// Unreferenced snapshots flag
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg(feature = "_cargo_insta_internal")]
pub enum UnreferencedSnapshots {
Auto,
Reject,
Delete,
Warn,
Ignore,
}

/// Snapshot update flag
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SnapshotUpdate {
Always,
Expand All @@ -59,14 +71,17 @@ pub enum Error {
Deserialize(crate::content::Error),
Io(std::io::Error),
Env(&'static str),
#[allow(unused)]
Config(&'static str),
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Deserialize(_) => write!(f, "failed to deserialize tool config"),
Error::Io(_) => write!(f, "io error while reading tool config"),
Error::Env(msg) => write!(f, "invalid value for env var '{}'", msg),
Error::Env(var) => write!(f, "invalid value for env var '{}'", var),
Error::Config(var) => write!(f, "invalid value for config '{}'", var),
}
}
}
Expand All @@ -76,7 +91,7 @@ impl std::error::Error for Error {
match self {
Error::Deserialize(ref err) => Some(err),
Error::Io(ref err) => Some(err),
Error::Env(_) => None,
_ => None,
}
}
}
Expand All @@ -93,6 +108,8 @@ pub struct ToolConfig {
#[cfg(feature = "_cargo_insta_internal")]
test_runner: TestRunner,
#[cfg(feature = "_cargo_insta_internal")]
test_unreferenced: UnreferencedSnapshots,
#[cfg(feature = "_cargo_insta_internal")]
auto_review: bool,
#[cfg(feature = "_cargo_insta_internal")]
auto_accept_unseen: bool,
Expand Down Expand Up @@ -196,6 +213,14 @@ impl ToolConfig {
.map_err(|_| Error::Env("INSTA_TEST_RUNNER"))?
},
#[cfg(feature = "_cargo_insta_internal")]
test_unreferenced: {
resolve(&cfg, &["test", "unreferenced"])
.and_then(|x| x.as_str())
.unwrap_or("ignore")
.parse::<UnreferencedSnapshots>()
.map_err(|_| Error::Config("unreferenced"))?
},
#[cfg(feature = "_cargo_insta_internal")]
auto_review: resolve(&cfg, &["test", "auto_review"])
.and_then(|x| x.as_bool())
.unwrap_or(false),
Expand Down Expand Up @@ -248,6 +273,10 @@ impl ToolConfig {
self.test_runner
}

pub fn test_unreferenced(&self) -> UnreferencedSnapshots {
self.test_unreferenced
}

/// Returns the auto review flag.
pub fn auto_review(&self) -> bool {
self.auto_review
Expand Down Expand Up @@ -355,6 +384,22 @@ impl std::str::FromStr for TestRunner {
}
}

#[cfg(feature = "_cargo_insta_internal")]
impl std::str::FromStr for UnreferencedSnapshots {
type Err = ();

fn from_str(value: &str) -> Result<UnreferencedSnapshots, ()> {
match value {
"auto" => Ok(UnreferencedSnapshots::Auto),
"reject" | "error" => Ok(UnreferencedSnapshots::Reject),
"delete" => Ok(UnreferencedSnapshots::Delete),
"warn" => Ok(UnreferencedSnapshots::Warn),
"ignore" => Ok(UnreferencedSnapshots::Ignore),
_ => Err(()),
}
}
}

/// Memoizes a snapshot file in the reference file.
pub fn memoize_snapshot_file(snapshot_file: &Path) {
if let Ok(path) = env::var("INSTA_SNAPSHOT_REFERENCES_FILE") {
Expand Down
8 changes: 5 additions & 3 deletions src/lib.rs
Expand Up @@ -287,13 +287,15 @@ pub mod internals {
#[doc(hidden)]
#[cfg(feature = "_cargo_insta_internal")]
pub mod _cargo_insta_support {
pub use crate::env::{
Error as ToolConfigError, OutputBehavior, SnapshotUpdate, TestRunner, ToolConfig,
};
pub use crate::{
env::{
Error as ToolConfigError, OutputBehavior, SnapshotUpdate, TestRunner, ToolConfig,
UnreferencedSnapshots,
},
output::{print_snapshot, print_snapshot_diff},
snapshot::PendingInlineSnapshot,
snapshot::SnapshotContents,
utils::is_ci,
};
}

Expand Down

0 comments on commit c1947bc

Please sign in to comment.