Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
38a2c0b
Add span based scrubber engine and Scrubber API
SimonCropp Jul 11, 2026
710e3c2
.
SimonCropp Jul 11, 2026
4a75188
.
SimonCropp Jul 11, 2026
a12f734
.
SimonCropp Jul 11, 2026
9784975
Docs changes
actions-user Jul 11, 2026
36dee85
Merge branch 'main' into span-based-scrubbers
SimonCropp Jul 11, 2026
1840c97
refs or cleanup
SimonCropp Jul 14, 2026
742a59f
Update ScrubEngine_Lines.cs
SimonCropp Jul 18, 2026
e18a1ca
Merge branch 'main' into span-based-scrubbers
SimonCropp Jul 18, 2026
2d260fa
Create release-notes-32.0.0.md
SimonCropp Jul 18, 2026
be85cae
Merge branch 'main' into span-based-scrubbers
SimonCropp Jul 18, 2026
7f1d0fd
.
SimonCropp Jul 18, 2026
da06f54
Use ordinal comparison for string prefix and suffix matching
SimonCropp Jul 18, 2026
140ddf5
Merge branch 'main' into span-based-scrubbers
SimonCropp Jul 18, 2026
1141bdc
Document registering Scrubber instances via AddScrubber
SimonCropp Jul 18, 2026
9a4616c
Document legacy AddScrubber overloads
SimonCropp Jul 18, 2026
84ecc05
.
SimonCropp Jul 18, 2026
ec17bef
.
SimonCropp Jul 18, 2026
9b043fd
Merge branch 'main' into span-based-scrubbers
SimonCropp Jul 18, 2026
16bccea
Merge branch 'main' into span-based-scrubbers
SimonCropp Jul 18, 2026
c62910b
Update appveyor.yml
SimonCropp Jul 18, 2026
afd9629
.
SimonCropp Jul 18, 2026
961049e
Update VerifyJsonWriter.cs
SimonCropp Jul 18, 2026
4cc5b01
.
SimonCropp Jul 18, 2026
41d4cf5
.
SimonCropp Jul 19, 2026
b0e1a20
.
SimonCropp Jul 19, 2026
089e6d7
.
SimonCropp Jul 19, 2026
e4d457b
.
SimonCropp Jul 19, 2026
2f4f4d4
.
SimonCropp Jul 19, 2026
d1dcb38
.
SimonCropp Jul 19, 2026
8ca08e7
.
SimonCropp Jul 19, 2026
c903eac
.
SimonCropp Jul 19, 2026
b23a58a
.
SimonCropp Jul 19, 2026
2df9c40
.
SimonCropp Jul 19, 2026
c4ac93c
Delete release-notes-32.0.0.md
SimonCropp Jul 19, 2026
81e01be
.
SimonCropp Jul 19, 2026
bbfd1b5
.
SimonCropp Jul 19, 2026
19acb46
Merge branch 'main' into span-based-scrubbers
SimonCropp Jul 19, 2026
f99eb5c
Update Scrubber.cs
SimonCropp Jul 19, 2026
5d4542f
Merge branch 'main' into span-based-scrubbers
SimonCropp Jul 19, 2026
6a8ed94
.
SimonCropp Jul 19, 2026
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/counter.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ To change this file edit the source file and then run MarkdownSnippets.

# Counter

The `Counter` class provides methods to attempt conversion of a `CharSpan` value into a string representation of supported types.
The `Counter` class provides methods to attempt conversion of a `ReadOnlySpan<char>` value into a string representation of supported types.

Supported types include

Expand All @@ -18,7 +18,7 @@ It handles matching equivalent values and assigning a number to each match. It i

## TryConvert

Takes a CharSpan and attempts to parse it to one of the supported types, then return the tokenized scrubbed value for that value.
Takes a `ReadOnlySpan<char>` and attempts to parse it to one of the supported types, then return the tokenized scrubbed value for that value.

One example usage is inside a custom scrubber:

Expand Down
2 changes: 1 addition & 1 deletion docs/dates.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ public Task InferredNamedDateFluent()
.AddNamedDate(namedDate);
}
```
<sup><a href='/src/Verify.Tests/DateScrubberTests.cs#L235-L249' title='Snippet source file'>snippet source</a> | <a href='#snippet-InferredNamedDateFluent' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.Tests/DateScrubberTests.cs#L260-L274' title='Snippet source file'>snippet source</a> | <a href='#snippet-InferredNamedDateFluent' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Result:
Expand Down
8 changes: 4 additions & 4 deletions docs/guids.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ var target = new GuidTarget

await Verify(target);
```
<sup><a href='/src/Verify.Tests/Serialization/SerializationTests.cs#L2320-L2333' title='Snippet source file'>snippet source</a> | <a href='#snippet-guid' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.Tests/Serialization/SerializationTests.cs#L2378-L2391' title='Snippet source file'>snippet source</a> | <a href='#snippet-guid' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Results in the following:
Expand Down Expand Up @@ -161,7 +161,7 @@ public Task NamedGuidInstance()
settings);
}
```
<sup><a href='/src/Verify.Tests/GuidScrubberTests.cs#L65-L81' title='Snippet source file'>snippet source</a> | <a href='#snippet-NamedGuidInstance' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.Tests/GuidScrubberTests.cs#L72-L88' title='Snippet source file'>snippet source</a> | <a href='#snippet-NamedGuidInstance' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


Expand All @@ -182,7 +182,7 @@ public Task NamedGuidFluent()
.AddNamedGuid(guid, "instanceNamed");
}
```
<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>
<sup><a href='/src/Verify.Tests/GuidScrubberTests.cs#L108-L122' 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#L137-L151' 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#L124-L138' title='Snippet source file'>snippet source</a> | <a href='#snippet-InferredNamedGuidFluent' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Result:
Expand Down
4 changes: 2 additions & 2 deletions docs/mdsource/counter.source.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Counter

The `Counter` class provides methods to attempt conversion of a `CharSpan` value into a string representation of supported types.
The `Counter` class provides methods to attempt conversion of a `ReadOnlySpan<char>` value into a string representation of supported types.

Supported types include

Expand All @@ -11,7 +11,7 @@ It handles matching equivalent values and assigning a number to each match. It i

## TryConvert

Takes a CharSpan and attempts to parse it to one of the supported types, then return the tokenized scrubbed value for that value.
Takes a `ReadOnlySpan<char>` and attempts to parse it to one of the supported types, then return the tokenized scrubbed value for that value.

One example usage is inside a custom scrubber:

Expand Down
47 changes: 45 additions & 2 deletions docs/mdsource/scrubbers.source.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,43 @@ Scrubbers run on the final string before doing the verification action.

Multiple scrubbers [can be defined at multiple levels](#Scrubber-levels).

By default scrubber are executed in reverse order. So the most recent added method scrubber through to earliest added global scrubber. All scrubber APIs support a `ScrubberLocation location`. To execute a scrubber last use `ScrubberLocation.Last`.
Scrubbing is performed by two mechanisms:

Scrubbers can be added multiple times to have them execute multiple times. This can be helpful when compounding multiple scrubbers together.
* **The scrub engine**: a span based engine that performs all built-in scrubbing (`ScrubLinesContaining`, `ScrubInlineGuids`, `ScrubMachineName`, etc).
* **Legacy scrubbers**: `AddScrubber(Action<StringBuilder>)` overloads. These run after the engine, and only when at least one is registered.


## The scrub engine

Each engine operation is available as a `Scrub*` method at any [level](#Scrubber-levels):

* `ScrubReplace(find, replacement)`: replace every occurrence of a string. Supports an ordinal `StringComparison` (`Ordinal` or `OrdinalIgnoreCase`) and an optional word boundary requirement. A multi-pair overload replaces the longest matching find at any position.
* `ScrubWindow(minLength, maxLength, matcher)`: slide a window over the text; the matcher returns a replacement or null. Used by the inline guid and date scrubbers.
* `ScrubMatch(matcher, minLength, maxLength)`: custom search logic. The matcher locates the next match within a segment.
* `ScrubLinesContaining(...)`, `ScrubLines(...)`, `ScrubLinesWithReplace(...)`, `ScrubEmptyLines()`: line scoped scrubbing.

snippet: ScrubEngine

`ScrubLines` and `ScrubLinesWithReplace` also accept span based delegates (`LineMatch` / `LineReplace`) that avoid allocating a string per line. Use an explicitly typed lambda parameter to select them, e.g. `ScrubLines((ReadOnlySpan<char> line) => ...)`; untyped lambdas bind the string overloads.

Engine semantics:

* **Quarantine**: text produced by a replacement is never re-examined by other engine scrubbers. Legacy scrubbers run afterwards and can still modify it.
* **Ordering** is engine determined, not registration determined: line removals run first, then line transforms (registration order), then inline scrubbers (unknown max length first, then longest max length first, ties broken by level then registration order). Directory replacements always run last, so scrubbers always see raw paths.
* **Length skip**: text shorter than a scrubber's minimum match length is never scanned by that scrubber.
* **Single line rule**: a match may never contain a line break.
* Text is newline normalized (`\r\n` and `\r` become `\n`) before scrubbers run.

`ScrubberLocation` on the built-in scrub methods is obsolete and ignored; it still applies to legacy `AddScrubber` overloads (default `First` executes in reverse registration order, `Last` in registration order).


## Legacy scrubbers

Instead of being executed by the engine, `AddScrubber(Action<StringBuilder>)` mutates the full text directly. Overloads also accept a `Counter`, and the context dictionary:

snippet: AddScrubber

Since these run after every engine scrubber, they can also modify text that the engine has already replaced.


## Available Scrubbers
Expand Down Expand Up @@ -147,6 +181,15 @@ snippet: ScrubbersSampleTUnit
snippet: Verify.XunitV3.Tests/Scrubbers/ScrubbersSample.Lines.verified.txt


## Extension specific scrubbers

Scrubbers can be scoped to verified files with a matching extension by passing the extension as the first argument. The extension is specified without a leading dot:

snippet: ScrubEngineExtension

A scrubber registered this way runs only for verified files with that extension, while a scrubber registered without an extension runs for all of them. Extension scoping is available at every [level](#Scrubber-levels), and for the legacy `AddScrubber(Action<StringBuilder>)` overloads.


## Scrubber levels

Scrubbers can be defined at three levels:
Expand Down
8 changes: 4 additions & 4 deletions docs/members-throw.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public Task CustomExceptionPropFluent()
.IgnoreMembersThatThrow<CustomException>();
}
```
<sup><a href='/src/Verify.Tests/Serialization/SerializationTests.cs#L4035-L4054' title='Snippet source file'>snippet source</a> | <a href='#snippet-IgnoreMembersThatThrow' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.Tests/Serialization/SerializationTests.cs#L4093-L4112' title='Snippet source file'>snippet source</a> | <a href='#snippet-IgnoreMembersThatThrow' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Or globally:
Expand All @@ -45,7 +45,7 @@ Or globally:
```cs
VerifierSettings.IgnoreMembersThatThrow<CustomException>();
```
<sup><a href='/src/Verify.Tests/Serialization/SerializationTests.cs#L4029-L4033' title='Snippet source file'>snippet source</a> | <a href='#snippet-IgnoreMembersThatThrowGlobal' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.Tests/Serialization/SerializationTests.cs#L4087-L4091' title='Snippet source file'>snippet source</a> | <a href='#snippet-IgnoreMembersThatThrowGlobal' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Result:
Expand Down Expand Up @@ -82,7 +82,7 @@ public Task ExceptionMessagePropFluent()
.IgnoreMembersThatThrow<Exception>(_ => _.Message == "Ignore");
}
```
<sup><a href='/src/Verify.Tests/Serialization/SerializationTests.cs#L2663-L2684' title='Snippet source file'>snippet source</a> | <a href='#snippet-IgnoreMembersThatThrowExpression' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.Tests/Serialization/SerializationTests.cs#L2721-L2742' title='Snippet source file'>snippet source</a> | <a href='#snippet-IgnoreMembersThatThrowExpression' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Or globally:
Expand All @@ -92,7 +92,7 @@ Or globally:
```cs
VerifierSettings.IgnoreMembersThatThrow<Exception>(_ => _.Message == "Ignore");
```
<sup><a href='/src/Verify.Tests/Serialization/SerializationTests.cs#L2656-L2660' title='Snippet source file'>snippet source</a> | <a href='#snippet-IgnoreMembersThatThrowExpressionGlobal' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.Tests/Serialization/SerializationTests.cs#L2714-L2718' title='Snippet source file'>snippet source</a> | <a href='#snippet-IgnoreMembersThatThrowExpressionGlobal' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Result:
Expand Down
4 changes: 2 additions & 2 deletions docs/named-tuples.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Given a method that returns a named tuple:
static (bool Member1, string Member2, string Member3) MethodWithNamedTuple() =>
(true, "A", "B");
```
<sup><a href='/src/Verify.Tests/Serialization/SerializationTests.cs#L2208-L2213' title='Snippet source file'>snippet source</a> | <a href='#snippet-MethodWithNamedTuple' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.Tests/Serialization/SerializationTests.cs#L2266-L2271' title='Snippet source file'>snippet source</a> | <a href='#snippet-MethodWithNamedTuple' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Can be verified:
Expand All @@ -29,7 +29,7 @@ Can be verified:
```cs
await VerifyTuple(() => MethodWithNamedTuple());
```
<sup><a href='/src/Verify.Tests/Serialization/SerializationTests.cs#L2202-L2206' title='Snippet source file'>snippet source</a> | <a href='#snippet-VerifyTuple' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.Tests/Serialization/SerializationTests.cs#L2260-L2264' title='Snippet source file'>snippet source</a> | <a href='#snippet-VerifyTuple' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Resulting in:
Expand Down
6 changes: 3 additions & 3 deletions docs/obsolete-members.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public Task WithObsoleteProp()
return Verify(target);
}
```
<sup><a href='/src/Verify.Tests/Serialization/SerializationTests.cs#L4182-L4203' title='Snippet source file'>snippet source</a> | <a href='#snippet-WithObsoleteProp' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.Tests/Serialization/SerializationTests.cs#L4240-L4261' title='Snippet source file'>snippet source</a> | <a href='#snippet-WithObsoleteProp' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Result:
Expand Down Expand Up @@ -79,7 +79,7 @@ public Task WithObsoletePropIncludedFluent()
.IncludeObsoletes();
}
```
<sup><a href='/src/Verify.Tests/Serialization/SerializationTests.cs#L4153-L4180' title='Snippet source file'>snippet source</a> | <a href='#snippet-WithObsoletePropIncluded' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.Tests/Serialization/SerializationTests.cs#L4211-L4238' title='Snippet source file'>snippet source</a> | <a href='#snippet-WithObsoletePropIncluded' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Or globally:
Expand All @@ -89,7 +89,7 @@ Or globally:
```cs
VerifierSettings.IncludeObsoletes();
```
<sup><a href='/src/Verify.Tests/Serialization/SerializationTests.cs#L4147-L4151' title='Snippet source file'>snippet source</a> | <a href='#snippet-WithObsoletePropIncludedGlobally' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.Tests/Serialization/SerializationTests.cs#L4205-L4209' title='Snippet source file'>snippet source</a> | <a href='#snippet-WithObsoletePropIncludedGlobally' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Result:
Expand Down
Loading
Loading