From 7781d8eb1c5c6b4565b3c1dfa609df05c053589a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 2 Jan 2023 13:51:40 +0100 Subject: [PATCH 1/3] Added initial work to support tool configs --- src/env.rs | 154 +++++++++++++++++++++++++++++++++++-------------- src/runtime.rs | 24 ++++---- 2 files changed, 126 insertions(+), 52 deletions(-) diff --git a/src/env.rs b/src/env.rs index 7d37c7ec..0eebff36 100644 --- a/src/env.rs +++ b/src/env.rs @@ -1,13 +1,25 @@ use std::collections::BTreeMap; -use std::io::Write; +use std::io::{self, Write}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::{env, fs}; +use crate::content::{yaml, Content}; use crate::utils::is_ci; lazy_static::lazy_static! { static ref WORKSPACES: Mutex>> = Mutex::new(BTreeMap::new()); + static ref TOOL_CONFIGS: Mutex>> = Mutex::new(BTreeMap::new()); +} + +pub fn get_tool_config(manifest_dir: &str) -> Arc { + let mut configs = TOOL_CONFIGS.lock().unwrap(); + if let Some(rv) = configs.get(manifest_dir) { + return rv.clone(); + } + let config = Arc::new(ToolConfig::load(manifest_dir)); + configs.insert(manifest_dir.to_string(), config.clone()); + config } /// How snapshots are supposed to be updated @@ -34,56 +46,114 @@ pub enum OutputBehavior { Nothing, } -/// Is insta told to force update snapshots? -pub fn force_update_snapshots() -> bool { - match env::var("INSTA_FORCE_UPDATE_SNAPSHOTS").ok().as_deref() { - None | Some("") | Some("0") => false, - Some("1") => true, - _ => panic!("invalid value for INSTA_FORCE_UPDATE_SNAPSHOTS"), - } +/// Represents a tool configuration. +#[derive(Debug)] +pub struct ToolConfig { + values: Content, } -/// Is insta instructed to fail in tests? -pub fn force_pass() -> bool { - match env::var("INSTA_FORCE_PASS").ok().as_deref() { - None | Some("") | Some("0") => false, - Some("1") => true, - _ => panic!("invalid value for INSTA_FORCE_PASS"), +impl ToolConfig { + /// Loads the tool config for a specific manifest. + pub fn load(manifest_dir: &str) -> ToolConfig { + let cargo_workspace = get_cargo_workspace(manifest_dir); + let path = cargo_workspace.join(".config/insta.yaml"); + let values = match fs::read_to_string(path) { + Ok(s) => yaml::parse_str(&s).expect("failed to deserialize tool config"), + Err(err) if matches!(err.kind(), io::ErrorKind::NotFound) => { + Content::Map(Default::default()) + } + Err(err) => panic!("failed to read tool config: {}", err), + }; + ToolConfig { values } + } + + fn resolve(&self, path: &[&str]) -> Option<&Content> { + path.iter() + .try_fold(&self.values, |node, segment| match node.resolve_inner() { + Content::Map(fields) => fields + .iter() + .find(|x| x.0.as_str() == Some(segment)) + .map(|x| &x.1), + Content::Struct(_, fields) | Content::StructVariant(_, _, _, fields) => { + fields.iter().find(|x| x.0 == *segment).map(|x| &x.1) + } + _ => None, + }) } -} -/// Returns the intended output behavior for insta. -pub fn get_output_behavior() -> OutputBehavior { - match env::var("INSTA_OUTPUT").ok().as_deref() { - None | Some("") | Some("diff") => OutputBehavior::Diff, - Some("summary") => OutputBehavior::Summary, - Some("minimal") => OutputBehavior::Minimal, - Some("none") => OutputBehavior::Nothing, - _ => panic!("invalid value for INSTA_OUTPUT"), + fn get_bool(&self, path: &[&str]) -> Option { + self.resolve(path).and_then(|x| x.as_bool()) } -} -/// Returns the intended snapshot update behavior. -pub fn get_snapshot_update_behavior(unseen: bool) -> SnapshotUpdate { - match env::var("INSTA_UPDATE").ok().as_deref() { - None | Some("") | Some("auto") => { - if is_ci() { - SnapshotUpdate::NoUpdate - } else { - SnapshotUpdate::NewFile - } + fn get_str(&self, path: &[&str]) -> Option<&str> { + self.resolve(path).and_then(|x| x.as_str()) + } + + /// Is insta told to force update snapshots? + pub fn force_update_snapshots(&self) -> bool { + match env::var("INSTA_FORCE_UPDATE_SNAPSHOTS").ok().as_deref() { + None | Some("") => self + .get_bool(&["snapshots", "force_update"]) + .unwrap_or(false), + Some("0") => false, + Some("1") => true, + _ => panic!("invalid value for INSTA_FORCE_UPDATE_SNAPSHOTS"), + } + } + + /// Is insta instructed to fail in tests? + pub fn force_pass(&self) -> bool { + match env::var("INSTA_FORCE_PASS").ok().as_deref() { + None | Some("") => self.get_bool(&["behavior", "force_pass"]).unwrap_or(false), + Some("0") => false, + Some("1") => true, + _ => panic!("invalid value for INSTA_FORCE_PASS"), + } + } + + /// Returns the intended output behavior for insta. + pub fn get_output_behavior(&self) -> OutputBehavior { + let env_var = env::var("INSTA_OUTPUT").ok(); + let val = match env_var.as_deref() { + None | Some("") => self.get_str(&["behavior", "output"]).unwrap_or("diff"), + Some(val) => val, + }; + match val { + "diff" => OutputBehavior::Diff, + "summary" => OutputBehavior::Summary, + "minimal" => OutputBehavior::Minimal, + "none" => OutputBehavior::Nothing, + _ => panic!("invalid value for INSTA_OUTPUT"), } - Some("always") | Some("1") => SnapshotUpdate::InPlace, - Some("new") => SnapshotUpdate::NewFile, - Some("unseen") => { - if unseen { - SnapshotUpdate::NewFile - } else { - SnapshotUpdate::InPlace + } + + /// Returns the intended snapshot update behavior. + pub fn get_snapshot_update_behavior(&self, unseen: bool) -> SnapshotUpdate { + let env_var = env::var("INSTA_UPDATE").ok(); + let val = match env_var.as_deref() { + None | Some("") => self.get_str(&["behavior", "update"]).unwrap_or("auto"), + Some(val) => val, + }; + match val { + "auto" => { + if is_ci() { + SnapshotUpdate::NoUpdate + } else { + SnapshotUpdate::NewFile + } + } + "always" | "1" => SnapshotUpdate::InPlace, + "new" => SnapshotUpdate::NewFile, + "unseen" => { + if unseen { + SnapshotUpdate::NewFile + } else { + SnapshotUpdate::InPlace + } } + "no" => SnapshotUpdate::NoUpdate, + _ => panic!("invalid value for INSTA_UPDATE"), } - Some("no") => SnapshotUpdate::NoUpdate, - _ => panic!("invalid value for INSTA_UPDATE"), } } diff --git a/src/runtime.rs b/src/runtime.rs index 6f4610b2..e81b4551 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -8,8 +8,8 @@ use std::str; use std::sync::{Arc, Mutex}; use crate::env::{ - force_pass, force_update_snapshots, get_cargo_workspace, get_output_behavior, - get_snapshot_update_behavior, memoize_snapshot_file, OutputBehavior, SnapshotUpdate, + get_cargo_workspace, get_tool_config, memoize_snapshot_file, OutputBehavior, SnapshotUpdate, + ToolConfig, }; use crate::output::{print_snapshot_diff_with_title, print_snapshot_summary_with_title}; use crate::settings::Settings; @@ -195,6 +195,7 @@ fn get_snapshot_filename( #[derive(Debug)] struct SnapshotAssertionContext<'a> { + tool_config: Arc, cargo_workspace: Arc, module_path: &'a str, snapshot_name: Option>, @@ -215,6 +216,7 @@ impl<'a> SnapshotAssertionContext<'a> { assertion_file: &'a str, assertion_line: u32, ) -> Result, Box> { + let tool_config = get_tool_config(manifest_dir); let cargo_workspace = get_cargo_workspace(manifest_dir); let snapshot_name; let mut snapshot_file = None; @@ -269,6 +271,7 @@ impl<'a> SnapshotAssertionContext<'a> { }; Ok(SnapshotAssertionContext { + tool_config, cargo_workspace, module_path, snapshot_name, @@ -347,8 +350,8 @@ impl<'a> SnapshotAssertionContext<'a> { .snapshot_file .as_ref() .map_or(false, |x| fs::metadata(x).is_ok()); - let should_print = get_output_behavior() != OutputBehavior::Nothing; - let snapshot_update = get_snapshot_update_behavior(unseen); + let should_print = self.tool_config.get_output_behavior() != OutputBehavior::Nothing; + let snapshot_update = self.tool_config.get_snapshot_update_behavior(unseen); match snapshot_update { SnapshotUpdate::InPlace => { @@ -427,7 +430,7 @@ fn prevent_inline_duplicate(function_name: &str, assertion_file: &str, assertion /// This prints the information about the snapshot fn print_snapshot_info(ctx: &SnapshotAssertionContext, new_snapshot: &Snapshot) { - match get_output_behavior() { + match ctx.tool_config.get_output_behavior() { OutputBehavior::Summary => { print_snapshot_summary_with_title( ctx.cargo_workspace.as_path(), @@ -483,7 +486,7 @@ fn finalize_assertion(ctx: &SnapshotAssertionContext, update_result: SnapshotUpd if fail_fast && update_result == SnapshotUpdate::NewFile - && get_output_behavior() != OutputBehavior::Nothing + && ctx.tool_config.get_output_behavior() != OutputBehavior::Nothing && !ctx.is_doctest { println!( @@ -492,8 +495,8 @@ fn finalize_assertion(ctx: &SnapshotAssertionContext, update_result: SnapshotUpd ); } - if update_result != SnapshotUpdate::InPlace && !force_pass() { - if fail_fast && get_output_behavior() != OutputBehavior::Nothing { + if update_result != SnapshotUpdate::InPlace && !ctx.tool_config.force_pass() { + if fail_fast && ctx.tool_config.get_output_behavior() != OutputBehavior::Nothing { println!( "{hint}", hint = style( @@ -512,7 +515,7 @@ fn finalize_assertion(ctx: &SnapshotAssertionContext, update_result: SnapshotUpd if let Some(glob_collector) = stack.last_mut() { glob_collector.failed += 1; if update_result == SnapshotUpdate::NewFile - && get_output_behavior() != OutputBehavior::Nothing + && ctx.tool_config.get_output_behavior() != OutputBehavior::Nothing { glob_collector.show_insta_hint = true; } @@ -560,6 +563,7 @@ pub fn assert_snapshot( assertion_file, assertion_line, )?; + let tool_config = get_tool_config(manifest_dir); // apply filters if they are available #[cfg(feature = "filters")] @@ -577,7 +581,7 @@ pub fn assert_snapshot( if ctx.old_snapshot.as_ref().map(|x| x.contents()) == Some(new_snapshot.contents()) { ctx.cleanup_passing()?; - if force_update_snapshots() { + if tool_config.force_update_snapshots() { ctx.update_snapshot(new_snapshot)?; } // otherwise print information and update snapshots. From b7039a508d33a5a459d24650e06df4fad4c03f13 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 2 Jan 2023 22:13:33 +0100 Subject: [PATCH 2/3] Refactored tool configs --- src/env.rs | 184 +++++++++++++++++++++++++++++++------------------ src/glob.rs | 5 +- src/lib.rs | 22 +++++- src/macros.rs | 2 +- src/runtime.rs | 32 ++++----- 5 files changed, 159 insertions(+), 86 deletions(-) diff --git a/src/env.rs b/src/env.rs index 0eebff36..b63277e4 100644 --- a/src/env.rs +++ b/src/env.rs @@ -24,7 +24,7 @@ pub fn get_tool_config(manifest_dir: &str) -> Arc { /// How snapshots are supposed to be updated #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum SnapshotUpdate { +pub enum SnapshotUpdateBehavior { /// Snapshots are updated in-place InPlace, /// Snapshots are placed in a new file with a .new suffix @@ -46,10 +46,24 @@ pub enum OutputBehavior { Nothing, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum SnapshotUpdateSetting { + Always, + Auto, + Unseen, + New, + No, +} + /// Represents a tool configuration. #[derive(Debug)] pub struct ToolConfig { - values: Content, + force_update_snapshots: bool, + force_pass: bool, + output: OutputBehavior, + snapshot_update: SnapshotUpdateSetting, + #[allow(unused)] + glob_fail_fast: bool, } impl ToolConfig { @@ -64,97 +78,121 @@ impl ToolConfig { } Err(err) => panic!("failed to read tool config: {}", err), }; - ToolConfig { values } - } - fn resolve(&self, path: &[&str]) -> Option<&Content> { - path.iter() - .try_fold(&self.values, |node, segment| match node.resolve_inner() { - Content::Map(fields) => fields - .iter() - .find(|x| x.0.as_str() == Some(segment)) - .map(|x| &x.1), - Content::Struct(_, fields) | Content::StructVariant(_, _, _, fields) => { - fields.iter().find(|x| x.0 == *segment).map(|x| &x.1) - } - _ => None, - }) - } + let force_update_snapshots = match env::var("INSTA_FORCE_UPDATE_SNAPSHOTS").as_deref() { + Err(_) | Ok("") => resolve(&values, &["behavior", "force_update"]) + .and_then(|x| x.as_bool()) + .unwrap_or(false), + Ok("0") => false, + Ok("1") => true, + _ => panic!("invalid value for INSTA_FORCE_UPDATE_SNAPSHOTS"), + }; - fn get_bool(&self, path: &[&str]) -> Option { - self.resolve(path).and_then(|x| x.as_bool()) - } + let force_pass = match env::var("INSTA_FORCE_PASS").as_deref() { + Err(_) | Ok("") => resolve(&values, &["behavior", "force_pass"]) + .and_then(|x| x.as_bool()) + .unwrap_or(false), + Ok("0") => false, + Ok("1") => true, + _ => panic!("invalid value for INSTA_FORCE_PASS"), + }; + + let output = { + let env_var = env::var("INSTA_OUTPUT"); + let val = match env_var.as_deref() { + Err(_) | Ok("") => resolve(&values, &["behavior", "output"]) + .and_then(|x| x.as_str()) + .unwrap_or("diff"), + Ok(val) => val, + }; + match val { + "diff" => OutputBehavior::Diff, + "summary" => OutputBehavior::Summary, + "minimal" => OutputBehavior::Minimal, + "none" => OutputBehavior::Nothing, + _ => panic!("invalid value for INSTA_OUTPUT"), + } + }; + + let snapshot_update = { + let env_var = env::var("INSTA_UPDATE"); + let val = match env_var.as_deref() { + Err(_) | Ok("") => resolve(&values, &["behavior", "update"]) + .and_then(|x| x.as_str()) + .unwrap_or("auto"), + Ok(val) => val, + }; + match val { + "auto" => SnapshotUpdateSetting::Auto, + "always" | "1" => SnapshotUpdateSetting::Always, + "new" => SnapshotUpdateSetting::New, + "unseen" => SnapshotUpdateSetting::Unseen, + "no" => SnapshotUpdateSetting::No, + _ => panic!("invalid value for INSTA_UPDATE"), + } + }; - fn get_str(&self, path: &[&str]) -> Option<&str> { - self.resolve(path).and_then(|x| x.as_str()) + let glob_fail_fast = match env::var("INSTA_GLOB_FAIL_FAST").as_deref() { + Err(_) | Ok("") => resolve(&values, &["behavior", "glob_fail_fast"]) + .and_then(|x| x.as_bool()) + .unwrap_or(false), + Ok("1") => true, + Ok("0") => false, + _ => panic!("invalid value for INSTA_GLOB_FAIL_FAST"), + }; + + ToolConfig { + force_update_snapshots, + force_pass, + output, + snapshot_update, + glob_fail_fast, + } } /// Is insta told to force update snapshots? pub fn force_update_snapshots(&self) -> bool { - match env::var("INSTA_FORCE_UPDATE_SNAPSHOTS").ok().as_deref() { - None | Some("") => self - .get_bool(&["snapshots", "force_update"]) - .unwrap_or(false), - Some("0") => false, - Some("1") => true, - _ => panic!("invalid value for INSTA_FORCE_UPDATE_SNAPSHOTS"), - } + self.force_update_snapshots } /// Is insta instructed to fail in tests? pub fn force_pass(&self) -> bool { - match env::var("INSTA_FORCE_PASS").ok().as_deref() { - None | Some("") => self.get_bool(&["behavior", "force_pass"]).unwrap_or(false), - Some("0") => false, - Some("1") => true, - _ => panic!("invalid value for INSTA_FORCE_PASS"), - } + self.force_pass } /// Returns the intended output behavior for insta. - pub fn get_output_behavior(&self) -> OutputBehavior { - let env_var = env::var("INSTA_OUTPUT").ok(); - let val = match env_var.as_deref() { - None | Some("") => self.get_str(&["behavior", "output"]).unwrap_or("diff"), - Some(val) => val, - }; - match val { - "diff" => OutputBehavior::Diff, - "summary" => OutputBehavior::Summary, - "minimal" => OutputBehavior::Minimal, - "none" => OutputBehavior::Nothing, - _ => panic!("invalid value for INSTA_OUTPUT"), - } + pub fn output_behavior(&self) -> OutputBehavior { + self.output } /// Returns the intended snapshot update behavior. - pub fn get_snapshot_update_behavior(&self, unseen: bool) -> SnapshotUpdate { - let env_var = env::var("INSTA_UPDATE").ok(); - let val = match env_var.as_deref() { - None | Some("") => self.get_str(&["behavior", "update"]).unwrap_or("auto"), - Some(val) => val, - }; - match val { - "auto" => { + pub fn snapshot_update_behavior(&self, unseen: bool) -> SnapshotUpdateBehavior { + match self.snapshot_update { + SnapshotUpdateSetting::Always => SnapshotUpdateBehavior::InPlace, + SnapshotUpdateSetting::Auto => { if is_ci() { - SnapshotUpdate::NoUpdate + SnapshotUpdateBehavior::NoUpdate } else { - SnapshotUpdate::NewFile + SnapshotUpdateBehavior::NewFile } } - "always" | "1" => SnapshotUpdate::InPlace, - "new" => SnapshotUpdate::NewFile, - "unseen" => { + SnapshotUpdateSetting::Unseen => { if unseen { - SnapshotUpdate::NewFile + SnapshotUpdateBehavior::NewFile } else { - SnapshotUpdate::InPlace + SnapshotUpdateBehavior::InPlace } } - "no" => SnapshotUpdate::NoUpdate, - _ => panic!("invalid value for INSTA_UPDATE"), + SnapshotUpdateSetting::New => SnapshotUpdateBehavior::NewFile, + SnapshotUpdateSetting::No => SnapshotUpdateBehavior::NoUpdate, } } + + /// Returns the value of glob_fail_fast + #[allow(unused)] + pub fn glob_fail_fast(&self) -> bool { + self.glob_fail_fast + } } /// Returns the cargo workspace for a manifest @@ -210,3 +248,17 @@ pub fn memoize_snapshot_file(snapshot_file: &Path) { .unwrap(); } } + +fn resolve<'a>(value: &'a Content, path: &[&str]) -> Option<&'a Content> { + path.iter() + .try_fold(value, |node, segment| match node.resolve_inner() { + Content::Map(fields) => fields + .iter() + .find(|x| x.0.as_str() == Some(segment)) + .map(|x| &x.1), + Content::Struct(_, fields) | Content::StructVariant(_, _, _, fields) => { + fields.iter().find(|x| x.0 == *segment).map(|x| &x.1) + } + _ => None, + }) +} diff --git a/src/glob.rs b/src/glob.rs index a8685d51..dd0eb9ce 100644 --- a/src/glob.rs +++ b/src/glob.rs @@ -5,6 +5,7 @@ use std::sync::Mutex; use globset::{GlobBuilder, GlobMatcher}; use walkdir::WalkDir; +use crate::env::get_tool_config; use crate::settings::Settings; use crate::utils::style; @@ -37,7 +38,7 @@ lazy_static::lazy_static! { }; } -pub fn glob_exec(base: &Path, pattern: &str, mut f: F) { +pub fn glob_exec(manifest_dir: &str, base: &Path, pattern: &str, mut f: F) { let glob = GlobBuilder::new(pattern) .case_insensitive(true) .literal_separator(true) @@ -52,7 +53,7 @@ pub fn glob_exec(base: &Path, pattern: &str, mut f: F) { GLOB_STACK.lock().unwrap().push(GlobCollector { failed: 0, show_insta_hint: false, - fail_fast: std::env::var("INSTA_GLOB_FAIL_FAST").as_deref() == Ok("1"), + fail_fast: get_tool_config(manifest_dir).glob_fail_fast(), }); // step 1: collect all matching files diff --git a/src/lib.rs b/src/lib.rs index 3a1ea32a..e346ef88 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -185,7 +185,27 @@ //! # Settings //! //! There are some settings that can be changed on a per-thread (and thus -//! per-test) basis. For more information see [Settings]. +//! per-test) basis. For more information see [Settings]. Additionally +//! there is a insta [tool config](#tool-config) for global settings. +//! +//! # Tool Config +//! +//! Insta will load a file `.config/insta.yaml` with settings that change the +//! behavior of insta between runs. The following config variables exist: +//! +//! ```yaml +//! behavior: +//! # also set by INSTA_FORCE_UPDATE +//! force_update: true/false +//! # also set by INSTA_FORCE_PASS +//! force_pass: true/false +//! # also set by INSTA_OUTPUT +//! output: "diff" | "summary" | "minimal" | "none" +//! # also set by INSTA_UPDATE +//! update: "auto" | "always" | "new" | "unseen" | "no" +//! # also set by INSTA_GLOB_FAIL_FAST +//! glob_fail_fast: true/false +//! ``` //! //! # Optional: Faster Runs //! diff --git a/src/macros.rs b/src/macros.rs index 356d1531..e84526ee 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -549,6 +549,6 @@ macro_rules! glob { .unwrap() .canonicalize() .unwrap_or_else(|e| panic!("failed to canonicalize insta::glob! base path: {}", e)); - $crate::_macro_support::glob_exec(&base, $glob, $closure); + $crate::_macro_support::glob_exec(env!("CARGO_MANIFEST_DIR"), &base, $glob, $closure); }}; } diff --git a/src/runtime.rs b/src/runtime.rs index e81b4551..c6c943c2 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -8,8 +8,8 @@ use std::str; use std::sync::{Arc, Mutex}; use crate::env::{ - get_cargo_workspace, get_tool_config, memoize_snapshot_file, OutputBehavior, SnapshotUpdate, - ToolConfig, + get_cargo_workspace, get_tool_config, memoize_snapshot_file, OutputBehavior, + SnapshotUpdateBehavior, ToolConfig, }; use crate::output::{print_snapshot_diff_with_title, print_snapshot_summary_with_title}; use crate::settings::Settings; @@ -345,16 +345,16 @@ impl<'a> SnapshotAssertionContext<'a> { pub fn update_snapshot( &self, new_snapshot: Snapshot, - ) -> Result> { + ) -> Result> { let unseen = self .snapshot_file .as_ref() .map_or(false, |x| fs::metadata(x).is_ok()); - let should_print = self.tool_config.get_output_behavior() != OutputBehavior::Nothing; - let snapshot_update = self.tool_config.get_snapshot_update_behavior(unseen); + let should_print = self.tool_config.output_behavior() != OutputBehavior::Nothing; + let snapshot_update = self.tool_config.snapshot_update_behavior(unseen); match snapshot_update { - SnapshotUpdate::InPlace => { + SnapshotUpdateBehavior::InPlace => { if let Some(ref snapshot_file) = self.snapshot_file { new_snapshot.save(snapshot_file)?; if should_print { @@ -380,7 +380,7 @@ impl<'a> SnapshotAssertionContext<'a> { ); } } - SnapshotUpdate::NewFile => { + SnapshotUpdateBehavior::NewFile => { if let Some(ref snapshot_file) = self.snapshot_file { let mut new_path = snapshot_file.to_path_buf(); new_path.set_extension("snap.new"); @@ -410,7 +410,7 @@ impl<'a> SnapshotAssertionContext<'a> { .save(self.pending_snapshots_path.as_ref().unwrap())?; } } - SnapshotUpdate::NoUpdate => {} + SnapshotUpdateBehavior::NoUpdate => {} } Ok(snapshot_update) @@ -430,7 +430,7 @@ fn prevent_inline_duplicate(function_name: &str, assertion_file: &str, assertion /// This prints the information about the snapshot fn print_snapshot_info(ctx: &SnapshotAssertionContext, new_snapshot: &Snapshot) { - match ctx.tool_config.get_output_behavior() { + match ctx.tool_config.output_behavior() { OutputBehavior::Summary => { print_snapshot_summary_with_title( ctx.cargo_workspace.as_path(), @@ -466,7 +466,7 @@ macro_rules! print_or_panic { } /// Finalizes the assertion based on the update result. -fn finalize_assertion(ctx: &SnapshotAssertionContext, update_result: SnapshotUpdate) { +fn finalize_assertion(ctx: &SnapshotAssertionContext, update_result: SnapshotUpdateBehavior) { // if we are in glob mode, we want to adjust the finalization // so that we do not show the hints immediately. let fail_fast = { @@ -485,8 +485,8 @@ fn finalize_assertion(ctx: &SnapshotAssertionContext, update_result: SnapshotUpd }; if fail_fast - && update_result == SnapshotUpdate::NewFile - && ctx.tool_config.get_output_behavior() != OutputBehavior::Nothing + && update_result == SnapshotUpdateBehavior::NewFile + && ctx.tool_config.output_behavior() != OutputBehavior::Nothing && !ctx.is_doctest { println!( @@ -495,8 +495,8 @@ fn finalize_assertion(ctx: &SnapshotAssertionContext, update_result: SnapshotUpd ); } - if update_result != SnapshotUpdate::InPlace && !ctx.tool_config.force_pass() { - if fail_fast && ctx.tool_config.get_output_behavior() != OutputBehavior::Nothing { + if update_result != SnapshotUpdateBehavior::InPlace && !ctx.tool_config.force_pass() { + if fail_fast && ctx.tool_config.output_behavior() != OutputBehavior::Nothing { println!( "{hint}", hint = style( @@ -514,8 +514,8 @@ fn finalize_assertion(ctx: &SnapshotAssertionContext, update_result: SnapshotUpd let mut stack = crate::glob::GLOB_STACK.lock().unwrap(); if let Some(glob_collector) = stack.last_mut() { glob_collector.failed += 1; - if update_result == SnapshotUpdate::NewFile - && ctx.tool_config.get_output_behavior() != OutputBehavior::Nothing + if update_result == SnapshotUpdateBehavior::NewFile + && ctx.tool_config.output_behavior() != OutputBehavior::Nothing { glob_collector.show_insta_hint = true; } From 33589779b7f09ea3141706b2cf8ec87c88fd290c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 2 Jan 2023 22:48:36 +0100 Subject: [PATCH 3/3] Reuse tool config in cargo-insta --- cargo-insta/src/cli.rs | 103 ++++++++++++++++----------------- src/env.rs | 128 ++++++++++++++++++++++++++++------------- src/lib.rs | 6 ++ src/runtime.rs | 6 +- 4 files changed, 147 insertions(+), 96 deletions(-) diff --git a/cargo-insta/src/cli.rs b/cargo-insta/src/cli.rs index a57057e0..1469934a 100644 --- a/cargo-insta/src/cli.rs +++ b/cargo-insta/src/cli.rs @@ -9,7 +9,9 @@ use std::{io, process}; 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}; +use insta::_cargo_insta_support::{ + print_snapshot, print_snapshot_diff, SnapshotUpdate, TestRunner, ToolConfig, +}; use serde::Serialize; use structopt::clap::AppSettings; use structopt::StructOpt; @@ -304,6 +306,7 @@ enum SnapshotKey<'a> { } struct LocationInfo<'a> { + tool_config: ToolConfig, workspace_root: PathBuf, packages: Option>, exts: Vec<&'a str>, @@ -343,6 +346,7 @@ fn handle_target_args(target_args: &TargetArgs) -> Result, Box< if let Some(workspace_root) = workspace_root { Ok(LocationInfo { + tool_config: ToolConfig::from_workspace(&workspace_root), workspace_root: workspace_root.to_owned(), packages: None, exts, @@ -352,6 +356,7 @@ fn handle_target_args(target_args: &TargetArgs) -> Result, Box< let metadata = get_package_metadata(manifest_path.as_ref().map(|x| x.as_path()))?; let packages = find_packages(&metadata, target_args.all || target_args.workspace)?; Ok(LocationInfo { + tool_config: ToolConfig::from_workspace(metadata.workspace_root()), workspace_root: metadata.workspace_root().to_path_buf(), packages: Some(packages), exts, @@ -384,16 +389,20 @@ fn load_snapshot_containers<'a>( Ok(snapshot_containers) } -fn process_snapshots(cmd: ProcessCommand, op: Option) -> Result<(), Box> { +fn process_snapshots( + quiet: bool, + snapshot_filter: Option<&[String]>, + loc: LocationInfo<'_>, + op: Option, +) -> Result<(), Box> { let term = Term::stdout(); - let loc = handle_target_args(&cmd.target_args)?; let mut snapshot_containers = load_snapshot_containers(&loc)?; let snapshot_count = snapshot_containers.iter().map(|x| x.0.len()).sum(); if snapshot_count == 0 { - if !cmd.quiet { + if !quiet { println!("{}: no snapshots to review", style("done").bold()); if !loc.no_ignore { println!("{}: {}", style("warning").yellow().bold(), IGNORE_MESSAGE); @@ -413,7 +422,7 @@ fn process_snapshots(cmd: ProcessCommand, op: Option) -> Result<(), B let snapshot_file = snapshot_container.snapshot_file().map(|x| x.to_path_buf()); for snapshot_ref in snapshot_container.iter_snapshots() { // if a filter is provided, check if the snapshot reference is included - if let Some(ref filter) = cmd.snapshot_filter { + if let Some(ref filter) = snapshot_filter { let key = if let Some(line) = snapshot_ref.line { format!("{}:{}", target_file.display(), line) } else { @@ -462,7 +471,7 @@ fn process_snapshots(cmd: ProcessCommand, op: Option) -> Result<(), B term.clear_screen()?; } - if !cmd.quiet { + if !quiet { println!("{}", style("insta review finished").bold()); if !accepted.is_empty() { println!("{}:", style("accepted").green()); @@ -545,63 +554,36 @@ fn make_deletion_walker(loc: &LocationInfo, package: Option<&str>) -> Walk { .build() } -#[derive(Clone, Copy)] -enum TestRunner { - CargoTest, - Nextest, -} - -fn detect_test_runner(preference: Option<&str>) -> Result> { - // fall back to INSTA_TEST_RUNNER env var if no preference is given - let preference = preference - .map(Cow::Borrowed) - .or_else(|| env::var("INSTA_TEST_RUNNER").ok().map(Cow::Owned)) - .unwrap_or(Cow::Borrowed("auto")); - - match &preference as &str { - // auto for now defaults to cargo-test still - "auto" | "cargo-test" => Ok(TestRunner::CargoTest), - "nextest" => Ok(TestRunner::Nextest), - _ => Err(err_msg("invalid test runner preference")), - } -} - fn test_run(mut cmd: TestCommand, color: &str) -> Result<(), Box> { - match env::var("INSTA_UPDATE").ok().as_deref() { - Some("auto") | Some("new") => {} - Some("always") => { + let loc = handle_target_args(&cmd.target_args)?; + match loc.tool_config.snapshot_update() { + SnapshotUpdate::Auto | SnapshotUpdate::New | SnapshotUpdate::No => {} + SnapshotUpdate::Always => { if !cmd.accept && !cmd.accept_unseen && !cmd.review { cmd.review = false; cmd.accept = true; } } - Some("unseen") => { + SnapshotUpdate::Unseen => { if !cmd.accept { cmd.accept_unseen = true; cmd.review = true; cmd.accept = false; } } - // silently ignored always - None | Some("") | Some("no") => {} - _ => { - return Err(err_msg("invalid value for INSTA_UPDATE")); - } } - let test_runner = detect_test_runner(cmd.test_runner.as_deref())?; + 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)?; if !cmd.keep_pending { - process_snapshots( - ProcessCommand { - target_args: cmd.target_args.clone(), - snapshot_filter: None, - quiet: true, - }, - Some(Operation::Reject), - )?; + process_snapshots(true, None, loc, Some(Operation::Reject))?; } let status = proc.status()?; @@ -690,11 +672,9 @@ fn test_run(mut cmd: TestCommand, color: &str) -> Result<(), Box> { if cmd.review || cmd.accept { process_snapshots( - ProcessCommand { - target_args: cmd.target_args.clone(), - snapshot_filter: None, - quiet: false, - }, + false, + None, + handle_target_args(&cmd.target_args)?, if cmd.accept { Some(Operation::Accept) } else { @@ -736,7 +716,7 @@ fn prepare_test_runner<'snapshot_ref>( snapshot_ref_file: Option<&'snapshot_ref Path>, ) -> Result<(process::Command, Option>), Box> { let mut proc = match test_runner { - TestRunner::CargoTest => { + TestRunner::CargoTest | TestRunner::Auto => { let mut proc = process::Command::new(get_cargo()); proc.arg("test"); proc @@ -916,9 +896,24 @@ pub fn run() -> Result<(), Box> { let color = opts.color.as_ref().map(|x| x.as_str()).unwrap_or("auto"); handle_color(color)?; match opts.command { - 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::Review(cmd) => process_snapshots( + cmd.quiet, + cmd.snapshot_filter.as_deref(), + handle_target_args(&cmd.target_args)?, + None, + ), + Command::Accept(cmd) => process_snapshots( + cmd.quiet, + cmd.snapshot_filter.as_deref(), + handle_target_args(&cmd.target_args)?, + Some(Operation::Accept), + ), + Command::Reject(cmd) => process_snapshots( + cmd.quiet, + cmd.snapshot_filter.as_deref(), + handle_target_args(&cmd.target_args)?, + Some(Operation::Reject), + ), Command::Test(cmd) => test_run(cmd, color), Command::Show(cmd) => show_cmd(cmd), Command::PendingSnapshots(cmd) => pending_snapshots_cmd(cmd), diff --git a/src/env.rs b/src/env.rs index b63277e4..089562f2 100644 --- a/src/env.rs +++ b/src/env.rs @@ -1,6 +1,7 @@ use std::collections::BTreeMap; use std::io::{self, Write}; use std::path::{Path, PathBuf}; +use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::{env, fs}; @@ -17,20 +18,17 @@ pub fn get_tool_config(manifest_dir: &str) -> Arc { if let Some(rv) = configs.get(manifest_dir) { return rv.clone(); } - let config = Arc::new(ToolConfig::load(manifest_dir)); + let config = Arc::new(ToolConfig::from_manifest_dir(manifest_dir)); configs.insert(manifest_dir.to_string(), config.clone()); config } -/// How snapshots are supposed to be updated +/// The test runner to use. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum SnapshotUpdateBehavior { - /// Snapshots are updated in-place - InPlace, - /// Snapshots are placed in a new file with a .new suffix - NewFile, - /// Snapshots are not updated at all. - NoUpdate, +pub enum TestRunner { + Auto, + CargoTest, + Nextest, } /// Controls how information is supposed to be displayed. @@ -47,7 +45,7 @@ pub enum OutputBehavior { } #[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum SnapshotUpdateSetting { +pub enum SnapshotUpdate { Always, Auto, Unseen, @@ -61,16 +59,22 @@ pub struct ToolConfig { force_update_snapshots: bool, force_pass: bool, output: OutputBehavior, - snapshot_update: SnapshotUpdateSetting, + snapshot_update: SnapshotUpdate, + test_runner: TestRunner, #[allow(unused)] glob_fail_fast: bool, } impl ToolConfig { /// Loads the tool config for a specific manifest. - pub fn load(manifest_dir: &str) -> ToolConfig { - let cargo_workspace = get_cargo_workspace(manifest_dir); - let path = cargo_workspace.join(".config/insta.yaml"); + pub fn from_manifest_dir(manifest_dir: &str) -> ToolConfig { + ToolConfig::from_workspace(&get_cargo_workspace(manifest_dir)) + } + + /// Loads the tool config from a cargo workspace. + pub fn from_workspace(workspace_dir: &Path) -> ToolConfig { + // TODO: make this return a result for better error reporting in cargo-insta + let path = workspace_dir.join(".config/insta.yaml"); let values = match fs::read_to_string(path) { Ok(s) => yaml::parse_str(&s).expect("failed to deserialize tool config"), Err(err) if matches!(err.kind(), io::ErrorKind::NotFound) => { @@ -123,15 +127,26 @@ impl ToolConfig { Ok(val) => val, }; match val { - "auto" => SnapshotUpdateSetting::Auto, - "always" | "1" => SnapshotUpdateSetting::Always, - "new" => SnapshotUpdateSetting::New, - "unseen" => SnapshotUpdateSetting::Unseen, - "no" => SnapshotUpdateSetting::No, + "auto" => SnapshotUpdate::Auto, + "always" | "1" => SnapshotUpdate::Always, + "new" => SnapshotUpdate::New, + "unseen" => SnapshotUpdate::Unseen, + "no" => SnapshotUpdate::No, _ => panic!("invalid value for INSTA_UPDATE"), } }; + let test_runner = { + let env_var = env::var("INSTA_TEST_RUNNER"); + TestRunner::from_str(match env_var.as_deref() { + Err(_) | Ok("") => resolve(&values, &["test", "runner"]) + .and_then(|x| x.as_str()) + .unwrap_or("auto"), + Ok(val) => val, + }) + .expect("invalid value for INSTA_TEST_RUNNER") + }; + let glob_fail_fast = match env::var("INSTA_GLOB_FAIL_FAST").as_deref() { Err(_) | Ok("") => resolve(&values, &["behavior", "glob_fail_fast"]) .and_then(|x| x.as_bool()) @@ -146,6 +161,7 @@ impl ToolConfig { force_pass, output, snapshot_update, + test_runner, glob_fail_fast, } } @@ -166,26 +182,13 @@ impl ToolConfig { } /// Returns the intended snapshot update behavior. - pub fn snapshot_update_behavior(&self, unseen: bool) -> SnapshotUpdateBehavior { - match self.snapshot_update { - SnapshotUpdateSetting::Always => SnapshotUpdateBehavior::InPlace, - SnapshotUpdateSetting::Auto => { - if is_ci() { - SnapshotUpdateBehavior::NoUpdate - } else { - SnapshotUpdateBehavior::NewFile - } - } - SnapshotUpdateSetting::Unseen => { - if unseen { - SnapshotUpdateBehavior::NewFile - } else { - SnapshotUpdateBehavior::InPlace - } - } - SnapshotUpdateSetting::New => SnapshotUpdateBehavior::NewFile, - SnapshotUpdateSetting::No => SnapshotUpdateBehavior::NoUpdate, - } + pub fn snapshot_update(&self) -> SnapshotUpdate { + self.snapshot_update + } + + /// Returns the intended test runner + pub fn test_runner(&self) -> TestRunner { + self.test_runner } /// Returns the value of glob_fail_fast @@ -195,6 +198,40 @@ impl ToolConfig { } } +/// How snapshots are supposed to be updated +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SnapshotUpdateBehavior { + /// Snapshots are updated in-place + InPlace, + /// Snapshots are placed in a new file with a .new suffix + NewFile, + /// Snapshots are not updated at all. + NoUpdate, +} + +/// Returns the intended snapshot update behavior. +pub fn snapshot_update_behavior(tool_config: &ToolConfig, unseen: bool) -> SnapshotUpdateBehavior { + match tool_config.snapshot_update { + SnapshotUpdate::Always => SnapshotUpdateBehavior::InPlace, + SnapshotUpdate::Auto => { + if is_ci() { + SnapshotUpdateBehavior::NoUpdate + } else { + SnapshotUpdateBehavior::NewFile + } + } + SnapshotUpdate::Unseen => { + if unseen { + SnapshotUpdateBehavior::NewFile + } else { + SnapshotUpdateBehavior::InPlace + } + } + SnapshotUpdate::New => SnapshotUpdateBehavior::NewFile, + SnapshotUpdate::No => SnapshotUpdateBehavior::NoUpdate, + } +} + /// Returns the cargo workspace for a manifest pub fn get_cargo_workspace(manifest_dir: &str) -> Arc { // we really do not care about poisoning here. @@ -235,6 +272,19 @@ pub fn get_cargo_workspace(manifest_dir: &str) -> Arc { } } +impl FromStr for TestRunner { + type Err = (); + + fn from_str(value: &str) -> Result { + match value { + "auto" => Ok(TestRunner::Auto), + "cargo-test" => Ok(TestRunner::CargoTest), + "nextest" => Ok(TestRunner::Nextest), + _ => 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") { diff --git a/src/lib.rs b/src/lib.rs index e346ef88..c01743de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -205,6 +205,11 @@ //! update: "auto" | "always" | "new" | "unseen" | "no" //! # also set by INSTA_GLOB_FAIL_FAST //! glob_fail_fast: true/false +//! +//! # these are used by cargo insta test +//! test: +//! # also set by INSTA_TEST_RUNNER +//! runner: "auto" | "cargo-test" | "nextest" //! ``` //! //! # Optional: Faster Runs @@ -271,6 +276,7 @@ pub mod internals { // exported for cargo-insta only #[doc(hidden)] pub mod _cargo_insta_support { + pub use crate::env::{OutputBehavior, SnapshotUpdate, TestRunner, ToolConfig}; pub use crate::{ output::{print_snapshot, print_snapshot_diff}, snapshot::PendingInlineSnapshot, diff --git a/src/runtime.rs b/src/runtime.rs index c6c943c2..1e54f624 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -8,8 +8,8 @@ use std::str; use std::sync::{Arc, Mutex}; use crate::env::{ - get_cargo_workspace, get_tool_config, memoize_snapshot_file, OutputBehavior, - SnapshotUpdateBehavior, ToolConfig, + get_cargo_workspace, get_tool_config, memoize_snapshot_file, snapshot_update_behavior, + OutputBehavior, SnapshotUpdateBehavior, ToolConfig, }; use crate::output::{print_snapshot_diff_with_title, print_snapshot_summary_with_title}; use crate::settings::Settings; @@ -351,7 +351,7 @@ impl<'a> SnapshotAssertionContext<'a> { .as_ref() .map_or(false, |x| fs::metadata(x).is_ok()); let should_print = self.tool_config.output_behavior() != OutputBehavior::Nothing; - let snapshot_update = self.tool_config.snapshot_update_behavior(unseen); + let snapshot_update = snapshot_update_behavior(&self.tool_config, unseen); match snapshot_update { SnapshotUpdateBehavior::InPlace => {