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 let mut f = fs::OpenOptions::new().create(true).append(true).open(p)?;
78 let mut s = json::to_string(&self.as_content());
79 s.push('\n');
80 f.write_all(s.as_bytes())?;
81 Ok(())
82 }
83
84 #[cfg(feature = "_cargo_insta_internal")]
85 fn from_content(content: Content) -> Result<PendingInlineSnapshot, Box<dyn Error>> {
86 if let Content::Map(map) = content {
87 let mut run_id = None;
88 let mut line = None;
89 let mut old = None;
90 let mut new = None;
91
92 for (key, value) in map.into_iter() {
93 match key.as_str() {
94 Some("run_id") => run_id = value.as_str().map(|x| x.to_string()),
95 Some("line") => line = value.as_u64().map(|x| x as u32),
96 Some("old") if !value.is_nil() => {
97 old = Some(Snapshot::from_content(value, TextSnapshotKind::Inline)?)
98 }
99 Some("new") if !value.is_nil() => {
100 new = Some(Snapshot::from_content(value, TextSnapshotKind::Inline)?)
101 }
102 _ => {}
103 }
104 }
105
106 Ok(PendingInlineSnapshot {
107 run_id: run_id.ok_or(content::Error::MissingField)?,
108 line: line.ok_or(content::Error::MissingField)?,
109 new,
110 old,
111 })
112 } else {
113 Err(content::Error::UnexpectedDataType.into())
114 }
115 }
116
117 fn as_content(&self) -> Content {
118 let fields = vec![
119 ("run_id", Content::from(self.run_id.as_str())),
120 ("line", Content::from(self.line)),
121 (
122 "new",
123 match &self.new {
124 Some(snap) => snap.as_content(),
125 None => Content::None,
126 },
127 ),
128 (
129 "old",
130 match &self.old {
131 Some(snap) => snap.as_content(),
132 None => Content::None,
133 },
134 ),
135 ];
136
137 Content::Struct("PendingInlineSnapshot", fields)
138 }
139}
140
141#[derive(Debug, Clone, PartialEq, Default)]
142pub enum SnapshotKind {
143 #[default]
144 Text,
145 Binary {
146 extension: String,
147 },
148}
149
150#[derive(Debug, Default, Clone, PartialEq)]
152pub struct MetaData {
153 pub(crate) source: Option<String>,
155 pub(crate) assertion_line: Option<u32>,
158 pub(crate) description: Option<String>,
160 pub(crate) expression: Option<String>,
162 pub(crate) info: Option<Content>,
164 pub(crate) input_file: Option<String>,
166 pub(crate) snapshot_kind: SnapshotKind,
168}
169
170impl MetaData {
171 pub fn source(&self) -> Option<&str> {
173 self.source.as_deref()
174 }
175
176 pub fn assertion_line(&self) -> Option<u32> {
178 self.assertion_line
179 }
180
181 pub fn expression(&self) -> Option<&str> {
183 self.expression.as_deref()
184 }
185
186 pub fn description(&self) -> Option<&str> {
188 self.description.as_deref().filter(|x| !x.is_empty())
189 }
190
191 #[doc(hidden)]
193 pub fn private_info(&self) -> Option<&Content> {
194 self.info.as_ref()
195 }
196
197 pub fn get_relative_source(&self, base: &Path) -> Option<PathBuf> {
199 self.source.as_ref().map(|source| {
200 base.join(source)
201 .canonicalize()
202 .ok()
203 .and_then(|s| s.strip_prefix(base).ok().map(|x| x.to_path_buf()))
204 .unwrap_or_else(|| base.to_path_buf())
205 })
206 }
207
208 pub fn input_file(&self) -> Option<&str> {
210 self.input_file.as_deref()
211 }
212
213 fn from_content(content: Content) -> Result<MetaData, Box<dyn Error>> {
214 if let Content::Map(map) = content {
215 let mut source = None;
216 let mut assertion_line = None;
217 let mut description = None;
218 let mut expression = None;
219 let mut info = None;
220 let mut input_file = None;
221 let mut snapshot_type = TmpSnapshotKind::Text;
222 let mut extension = None;
223
224 enum TmpSnapshotKind {
225 Text,
226 Binary,
227 }
228
229 for (key, value) in map.into_iter() {
230 match key.as_str() {
231 Some("source") => source = value.as_str().map(|x| x.to_string()),
232 Some("assertion_line") => assertion_line = value.as_u64().map(|x| x as u32),
233 Some("description") => description = value.as_str().map(Into::into),
234 Some("expression") => expression = value.as_str().map(Into::into),
235 Some("info") if !value.is_nil() => info = Some(value),
236 Some("input_file") => input_file = value.as_str().map(Into::into),
237 Some("snapshot_kind") => {
238 snapshot_type = match value.as_str() {
239 Some("binary") => TmpSnapshotKind::Binary,
240 _ => TmpSnapshotKind::Text,
241 }
242 }
243 Some("extension") => {
244 extension = value.as_str().map(Into::into);
245 }
246 _ => {}
247 }
248 }
249
250 Ok(MetaData {
251 source,
252 assertion_line,
253 description,
254 expression,
255 info,
256 input_file,
257 snapshot_kind: match snapshot_type {
258 TmpSnapshotKind::Text => SnapshotKind::Text,
259 TmpSnapshotKind::Binary => SnapshotKind::Binary {
260 extension: extension.ok_or(content::Error::MissingField)?,
261 },
262 },
263 })
264 } else {
265 Err(content::Error::UnexpectedDataType.into())
266 }
267 }
268
269 fn as_content(&self) -> Content {
270 let mut fields = Vec::new();
271 if let Some(source) = self.source.as_deref() {
272 fields.push(("source", Content::from(source)));
273 }
274 if let Some(line) = self.assertion_line {
275 fields.push(("assertion_line", Content::from(line)));
276 }
277 if let Some(description) = self.description.as_deref() {
278 fields.push(("description", Content::from(description)));
279 }
280 if let Some(expression) = self.expression.as_deref() {
281 fields.push(("expression", Content::from(expression)));
282 }
283 if let Some(info) = &self.info {
284 fields.push(("info", info.to_owned()));
285 }
286 if let Some(input_file) = self.input_file.as_deref() {
287 fields.push(("input_file", Content::from(input_file)));
288 }
289
290 match self.snapshot_kind {
291 SnapshotKind::Text => {}
292 SnapshotKind::Binary { ref extension } => {
293 fields.push(("extension", Content::from(extension.clone())));
294 fields.push(("snapshot_kind", Content::from("binary")));
295 }
296 }
297
298 Content::Struct("MetaData", fields)
299 }
300
301 fn trim_for_persistence(&self) -> Cow<'_, MetaData> {
304 if self.assertion_line.is_some() {
312 let mut rv = self.clone();
313 rv.assertion_line = None;
314 Cow::Owned(rv)
315 } else {
316 Cow::Borrowed(self)
317 }
318 }
319}
320
321#[derive(Debug, PartialEq, Eq, Clone, Copy)]
322pub enum TextSnapshotKind {
323 Inline,
324 File,
325}
326
327#[derive(Debug, Clone)]
329pub struct Snapshot {
330 module_name: String,
331 snapshot_name: Option<String>,
332 metadata: MetaData,
333 snapshot: SnapshotContents,
334}
335
336impl Snapshot {
337 pub fn from_file(p: &Path) -> Result<Snapshot, Box<dyn Error>> {
339 let mut f = BufReader::new(fs::File::open(p)?);
340 let mut buf = String::new();
341
342 f.read_line(&mut buf)?;
343
344 let metadata = if buf.trim_end() == "---" {
346 loop {
347 let read = f.read_line(&mut buf)?;
348 if read == 0 {
349 break;
350 }
351 if buf[buf.len() - read..].trim_end() == "---" {
352 buf.truncate(buf.len() - read);
353 break;
354 }
355 }
356 let content = yaml::parse_str(&buf, p)?;
357 MetaData::from_content(content)?
358 } else {
362 let mut rv = MetaData::default();
363 loop {
364 buf.clear();
365 let read = f.read_line(&mut buf)?;
366 if read == 0 || buf.trim_end().is_empty() {
367 buf.truncate(buf.len() - read);
368 break;
369 }
370 let mut iter = buf.splitn(2, ':');
371 if let Some(key) = iter.next() {
372 if let Some(value) = iter.next() {
373 let value = value.trim();
374 match key.to_lowercase().as_str() {
375 "expression" => rv.expression = Some(value.to_string()),
376 "source" => rv.source = Some(value.into()),
377 _ => {}
378 }
379 }
380 }
381 }
382 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());
383 rv
384 };
385
386 let contents = match metadata.snapshot_kind {
387 SnapshotKind::Text => {
388 buf.clear();
389 for (idx, line) in f.lines().enumerate() {
390 let line = line?;
391 if idx > 0 {
392 buf.push('\n');
393 }
394 buf.push_str(&line);
395 }
396
397 TextSnapshotContents {
398 contents: buf,
399 kind: TextSnapshotKind::File,
400 }
401 .into()
402 }
403 SnapshotKind::Binary { ref extension } => {
404 let path = build_binary_path(extension, p);
405 let contents = fs::read(path)?;
406
407 SnapshotContents::Binary(Rc::new(contents))
408 }
409 };
410
411 let (snapshot_name, module_name) = names_of_path(p);
412
413 Ok(Snapshot::from_components(
414 module_name,
415 Some(snapshot_name),
416 metadata,
417 contents,
418 ))
419 }
420
421 pub(crate) fn from_components(
422 module_name: String,
423 snapshot_name: Option<String>,
424 metadata: MetaData,
425 snapshot: SnapshotContents,
426 ) -> Snapshot {
427 Snapshot {
428 module_name,
429 snapshot_name,
430 metadata,
431 snapshot,
432 }
433 }
434
435 #[cfg(feature = "_cargo_insta_internal")]
436 fn from_content(content: Content, kind: TextSnapshotKind) -> Result<Snapshot, Box<dyn Error>> {
437 if let Content::Map(map) = content {
438 let mut module_name = None;
439 let mut snapshot_name = None;
440 let mut metadata = None;
441 let mut snapshot = None;
442
443 for (key, value) in map.into_iter() {
444 match key.as_str() {
445 Some("module_name") => module_name = value.as_str().map(|x| x.to_string()),
446 Some("snapshot_name") => snapshot_name = value.as_str().map(|x| x.to_string()),
447 Some("metadata") => metadata = Some(MetaData::from_content(value)?),
448 Some("snapshot") => {
449 snapshot = Some(
450 TextSnapshotContents {
451 contents: value
452 .as_str()
453 .ok_or(content::Error::UnexpectedDataType)?
454 .to_string(),
455 kind,
456 }
457 .into(),
458 );
459 }
460 _ => {}
461 }
462 }
463
464 Ok(Snapshot {
465 module_name: module_name.ok_or(content::Error::MissingField)?,
466 snapshot_name,
467 metadata: metadata.ok_or(content::Error::MissingField)?,
468 snapshot: snapshot.ok_or(content::Error::MissingField)?,
469 })
470 } else {
471 Err(content::Error::UnexpectedDataType.into())
472 }
473 }
474
475 fn as_content(&self) -> Content {
476 let mut fields = vec![("module_name", Content::from(self.module_name.as_str()))];
477 if let Some(name) = self.snapshot_name.as_deref() {
480 fields.push(("snapshot_name", Content::from(name)));
481 }
482 fields.push(("metadata", self.metadata.as_content()));
483
484 if let SnapshotContents::Text(ref content) = self.snapshot {
485 fields.push(("snapshot", Content::from(content.to_string())));
486 }
487
488 Content::Struct("Content", fields)
489 }
490
491 pub fn module_name(&self) -> &str {
493 &self.module_name
494 }
495
496 pub fn snapshot_name(&self) -> Option<&str> {
498 self.snapshot_name.as_deref()
499 }
500
501 pub fn metadata(&self) -> &MetaData {
503 &self.metadata
504 }
505
506 pub fn contents(&self) -> &SnapshotContents {
508 &self.snapshot
509 }
510
511 pub fn matches(&self, other: &Self) -> bool {
513 self.contents() == other.contents()
514 && self.metadata.snapshot_kind == other.metadata.snapshot_kind
516 }
517
518 pub fn matches_fully(&self, other: &Self) -> bool {
522 match (self.contents(), other.contents()) {
523 (SnapshotContents::Text(self_contents), SnapshotContents::Text(other_contents)) => {
524 let contents_match_exact = self_contents.matches_latest(other_contents);
538 match self_contents.kind {
539 TextSnapshotKind::File => {
540 self.metadata.trim_for_persistence()
541 == other.metadata.trim_for_persistence()
542 && contents_match_exact
543 }
544 TextSnapshotKind::Inline => contents_match_exact,
545 }
546 }
547 _ => self.matches(other),
548 }
549 }
550
551 fn serialize_snapshot(&self, md: &MetaData) -> String {
552 let mut buf = yaml::to_string(&md.as_content());
553 buf.push_str("---\n");
554
555 if let SnapshotContents::Text(ref contents) = self.snapshot {
556 buf.push_str(&contents.to_string());
557 buf.push('\n');
558 }
559
560 buf
561 }
562
563 fn save_with_metadata(&self, path: &Path, md: &MetaData) -> Result<(), Box<dyn Error>> {
567 if let Some(folder) = path.parent() {
568 fs::create_dir_all(folder)?;
569 }
570
571 let serialized_snapshot = self.serialize_snapshot(md);
572 fs::write(path, serialized_snapshot)
573 .map_err(|e| content::Error::FileIo(e, path.to_path_buf()))?;
574
575 if let SnapshotContents::Binary(ref contents) = self.snapshot {
576 fs::write(self.build_binary_path(path).unwrap(), &**contents)
577 .map_err(|e| content::Error::FileIo(e, path.to_path_buf()))?;
578 }
579
580 Ok(())
581 }
582
583 pub fn build_binary_path(&self, path: impl Into<PathBuf>) -> Option<PathBuf> {
584 if let SnapshotKind::Binary { ref extension } = self.metadata.snapshot_kind {
585 Some(build_binary_path(extension, path))
586 } else {
587 None
588 }
589 }
590
591 #[doc(hidden)]
593 pub fn save(&self, path: &Path) -> Result<(), Box<dyn Error>> {
594 self.save_with_metadata(path, &self.metadata.trim_for_persistence())
595 }
596
597 pub(crate) fn save_new(&self, path: &Path) -> Result<PathBuf, Box<dyn Error>> {
602 let new_path = path.to_path_buf().with_extension("snap.new");
605 self.save_with_metadata(&new_path, &self.metadata)?;
606 Ok(new_path)
607 }
608}
609
610#[derive(Debug, Clone)]
612pub enum SnapshotContents {
613 Text(TextSnapshotContents),
614
615 Binary(Rc<Vec<u8>>),
620}
621
622#[derive(Debug, PartialEq, Eq, Clone)]
624pub struct TextSnapshotContents {
625 contents: String,
626 pub kind: TextSnapshotKind,
627}
628
629impl From<TextSnapshotContents> for SnapshotContents {
630 fn from(value: TextSnapshotContents) -> Self {
631 SnapshotContents::Text(value)
632 }
633}
634
635impl SnapshotContents {
636 pub fn is_binary(&self) -> bool {
637 matches!(self, SnapshotContents::Binary(_))
638 }
639}
640
641impl TextSnapshotContents {
642 pub fn new(contents: String, kind: TextSnapshotKind) -> TextSnapshotContents {
643 TextSnapshotContents { contents, kind }
648 }
649
650 pub fn matches_fully(&self, other: &TextSnapshotContents) -> bool {
652 self.contents == other.contents
653 }
654
655 pub fn matches_latest(&self, other: &Self) -> bool {
657 self.to_string() == other.to_string()
658 }
659
660 pub fn matches_legacy(&self, other: &Self) -> bool {
661 fn as_str_legacy(sc: &TextSnapshotContents) -> String {
662 let out = sc.to_string();
664 let out = out.trim_start_matches(['\r', '\n']);
666 let out = match out.strip_prefix("---\n") {
669 Some(old_snapshot) => old_snapshot,
670 None => out,
671 };
672 match sc.kind {
673 TextSnapshotKind::Inline => {
674 let out = legacy_inline_normalize(out);
675 let is_legacy_single_line_in_multiline =
691 sc.contents.contains('\n') && sc.contents.trim_end().lines().count() <= 1;
692 if is_legacy_single_line_in_multiline {
693 out.trim_start().to_string()
694 } else {
695 out
696 }
697 }
698 TextSnapshotKind::File => out.to_string(),
699 }
700 }
701 as_str_legacy(self) == as_str_legacy(other)
702 }
703
704 pub(crate) fn from_inline_literal(contents: &str) -> Self {
711 if contents.trim_end().lines().count() <= 1 {
713 return Self::new(contents.trim_end().to_string(), TextSnapshotKind::Inline);
714 }
715
716 let lines = contents.lines().collect::<Vec<&str>>();
719 let (first, remainder) = lines.split_first().unwrap();
720 let snapshot = {
721 if first != &"" {
724 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);
725 once(first)
726 .chain(remainder.iter())
727 .cloned()
728 .collect::<Vec<&str>>()
729 .join("\n")
730 } else {
731 remainder.join("\n")
732 }
733 };
734 Self::new(snapshot, TextSnapshotKind::Inline)
735 }
736
737 fn normalize(&self) -> String {
738 let kind_specific_normalization = match self.kind {
739 TextSnapshotKind::Inline => normalize_inline(&self.contents),
740 TextSnapshotKind::File => self.contents.clone(),
741 };
742 let out = kind_specific_normalization.trim_end();
744 out.replace("\r\n", "\n")
745 }
746
747 pub fn to_inline(&self, indentation: &str) -> String {
750 let contents = self.normalize();
751 let mut out = String::new();
752
753 let has_control_chars = contents
757 .chars()
758 .any(|c| c.is_control() && !['\n', '\t', '\x1b'].contains(&c));
759
760 if !has_control_chars && contents.contains(['\\', '"', '\n']) {
764 out.push('r');
765 }
766
767 let delimiter = "#".repeat(required_hashes(&contents));
768
769 out.push_str(&delimiter);
770
771 if has_control_chars {
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.is_whitespace())
874 .collect::<String>()
875}
876
877fn min_indentation(snapshot: &str) -> String {
878 let lines = snapshot.trim_end().lines();
879
880 lines
881 .filter(|l| !l.is_empty())
882 .map(leading_space)
883 .min_by(|a, b| a.len().cmp(&b.len()))
884 .unwrap_or("".into())
885}
886
887fn normalize_inline(snapshot: &str) -> String {
891 if snapshot.trim_end().lines().count() <= 1 {
893 return snapshot.trim_end().to_string();
894 }
895
896 let indentation = min_indentation(snapshot);
897 snapshot
898 .lines()
899 .map(|l| l.get(indentation.len()..).unwrap_or(""))
900 .collect::<Vec<&str>>()
901 .join("\n")
902}
903
904#[test]
905fn test_normalize_inline_snapshot() {
906 fn normalized_of_literal(snapshot: &str) -> String {
907 normalize_inline(&TextSnapshotContents::from_inline_literal(snapshot).contents)
908 }
909
910 use similar_asserts::assert_eq;
911 assert_eq!(
915 normalized_of_literal(
916 "
917 1
918 2
919"
920 ),
921 "1
9222"
923 );
924
925 assert_eq!(
926 normalized_of_literal(
927 r#"
928 1
929 2
930 "#
931 ),
932 r" 1
9332
934"
935 );
936
937 assert_eq!(
938 normalized_of_literal(
939 "
940 1
941 2
942 "
943 ),
944 r"1
9452
946"
947 );
948
949 assert_eq!(
950 normalized_of_literal(
951 "
952 1
953 2
954"
955 ),
956 "1
9572"
958 );
959
960 assert_eq!(
961 normalized_of_literal(
962 "
963 a
964 "
965 ),
966 " a"
967 );
968
969 assert_eq!(normalized_of_literal(""), "");
970
971 assert_eq!(
972 normalized_of_literal(
973 r#"
974 a
975 b
976c
977 "#
978 ),
979 " a
980 b
981c
982 "
983 );
984
985 assert_eq!(
986 normalized_of_literal(
987 "
988a
989 "
990 ),
991 "a"
992 );
993
994 assert_eq!(
999 normalized_of_literal(
1000 "
1001 a"
1002 ),
1003 " a"
1004 );
1005
1006 }
1016
1017fn names_of_path(path: &Path) -> (String, String) {
1019 let parts: Vec<&str> = path
1022 .file_stem()
1023 .unwrap()
1024 .to_str()
1025 .unwrap_or("")
1026 .rsplitn(2, "__")
1027 .collect();
1028
1029 match parts.as_slice() {
1030 [snapshot_name, module_name] => (snapshot_name.to_string(), module_name.to_string()),
1031 [snapshot_name] => (snapshot_name.to_string(), String::new()),
1032 _ => (String::new(), "<unknown>".to_string()),
1033 }
1034}
1035
1036#[test]
1037fn test_names_of_path() {
1038 assert_debug_snapshot!(
1039 names_of_path(Path::new("/src/snapshots/insta_tests__tests__name_foo.snap")), @r#"
1040 (
1041 "name_foo",
1042 "insta_tests__tests",
1043 )
1044 "#
1045 );
1046 assert_debug_snapshot!(
1047 names_of_path(Path::new("/src/snapshots/name_foo.snap")), @r#"
1048 (
1049 "name_foo",
1050 "",
1051 )
1052 "#
1053 );
1054 assert_debug_snapshot!(
1055 names_of_path(Path::new("foo/src/snapshots/go1.20.5.snap")), @r#"
1056 (
1057 "go1.20.5",
1058 "",
1059 )
1060 "#
1061 );
1062}
1063
1064fn legacy_inline_normalize(frozen_value: &str) -> String {
1066 if !frozen_value.trim_start().starts_with('⋮') {
1067 return frozen_value.to_string();
1068 }
1069 let mut buf = String::new();
1070 let mut line_iter = frozen_value.lines();
1071 let mut indentation = 0;
1072
1073 for line in &mut line_iter {
1074 let line_trimmed = line.trim_start();
1075 if line_trimmed.is_empty() {
1076 continue;
1077 }
1078 indentation = line.len() - line_trimmed.len();
1079 buf.push_str(&line_trimmed[3..]);
1081 buf.push('\n');
1082 break;
1083 }
1084
1085 for line in &mut line_iter {
1086 if let Some(prefix) = line.get(..indentation) {
1087 if !prefix.trim().is_empty() {
1088 return "".to_string();
1089 }
1090 }
1091 if let Some(remainder) = line.get(indentation..) {
1092 if let Some(rest) = remainder.strip_prefix('⋮') {
1093 buf.push_str(rest);
1094 buf.push('\n');
1095 } else if remainder.trim().is_empty() {
1096 continue;
1097 } else {
1098 return "".to_string();
1099 }
1100 }
1101 }
1102
1103 buf.trim_end().to_string()
1104}
1105
1106#[test]
1107fn test_snapshot_contents_to_inline() {
1108 use similar_asserts::assert_eq;
1109 let snapshot_contents =
1110 TextSnapshotContents::new("testing".to_string(), TextSnapshotKind::Inline);
1111 assert_eq!(snapshot_contents.to_inline(""), r#""testing""#);
1112
1113 assert_eq!(
1114 TextSnapshotContents::new("\na\nb".to_string(), TextSnapshotKind::Inline).to_inline(""),
1115 r##"r"
1116
1117a
1118b
1119""##
1120 );
1121
1122 assert_eq!(
1123 TextSnapshotContents::new("a\nb".to_string(), TextSnapshotKind::Inline).to_inline(" "),
1124 r##"r"
1125 a
1126 b
1127 ""##
1128 );
1129
1130 assert_eq!(
1131 TextSnapshotContents::new("\n a\n b".to_string(), TextSnapshotKind::Inline)
1132 .to_inline(""),
1133 r##"r"
1134
1135a
1136b
1137""##
1138 );
1139
1140 assert_eq!(
1141 TextSnapshotContents::new("\na\n\nb".to_string(), TextSnapshotKind::Inline)
1142 .to_inline(" "),
1143 r##"r"
1144
1145 a
1146
1147 b
1148 ""##
1149 );
1150
1151 assert_eq!(
1152 TextSnapshotContents::new(
1153 "ab
1154 "
1155 .to_string(),
1156 TextSnapshotKind::Inline
1157 )
1158 .to_inline(""),
1159 r#""ab""#
1160 );
1161
1162 assert_eq!(
1163 TextSnapshotContents::new(
1164 " ab
1165 "
1166 .to_string(),
1167 TextSnapshotKind::Inline
1168 )
1169 .to_inline(""),
1170 r##"" ab""##
1171 );
1172
1173 assert_eq!(
1174 TextSnapshotContents::new("\n ab\n".to_string(), TextSnapshotKind::Inline).to_inline(""),
1175 r##"r"
1176
1177ab
1178""##
1179 );
1180
1181 assert_eq!(
1182 TextSnapshotContents::new("ab".to_string(), TextSnapshotKind::Inline).to_inline(""),
1183 r#""ab""#
1184 );
1185
1186 assert_eq!(
1188 TextSnapshotContents::new("a\tb".to_string(), TextSnapshotKind::Inline).to_inline(""),
1189 r##""a b""##
1190 );
1191
1192 assert_eq!(
1193 TextSnapshotContents::new("a\t\nb".to_string(), TextSnapshotKind::Inline).to_inline(""),
1194 r##"r"
1195a
1196b
1197""##
1198 );
1199
1200 assert_eq!(
1201 TextSnapshotContents::new("a\rb".to_string(), TextSnapshotKind::Inline).to_inline(""),
1202 r##""a\rb""##
1203 );
1204
1205 assert_eq!(
1206 TextSnapshotContents::new("a\0b".to_string(), TextSnapshotKind::Inline).to_inline(""),
1207 r##""a\0b""##
1209 );
1210
1211 assert_eq!(
1212 TextSnapshotContents::new("a\u{FFFD}b".to_string(), TextSnapshotKind::Inline).to_inline(""),
1213 r##""a�b""##
1215 );
1216}
1217
1218#[test]
1219fn test_snapshot_contents_hashes() {
1220 assert_eq!(
1221 TextSnapshotContents::new("a###b".to_string(), TextSnapshotKind::Inline).to_inline(""),
1222 r#""a###b""#
1223 );
1224
1225 assert_eq!(
1226 TextSnapshotContents::new("a\n\\###b".to_string(), TextSnapshotKind::Inline).to_inline(""),
1227 r#####"r"
1228a
1229\###b
1230""#####
1231 );
1232}
1233
1234#[test]
1235fn test_min_indentation() {
1236 use similar_asserts::assert_eq;
1237 assert_eq!(
1238 min_indentation(
1239 r#"
1240 1
1241 2
1242 "#,
1243 ),
1244 " ".to_string()
1245 );
1246
1247 assert_eq!(
1248 min_indentation(
1249 r#"
1250 1
1251 2"#
1252 ),
1253 " ".to_string()
1254 );
1255
1256 assert_eq!(
1257 min_indentation(
1258 r#"
1259 1
1260 2
1261 "#
1262 ),
1263 " ".to_string()
1264 );
1265
1266 assert_eq!(
1267 min_indentation(
1268 r#"
1269 1
1270 2
1271"#
1272 ),
1273 " ".to_string()
1274 );
1275
1276 assert_eq!(
1277 min_indentation(
1278 r#"
1279 a
1280 "#
1281 ),
1282 " ".to_string()
1283 );
1284
1285 assert_eq!(min_indentation(""), "".to_string());
1286
1287 assert_eq!(
1288 min_indentation(
1289 r#"
1290 a
1291 b
1292c
1293 "#
1294 ),
1295 "".to_string()
1296 );
1297
1298 assert_eq!(
1299 min_indentation(
1300 r#"
1301a
1302 "#
1303 ),
1304 "".to_string()
1305 );
1306
1307 assert_eq!(
1308 min_indentation(
1309 "
1310 a"
1311 ),
1312 " ".to_string()
1313 );
1314
1315 assert_eq!(
1316 min_indentation(
1317 r#"a
1318 a"#
1319 ),
1320 "".to_string()
1321 );
1322
1323 assert_eq!(
1324 normalize_inline(
1325 r#"
1326 1
1327 2"#
1328 ),
1329 r###"
1330 1
13312"###
1332 );
1333
1334 assert_eq!(
1335 normalize_inline(
1336 r#"
1337 1
1338 2
1339 "#
1340 ),
1341 r###"
13421
13432
1344"###
1345 );
1346}
1347
1348#[test]
1349fn test_min_indentation_additional() {
1350 use similar_asserts::assert_eq;
1351
1352 let t = r#"
1353 1
1354 2
1355"#;
1356 assert_eq!(min_indentation(t), " ".to_string());
1357
1358 let t = r#"
1359 a
1360 "#;
1361 assert_eq!(min_indentation(t), " ".to_string());
1362
1363 let t = "";
1364 assert_eq!(min_indentation(t), "".to_string());
1365
1366 let t = r#"
1367 a
1368 b
1369c
1370 "#;
1371 assert_eq!(min_indentation(t), "".to_string());
1372
1373 let t = r#"
1374a"#;
1375 assert_eq!(min_indentation(t), "".to_string());
1376
1377 let t = r#"
1378 a"#;
1379 assert_eq!(min_indentation(t), " ".to_string());
1380
1381 let t = r#"a
1382 a"#;
1383 assert_eq!(min_indentation(t), "".to_string());
1384
1385 let t = r#"
1386 1
1387 2
1388 "#;
1389 assert_eq!(min_indentation(t), " ".to_string());
1390
1391 let t = r#"
1392 1
1393 2"#;
1394 assert_eq!(min_indentation(t), " ".to_string());
1395
1396 let t = r#"
1397 1
1398 2"#;
1399 assert_eq!(min_indentation(t), " ".to_string());
1400}
1401
1402#[test]
1403fn test_inline_snapshot_value_newline() {
1404 assert_eq!(normalize_inline("\n"), "");
1406}
1407
1408#[test]
1409fn test_parse_yaml_error() {
1410 use std::env::temp_dir;
1411 let mut temp = temp_dir();
1412 temp.push("bad.yaml");
1413 let mut f = fs::File::create(temp.clone()).unwrap();
1414
1415 let invalid = r#"---
1416 This is invalid yaml:
1417 {
1418 {
1419 ---
1420 "#;
1421
1422 f.write_all(invalid.as_bytes()).unwrap();
1423
1424 let error = format!("{}", Snapshot::from_file(temp.as_path()).unwrap_err());
1425 assert!(error.contains("Failed parsing the YAML from"));
1426 assert!(error.contains("bad.yaml"));
1427}
1428
1429#[test]
1431fn test_ownership() {
1432 use std::ops::Range;
1434 let r = Range { start: 0, end: 10 };
1435 assert_debug_snapshot!(r, @"0..10");
1436 assert_debug_snapshot!(r, @"0..10");
1437}
1438
1439#[test]
1440fn test_empty_lines() {
1441 assert_snapshot!(r#"single line should fit on a single line"#, @"single line should fit on a single line");
1442 assert_snapshot!(r##"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");
1443
1444 assert_snapshot!(r#"multiline content starting on first line
1445
1446 final line
1447 "#, @r"
1448 multiline content starting on first line
1449
1450 final line
1451 ");
1452
1453 assert_snapshot!(r#"
1454 multiline content starting on second line
1455
1456 final line
1457 "#, @r"
1458
1459 multiline content starting on second line
1460
1461 final line
1462 ");
1463}