From caeaeed6a6e4c4c7ba5b625ad949e19e368a0e64 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sun, 5 Jul 2026 18:00:23 +1000 Subject: [PATCH 1/2] Fix correctness and perf issues in compare, recording, naming and checks - StreamComparer: compare both streams' read counts and return NotEqual on a length difference, closing a silent false-pass for non-seekable/empty received streams - Callbacks: await every registered OnFirstVerify/OnDelete/OnVerifyMismatch handler via GetInvocationList instead of only the last (static and instance) - Recording.TryStop: snapshot the items then Clear and Pause the shared State, so a stop consumed inside the engine's async flow is observable to the test (no re-appending on a second verify, IsRecording reflects it) - Recording: Disposable/NamedDisposable use TryPause so disposing after an explicit Stop is a no-op instead of throwing - Recording.IgnoreNames: copy-on-write HashSet read via Volatile.Read, so IsIgnored on Recording.Add cannot race a concurrent mutation - MatchingFileFinder: account for a trailing separator on the directory so UseDirectory("snapshots/") still finds verified files and cleans received - InnerVerifyChecks: look for .gitignore (was .gitIgnore), so the check runs on case-sensitive file systems; message updated to match - Naming: pass pathFriendly through to collection items so pathFriendly:false callers keep path chars raw in snapshot content - DanglingSnapshotsCheck: materialize tracked files into HashSets once instead of LINQ Contains over a ConcurrentBag per file (O(files*tracked) -> O(files)) - Add tests for the above; update the two VerifyChecksTests snapshots for the .gitignore casing --- src/Verify.Tests/CallbackTests.cs | 29 +++++++++++ .../VerifyChecksTests.GitIgnore.verified.txt | 4 +- .../VerifyChecksTests.Invalid.verified.txt | 4 +- .../Naming/MatchingFileFinderTests.cs | 51 +++++++++++++++++++ .../Naming/NameForParameterTests.cs | 15 ++++++ src/Verify.Tests/RecordingTests.cs | 31 +++++++++++ src/Verify.Tests/StreamComparerTests.cs | 28 ++++++++++ src/Verify/Callbacks/VerifierSettings.cs | 30 +++++++---- src/Verify/Callbacks/VerifySettings.cs | 16 ++++-- src/Verify/Compare/StreamComparer.cs | 17 ++++--- .../ConventionCheck/DanglingSnapshotsCheck.cs | 10 +++- .../ConventionCheck/InnerVerifyChecks.cs | 6 +-- src/Verify/Naming/MatchingFileFinder.cs | 12 ++++- src/Verify/Naming/VerifierSettings.cs | 2 +- src/Verify/Recording/Recording.cs | 37 +++++++++++--- src/Verify/Recording/Recording_Named.cs | 4 +- 16 files changed, 260 insertions(+), 36 deletions(-) create mode 100644 src/Verify.Tests/CallbackTests.cs create mode 100644 src/Verify.Tests/Naming/MatchingFileFinderTests.cs diff --git a/src/Verify.Tests/CallbackTests.cs b/src/Verify.Tests/CallbackTests.cs new file mode 100644 index 0000000000..ab132c4110 --- /dev/null +++ b/src/Verify.Tests/CallbackTests.cs @@ -0,0 +1,29 @@ +public class CallbackTests +{ + [Fact] + public async Task OnFirstVerifyAwaitsAllHandlers() + { + var settings = new VerifySettings(); + var ran = new List(); + // The slower handler is registered first: if only the last handler is + // awaited (the bug), it will not have completed by the assert. + settings.OnFirstVerify( + async (_, _, _) => + { + await Task.Delay(200); + ran.Add(1); + }); + settings.OnFirstVerify( + async (_, _, _) => + { + await Task.Delay(1); + ran.Add(2); + }); + + var filePair = new FilePair("txt", "received.txt", "verified.txt"); + var result = new NewResult(filePair, null); + await settings.RunOnFirstVerify(result, false); + + Assert.Equal(2, ran.Count); + } +} diff --git a/src/Verify.Tests/InnerVerifyChecksTests/VerifyChecksTests.GitIgnore.verified.txt b/src/Verify.Tests/InnerVerifyChecksTests/VerifyChecksTests.GitIgnore.verified.txt index ebb01c0e49..84bb9d1fe4 100644 --- a/src/Verify.Tests/InnerVerifyChecksTests/VerifyChecksTests.GitIgnore.verified.txt +++ b/src/Verify.Tests/InnerVerifyChecksTests/VerifyChecksTests.GitIgnore.verified.txt @@ -1,8 +1,8 @@ { Type: VerifyCheckException, Message: -Expected .gitIgnore to contain settings for Verify. -Path: file:///{ProjectDirectory}InnerVerifyChecksTests/Invalid/.gitIgnore +Expected .gitignore to contain settings for Verify. +Path: file:///{ProjectDirectory}InnerVerifyChecksTests/Invalid/.gitignore Recommended settings: # Verify diff --git a/src/Verify.Tests/InnerVerifyChecksTests/VerifyChecksTests.Invalid.verified.txt b/src/Verify.Tests/InnerVerifyChecksTests/VerifyChecksTests.Invalid.verified.txt index ebb01c0e49..84bb9d1fe4 100644 --- a/src/Verify.Tests/InnerVerifyChecksTests/VerifyChecksTests.Invalid.verified.txt +++ b/src/Verify.Tests/InnerVerifyChecksTests/VerifyChecksTests.Invalid.verified.txt @@ -1,8 +1,8 @@ { Type: VerifyCheckException, Message: -Expected .gitIgnore to contain settings for Verify. -Path: file:///{ProjectDirectory}InnerVerifyChecksTests/Invalid/.gitIgnore +Expected .gitignore to contain settings for Verify. +Path: file:///{ProjectDirectory}InnerVerifyChecksTests/Invalid/.gitignore Recommended settings: # Verify diff --git a/src/Verify.Tests/Naming/MatchingFileFinderTests.cs b/src/Verify.Tests/Naming/MatchingFileFinderTests.cs new file mode 100644 index 0000000000..16a4eddf20 --- /dev/null +++ b/src/Verify.Tests/Naming/MatchingFileFinderTests.cs @@ -0,0 +1,51 @@ +public class MatchingFileFinderTests +{ + [Fact] + public void FindVerifiedWithTrailingSeparator() + { + var directory = CreateTempDirectory(); + try + { + File.WriteAllText(Path.Combine(directory, "SomeTest.verified.txt"), "a"); + File.WriteAllText(Path.Combine(directory, "Other.verified.txt"), "b"); + + var withSeparator = directory + Path.DirectorySeparatorChar; + var found = MatchingFileFinder.FindVerified("SomeTest", withSeparator) + .ToList(); + + Assert.Single(found); + Assert.EndsWith("SomeTest.verified.txt", found[0]); + } + finally + { + Directory.Delete(directory, true); + } + } + + [Fact] + public void FindVerifiedWithoutTrailingSeparator() + { + var directory = CreateTempDirectory(); + try + { + File.WriteAllText(Path.Combine(directory, "SomeTest.verified.txt"), "a"); + File.WriteAllText(Path.Combine(directory, "Other.verified.txt"), "b"); + + var found = MatchingFileFinder.FindVerified("SomeTest", directory) + .ToList(); + + Assert.Single(found); + } + finally + { + Directory.Delete(directory, true); + } + } + + static string CreateTempDirectory() + { + var directory = Path.Combine(Path.GetTempPath(), $"MatchingFileFinder{Guid.NewGuid():N}"); + Directory.CreateDirectory(directory); + return directory; + } +} diff --git a/src/Verify.Tests/Naming/NameForParameterTests.cs b/src/Verify.Tests/Naming/NameForParameterTests.cs index 973f201b17..d006f67f77 100644 --- a/src/Verify.Tests/Naming/NameForParameterTests.cs +++ b/src/Verify.Tests/Naming/NameForParameterTests.cs @@ -12,6 +12,21 @@ public Task StringEmpty() => public Task StringInvalidPathChar() => Verify(VerifierSettings.GetNameForParameter("a/a", counter: CounterBuilder.Empty())); + [Fact] + public void CollectionItemPathFriendlyFalseNotCleaned() + { + // pathFriendly must flow to collection items, so path chars are kept + // raw when the caller does not want file-name cleaning applied. + var name = VerifierSettings.GetNameForParameter( + new List + { + "a/b" + }, + counter: CounterBuilder.Empty(), + pathFriendly: false); + Assert.Contains("a/b", name); + } + [Fact] public Task Int() => Verify(VerifierSettings.GetNameForParameter(10, counter: CounterBuilder.Empty())); diff --git a/src/Verify.Tests/RecordingTests.cs b/src/Verify.Tests/RecordingTests.cs index fd4a7c581a..edff2ad480 100644 --- a/src/Verify.Tests/RecordingTests.cs +++ b/src/Verify.Tests/RecordingTests.cs @@ -10,6 +10,37 @@ public Task IgnoreNames() return Verify(); } + [Fact] + public async Task StoppedInChildContextIsNotRecording() + { + Recording.Start(); + Recording.Add("name", "value"); + // Consuming inside a child execution context (as the verify engine does) + // must not leave the recording active in this context. + await Task.Run(() => Recording.TryStop(out _)); + Assert.False(Recording.IsRecording()); + } + + [Fact] + public void DisposeAfterStopDoesNotThrow() + { + using (Recording.Start()) + { + Recording.Add("name", "value"); + Recording.Stop(); + } + } + + [Fact] + public void DisposeNamedAfterStopDoesNotThrow() + { + using (Recording.Start("DisposeNamedAfterStop")) + { + Recording.Add("DisposeNamedAfterStop", "name", "value"); + Recording.Stop("DisposeNamedAfterStop"); + } + } + [Fact] public Task Dates() { diff --git a/src/Verify.Tests/StreamComparerTests.cs b/src/Verify.Tests/StreamComparerTests.cs index 58d66c73ca..5db835b3d7 100644 --- a/src/Verify.Tests/StreamComparerTests.cs +++ b/src/Verify.Tests/StreamComparerTests.cs @@ -35,6 +35,34 @@ public async Task BinaryNotEquals() Assert.False(result.IsEqual); } + [Fact] + public async Task PrefixIsNotEqual() + { + // received is an 8-byte-aligned prefix of verified; must not be Equal. + using var received = new MemoryStream(new byte[16]); + using var verified = new MemoryStream(new byte[32]); + var result = await StreamComparer.AreEqual(received, verified); + Assert.False(result.IsEqual); + } + + [Fact] + public async Task EmptyReceivedIsNotEqual() + { + using var received = new MemoryStream(); + using var verified = new MemoryStream(new byte[16]); + var result = await StreamComparer.AreEqual(received, verified); + Assert.False(result.IsEqual); + } + + [Fact] + public async Task LongerReceivedIsNotEqual() + { + using var received = new MemoryStream(new byte[32]); + using var verified = new MemoryStream(new byte[16]); + var result = await StreamComparer.AreEqual(received, verified); + Assert.False(result.IsEqual); + } + [Fact] public async Task ShouldNotLock() { diff --git a/src/Verify/Callbacks/VerifierSettings.cs b/src/Verify/Callbacks/VerifierSettings.cs index 4b2a07185e..7fd12a72e3 100644 --- a/src/Verify/Callbacks/VerifierSettings.cs +++ b/src/Verify/Callbacks/VerifierSettings.cs @@ -35,14 +35,20 @@ public static void OnFirstVerify(FirstVerify firstVerify) handleOnFirstVerify += firstVerify; } - internal static Task RunOnFirstVerify(NewResult item, bool autoVerify) + internal static async Task RunOnFirstVerify(NewResult item, bool autoVerify) { if (handleOnFirstVerify is null) { - return Task.CompletedTask; + return; } - return handleOnFirstVerify(item.File, item.ReceivedText?.ToString(), autoVerify); + var receivedText = item.ReceivedText?.ToString(); + // Await every registered handler; invoking the multicast delegate + // directly would only await the last handler's Task. + foreach (var handler in handleOnFirstVerify.GetInvocationList()) + { + await ((FirstVerify) handler)(item.File, receivedText, autoVerify); + } } static VerifyDelete? handleOnVerifyDelete; @@ -55,24 +61,30 @@ public static void OnDelete(VerifyDelete verifyDelete) static VerifyMismatch? handleOnVerifyMismatch; - internal static Task RunOnVerifyDelete(string file, bool autoVerify) + internal static async Task RunOnVerifyDelete(string file, bool autoVerify) { if (handleOnVerifyDelete is null) { - return Task.CompletedTask; + return; } - return handleOnVerifyDelete(file, autoVerify); + foreach (var handler in handleOnVerifyDelete.GetInvocationList()) + { + await ((VerifyDelete) handler)(file, autoVerify); + } } - internal static Task RunOnVerifyMismatch(FilePair item, string? message, bool autoVerify) + internal static async Task RunOnVerifyMismatch(FilePair item, string? message, bool autoVerify) { if (handleOnVerifyMismatch is null) { - return Task.CompletedTask; + return; } - return handleOnVerifyMismatch(item, message, autoVerify); + foreach (var handler in handleOnVerifyMismatch.GetInvocationList()) + { + await ((VerifyMismatch) handler)(item, message, autoVerify); + } } public static void OnVerifyMismatch(VerifyMismatch verifyMismatch) diff --git a/src/Verify/Callbacks/VerifySettings.cs b/src/Verify/Callbacks/VerifySettings.cs index 6e36bedb7d..dab8111cd2 100644 --- a/src/Verify/Callbacks/VerifySettings.cs +++ b/src/Verify/Callbacks/VerifySettings.cs @@ -10,7 +10,11 @@ internal async Task RunOnFirstVerify(NewResult item, bool autoVerify) { if (handleOnFirstVerify is not null) { - await handleOnFirstVerify(item.File, item.ReceivedText?.ToString(), autoVerify); + var receivedText = item.ReceivedText?.ToString(); + foreach (var handler in handleOnFirstVerify.GetInvocationList()) + { + await ((FirstVerify) handler)(item.File, receivedText, autoVerify); + } } await VerifierSettings.RunOnFirstVerify(item, autoVerify); @@ -26,7 +30,10 @@ internal async Task RunOnVerifyDelete(string file, bool autoVerify) { if (handleOnVerifyDelete is not null) { - await handleOnVerifyDelete(file, autoVerify); + foreach (var handler in handleOnVerifyDelete.GetInvocationList()) + { + await ((VerifyDelete) handler)(file, autoVerify); + } } await VerifierSettings.RunOnVerifyDelete(file, autoVerify); @@ -38,7 +45,10 @@ internal async Task RunOnVerifyMismatch(FilePair item, string? message, bool aut { if (handleOnVerifyMismatch is not null) { - await handleOnVerifyMismatch(item, message, autoVerify); + foreach (var handler in handleOnVerifyMismatch.GetInvocationList()) + { + await ((VerifyMismatch) handler)(item, message, autoVerify); + } } await VerifierSettings.RunOnVerifyMismatch(item, message, autoVerify); diff --git a/src/Verify/Compare/StreamComparer.cs b/src/Verify/Compare/StreamComparer.cs index ee8741e7ad..832a1df893 100644 --- a/src/Verify/Compare/StreamComparer.cs +++ b/src/Verify/Compare/StreamComparer.cs @@ -14,18 +14,23 @@ public static async Task AreEqual(Stream stream1, Stream stream2) while (true) { - var count = await ReadBufferAsync(stream1, buffer1); + var count1 = await ReadBufferAsync(stream1, buffer1); + var count2 = await ReadBufferAsync(stream2, buffer2); - //no need to compare size here since only enter on files being same size + // Callers do not always guarantee the streams are the same length + // (e.g. a non-seekable received stream), so a length difference must + // be treated as not-equal instead of a short-circuit to equal. + if (count1 != count2) + { + return CompareResult.NotEqual(); + } - if (count == 0) + if (count1 == 0) { return CompareResult.Equal; } - await ReadBufferAsync(stream2, buffer2); - - for (var i = 0; i < count; i += sizeof(long)) + for (var i = 0; i < count1; i += sizeof(long)) { if (BitConverter.ToInt64(buffer1, i) != BitConverter.ToInt64(buffer2, i)) { diff --git a/src/Verify/ConventionCheck/DanglingSnapshotsCheck.cs b/src/Verify/ConventionCheck/DanglingSnapshotsCheck.cs index 7c5aa6164f..d2d1d2bba9 100644 --- a/src/Verify/ConventionCheck/DanglingSnapshotsCheck.cs +++ b/src/Verify/ConventionCheck/DanglingSnapshotsCheck.cs @@ -41,11 +41,17 @@ static void AppendItems(StringBuilder builder, List list, string title) } } + // Materialize the tracked files into sets once. Enumerating a ConcurrentBag + // (via LINQ Contains) per file on disk snapshots the whole bag each time, + // making the check O(files * tracked). + var tracked = new HashSet(trackedFiles); + var trackedIgnoreCase = new HashSet(trackedFiles, StringComparer.OrdinalIgnoreCase); + List untracked = []; List incorrectCase = []; foreach (var file in filesOnDisk) { - if (trackedFiles.Contains(file)) + if (tracked.Contains(file)) { continue; } @@ -57,7 +63,7 @@ static void AppendItems(StringBuilder builder, List list, string title) continue; } - if (trackedFiles.Contains(file, StringComparer.OrdinalIgnoreCase)) + if (trackedIgnoreCase.Contains(file)) { incorrectCase.Add(suffix); } diff --git a/src/Verify/ConventionCheck/InnerVerifyChecks.cs b/src/Verify/ConventionCheck/InnerVerifyChecks.cs index e4cc2f1767..945afdcce8 100644 --- a/src/Verify/ConventionCheck/InnerVerifyChecks.cs +++ b/src/Verify/ConventionCheck/InnerVerifyChecks.cs @@ -338,10 +338,10 @@ static string GetPath(string path) => internal static async Task CheckGitIgnore(string solutionDirectory) { - var path = Path.Combine(solutionDirectory, ".gitIgnore"); + var path = Path.Combine(solutionDirectory, ".gitignore"); if (!File.Exists(path)) { - path = Path.Combine(solutionDirectory, "../.gitIgnore"); + path = Path.Combine(solutionDirectory, "../.gitignore"); } if (!File.Exists(path)) @@ -359,7 +359,7 @@ internal static async Task CheckGitIgnore(string solutionDirectory) throw new VerifyCheckException( $""" - Expected .gitIgnore to contain settings for Verify. + Expected .gitignore to contain settings for Verify. Path: {GetPath(path)} Recommended settings: diff --git a/src/Verify/Naming/MatchingFileFinder.cs b/src/Verify/Naming/MatchingFileFinder.cs index 07cdbbc7a0..f7a1450be0 100644 --- a/src/Verify/Naming/MatchingFileFinder.cs +++ b/src/Verify/Naming/MatchingFileFinder.cs @@ -21,7 +21,17 @@ public static IEnumerable FindVerified(string fileNamePrefix, string dir static List Find(string directory, string searchPattern, string nonIndexedPattern, string indexedPattern) { - var startIndex = directory.Length + 1; + // Directory.EnumerateFiles inserts a separator only when the directory + // does not already end with one, so the file-name offset depends on + // whether the directory has a trailing separator (e.g. UseDirectory("snapshots/")). + var startIndex = directory.Length; + if (directory.Length > 0 && + directory[^1] != Path.DirectorySeparatorChar && + directory[^1] != Path.AltDirectorySeparatorChar) + { + startIndex++; + } + var list = new List(); var nonIndexedPatternSpan = nonIndexedPattern.AsSpan(); var indexedPatternSpan = indexedPattern.AsSpan(); diff --git a/src/Verify/Naming/VerifierSettings.cs b/src/Verify/Naming/VerifierSettings.cs index 0abe0351e2..2bfa08659f 100644 --- a/src/Verify/Naming/VerifierSettings.cs +++ b/src/Verify/Naming/VerifierSettings.cs @@ -139,7 +139,7 @@ public static void AppendParameter(object? parameter, StringBuilder builder, boo foreach (var item in enumerable) { - AppendParameter(item, builder, false, counter); + AppendParameter(item, builder, false, counter, pathFriendly); builder.Append(','); } diff --git a/src/Verify/Recording/Recording.cs b/src/Verify/Recording/Recording.cs index 3e95f62661..f24828ca48 100644 --- a/src/Verify/Recording/Recording.cs +++ b/src/Verify/Recording/Recording.cs @@ -2,13 +2,28 @@ public static partial class Recording { - static List ignored = []; + static HashSet ignored = []; + static readonly object ignoredLock = new(); - public static void IgnoreNames(params string[] names) => - ignored.AddRange(names); + public static void IgnoreNames(params string[] names) + { + // Copy-on-write: IsIgnored reads this on every Recording.Add, potentially + // from other threads, so publish a new set rather than mutating in place. + lock (ignoredLock) + { + var copy = new HashSet(ignored); + foreach (var name in names) + { + copy.Add(name); + } + + ignored = copy; + } + } public static bool IsIgnored(string name) => - ignored.Contains(name); + Volatile.Read(ref ignored) + .Contains(name); static AsyncLocal asyncLocal = new(); @@ -57,7 +72,15 @@ public static bool TryStop([NotNullWhen(true)] out IReadOnlyCollection return false; } - recorded = value.Items; + // Nulling asyncLocal only affects the current execution context. When the + // verify engine consumes a recording mid-verify, that null does not flow + // back to the calling test, which would otherwise keep seeing an active, + // unconsumed recording (re-appending the same items on the next verify). + // Snapshot the items, then stop the shared State so the stop is observable + // through the caller's reference. + recorded = value.Items.ToList(); + value.Clear(); + value.Pause(); asyncLocal.Value = null; return true; } @@ -114,8 +137,10 @@ public static IDisposable Start() class Disposable : IDisposable { + // TryPause (not Pause) so disposing after an explicit Stop is a no-op + // rather than throwing from CurrentState. public void Dispose() => - Pause(); + TryPause(); } public static void Pause() => diff --git a/src/Verify/Recording/Recording_Named.cs b/src/Verify/Recording/Recording_Named.cs index 00c6232ce9..86bb07f10c 100644 --- a/src/Verify/Recording/Recording_Named.cs +++ b/src/Verify/Recording/Recording_Named.cs @@ -75,8 +75,10 @@ public static IDisposable Start(string identifier) class NamedDisposable(string identifier) : IDisposable { + // TryPause (not Pause) so disposing after an explicit Stop is a no-op + // rather than throwing from CurrentStateNamed. public void Dispose() => - Pause(identifier); + TryPause(identifier); } public static void Pause(string identifier) => From 7d24d339b3fc5cfb718a5b7894245e72289e5be8 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 5 Jul 2026 08:01:14 +0000 Subject: [PATCH 2/2] Docs changes --- docs/comparer.md | 19 ++++++++++++------- docs/recording.md | 22 +++++++++++----------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/docs/comparer.md b/docs/comparer.md index 22f93e6e7c..a5cbf2ab0a 100644 --- a/docs/comparer.md +++ b/docs/comparer.md @@ -124,18 +124,23 @@ public static async Task AreEqual(Stream stream1, Stream stream2) while (true) { - var count = await ReadBufferAsync(stream1, buffer1); + var count1 = await ReadBufferAsync(stream1, buffer1); + var count2 = await ReadBufferAsync(stream2, buffer2); - //no need to compare size here since only enter on files being same size + // Callers do not always guarantee the streams are the same length + // (e.g. a non-seekable received stream), so a length difference must + // be treated as not-equal instead of a short-circuit to equal. + if (count1 != count2) + { + return CompareResult.NotEqual(); + } - if (count == 0) + if (count1 == 0) { return CompareResult.Equal; } - await ReadBufferAsync(stream2, buffer2); - - for (var i = 0; i < count; i += sizeof(long)) + for (var i = 0; i < count1; i += sizeof(long)) { if (BitConverter.ToInt64(buffer1, i) != BitConverter.ToInt64(buffer2, i)) { @@ -172,7 +177,7 @@ static async Task ReadBufferAsync(Stream stream, byte[] buffer) return bytesRead; } ``` -snippet source | anchor +snippet source | anchor diff --git a/docs/recording.md b/docs/recording.md index 137857f7ac..998e98a0c3 100644 --- a/docs/recording.md +++ b/docs/recording.md @@ -25,7 +25,7 @@ public Task Usage() return Verify("TheValue"); } ``` -snippet source | anchor +snippet source | anchor Results in: @@ -61,7 +61,7 @@ public Task TryAdd() return Verify("TheValue"); } ``` -snippet source | anchor +snippet source | anchor @@ -85,7 +85,7 @@ public Task RecordingScoped() return Verify(); } ``` -snippet source | anchor +snippet source | anchor Results in: @@ -117,7 +117,7 @@ public Task SameKey() return Verify("TheValue"); } ``` -snippet source | anchor +snippet source | anchor Results in: @@ -156,7 +156,7 @@ public Task Identifier() return Verify(Recording.Stop("identifier")); } ``` -snippet source | anchor +snippet source | anchor Results in: @@ -188,7 +188,7 @@ public Task Case() return Verify("TheValue"); } ``` -snippet source | anchor +snippet source | anchor Results in: @@ -223,7 +223,7 @@ public Task Stop() return Verify(appends.Where(_ => _.Name != "name1")); } ``` -snippet source | anchor +snippet source | anchor Results in: @@ -255,7 +255,7 @@ public Task StopNotInResult() return Verify("other data"); } ``` -snippet source | anchor +snippet source | anchor Results in: @@ -284,7 +284,7 @@ public void IsRecording() Assert.True(Recording.IsRecording()); } ``` -snippet source | anchor +snippet source | anchor This can be helpful if the cost of capturing data, to add to recording, is high. @@ -307,7 +307,7 @@ public Task Clear() return Verify(); } ``` -snippet source | anchor +snippet source | anchor Results in: @@ -343,7 +343,7 @@ public Task PauseResume() return Verify(); } ``` -snippet source | anchor +snippet source | anchor Results in: