diff --git a/CHANGELOG.md b/CHANGELOG.md index 7abf0427..dbaf4b6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ All notable changes to insta and cargo-insta are documented here. nextest. Shows a deprecation warning when nextest is used with doctests without this flag, to prepare `cargo insta` to no longer run a separate doctest process when using nextest in the future. #803 +- We no longer trim starting newlines during assertions, which allows asserting + the number of leading newlines match. Existing assertions with different + leading newlines will pass and print a warning suggesting running with + `--force-update-snapshots`. They may fail in the future. (Note that we still + currently allow differing _trailing_ newlines, though may adjust this in the + future). #563 + + ## 1.43.2 - Fix panics when `cargo metadata` fails to execute or parse (e.g., when cargo is not in PATH or returns invalid output). Now falls back to using the manifest directory as the workspace root. #798 (@adriangb) diff --git a/cargo-insta/tests/functional/binary.rs b/cargo-insta/tests/functional/binary.rs index dfa03573..a4050a47 100644 --- a/cargo-insta/tests/functional/binary.rs +++ b/cargo-insta/tests/functional/binary.rs @@ -26,8 +26,7 @@ fn test_binary_snapshot() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,8 @@ - + @@ -1,3 +1,7 @@ + Cargo.lock Cargo.toml src @@ -66,8 +65,7 @@ fn test_binary_snapshot() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,8 @@ - + @@ -1,3 +1,7 @@ + Cargo.lock Cargo.toml src @@ -123,8 +121,7 @@ fn test_binary_snapshot() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,10 @@ - + @@ -1,3 +1,9 @@ + Cargo.lock Cargo.toml src @@ -147,8 +144,7 @@ fn test_binary_snapshot() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,8 @@ - + @@ -1,3 +1,7 @@ + Cargo.lock Cargo.toml src @@ -199,8 +195,7 @@ fn test_binary_snapshot() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,6 @@ - + @@ -1,3 +1,5 @@ + Cargo.lock Cargo.toml src @@ -237,8 +232,7 @@ fn test() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,7 @@ - + @@ -1,3 +1,6 @@ + Cargo.lock Cargo.toml src @@ -268,8 +262,7 @@ fn test() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,8 @@ - + @@ -1,3 +1,7 @@ + Cargo.lock Cargo.toml src @@ -308,8 +301,7 @@ fn test() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,8 @@ - + @@ -1,3 +1,7 @@ + Cargo.lock Cargo.toml src @@ -340,8 +332,7 @@ fn test() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,7 @@ - + @@ -1,3 +1,6 @@ + Cargo.lock Cargo.toml src @@ -381,8 +372,7 @@ fn test_snapshot() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,8 @@ - + @@ -1,3 +1,7 @@ + Cargo.lock Cargo.toml src @@ -411,8 +401,7 @@ fn test_snapshot() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,6 @@ - + @@ -1,3 +1,5 @@ + Cargo.lock Cargo.toml src diff --git a/cargo-insta/tests/functional/delete_pending.rs b/cargo-insta/tests/functional/delete_pending.rs index 37869f1b..1f03a495 100644 --- a/cargo-insta/tests/functional/delete_pending.rs +++ b/cargo-insta/tests/functional/delete_pending.rs @@ -45,8 +45,7 @@ fn test_snapshot_file() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,7 @@ - + @@ -1,3 +1,6 @@ + Cargo.lock Cargo.toml src @@ -70,8 +69,7 @@ fn test_snapshot_file() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,6 @@ - + @@ -1,3 +1,5 @@ + Cargo.lock Cargo.toml src @@ -116,8 +114,7 @@ fn test_file_snapshot() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,7 @@ - + @@ -1,3 +1,6 @@ + Cargo.lock Cargo.toml src @@ -158,8 +155,7 @@ fn test_file_snapshot() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,9 @@ - + @@ -1,3 +1,8 @@ + Cargo.lock Cargo.toml src @@ -183,8 +179,7 @@ fn test_file_snapshot() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,7 @@ - + @@ -1,3 +1,6 @@ + Cargo.lock Cargo.toml src @@ -206,8 +201,7 @@ fn test_file_snapshot() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,9 @@ - + @@ -1,3 +1,8 @@ + Cargo.lock Cargo.toml src @@ -250,8 +244,7 @@ fn test_file_snapshot() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,7 @@ - + @@ -1,3 +1,6 @@ + Cargo.lock Cargo.toml src diff --git a/cargo-insta/tests/functional/inline_snapshot_trimming.rs b/cargo-insta/tests/functional/inline_snapshot_trimming.rs new file mode 100644 index 00000000..d2f3f0bf --- /dev/null +++ b/cargo-insta/tests/functional/inline_snapshot_trimming.rs @@ -0,0 +1,342 @@ +use crate::TestFiles; +use std::process::Stdio; + +/// # Inline Snapshot Leading Newline Tests +/// +/// These tests verify the new behavior where multiline inline snapshots +/// must start with a newline after the opening delimiter. +/// +/// ## New Behavior: +/// 1. Multiline snapshots should start with a newline after the delimiter +/// 2. If they don't, a warning is issued +/// 3. The leading newline is stripped during processing +/// 4. Single-line snapshots are unaffected +/// +/// ## Backwards Compatibility: +/// - Old snapshots with excess indentation still work (trimming already existed) +/// - No warnings are issued for indentation (only for missing newlines) +#[test] +fn test_warning_only_for_missing_newline() { + // Test 1: Missing leading newline - SHOULD WARN + let test_project = TestFiles::new() + .add_cargo_toml("missing_newline") + .add_file( + "src/lib.rs", + r#" +#[test] +fn test_missing() { + insta::assert_snapshot!("line1\nline2", @"line1 +line2"); +} +"# + .to_string(), + ) + .create_project(); + + let output = test_project + .insta_cmd() + .args(["test", "--accept"]) + .stderr(Stdio::piped()) + .output() + .unwrap(); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("Multiline inline snapshot values should start and end with a newline"), + "Should warn for missing leading newline" + ); + assert!( + stderr.contains("The existing value's first line is `line1`"), + "Warning should show the problematic line" + ); + + // Test 2: Proper leading newline - SHOULD NOT WARN + let test_project = TestFiles::new() + .add_cargo_toml("proper_newline") + .add_file( + "src/lib.rs", + r#" +#[test] +fn test_proper() { + insta::assert_snapshot!("line1\nline2", @" +line1 +line2 +"); +} +"# + .to_string(), + ) + .create_project(); + + let output = test_project + .insta_cmd() + .args(["test", "--accept"]) + .stderr(Stdio::piped()) + .output() + .unwrap(); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + !stderr.contains("Multiline inline snapshot values should start and end with a newline"), + "Should NOT warn when leading newline is present. Got: {stderr}" + ); + + // Test 3: Single-line - SHOULD NOT WARN + let test_project = TestFiles::new() + .add_cargo_toml("single_line") + .add_file( + "src/lib.rs", + r#" +#[test] +fn test_single() { + insta::assert_snapshot!("single", @"single"); +} +"# + .to_string(), + ) + .create_project(); + + let output = test_project + .insta_cmd() + .args(["test", "--accept"]) + .stderr(Stdio::piped()) + .output() + .unwrap(); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + !stderr.contains("Multiline inline snapshot values should start and end with a newline"), + "Should NOT warn for single-line snapshots. Got: {stderr}" + ); +} + +/// Test that leading newlines are properly handled (stripped from multiline) +#[test] +fn test_leading_newline_processing() { + let test_project = TestFiles::new() + .add_cargo_toml("newline_processing") + .add_file( + "src/lib.rs", + r#" +#[test] +fn test_multiline_with_leading_newline() { + // The leading newline should be stripped during processing + let value = "content"; + insta::assert_snapshot!(value, @" +content +"); +} + +#[test] +fn test_multiline_with_indentation() { + // Leading newline + indentation trimming (pre-existing feature) + let value = "line1\nline2"; + insta::assert_snapshot!(value, @" + line1 + line2 + "); +} +"# + .to_string(), + ) + .create_project(); + + // Tests should pass + let output = test_project.insta_cmd().args(["test"]).output().unwrap(); + assert!( + output.status.success(), + "Tests should pass with proper newline handling" + ); +} + +/// Test backwards compatibility - old format multiline without leading newline still works +#[test] +fn test_backwards_compatibility() { + let test_project = TestFiles::new() + .add_cargo_toml("backwards_compat") + .add_file( + "src/lib.rs", + r#" +#[test] +fn test_old_format_multiline() { + // Old format without leading newline - should still pass but with warning + insta::assert_snapshot!("hello\nworld", @"hello +world"); +} +"# + .to_string(), + ) + .create_project(); + + // Run test - should pass despite old format + let output = test_project + .insta_cmd() + .args(["test"]) + .stderr(Stdio::piped()) + .output() + .unwrap(); + + let stderr = String::from_utf8_lossy(&output.stderr); + + // Should warn about missing leading newline + assert!( + stderr.contains("Multiline inline snapshot values should start and end with a newline"), + "Should warn about missing leading newline in old format" + ); + + // But should still pass (backwards compatibility) + assert!( + output.status.success(), + "Old format should still pass with warning" + ); +} + +/// Test that no warnings are issued for excess indentation (only for missing newlines) +#[test] +fn test_no_indentation_warnings() { + let test_project = TestFiles::new() + .add_cargo_toml("indentation_test") + .add_file( + "src/lib.rs", + r#" +#[test] +fn test_excess_indentation() { + // Has leading newline but lots of indentation - should NOT warn + insta::assert_snapshot!("content", @" + content + "); +} + +#[test] +fn test_multiline_excess_indentation() { + // Multiline with proper leading newline but excess indentation - should NOT warn + insta::assert_snapshot!("line1\nline2", @" + line1 + line2 + "); +} +"# + .to_string(), + ) + .create_project(); + + // Run with --accept + let output = test_project + .insta_cmd() + .args(["test", "--accept"]) + .stderr(Stdio::piped()) + .output() + .unwrap(); + + let stderr = String::from_utf8_lossy(&output.stderr); + + // Should pass without warnings + assert!(output.status.success(), "Tests should pass"); + + // Verify NO warnings about missing newlines (they have proper newlines) + assert!( + !stderr.contains("Multiline inline snapshot values should start and end with a newline"), + "Should NOT warn about excess indentation (only missing newlines trigger warnings)" + ); +} + +/// Test edge cases for single-line vs multiline detection +#[test] +fn test_single_vs_multiline_detection() { + let test_project = TestFiles::new() + .add_cargo_toml("line_detection") + .add_file( + "src/lib.rs", + r#" +#[test] +fn test_single_line_with_escaped_n() { + // Contains literal backslash-n, not a newline - single line + insta::assert_snapshot!("has\\\\n", @"has\\n"); +} + +#[test] +fn test_actual_multiline() { + // Actual multiline content - should require leading newline + insta::assert_snapshot!("line1\nline2", @" +line1 +line2 +"); +} + +#[test] +fn test_empty_string() { + // Empty strings are single-line + insta::assert_snapshot!("", @""); +} + +#[test] +fn test_whitespace_only() { + // Whitespace-only with trimming + insta::assert_snapshot!(" ", @" "); +} +"# + .to_string(), + ) + .create_project(); + + // Run with --accept in case any snapshots need updating + let output = test_project + .insta_cmd() + .args(["test", "--accept"]) + .output() + .unwrap(); + assert!( + output.status.success(), + "All line detection tests should pass" + ); +} + +/// Test that warnings persist across runs until fixed +#[test] +fn test_warning_persistence() { + let test_project = TestFiles::new() + .add_cargo_toml("warning_persist") + .add_file( + "src/lib.rs", + r#" +#[test] +fn test_needs_newline() { + // Missing leading newline + insta::assert_snapshot!("line1\nline2", @"line1 +line2"); +} +"# + .to_string(), + ) + .create_project(); + + // First run should warn + let output = test_project + .insta_cmd() + .args(["test"]) + .stderr(Stdio::piped()) + .output() + .unwrap(); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("Multiline inline snapshot values should start and end with a newline"), + "Should warn on first run" + ); + assert!(output.status.success()); + + // Second run should still warn (warning persists until fixed) + let output = test_project + .insta_cmd() + .args(["test"]) + .stderr(Stdio::piped()) + .output() + .unwrap(); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("Multiline inline snapshot values should start and end with a newline"), + "Should continue warning until format is fixed" + ); + assert!(output.status.success()); +} diff --git a/cargo-insta/tests/functional/main.rs b/cargo-insta/tests/functional/main.rs index 6e2d3cb3..c7d3ca82 100644 --- a/cargo-insta/tests/functional/main.rs +++ b/cargo-insta/tests/functional/main.rs @@ -72,6 +72,7 @@ mod binary; mod delete_pending; mod glob_filter; mod inline; +mod inline_snapshot_trimming; mod nextest_doctest; mod test_workspace_source_path; mod unreferenced; @@ -281,6 +282,7 @@ impl TestProject { let path_str = path.to_str().map(|s| s.replace('\\', "/")).unwrap(); format!("{}{}", " ".repeat(entry.depth()), path_str) }) + .filter(|line| !line.is_empty()) .chain(std::iter::once(String::new())) .collect::>() .join("\n") @@ -508,6 +510,7 @@ fn test_wrong_indent_force() { foo foo "#, @r#" + foo foo "#); @@ -538,8 +541,6 @@ fn test_wrong_indent_force() { #[test] fn test_matches_fully_linebreaks() { - // Until #563 merges, we should be OK with different leading newlines, even - // in exact / full match mode. let test_project = TestFiles::new() .add_cargo_toml("exact-match-inline") .add_file( @@ -555,6 +556,8 @@ fn test_additional_linebreak() { "insta_tests__tests", ) "#, @r#" + + ( "name_foo", "insta_tests__tests", @@ -566,8 +569,7 @@ fn test_additional_linebreak() { ) .create_project(); - // Confirm the test passes despite the indent - let output = test_project + assert!(&test_project .insta_cmd() .args([ "test", @@ -577,8 +579,9 @@ fn test_additional_linebreak() { "--nocapture", ]) .output() - .unwrap(); - assert!(&output.status.success()); + .unwrap() + .status + .success()); } #[test] @@ -619,6 +622,7 @@ fn foo_always_missing() { // Check for the name clash error message assert!(error_output.contains("Insta snapshot name clash detected between 'foo_always_missing' and 'test_foo_always_missing' in 'snapshot_name_clash_test'. Rename one function.")); } + #[test] fn test_hidden_snapshots() { let test_project = TestFiles::new() diff --git a/cargo-insta/tests/functional/unreferenced.rs b/cargo-insta/tests/functional/unreferenced.rs index ebcbcd9d..042ac4fb 100644 --- a/cargo-insta/tests/functional/unreferenced.rs +++ b/cargo-insta/tests/functional/unreferenced.rs @@ -177,8 +177,7 @@ Unused snapshot insta::assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,8 @@ - + @@ -1,3 +1,7 @@ + Cargo.lock Cargo.toml src @@ -207,8 +206,7 @@ Unused snapshot insta::assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,7 @@ - + @@ -1,3 +1,6 @@ + Cargo.lock Cargo.toml src diff --git a/cargo-insta/tests/functional/workspace.rs b/cargo-insta/tests/functional/workspace.rs index 2939efc0..e77678db 100644 --- a/cargo-insta/tests/functional/workspace.rs +++ b/cargo-insta/tests/functional/workspace.rs @@ -91,8 +91,7 @@ fn test_root_crate_workspace_accept() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,8 +1,13 @@ - + @@ -1,7 +1,12 @@ + Cargo.lock Cargo.toml member @@ -148,13 +147,12 @@ fn test_root_crate_no_all() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,5 @@ - + @@ -1,3 +1,4 @@ + Cargo.lock Cargo.toml member member/Cargo.toml - @@ -6,3 +7,5 @@ + @@ -5,3 +6,5 @@ member/src/lib.rs src src/main.rs @@ -250,8 +248,7 @@ fn test_virtual_manifest_all() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,10 +1,15 @@ - + @@ -1,9 +1,14 @@ + Cargo.lock Cargo.toml member-1 @@ -287,8 +284,7 @@ fn test_virtual_manifest_default() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,10 +1,15 @@ - + @@ -1,9 +1,14 @@ + Cargo.lock Cargo.toml member-1 @@ -324,8 +320,7 @@ fn test_virtual_manifest_single_crate() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,9 +1,12 @@ - + @@ -1,8 +1,11 @@ + Cargo.lock Cargo.toml member-1 @@ -810,8 +805,7 @@ fn test_inline() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,7 @@ - + @@ -1,3 +1,6 @@ + Cargo.lock Cargo.toml src @@ -866,8 +860,7 @@ fn test_in_root() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,3 +1,6 @@ - + @@ -1,2 +1,5 @@ + Cargo.lock Cargo.toml root_test.rs @@ -957,8 +950,7 @@ fn test_inline() { assert_snapshot!(test_project.file_tree_diff(), @r" --- Original file tree +++ Updated file tree - @@ -1,4 +1,7 @@ - + @@ -1,3 +1,6 @@ + Cargo.lock Cargo.toml src diff --git a/insta/src/runtime.rs b/insta/src/runtime.rs index 796565ae..7da915e7 100644 --- a/insta/src/runtime.rs +++ b/insta/src/runtime.rs @@ -384,8 +384,7 @@ impl<'a> SnapshotAssertionContext<'a> { module_path.replace("::", "__"), None, MetaData::default(), - TextSnapshotContents::new(contents.to_string(), TextSnapshotKind::Inline) - .into(), + SnapshotContents::Text(TextSnapshotContents::from_inline_literal(contents)), )); } }; diff --git a/insta/src/snapshot.rs b/insta/src/snapshot.rs index a70b0504..a935d42a 100644 --- a/insta/src/snapshot.rs +++ b/insta/src/snapshot.rs @@ -6,12 +6,13 @@ use crate::{ use once_cell::sync::Lazy; use std::env; use std::error::Error; +use std::fmt; use std::fs; use std::io::{BufRead, BufReader, Write}; use std::path::{Path, PathBuf}; use std::rc::Rc; use std::time::{SystemTime, UNIX_EPOCH}; -use std::{borrow::Cow, fmt}; +use std::{borrow::Cow, iter::once}; static RUN_ID: Lazy = Lazy::new(|| { if let Ok(run_id) = env::var("NEXTEST_RUN_ID") { @@ -646,6 +647,11 @@ impl TextSnapshotContents { TextSnapshotContents { contents, kind } } + /// Matches another snapshot without any normalization + pub fn matches_fully(&self, other: &TextSnapshotContents) -> bool { + self.contents == other.contents + } + /// Snapshot matches based on the latest format. pub fn matches_latest(&self, other: &Self) -> bool { self.to_string() == other.to_string() @@ -653,30 +659,64 @@ impl TextSnapshotContents { pub fn matches_legacy(&self, other: &Self) -> bool { fn as_str_legacy(sc: &TextSnapshotContents) -> String { + // First do the standard normalization let out = sc.to_string(); + // Legacy snapshots trim newlines at the start. + let out = out.trim_start_matches(['\r', '\n']); // Legacy inline snapshots have `---` at the start, so this strips that if // it exists. let out = match out.strip_prefix("---\n") { - Some(old_snapshot) => old_snapshot.to_string(), + Some(old_snapshot) => old_snapshot, None => out, }; match sc.kind { - TextSnapshotKind::Inline => legacy_inline_normalize(&out), - TextSnapshotKind::File => out, + TextSnapshotKind::Inline => legacy_inline_normalize(out), + TextSnapshotKind::File => out.to_string(), } } as_str_legacy(self) == as_str_legacy(other) } + /// Convert a literal snapshot value (i.e. the string inside the quotes, + /// from a rust file) to the value we retain in the struct. This is a small + /// change to the value: we remove the leading newline and coerce newlines + /// to `\n`. Otherwise, the value is retained unnormalized (generally we + /// want to retain unnormalized values so we can run `matches_fully` on + /// them) + pub(crate) fn from_inline_literal(contents: &str) -> Self { + // If it's a single line string, then we don't do anything. + if contents.trim_end().lines().count() <= 1 { + return Self::new(contents.trim_end().to_string(), TextSnapshotKind::Inline); + } + + // If it's multiline, we trim the first line, which should be empty. + // (Possibly in the future we'll do the same for the final line too) + let lines = contents.lines().collect::>(); + let (first, remainder) = lines.split_first().unwrap(); + let snapshot = { + // If the first isn't empty, something is up — include the first line + // and print a warning. + if first != &"" { + 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); + once(first) + .chain(remainder.iter()) + .cloned() + .collect::>() + .join("\n") + } else { + remainder.join("\n") + } + }; + Self::new(snapshot, TextSnapshotKind::Inline) + } + fn normalize(&self) -> String { let kind_specific_normalization = match self.kind { - TextSnapshotKind::Inline => normalize_inline_snapshot(&self.contents), + TextSnapshotKind::Inline => normalize_inline(&self.contents), TextSnapshotKind::File => self.contents.clone(), }; // Then this we do for both kinds - let out = kind_specific_normalization - .trim_start_matches(['\r', '\n']) - .trim_end(); + let out = kind_specific_normalization.trim_end(); out.replace("\r\n", "\n") } @@ -813,11 +853,6 @@ fn leading_space(value: &str) -> String { fn min_indentation(snapshot: &str) -> String { let lines = snapshot.trim_end().lines(); - if lines.clone().count() <= 1 { - // not a multi-line string - return "".into(); - } - lines .filter(|l| !l.is_empty()) .map(leading_space) @@ -825,8 +860,15 @@ fn min_indentation(snapshot: &str) -> String { .unwrap_or("".into()) } -/// Removes excess indentation, and changes newlines to \n. -fn normalize_inline_snapshot(snapshot: &str) -> String { +/// Normalize snapshot value, which we apply to both generated and literal +/// snapshots. Remove excess indentation, excess ending whitespace and coerce +/// newlines to `\n`. +fn normalize_inline(snapshot: &str) -> String { + // If it's a single line string, then we don't do anything. + if snapshot.trim_end().lines().count() <= 1 { + return snapshot.trim_end().to_string(); + } + let indentation = min_indentation(snapshot); snapshot .lines() @@ -835,6 +877,119 @@ fn normalize_inline_snapshot(snapshot: &str) -> String { .join("\n") } +#[test] +fn test_normalize_inline_snapshot() { + fn normalized_of_literal(snapshot: &str) -> String { + normalize_inline(&TextSnapshotContents::from_inline_literal(snapshot).contents) + } + + use similar_asserts::assert_eq; + // here we do exact matching (rather than `assert_snapshot`) to ensure we're + // not incorporating the modifications that insta itself makes + + assert_eq!( + normalized_of_literal( + " + 1 + 2 +" + ), + "1 +2" + ); + + assert_eq!( + normalized_of_literal( + r#" + 1 + 2 + "# + ), + r" 1 +2 +" + ); + + assert_eq!( + normalized_of_literal( + " + 1 + 2 + " + ), + r"1 +2 +" + ); + + assert_eq!( + normalized_of_literal( + " + 1 + 2 +" + ), + "1 +2" + ); + + assert_eq!( + normalized_of_literal( + " + a + " + ), + " a" + ); + + assert_eq!(normalized_of_literal(""), ""); + + assert_eq!( + normalized_of_literal( + r#" + a + b +c + "# + ), + " a + b +c + " + ); + + assert_eq!( + normalized_of_literal( + " +a + " + ), + "a" + ); + + // This is a bit of a weird case, but because it's not a true multiline + // (which requires an opening and closing newline), we don't trim the + // indentation. Not terrible if this needs to change. The next test shows + // how a real multiline string is handled. + assert_eq!( + normalized_of_literal( + " + a" + ), + " a" + ); + + // This test will pass but raise a warning, so we comment it out for the moment. + // assert_eq!( + // normalized_of_literal( + // "a + // a" + // ), + // "a + // a" + // ); +} + /// Extracts the module and snapshot name from a snapshot path fn names_of_path(path: &Path) -> (String, String) { // The final part of the snapshot file name is the test name; the @@ -925,18 +1080,16 @@ fn legacy_inline_normalize(frozen_value: &str) -> String { } #[test] -fn test_snapshot_contents() { +fn test_snapshot_contents_to_inline() { use similar_asserts::assert_eq; let snapshot_contents = TextSnapshotContents::new("testing".to_string(), TextSnapshotKind::Inline); assert_eq!(snapshot_contents.to_inline(""), r#""testing""#); - let t = &" -a -b"[1..]; assert_eq!( - TextSnapshotContents::new(t.to_string(), TextSnapshotKind::Inline).to_inline(""), + TextSnapshotContents::new("\na\nb".to_string(), TextSnapshotKind::Inline).to_inline(""), r##"r" + a b ""## @@ -954,6 +1107,7 @@ b TextSnapshotContents::new("\n a\n b".to_string(), TextSnapshotKind::Inline) .to_inline(""), r##"r" + a b ""## @@ -963,15 +1117,41 @@ b TextSnapshotContents::new("\na\n\nb".to_string(), TextSnapshotKind::Inline) .to_inline(" "), r##"r" + a b ""## ); + assert_eq!( + TextSnapshotContents::new( + "ab + " + .to_string(), + TextSnapshotKind::Inline + ) + .to_inline(""), + r#""ab""# + ); + + assert_eq!( + TextSnapshotContents::new( + " ab + " + .to_string(), + TextSnapshotKind::Inline + ) + .to_inline(""), + r##"" ab""## + ); + assert_eq!( TextSnapshotContents::new("\n ab\n".to_string(), TextSnapshotKind::Inline).to_inline(""), - r##""ab""## + r##"r" + +ab +""## ); assert_eq!( @@ -1028,118 +1208,96 @@ a } #[test] -fn test_normalize_inline_snapshot() { +fn test_min_indentation() { use similar_asserts::assert_eq; - // here we do exact matching (rather than `assert_snapshot`) - // to ensure we're not incorporating the modifications this library makes assert_eq!( - normalize_inline_snapshot( + min_indentation( r#" 1 2 "#, ), - r###" -1 -2 -"### + " ".to_string() ); assert_eq!( - normalize_inline_snapshot( + min_indentation( r#" 1 2"# ), - r###" - 1 -2"### + " ".to_string() ); assert_eq!( - normalize_inline_snapshot( + min_indentation( r#" 1 2 "# ), - r###" -1 -2 -"### + " ".to_string() ); assert_eq!( - normalize_inline_snapshot( + min_indentation( r#" 1 2 "# ), - r###" -1 -2"### + " ".to_string() ); assert_eq!( - normalize_inline_snapshot( + min_indentation( r#" a "# ), - " -a -" + " ".to_string() ); - assert_eq!(normalize_inline_snapshot(""), ""); + assert_eq!(min_indentation(""), "".to_string()); assert_eq!( - normalize_inline_snapshot( + min_indentation( r#" a b c "# ), - r###" - a - b -c - "### + "".to_string() ); assert_eq!( - normalize_inline_snapshot( + min_indentation( r#" a "# ), - " -a - " + "".to_string() ); assert_eq!( - normalize_inline_snapshot( + min_indentation( " a" ), - " -a" + " ".to_string() ); assert_eq!( - normalize_inline_snapshot( + min_indentation( r#"a a"# ), - r###"a - a"### + "".to_string() ); assert_eq!( - normalize_inline_snapshot( + normalize_inline( r#" 1 2"# @@ -1150,7 +1308,7 @@ a" ); assert_eq!( - normalize_inline_snapshot( + normalize_inline( r#" 1 2 @@ -1164,24 +1322,8 @@ a" } #[test] -fn test_min_indentation() { +fn test_min_indentation_additional() { use similar_asserts::assert_eq; - let t = r#" - 1 - 2 - "#; - assert_eq!(min_indentation(t), " ".to_string()); - - let t = r#" - 1 - 2"#; - assert_eq!(min_indentation(t), " ".to_string()); - - let t = r#" - 1 - 2 - "#; - assert_eq!(min_indentation(t), " ".to_string()); let t = r#" 1 @@ -1205,12 +1347,11 @@ c assert_eq!(min_indentation(t), "".to_string()); let t = r#" -a - "#; +a"#; assert_eq!(min_indentation(t), "".to_string()); - let t = " - a"; + let t = r#" + a"#; assert_eq!(min_indentation(t), " ".to_string()); let t = r#"a @@ -1237,7 +1378,7 @@ a #[test] fn test_inline_snapshot_value_newline() { // https://github.com/mitsuhiko/insta/issues/39 - assert_eq!(normalize_inline_snapshot("\n"), ""); + assert_eq!(normalize_inline("\n"), ""); } #[test] @@ -1274,7 +1415,7 @@ fn test_ownership() { #[test] fn test_empty_lines() { assert_snapshot!(r#"single line should fit on a single line"#, @"single line should fit on a single line"); - 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"); + 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"); assert_snapshot!(r#"multiline content starting on first line