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