insta/
settings.rs

1use once_cell::sync::Lazy;
2#[cfg(feature = "serde")]
3use serde::{de::value::Error as ValueError, Serialize};
4use std::cell::RefCell;
5use std::future::Future;
6use std::mem;
7use std::path::{Path, PathBuf};
8use std::pin::Pin;
9use std::sync::Arc;
10use std::task::{Context, Poll};
11
12use crate::content::Content;
13#[cfg(feature = "serde")]
14use crate::content::ContentSerializer;
15#[cfg(feature = "filters")]
16use crate::filters::Filters;
17#[cfg(feature = "redactions")]
18use crate::redaction::{dynamic_redaction, sorted_redaction, ContentPath, Redaction, Selector};
19
20static DEFAULT_SETTINGS: Lazy<Arc<ActualSettings>> = Lazy::new(|| {
21    Arc::new(ActualSettings {
22        sort_maps: false,
23        snapshot_path: "snapshots".into(),
24        snapshot_suffix: "".into(),
25        input_file: None,
26        description: None,
27        info: None,
28        omit_expression: false,
29        prepend_module_to_snapshot: true,
30        #[cfg(feature = "redactions")]
31        redactions: Redactions::default(),
32        #[cfg(feature = "filters")]
33        filters: Filters::default(),
34        #[cfg(feature = "glob")]
35        allow_empty_glob: false,
36    })
37});
38
39thread_local!(static CURRENT_SETTINGS: RefCell<Settings> = RefCell::new(Settings::new()));
40
41/// Represents stored redactions.
42#[cfg(feature = "redactions")]
43#[cfg_attr(docsrs, doc(cfg(feature = "redactions")))]
44#[derive(Clone, Default)]
45pub struct Redactions(Vec<(Selector<'static>, Arc<Redaction>)>);
46
47#[cfg(feature = "redactions")]
48impl<'a> From<Vec<(&'a str, Redaction)>> for Redactions {
49    fn from(value: Vec<(&'a str, Redaction)>) -> Redactions {
50        Redactions(
51            value
52                .into_iter()
53                .map(|x| (Selector::parse(x.0).unwrap().make_static(), Arc::new(x.1)))
54                .collect(),
55        )
56    }
57}
58
59#[cfg(feature = "redactions")]
60impl Redactions {
61    /// Applies all redactions to the given content.
62    pub(crate) fn apply_to_content(&self, mut content: Content) -> Content {
63        for (selector, redaction) in self.0.iter() {
64            content = selector.redact(content, redaction);
65        }
66        content
67    }
68}
69
70#[derive(Clone)]
71#[doc(hidden)]
72pub struct ActualSettings {
73    pub sort_maps: bool,
74    pub snapshot_path: PathBuf,
75    pub snapshot_suffix: String,
76    pub input_file: Option<PathBuf>,
77    pub description: Option<String>,
78    pub info: Option<Content>,
79    pub omit_expression: bool,
80    pub prepend_module_to_snapshot: bool,
81    #[cfg(feature = "redactions")]
82    pub redactions: Redactions,
83    #[cfg(feature = "filters")]
84    pub filters: Filters,
85    #[cfg(feature = "glob")]
86    pub allow_empty_glob: bool,
87}
88
89impl ActualSettings {
90    pub fn sort_maps(&mut self, value: bool) {
91        self.sort_maps = value;
92    }
93
94    pub fn snapshot_path<P: AsRef<Path>>(&mut self, path: P) {
95        self.snapshot_path = path.as_ref().to_path_buf();
96    }
97
98    pub fn snapshot_suffix<I: Into<String>>(&mut self, suffix: I) {
99        self.snapshot_suffix = suffix.into();
100    }
101
102    pub fn input_file<P: AsRef<Path>>(&mut self, p: P) {
103        self.input_file = Some(p.as_ref().to_path_buf());
104    }
105
106    pub fn description<S: Into<String>>(&mut self, value: S) {
107        self.description = Some(value.into());
108    }
109
110    #[cfg(feature = "serde")]
111    pub fn info<S: Serialize>(&mut self, s: &S) {
112        let serializer = ContentSerializer::<ValueError>::new();
113        let content = Serialize::serialize(s, serializer).unwrap();
114
115        // Apply redactions to metadata immediately when set. Unlike snapshot
116        // content (which is redacted lazily during serialization), metadata is
117        // redacted eagerly to ensure sensitive data never reaches the stored
118        // settings. The redacted content is then written to the snapshot file
119        // as-is without further redaction.
120        #[cfg(feature = "redactions")]
121        let content = self.redactions.apply_to_content(content);
122
123        self.info = Some(content);
124    }
125
126    pub fn raw_info(&mut self, content: &Content) {
127        self.info = Some(content.to_owned());
128    }
129
130    pub fn omit_expression(&mut self, value: bool) {
131        self.omit_expression = value;
132    }
133
134    pub fn prepend_module_to_snapshot(&mut self, value: bool) {
135        self.prepend_module_to_snapshot = value;
136    }
137
138    #[cfg(feature = "redactions")]
139    pub fn redactions<R: Into<Redactions>>(&mut self, r: R) {
140        self.redactions = r.into();
141    }
142
143    #[cfg(feature = "filters")]
144    pub fn filters<F: Into<Filters>>(&mut self, f: F) {
145        self.filters = f.into();
146    }
147
148    #[cfg(feature = "glob")]
149    pub fn allow_empty_glob(&mut self, value: bool) {
150        self.allow_empty_glob = value;
151    }
152}
153
154/// Configures how insta operates at test time.
155///
156/// Settings are always bound to a thread, and some default settings are always
157/// available.  These settings can be changed and influence how insta behaves on
158/// that thread.  They can be either temporarily or permanently changed.
159///
160/// This can be used to influence how the snapshot macros operate.
161/// For instance, it can be useful to force ordering of maps when
162/// unordered structures are used through settings.
163///
164/// Some of the settings can be changed but shouldn't as it will make it harder
165/// for tools like cargo-insta or an editor integration to locate the snapshot
166/// files.
167///
168/// Settings can also be configured with the [`with_settings!`] macro.
169///
170/// Example:
171///
172/// ```ignore
173/// use insta;
174///
175/// let mut settings = insta::Settings::clone_current();
176/// settings.set_sort_maps(true);
177/// settings.bind(|| {
178///     // runs the assertion with the changed settings enabled
179///     insta::assert_snapshot!(...);
180/// });
181/// ```
182#[derive(Clone)]
183pub struct Settings {
184    inner: Arc<ActualSettings>,
185}
186
187impl Default for Settings {
188    fn default() -> Settings {
189        Settings {
190            inner: DEFAULT_SETTINGS.clone(),
191        }
192    }
193}
194
195impl Settings {
196    /// Returns the default settings.
197    ///
198    /// It's recommended to use [`Self::clone_current`] instead so that
199    /// already applied modifications are not discarded.
200    pub fn new() -> Settings {
201        Settings::default()
202    }
203
204    /// Returns a copy of the current settings.
205    pub fn clone_current() -> Settings {
206        Settings::with(|x| x.clone())
207    }
208
209    /// Internal helper for macros
210    #[doc(hidden)]
211    pub fn _private_inner_mut(&mut self) -> &mut ActualSettings {
212        Arc::make_mut(&mut self.inner)
213    }
214
215    /// Enables forceful sorting of maps before serialization.
216    ///
217    /// Note that this only applies to snapshots that undergo serialization
218    /// (eg: does not work for [`assert_debug_snapshot!`](crate::assert_debug_snapshot!)).
219    ///
220    /// The default value is `false`.
221    pub fn set_sort_maps(&mut self, value: bool) {
222        self._private_inner_mut().sort_maps = value;
223    }
224
225    /// Returns the current value for map sorting.
226    pub fn sort_maps(&self) -> bool {
227        self.inner.sort_maps
228    }
229
230    /// Disables prepending of modules to the snapshot filename.
231    ///
232    /// By default, the filename of a snapshot is `<module>__<name>.snap`.
233    /// Setting this flag to `false` changes the snapshot filename to just
234    /// `<name>.snap`.
235    ///
236    /// The default value is `true`.
237    pub fn set_prepend_module_to_snapshot(&mut self, value: bool) {
238        self._private_inner_mut().prepend_module_to_snapshot(value);
239    }
240
241    /// Returns the current value for module name prepending.
242    pub fn prepend_module_to_snapshot(&self) -> bool {
243        self.inner.prepend_module_to_snapshot
244    }
245
246    /// Allows the [`glob!`] macro to succeed if it matches no files.
247    ///
248    /// By default, the glob macro will fail the test if it does not find
249    /// any files to prevent accidental typos.  This can be disabled when
250    /// fixtures should be conditional.
251    ///
252    /// The default value is `false`.
253    #[cfg(feature = "glob")]
254    pub fn set_allow_empty_glob(&mut self, value: bool) {
255        self._private_inner_mut().allow_empty_glob(value);
256    }
257
258    /// Returns the current value for the empty glob setting.
259    #[cfg(feature = "glob")]
260    pub fn allow_empty_glob(&self) -> bool {
261        self.inner.allow_empty_glob
262    }
263
264    /// Sets the snapshot suffix.
265    ///
266    /// The snapshot suffix is added to all snapshot names with an `@` sign
267    /// between.  For instance, if the snapshot suffix is set to `"foo"`, and
268    /// the snapshot would be named `"snapshot"`, it turns into `"snapshot@foo"`.
269    /// This is useful to separate snapshots if you want to use test
270    /// parameterization.
271    pub fn set_snapshot_suffix<I: Into<String>>(&mut self, suffix: I) {
272        self._private_inner_mut().snapshot_suffix(suffix);
273    }
274
275    /// Removes the snapshot suffix.
276    pub fn remove_snapshot_suffix(&mut self) {
277        self.set_snapshot_suffix("");
278    }
279
280    /// Returns the current snapshot suffix.
281    pub fn snapshot_suffix(&self) -> Option<&str> {
282        if self.inner.snapshot_suffix.is_empty() {
283            None
284        } else {
285            Some(&self.inner.snapshot_suffix)
286        }
287    }
288
289    /// Sets the input file reference.
290    ///
291    /// This value is completely unused by the snapshot testing system, but it
292    /// allows storing some metadata with a snapshot that refers back to the
293    /// input file.  The path stored here is made relative to the workspace root
294    /// before storing with the snapshot.
295    pub fn set_input_file<P: AsRef<Path>>(&mut self, p: P) {
296        self._private_inner_mut().input_file(p);
297    }
298
299    /// Removes the input file reference.
300    pub fn remove_input_file(&mut self) {
301        self._private_inner_mut().input_file = None;
302    }
303
304    /// Returns the current input file reference.
305    pub fn input_file(&self) -> Option<&Path> {
306        self.inner.input_file.as_deref()
307    }
308
309    /// Sets the description.
310    ///
311    /// The description is stored alongside the snapshot and will be displayed
312    /// in the diff UI.  When a snapshot is captured, the Rust expression for that
313    /// snapshot is always retained.  However, sometimes that information is not
314    /// super useful by itself, particularly when working with loops and generated
315    /// tests.  In that case the `description` can be set as extra information.
316    ///
317    /// See also [`Self::set_info`].
318    pub fn set_description<S: Into<String>>(&mut self, value: S) {
319        self._private_inner_mut().description(value);
320    }
321
322    /// Removes the description.
323    pub fn remove_description(&mut self) {
324        self._private_inner_mut().description = None;
325    }
326
327    /// Returns the current description
328    pub fn description(&self) -> Option<&str> {
329        self.inner.description.as_deref()
330    }
331
332    /// Sets the info.
333    ///
334    /// The `info` is similar to `description` but for structured data.  This is
335    /// stored with the snapshot and shown in the review UI.  This for instance
336    /// can be used to show extended information that can make a reviewer better
337    /// understand what the snapshot is supposed to be testing.
338    ///
339    /// As an example the input parameters to the function that creates the snapshot
340    /// can be persisted here.
341    ///
342    /// **Note:** Redactions configured via [`Self::add_redaction`] are automatically
343    /// applied to the info metadata when it is set.
344    ///
345    /// Alternatively you can use [`Self::set_raw_info`] instead.
346    #[cfg(feature = "serde")]
347    #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
348    pub fn set_info<S: Serialize>(&mut self, s: &S) {
349        self._private_inner_mut().info(s);
350    }
351
352    /// Sets the info from a content object.
353    ///
354    /// This works like [`Self::set_info`] but does not require [`serde`].
355    ///
356    /// **Note:** Unlike [`Self::set_info`], this method does NOT automatically apply
357    /// redactions. If you need redactions applied to metadata, use [`Self::set_info`]
358    /// instead (which requires the `serde` feature).
359    pub fn set_raw_info(&mut self, content: &Content) {
360        self._private_inner_mut().raw_info(content);
361    }
362
363    /// Removes the info.
364    pub fn remove_info(&mut self) {
365        self._private_inner_mut().info = None;
366    }
367
368    /// Returns the current info
369    pub(crate) fn info(&self) -> Option<&Content> {
370        self.inner.info.as_ref()
371    }
372
373    /// Returns the current info
374    pub fn has_info(&self) -> bool {
375        self.inner.info.is_some()
376    }
377
378    /// If set to true, does not retain the expression in the snapshot.
379    pub fn set_omit_expression(&mut self, value: bool) {
380        self._private_inner_mut().omit_expression(value);
381    }
382
383    /// Returns true if expressions are omitted from snapshots.
384    pub fn omit_expression(&self) -> bool {
385        self.inner.omit_expression
386    }
387
388    /// Registers redactions that should be applied.
389    ///
390    /// This can be useful if redactions must be shared across multiple
391    /// snapshots.
392    ///
393    /// Note that this only applies to snapshots that undergo serialization
394    /// (eg: does not work for [`assert_debug_snapshot!`](crate::assert_debug_snapshot!).)
395    #[cfg(feature = "redactions")]
396    #[cfg_attr(docsrs, doc(cfg(feature = "redactions")))]
397    pub fn add_redaction<R: Into<Redaction>>(&mut self, selector: &str, replacement: R) {
398        self.add_redaction_impl(selector, replacement.into())
399    }
400
401    #[cfg(feature = "redactions")]
402    fn add_redaction_impl(&mut self, selector: &str, replacement: Redaction) {
403        self._private_inner_mut().redactions.0.push((
404            Selector::parse(selector).unwrap().make_static(),
405            Arc::new(replacement),
406        ));
407    }
408
409    /// Registers a replacement callback.
410    ///
411    /// This works similar to a redaction but instead of changing the value it
412    /// asserts the value at a certain place.  This function is internally
413    /// supposed to call things like [`assert_eq!`].
414    ///
415    /// This is a shortcut to `add_redaction(selector, dynamic_redaction(...))`;
416    #[cfg(feature = "redactions")]
417    #[cfg_attr(docsrs, doc(cfg(feature = "redactions")))]
418    pub fn add_dynamic_redaction<I, F>(&mut self, selector: &str, func: F)
419    where
420        I: Into<Content>,
421        F: Fn(Content, ContentPath<'_>) -> I + Send + Sync + 'static,
422    {
423        self.add_redaction(selector, dynamic_redaction(func));
424    }
425
426    /// A special redaction that sorts a sequence or map.
427    ///
428    /// This is a shortcut to `add_redaction(selector, sorted_redaction())`.
429    #[cfg(feature = "redactions")]
430    #[cfg_attr(docsrs, doc(cfg(feature = "redactions")))]
431    pub fn sort_selector(&mut self, selector: &str) {
432        self.add_redaction(selector, sorted_redaction());
433    }
434
435    /// Replaces the currently set redactions.
436    ///
437    /// The default set is empty.
438    #[cfg(feature = "redactions")]
439    #[cfg_attr(docsrs, doc(cfg(feature = "redactions")))]
440    pub fn set_redactions<R: Into<Redactions>>(&mut self, redactions: R) {
441        self._private_inner_mut().redactions(redactions);
442    }
443
444    /// Removes all redactions.
445    #[cfg(feature = "redactions")]
446    #[cfg_attr(docsrs, doc(cfg(feature = "redactions")))]
447    pub fn clear_redactions(&mut self) {
448        self._private_inner_mut().redactions.0.clear();
449    }
450
451    /// Apply redactions to content.
452    #[cfg(feature = "redactions")]
453    #[cfg_attr(docsrs, doc(cfg(feature = "redactions")))]
454    pub(crate) fn apply_redactions(&self, content: Content) -> Content {
455        self.inner.redactions.apply_to_content(content)
456    }
457
458    /// Adds a new filter.
459    ///
460    /// Filters are similar to redactions but are applied as regex onto the final snapshot
461    /// value.  This can be used to perform modifications to the snapshot string that would
462    /// be impossible to do with redactions because for instance the value is just a string.
463    ///
464    /// The first argument is the [`regex`] pattern to apply, the second is a replacement
465    /// string.  The replacement string has the same functionality as the second argument
466    /// to [`regex::Regex::replace`].
467    ///
468    /// This is useful to perform some cleanup procedures on the snapshot for unstable values.
469    ///
470    /// ```rust
471    /// # use insta::Settings;
472    /// # async fn foo() {
473    /// # let mut settings = Settings::new();
474    /// settings.add_filter(r"\b[[:xdigit:]]{32}\b", "[UID]");
475    /// # }
476    /// ```
477    #[cfg(feature = "filters")]
478    #[cfg_attr(docsrs, doc(cfg(feature = "filters")))]
479    pub fn add_filter<S: Into<String>>(&mut self, regex: &str, replacement: S) {
480        self._private_inner_mut().filters.add(regex, replacement);
481    }
482
483    /// Replaces the currently set filters.
484    ///
485    /// The default set is empty.
486    #[cfg(feature = "filters")]
487    #[cfg_attr(docsrs, doc(cfg(feature = "filters")))]
488    pub fn set_filters<F: Into<Filters>>(&mut self, filters: F) {
489        self._private_inner_mut().filters(filters);
490    }
491
492    /// Removes all filters.
493    #[cfg(feature = "filters")]
494    #[cfg_attr(docsrs, doc(cfg(feature = "filters")))]
495    pub fn clear_filters(&mut self) {
496        self._private_inner_mut().filters.clear();
497    }
498
499    /// Returns the current filters
500    #[cfg(feature = "filters")]
501    #[cfg_attr(docsrs, doc(cfg(feature = "filters")))]
502    pub(crate) fn filters(&self) -> &Filters {
503        &self.inner.filters
504    }
505
506    /// Sets the snapshot path.
507    ///
508    /// If not absolute, it's relative to where the test is in.
509    ///
510    /// Defaults to `snapshots`.
511    pub fn set_snapshot_path<P: AsRef<Path>>(&mut self, path: P) {
512        self._private_inner_mut().snapshot_path(path);
513    }
514
515    /// Returns the snapshot path.
516    pub fn snapshot_path(&self) -> &Path {
517        &self.inner.snapshot_path
518    }
519
520    /// Runs a function with the current settings bound to the thread.
521    ///
522    /// This is an alternative to [`Self::bind_to_scope`]()
523    /// which does not require holding on to a drop guard.  The return value
524    /// of the closure is passed through.
525    ///
526    /// ```
527    /// # use insta::Settings;
528    /// let mut settings = Settings::clone_current();
529    /// settings.set_sort_maps(true);
530    /// settings.bind(|| {
531    ///     // do stuff here
532    /// });
533    /// ```
534    pub fn bind<F: FnOnce() -> R, R>(&self, f: F) -> R {
535        let _guard = self.bind_to_scope();
536        f()
537    }
538
539    /// Like [`Self::bind`] but for futures.
540    ///
541    /// This lets you bind settings for the duration of a future like this:
542    ///
543    /// ```rust
544    /// # use insta::Settings;
545    /// # async fn foo() {
546    /// let settings = Settings::new();
547    /// settings.bind_async(async {
548    ///     // do assertions here
549    /// }).await;
550    /// # }
551    /// ```
552    pub fn bind_async<F: Future<Output = T>, T>(&self, future: F) -> impl Future<Output = T> {
553        struct BindingFuture<F> {
554            settings: Arc<ActualSettings>,
555            future: F,
556        }
557
558        impl<F: Future> Future for BindingFuture<F> {
559            type Output = F::Output;
560
561            fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
562                let inner = self.settings.clone();
563                // SAFETY: This is okay because `future` is pinned when `self` is.
564                let future = unsafe { self.map_unchecked_mut(|s| &mut s.future) };
565                CURRENT_SETTINGS.with(|x| {
566                    let old = {
567                        let mut current = x.borrow_mut();
568                        let old = current.inner.clone();
569                        current.inner = inner;
570                        old
571                    };
572                    let rv = future.poll(cx);
573                    let mut current = x.borrow_mut();
574                    current.inner = old;
575                    rv
576                })
577            }
578        }
579
580        BindingFuture {
581            settings: self.inner.clone(),
582            future,
583        }
584    }
585
586    /// Binds the settings to the current thread and resets when the drop
587    /// guard is released.
588    ///
589    /// This is the recommended way to temporarily bind settings. It replaces
590    /// the earlier [`bind_to_scope`](Settings::bind_to_scope), and relies on
591    /// drop guards.  An alternative is [`bind`](Settings::bind), which binds
592    /// for the duration of the block it wraps.
593    ///
594    /// ```
595    /// # use insta::Settings;
596    /// let mut settings = Settings::clone_current();
597    /// settings.set_sort_maps(true);
598    /// let _guard = settings.bind_to_scope();
599    /// // do stuff here
600    /// ```
601    pub fn bind_to_scope(&self) -> SettingsBindDropGuard {
602        CURRENT_SETTINGS.with(|x| {
603            let mut x = x.borrow_mut();
604            let old = mem::replace(&mut x.inner, self.inner.clone());
605            SettingsBindDropGuard(Some(old), std::marker::PhantomData)
606        })
607    }
608
609    /// Runs a function with the current settings.
610    pub(crate) fn with<R, F: FnOnce(&Settings) -> R>(f: F) -> R {
611        CURRENT_SETTINGS.with(|x| f(&x.borrow()))
612    }
613}
614
615/// Returned from [`Settings::bind_to_scope`]
616///
617/// This type is not shareable between threads:
618///
619/// ```compile_fail E0277
620/// let mut settings = insta::Settings::clone_current();
621/// settings.set_snapshot_suffix("test drop guard");
622/// let guard = settings.bind_to_scope();
623///
624/// std::thread::spawn(move || { let guard = guard; }); // doesn't compile
625/// ```
626///
627/// This is to ensure tests under async runtimes like `tokio` don't show unexpected results
628#[must_use = "The guard is immediately dropped so binding has no effect. Use `let _guard = ...` to bind it."]
629pub struct SettingsBindDropGuard(
630    Option<Arc<ActualSettings>>,
631    /// A ZST that is not [`Send`] but is [`Sync`]
632    ///
633    /// This is necessary due to the lack of stable [negative impls](https://github.com/rust-lang/rust/issues/68318).
634    ///
635    /// Required as [`SettingsBindDropGuard`] modifies a thread local variable which would end up
636    /// with unexpected results if sent to a different thread.
637    std::marker::PhantomData<std::sync::MutexGuard<'static, ()>>,
638);
639
640impl Drop for SettingsBindDropGuard {
641    fn drop(&mut self) {
642        CURRENT_SETTINGS.with(|x| {
643            x.borrow_mut().inner = self.0.take().unwrap();
644        })
645    }
646}