diff --git a/src/DiffEngine.Tests/InlinePatcherFsTests.cs b/src/DiffEngine.Tests/InlinePatcherFsTests.cs
index 86350c0b..8eb34314 100644
--- a/src/DiffEngine.Tests/InlinePatcherFsTests.cs
+++ b/src/DiffEngine.Tests/InlinePatcherFsTests.cs
@@ -1,8 +1,27 @@
public class InlinePatcherFsTests
{
+ ///
+ /// A whole file of source, written as a raw string so that it reads as the code it stands for.
+ ///
+ /// Normalized on the way in because a raw string carries the line endings of the file holding
+ /// it rather than normalizing them, and every expectation here is written in LF. The checkout
+ /// is LF whatever the platform (* text=auto eol=lf), so this only ever matters to a file
+ /// that arrived some other way - but it is line endings, in the suite that patches them, and
+ /// the failure it produces names the wrong thing entirely.
+ ///
+ ///
+ /// A fixture whose subject is line endings, tabs, or runs of quotes is built by hand instead: a
+ /// raw string cannot carry those without a delimiter wider than the thing being described, or
+ /// without indentation that a formatter is free to rewrite. F# writes its multi-line snapshots
+ /// triple quoted, so that last one covers most of the fixtures here.
+ ///
+ ///
+ static string Source(string source) =>
+ SourceLanguage.NormalizeNewlines(source);
+
// Line 5 is the first line of the body
static string Test(string body) =>
- $"module Tests\n\n[]\nlet MyTest () =\n{body}\n";
+ Source($"module Tests\n\n[]\nlet MyTest () =\n{body}\n");
static PatchStatus TryApply(
string source,
@@ -101,9 +120,11 @@ await Assert.That(newSource).IsEqualTo(
public async Task DeepCallSiteIndentsFurther()
{
var source = Test(
- " let inner () =\n" +
- " Verifier.Verify(15).Snapshot().ToTask()\n" +
- " inner ()");
+ """
+ let inner () =
+ Verifier.Verify(15).Snapshot().ToTask()
+ inner ()
+ """);
var status = TryApply(source, 6, InlinePatchMode.Set, null, "a\nb", out var newSource, out _);
@@ -191,15 +212,16 @@ public async Task ValueAnchorFindsTheCall()
[Test]
public async Task ValueAnchorBeatsAStaleHint()
{
- var source = string.Join(
- "\n",
- "module Tests",
- "",
- "let TestA () =",
- " Verifier.Verify(a).Snapshot(\"a\").ToTask()",
- "",
- "let TestB () =",
- " Verifier.Verify(b).Snapshot(\"b\").ToTask()");
+ var source = Source(
+ """
+ module Tests
+
+ let TestA () =
+ Verifier.Verify(a).Snapshot("a").ToTask()
+
+ let TestB () =
+ Verifier.Verify(b).Snapshot("b").ToTask()
+ """);
var status = TryApply(source, 4, InlinePatchMode.Set, null, "new", out var newSource, out _, originalValue: "b");
@@ -250,15 +272,16 @@ public async Task ValueAnchorAcrossAMultiLineLiteral()
}
static string TwoTests(string literalA, string literalB) =>
- string.Join(
- "\n",
- "module Tests",
- "",
- "let TestA () =",
- $" Verifier.Verify(a).Snapshot({literalA}).ToTask()",
- "",
- "let TestB () =",
- $" Verifier.Verify(b).Snapshot({literalB}).ToTask()");
+ Source(
+ $$"""
+ module Tests
+
+ let TestA () =
+ Verifier.Verify(a).Snapshot({{literalA}}).ToTask()
+
+ let TestB () =
+ Verifier.Verify(b).Snapshot({{literalB}}).ToTask()
+ """);
// A call above TestB's declaration is not inside TestB, whatever the hint says, so the
// identical snapshot in the test above is not even a candidate
@@ -346,31 +369,60 @@ public async Task AppendGoesInFrontOfToTask()
await Assert.That(status).IsEqualTo(PatchStatus.Applied);
await Assert.That(newSource).IsEqualTo(
Test(
- " Verifier.Verify(15)\n" +
- " .Snapshot(\"new\").ToTask() |> Async.AwaitTask"));
+ """
+ Verifier.Verify(15)
+ .Snapshot("new").ToTask() |> Async.AwaitTask
+ """));
}
[Test]
public async Task AppendToAMultiLineChain()
{
var source = Test(
- " Verifier\n" +
- " .Verify(15)\n" +
- " .UseMethodName(\"customName\")\n" +
- " .ToTask()\n" +
- " |> Async.AwaitTask");
+ """
+ Verifier
+ .Verify(15)
+ .UseMethodName("customName")
+ .ToTask()
+ |> Async.AwaitTask
+ """);
var status = TryApply(source, 6, InlinePatchMode.Append, null, "new", out var newSource, out _);
await Assert.That(status).IsEqualTo(PatchStatus.Applied);
await Assert.That(newSource).IsEqualTo(
Test(
- " Verifier\n" +
- " .Verify(15)\n" +
- " .UseMethodName(\"customName\")\n" +
- " .Snapshot(\"new\")\n" +
- " .ToTask()\n" +
- " |> Async.AwaitTask"));
+ """
+ Verifier
+ .Verify(15)
+ .UseMethodName("customName")
+ .Snapshot("new")
+ .ToTask()
+ |> Async.AwaitTask
+ """));
+ }
+
+ // A let binding inside the test is a declaration exactly as the test's own let is, and reading
+ // one as another member declared the hint stale, which put the call it names out of reach
+ [Test]
+ public async Task AppendReachesAHintPastALocalBinding()
+ {
+ var source = Test(
+ """
+ let value = build ()
+ Verifier.Verify(value).ToTask()
+ """);
+
+ var status = TryApply(source, 6, InlinePatchMode.Append, null, "new", out var newSource, out _, memberName: "MyTest");
+
+ await Assert.That(status).IsEqualTo(PatchStatus.Applied);
+ await Assert.That(newSource).IsEqualTo(
+ Test(
+ """
+ let value = build ()
+ Verifier.Verify(value)
+ .Snapshot("new").ToTask()
+ """));
}
// Awaited in a task expression instead, so there is no ToTask and the chain end is the
@@ -379,19 +431,23 @@ await Assert.That(newSource).IsEqualTo(
public async Task AppendWithNoToTask()
{
var source = Test(
- " task {\n" +
- " do! Verifier.Verify(15)\n" +
- " }");
+ """
+ task {
+ do! Verifier.Verify(15)
+ }
+ """);
var status = TryApply(source, 6, InlinePatchMode.Append, null, "new", out var newSource, out _);
await Assert.That(status).IsEqualTo(PatchStatus.Applied);
await Assert.That(newSource).IsEqualTo(
Test(
- " task {\n" +
- " do! Verifier.Verify(15)\n" +
- " .Snapshot(\"new\")\n" +
- " }"));
+ """
+ task {
+ do! Verifier.Verify(15)
+ .Snapshot("new")
+ }
+ """));
}
[Test]
@@ -465,13 +521,14 @@ public async Task RemoveFromASingleLineChain()
[Test]
public async Task LineCommentedOutCallIsSkipped()
{
- var source = string.Join(
- "\n",
- "module Tests",
- "",
- "// Verifier.Verify(x).Snapshot(\"doc example\")",
- "let MyTest () =",
- " Verifier.Verify(x).Snapshot().ToTask()");
+ var source = Source(
+ """
+ module Tests
+
+ // Verifier.Verify(x).Snapshot("doc example")
+ let MyTest () =
+ Verifier.Verify(x).Snapshot().ToTask()
+ """);
var status = TryApply(source, 3, InlinePatchMode.Set, null, "new", out var newSource, out _);
@@ -483,13 +540,14 @@ public async Task LineCommentedOutCallIsSkipped()
[Test]
public async Task BlockCommentedOutCallIsSkipped()
{
- var source = string.Join(
- "\n",
- "module Tests",
- "",
- "(* Verifier.Verify(x).Snapshot(\"doc example\") *)",
- "let MyTest () =",
- " Verifier.Verify(x).Snapshot().ToTask()");
+ var source = Source(
+ """
+ module Tests
+
+ (* Verifier.Verify(x).Snapshot("doc example") *)
+ let MyTest () =
+ Verifier.Verify(x).Snapshot().ToTask()
+ """);
var status = TryApply(source, 3, InlinePatchMode.Set, null, "new", out var newSource, out _);
@@ -502,13 +560,14 @@ public async Task BlockCommentedOutCallIsSkipped()
[Test]
public async Task NestedBlockCommentIsOneComment()
{
- var source = string.Join(
- "\n",
- "module Tests",
- "",
- "(* outer (* inner *) .Snapshot(\"commented\") *)",
- "let MyTest () =",
- " Verifier.Verify(x).Snapshot().ToTask()");
+ var source = Source(
+ """
+ module Tests
+
+ (* outer (* inner *) .Snapshot("commented") *)
+ let MyTest () =
+ Verifier.Verify(x).Snapshot().ToTask()
+ """);
var status = TryApply(source, 3, InlinePatchMode.Set, null, "new", out var newSource, out _);
@@ -521,13 +580,14 @@ public async Task NestedBlockCommentIsOneComment()
[Test]
public async Task MultiplyOperatorIsNotAComment()
{
- var source = string.Join(
- "\n",
- "module Tests",
- "",
- "let multiply = (*)",
- "let MyTest () =",
- " Verifier.Verify(x).Snapshot().ToTask()");
+ var source = Source(
+ """
+ module Tests
+
+ let multiply = (*)
+ let MyTest () =
+ Verifier.Verify(x).Snapshot().ToTask()
+ """);
var status = TryApply(source, 5, InlinePatchMode.Set, null, "new", out var newSource, out _);
@@ -539,8 +599,10 @@ public async Task MultiplyOperatorIsNotAComment()
public async Task CallInsideAStringIsSkipped()
{
var source = Test(
- " let text = \"Verifier.Verify(x).Snapshot(\\\"y\\\")\"\n" +
- " Verifier.Verify(text).Snapshot().ToTask()");
+ """
+ let text = "Verifier.Verify(x).Snapshot(\"y\")"
+ Verifier.Verify(text).Snapshot().ToTask()
+ """);
var status = TryApply(source, 5, InlinePatchMode.Set, null, "new", out var newSource, out _);
@@ -569,8 +631,10 @@ public async Task CallInsideATripleQuotedStringIsSkipped()
public async Task TypeParameterIsNotACharLiteral()
{
var source = Test(
- " let values : 'T list = []\n" +
- " Verifier.Verify(values).Snapshot(\"old\").ToTask()");
+ """
+ let values : 'T list = []
+ Verifier.Verify(values).Snapshot("old").ToTask()
+ """);
var status = TryApply(source, 6, InlinePatchMode.Set, "\"old\"", "new", out var newSource, out _);
@@ -582,8 +646,10 @@ public async Task TypeParameterIsNotACharLiteral()
public async Task TickInAnIdentifierIsNotACharLiteral()
{
var source = Test(
- " let value' = 15\n" +
- " Verifier.Verify(value').Snapshot(\"old\").ToTask()");
+ """
+ let value' = 15
+ Verifier.Verify(value').Snapshot("old").ToTask()
+ """);
var status = TryApply(source, 6, InlinePatchMode.Set, "\"old\"", "new", out var newSource, out _);
@@ -596,8 +662,10 @@ public async Task TickInAnIdentifierIsNotACharLiteral()
public async Task CharLiteralIsSkipped()
{
var source = Test(
- " let quote = '\"'\n" +
- " Verifier.Verify(quote).Snapshot(\"old\").ToTask()");
+ """
+ let quote = '"'
+ Verifier.Verify(quote).Snapshot("old").ToTask()
+ """);
var status = TryApply(source, 6, InlinePatchMode.Set, "\"old\"", "new", out var newSource, out _);
@@ -609,11 +677,12 @@ public async Task CharLiteralIsSkipped()
[Test]
public async Task LetDeclarationIsNotMistakenForACall()
{
- var source = string.Join(
- "\n",
- "module Tests",
- "",
- "let Snapshot (expected: string) = expected");
+ var source = Source(
+ """
+ module Tests
+
+ let Snapshot (expected: string) = expected
+ """);
var status = TryApply(source, 3, InlinePatchMode.Set, null, "new", out _, out var reason);
@@ -624,13 +693,14 @@ public async Task LetDeclarationIsNotMistakenForACall()
[Test]
public async Task MemberDeclarationIsNotMistakenForACall()
{
- var source = string.Join(
- "\n",
- "module Tests",
- "",
- "type Extensions =",
- " member this.Snapshot (expected: string) = expected",
- " static member Snapshot (expected: string, other: string) = expected");
+ var source = Source(
+ """
+ module Tests
+
+ type Extensions =
+ member this.Snapshot (expected: string) = expected
+ static member Snapshot (expected: string, other: string) = expected
+ """);
var status = TryApply(source, 4, InlinePatchMode.Set, null, "new", out _, out var reason);
@@ -677,15 +747,16 @@ public async Task NoCallFound()
[Test]
public async Task SequentialPatchesOfIdenticalLiterals()
{
- var source = string.Join(
- "\n",
- "module Tests",
- "",
- "let TestA () =",
- " Verifier.Verify(a).Snapshot(\"old\").ToTask()",
- "",
- "let TestB () =",
- " Verifier.Verify(b).Snapshot(\"old\").ToTask()");
+ var source = Source(
+ """
+ module Tests
+
+ let TestA () =
+ Verifier.Verify(a).Snapshot("old").ToTask()
+
+ let TestB () =
+ Verifier.Verify(b).Snapshot("old").ToTask()
+ """);
var first = TryApply(source, 4, InlinePatchMode.Set, "\"old\"", "newA", out var afterFirst, out _);
var second = TryApply(afterFirst, 7, InlinePatchMode.Set, "\"old\"", "newB", out var afterSecond, out _);
diff --git a/src/DiffEngine.Tests/InlinePatcherTests.cs b/src/DiffEngine.Tests/InlinePatcherTests.cs
index 6dd990ad..2cdcf063 100644
--- a/src/DiffEngine.Tests/InlinePatcherTests.cs
+++ b/src/DiffEngine.Tests/InlinePatcherTests.cs
@@ -14,8 +14,26 @@ static PatchStatus TryApply(
const string rawOld = "\"\"\"\n old\n \"\"\"";
+ ///
+ /// A whole file of source, written as a raw string so that it reads as the code it stands for.
+ ///
+ /// Normalized on the way in because a raw string carries the line endings of the file holding
+ /// it rather than normalizing them, and every expectation here is written in LF. The checkout
+ /// is LF whatever the platform (* text=auto eol=lf), so this only ever matters to a file
+ /// that arrived some other way - but it is line endings, in the suite that patches them, and
+ /// the failure it produces names the wrong thing entirely.
+ ///
+ ///
+ /// A fixture whose subject is line endings, tabs, or runs of quotes is built by hand instead: a
+ /// raw string cannot carry those without a delimiter wider than the thing being described, or
+ /// without indentation that a formatter is free to rewrite.
+ ///
+ ///
+ static string Source(string source) =>
+ SourceLanguage.NormalizeNewlines(source);
+
static string Method(string body) =>
- $"class Tests\n{{\n async Task Test()\n {{\n{body}\n }}\n}}";
+ Source($"class Tests\n{{\n async Task Test()\n {{\n{body}\n }}\n}}");
[Test]
public async Task ReplaceRawLiteral()
@@ -132,8 +150,10 @@ public async Task ASnapshotBeforeAVerbatimStringOpeningOnAnEscapedQuoteIsStillFo
public async Task RemoveTakesTheCallTheAnchorNamesRatherThanTheNearest()
{
var source = Method(
- " await A().Snapshot(\"one\");\n" +
- " await B().Snapshot(\"two\");");
+ """
+ await A().Snapshot("one");
+ await B().Snapshot("two");
+ """);
// Hint on the second call, anchor on the first
var status = TryApply(source, 6, InlinePatchMode.Remove, "\"one\"", "", out var newSource, out _);
@@ -167,19 +187,21 @@ public async Task RemoveReportsWhenTheAnchorIsGone()
[Test]
public async Task AStaleHintDoesNotReachIntoTheNextMember()
{
- var source =
- "class Tests\n" +
- "{\n" +
- " async Task First()\n" +
- " {\n" +
- " await Snapshot(\"dup\");\n" +
- " }\n" +
- "\n" +
- " async Task Second()\n" +
- " {\n" +
- " await Snapshot(\"dup\");\n" +
- " }\n" +
- "}";
+ var source = Source(
+ """
+ class Tests
+ {
+ async Task First()
+ {
+ await Snapshot("dup");
+ }
+
+ async Task Second()
+ {
+ await Snapshot("dup");
+ }
+ }
+ """);
// Line 10 is Second's snapshot; the patch came from First
var status = TryApply(source, 10, InlinePatchMode.Set, "\"dup\"", "new", out var newSource, out _, memberName: "First");
@@ -226,8 +248,10 @@ public async Task AnAnchorMatchesAcrossMixedLineEndings()
public async Task ATrailingCommentIsNotPartOfTheArgument()
{
var source = Method(
- " await Snapshot(\"old\" // note\n" +
- " );");
+ """
+ await Snapshot("old" // note
+ );
+ """);
var status = TryApply(source, 5, InlinePatchMode.Set, "\"old\"", "new", out var newSource, out var reason);
@@ -291,16 +315,17 @@ public async Task DuplicateLiteralsPicksNearestToHint()
// Two call sites, A on line 4 and B on line 7
static string TwoCallSites(string literalA, string literalB) =>
- string.Join(
- "\n",
- "class Tests",
- "{",
- " Task A() =>",
- $" Verify(a).Snapshot({literalA});",
- "",
- " Task B() =>",
- $" Verify(b).Snapshot({literalB});",
- "}");
+ Source(
+ $$"""
+ class Tests
+ {
+ Task A() =>
+ Verify(a).Snapshot({{literalA}});
+
+ Task B() =>
+ Verify(b).Snapshot({{literalB}});
+ }
+ """);
static (string a, string b) Segments(string text)
{
@@ -328,15 +353,16 @@ public async Task DuplicateLiteralsPicksNearestToHintFirst()
[Test]
public async Task EquidistantDuplicatesPreferAtOrAfterHint()
{
- var source = string.Join(
- "\n",
- "class Tests",
- "{",
- " Task A() =>",
- " Verify(a).Snapshot(\"dup\");",
- " Task B() =>",
- " Verify(b).Snapshot(\"dup\");",
- "}");
+ var source = Source(
+ """
+ class Tests
+ {
+ Task A() =>
+ Verify(a).Snapshot("dup");
+ Task B() =>
+ Verify(b).Snapshot("dup");
+ }
+ """);
// Line 5 is equidistant from the sites on lines 4 and 6
var status = TryApply(source, 5, InlinePatchMode.Set, "\"dup\"", "new", out var newSource, out _);
@@ -525,16 +551,17 @@ public async Task MemberNameBeatsAStaleHint()
[Test]
public async Task RecordedLineWinsOverTheMemberDeclaration()
{
- var source = string.Join(
- "\n",
- "class Tests",
- "{",
- " async Task Test()",
- " {",
- " await Verify(a).Snapshot(\"dup\");",
- " await Verify(b).Snapshot(\"dup\");",
- " }",
- "}");
+ var source = Source(
+ """
+ class Tests
+ {
+ async Task Test()
+ {
+ await Verify(a).Snapshot("dup");
+ await Verify(b).Snapshot("dup");
+ }
+ }
+ """);
var status = TryApply(source, 6, InlinePatchMode.Set, "\"dup\"", "new", out var newSource, out _, memberName: "Test");
@@ -565,16 +592,17 @@ public async Task UnknownMemberNameFallsBackToTheHint()
[Test]
public async Task AppendDoesNotReachAHelperDeclaredAboveTheMember()
{
- var source = string.Join(
- "\n",
- "class Tests",
- "{",
- " static Task Run(string value) =>",
- " Verify(value);",
- "",
- " async Task Test() =>",
- " await Run(\"value\");",
- "}");
+ var source = Source(
+ """
+ class Tests
+ {
+ static Task Run(string value) =>
+ Verify(value);
+
+ async Task Test() =>
+ await Run("value");
+ }
+ """);
var status = TryApply(source, 4, InlinePatchMode.Append, null, "new", out _, out var reason, memberName: "Test");
@@ -587,19 +615,20 @@ public async Task AppendDoesNotReachAHelperDeclaredAboveTheMember()
[Test]
public async Task AppendPrefersACallInsideTheMemberOverAHelperAbove()
{
- var source = string.Join(
- "\n",
- "class Tests",
- "{",
- " static Task Run(string value) =>",
- " Verify(value);",
- "",
- " async Task Test()",
- " {",
- " await Verify(direct);",
- " await Run(\"value\");",
- " }",
- "}");
+ var source = Source(
+ """
+ class Tests
+ {
+ static Task Run(string value) =>
+ Verify(value);
+
+ async Task Test()
+ {
+ await Verify(direct);
+ await Run("value");
+ }
+ }
+ """);
var status = TryApply(source, 4, InlinePatchMode.Append, null, "new", out var newSource, out _, memberName: "Test");
@@ -609,6 +638,56 @@ await Assert.That(newSource).Contains(
" .Snapshot(\"new\");");
}
+ // The ordinary shape of a test: a local, then a verify call on it. The local sits between the
+ // member's declaration and the hint, and taking it for another member declared the hint stale -
+ // which drops it from the first try and from the outward walk both, so the call sitting exactly
+ // where the hint said was never looked at and an append reported NotFound
+ [Test]
+ public async Task AppendReachesAHintPastALocalDeclaration()
+ {
+ var source = Source(
+ """
+ class Tests
+ {
+ async Task Test()
+ {
+ var value = Build();
+ await Verify(value);
+ }
+ }
+ """);
+
+ var status = TryApply(source, 6, InlinePatchMode.Append, null, "new", out var newSource, out _, memberName: "Test");
+
+ await Assert.That(status).IsEqualTo(PatchStatus.Applied);
+ await Assert.That(newSource).Contains(
+ " await Verify(value)\n" +
+ " .Snapshot(\"new\");");
+ }
+
+ // Same cause, and every mode that locates by hint pays it: an explicitly typed local is as
+ // much a declaration as a var one
+ [Test]
+ public async Task SetReachesAHintPastALocalDeclaration()
+ {
+ var source = Source(
+ """
+ class Tests
+ {
+ async Task Test()
+ {
+ string value = Build();
+ await Snapshot("old");
+ }
+ }
+ """);
+
+ var status = TryApply(source, 6, InlinePatchMode.Set, "\"old\"", "new", out var newSource, out _, memberName: "Test");
+
+ await Assert.That(status).IsEqualTo(PatchStatus.Applied);
+ await Assert.That(newSource).Contains("await Snapshot(\"new\");");
+ }
+
[Test]
public async Task ExpressionWinsOverValue()
{
@@ -675,9 +754,11 @@ await Assert.That(newSource).Contains(
public async Task AppendGoesAfterAnExistingChain()
{
var source = Method(
- " await Verify(value)\n" +
- " .UseDirectory(\"snapshots\")\n" +
- " .ScrubLinesContaining(\"x\");");
+ """
+ await Verify(value)
+ .UseDirectory("snapshots")
+ .ScrubLinesContaining("x");
+ """);
var status = TryApply(source, 5, InlinePatchMode.Append, null, "new", out var newSource, out _);
@@ -703,11 +784,13 @@ public async Task AppendToAnEntryPointOverload()
public async Task AppendToAMultiLineVerifyCall()
{
var source = Method(
- " await Verify(\n" +
- " new\n" +
- " {\n" +
- " value\n" +
- " });");
+ """
+ await Verify(
+ new
+ {
+ value
+ });
+ """);
var status = TryApply(source, 5, InlinePatchMode.Append, null, "new", out var newSource, out _);
@@ -924,17 +1007,21 @@ public async Task RemoveTakesTheWholeLine()
public async Task RemoveLeavesTheRestOfTheChain()
{
var source = Method(
- " await Verify(value)\n" +
- " .UseDirectory(\"snapshots\")\n" +
- " .Snapshot(\"old\");");
+ """
+ await Verify(value)
+ .UseDirectory("snapshots")
+ .Snapshot("old");
+ """);
var status = TryApply(source, 7, InlinePatchMode.Remove, null, "", out var newSource, out _);
await Assert.That(status).IsEqualTo(PatchStatus.Applied);
await Assert.That(newSource).IsEqualTo(
Method(
- " await Verify(value)\n" +
- " .UseDirectory(\"snapshots\");"));
+ """
+ await Verify(value)
+ .UseDirectory("snapshots");
+ """));
}
[Test]
@@ -1466,14 +1553,15 @@ public async Task SuffixedLiteralIsNotPatchedThroughItsQuote()
[Test]
public async Task CommentedOutCallIsSkipped()
{
- var source = string.Join(
- "\n",
- "class Tests",
- "{",
- " // await Verify(x).Snapshot(\"doc example\");",
- " async Task Test() =>",
- " await Verify(x).Snapshot();",
- "}");
+ var source = Source(
+ """
+ class Tests
+ {
+ // await Verify(x).Snapshot("doc example");
+ async Task Test() =>
+ await Verify(x).Snapshot();
+ }
+ """);
var status = TryApply(source, 3, InlinePatchMode.Set, null, "new", out var newSource, out _);
@@ -1486,8 +1574,10 @@ public async Task CommentedOutCallIsSkipped()
public async Task CallInsideAStringIsSkipped()
{
var source = Method(
- " var text = \"await Snapshot(\\\"x\\\")\";\n" +
- " await Verify(x).Snapshot();");
+ """
+ var text = "await Snapshot(\"x\")";
+ await Verify(x).Snapshot();
+ """);
var status = TryApply(source, 5, InlinePatchMode.Set, null, "new", out var newSource, out _);
@@ -1500,13 +1590,14 @@ public async Task CallInsideAStringIsSkipped()
[Test]
public async Task SnapshotDeclarationIsNotMistakenForACall()
{
- var source = string.Join(
- "\n",
- "static class Extensions",
- "{",
- " public static Task Snapshot(this Task task, string? expected = null) =>",
- " task;",
- "}");
+ var source = Source(
+ """
+ static class Extensions
+ {
+ public static Task Snapshot(this Task task, string? expected = null) =>
+ task;
+ }
+ """);
var status = TryApply(source, 3, InlinePatchMode.Set, null, "new", out _, out var reason);
@@ -1517,12 +1608,13 @@ public async Task SnapshotDeclarationIsNotMistakenForACall()
[Test]
public async Task AppendSkipsAVerifyPrefixedDeclaration()
{
- var source = string.Join(
- "\n",
- "class Tests",
- "{",
- " Task VerifyThing(string value) => Verify(value);",
- "}");
+ var source = Source(
+ """
+ class Tests
+ {
+ Task VerifyThing(string value) => Verify(value);
+ }
+ """);
var status = TryApply(source, 3, InlinePatchMode.Append, null, "new", out var newSource, out _);
@@ -1537,8 +1629,10 @@ await Assert.That(newSource).Contains(
public async Task AppendGoesAfterACommentInTheChain()
{
var source = Method(
- " await Verify(value) // note\n" +
- " .UseDirectory(\"snapshots\");");
+ """
+ await Verify(value) // note
+ .UseDirectory("snapshots");
+ """);
var status = TryApply(source, 5, InlinePatchMode.Append, null, "new", out var newSource, out _);
@@ -1552,8 +1646,10 @@ await Assert.That(newSource).Contains(
public async Task LiteralInACommentIsNotPatched()
{
var source = Method(
- " // was \"old\"\n" +
- " await Verify(x).Snapshot(\"old\");");
+ """
+ // was "old"
+ await Verify(x).Snapshot("old");
+ """);
var status = TryApply(source, 5, InlinePatchMode.Set, "\"old\"", "new", out var newSource, out _);
@@ -1565,15 +1661,16 @@ public async Task LiteralInACommentIsNotPatched()
[Test]
public async Task LiteralInAnotherMethodIsNotPatched()
{
- var source = string.Join(
- "\n",
- "class Tests",
- "{",
- " void Helper() => Log(\"old\");",
- "",
- " async Task Test() =>",
- " await Verify(x).Snapshot(\"old\");",
- "}");
+ var source = Source(
+ """
+ class Tests
+ {
+ void Helper() => Log("old");
+
+ async Task Test() =>
+ await Verify(x).Snapshot("old");
+ }
+ """);
var status = TryApply(source, 3, InlinePatchMode.Set, "\"old\"", "new", out var newSource, out _);
@@ -1655,17 +1752,21 @@ public async Task CommentAfterTheArgumentIsKept()
public async Task RemoveLeavesALineCommentAboveIntact()
{
var source = Method(
- " await Verify(value)\n" +
- " // note\n" +
- " .Snapshot(\"old\");");
+ """
+ await Verify(value)
+ // note
+ .Snapshot("old");
+ """);
var status = TryApply(source, 7, InlinePatchMode.Remove, null, "", out var newSource, out _);
await Assert.That(status).IsEqualTo(PatchStatus.Applied);
await Assert.That(newSource).IsEqualTo(
Method(
- " await Verify(value)\n" +
- " // note\n" +
- " ;"));
+ """
+ await Verify(value)
+ // note
+ ;
+ """));
}
}
diff --git a/src/DiffEngine/Inline/InlinePatcher.cs b/src/DiffEngine/Inline/InlinePatcher.cs
index 7034122c..ef11f3a1 100644
--- a/src/DiffEngine/Inline/InlinePatcher.cs
+++ b/src/DiffEngine/Inline/InlinePatcher.cs
@@ -689,13 +689,13 @@ static bool TryFindCall(
var floor = memberLine is null ? 1 : Clamp(memberLine.Value, lineCount);
var origin = memberLine is null ? lineHint : floor;
// The recorded line is tried first so that two snapshots in one member stay apart. It is
- // only evidence about this member while it is still inside it, though, and a declaration
- // between the two says it is not: the hint went stale, something above it moved, and it
- // now points into the test next door. Trying it anyway rewrote that test's snapshot and
- // left this one alone, which is the failure the member name exists to prevent
+ // only evidence about this member while it is still inside it, though, and another member
+ // declared between the two says it is not: the hint went stale, something above it moved,
+ // and it now points into the test next door. Trying it anyway rewrote that test's snapshot
+ // and left this one alone, which is the failure the member name exists to prevent
if (lineHint >= floor &&
(memberLine is null ||
- !DeclarationBetween(source, scan, lineStarts, floor, lineHint)))
+ !MemberDeclaredBetween(source, scan, lineStarts, floor, lineHint)))
{
foreach (var call in CallsOnLine(source, scan, lineStarts, lineHint, names, byPrefix))
{
@@ -734,11 +734,21 @@ static bool TryFindCall(
}
///
- /// Whether a declaration sits after and at or before
- /// . Cheap because it only ever runs over the span between a
- /// member's declaration and the recorded line.
+ /// Whether another member is declared after and at or before
+ /// , which is what says the recorded line has left the member it was
+ /// recorded in. Cheap because it only ever runs over the span between a member's declaration
+ /// and the recorded line.
+ ///
+ /// Indentation is what tells a member from a local, because nothing in front of the name does:
+ /// var hash = Hash() and F#'s let hash = hash () are declarations to
+ /// exactly as a sibling test method is. A declaration
+ /// indented past the member's own sits inside its body - a local, a local function, a nested
+ /// type - and none of those put the recorded line in another member. Counting them did, which
+ /// made the hint unreachable for the ordinary shape of a test: a local, then a verify call on
+ /// it. A sibling shares the member's own indentation, so the comparison is inclusive.
+ ///
///
- static bool DeclarationBetween(string source, SourceScan scan, List lineStarts, int afterLine, int uptoLine)
+ static bool MemberDeclaredBetween(string source, SourceScan scan, List lineStarts, int afterLine, int uptoLine)
{
if (uptoLine <= afterLine ||
afterLine >= lineStarts.Count)
@@ -746,6 +756,7 @@ static bool DeclarationBetween(string source, SourceScan scan, List lineSta
return false;
}
+ var memberIndent = LeadingWhitespace(source, lineStarts, lineStarts[afterLine - 1]).Length;
var start = lineStarts[afterLine];
var end = uptoLine < lineStarts.Count ? lineStarts[uptoLine] : source.Length;
for (var index = start; index < end; index++)
@@ -757,7 +768,8 @@ static bool DeclarationBetween(string source, SourceScan scan, List lineSta
continue;
}
- if (scan.IsDeclaration(index))
+ if (scan.IsDeclaration(index) &&
+ LeadingWhitespace(source, lineStarts, index).Length <= memberIndent)
{
return true;
}
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index b6fb339d..2e727e82 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -1,7 +1,7 @@
- 20.0.0-beta.33
+ 20.0.0-beta.34
1.0.0
Testing, Snapshot, Diff, Compare
Launches diff tools based on file extensions. Designed to be consumed by snapshot testing libraries.