1use crate::{
2 content::{self, json, yaml, Content},
3 elog,
4 utils::style,
5};
6use once_cell::sync::Lazy;
7use std::env;
8use std::error::Error;
9use std::fmt;
10use std::fs;
11use std::io::{BufRead, BufReader, Write};
12use std::path::{Path, PathBuf};
13use std::rc::Rc;
14use std::time::{SystemTime, UNIX_EPOCH};
15use std::{borrow::Cow, iter::once};
16
17static RUN_ID: Lazy<String> = Lazy::new(|| {
18 if let Ok(run_id) = env::var("NEXTEST_RUN_ID") {
19 run_id
20 } else {
21 let d = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
22 format!("{}-{}", d.as_secs(), d.subsec_nanos())
23 }
24});
25
26#[derive(Debug)]
29pub struct PendingInlineSnapshot {
30 pub run_id: String,
31 pub line: u32,
32 pub new: Option<Snapshot>,
33 pub old: Option<Snapshot>,
34}
35
36impl PendingInlineSnapshot {
37 pub fn new(new: Option<Snapshot>, old: Option<Snapshot>, line: u32) -> PendingInlineSnapshot {
38 PendingInlineSnapshot {
39 new,
40 old,
41 line,
42 run_id: RUN_ID.clone(),
43 }
44 }
45
46 #[cfg(feature = "_cargo_insta_internal")]
47 pub fn load_batch(p: &Path) -> Result<Vec<PendingInlineSnapshot>, Box<dyn Error>> {
48 let contents =
49 fs::read_to_string(p).map_err(|e| content::Error::FileIo(e, p.to_path_buf()))?;
50
51 let mut rv: Vec<Self> = contents
52 .lines()
53 .map(|line| {
54 let value = yaml::parse_str(line, p)?;
55 Self::from_content(value)
56 })
57 .collect::<Result<_, Box<dyn Error>>>()?;
58
59 if let Some(last_run_id) = rv.last().map(|x| x.run_id.clone()) {
61 rv.retain(|x| x.run_id == last_run_id);
62 }
63
64 Ok(rv)
65 }
66
67 #[cfg(feature = "_cargo_insta_internal")]
68 pub fn save_batch(p: &Path, batch: &[PendingInlineSnapshot]) -> Result<(), Box<dyn Error>> {
69 fs::remove_file(p).ok();
70 for snap in batch {
71 snap.save(p)?;
72 }
73 Ok(())
74 }
75
76 pub fn save(&self, p: &Path) -> Result<(), Box<dyn Error>> {
77 if let Some(parent) = p.parent() {
79 fs::create_dir_all(parent)?;
80 }
81 let mut f = fs::OpenOptions::new().create(true).append(true).open(p)?;
82 let mut s = json::to_string(&self.as_content());
83 s.push('\n');
84 f.write_all(s.as_bytes())?;
85 Ok(())
86 }
87
88 #[cfg(feature = "_cargo_insta_internal")]
89 fn from_content(content: Content) -> Result<PendingInlineSnapshot, Box<dyn Error>> {
90 if let Content::Map(map) = content {
91 let mut run_id = None;
92 let mut line = None;
93 let mut old = None;
94 let mut new = None;
95
96 for (key, value) in map.into_iter() {
97 match key.as_str() {
98 Some("run_id") => run_id = value.as_str().map(|x| x.to_string()),
99 Some("line") => line = value.as_u64().map(|x| x as u32),
100 Some("old") if !value.is_nil() => {
101 old = Some(Snapshot::from_content(value, TextSnapshotKind::Inline)?)
102 }
103 Some("new") if !value.is_nil() => {
104 new = Some(Snapshot::from_content(value, TextSnapshotKind::Inline)?)
105 }
106 _ => {}
107 }
108 }
109
110 Ok(PendingInlineSnapshot {
111 run_id: run_id.ok_or(content::Error::MissingField)?,
112 line: line.ok_or(content::Error::MissingField)?,
113 new,
114 old,
115 })
116 } else {
117 Err(content::Error::UnexpectedDataType.into())
118 }
119 }
120
121 fn as_content(&self) -> Content {
122 let fields = vec![
123 ("run_id", Content::from(self.run_id.as_str())),
124 ("line", Content::from(self.line)),
125 (
126 "new",
127 match &self.new {
128 Some(snap) => snap.as_content(),
129 None => Content::None,
130 },
131 ),
132 (
133 "old",
134 match &self.old {
135 Some(snap) => snap.as_content(),
136 None => Content::None,
137 },
138 ),
139 ];
140
141 Content::Struct("PendingInlineSnapshot", fields)
142 }
143}
144
145#[derive(Debug, Clone, PartialEq, Default)]
146pub enum SnapshotKind {
147 #[default]
148 Text,
149 Binary {
150 extension: String,
151 },
152}
153
154#[derive(Debug, Default, Clone, PartialEq)]
156pub struct MetaData {
157 pub(crate) source: Option<String>,
159 pub(crate) assertion_line: Option<u32>,
162 pub(crate) description: Option<String>,
164 pub(crate) expression: Option<String>,
166 pub(crate) info: Option<Content>,
168 pub(crate) input_file: Option<String>,
170 pub(crate) snapshot_kind: SnapshotKind,
172}
173
174impl MetaData {
175 pub fn source(&self) -> Option<&str> {
177 self.source.as_deref()
178 }
179
180 pub fn assertion_line(&self) -> Option<u32> {
182 self.assertion_line
183 }
184
185 pub fn expression(&self) -> Option<&str> {
187 self.expression.as_deref()
188 }
189
190 pub fn description(&self) -> Option<&str> {
192 self.description.as_deref().filter(|x| !x.is_empty())
193 }
194
195 #[doc(hidden)]
197 pub fn private_info(&self) -> Option<&Content> {
198 self.info.as_ref()
199 }
200
201 pub fn get_relative_source(&self, base: &Path) -> Option<PathBuf> {
203 self.source.as_ref().map(|source| {
204 base.join(source)
205 .canonicalize()
206 .ok()
207 .and_then(|s| s.strip_prefix(base).ok().map(|x| x.to_path_buf()))
208 .unwrap_or_else(|| base.to_path_buf())
209 })
210 }
211
212 pub fn input_file(&self) -> Option<&str> {
214 self.input_file.as_deref()
215 }
216
217 fn from_content(content: Content) -> Result<MetaData, Box<dyn Error>> {
218 if let Content::Map(map) = content {
219 let mut source = None;
220 let mut assertion_line = None;
221 let mut description = None;
222 let mut expression = None;
223 let mut info = None;
224 let mut input_file = None;
225 let mut snapshot_type = TmpSnapshotKind::Text;
226 let mut extension = None;
227
228 enum TmpSnapshotKind {
229 Text,
230 Binary,
231 }
232
233 for (key, value) in map.into_iter() {
234 match key.as_str() {
235 Some("source") => source = value.as_str().map(|x| x.to_string()),
236 Some("assertion_line") => assertion_line = value.as_u64().map(|x| x as u32),
237 Some("description") => description = value.as_str().map(Into::into),
238 Some("expression") => expression = value.as_str().map(Into::into),
239 Some("info") if !value.is_nil() => info = Some(value),
240 Some("input_file") => input_file = value.as_str().map(Into::into),
241 Some("snapshot_kind") => {
242 snapshot_type = match value.as_str() {
243 Some("binary") => TmpSnapshotKind::Binary,
244 _ => TmpSnapshotKind::Text,
245 }
246 }
247 Some("extension") => {
248 extension = value.as_str().map(Into::into);
249 }
250 _ => {}
251 }
252 }
253
254 Ok(MetaData {
255 source,
256 assertion_line,
257 description,
258 expression,
259 info,
260 input_file,
261 snapshot_kind: match snapshot_type {
262 TmpSnapshotKind::Text => SnapshotKind::Text,
263 TmpSnapshotKind::Binary => SnapshotKind::Binary {
264 extension: extension.ok_or(content::Error::MissingField)?,
265 },
266 },
267 })
268 } else {
269 Err(content::Error::UnexpectedDataType.into())
270 }
271 }
272
273 fn as_content(&self) -> Content {
274 let mut fields = Vec::new();
275 if let Some(source) = self.source.as_deref() {
276 fields.push(("source", Content::from(source)));
277 }
278 if let Some(line) = self.assertion_line {
279 fields.push(("assertion_line", Content::from(line)));
280 }
281 if let Some(description) = self.description.as_deref() {
282 fields.push(("description", Content::from(description)));
283 }
284 if let Some(expression) = self.expression.as_deref() {
285 fields.push(("expression", Content::from(expression)));
286 }
287 if let Some(info) = &self.info {
288 fields.push(("info", info.to_owned()));
289 }
290 if let Some(input_file) = self.input_file.as_deref() {
291 fields.push(("input_file", Content::from(input_file)));
292 }
293
294 match self.snapshot_kind {
295 SnapshotKind::Text => {}
296 SnapshotKind::Binary { ref extension } => {
297 fields.push(("extension", Content::from(extension.clone())));
298 fields.push(("snapshot_kind", Content::from("binary")));
299 }
300 }
301
302 Content::Struct("MetaData", fields)
303 }
304
305 pub(crate) fn trim_for_persistence(&self) -> Cow<'_, MetaData> {
308 if self.assertion_line.is_some() {
316 let mut rv = self.clone();
317 rv.assertion_line = None;
318 Cow::Owned(rv)
319 } else {
320 Cow::Borrowed(self)
321 }
322 }
323}
324
325#[derive(Debug, PartialEq, Eq, Clone, Copy)]
326pub enum TextSnapshotKind {
327 Inline,
328 File,
329}
330
331#[derive(Debug, Clone)]
333pub struct Snapshot {
334 module_name: String,
335 snapshot_name: Option<String>,
336 pub(crate) metadata: MetaData,
337 snapshot: SnapshotContents,
338}
339
340impl Snapshot {
341 pub fn from_file(p: &Path) -> Result<Snapshot, Box<dyn Error>> {
343 let mut f = BufReader::new(fs::File::open(p)?);
344 let mut buf = String::new();
345
346 f.read_line(&mut buf)?;
347
348 let metadata = if buf.trim_end() == "---" {
350 loop {
351 let read = f.read_line(&mut buf)?;
352 if read == 0 {
353 break;
354 }
355 if buf[buf.len() - read..].trim_end() == "---" {
356 buf.truncate(buf.len() - read);
357 break;
358 }
359 }
360 let content = yaml::parse_str(&buf, p)?;
361 MetaData::from_content(content)?
362 } else {
366 let mut rv = MetaData::default();
367 loop {
368 buf.clear();
369 let read = f.read_line(&mut buf)?;
370 if read == 0 || buf.trim_end().is_empty() {
371 buf.truncate(buf.len() - read);
372 break;
373 }
374 let mut iter = buf.splitn(2, ':');
375 if let Some(key) = iter.next() {
376 if let Some(value) = iter.next() {
377 let value = value.trim();
378 match key.to_lowercase().as_str() {
379 "expression" => rv.expression = Some(value.to_string()),
380 "source" => rv.source = Some(value.into()),
381 _ => {}
382 }
383 }
384 }
385 }
386 elog!("A snapshot uses a legacy snapshot format; please update it to the new format with `cargo insta test --force-update-snapshots --accept`.\nSnapshot is at: {}", p.to_string_lossy());
387 rv
388 };
389
390 let contents = match metadata.snapshot_kind {
391 SnapshotKind::Text => {
392 buf.clear();
393 for (idx, line) in f.lines().enumerate() {
394 let line = line?;
395 if idx > 0 {
396 buf.push('\n');
397 }
398 buf.push_str(&line);
399 }
400
401 TextSnapshotContents {
402 contents: buf,
403 kind: TextSnapshotKind::File,
404 }
405 .into()
406 }
407 SnapshotKind::Binary { ref extension } => {
408 let path = build_binary_path(extension, p);
409 let contents = fs::read(path)?;
410
411 SnapshotContents::Binary(Rc::new(contents))
412 }
413 };
414
415 let (snapshot_name, module_name) = names_of_path(p);
416
417 Ok(Snapshot::from_components(
418 module_name,
419 Some(snapshot_name),
420 metadata,
421 contents,
422 ))
423 }
424
425 pub(crate) fn from_components(
426 module_name: String,
427 snapshot_name: Option<String>,
428 metadata: MetaData,
429 snapshot: SnapshotContents,
430 ) -> Snapshot {
431 Snapshot {
432 module_name,
433 snapshot_name,
434 metadata,
435 snapshot,
436 }
437 }
438
439 #[cfg(feature = "_cargo_insta_internal")]
440 fn from_content(content: Content, kind: TextSnapshotKind) -> Result<Snapshot, Box<dyn Error>> {
441 if let Content::Map(map) = content {
442 let mut module_name = None;
443 let mut snapshot_name = None;
444 let mut metadata = None;
445 let mut snapshot = None;
446
447 for (key, value) in map.into_iter() {
448 match key.as_str() {
449 Some("module_name") => module_name = value.as_str().map(|x| x.to_string()),
450 Some("snapshot_name") => snapshot_name = value.as_str().map(|x| x.to_string()),
451 Some("metadata") => metadata = Some(MetaData::from_content(value)?),
452 Some("snapshot") => {
453 snapshot = Some(
454 TextSnapshotContents {
455 contents: value
456 .as_str()
457 .ok_or(content::Error::UnexpectedDataType)?
458 .to_string(),
459 kind,
460 }
461 .into(),
462 );
463 }
464 _ => {}
465 }
466 }
467
468 Ok(Snapshot {
469 module_name: module_name.ok_or(content::Error::MissingField)?,
470 snapshot_name,
471 metadata: metadata.ok_or(content::Error::MissingField)?,
472 snapshot: snapshot.ok_or(content::Error::MissingField)?,
473 })
474 } else {
475 Err(content::Error::UnexpectedDataType.into())
476 }
477 }
478
479 fn as_content(&self) -> Content {
480 let mut fields = vec![("module_name", Content::from(self.module_name.as_str()))];
481 if let Some(name) = self.snapshot_name.as_deref() {
484 fields.push(("snapshot_name", Content::from(name)));
485 }
486 fields.push(("metadata", self.metadata.as_content()));
487
488 if let SnapshotContents::Text(ref content) = self.snapshot {
489 fields.push(("snapshot", Content::from(content.to_string())));
490 }
491
492 Content::Struct("Content", fields)
493 }
494
495 pub fn module_name(&self) -> &str {
497 &self.module_name
498 }
499
500 pub fn snapshot_name(&self) -> Option<&str> {
502 self.snapshot_name.as_deref()
503 }
504
505 pub fn metadata(&self) -> &MetaData {
507 &self.metadata
508 }
509
510 pub fn contents(&self) -> &SnapshotContents {
512 &self.snapshot
513 }
514
515 pub fn as_text(&self) -> Option<&TextSnapshotContents> {
517 self.snapshot.as_text()
518 }
519
520 fn serialize_snapshot(&self, md: &MetaData) -> String {
521 let mut buf = yaml::to_string(&md.as_content());
522 buf.push_str("---\n");
523
524 if let SnapshotContents::Text(ref contents) = self.snapshot {
525 buf.push_str(&contents.to_string());
526 buf.push('\n');
527 }
528
529 buf
530 }
531
532 fn save_with_metadata(&self, path: &Path, md: &MetaData) -> Result<(), Box<dyn Error>> {
536 if let Some(folder) = path.parent() {
537 fs::create_dir_all(folder)?;
538 }
539
540 let serialized_snapshot = self.serialize_snapshot(md);
541 fs::write(path, serialized_snapshot)
542 .map_err(|e| content::Error::FileIo(e, path.to_path_buf()))?;
543
544 if let SnapshotContents::Binary(ref contents) = self.snapshot {
545 fs::write(self.build_binary_path(path).unwrap(), &**contents)
546 .map_err(|e| content::Error::FileIo(e, path.to_path_buf()))?;
547 }
548
549 Ok(())
550 }
551
552 pub fn build_binary_path(&self, path: impl Into<PathBuf>) -> Option<PathBuf> {
553 if let SnapshotKind::Binary { ref extension } = self.metadata.snapshot_kind {
554 Some(build_binary_path(extension, path))
555 } else {
556 None
557 }
558 }
559
560 #[doc(hidden)]
562 pub fn save(&self, path: &Path) -> Result<(), Box<dyn Error>> {
563 self.save_with_metadata(path, &self.metadata.trim_for_persistence())
564 }
565
566 pub(crate) fn save_new(&self, path: &Path) -> Result<PathBuf, Box<dyn Error>> {
571 let new_path = path.to_path_buf().with_extension("snap.new");
574 self.save_with_metadata(&new_path, &self.metadata)?;
575 Ok(new_path)
576 }
577}
578
579#[derive(Debug, Clone)]
581pub enum SnapshotContents {
582 Text(TextSnapshotContents),
583
584 Binary(Rc<Vec<u8>>),
589}
590
591#[derive(Debug, PartialEq, Eq, Clone)]
593pub struct TextSnapshotContents {
594 contents: String,
595 pub kind: TextSnapshotKind,
596}
597
598impl From<TextSnapshotContents> for SnapshotContents {
599 fn from(value: TextSnapshotContents) -> Self {
600 SnapshotContents::Text(value)
601 }
602}
603
604impl SnapshotContents {
605 pub fn is_binary(&self) -> bool {
606 matches!(self, SnapshotContents::Binary(_))
607 }
608
609 pub fn as_text(&self) -> Option<&TextSnapshotContents> {
611 match self {
612 SnapshotContents::Text(t) => Some(t),
613 SnapshotContents::Binary(_) => None,
614 }
615 }
616}
617
618impl TextSnapshotContents {
619 pub fn new(contents: String, kind: TextSnapshotKind) -> TextSnapshotContents {
620 TextSnapshotContents { contents, kind }
625 }
626
627 pub fn matches_fully(&self, other: &TextSnapshotContents) -> bool {
629 self.contents == other.contents
630 }
631
632 pub fn matches_latest(&self, other: &Self) -> bool {
634 self.to_string() == other.to_string()
635 }
636
637 pub fn matches_legacy(&self, other: &Self) -> bool {
638 fn as_str_legacy(sc: &TextSnapshotContents) -> String {
639 let out = sc.to_string();
641 let out = out.trim_start_matches(['\r', '\n']);
643 let out = match out.strip_prefix("---\n") {
646 Some(old_snapshot) => old_snapshot,
647 None => out,
648 };
649 match sc.kind {
650 TextSnapshotKind::Inline => {
651 let out = legacy_inline_normalize(out);
652 let is_legacy_single_line_in_multiline =
668 sc.contents.contains('\n') && sc.contents.trim_end().lines().count() <= 1;
669 if is_legacy_single_line_in_multiline {
670 out.trim_start().to_string()
671 } else {
672 out
673 }
674 }
675 TextSnapshotKind::File => out.to_string(),
676 }
677 }
678 as_str_legacy(self) == as_str_legacy(other)
679 }
680
681 fn needs_escaped_format(contents: &str) -> bool {
698 contents
699 .chars()
700 .any(|c| c.is_control() && !['\n', '\t', '\x1b'].contains(&c))
701 }
702
703 pub(crate) fn from_inline_literal(contents: &str) -> Self {
704 if contents.trim_end().lines().count() <= 1 {
706 return Self::new(contents.trim_end().to_string(), TextSnapshotKind::Inline);
707 }
708
709 if Self::needs_escaped_format(contents) {
712 return Self::new(contents.trim_end().to_string(), TextSnapshotKind::Inline);
713 }
714
715 let lines = contents.lines().collect::<Vec<&str>>();
718 let (first, remainder) = lines.split_first().unwrap();
719 let snapshot = {
720 if first != &"" {
723 elog!("{} {}{}{}\n{}",style("Multiline inline snapshot values should start and end with a newline.").yellow().bold()," The current value will fail to match in the future. Run `cargo insta test --force-update-snapshots` to rewrite snapshots. The existing value's first line is `", first, "`. Full value:", contents);
724 once(first)
725 .chain(remainder.iter())
726 .cloned()
727 .collect::<Vec<&str>>()
728 .join("\n")
729 } else {
730 remainder.join("\n")
731 }
732 };
733 Self::new(snapshot, TextSnapshotKind::Inline)
734 }
735
736 fn normalize(&self) -> String {
737 let kind_specific_normalization = match self.kind {
738 TextSnapshotKind::Inline => normalize_inline(&self.contents),
739 TextSnapshotKind::File => self.contents.clone(),
740 };
741 let out = kind_specific_normalization.trim_end();
743 out.replace("\r\n", "\n")
749 }
750
751 pub fn to_inline(&self, indentation: &str) -> String {
754 let contents = self.normalize();
755 let mut out = String::new();
756
757 let needs_escaping = Self::needs_escaped_format(&contents);
758
759 if !needs_escaping && contents.contains(['\\', '"']) {
765 out.push('r');
766 }
767
768 let delimiter = "#".repeat(required_hashes(&contents));
769
770 out.push_str(&delimiter);
771
772 if needs_escaping {
775 out.push_str(format!("{contents:?}").as_str());
776 } else {
777 out.push('"');
778 if contents.contains('\n') {
781 out.extend(
782 contents
783 .lines()
784 .map(|l| {
788 format!(
789 "\n{i}{l}",
790 i = if l.is_empty() { "" } else { indentation },
791 l = l
792 )
793 })
794 .chain(Some(format!("\n{indentation}"))),
797 );
798 } else {
799 out.push_str(contents.as_str());
800 }
801 out.push('"');
802 }
803
804 out.push_str(&delimiter);
805 out
806 }
807}
808
809impl fmt::Display for TextSnapshotContents {
810 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
813 write!(f, "{}", self.normalize())
814 }
815}
816
817impl PartialEq for SnapshotContents {
818 fn eq(&self, other: &Self) -> bool {
819 match (self, other) {
820 (SnapshotContents::Text(this), SnapshotContents::Text(other)) => {
821 if this.matches_latest(other) {
823 true
824 } else if this.matches_legacy(other) {
825 elog!("{} {}\n{}",style("Snapshot test passes but the existing value is in a legacy format. Please run `cargo insta test --force-update-snapshots` to update to a newer format.").yellow().bold(),"Snapshot contents:", this.to_string());
826 true
827 } else {
828 false
829 }
830 }
831 (SnapshotContents::Binary(this), SnapshotContents::Binary(other)) => this == other,
832 _ => false,
833 }
834 }
835}
836
837fn build_binary_path(extension: &str, path: impl Into<PathBuf>) -> PathBuf {
838 let path = path.into();
839 let mut new_extension = path.extension().unwrap().to_os_string();
840 new_extension.push(".");
841 new_extension.push(extension);
842
843 path.with_extension(new_extension)
844}
845
846fn required_hashes(text: &str) -> usize {
848 text.split('"')
849 .skip(1) .map(|s| s.chars().take_while(|&c| c == '#').count() + 1)
851 .max()
852 .unwrap_or_default()
853}
854
855#[test]
856fn test_required_hashes() {
857 assert_snapshot!(required_hashes(""), @"0");
858 assert_snapshot!(required_hashes("Hello, world!"), @"0");
859 assert_snapshot!(required_hashes("\"\""), @"1");
860 assert_snapshot!(required_hashes("##"), @"0");
861 assert_snapshot!(required_hashes("\"#\"#"), @"2");
862 assert_snapshot!(required_hashes(r##""#"##), @"2");
863 assert_snapshot!(required_hashes(r######"foo ""##### bar "###" baz"######), @"6");
864 assert_snapshot!(required_hashes("\"\"\""), @"1");
865 assert_snapshot!(required_hashes("####"), @"0");
866 assert_snapshot!(required_hashes(r###"\"\"##\"\""###), @"3");
867 assert_snapshot!(required_hashes(r###"r"#"Raw string"#""###), @"2");
868}
869
870fn leading_space(value: &str) -> String {
871 value
872 .chars()
873 .take_while(|x| *x == ' ' || *x == '\t')
876 .collect::<String>()
877}
878
879fn min_indentation(snapshot: &str) -> String {
880 let lines = snapshot.trim_end().lines();
881
882 lines
883 .filter(|l| !l.is_empty())
884 .map(leading_space)
885 .min_by(|a, b| a.len().cmp(&b.len()))
886 .unwrap_or("".into())
887}
888
889fn normalize_inline(snapshot: &str) -> String {
893 if snapshot.trim_end().lines().count() <= 1 {
895 return snapshot.trim_end().to_string();
896 }
897
898 let indentation = min_indentation(snapshot);
899 snapshot
900 .lines()
901 .map(|l| l.get(indentation.len()..).unwrap_or(""))
902 .collect::<Vec<&str>>()
903 .join("\n")
904}
905
906#[test]
907fn test_normalize_inline_snapshot() {
908 fn normalized_of_literal(snapshot: &str) -> String {
909 normalize_inline(&TextSnapshotContents::from_inline_literal(snapshot).contents)
910 }
911
912 use similar_asserts::assert_eq;
913 assert_eq!(
917 normalized_of_literal(
918 "
919 1
920 2
921"
922 ),
923 "1
9242"
925 );
926
927 assert_eq!(
928 normalized_of_literal(
929 "
930 1
931 2
932 "
933 ),
934 " 1
9352
936"
937 );
938
939 assert_eq!(
940 normalized_of_literal(
941 "
942 1
943 2
944 "
945 ),
946 "1
9472
948"
949 );
950
951 assert_eq!(
952 normalized_of_literal(
953 "
954 1
955 2
956"
957 ),
958 "1
9592"
960 );
961
962 assert_eq!(
963 normalized_of_literal(
964 "
965 a
966 "
967 ),
968 " a"
969 );
970
971 assert_eq!(normalized_of_literal(""), "");
972
973 assert_eq!(
974 normalized_of_literal(
975 "
976 a
977 b
978c
979 "
980 ),
981 " a
982 b
983c
984 "
985 );
986
987 assert_eq!(
988 normalized_of_literal(
989 "
990a
991 "
992 ),
993 "a"
994 );
995
996 assert_eq!(
1001 normalized_of_literal(
1002 "
1003 a"
1004 ),
1005 " a"
1006 );
1007
1008 }
1018
1019fn names_of_path(path: &Path) -> (String, String) {
1021 let parts: Vec<&str> = path
1024 .file_stem()
1025 .unwrap()
1026 .to_str()
1027 .unwrap_or("")
1028 .rsplitn(2, "__")
1029 .collect();
1030
1031 match parts.as_slice() {
1032 [snapshot_name, module_name] => (snapshot_name.to_string(), module_name.to_string()),
1033 [snapshot_name] => (snapshot_name.to_string(), String::new()),
1034 _ => (String::new(), "<unknown>".to_string()),
1035 }
1036}
1037
1038#[test]
1039fn test_names_of_path() {
1040 assert_debug_snapshot!(
1041 names_of_path(Path::new("/src/snapshots/insta_tests__tests__name_foo.snap")), @r#"
1042 (
1043 "name_foo",
1044 "insta_tests__tests",
1045 )
1046 "#
1047 );
1048 assert_debug_snapshot!(
1049 names_of_path(Path::new("/src/snapshots/name_foo.snap")), @r#"
1050 (
1051 "name_foo",
1052 "",
1053 )
1054 "#
1055 );
1056 assert_debug_snapshot!(
1057 names_of_path(Path::new("foo/src/snapshots/go1.20.5.snap")), @r#"
1058 (
1059 "go1.20.5",
1060 "",
1061 )
1062 "#
1063 );
1064}
1065
1066fn legacy_inline_normalize(frozen_value: &str) -> String {
1068 if !frozen_value.trim_start().starts_with('⋮') {
1069 return frozen_value.to_string();
1070 }
1071 let mut buf = String::new();
1072 let mut line_iter = frozen_value.lines();
1073 let mut indentation = 0;
1074
1075 for line in &mut line_iter {
1076 let line_trimmed = line.trim_start();
1077 if line_trimmed.is_empty() {
1078 continue;
1079 }
1080 indentation = line.len() - line_trimmed.len();
1081 buf.push_str(&line_trimmed[3..]);
1083 buf.push('\n');
1084 break;
1085 }
1086
1087 for line in &mut line_iter {
1088 if let Some(prefix) = line.get(..indentation) {
1089 if !prefix.trim().is_empty() {
1090 return "".to_string();
1091 }
1092 }
1093 if let Some(remainder) = line.get(indentation..) {
1094 if let Some(rest) = remainder.strip_prefix('⋮') {
1095 buf.push_str(rest);
1096 buf.push('\n');
1097 } else if remainder.trim().is_empty() {
1098 continue;
1099 } else {
1100 return "".to_string();
1101 }
1102 }
1103 }
1104
1105 buf.trim_end().to_string()
1106}
1107
1108#[test]
1109fn test_snapshot_contents_to_inline() {
1110 use similar_asserts::assert_eq;
1111 let snapshot_contents =
1112 TextSnapshotContents::new("testing".to_string(), TextSnapshotKind::Inline);
1113 assert_eq!(snapshot_contents.to_inline(""), r#""testing""#);
1114
1115 assert_eq!(
1116 TextSnapshotContents::new("\na\nb".to_string(), TextSnapshotKind::Inline).to_inline(""),
1117 r##""
1118
1119a
1120b
1121""##
1122 );
1123
1124 assert_eq!(
1125 TextSnapshotContents::new("a\nb".to_string(), TextSnapshotKind::Inline).to_inline(" "),
1126 r##""
1127 a
1128 b
1129 ""##
1130 );
1131
1132 assert_eq!(
1133 TextSnapshotContents::new("\n a\n b".to_string(), TextSnapshotKind::Inline)
1134 .to_inline(""),
1135 r##""
1136
1137a
1138b
1139""##
1140 );
1141
1142 assert_eq!(
1143 TextSnapshotContents::new("\na\n\nb".to_string(), TextSnapshotKind::Inline)
1144 .to_inline(" "),
1145 r##""
1146
1147 a
1148
1149 b
1150 ""##
1151 );
1152
1153 assert_eq!(
1154 TextSnapshotContents::new(
1155 "ab
1156 "
1157 .to_string(),
1158 TextSnapshotKind::Inline
1159 )
1160 .to_inline(""),
1161 r#""ab""#
1162 );
1163
1164 assert_eq!(
1165 TextSnapshotContents::new(
1166 " ab
1167 "
1168 .to_string(),
1169 TextSnapshotKind::Inline
1170 )
1171 .to_inline(""),
1172 r##"" ab""##
1173 );
1174
1175 assert_eq!(
1176 TextSnapshotContents::new("\n ab\n".to_string(), TextSnapshotKind::Inline).to_inline(""),
1177 r##""
1178
1179ab
1180""##
1181 );
1182
1183 assert_eq!(
1184 TextSnapshotContents::new("ab".to_string(), TextSnapshotKind::Inline).to_inline(""),
1185 r#""ab""#
1186 );
1187
1188 assert_eq!(
1190 TextSnapshotContents::new("a\tb".to_string(), TextSnapshotKind::Inline).to_inline(""),
1191 r##""a b""##
1192 );
1193
1194 assert_eq!(
1195 TextSnapshotContents::new("a\t\nb".to_string(), TextSnapshotKind::Inline).to_inline(""),
1196 "\"
1197a\t
1198b
1199\""
1200 );
1201
1202 assert_eq!(
1203 TextSnapshotContents::new("a\rb".to_string(), TextSnapshotKind::Inline).to_inline(""),
1204 r##""a\rb""##
1205 );
1206
1207 assert_eq!(
1210 TextSnapshotContents::new("\n\r foo bar".to_string(), TextSnapshotKind::Inline)
1211 .to_inline(""),
1212 r##""\n\r foo bar""##
1213 );
1214
1215 assert_eq!(
1216 TextSnapshotContents::new("a\0b".to_string(), TextSnapshotKind::Inline).to_inline(""),
1217 r##""a\0b""##
1219 );
1220
1221 assert_eq!(
1222 TextSnapshotContents::new("a\u{FFFD}b".to_string(), TextSnapshotKind::Inline).to_inline(""),
1223 r##""a�b""##
1225 );
1226
1227 assert_eq!(
1229 TextSnapshotContents::new("hello\r\nworld".to_string(), TextSnapshotKind::Inline)
1230 .to_inline(" "),
1231 r##""
1233 hello
1234 world
1235 ""##
1236 );
1237
1238 assert_eq!(
1239 TextSnapshotContents::new("\r\nhello".to_string(), TextSnapshotKind::Inline).to_inline(""),
1240 r##""
1242
1243hello
1244""##
1245 );
1246
1247 assert_eq!(
1248 TextSnapshotContents::new("hello\r\n".to_string(), TextSnapshotKind::Inline).to_inline(""),
1249 r##""hello""##
1251 );
1252
1253 assert_eq!(
1256 TextSnapshotContents::new("hello\n\rworld".to_string(), TextSnapshotKind::Inline)
1257 .to_inline(""),
1258 r##""hello\n\rworld""##
1259 );
1260
1261 assert_eq!(
1263 TextSnapshotContents::new("a\rb\r\nc".to_string(), TextSnapshotKind::Inline).to_inline(""),
1264 r##""a\rb\nc""##
1266 );
1267}
1268
1269#[test]
1272fn test_escaped_format_preserves_content() {
1273 assert_eq!(
1281 TextSnapshotContents::from_inline_literal("\n\r foo").contents,
1282 "\n\r foo"
1283 );
1284 assert_eq!(
1285 TextSnapshotContents::from_inline_literal("a\rb").contents,
1286 "a\rb"
1287 );
1288
1289 assert_eq!(
1292 TextSnapshotContents::from_inline_literal("\nfoo\nbar\n").contents,
1293 "foo\nbar"
1294 );
1295}
1296
1297#[test]
1298fn test_snapshot_contents_hashes() {
1299 assert_eq!(
1300 TextSnapshotContents::new("a###b".to_string(), TextSnapshotKind::Inline).to_inline(""),
1301 r#""a###b""#
1302 );
1303
1304 assert_eq!(
1305 TextSnapshotContents::new("a\n\\###b".to_string(), TextSnapshotKind::Inline).to_inline(""),
1306 r#####"r"
1307a
1308\###b
1309""#####
1310 );
1311}
1312
1313#[test]
1314fn test_snapshot_contents_as_text() {
1315 let text = SnapshotContents::Text(TextSnapshotContents::new(
1316 "hello".to_string(),
1317 TextSnapshotKind::Inline,
1318 ));
1319 assert!(text.as_text().is_some());
1320 assert_eq!(text.as_text().unwrap().to_string(), "hello");
1321
1322 let binary = SnapshotContents::Binary(Rc::new(vec![1, 2, 3]));
1323 assert!(binary.as_text().is_none());
1324}
1325
1326#[test]
1327fn test_min_indentation() {
1328 use similar_asserts::assert_eq;
1329 assert_eq!(
1330 min_indentation(
1331 "
1332 1
1333 2
1334 ",
1335 ),
1336 " ".to_string()
1337 );
1338
1339 assert_eq!(
1340 min_indentation(
1341 "
1342 1
1343 2"
1344 ),
1345 " ".to_string()
1346 );
1347
1348 assert_eq!(
1349 min_indentation(
1350 "
1351 1
1352 2
1353 "
1354 ),
1355 " ".to_string()
1356 );
1357
1358 assert_eq!(
1359 min_indentation(
1360 "
1361 1
1362 2
1363"
1364 ),
1365 " ".to_string()
1366 );
1367
1368 assert_eq!(
1369 min_indentation(
1370 "
1371 a
1372 "
1373 ),
1374 " ".to_string()
1375 );
1376
1377 assert_eq!(min_indentation(""), "".to_string());
1378
1379 assert_eq!(
1380 min_indentation(
1381 "
1382 a
1383 b
1384c
1385 "
1386 ),
1387 "".to_string()
1388 );
1389
1390 assert_eq!(
1391 min_indentation(
1392 "
1393a
1394 "
1395 ),
1396 "".to_string()
1397 );
1398
1399 assert_eq!(
1400 min_indentation(
1401 "
1402 a"
1403 ),
1404 " ".to_string()
1405 );
1406
1407 assert_eq!(
1408 min_indentation(
1409 "a
1410 a"
1411 ),
1412 "".to_string()
1413 );
1414
1415 assert_eq!(
1416 normalize_inline(
1417 "
1418 1
1419 2"
1420 ),
1421 "
1422 1
14232"
1424 );
1425
1426 assert_eq!(
1427 normalize_inline(
1428 "
1429 1
1430 2
1431 "
1432 ),
1433 "
14341
14352
1436"
1437 );
1438}
1439
1440#[test]
1441fn test_min_indentation_additional() {
1442 use similar_asserts::assert_eq;
1443
1444 let t = "
1445 1
1446 2
1447";
1448 assert_eq!(min_indentation(t), " ".to_string());
1449
1450 let t = "
1451 a
1452 ";
1453 assert_eq!(min_indentation(t), " ".to_string());
1454
1455 let t = "";
1456 assert_eq!(min_indentation(t), "".to_string());
1457
1458 let t = "
1459 a
1460 b
1461c
1462 ";
1463 assert_eq!(min_indentation(t), "".to_string());
1464
1465 let t = "
1466a";
1467 assert_eq!(min_indentation(t), "".to_string());
1468
1469 let t = "
1470 a";
1471 assert_eq!(min_indentation(t), " ".to_string());
1472
1473 let t = "a
1474 a";
1475 assert_eq!(min_indentation(t), "".to_string());
1476
1477 let t = "
1478 1
1479 2
1480 ";
1481 assert_eq!(min_indentation(t), " ".to_string());
1482
1483 let t = "
1484 1
1485 2";
1486 assert_eq!(min_indentation(t), " ".to_string());
1487
1488 let t = "
1489 1
1490 2";
1491 assert_eq!(min_indentation(t), " ".to_string());
1492}
1493
1494#[test]
1495fn test_inline_snapshot_value_newline() {
1496 assert_eq!(normalize_inline("\n"), "");
1498}
1499
1500#[test]
1501fn test_parse_yaml_error() {
1502 use std::env::temp_dir;
1503 let mut temp = temp_dir();
1504 temp.push("bad.yaml");
1505 let mut f = fs::File::create(temp.clone()).unwrap();
1506
1507 let invalid = "---
1508 This is invalid yaml:
1509 {
1510 {
1511 ---
1512 ";
1513
1514 f.write_all(invalid.as_bytes()).unwrap();
1515
1516 let error = format!("{}", Snapshot::from_file(temp.as_path()).unwrap_err());
1517 assert!(error.contains("Failed parsing the YAML from"));
1518 assert!(error.contains("bad.yaml"));
1519}
1520
1521#[test]
1523fn test_ownership() {
1524 use std::ops::Range;
1526 let r = Range { start: 0, end: 10 };
1527 assert_debug_snapshot!(r, @"0..10");
1528 assert_debug_snapshot!(r, @"0..10");
1529}
1530
1531#[test]
1532fn test_empty_lines() {
1533 assert_snapshot!("single line should fit on a single line", @"single line should fit on a single line");
1534 assert_snapshot!("single line should fit on a single line, even if it's really really really really really really really really really long", @"single line should fit on a single line, even if it's really really really really really really really really really long");
1535
1536 assert_snapshot!("multiline content starting on first line
1537
1538 final line
1539 ", @"
1540 multiline content starting on first line
1541
1542 final line
1543 ");
1544
1545 assert_snapshot!("
1546 multiline content starting on second line
1547
1548 final line
1549 ", @"
1550
1551 multiline content starting on second line
1552
1553 final line
1554 ");
1555}