1use std::cell::RefCell;
2use std::collections::{BTreeMap, BTreeSet};
3use std::error::Error;
4use std::fs;
5use std::io::{ErrorKind, Write};
6use std::path::{Path, PathBuf};
7use std::rc::Rc;
8use std::str;
9use std::sync::{Arc, Mutex};
10use std::{borrow::Cow, env};
11
12use crate::settings::Settings;
13use crate::snapshot::{
14 MetaData, PendingInlineSnapshot, Snapshot, SnapshotContents, SnapshotKind, TextSnapshotContents,
15};
16use crate::utils::{path_to_storage, style};
17use crate::{env::get_tool_config, output::SnapshotPrinter};
18use crate::{
19 env::{
20 memoize_snapshot_file, snapshot_update_behavior, OutputBehavior, SnapshotUpdateBehavior,
21 ToolConfig,
22 },
23 snapshot::TextSnapshotKind,
24};
25
26use once_cell::sync::Lazy;
27
28static TEST_NAME_COUNTERS: Lazy<Mutex<BTreeMap<String, usize>>> =
29 Lazy::new(|| Mutex::new(BTreeMap::new()));
30static TEST_NAME_CLASH_DETECTION: Lazy<Mutex<BTreeMap<String, bool>>> =
31 Lazy::new(|| Mutex::new(BTreeMap::new()));
32static INLINE_DUPLICATES: Lazy<Mutex<BTreeSet<String>>> = Lazy::new(|| Mutex::new(BTreeSet::new()));
33
34thread_local! {
35 static RECORDED_DUPLICATES: RefCell<Vec<BTreeMap<String, Snapshot>>> = RefCell::default()
36}
37
38#[macro_export]
41macro_rules! elog {
42 () => (write!(std::io::stderr()).ok());
43 ($($arg:tt)*) => ({
44 writeln!(std::io::stderr(), $($arg)*).ok();
45 })
46}
47#[cfg(feature = "glob")]
48macro_rules! print_or_panic {
49 ($fail_fast:expr, $($tokens:tt)*) => {{
50 if (!$fail_fast) {
51 eprintln!($($tokens)*);
52 eprintln!();
53 } else {
54 panic!($($tokens)*);
55 }
56 }}
57}
58
59#[derive(Debug)]
65pub struct AutoName;
66
67pub struct InlineValue<'a>(pub &'a str);
68
69type SnapshotName<'a> = Option<Cow<'a, str>>;
71
72pub struct BinarySnapshotValue<'a> {
73 pub name_and_extension: &'a str,
74 pub content: Vec<u8>,
75}
76
77pub enum SnapshotValue<'a> {
78 FileText {
80 name: SnapshotName<'a>,
81
82 content: &'a str,
84 },
85
86 InlineText {
88 reference_content: &'a str,
90
91 content: &'a str,
93 },
94
95 Binary {
97 name: SnapshotName<'a>,
98
99 content: Vec<u8>,
101
102 extension: &'a str,
104 },
105}
106
107impl<'a> From<(AutoName, &'a str)> for SnapshotValue<'a> {
108 fn from((_, content): (AutoName, &'a str)) -> Self {
109 SnapshotValue::FileText {
110 name: None,
111 content,
112 }
113 }
114}
115
116impl<'a> From<(Option<String>, &'a str)> for SnapshotValue<'a> {
117 fn from((name, content): (Option<String>, &'a str)) -> Self {
118 SnapshotValue::FileText {
119 name: name.map(Cow::Owned),
120 content,
121 }
122 }
123}
124
125impl<'a> From<(String, &'a str)> for SnapshotValue<'a> {
126 fn from((name, content): (String, &'a str)) -> Self {
127 SnapshotValue::FileText {
128 name: Some(Cow::Owned(name)),
129 content,
130 }
131 }
132}
133
134impl<'a> From<(Option<&'a str>, &'a str)> for SnapshotValue<'a> {
135 fn from((name, content): (Option<&'a str>, &'a str)) -> Self {
136 SnapshotValue::FileText {
137 name: name.map(Cow::Borrowed),
138 content,
139 }
140 }
141}
142
143impl<'a> From<(&'a str, &'a str)> for SnapshotValue<'a> {
144 fn from((name, content): (&'a str, &'a str)) -> Self {
145 SnapshotValue::FileText {
146 name: Some(Cow::Borrowed(name)),
147 content,
148 }
149 }
150}
151
152impl<'a> From<(InlineValue<'a>, &'a str)> for SnapshotValue<'a> {
153 fn from((InlineValue(reference_content), content): (InlineValue<'a>, &'a str)) -> Self {
154 SnapshotValue::InlineText {
155 reference_content,
156 content,
157 }
158 }
159}
160
161impl<'a> From<BinarySnapshotValue<'a>> for SnapshotValue<'a> {
162 fn from(
163 BinarySnapshotValue {
164 name_and_extension,
165 content,
166 }: BinarySnapshotValue<'a>,
167 ) -> Self {
168 let (name, extension) = name_and_extension.split_once('.').unwrap_or_else(|| {
169 panic!(
170 "\"{}\" does not match the format \"name.extension\"",
171 name_and_extension,
172 )
173 });
174
175 let name = if name.is_empty() {
176 None
177 } else {
178 Some(Cow::Borrowed(name))
179 };
180
181 SnapshotValue::Binary {
182 name,
183 extension,
184 content,
185 }
186 }
187}
188
189fn is_doctest(function_name: &str) -> bool {
190 function_name.starts_with("rust_out::main::_doctest")
191}
192
193fn detect_snapshot_name(function_name: &str, module_path: &str) -> Result<String, &'static str> {
194 let name = function_name.rsplit("::").next().unwrap();
196
197 let (name, test_prefixed) = if let Some(stripped) = name.strip_prefix("test_") {
198 (stripped, true)
199 } else {
200 (name, false)
201 };
202
203 let name = add_suffix_to_snapshot_name(Cow::Borrowed(name));
205 let key = format!("{}::{}", module_path.replace("::", "__"), name);
206
207 let mut name_clash_detection = TEST_NAME_CLASH_DETECTION
210 .lock()
211 .unwrap_or_else(|x| x.into_inner());
212 match name_clash_detection.get(&key) {
213 None => {
214 name_clash_detection.insert(key.clone(), test_prefixed);
215 }
216 Some(&was_test_prefixed) => {
217 if was_test_prefixed != test_prefixed {
218 panic!(
219 "Insta snapshot name clash detected between '{}' \
220 and 'test_{}' in '{}'. Rename one function.",
221 name, name, module_path
222 );
223 }
224 }
225 }
226
227 if allow_duplicates() {
230 return Ok(name.to_string());
231 }
232
233 let mut counters = TEST_NAME_COUNTERS.lock().unwrap_or_else(|x| x.into_inner());
236 let test_idx = counters.get(&key).cloned().unwrap_or(0) + 1;
237 let rv = if test_idx == 1 {
238 name.to_string()
239 } else {
240 format!("{}-{}", name, test_idx)
241 };
242 counters.insert(key, test_idx);
243
244 Ok(rv)
245}
246
247fn add_suffix_to_snapshot_name(name: Cow<'_, str>) -> Cow<'_, str> {
249 Settings::with(|settings| {
250 settings
251 .snapshot_suffix()
252 .map(|suffix| Cow::Owned(format!("{}@{}", name, suffix)))
253 .unwrap_or_else(|| name)
254 })
255}
256
257fn get_snapshot_filename(
258 module_path: &str,
259 assertion_file: &str,
260 snapshot_name: &str,
261 cargo_workspace: &Path,
262 is_doctest: bool,
263) -> PathBuf {
264 let root = Path::new(cargo_workspace);
265 let base = Path::new(assertion_file);
266 Settings::with(|settings| {
267 root.join(base.parent().unwrap())
268 .join(settings.snapshot_path())
269 .join({
270 use std::fmt::Write;
271 let mut f = String::new();
272 if settings.prepend_module_to_snapshot() {
273 if is_doctest {
274 write!(
275 &mut f,
276 "doctest_{}__",
277 base.file_name()
278 .unwrap()
279 .to_string_lossy()
280 .replace('.', "_")
281 )
282 .unwrap();
283 } else {
284 write!(&mut f, "{}__", module_path.replace("::", "__")).unwrap();
285 }
286 }
287 write!(
288 &mut f,
289 "{}.snap",
290 snapshot_name.replace(&['/', '\\'][..], "__")
291 )
292 .unwrap();
293 f
294 })
295 })
296}
297
298#[derive(Debug)]
302struct SnapshotAssertionContext<'a> {
303 tool_config: Arc<ToolConfig>,
304 workspace: &'a Path,
305 module_path: &'a str,
306 snapshot_name: Option<Cow<'a, str>>,
307 snapshot_file: Option<PathBuf>,
308 duplication_key: Option<String>,
309 old_snapshot: Option<Snapshot>,
310 pending_snapshots_path: Option<PathBuf>,
311 assertion_file: &'a str,
312 assertion_line: u32,
313 is_doctest: bool,
314 snapshot_kind: SnapshotKind,
315}
316
317impl<'a> SnapshotAssertionContext<'a> {
318 fn prepare(
319 new_snapshot_value: &SnapshotValue<'a>,
320 workspace: &'a Path,
321 function_name: &'a str,
322 module_path: &'a str,
323 assertion_file: &'a str,
324 assertion_line: u32,
325 ) -> Result<SnapshotAssertionContext<'a>, Box<dyn Error>> {
326 let tool_config = get_tool_config(workspace);
327 let snapshot_name;
328 let mut duplication_key = None;
329 let mut snapshot_file = None;
330 let mut old_snapshot = None;
331 let mut pending_snapshots_path = None;
332 let is_doctest = is_doctest(function_name);
333
334 match new_snapshot_value {
335 SnapshotValue::FileText { name, .. } | SnapshotValue::Binary { name, .. } => {
336 let name = match &name {
337 Some(name) => add_suffix_to_snapshot_name(name.clone()),
338 None => {
339 if is_doctest {
340 panic!("Cannot determine reliable names for snapshot in doctests. Please use explicit names instead.");
341 }
342 detect_snapshot_name(function_name, module_path)
343 .unwrap()
344 .into()
345 }
346 };
347 if allow_duplicates() {
348 duplication_key = Some(format!("named:{}|{}", module_path, name));
349 }
350 let file = get_snapshot_filename(
351 module_path,
352 assertion_file,
353 &name,
354 workspace,
355 is_doctest,
356 );
357 if fs::metadata(&file).is_ok() {
358 old_snapshot = Some(Snapshot::from_file(&file)?);
359 }
360 snapshot_name = Some(name);
361 snapshot_file = Some(file);
362 }
363 SnapshotValue::InlineText {
364 reference_content: contents,
365 ..
366 } => {
367 if allow_duplicates() {
368 duplication_key = Some(format!(
369 "inline:{}|{}|{}",
370 function_name, assertion_file, assertion_line
371 ));
372 } else {
373 prevent_inline_duplicate(function_name, assertion_file, assertion_line);
374 }
375 snapshot_name = detect_snapshot_name(function_name, module_path)
376 .ok()
377 .map(Cow::Owned);
378 let mut pending_file = workspace.join(assertion_file);
379 pending_file.set_file_name(format!(
380 ".{}.pending-snap",
381 pending_file
382 .file_name()
383 .expect("no filename")
384 .to_str()
385 .expect("non unicode filename")
386 ));
387 pending_snapshots_path = Some(pending_file);
388 old_snapshot = Some(Snapshot::from_components(
389 module_path.replace("::", "__"),
390 None,
391 MetaData::default(),
392 TextSnapshotContents::new(contents.to_string(), TextSnapshotKind::Inline)
393 .into(),
394 ));
395 }
396 };
397
398 let snapshot_type = match new_snapshot_value {
399 SnapshotValue::FileText { .. } | SnapshotValue::InlineText { .. } => SnapshotKind::Text,
400 &SnapshotValue::Binary { extension, .. } => SnapshotKind::Binary {
401 extension: extension.to_string(),
402 },
403 };
404
405 Ok(SnapshotAssertionContext {
406 tool_config,
407 workspace,
408 module_path,
409 snapshot_name,
410 snapshot_file,
411 old_snapshot,
412 pending_snapshots_path,
413 assertion_file,
414 assertion_line,
415 duplication_key,
416 is_doctest,
417 snapshot_kind: snapshot_type,
418 })
419 }
420
421 pub fn localize_path(&self, p: &Path) -> Option<PathBuf> {
423 let workspace = self.workspace.canonicalize().ok()?;
424 let p = self.workspace.join(p).canonicalize().ok()?;
425 p.strip_prefix(&workspace).ok().map(|x| x.to_path_buf())
426 }
427
428 pub fn new_snapshot(&self, contents: SnapshotContents, expr: &str) -> Snapshot {
430 assert_eq!(
431 contents.is_binary(),
432 matches!(self.snapshot_kind, SnapshotKind::Binary { .. })
433 );
434
435 Snapshot::from_components(
436 self.module_path.replace("::", "__"),
437 self.snapshot_name.as_ref().map(|x| x.to_string()),
438 Settings::with(|settings| MetaData {
439 source: Some(path_to_storage(Path::new(self.assertion_file))),
440 assertion_line: Some(self.assertion_line),
441 description: settings.description().map(Into::into),
442 expression: if settings.omit_expression() {
443 None
444 } else {
445 Some(expr.to_string())
446 },
447 info: settings.info().map(ToOwned::to_owned),
448 input_file: settings
449 .input_file()
450 .and_then(|x| self.localize_path(x))
451 .map(|x| path_to_storage(&x)),
452 snapshot_kind: self.snapshot_kind.clone(),
453 }),
454 contents,
455 )
456 }
457
458 pub fn cleanup_passing(&self) -> Result<(), Box<dyn Error>> {
460 if let Some(ref snapshot_file) = self.snapshot_file {
463 let snapshot_file = snapshot_file.clone().with_extension("snap.new");
464 fs::remove_file(snapshot_file).ok();
465 }
466
467 if let Some(ref pending_snapshots) = self.pending_snapshots_path {
469 if fs::metadata(pending_snapshots).is_ok() {
470 PendingInlineSnapshot::new(None, None, self.assertion_line)
471 .save(pending_snapshots)?;
472 }
473 }
474 Ok(())
475 }
476
477 pub fn cleanup_previous_pending_binary_snapshots(&self) -> Result<(), Box<dyn Error>> {
481 if let Some(ref path) = self.snapshot_file {
482 let file_name_prefix = format!("{}.new.", path.file_name().unwrap().to_str().unwrap());
485
486 let read_dir = path.parent().unwrap().read_dir();
487
488 match read_dir {
489 Err(e) if e.kind() == ErrorKind::NotFound => return Ok(()),
490 _ => (),
491 }
492
493 for entry in read_dir? {
496 let entry = entry?;
497 let entry_file_name = entry.file_name();
498
499 if entry_file_name
502 .to_str()
503 .map(|f| f.starts_with(&file_name_prefix))
504 .unwrap_or(false)
505 {
506 std::fs::remove_file(entry.path())?;
507 }
508 }
509 }
510
511 Ok(())
512 }
513
514 pub fn update_snapshot(
516 &self,
517 new_snapshot: Snapshot,
518 ) -> Result<SnapshotUpdateBehavior, Box<dyn Error>> {
519 let unseen = self
522 .snapshot_file
523 .as_ref()
524 .map_or(false, |x| fs::metadata(x).is_ok());
525 let should_print = self.tool_config.output_behavior() != OutputBehavior::Nothing;
526 let snapshot_update = snapshot_update_behavior(&self.tool_config, unseen);
527
528 let snapshot_update =
532 if snapshot_update == SnapshotUpdateBehavior::InPlace && self.snapshot_file.is_none() {
534 SnapshotUpdateBehavior::NewFile
535 } else {
536 snapshot_update
537 };
538
539 match snapshot_update {
540 SnapshotUpdateBehavior::InPlace => {
541 if let Some(ref snapshot_file) = self.snapshot_file {
542 new_snapshot.save(snapshot_file)?;
543 if should_print {
544 elog!(
545 "{} {}",
546 if unseen {
547 style("created previously unseen snapshot").green()
548 } else {
549 style("updated snapshot").green()
550 },
551 style(snapshot_file.display()).cyan().underlined(),
552 );
553 }
554 } else {
555 unreachable!()
557 }
558 }
559 SnapshotUpdateBehavior::NewFile => {
560 if let Some(ref snapshot_file) = self.snapshot_file {
561 let new_path = new_snapshot.save_new(snapshot_file)?;
563 if should_print {
564 elog!(
565 "{} {}",
566 style("stored new snapshot").green(),
567 style(new_path.display()).cyan().underlined(),
568 );
569 }
570 } else if self.is_doctest {
571 if should_print {
572 elog!(
573 "{}",
574 style("warning: cannot update inline snapshots in doctests")
575 .red()
576 .bold(),
577 );
578 }
579 } else {
580 PendingInlineSnapshot::new(
581 Some(new_snapshot),
582 self.old_snapshot.clone(),
583 self.assertion_line,
584 )
585 .save(self.pending_snapshots_path.as_ref().unwrap())?;
586 }
587 }
588 SnapshotUpdateBehavior::NoUpdate => {}
589 }
590
591 Ok(snapshot_update)
592 }
593
594 fn print_snapshot_info(&self, new_snapshot: &Snapshot) {
596 let mut printer =
597 SnapshotPrinter::new(self.workspace, self.old_snapshot.as_ref(), new_snapshot);
598 printer.set_line(Some(self.assertion_line));
599 printer.set_snapshot_file(self.snapshot_file.as_deref());
600 printer.set_title(Some("Snapshot Summary"));
601 printer.set_show_info(true);
602 match self.tool_config.output_behavior() {
603 OutputBehavior::Summary => {
604 printer.print();
605 }
606 OutputBehavior::Diff => {
607 printer.set_show_diff(true);
608 printer.print();
609 }
610 _ => {}
611 }
612 }
613
614 fn finalize(&self, update_result: SnapshotUpdateBehavior) {
617 let fail_fast = {
620 #[cfg(feature = "glob")]
621 {
622 if let Some(top) = crate::glob::GLOB_STACK.lock().unwrap().last() {
623 top.fail_fast
624 } else {
625 true
626 }
627 }
628 #[cfg(not(feature = "glob"))]
629 {
630 true
631 }
632 };
633
634 if fail_fast
635 && update_result == SnapshotUpdateBehavior::NewFile
636 && self.tool_config.output_behavior() != OutputBehavior::Nothing
637 && !self.is_doctest
638 {
639 println!(
640 "{hint}",
641 hint = style("To update snapshots run `cargo insta review`").dim(),
642 );
643 }
644
645 if update_result != SnapshotUpdateBehavior::InPlace && !self.tool_config.force_pass() {
646 if fail_fast && self.tool_config.output_behavior() != OutputBehavior::Nothing {
647 let msg = if env::var("INSTA_CARGO_INSTA") == Ok("1".to_string()) {
648 "Stopped on the first failure."
649 } else {
650 "Stopped on the first failure. Run `cargo insta test` to run all snapshots."
651 };
652 println!("{hint}", hint = style(msg).dim(),);
653 }
654
655 #[cfg(feature = "glob")]
659 {
660 let mut stack = crate::glob::GLOB_STACK.lock().unwrap();
661 if let Some(glob_collector) = stack.last_mut() {
662 glob_collector.failed += 1;
663 if update_result == SnapshotUpdateBehavior::NewFile
664 && self.tool_config.output_behavior() != OutputBehavior::Nothing
665 {
666 glob_collector.show_insta_hint = true;
667 }
668
669 print_or_panic!(
670 fail_fast,
671 "snapshot assertion from glob for '{}' failed in line {}",
672 self.snapshot_name.as_deref().unwrap_or("unnamed snapshot"),
673 self.assertion_line
674 );
675 return;
676 }
677 }
678
679 panic!(
680 "snapshot assertion for '{}' failed in line {}",
681 self.snapshot_name.as_deref().unwrap_or("unnamed snapshot"),
682 self.assertion_line
683 );
684 }
685 }
686}
687
688fn prevent_inline_duplicate(function_name: &str, assertion_file: &str, assertion_line: u32) {
689 let key = format!("{}|{}|{}", function_name, assertion_file, assertion_line);
690 let mut set = INLINE_DUPLICATES.lock().unwrap();
691 if set.contains(&key) {
692 drop(set);
694 panic!(
695 "Insta does not allow inline snapshot assertions in loops. \
696 Wrap your assertions in allow_duplicates! to change this."
697 );
698 }
699 set.insert(key);
700}
701
702fn record_snapshot_duplicate(
703 results: &mut BTreeMap<String, Snapshot>,
704 snapshot: &Snapshot,
705 ctx: &SnapshotAssertionContext,
706) {
707 let key = ctx.duplication_key.as_deref().unwrap();
708 if let Some(prev_snapshot) = results.get(key) {
709 if prev_snapshot.contents() != snapshot.contents() {
710 println!("Snapshots in allow-duplicates block do not match.");
711 let mut printer = SnapshotPrinter::new(ctx.workspace, Some(prev_snapshot), snapshot);
712 printer.set_line(Some(ctx.assertion_line));
713 printer.set_snapshot_file(ctx.snapshot_file.as_deref());
714 printer.set_title(Some("Differences in Block"));
715 printer.set_snapshot_hints("previous assertion", "current assertion");
716 if ctx.tool_config.output_behavior() == OutputBehavior::Diff {
717 printer.set_show_diff(true);
718 }
719 printer.print();
720 panic!(
721 "snapshot assertion for '{}' failed in line {}. Result \
722 does not match previous snapshot in allow-duplicates block.",
723 ctx.snapshot_name.as_deref().unwrap_or("unnamed snapshot"),
724 ctx.assertion_line
725 );
726 }
727 } else {
728 results.insert(key.to_string(), snapshot.clone());
729 }
730}
731
732fn allow_duplicates() -> bool {
734 RECORDED_DUPLICATES.with(|x| !x.borrow().is_empty())
735}
736
737pub fn with_allow_duplicates<R, F>(f: F) -> R
739where
740 F: FnOnce() -> R,
741{
742 RECORDED_DUPLICATES.with(|x| x.borrow_mut().push(BTreeMap::new()));
743 let rv = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
744 RECORDED_DUPLICATES.with(|x| x.borrow_mut().pop().unwrap());
745 match rv {
746 Ok(rv) => rv,
747 Err(payload) => std::panic::resume_unwind(payload),
748 }
749}
750
751#[allow(clippy::too_many_arguments)]
758pub fn assert_snapshot(
759 snapshot_value: SnapshotValue<'_>,
760 workspace: &Path,
761 function_name: &str,
762 module_path: &str,
763 assertion_file: &str,
764 assertion_line: u32,
765 expr: &str,
766) -> Result<(), Box<dyn Error>> {
767 let ctx = SnapshotAssertionContext::prepare(
768 &snapshot_value,
769 workspace,
770 function_name,
771 module_path,
772 assertion_file,
773 assertion_line,
774 )?;
775
776 ctx.cleanup_previous_pending_binary_snapshots()?;
777
778 let content = match snapshot_value {
779 SnapshotValue::FileText { content, .. } | SnapshotValue::InlineText { content, .. } => {
780 #[cfg(feature = "filters")]
782 let content = Settings::with(|settings| settings.filters().apply_to(content));
783
784 let kind = match ctx.snapshot_file {
785 Some(_) => TextSnapshotKind::File,
786 None => TextSnapshotKind::Inline,
787 };
788
789 TextSnapshotContents::new(content.into(), kind).into()
790 }
791 SnapshotValue::Binary {
792 content, extension, ..
793 } => {
794 assert!(
795 extension != "new",
796 "'.new' is not allowed as a file extension"
797 );
798 assert!(
799 !extension.starts_with("new."),
800 "file extensions starting with 'new.' are not allowed",
801 );
802
803 SnapshotContents::Binary(Rc::new(content))
804 }
805 };
806
807 let new_snapshot = ctx.new_snapshot(content, expr);
808
809 if let Some(ref snapshot_file) = ctx.snapshot_file {
811 memoize_snapshot_file(snapshot_file);
812 }
813
814 RECORDED_DUPLICATES.with(|x| {
818 if let Some(results) = x.borrow_mut().last_mut() {
819 record_snapshot_duplicate(results, &new_snapshot, &ctx);
820 }
821 });
822
823 let pass = ctx
824 .old_snapshot
825 .as_ref()
826 .map(|x| {
827 if ctx.tool_config.require_full_match() {
828 x.matches_fully(&new_snapshot)
829 } else {
830 x.matches(&new_snapshot)
831 }
832 })
833 .unwrap_or(false);
834
835 if pass {
836 ctx.cleanup_passing()?;
837
838 if matches!(
839 ctx.tool_config.snapshot_update(),
840 crate::env::SnapshotUpdate::Force
841 ) {
842 ctx.update_snapshot(new_snapshot)?;
843 }
844 } else {
846 ctx.print_snapshot_info(&new_snapshot);
847 let update_result = ctx.update_snapshot(new_snapshot)?;
848 ctx.finalize(update_result);
849 }
850
851 Ok(())
852}
853
854#[allow(rustdoc::private_doc_tests)]
855const _DOCTEST1: bool = false;