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}