Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/guids.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ public Task NamedGuidFluent()
.AddNamedGuid(guid, "instanceNamed");
}
```
<sup><a href='/src/Verify.Tests/GuidScrubberTests.cs#L104-L118' title='Snippet source file'>snippet source</a> | <a href='#snippet-NamedGuidFluent' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.Tests/GuidScrubberTests.cs#L121-L135' title='Snippet source file'>snippet source</a> | <a href='#snippet-NamedGuidFluent' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


Expand Down Expand Up @@ -217,7 +217,7 @@ public Task InferredNamedGuidFluent()
.AddNamedGuid(namedGuid);
}
```
<sup><a href='/src/Verify.Tests/GuidScrubberTests.cs#L120-L134' title='Snippet source file'>snippet source</a> | <a href='#snippet-InferredNamedGuidFluent' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.Tests/GuidScrubberTests.cs#L137-L151' title='Snippet source file'>snippet source</a> | <a href='#snippet-InferredNamedGuidFluent' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Result:
Expand Down
6 changes: 5 additions & 1 deletion src/Verify.MSTest.SourceGenerator/UsesVerifyGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
static bool HasTestClassAttribute(INamedTypeSymbol symbol, INamedTypeSymbol testClassType) =>
!symbol.HasAttributeOfType(testClassType, includeDerived: true);

// Require at least one attribute list: both paths only care about classes
// carrying [UsesVerify] or [TestClass]. Without this filter the uncached
// CreateSyntaxProvider transform runs semantic work for every class in the
// consuming project on every keystroke.
static bool IsSyntaxEligibleForGeneration(SyntaxNode node, Cancel _) =>
node is ClassDeclarationSyntax;
node is ClassDeclarationSyntax { AttributeLists.Count: > 0 };

static bool IsAssemblyEligibleForGeneration(IAssemblySymbol assembly, INamedTypeSymbol markerType) =>
assembly.HasAttributeOfType(markerType, includeDerived: false);
Expand Down
7 changes: 6 additions & 1 deletion src/Verify.MSTest/TestExecutionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ namespace VerifyMSTest;
public record TestExecutionContext(TestContext TestContext, Type TestClass)
{
public Assembly Assembly { get; } = TestClass.Assembly;
public MethodInfo Method { get; } = FindMethod(TestClass, TestContext);

MethodInfo? method;

// Resolved lazily: the method scan is only needed when a Verify call actually
// builds a verifier, not for every test that constructs a context.
public MethodInfo Method => method ??= FindMethod(TestClass, TestContext);

static MethodInfo FindMethod(Type type, TestContext context)
{
Expand Down
7 changes: 6 additions & 1 deletion src/Verify.MSTest/Verifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ public static InnerVerifier BuildVerifier(VerifySettings settings, string source
if (!settings.HasParameters)
{
var data = context.TestContext.TestData;
if (data != null)
// Only apply when the data length matches the method parameter count.
// A params-array DataRow exposes raw pre-binding data whose length does
// not match the parameter count, which would break parameterized
// snapshot file naming. XunitV3 applies the same guard.
if (data != null &&
data.Length == method.ParameterNames()?.Count)
{
settings.SetParameters(data);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Verify.MSTest/Verifier_Archive.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public static SettingsTask VerifyZip(
bool persistArchive = false,
string? archiveExtension = null,
[CallerFilePath] string sourceFile = "") =>
Verify(settings, sourceFile, _ => _.VerifyZip(stream, include, info, fileScrubber, includeStructure, persistArchive), true);
Verify(settings, sourceFile, _ => _.VerifyZip(stream, include, info, fileScrubber, includeStructure, persistArchive, archiveExtension), true);

/// <summary>
/// Verifies the contents of a <see cref="ZipArchive" />
Expand Down
4 changes: 2 additions & 2 deletions src/Verify.NUnit/VerifyBase_Stream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public SettingsTask Verify<T>(
VerifySettings? settings = null,
object? info = null)
where T : Stream =>
Verifier.Verify(target, extension, settings, info, sourceFile);
Verifier.Verify(target, extension, settings ?? this.settings, info, sourceFile);

[Pure]
public SettingsTask Verify<T>(
Expand All @@ -49,7 +49,7 @@ public SettingsTask Verify<T>(
VerifySettings? settings = null,
object? info = null)
where T : Stream =>
Verifier.Verify(target, extension, settings, info, sourceFile);
Verifier.Verify(target, extension, settings ?? this.settings, info, sourceFile);

[Pure]
public SettingsTask Verify(
Expand Down
2 changes: 1 addition & 1 deletion src/Verify.NUnit/VerifyBase_Tuple.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ public partial class VerifyBase
public SettingsTask VerifyTuple(
Expression<Func<ITuple>> target,
VerifySettings? settings = null) =>
Verifier.VerifyTuple(target, settings ?? this.settings);
Verifier.VerifyTuple(target, settings ?? this.settings, sourceFile);
}
#endif
7 changes: 5 additions & 2 deletions src/Verify.Tests/DateFormatLengthCalculatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@
[InlineData("zz", 3, 3)]
[InlineData("zzz", 6, 6)]
[InlineData("zzzz", 6, 6)]
[InlineData("K", 6, 6)]
[InlineData("KK", 12, 12)]
// K renders as "" (Unspecified), "Z" (Utc, 1 char) or "+11:00" (offset, 6 chars),
// so its minimum contribution is 0 (not 6) — otherwise round-trip/"o" formats
// scrub only the offset form and leak the Z / offset-less forms.
[InlineData("K", 6, 0)]
[InlineData("KK", 12, 0)]
[InlineData(":", 1, 1)]
[InlineData("':'", 1, 1)]
[InlineData("/", 1, 1)]
Expand Down
17 changes: 17 additions & 0 deletions src/Verify.Tests/GuidScrubberTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,23 @@ public void MultipleChunks()
Assert.Equal("[Guid_1][Guid_2]", builder.ToString());
}

[Fact]
public void GuidSpanningThreeChunks()
{
// Capacity 4 forces small chunks [4][4][rest]; the 36-char guid spans all
// three, and the 4-char middle chunk is shorter than the 35-char carryover.
// The carryover must accumulate across chunks or the prefix is dropped and
// the guid is never found (silent leak).
var guid = "173535ae-995b-4cc6-a74e-8cd4be57039c";
var builder = new StringBuilder(capacity: 4);
builder.Append(guid[..4]); // chunk0
builder.Append(guid[4..8]); // chunk1 (short middle chunk)
builder.Append(guid[8..]); // chunk2
using var counter = Counter.Start();
GuidScrubber.ReplaceGuids(builder, counter);
Assert.Equal("Guid_1", builder.ToString());
}

#region NamedGuidFluent

[Fact]
Expand Down
23 changes: 23 additions & 0 deletions src/Verify.Tests/LinesScrubberTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,29 @@ public Task ScrubLinesContaining_case_sensitive()
""");
}

[Fact]
public void ReplaceLines_AllRemovedNoTrailingNewline()
{
// Single line, no trailing newline, and every line replaced with null.
// The trailing-newline trim must guard on the rebuilt builder length, not
// the original string length, otherwise Length -= 1 underflows.
var builder = new StringBuilder("single line");

builder.ReplaceLines(_ => null);

Assert.Equal(string.Empty, builder.ToString());
}

[Fact]
public void ReplaceLines_ReplacesLines()
{
var builder = new StringBuilder("a\nb\nc");

builder.ReplaceLines(line => line == "b" ? "B" : line);

Assert.Equal("a\nB\nc", builder.ToString());
}

[Fact]
public void FilterLines_RemovesSingleLine()
{
Expand Down
16 changes: 16 additions & 0 deletions src/Verify.Tests/Serialization/DirectoryReplacementTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,22 @@ public void MultipleChunks()
Assert.Equal("{Child} {Parent} ", builder.ToString());
}

[Fact]
public void PathSpanningThreeChunks()
{
// Capacity 4 forces small chunks [4][4][rest]; the 16-char path spans all
// three, and the 4-char middle chunk is shorter than the carryover. The
// carryover must accumulate across chunks or the prefix is dropped and the
// path leaks unscrubbed.
List<DirectoryReplacements.Pair> pairs = [new("C:/Parent/ChildX", "{replace}")];
var builder = new StringBuilder(capacity: 4);
builder.Append("C:/P"); // chunk0
builder.Append("aren"); // chunk1 (short middle chunk)
builder.Append("t/ChildX.");
DirectoryReplacements.Replace(builder, pairs);
Assert.Equal("{replace}.", builder.ToString());
}

[Fact]
public void ProcessLongerDirectoryFirst()
{
Expand Down
38 changes: 38 additions & 0 deletions src/Verify.Tests/UserMachineScrubberChunkTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
public class UserMachineScrubberChunkTests
{
[Fact]
public void CrossChunkMatchEndingExactlyAtChunkBoundary()
{
// "ABCD" fills the capacity-4 chunk; the next append lands in a fresh
// 16-char chunk holding the remaining 16 chars of the 20-char match, so
// the match ends exactly at that chunk's boundary with a chunk after it.
// The trailing-char check must not read chunkSpan[chunkSpan.Length].
var builder = new StringBuilder(capacity: 4);
builder.Append("ABCD");
builder.Append("EFGHIJKLMNOPQRST");
builder.Append('.');

UserMachineScrubber.PerformReplacements(builder, "ABCDEFGHIJKLMNOPQRST", "TheUserName");

Assert.Equal("TheUserName.", builder.ToString());
}

[Fact]
public void TokenSpanningThreeChunks()
{
// Capacity 4 forces small chunks [4][4][…]. The 10-char match spans all
// three, and the 4-char middle chunk is shorter than the match. The
// carryover must accumulate across chunks; otherwise the first chunk's
// prefix is dropped when the short middle chunk overwrites it, and the
// match is never found (silent leak).
var find = "ABCDEFGHIJ"; // 10 chars
var builder = new StringBuilder(capacity: 4);
builder.Append("ABCD"); // chunk0 = find[0..4]
builder.Append("EFGH"); // chunk1 = find[4..8] (short middle chunk)
builder.Append("IJ."); // chunk2 = find[8..10] + wrapper

UserMachineScrubber.PerformReplacements(builder, find, "TheUserName");

Assert.Equal("TheUserName.", builder.ToString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,10 @@ public static (int max, int min) InnerGetLength(scoped CharSpan format, Culture

break;
case 'K':
// K renders as "" (Unspecified), "Z" (Utc, 1 char) or
// "+11:00" (offset, 6 chars), so it can contribute as few as
// 0 chars. Only the maximum is 6; the minimum stays 0.
tokenLen = 1;
minLength += 6;
maxLength += 6;
break;
case ':':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,22 @@ static List<Match> FindMatches(StringBuilder builder, List<Pair> pairs)
}
}

// Save last N chars for next iteration
carryoverLength = Math.Min(carryoverSize, chunk.Length);
chunkSpan.Slice(chunk.Length - carryoverLength, carryoverLength).CopyTo(carryoverBuffer);
// Roll the carryover forward: keep the last carryoverSize chars of
// everything seen so far. Rebuilding it from the current chunk alone
// drops the prefix when a chunk is shorter than carryoverSize, so a
// path spanning three or more chunks would never be found.
if (chunk.Length >= carryoverSize)
{
chunkSpan.Slice(chunk.Length - carryoverSize, carryoverSize).CopyTo(carryoverBuffer);
carryoverLength = carryoverSize;
}
else
{
var keep = Math.Min(carryoverLength, carryoverSize - chunk.Length);
carryoverBuffer.Slice(carryoverLength - keep, keep).CopyTo(carryoverBuffer);
chunkSpan.CopyTo(carryoverBuffer[keep..]);
carryoverLength = keep + chunk.Length;
}

previousChunkAbsoluteEnd = absolutePosition + chunk.Length;
absolutePosition += chunk.Length;
Expand Down
19 changes: 16 additions & 3 deletions src/Verify/Serialization/Scrubbers/GuidScrubber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,22 @@ static List<Match> FindMatches(StringBuilder builder, Counter counter)
}
}

// Save last 35 chars for next iteration
carryoverLength = Math.Min(35, chunk.Length);
chunkSpan.Slice(chunk.Length - carryoverLength, carryoverLength).CopyTo(carryoverBuffer);
// Roll the carryover forward: keep the last 35 chars of everything seen
// so far. Rebuilding it from the current chunk alone drops the prefix
// when a chunk is shorter than 35, so a guid spanning three or more
// chunks would never be found.
if (chunk.Length >= 35)
{
chunkSpan.Slice(chunk.Length - 35, 35).CopyTo(carryoverBuffer);
carryoverLength = 35;
}
else
{
var keep = Math.Min(carryoverLength, 35 - chunk.Length);
carryoverBuffer.Slice(carryoverLength - keep, keep).CopyTo(carryoverBuffer);
chunkSpan.CopyTo(carryoverBuffer[keep..]);
carryoverLength = keep + chunk.Length;
}

previousChunkAbsoluteEnd = absolutePosition + chunk.Length;
absolutePosition += chunk.Length;
Expand Down
2 changes: 1 addition & 1 deletion src/Verify/Serialization/Scrubbers/LinesScrubber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public static void ReplaceLines(this StringBuilder input, Func<string, string?>
}
}

if (theString.Length > 0 &&
if (input.Length > 0 &&
!theString.EndsWith('\n'))
{
input.Length -= 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,13 @@ static List<int> FindMatches(StringBuilder builder, string find)
continue;
}

// Check trailing character
// Check trailing character. Use the builder indexer rather
// than chunkSpan[neededFromCurrent], which is out of range when
// the match ends exactly at this chunk's boundary, so the check
// still works when there is a following chunk.
var endPosition = startPosition + find.Length;
var validEnd = endPosition >= builder.Length ||
IsValidWrapper(chunkSpan[neededFromCurrent]);
IsValidWrapper(builder[endPosition]);

if (!validEnd)
{
Expand Down Expand Up @@ -115,9 +118,22 @@ static List<int> FindMatches(StringBuilder builder, string find)
}
}

// Save last N chars for next iteration
carryoverLength = Math.Min(carryoverSize, chunk.Length);
chunkSpan.Slice(chunk.Length - carryoverLength, carryoverLength).CopyTo(carryoverBuffer);
// Roll the carryover forward: keep the last carryoverSize chars of
// everything seen so far. Rebuilding it from the current chunk alone
// drops the prefix when a chunk is shorter than the search string, so
// a token spanning three or more chunks would never be found.
if (chunk.Length >= carryoverSize)
{
chunkSpan.Slice(chunk.Length - carryoverSize, carryoverSize).CopyTo(carryoverBuffer);
carryoverLength = carryoverSize;
}
else
{
var keep = Math.Min(carryoverLength, carryoverSize - chunk.Length);
carryoverBuffer.Slice(carryoverLength - keep, keep).CopyTo(carryoverBuffer);
chunkSpan.CopyTo(carryoverBuffer[keep..]);
carryoverLength = keep + chunk.Length;
}

previousChunkAbsoluteEnd = absolutePosition + chunk.Length;
absolutePosition += chunk.Length;
Expand Down
Loading