Skip to content

Commit 69cda1a

Browse files
committed
Fix chunk-boundary scrubber bugs and test-adapter defects
Scrubbers (Verify core): - UserMachineScrubber: fix IndexOutOfRangeException when a match ends exactly on a chunk boundary with a following chunk (read the trailing char via the builder indexer instead of chunkSpan[neededFromCurrent]) - UserMachineScrubber, GuidScrubber, DirectoryReplacements: roll the carryover forward across chunks so a token spanning three or more chunks (with a middle chunk shorter than the token) is no longer silently left unscrubbed - DateFormatLengthCalculator: 'K' contributes 0 to the minimum length (it can render as "", "Z" or "+11:00"), so round-trip/"o" formats now scrub the Z and offset-less forms instead of only the offset form - LinesScrubber.ReplaceLines: guard the trailing-newline trim on the rebuilt builder length, not the original string length, fixing an ArgumentOutOfRangeException when every line is replaced with null Test-framework adapters: - MSTest Verifier: only apply TestData parameters when their count matches the method parameter count (matches XunitV3; params-array DataRows no longer break parameterized snapshot naming) - MSTest Verifier_Archive: forward the dropped archiveExtension in the VerifyZip(Stream) overload - MSTest TestExecutionContext: resolve Method lazily so the method scan runs only when a verifier is built - MSTest source generator: require an attribute list in the syntax predicate so the uncached CreateSyntaxProvider transform no longer runs semantic work for every class on every keystroke - NUnit VerifyBase_Tuple: pass sourceFile so snapshots resolve to the test's path rather than the package build path - NUnit VerifyBase_Stream: use settings ?? this.settings in the Task<T> and ValueTask<T> stream overloads so instance settings are honored Add repro tests for the scrubber fixes.
1 parent aefcdb1 commit 69cda1a

16 files changed

Lines changed: 177 additions & 22 deletions

src/Verify.MSTest.SourceGenerator/UsesVerifyGenerator.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
118118
static bool HasTestClassAttribute(INamedTypeSymbol symbol, INamedTypeSymbol testClassType) =>
119119
!symbol.HasAttributeOfType(testClassType, includeDerived: true);
120120

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

124128
static bool IsAssemblyEligibleForGeneration(IAssemblySymbol assembly, INamedTypeSymbol markerType) =>
125129
assembly.HasAttributeOfType(markerType, includeDerived: false);

src/Verify.MSTest/TestExecutionContext.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ namespace VerifyMSTest;
33
public record TestExecutionContext(TestContext TestContext, Type TestClass)
44
{
55
public Assembly Assembly { get; } = TestClass.Assembly;
6-
public MethodInfo Method { get; } = FindMethod(TestClass, TestContext);
6+
7+
MethodInfo? method;
8+
9+
// Resolved lazily: the method scan is only needed when a Verify call actually
10+
// builds a verifier, not for every test that constructs a context.
11+
public MethodInfo Method => method ??= FindMethod(TestClass, TestContext);
712

813
static MethodInfo FindMethod(Type type, TestContext context)
914
{

src/Verify.MSTest/Verifier.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ public static InnerVerifier BuildVerifier(VerifySettings settings, string source
3333
if (!settings.HasParameters)
3434
{
3535
var data = context.TestContext.TestData;
36-
if (data != null)
36+
// Only apply when the data length matches the method parameter count.
37+
// A params-array DataRow exposes raw pre-binding data whose length does
38+
// not match the parameter count, which would break parameterized
39+
// snapshot file naming. XunitV3 applies the same guard.
40+
if (data != null &&
41+
data.Length == method.ParameterNames()?.Count)
3742
{
3843
settings.SetParameters(data);
3944
}

src/Verify.MSTest/Verifier_Archive.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public static SettingsTask VerifyZip(
4747
bool persistArchive = false,
4848
string? archiveExtension = null,
4949
[CallerFilePath] string sourceFile = "") =>
50-
Verify(settings, sourceFile, _ => _.VerifyZip(stream, include, info, fileScrubber, includeStructure, persistArchive), true);
50+
Verify(settings, sourceFile, _ => _.VerifyZip(stream, include, info, fileScrubber, includeStructure, persistArchive, archiveExtension), true);
5151

5252
/// <summary>
5353
/// Verifies the contents of a <see cref="ZipArchive" />

src/Verify.NUnit/VerifyBase_Stream.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public SettingsTask Verify<T>(
4040
VerifySettings? settings = null,
4141
object? info = null)
4242
where T : Stream =>
43-
Verifier.Verify(target, extension, settings, info, sourceFile);
43+
Verifier.Verify(target, extension, settings ?? this.settings, info, sourceFile);
4444

4545
[Pure]
4646
public SettingsTask Verify<T>(
@@ -49,7 +49,7 @@ public SettingsTask Verify<T>(
4949
VerifySettings? settings = null,
5050
object? info = null)
5151
where T : Stream =>
52-
Verifier.Verify(target, extension, settings, info, sourceFile);
52+
Verifier.Verify(target, extension, settings ?? this.settings, info, sourceFile);
5353

5454
[Pure]
5555
public SettingsTask Verify(

src/Verify.NUnit/VerifyBase_Tuple.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ public partial class VerifyBase
77
public SettingsTask VerifyTuple(
88
Expression<Func<ITuple>> target,
99
VerifySettings? settings = null) =>
10-
Verifier.VerifyTuple(target, settings ?? this.settings);
10+
Verifier.VerifyTuple(target, settings ?? this.settings, sourceFile);
1111
}
1212
#endif

src/Verify.Tests/DateFormatLengthCalculatorTests.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,11 @@
4343
[InlineData("zz", 3, 3)]
4444
[InlineData("zzz", 6, 6)]
4545
[InlineData("zzzz", 6, 6)]
46-
[InlineData("K", 6, 6)]
47-
[InlineData("KK", 12, 12)]
46+
// K renders as "" (Unspecified), "Z" (Utc, 1 char) or "+11:00" (offset, 6 chars),
47+
// so its minimum contribution is 0 (not 6) — otherwise round-trip/"o" formats
48+
// scrub only the offset form and leak the Z / offset-less forms.
49+
[InlineData("K", 6, 0)]
50+
[InlineData("KK", 12, 0)]
4851
[InlineData(":", 1, 1)]
4952
[InlineData("':'", 1, 1)]
5053
[InlineData("/", 1, 1)]

src/Verify.Tests/GuidScrubberTests.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,23 @@ public void MultipleChunks()
101101
Assert.Equal("[Guid_1][Guid_2]", builder.ToString());
102102
}
103103

104+
[Fact]
105+
public void GuidSpanningThreeChunks()
106+
{
107+
// Capacity 4 forces small chunks [4][4][rest]; the 36-char guid spans all
108+
// three, and the 4-char middle chunk is shorter than the 35-char carryover.
109+
// The carryover must accumulate across chunks or the prefix is dropped and
110+
// the guid is never found (silent leak).
111+
var guid = "173535ae-995b-4cc6-a74e-8cd4be57039c";
112+
var builder = new StringBuilder(capacity: 4);
113+
builder.Append(guid[..4]); // chunk0
114+
builder.Append(guid[4..8]); // chunk1 (short middle chunk)
115+
builder.Append(guid[8..]); // chunk2
116+
using var counter = Counter.Start();
117+
GuidScrubber.ReplaceGuids(builder, counter);
118+
Assert.Equal("Guid_1", builder.ToString());
119+
}
120+
104121
#region NamedGuidFluent
105122

106123
[Fact]

src/Verify.Tests/LinesScrubberTests.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,29 @@ public Task ScrubLinesContaining_case_sensitive()
7878
""");
7979
}
8080

81+
[Fact]
82+
public void ReplaceLines_AllRemovedNoTrailingNewline()
83+
{
84+
// Single line, no trailing newline, and every line replaced with null.
85+
// The trailing-newline trim must guard on the rebuilt builder length, not
86+
// the original string length, otherwise Length -= 1 underflows.
87+
var builder = new StringBuilder("single line");
88+
89+
builder.ReplaceLines(_ => null);
90+
91+
Assert.Equal(string.Empty, builder.ToString());
92+
}
93+
94+
[Fact]
95+
public void ReplaceLines_ReplacesLines()
96+
{
97+
var builder = new StringBuilder("a\nb\nc");
98+
99+
builder.ReplaceLines(line => line == "b" ? "B" : line);
100+
101+
Assert.Equal("a\nB\nc", builder.ToString());
102+
}
103+
81104
[Fact]
82105
public void FilterLines_RemovesSingleLine()
83106
{

src/Verify.Tests/Serialization/DirectoryReplacementTests.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,22 @@ public void MultipleChunks()
4545
Assert.Equal("{Child} {Parent} ", builder.ToString());
4646
}
4747

48+
[Fact]
49+
public void PathSpanningThreeChunks()
50+
{
51+
// Capacity 4 forces small chunks [4][4][rest]; the 16-char path spans all
52+
// three, and the 4-char middle chunk is shorter than the carryover. The
53+
// carryover must accumulate across chunks or the prefix is dropped and the
54+
// path leaks unscrubbed.
55+
List<DirectoryReplacements.Pair> pairs = [new("C:/Parent/ChildX", "{replace}")];
56+
var builder = new StringBuilder(capacity: 4);
57+
builder.Append("C:/P"); // chunk0
58+
builder.Append("aren"); // chunk1 (short middle chunk)
59+
builder.Append("t/ChildX.");
60+
DirectoryReplacements.Replace(builder, pairs);
61+
Assert.Equal("{replace}.", builder.ToString());
62+
}
63+
4864
[Fact]
4965
public void ProcessLongerDirectoryFirst()
5066
{

0 commit comments

Comments
 (0)