| use std::cell::RefCell; |
| use std::future::Future; |
| use std::mem; |
| use std::path::{Path, PathBuf}; |
| use std::pin::Pin; |
| use std::sync::Arc; |
| use std::task::{Context, Poll}; |
| |
| #[cfg(feature = "serde")] |
| use serde::{de::value::Error as ValueError, Serialize}; |
| |
| use crate::content::Content; |
| #[cfg(feature = "serde")] |
| use crate::content::ContentSerializer; |
| #[cfg(feature = "filters")] |
| use crate::filters::Filters; |
| #[cfg(feature = "redactions")] |
| use crate::redaction::{dynamic_redaction, sorted_redaction, ContentPath, Redaction, Selector}; |
| |
| lazy_static::lazy_static! { |
| static ref DEFAULT_SETTINGS: Arc<ActualSettings> = { |
| Arc::new(ActualSettings { |
| sort_maps: false, |
| snapshot_path: "snapshots".into(), |
| snapshot_suffix: "".into(), |
| input_file: None, |
| description: None, |
| info: None, |
| omit_expression: false, |
| prepend_module_to_snapshot: true, |
| #[cfg(feature = "redactions")] |
| redactions: Redactions::default(), |
| #[cfg(feature = "filters")] |
| filters: Filters::default(), |
| #[cfg(feature = "glob")] |
| allow_empty_glob: false, |
| }) |
| }; |
| } |
| thread_local!(static CURRENT_SETTINGS: RefCell<Settings> = RefCell::new(Settings::new())); |
| |
| /// Represents stored redactions. |
| #[cfg(feature = "redactions")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "redactions")))] |
| #[derive(Clone, Default)] |
| pub struct Redactions(Vec<(Selector<'static>, Arc<Redaction>)>); |
| |
| #[cfg(feature = "redactions")] |
| impl<'a> From<Vec<(&'a str, Redaction)>> for Redactions { |
| fn from(value: Vec<(&'a str, Redaction)>) -> Redactions { |
| Redactions( |
| value |
| .into_iter() |
| .map(|x| (Selector::parse(x.0).unwrap().make_static(), Arc::new(x.1))) |
| .collect(), |
| ) |
| } |
| } |
| |
| #[derive(Clone)] |
| #[doc(hidden)] |
| pub struct ActualSettings { |
| pub sort_maps: bool, |
| pub snapshot_path: PathBuf, |
| pub snapshot_suffix: String, |
| pub input_file: Option<PathBuf>, |
| pub description: Option<String>, |
| pub info: Option<Content>, |
| pub omit_expression: bool, |
| pub prepend_module_to_snapshot: bool, |
| #[cfg(feature = "redactions")] |
| pub redactions: Redactions, |
| #[cfg(feature = "filters")] |
| pub filters: Filters, |
| #[cfg(feature = "glob")] |
| pub allow_empty_glob: bool, |
| } |
| |
| impl ActualSettings { |
| pub fn sort_maps(&mut self, value: bool) { |
| self.sort_maps = value; |
| } |
| |
| pub fn snapshot_path<P: AsRef<Path>>(&mut self, path: P) { |
| self.snapshot_path = path.as_ref().to_path_buf(); |
| } |
| |
| pub fn snapshot_suffix<I: Into<String>>(&mut self, suffix: I) { |
| self.snapshot_suffix = suffix.into(); |
| } |
| |
| pub fn input_file<P: AsRef<Path>>(&mut self, p: P) { |
| self.input_file = Some(p.as_ref().to_path_buf()); |
| } |
| |
| pub fn description<S: Into<String>>(&mut self, value: S) { |
| self.description = Some(value.into()); |
| } |
| |
| #[cfg(feature = "serde")] |
| pub fn info<S: Serialize>(&mut self, s: &S) { |
| let serializer = ContentSerializer::<ValueError>::new(); |
| let content = Serialize::serialize(s, serializer).unwrap(); |
| self.info = Some(content); |
| } |
| |
| pub fn raw_info(&mut self, content: &Content) { |
| self.info = Some(content.to_owned()); |
| } |
| |
| pub fn omit_expression(&mut self, value: bool) { |
| self.omit_expression = value; |
| } |
| |
| pub fn prepend_module_to_snapshot(&mut self, value: bool) { |
| self.prepend_module_to_snapshot = value; |
| } |
| |
| #[cfg(feature = "redactions")] |
| pub fn redactions<R: Into<Redactions>>(&mut self, r: R) { |
| self.redactions = r.into(); |
| } |
| |
| #[cfg(feature = "filters")] |
| pub fn filters<F: Into<Filters>>(&mut self, f: F) { |
| self.filters = f.into(); |
| } |
| |
| #[cfg(feature = "glob")] |
| pub fn allow_empty_glob(&mut self, value: bool) { |
| self.allow_empty_glob = value; |
| } |
| } |
| |
| /// Configures how insta operates at test time. |
| /// |
| /// Settings are always bound to a thread and some default settings are always |
| /// available. These settings can be changed and influence how insta behaves on |
| /// that thread. They can either temporarily or permanently changed. |
| /// |
| /// This can be used to influence how the snapshot macros operate. |
| /// For instance it can be useful to force ordering of maps when |
| /// unordered structures are used through settings. |
| /// |
| /// Some of the settings can be changed but shouldn't as it will make it harder |
| /// for tools like cargo-insta or an editor integration to locate the snapshot |
| /// files. |
| /// |
| /// Settings can also be configured with the [`with_settings!`] macro. |
| /// |
| /// Example: |
| /// |
| /// ```ignore |
| /// use insta; |
| /// |
| /// let mut settings = insta::Settings::clone_current(); |
| /// settings.set_sort_maps(true); |
| /// settings.bind(|| { |
| /// // runs the assertion with the changed settings enabled |
| /// insta::assert_snapshot!(...); |
| /// }); |
| /// ``` |
| #[derive(Clone)] |
| pub struct Settings { |
| inner: Arc<ActualSettings>, |
| } |
| |
| impl Default for Settings { |
| fn default() -> Settings { |
| Settings { |
| inner: DEFAULT_SETTINGS.clone(), |
| } |
| } |
| } |
| |
| impl Settings { |
| /// Returns the default settings. |
| /// |
| /// It's recommended to use `clone_current` instead so that |
| /// already applied modifications are not discarded. |
| pub fn new() -> Settings { |
| Settings::default() |
| } |
| |
| /// Returns a copy of the current settings. |
| pub fn clone_current() -> Settings { |
| Settings::with(|x| x.clone()) |
| } |
| |
| /// Internal helper for macros |
| #[doc(hidden)] |
| pub fn _private_inner_mut(&mut self) -> &mut ActualSettings { |
| Arc::make_mut(&mut self.inner) |
| } |
| |
| /// Enables forceful sorting of maps before serialization. |
| /// |
| /// Note that this only applies to snapshots that undergo serialization |
| /// (eg: does not work for `assert_debug_snapshot!`.) |
| /// |
| /// The default value is `false`. |
| pub fn set_sort_maps(&mut self, value: bool) { |
| self._private_inner_mut().sort_maps = value; |
| } |
| |
| /// Returns the current value for map sorting. |
| pub fn sort_maps(&self) -> bool { |
| self.inner.sort_maps |
| } |
| |
| /// Disables prepending of modules to the snapshot filename. |
| /// |
| /// By default the filename of a snapshot is `<module>__<name>.snap`. |
| /// Setting this flag to `false` changes the snapshot filename to just |
| /// `<name>.snap`. |
| /// |
| /// The default value is `true`. |
| pub fn set_prepend_module_to_snapshot(&mut self, value: bool) { |
| self._private_inner_mut().prepend_module_to_snapshot(value); |
| } |
| |
| /// Returns the current value for module name prepending. |
| pub fn prepend_module_to_snapshot(&self) -> bool { |
| self.inner.prepend_module_to_snapshot |
| } |
| |
| /// Allows the [`glob!`] macro to succeed if it matches no files. |
| /// |
| /// By default the glob macro will fail the test if it does not find |
| /// any files to prevent accidental typos. This can be disabled when |
| /// fixtures should be conditional. |
| /// |
| /// The default value is `false`. |
| #[cfg(feature = "glob")] |
| pub fn set_allow_empty_glob(&mut self, value: bool) { |
| self._private_inner_mut().allow_empty_glob(value); |
| } |
| |
| /// Returns the current value for the empty glob setting. |
| #[cfg(feature = "glob")] |
| pub fn allow_empty_glob(&self) -> bool { |
| self.inner.allow_empty_glob |
| } |
| |
| /// Sets the snapshot suffix. |
| /// |
| /// The snapshot suffix is added to all snapshot names with an `@` sign |
| /// between. For instance if the snapshot suffix is set to `"foo"` and |
| /// the snapshot would be named `"snapshot"` it turns into `"snapshot@foo"`. |
| /// This is useful to separate snapshots if you want to use test |
| /// parameterization. |
| pub fn set_snapshot_suffix<I: Into<String>>(&mut self, suffix: I) { |
| self._private_inner_mut().snapshot_suffix(suffix); |
| } |
| |
| /// Removes the snapshot suffix. |
| pub fn remove_snapshot_suffix(&mut self) { |
| self.set_snapshot_suffix(""); |
| } |
| |
| /// Returns the current snapshot suffix. |
| pub fn snapshot_suffix(&self) -> Option<&str> { |
| if self.inner.snapshot_suffix.is_empty() { |
| None |
| } else { |
| Some(&self.inner.snapshot_suffix) |
| } |
| } |
| |
| /// Sets the input file reference. |
| /// |
| /// This value is completely unused by the snapshot testing system but |
| /// it lets you store some meta data with a snapshot that refers you back |
| /// to the input file. The path stored here is made relative to the |
| /// workspace root before storing with the snapshot. |
| pub fn set_input_file<P: AsRef<Path>>(&mut self, p: P) { |
| self._private_inner_mut().input_file(p); |
| } |
| |
| /// Removes the input file reference. |
| pub fn remove_input_file(&mut self) { |
| self._private_inner_mut().input_file = None; |
| } |
| |
| /// Returns the current input file reference. |
| pub fn input_file(&self) -> Option<&Path> { |
| self.inner.input_file.as_deref() |
| } |
| |
| /// Sets the description. |
| /// |
| /// The description is stored alongside the snapshot and will be displayed |
| /// in the diff UI. When a snapshot is captured the Rust expression for that |
| /// snapshot is always retained. However sometimes that information is not |
| /// super useful by itself, particularly when working with loops and generated |
| /// tests. In that case the `description` can be set as extra information. |
| /// |
| /// See also [`set_info`](Self::set_info). |
| pub fn set_description<S: Into<String>>(&mut self, value: S) { |
| self._private_inner_mut().description(value); |
| } |
| |
| /// Removes the description. |
| pub fn remove_description(&mut self) { |
| self._private_inner_mut().description = None; |
| } |
| |
| /// Returns the current description |
| pub fn description(&self) -> Option<&str> { |
| self.inner.description.as_deref() |
| } |
| |
| /// Sets the info. |
| /// |
| /// The `info` is similar to `description` but for structured data. This is |
| /// stored with the snapshot and shown in the review UI. This for instance |
| /// can be used to show extended information that can make a reviewer better |
| /// understand what the snapshot is supposed to be testing. |
| /// |
| /// As an example the input parameters to the function that creates the snapshot |
| /// can be persisted here. |
| /// |
| /// Alternatively you can use [`set_raw_info`](Self::set_raw_info) instead. |
| #[cfg(feature = "serde")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] |
| pub fn set_info<S: Serialize>(&mut self, s: &S) { |
| self._private_inner_mut().info(s); |
| } |
| |
| /// Sets the info from a content object. |
| /// |
| /// This works like [`set_info`](Self::set_info) but does not require `serde`. |
| pub fn set_raw_info(&mut self, content: &Content) { |
| self._private_inner_mut().raw_info(content); |
| } |
| |
| /// Removes the info. |
| pub fn remove_info(&mut self) { |
| self._private_inner_mut().info = None; |
| } |
| |
| /// Returns the current info |
| pub(crate) fn info(&self) -> Option<&Content> { |
| self.inner.info.as_ref() |
| } |
| |
| /// Returns the current info |
| pub fn has_info(&self) -> bool { |
| self.inner.info.is_some() |
| } |
| |
| /// If set to true, does not retain the expression in the snapshot. |
| pub fn set_omit_expression(&mut self, value: bool) { |
| self._private_inner_mut().omit_expression(value); |
| } |
| |
| /// Returns true if expressions are omitted from snapshots. |
| pub fn omit_expression(&self) -> bool { |
| self.inner.omit_expression |
| } |
| |
| /// Registers redactions that should be applied. |
| /// |
| /// This can be useful if redactions must be shared across multiple |
| /// snapshots. |
| /// |
| /// Note that this only applies to snapshots that undergo serialization |
| /// (eg: does not work for `assert_debug_snapshot!`.) |
| #[cfg(feature = "redactions")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "redactions")))] |
| pub fn add_redaction<R: Into<Redaction>>(&mut self, selector: &str, replacement: R) { |
| self.add_redaction_impl(selector, replacement.into()) |
| } |
| |
| #[cfg(feature = "redactions")] |
| fn add_redaction_impl(&mut self, selector: &str, replacement: Redaction) { |
| self._private_inner_mut().redactions.0.push(( |
| Selector::parse(selector).unwrap().make_static(), |
| Arc::new(replacement), |
| )); |
| } |
| |
| /// Registers a replacement callback. |
| /// |
| /// This works similar to a redaction but instead of changing the value it |
| /// asserts the value at a certain place. This function is internally |
| /// supposed to call things like `assert_eq!`. |
| /// |
| /// This is a shortcut to `add_redaction(selector, dynamic_redaction(...))`; |
| #[cfg(feature = "redactions")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "redactions")))] |
| pub fn add_dynamic_redaction<I, F>(&mut self, selector: &str, func: F) |
| where |
| I: Into<Content>, |
| F: Fn(Content, ContentPath<'_>) -> I + Send + Sync + 'static, |
| { |
| self.add_redaction(selector, dynamic_redaction(func)); |
| } |
| |
| /// A special redaction that sorts a sequence or map. |
| /// |
| /// This is a shortcut to `add_redaction(selector, sorted_redaction())`. |
| #[cfg(feature = "redactions")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "redactions")))] |
| pub fn sort_selector(&mut self, selector: &str) { |
| self.add_redaction(selector, sorted_redaction()); |
| } |
| |
| /// Replaces the currently set redactions. |
| /// |
| /// The default set is empty. |
| #[cfg(feature = "redactions")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "redactions")))] |
| pub fn set_redactions<R: Into<Redactions>>(&mut self, redactions: R) { |
| self._private_inner_mut().redactions(redactions); |
| } |
| |
| /// Removes all redactions. |
| #[cfg(feature = "redactions")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "redactions")))] |
| pub fn clear_redactions(&mut self) { |
| self._private_inner_mut().redactions.0.clear(); |
| } |
| |
| /// Iterate over the redactions. |
| #[cfg(feature = "redactions")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "redactions")))] |
| pub(crate) fn iter_redactions(&self) -> impl Iterator<Item = (&Selector, &Redaction)> { |
| self.inner.redactions.0.iter().map(|(a, b)| (a, &**b)) |
| } |
| |
| /// Adds a new filter. |
| /// |
| /// Filters are similar to redactions but are applied as regex onto the final snapshot |
| /// value. This can be used to perform modifications to the snapshot string that would |
| /// be impossible to do with redactions because for instance the value is just a string. |
| /// |
| /// The first argument is the [`regex`] pattern to apply, the second is a replacement |
| /// string. The replacement string has the same functionality as the second argument |
| /// to [`Regex::replace`](regex::Regex::replace). |
| /// |
| /// This is useful to perform some cleanup procedures on the snapshot for unstable values. |
| /// |
| /// ```rust |
| /// # use insta::Settings; |
| /// # async fn foo() { |
| /// # let mut settings = Settings::new(); |
| /// settings.add_filter(r"\b[[:xdigit:]]{32}\b", "[UID]"); |
| /// # } |
| /// ``` |
| #[cfg(feature = "filters")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "filters")))] |
| pub fn add_filter<S: Into<String>>(&mut self, regex: &str, replacement: S) { |
| self._private_inner_mut().filters.add(regex, replacement); |
| } |
| |
| /// Replaces the currently set filters. |
| /// |
| /// The default set is empty. |
| #[cfg(feature = "filters")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "filters")))] |
| pub fn set_filters<F: Into<Filters>>(&mut self, filters: F) { |
| self._private_inner_mut().filters(filters); |
| } |
| |
| /// Removes all filters. |
| #[cfg(feature = "filters")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "filters")))] |
| pub fn clear_filters(&mut self) { |
| self._private_inner_mut().filters.clear(); |
| } |
| |
| /// Returns the current filters |
| #[cfg(feature = "filters")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "filters")))] |
| pub(crate) fn filters(&self) -> &Filters { |
| &self.inner.filters |
| } |
| |
| /// Sets the snapshot path. |
| /// |
| /// If not absolute it's relative to where the test is in. |
| /// |
| /// Defaults to `snapshots`. |
| pub fn set_snapshot_path<P: AsRef<Path>>(&mut self, path: P) { |
| self._private_inner_mut().snapshot_path(path); |
| } |
| |
| /// Returns the snapshot path. |
| pub fn snapshot_path(&self) -> &Path { |
| &self.inner.snapshot_path |
| } |
| |
| /// Runs a function with the current settings bound to the thread. |
| /// |
| /// This is an alternative to [`bind_to_scope`](Settings::bind_to_scope) |
| /// which does not require holding on to a drop guard. The return value |
| /// of the closure is passed through. |
| /// |
| /// ``` |
| /// # use insta::Settings; |
| /// let mut settings = Settings::clone_current(); |
| /// settings.set_sort_maps(true); |
| /// settings.bind(|| { |
| /// // do stuff here |
| /// }); |
| /// ``` |
| pub fn bind<F: FnOnce() -> R, R>(&self, f: F) -> R { |
| let _guard = self.bind_to_scope(); |
| f() |
| } |
| |
| /// Like `bind` but for futures. |
| /// |
| /// This lets you bind settings for the duration of a future like this: |
| /// |
| /// ```rust |
| /// # use insta::Settings; |
| /// # async fn foo() { |
| /// let settings = Settings::new(); |
| /// settings.bind_async(async { |
| /// // do assertions here |
| /// }).await; |
| /// # } |
| /// ``` |
| pub fn bind_async<F: Future<Output = T>, T>(&self, future: F) -> impl Future<Output = T> { |
| struct BindingFuture<F>(Arc<ActualSettings>, F); |
| |
| impl<F: Future> Future for BindingFuture<F> { |
| type Output = F::Output; |
| |
| fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> { |
| let inner = self.0.clone(); |
| let future = unsafe { self.map_unchecked_mut(|s| &mut s.1) }; |
| CURRENT_SETTINGS.with(|x| { |
| let old = { |
| let mut current = x.borrow_mut(); |
| let old = current.inner.clone(); |
| current.inner = inner; |
| old |
| }; |
| let rv = future.poll(cx); |
| let mut current = x.borrow_mut(); |
| current.inner = old; |
| rv |
| }) |
| } |
| } |
| |
| BindingFuture(self.inner.clone(), future) |
| } |
| |
| /// Binds the settings to the current thread and resets when the drop |
| /// guard is released. |
| /// |
| /// This is the recommended way to temporarily bind settings and replaces |
| /// the earlier [`bind_to_scope`](Settings::bind_to_scope) and relies on |
| /// drop guards. An alterantive is [`bind`](Settings::bind) which binds |
| /// for the duration of the block it wraps. |
| /// |
| /// ``` |
| /// # use insta::Settings; |
| /// let mut settings = Settings::clone_current(); |
| /// settings.set_sort_maps(true); |
| /// let _guard = settings.bind_to_scope(); |
| /// // do stuff here |
| /// ``` |
| pub fn bind_to_scope(&self) -> SettingsBindDropGuard { |
| CURRENT_SETTINGS.with(|x| { |
| let mut x = x.borrow_mut(); |
| let old = mem::replace(&mut x.inner, self.inner.clone()); |
| SettingsBindDropGuard(Some(old)) |
| }) |
| } |
| |
| /// Runs a function with the current settings. |
| pub(crate) fn with<R, F: FnOnce(&Settings) -> R>(f: F) -> R { |
| CURRENT_SETTINGS.with(|x| f(&x.borrow())) |
| } |
| } |
| |
| /// Returned from [`bind_to_scope`](Settings::bind_to_scope) |
| #[must_use = "The guard is immediately dropped so binding has no effect. Use `let _guard = ...` to bind it."] |
| pub struct SettingsBindDropGuard(Option<Arc<ActualSettings>>); |
| |
| impl Drop for SettingsBindDropGuard { |
| fn drop(&mut self) { |
| CURRENT_SETTINGS.with(|x| { |
| x.borrow_mut().inner = self.0.take().unwrap(); |
| }) |
| } |
| } |