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
37 changes: 37 additions & 0 deletions docs/converter.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,43 @@ return new(
<!-- endSnippet -->


## Excluding targets

Some converters emit the source document (for example a `pdf`, `docx`, or `xlsx`) alongside the info file and the derived targets. That source document is then committed as a `.verified.{extension}` file. Where the document is large, or where its bytes cannot be made deterministic, it can be excluded from the snapshot. The info file and the derived targets continue to verify.

`ExcludeTargets` takes one or more extensions, and drops every matching target:

<!-- snippet: ExcludeTargets -->
<a id='snippet-ExcludeTargets'></a>
```cs
[Fact]
public Task ExcludeConverterSourceTarget() =>
Verify(new MemoryStream("source-document"u8.ToArray()), "excludesource")
.ExcludeTargets("excludesource");
```
<sup><a href='/src/Verify.Tests/Converters/ExcludeTargetsTests.cs#L67-L74' title='Snippet source file'>snippet source</a> | <a href='#snippet-ExcludeTargets' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Any existing verified file for an excluded extension is then reported as pending deletion.

To exclude an extension for every test, call `ExcludeTargets` on `VerifierSettings` at initialization:

<!-- snippet: StaticExcludeTargets -->
<a id='snippet-StaticExcludeTargets'></a>
```cs
public static class ModuleInitializer
{
[ModuleInitializer]
public static void Init() =>
VerifierSettings.ExcludeTargets("pdf");
}
```
<sup><a href='/src/ModuleInitDocs/ExcludeTargets.cs#L3-L12' title='Snippet source file'>snippet source</a> | <a href='#snippet-StaticExcludeTargets' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Excluding every target of a verification is an error, since a verification requires at least one target.


## Shipping

Converters can be shipped as NuGet packages:
Expand Down
17 changes: 17 additions & 0 deletions docs/mdsource/converter.source.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,23 @@ If cleanup needs to occur after verification a callback can be passes to `Conver
snippet: ConversionResultWithCleanup


## Excluding targets

Some converters emit the source document (for example a `pdf`, `docx`, or `xlsx`) alongside the info file and the derived targets. That source document is then committed as a `.verified.{extension}` file. Where the document is large, or where its bytes cannot be made deterministic, it can be excluded from the snapshot. The info file and the derived targets continue to verify.

`ExcludeTargets` takes one or more extensions, and drops every matching target:

snippet: ExcludeTargets

Any existing verified file for an excluded extension is then reported as pending deletion.

To exclude an extension for every test, call `ExcludeTargets` on `VerifierSettings` at initialization:

snippet: StaticExcludeTargets

Excluding every target of a verification is an error, since a verification requires at least one target.


## Shipping

Converters can be shipped as NuGet packages:
Expand Down
13 changes: 13 additions & 0 deletions src/ModuleInitDocs/ExcludeTargets.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
public class ExcludeTargets
{
#region StaticExcludeTargets

public static class ModuleInitializer
{
[ModuleInitializer]
public static void Init() =>
VerifierSettings.ExcludeTargets("pdf");
}

#endregion
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
null
20 changes: 20 additions & 0 deletions src/StaticSettingsTests/ExcludeTargetsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
public class ExcludeTargetsTests :
BaseTest
{
[Fact]
public async Task GlobalExclusionAppliesToAllVerifications()
{
VerifierSettings.ExcludeTargets("excludedbin");
Target[] targets = [new("excludedbin", new MemoryStream("binary-content"u8.ToArray()))];
var result = await Verify(null, targets)
.DisableDiff();
Assert.Single(result.Files);
}

[Fact]
public void AfterVerifyHasBeenRunThrows()
{
InnerVerifier.verifyHasBeenRun = true;
Assert.Throws<Exception>(() => VerifierSettings.ExcludeTargets("excludedbin"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
derived from source
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
the info
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
null
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
null
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
null
102 changes: 102 additions & 0 deletions src/Verify.Tests/Converters/ExcludeTargetsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
public class ExcludeTargetsTests
{
// Mimics a document converter (eg Verify.PdfPig) that emits the source document
// alongside targets derived from it.
[ModuleInitializer]
public static void Init() =>
VerifierSettings.RegisterStreamConverter(
"excludesource",
(_, _, _) =>
new(
null,
[
new("excludesource", new MemoryStream("source-document"u8.ToArray())),
new("txt", "derived from source")
]));

class SourceDocument;

// The same shape, reached through a file converter rather than a stream converter.
[ModuleInitializer]
public static void FileConverterInit() =>
VerifierSettings.RegisterFileConverter<SourceDocument>(
(_, _) =>
new(
"the info",
[new("excludedbin", new MemoryStream("source-document"u8.ToArray()))]));

static Target[] BuildTargets() =>
[
new("excludedbin", new MemoryStream("binary-content"u8.ToArray()))
];

// Only the root txt target survives, so no verified file is required for the excluded extension.
[Fact]
public async Task ExcludeStreamTarget()
{
var result = await Verify(null, BuildTargets())
.ExcludeTargets("excludedbin")
.DisableDiff();
Assert.Single(result.Files);
}

[Fact]
public async Task ExtensionMatchIsCaseInsensitive()
{
var result = await Verify(null, BuildTargets())
.ExcludeTargets("EXCLUDEDBIN")
.DisableDiff();
Assert.Single(result.Files);
}

// The engine disposes the streams it consumes. An excluded stream never reaches it,
// so the exclusion has to dispose it instead.
[Fact]
public async Task ExcludedStreamIsDisposed()
{
var stream = new MemoryStream("binary-content"u8.ToArray());
Target[] targets = [new("excludedbin", stream)];
await Verify(null, targets)
.ExcludeTargets("excludedbin")
.DisableDiff();
Assert.False(stream.CanRead);
}

// The source document a converter emits can be excluded, keeping its derived targets.
// Were the exclusion to fail, an unexpected excludesource target would be reported as new.
#region ExcludeTargets

[Fact]
public Task ExcludeConverterSourceTarget() =>
Verify(new MemoryStream("source-document"u8.ToArray()), "excludesource")
.ExcludeTargets("excludesource");

#endregion

[Fact]
public async Task ExcludeFileConverterSourceTarget()
{
var result = await Verify(new SourceDocument())
.ExcludeTargets("excludedbin")
.DisableDiff();
Assert.Single(result.Files);
}

[Fact]
public async Task ExcludingAllTargetsThrows()
{
var exception = await Assert.ThrowsAsync<Exception>(
() => Verify(new MemoryStream("binary-content"u8.ToArray()), "excludedbin")
.ExcludeTargets("excludedbin")
.DisableDiff());
Assert.Contains("All targets have been excluded", exception.Message);
}

[Fact]
public void NoExtensionsThrows() =>
Assert.Throws<ArgumentException>(() => new VerifySettings().ExcludeTargets());

[Fact]
public void ExtensionWithPeriodThrows() =>
Assert.Throws<ArgumentException>(() => new VerifySettings().ExcludeTargets(".pdf"));
}
1 change: 1 addition & 0 deletions src/Verify/Serialization/VerifierSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ internal static void Reset()
omitContentFromException = false;
encoding = new UTF8Encoding(true, true);
addAttachments = true;
excludedTargets = null;
GlobalScrubbers.Clear();
GlobalIgnoredParameters = null;
GlobalIgnoreConstructorParameters = false;
Expand Down
68 changes: 68 additions & 0 deletions src/Verify/Splitters/Settings_ExcludeTargets.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
namespace VerifyTests;

public static partial class VerifierSettings
{
internal static HashSet<string>? excludedTargets;

/// <summary>
/// Excludes every target with one of <paramref name="extensions" /> from all verifications.
/// Any existing verified file for an excluded extension is treated as pending deletion.
/// Intended for converters that emit a source document (eg <c>pdf</c> or <c>docx</c>) alongside
/// the info file and rendered pages, where committing the source document is not wanted.
/// </summary>
public static void ExcludeTargets(params string[] extensions)
{
InnerVerifier.ThrowIfVerifyHasBeenRun();
excludedTargets = AddExcludedTargets(excludedTargets, extensions);
}

internal static bool AnyExcludedTargets(VerifySettings settings) =>
excludedTargets != null ||
settings.excludedTargets != null;

internal static bool IsTargetExcluded(VerifySettings settings, string extension) =>
excludedTargets?.Contains(extension) == true ||
settings.excludedTargets?.Contains(extension) == true;

internal static HashSet<string> AddExcludedTargets(HashSet<string>? existing, string[] extensions)
{
if (extensions.Length == 0)
{
throw new ArgumentException("At least one extension is required.", nameof(extensions));
}

existing ??= new(StringComparer.OrdinalIgnoreCase);
foreach (var extension in extensions)
{
Guards.AgainstBadExtension(extension);
existing.Add(extension);
}

return existing;
}
}

public partial class VerifySettings
{
internal HashSet<string>? excludedTargets;

/// <summary>
/// Excludes every target with one of <paramref name="extensions" /> from the current verification.
/// Any existing verified file for an excluded extension is treated as pending deletion.
/// Intended for converters that emit a source document (eg <c>pdf</c> or <c>docx</c>) alongside
/// the info file and rendered pages, where committing the source document is not wanted.
/// </summary>
public void ExcludeTargets(params string[] extensions) =>
excludedTargets = VerifierSettings.AddExcludedTargets(excludedTargets, extensions);
}

public partial class SettingsTask
{
/// <inheritdoc cref="VerifySettings.ExcludeTargets" />
[Pure]
public SettingsTask ExcludeTargets(params string[] extensions)
{
CurrentSettings.ExcludeTargets(extensions);
return this;
}
}
37 changes: 37 additions & 0 deletions src/Verify/Verifier/InnerVerifier_Inner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ async Task<VerifyResult> VerifyInner(object? root, Func<Task>? cleanup, IEnumera
var (extraTargets, extraCleanup) = await GetTargets(targets, doExtensionConversion);
cleanup = cleanup.Then(extraCleanup);
resultTargets.AddRange(extraTargets);
cleanup = RemoveExcludedTargets(resultTargets, cleanup, out var removedTargets);
if (removedTargets &&
resultTargets.Count == 0)
{
await cleanup();
throw new("All targets have been excluded by ExcludeTargets. A verification requires at least one target.");
}

var engine = new VerifyEngine(
directory,
settings,
Expand Down Expand Up @@ -48,6 +56,35 @@ async Task<VerifyResult> VerifyInner(object? root, Func<Task>? cleanup, IEnumera
return new(filePairs, root);
}

Func<Task> RemoveExcludedTargets(List<Target> targets, Func<Task> cleanup, out bool removed)
{
removed = false;
if (!VerifierSettings.AnyExcludedTargets(settings))
{
return cleanup;
}

for (var index = targets.Count - 1; index >= 0; index--)
{
var target = targets[index];
if (!VerifierSettings.IsTargetExcluded(settings, target.Extension))
{
continue;
}

if (target.IsStream)
{
// VerifyEngine disposes the streams it consumes, so an excluded stream never reaches it
cleanup = cleanup.Then(target.StreamData.DisposeAsyncEx);
}

targets.RemoveAt(index);
removed = true;
}

return cleanup;
}

async Task<(List<Target> extra, Func<Task> cleanup)> GetTargets(IEnumerable<Target> targets, bool doExtensionConversion)
{
List<Target> list = [..targets, ..VerifierSettings.GetFileAppenders(settings)];
Expand Down
5 changes: 5 additions & 0 deletions src/Verify/VerifySettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ public VerifySettings(VerifySettings? settings)
// (AppendFile/UseStreamComparer/etc.) would mutate a shared base settings
// instance and leak across tests.
appendedFiles = settings.appendedFiles?.ToList();
if (settings.excludedTargets != null)
{
excludedTargets = new(settings.excludedTargets, StringComparer.OrdinalIgnoreCase);
}

UniqueDirectory = settings.UniqueDirectory;
Directory = settings.Directory;
autoVerify = settings.autoVerify;
Expand Down
Loading