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}