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::fs;
10use std::io::{BufRead, BufReader, Write};
11use std::path::{Path, PathBuf};
12use std::rc::Rc;
13use std::time::{SystemTime, UNIX_EPOCH};
14use std::{borrow::Cow, fmt};
15
16static RUN_ID: Lazy<String> = Lazy::new(|| {
17 if let Ok(run_id) = env::var("NEXTEST_RUN_ID") {
18 run_id
19 } else {
20 let d = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
21 format!("{}-{}", d.as_secs(), d.subsec_nanos())
22 }
23});
24
25#[derive(Debug)]
28pub struct PendingInlineSnapshot {
29 pub run_id: String,
30 pub line: u32,
31 pub new: Option<Snapshot>,
32 pub old: Option<Snapshot>,
33}
34
35impl PendingInlineSnapshot {
36 pub fn new(new: Option<Snapshot>, old: Option<Snapshot>, line: u32) -> PendingInlineSnapshot {
37 PendingInlineSnapshot {
38 new,
39 old,
40 line,
41 run_id: RUN_ID.clone(),
42 }
43 }
44
45 #[cfg(feature = "_cargo_insta_internal")]
46 pub fn load_batch(p: &Path) -> Result<Vec<PendingInlineSnapshot>, Box<dyn Error>> {
47 let contents =
48 fs::read_to_string(p).map_err(|e| content::Error::FileIo(e, p.to_path_buf()))?;
49
50 let mut rv: Vec<Self> = contents
51 .lines()
52 .map(|line| {
53 let value = yaml::parse_str(line, p)?;
54 Self::from_content(value)
55 })
56 .collect::<Result<_, Box<dyn Error>>>()?;
57
58 if let Some(last_run_id) = rv.last().map(|x| x.run_id.clone()) {
60 rv.retain(|x| x.run_id == last_run_id);
61 }
62
63 Ok(rv)
64 }
65
66 #[cfg(feature = "_cargo_insta_internal")]
67 pub fn save_batch(p: &Path, batch: &[PendingInlineSnapshot]) -> Result<(), Box<dyn Error>> {
68 fs::remove_file(p).ok();
69 for snap in batch {
70 snap.save(p)?;
71 }
72 Ok(())
73 }
74
75 pub fn save(&self, p: &Path) -> Result<(), Box<dyn Error>> {
76 let mut f = fs::OpenOptions::new().create(true).append(true).open(p)?;
77 let mut s = json::to_string(&self.as_content());
78 s.push('\n');
79 f.write_all(s.as_bytes())?;
80 Ok(())
81 }
82
83 #[cfg(feature = "_cargo_insta_internal")]
84 fn from_content(content: Content) -> Result<PendingInlineSnapshot, Box<dyn Error>> {
85 if let Content::Map(map) = content {
86 let mut run_id = None;
87 let mut line = None;
88 let mut old = None;
89 let mut new = None;
90
91 for (key, value) in map.into_iter() {
92 match key.as_str() {
93 Some("run_id") => run_id = value.as_str().map(|x| x.to_string()),
94 Some("line") => line = value.as_u64().map(|x| x as u32),
95 Some("old") if !value.is_nil() => {
96 old = Some(Snapshot::from_content(value, TextSnapshotKind::Inline)?)
97 }
98 Some("new") if !value.is_nil() => {
99 new = Some(Snapshot::from_content(value, TextSnapshotKind::Inline)?)
100 }
101 _ => {}
102 }
103 }
104
105 Ok(PendingInlineSnapshot {
106 run_id: run_id.ok_or(content::Error::MissingField)?,
107 line: line.ok_or(content::Error::MissingField)?,
108 new,
109 old,
110 })
111 } else {
112 Err(content::Error::UnexpectedDataType.into())
113 }
114 }
115
116 fn as_content(&self) -> Content {
117 let fields = vec![
118 ("run_id", Content::from(self.run_id.as_str())),
119 ("line", Content::from(self.line)),
120 (
121 "new",
122 match &self.new {
123 Some(snap) => snap.as_content(),
124 None => Content::None,
125 },
126 ),
127 (
128 "old",
129 match &self.old {
130 Some(snap) => snap.as_content(),
131 None => Content::None,
132 },
133 ),
134 ];
135
136 Content::Struct("PendingInlineSnapshot", fields)
137 }
138}
139
140#[derive(Debug, Clone, PartialEq)]
141pub enum SnapshotKind {
142 Text,
143 Binary { extension: String },
144}
145
146impl Default for SnapshotKind {
147 fn default() -> Self {
148 SnapshotKind::Text
149 }
150}
151
152#[derive(Debug, Default, Clone, PartialEq)]
154pub struct MetaData {
155 pub(crate) source: Option<String>,
157 pub(crate) assertion_line: Option<u32>,
160 pub(crate) description: Option<String>,
162 pub(crate) expression: Option<String>,
164 pub(crate) info: Option<Content>,
166 pub(crate) input_file: Option<String>,
168 pub(crate) snapshot_kind: SnapshotKind,
170}
171
172impl MetaData {
173 pub fn source(&self) -> Option<&str> {
175 self.source.as_deref()
176 }
177
178 pub fn assertion_line(&self) -> Option<u32> {
180 self.assertion_line
181 }
182
183 pub fn expression(&self) -> Option<&str> {
185 self.expression.as_deref()
186 }
187
188 pub fn description(&self) -> Option<&str> {
190 self.description.as_deref().filter(|x| !x.is_empty())
191 }
192
193 #[doc(hidden)]
195 pub fn private_info(&self) -> Option<&Content> {
196 self.info.as_ref()
197 }
198
199 pub fn get_relative_source(&self, base: &Path) -> Option<PathBuf> {
201 self.source.as_ref().map(|source| {
202 base.join(source)
203 .canonicalize()
204 .ok()
205 .and_then(|s| s.strip_prefix(base).ok().map(|x| x.to_path_buf()))
206 .unwrap_or_else(|| base.to_path_buf())
207 })
208 }
209
210 pub fn input_file(&self) -> Option<&str> {
212 self.input_file.as_deref()
213 }
214
215 fn from_content(content: Content) -> Result<MetaData, Box<dyn Error>> {
216 if let Content::Map(map) = content {
217 let mut source = None;
218 let mut assertion_line = None;
219 let mut description = None;
220 let mut expression = None;
221 let mut info = None;
222 let mut input_file = None;
223 let mut snapshot_type = TmpSnapshotKind::Text;
224 let mut extension = None;
225
226 enum TmpSnapshotKind {
227 Text,
228 Binary,
229 }
230
231 for (key, value) in map.into_iter() {
232 match key.as_str() {
233 Some("source") => source = value.as_str().map(|x| x.to_string()),
234 Some("assertion_line") => assertion_line = value.as_u64().map(|x| x as u32),
235 Some("description") => description = value.as_str().map(Into::into),
236 Some("expression") => expression = value.as_str().map(Into::into),
237 Some("info") if !value.is_nil() => info = Some(value),
238 Some("input_file") => input_file = value.as_str().map(Into::into),
239 Some("snapshot_kind") => {
240 snapshot_type = match value.as_str() {
241 Some("binary") => TmpSnapshotKind::Binary,
242 _ => TmpSnapshotKind::Text,
243 }
244 }
245 Some("extension") => {
246 extension = value.as_str().map(Into::into);
247 }
248 _ => {}
249 }
250 }
251
252 Ok(MetaData {
253 source,
254 assertion_line,
255 description,
256 expression,
257 info,
258 input_file,
259 snapshot_kind: match snapshot_type {
260 TmpSnapshotKind::Text => SnapshotKind::Text,
261 TmpSnapshotKind::Binary => SnapshotKind::Binary {
262 extension: extension.ok_or(content::Error::MissingField)?,
263 },
264 },
265 })
266 } else {
267 Err(content::Error::UnexpectedDataType.into())
268 }
269 }
270
271 fn as_content(&self) -> Content {
272 let mut fields = Vec::new();
273 if let Some(source) = self.source.as_deref() {
274 fields.push(("source", Content::from(source)));
275 }
276 if let Some(line) = self.assertion_line {
277 fields.push(("assertion_line", Content::from(line)));
278 }
279 if let Some(description) = self.description.as_deref() {
280 fields.push(("description", Content::from(description)));
281 }
282 if let Some(expression) = self.expression.as_deref() {
283 fields.push(("expression", Content::from(expression)));
284 }
285 if let Some(info) = &self.info {
286 fields.push(("info", info.to_owned()));
287 }
288 if let Some(input_file) = self.input_file.as_deref() {
289 fields.push(("input_file", Content::from(input_file)));
290 }
291
292 match self.snapshot_kind {
293 SnapshotKind::Text => {}
294 SnapshotKind::Binary { ref extension } => {
295 fields.push(("extension", Content::from(extension.clone())));
296 fields.push(("snapshot_kind", Content::from("binary")));
297 }
298 }
299
300 Content::Struct("MetaData", fields)
301 }
302
303 fn trim_for_persistence(&self) -> Cow<'_, MetaData> {
306 if self.assertion_line.is_some() {
314 let mut rv = self.clone();
315 rv.assertion_line = None;
316 Cow::Owned(rv)
317 } else {
318 Cow::Borrowed(self)
319 }
320 }
321}
322
323#[derive(Debug, PartialEq, Eq, Clone, Copy)]
324pub enum TextSnapshotKind {
325 Inline,
326 File,
327}
328
329#[derive(Debug, Clone)]
331pub struct Snapshot {
332 module_name: String,
333 snapshot_name: Option<String>,
334 metadata: MetaData,
335 snapshot: SnapshotContents,
336}
337
338impl Snapshot {
339 pub fn from_file(p: &Path) -> Result<Snapshot, Box<dyn Error>> {
341 let mut f = BufReader::new(fs::File::open(p)?);
342 let mut buf = String::new();
343
344 f.read_line(&mut buf)?;
345
346 let metadata = if buf.trim_end() == "---" {
348 loop {
349 let read = f.read_line(&mut buf)?;
350 if read == 0 {
351 break;
352 }
353 if buf[buf.len() - read..].trim_end() == "---" {
354 buf.truncate(buf.len() - read);
355 break;
356 }
357 }
358 let content = yaml::parse_str(&buf, p)?;
359 MetaData::from_content(content)?
360 } else {
364 let mut rv = MetaData::default();
365 loop {
366 buf.clear();
367 let read = f.read_line(&mut buf)?;
368 if read == 0 || buf.trim_end().is_empty() {
369 buf.truncate(buf.len() - read);
370 break;
371 }
372 let mut iter = buf.splitn(2, ':');
373 if let Some(key) = iter.next() {
374 if let Some(value) = iter.next() {
375 let value = value.trim();
376 match key.to_lowercase().as_str() {
377 "expression" => rv.expression = Some(value.to_string()),
378 "source" => rv.source = Some(value.into()),
379 _ => {}
380 }
381 }
382 }
383 }
384 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());
385 rv
386 };
387
388 let contents = match metadata.snapshot_kind {
389 SnapshotKind::Text => {
390 buf.clear();
391 for (idx, line) in f.lines().enumerate() {
392 let line = line?;
393 if idx > 0 {
394 buf.push('\n');
395 }
396 buf.push_str(&line);
397 }
398
399 TextSnapshotContents {
400 contents: buf,
401 kind: TextSnapshotKind::File,
402 }
403 .into()
404 }
405 SnapshotKind::Binary { ref extension } => {
406 let path = build_binary_path(extension, p);
407 let contents = fs::read(path)?;
408
409 SnapshotContents::Binary(Rc::new(contents))
410 }
411 };
412
413 let (snapshot_name, module_name) = names_of_path(p);
414
415 Ok(Snapshot::from_components(
416 module_name,
417 Some(snapshot_name),
418 metadata,
419 contents,
420 ))
421 }
422
423 pub(crate) fn from_components(
424 module_name: String,
425 snapshot_name: Option<String>,
426 metadata: MetaData,
427 snapshot: SnapshotContents,
428 ) -> Snapshot {
429 Snapshot {
430 module_name,
431 snapshot_name,
432 metadata,
433 snapshot,
434 }
435 }
436
437 #[cfg(feature = "_cargo_insta_internal")]
438 fn from_content(content: Content, kind: TextSnapshotKind) -> Result<Snapshot, Box<dyn Error>> {
439 if let Content::Map(map) = content {
440 let mut module_name = None;
441 let mut snapshot_name = None;
442 let mut metadata = None;
443 let mut snapshot = None;
444
445 for (key, value) in map.into_iter() {
446 match key.as_str() {
447 Some("module_name") => module_name = value.as_str().map(|x| x.to_string()),
448 Some("snapshot_name") => snapshot_name = value.as_str().map(|x| x.to_string()),
449 Some("metadata") => metadata = Some(MetaData::from_content(value)?),
450 Some("snapshot") => {
451 snapshot = Some(
452 TextSnapshotContents {
453 contents: value
454 .as_str()
455 .ok_or(content::Error::UnexpectedDataType)?
456 .to_string(),
457 kind,
458 }
459 .into(),
460 );
461 }
462 _ => {}
463 }
464 }
465
466 Ok(Snapshot {
467 module_name: module_name.ok_or(content::Error::MissingField)?,
468 snapshot_name,
469 metadata: metadata.ok_or(content::Error::MissingField)?,
470 snapshot: snapshot.ok_or(content::Error::MissingField)?,
471 })
472 } else {
473 Err(content::Error::UnexpectedDataType.into())
474 }
475 }
476
477 fn as_content(&self) -> Content {
478 let mut fields = vec![("module_name", Content::from(self.module_name.as_str()))];
479 if let Some(name) = self.snapshot_name.as_deref() {
482 fields.push(("snapshot_name", Content::from(name)));
483 }
484 fields.push(("metadata", self.metadata.as_content()));
485
486 if let SnapshotContents::Text(ref content) = self.snapshot {
487 fields.push(("snapshot", Content::from(content.to_string())));
488 }
489
490 Content::Struct("Content", fields)
491 }
492
493 pub fn module_name(&self) -> &str {
495 &self.module_name
496 }
497
498 pub fn snapshot_name(&self) -> Option<&str> {
500 self.snapshot_name.as_deref()
501 }
502
503 pub fn metadata(&self) -> &MetaData {
505 &self.metadata
506 }
507
508 pub fn contents(&self) -> &SnapshotContents {
510 &self.snapshot
511 }
512
513 pub fn matches(&self, other: &Self) -> bool {
515 self.contents() == other.contents()
516 && self.metadata.snapshot_kind == other.metadata.snapshot_kind
518 }
519
520 pub fn matches_fully(&self, other: &Self) -> bool {
524 match (self.contents(), other.contents()) {
525 (SnapshotContents::Text(self_contents), SnapshotContents::Text(other_contents)) => {
526 let contents_match_exact = self_contents.matches_latest(other_contents);
540 match self_contents.kind {
541 TextSnapshotKind::File => {
542 self.metadata.trim_for_persistence()
543 == other.metadata.trim_for_persistence()
544 && contents_match_exact
545 }
546 TextSnapshotKind::Inline => contents_match_exact,
547 }
548 }
549 _ => self.matches(other),
550 }
551 }
552
553 fn serialize_snapshot(&self, md: &MetaData) -> String {
554 let mut buf = yaml::to_string(&md.as_content());
555 buf.push_str("---\n");
556
557 if let SnapshotContents::Text(ref contents) = self.snapshot {
558 buf.push_str(&contents.to_string());
559 buf.push('\n');
560 }
561
562 buf
563 }
564
565 fn save_with_metadata(&self, path: &Path, md: &MetaData) -> Result<(), Box<dyn Error>> {
569 if let Some(folder) = path.parent() {
570 fs::create_dir_all(folder)?;
571 }
572
573 let serialized_snapshot = self.serialize_snapshot(md);
574 fs::write(path, serialized_snapshot)
575 .map_err(|e| content::Error::FileIo(e, path.to_path_buf()))?;
576
577 if let SnapshotContents::Binary(ref contents) = self.snapshot {
578 fs::write(self.build_binary_path(path).unwrap(), &**contents)
579 .map_err(|e| content::Error::FileIo(e, path.to_path_buf()))?;
580 }
581
582 Ok(())
583 }
584
585 pub fn build_binary_path(&self, path: impl Into<PathBuf>) -> Option<PathBuf> {
586 if let SnapshotKind::Binary { ref extension } = self.metadata.snapshot_kind {
587 Some(build_binary_path(extension, path))
588 } else {
589 None
590 }
591 }
592
593 #[doc(hidden)]
595 pub fn save(&self, path: &Path) -> Result<(), Box<dyn Error>> {
596 self.save_with_metadata(path, &self.metadata.trim_for_persistence())
597 }
598
599 pub(crate) fn save_new(&self, path: &Path) -> Result<PathBuf, Box<dyn Error>> {
604 let new_path = path.to_path_buf().with_extension("snap.new");
607 self.save_with_metadata(&new_path, &self.metadata)?;
608 Ok(new_path)
609 }
610}
611
612#[derive(Debug, Clone)]
614pub enum SnapshotContents {
615 Text(TextSnapshotContents),
616
617 Binary(Rc<Vec<u8>>),
622}
623
624#[derive(Debug, PartialEq, Eq, Clone)]
626pub struct TextSnapshotContents {
627 contents: String,
628 pub kind: TextSnapshotKind,
629}
630
631impl From<TextSnapshotContents> for SnapshotContents {
632 fn from(value: TextSnapshotContents) -> Self {
633 SnapshotContents::Text(value)
634 }
635}
636
637impl SnapshotContents {
638 pub fn is_binary(&self) -> bool {
639 matches!(self, SnapshotContents::Binary(_))
640 }
641}
642
643impl TextSnapshotContents {
644 pub fn new(contents: String, kind: TextSnapshotKind) -> TextSnapshotContents {
645 TextSnapshotContents { contents, kind }
650 }
651
652 pub fn matches_latest(&self, other: &Self) -> bool {
654 self.to_string() == other.to_string()
655 }
656
657 pub fn matches_legacy(&self, other: &Self) -> bool {
658 fn as_str_legacy(sc: &TextSnapshotContents) -> String {
659 let out = sc.to_string();
660 let out = match out.strip_prefix("---\n") {
663 Some(old_snapshot) => old_snapshot.to_string(),
664 None => out,
665 };
666 match sc.kind {
667 TextSnapshotKind::Inline => legacy_inline_normalize(&out),
668 TextSnapshotKind::File => out,
669 }
670 }
671 as_str_legacy(self) == as_str_legacy(other)
672 }
673
674 fn normalize(&self) -> String {
675 let kind_specific_normalization = match self.kind {
676 TextSnapshotKind::Inline => normalize_inline_snapshot(&self.contents),
677 TextSnapshotKind::File => self.contents.clone(),
678 };
679 let out = kind_specific_normalization
681 .trim_start_matches(['\r', '\n'])
682 .trim_end();
683 out.replace("\r\n", "\n")
684 }
685
686 pub fn to_inline(&self, indentation: &str) -> String {
689 let contents = self.normalize();
690 let mut out = String::new();
691
692 let has_control_chars = contents
696 .chars()
697 .any(|c| c.is_control() && !['\n', '\t', '\x1b'].contains(&c));
698
699 if !has_control_chars && contents.contains(['\\', '"', '\n']) {
703 out.push('r');
704 }
705
706 let delimiter = "#".repeat(required_hashes(&contents));
707
708 out.push_str(&delimiter);
709
710 if has_control_chars {
714 out.push_str(format!("{:?}", contents).as_str());
715 } else {
716 out.push('"');
717 if contents.contains('\n') {
720 out.extend(
721 contents
722 .lines()
723 .map(|l| {
727 format!(
728 "\n{i}{l}",
729 i = if l.is_empty() { "" } else { indentation },
730 l = l
731 )
732 })
733 .chain(Some(format!("\n{}", indentation))),
736 );
737 } else {
738 out.push_str(contents.as_str());
739 }
740 out.push('"');
741 }
742
743 out.push_str(&delimiter);
744 out
745 }
746}
747
748impl fmt::Display for TextSnapshotContents {
749 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
752 write!(f, "{}", self.normalize())
753 }
754}
755
756impl PartialEq for SnapshotContents {
757 fn eq(&self, other: &Self) -> bool {
758 match (self, other) {
759 (SnapshotContents::Text(this), SnapshotContents::Text(other)) => {
760 if this.matches_latest(other) {
762 true
763 } else if this.matches_legacy(other) {
764 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());
765 true
766 } else {
767 false
768 }
769 }
770 (SnapshotContents::Binary(this), SnapshotContents::Binary(other)) => this == other,
771 _ => false,
772 }
773 }
774}
775
776fn build_binary_path(extension: &str, path: impl Into<PathBuf>) -> PathBuf {
777 let path = path.into();
778 let mut new_extension = path.extension().unwrap().to_os_string();
779 new_extension.push(".");
780 new_extension.push(extension);
781
782 path.with_extension(new_extension)
783}
784
785fn required_hashes(text: &str) -> usize {
787 let splits = text.split('"');
788 if splits.clone().count() <= 1 {
789 return 0;
790 }
791
792 splits
793 .map(|s| s.chars().take_while(|&c| c == '#').count() + 1)
794 .max()
795 .unwrap()
796}
797
798#[test]
799fn test_required_hashes() {
800 assert_snapshot!(required_hashes(""), @"0");
801 assert_snapshot!(required_hashes("Hello, world!"), @"0");
802 assert_snapshot!(required_hashes("\"\""), @"1");
803 assert_snapshot!(required_hashes("##"), @"0");
804 assert_snapshot!(required_hashes("\"#\"#"), @"2");
805 assert_snapshot!(required_hashes(r##""#"##), @"2");
806 assert_snapshot!(required_hashes(r######"foo ""##### bar "###" baz"######), @"6");
807 assert_snapshot!(required_hashes("\"\"\""), @"1");
808 assert_snapshot!(required_hashes("####"), @"0");
809 assert_snapshot!(required_hashes(r###"\"\"##\"\""###), @"3");
810 assert_snapshot!(required_hashes(r###"r"#"Raw string"#""###), @"2");
811}
812
813fn leading_space(value: &str) -> String {
814 value
815 .chars()
816 .take_while(|x| x.is_whitespace())
817 .collect::<String>()
818}
819
820fn min_indentation(snapshot: &str) -> String {
821 let lines = snapshot.trim_end().lines();
822
823 if lines.clone().count() <= 1 {
824 return "".into();
826 }
827
828 lines
829 .filter(|l| !l.is_empty())
830 .map(leading_space)
831 .min_by(|a, b| a.len().cmp(&b.len()))
832 .unwrap_or("".into())
833}
834
835fn normalize_inline_snapshot(snapshot: &str) -> String {
837 let indentation = min_indentation(snapshot);
838 snapshot
839 .lines()
840 .map(|l| l.get(indentation.len()..).unwrap_or(""))
841 .collect::<Vec<&str>>()
842 .join("\n")
843}
844
845fn names_of_path(path: &Path) -> (String, String) {
847 let parts: Vec<&str> = path
850 .file_stem()
851 .unwrap()
852 .to_str()
853 .unwrap_or("")
854 .rsplitn(2, "__")
855 .collect();
856
857 match parts.as_slice() {
858 [snapshot_name, module_name] => (snapshot_name.to_string(), module_name.to_string()),
859 [snapshot_name] => (snapshot_name.to_string(), String::new()),
860 _ => (String::new(), "<unknown>".to_string()),
861 }
862}
863
864#[test]
865fn test_names_of_path() {
866 assert_debug_snapshot!(
867 names_of_path(Path::new("/src/snapshots/insta_tests__tests__name_foo.snap")), @r###"
868 (
869 "name_foo",
870 "insta_tests__tests",
871 )
872 "###
873 );
874 assert_debug_snapshot!(
875 names_of_path(Path::new("/src/snapshots/name_foo.snap")), @r###"
876 (
877 "name_foo",
878 "",
879 )
880 "###
881 );
882 assert_debug_snapshot!(
883 names_of_path(Path::new("foo/src/snapshots/go1.20.5.snap")), @r###"
884 (
885 "go1.20.5",
886 "",
887 )
888 "###
889 );
890}
891
892fn legacy_inline_normalize(frozen_value: &str) -> String {
894 if !frozen_value.trim_start().starts_with('⋮') {
895 return frozen_value.to_string();
896 }
897 let mut buf = String::new();
898 let mut line_iter = frozen_value.lines();
899 let mut indentation = 0;
900
901 for line in &mut line_iter {
902 let line_trimmed = line.trim_start();
903 if line_trimmed.is_empty() {
904 continue;
905 }
906 indentation = line.len() - line_trimmed.len();
907 buf.push_str(&line_trimmed[3..]);
909 buf.push('\n');
910 break;
911 }
912
913 for line in &mut line_iter {
914 if let Some(prefix) = line.get(..indentation) {
915 if !prefix.trim().is_empty() {
916 return "".to_string();
917 }
918 }
919 if let Some(remainder) = line.get(indentation..) {
920 if let Some(rest) = remainder.strip_prefix('⋮') {
921 buf.push_str(rest);
922 buf.push('\n');
923 } else if remainder.trim().is_empty() {
924 continue;
925 } else {
926 return "".to_string();
927 }
928 }
929 }
930
931 buf.trim_end().to_string()
932}
933
934#[test]
935fn test_snapshot_contents() {
936 use similar_asserts::assert_eq;
937 let snapshot_contents =
938 TextSnapshotContents::new("testing".to_string(), TextSnapshotKind::Inline);
939 assert_eq!(snapshot_contents.to_inline(""), r#""testing""#);
940
941 let t = &"
942a
943b"[1..];
944 assert_eq!(
945 TextSnapshotContents::new(t.to_string(), TextSnapshotKind::Inline).to_inline(""),
946 r##"r"
947a
948b
949""##
950 );
951
952 assert_eq!(
953 TextSnapshotContents::new("a\nb".to_string(), TextSnapshotKind::Inline).to_inline(" "),
954 r##"r"
955 a
956 b
957 ""##
958 );
959
960 assert_eq!(
961 TextSnapshotContents::new("\n a\n b".to_string(), TextSnapshotKind::Inline)
962 .to_inline(""),
963 r##"r"
964a
965b
966""##
967 );
968
969 assert_eq!(
970 TextSnapshotContents::new("\na\n\nb".to_string(), TextSnapshotKind::Inline)
971 .to_inline(" "),
972 r##"r"
973 a
974
975 b
976 ""##
977 );
978
979 assert_eq!(
980 TextSnapshotContents::new("\n ab\n".to_string(), TextSnapshotKind::Inline).to_inline(""),
981 r##""ab""##
982 );
983
984 assert_eq!(
985 TextSnapshotContents::new("ab".to_string(), TextSnapshotKind::Inline).to_inline(""),
986 r#""ab""#
987 );
988
989 assert_eq!(
991 TextSnapshotContents::new("a\tb".to_string(), TextSnapshotKind::Inline).to_inline(""),
992 r##""a b""##
993 );
994
995 assert_eq!(
996 TextSnapshotContents::new("a\t\nb".to_string(), TextSnapshotKind::Inline).to_inline(""),
997 r##"r"
998a
999b
1000""##
1001 );
1002
1003 assert_eq!(
1004 TextSnapshotContents::new("a\rb".to_string(), TextSnapshotKind::Inline).to_inline(""),
1005 r##""a\rb""##
1006 );
1007
1008 assert_eq!(
1009 TextSnapshotContents::new("a\0b".to_string(), TextSnapshotKind::Inline).to_inline(""),
1010 r##""a\0b""##
1012 );
1013
1014 assert_eq!(
1015 TextSnapshotContents::new("a\u{FFFD}b".to_string(), TextSnapshotKind::Inline).to_inline(""),
1016 r##""a�b""##
1018 );
1019}
1020
1021#[test]
1022fn test_snapshot_contents_hashes() {
1023 assert_eq!(
1024 TextSnapshotContents::new("a###b".to_string(), TextSnapshotKind::Inline).to_inline(""),
1025 r#""a###b""#
1026 );
1027
1028 assert_eq!(
1029 TextSnapshotContents::new("a\n\\###b".to_string(), TextSnapshotKind::Inline).to_inline(""),
1030 r#####"r"
1031a
1032\###b
1033""#####
1034 );
1035}
1036
1037#[test]
1038fn test_normalize_inline_snapshot() {
1039 use similar_asserts::assert_eq;
1040 assert_eq!(
1043 normalize_inline_snapshot(
1044 r#"
1045 1
1046 2
1047 "#,
1048 ),
1049 r###"
10501
10512
1052"###
1053 );
1054
1055 assert_eq!(
1056 normalize_inline_snapshot(
1057 r#"
1058 1
1059 2"#
1060 ),
1061 r###"
1062 1
10632"###
1064 );
1065
1066 assert_eq!(
1067 normalize_inline_snapshot(
1068 r#"
1069 1
1070 2
1071 "#
1072 ),
1073 r###"
10741
10752
1076"###
1077 );
1078
1079 assert_eq!(
1080 normalize_inline_snapshot(
1081 r#"
1082 1
1083 2
1084"#
1085 ),
1086 r###"
10871
10882"###
1089 );
1090
1091 assert_eq!(
1092 normalize_inline_snapshot(
1093 r#"
1094 a
1095 "#
1096 ),
1097 "
1098a
1099"
1100 );
1101
1102 assert_eq!(normalize_inline_snapshot(""), "");
1103
1104 assert_eq!(
1105 normalize_inline_snapshot(
1106 r#"
1107 a
1108 b
1109c
1110 "#
1111 ),
1112 r###"
1113 a
1114 b
1115c
1116 "###
1117 );
1118
1119 assert_eq!(
1120 normalize_inline_snapshot(
1121 r#"
1122a
1123 "#
1124 ),
1125 "
1126a
1127 "
1128 );
1129
1130 assert_eq!(
1131 normalize_inline_snapshot(
1132 "
1133 a"
1134 ),
1135 "
1136a"
1137 );
1138
1139 assert_eq!(
1140 normalize_inline_snapshot(
1141 r#"a
1142 a"#
1143 ),
1144 r###"a
1145 a"###
1146 );
1147
1148 assert_eq!(
1149 normalize_inline_snapshot(
1150 r#"
1151 1
1152 2"#
1153 ),
1154 r###"
1155 1
11562"###
1157 );
1158
1159 assert_eq!(
1160 normalize_inline_snapshot(
1161 r#"
1162 1
1163 2
1164 "#
1165 ),
1166 r###"
11671
11682
1169"###
1170 );
1171}
1172
1173#[test]
1174fn test_min_indentation() {
1175 use similar_asserts::assert_eq;
1176 let t = r#"
1177 1
1178 2
1179 "#;
1180 assert_eq!(min_indentation(t), " ".to_string());
1181
1182 let t = r#"
1183 1
1184 2"#;
1185 assert_eq!(min_indentation(t), " ".to_string());
1186
1187 let t = r#"
1188 1
1189 2
1190 "#;
1191 assert_eq!(min_indentation(t), " ".to_string());
1192
1193 let t = r#"
1194 1
1195 2
1196"#;
1197 assert_eq!(min_indentation(t), " ".to_string());
1198
1199 let t = r#"
1200 a
1201 "#;
1202 assert_eq!(min_indentation(t), " ".to_string());
1203
1204 let t = "";
1205 assert_eq!(min_indentation(t), "".to_string());
1206
1207 let t = r#"
1208 a
1209 b
1210c
1211 "#;
1212 assert_eq!(min_indentation(t), "".to_string());
1213
1214 let t = r#"
1215a
1216 "#;
1217 assert_eq!(min_indentation(t), "".to_string());
1218
1219 let t = "
1220 a";
1221 assert_eq!(min_indentation(t), " ".to_string());
1222
1223 let t = r#"a
1224 a"#;
1225 assert_eq!(min_indentation(t), "".to_string());
1226
1227 let t = r#"
1228 1
1229 2
1230 "#;
1231 assert_eq!(min_indentation(t), " ".to_string());
1232
1233 let t = r#"
1234 1
1235 2"#;
1236 assert_eq!(min_indentation(t), " ".to_string());
1237
1238 let t = r#"
1239 1
1240 2"#;
1241 assert_eq!(min_indentation(t), " ".to_string());
1242}
1243
1244#[test]
1245fn test_inline_snapshot_value_newline() {
1246 assert_eq!(normalize_inline_snapshot("\n"), "");
1248}
1249
1250#[test]
1251fn test_parse_yaml_error() {
1252 use std::env::temp_dir;
1253 let mut temp = temp_dir();
1254 temp.push("bad.yaml");
1255 let mut f = fs::File::create(temp.clone()).unwrap();
1256
1257 let invalid = r#"---
1258 This is invalid yaml:
1259 {
1260 {
1261 ---
1262 "#;
1263
1264 f.write_all(invalid.as_bytes()).unwrap();
1265
1266 let error = format!("{}", Snapshot::from_file(temp.as_path()).unwrap_err());
1267 assert!(error.contains("Failed parsing the YAML from"));
1268 assert!(error.contains("bad.yaml"));
1269}
1270
1271#[test]
1273fn test_ownership() {
1274 use std::ops::Range;
1276 let r = Range { start: 0, end: 10 };
1277 assert_debug_snapshot!(r, @"0..10");
1278 assert_debug_snapshot!(r, @"0..10");
1279}
1280
1281#[test]
1282fn test_empty_lines() {
1283 assert_snapshot!(r#"single line should fit on a single line"#, @"single line should fit on a single line");
1284 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");
1285
1286 assert_snapshot!(r#"multiline content starting on first line
1287
1288 final line
1289 "#, @r###"
1290 multiline content starting on first line
1291
1292 final line
1293
1294 "###);
1295
1296 assert_snapshot!(r#"
1297 multiline content starting on second line
1298
1299 final line
1300 "#, @r###"
1301
1302 multiline content starting on second line
1303
1304 final line
1305
1306 "###);
1307}