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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ src/Verify.Tests/Tests.TextNegative.verified.tmp
*nCrunchTemp*
*.cache
src/Verify.Tests/NewLineTests.StringWithDifferingNewline.verified.txt
src/Verify.Tests/NewLineTests.TrailingNewlinesRaw.verified.txt
src/Verify.MSTest.Tests/Tests.AutoVerifyHasAttachment.verified.txt
src/Verify.NUnit.Tests/Tests.AutoVerifyHasAttachment.verified.txt
src/Verify.TUnit.Tests/Tests.AutoVerifyHasAttachment.verified.txt
Expand Down
12 changes: 11 additions & 1 deletion docs/mdsource/naming.source.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,17 @@ eg. add the following to `.gitignore`

When a test project uses more than one `TargetFrameworks` (eg `<TargetFrameworks>net48;net7.0</TargetFrameworks>`) the runtime and version will always be added as a uniqueness to the received file name. This prevents file locking contention when the tests from both target framework run in parallel.

This applies to the default file naming. Under [UseUniqueDirectory](#useuniquedirectory) the received files use the same uniqueness as the verified files.
Under [UseUniqueDirectory](#useuniquedirectory) the same applies to the received side only. In split mode the received directory carries the runtime and version, and otherwise the received files inside the shared directory do:

```
TheTest.TheMethod/target.DotNet7_0.received.txt
TheTest.TheMethod/target.verified.txt

TheTest.TheMethod.DotNet7_0.received/target.txt
TheTest.TheMethod.verified/target.txt
```

The verified names are left unscoped, so the one snapshot is shared by every target framework.


## Received and single-targeting
Expand Down
12 changes: 11 additions & 1 deletion docs/naming.md
Original file line number Diff line number Diff line change
Expand Up @@ -899,7 +899,17 @@ eg. add the following to `.gitignore`

When a test project uses more than one `TargetFrameworks` (eg `<TargetFrameworks>net48;net7.0</TargetFrameworks>`) the runtime and version will always be added as a uniqueness to the received file name. This prevents file locking contention when the tests from both target framework run in parallel.

This applies to the default file naming. Under [UseUniqueDirectory](#useuniquedirectory) the received files use the same uniqueness as the verified files.
Under [UseUniqueDirectory](#useuniquedirectory) the same applies to the received side only. In split mode the received directory carries the runtime and version, and otherwise the received files inside the shared directory do:

```
TheTest.TheMethod/target.DotNet7_0.received.txt
TheTest.TheMethod/target.verified.txt

TheTest.TheMethod.DotNet7_0.received/target.txt
TheTest.TheMethod.verified/target.txt
```

The verified names are left unscoped, so the one snapshot is shared by every target framework.


## Received and single-targeting
Expand Down
36 changes: 35 additions & 1 deletion src/Verify.Tests/AotCompatibilityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ public async Task Serialize_AnonymousObject_UnderAot()
await File.WriteAllTextAsync(Path.Combine(tempDir, "Program.cs"), programContent);

var (publishSuccess, publishOutput) = await PublishProject(tempDir);
Assert.True(publishSuccess, $"Publish failed: {publishOutput}");
if (!publishSuccess)
{
SkipIfAotToolchainMissing(publishOutput);
Assert.Fail($"Publish failed:{Environment.NewLine}{ErrorLines(publishOutput)}");
}

var exePath = GetPublishedExePath(tempDir, "AotTestApp");
var (exitCode, stdout, _) = await RunExecutable(exePath);
Expand All @@ -64,6 +68,36 @@ public async Task Serialize_AnonymousObject_UnderAot()
Assert.Contains("OK", stdout);
}

// A native AOT publish needs a platform linker, which on Windows comes from the "Desktop development
// with C++" workload. That is a machine prerequisite rather than anything Verify controls, so name it
// and skip, instead of reporting it as a Verify failure buried in a wall of publish output.
static void SkipIfAotToolchainMissing(string publishOutput)
{
if (publishOutput.Contains("Platform linker not found"))
{
Assert.Skip("Native AOT prerequisites are missing: the platform linker was not found. On Windows install the 'Desktop development with C++' workload. See https://aka.ms/nativeaot-prerequisites");
}
}

// The publish output is hundreds of lines of MSBuild noise, and the reason for the failure is the
// handful of error lines in it.
static string ErrorLines(string publishOutput)
{
var errors = publishOutput
.Split('\n')
.Select(_ => _.Trim())
.Where(_ => _.Contains(": error ", StringComparison.Ordinal))
.Distinct()
.ToList();

if (errors.Count == 0)
{
return publishOutput;
}

return string.Join(Environment.NewLine, errors);
}

static async Task<(bool success, string output)> PublishProject(string projectDir)
{
var startInfo = new ProcessStartInfo
Expand Down
58 changes: 58 additions & 0 deletions src/Verify.Tests/Naming/UniqueDirectoryReceivedTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Verify.Tests targets multiple frameworks. The directory convention derives both the received and the
// verified paths from the verified prefix, so, unlike the file convention, the received paths would
// otherwise be shared by every target framework, and the cleanup each run does on init would delete the
// received output of the others. The verified paths have to stay unscoped, so accepting is unaffected.
public class UniqueDirectoryReceivedTests
{
[Fact]
public async Task ReceivedIsScopedToRuntimeAndVersion()
{
var directory = Path.GetFullPath(CurrentFile.Relative($"{nameof(UniqueDirectoryReceivedTests)}.{nameof(ReceivedIsScopedToRuntimeAndVersion)}"));
var scoped = Path.Combine(directory, $"target.{Namer.RuntimeAndVersion}.received.txt");
var unscoped = Path.Combine(directory, "target.received.txt");
try
{
await Assert.ThrowsAsync<VerifyException>(
() => Verify("Value")
.UseUniqueDirectory()
.DisableDiff());
Assert.True(File.Exists(scoped));
Assert.False(File.Exists(unscoped));
// the verified name is not scoped, so every framework shares the one snapshot
Assert.False(File.Exists(Path.Combine(directory, $"target.{Namer.RuntimeAndVersion}.verified.txt")));
}
finally
{
File.Delete(scoped);
File.Delete(unscoped);
}
}

[Fact]
public async Task SplitModeReceivedIsScopedToRuntimeAndVersion()
{
var prefix = Path.GetFullPath(CurrentFile.Relative($"{nameof(UniqueDirectoryReceivedTests)}.{nameof(SplitModeReceivedIsScopedToRuntimeAndVersion)}"));
var scoped = $"{prefix}.{Namer.RuntimeAndVersion}.received";
var unscoped = $"{prefix}.received";
try
{
await Assert.ThrowsAsync<VerifyException>(
() => Verify("Value")
.UseUniqueDirectory()
.UseSplitModeForUniqueDirectory()
.DisableDiff());
Assert.True(File.Exists(Path.Combine(scoped, "target.txt")));
Assert.False(Directory.Exists(unscoped));
// the verified directory is not scoped, so every framework shares the one snapshot directory.
// It is left in place, since the other frameworks may be asserting on it concurrently.
Assert.True(Directory.Exists($"{prefix}.verified"));
}
finally
{
IoHelpers.DeleteDirectory(scoped);
// nothing writes the unscoped directory once the received side is scoped, so this only
// clears an artifact left by a run against the unfixed behavior
IoHelpers.DeleteDirectory(unscoped);
}
}
}

This file was deleted.

27 changes: 23 additions & 4 deletions src/Verify/Verifier/InnerVerifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,12 @@ void InitForDirectoryConvention(Namer namer, string typeAndMethod, Action<String

var directoryPrefix = Path.Combine(directory, verifiedPrefix);

var receivedUniqueness = ReceivedUniquenessForDirectoryConvention(namer);

if (ShouldUseUniqueDirectorySplitMode(settings))
{
var verifiedDirectory = $"{directoryPrefix}.verified";
var receivedDirectory = $"{directoryPrefix}.received";
var receivedDirectory = $"{directoryPrefix}{receivedUniqueness}.received";
Directory.CreateDirectory(verifiedDirectory);
verifiedFiles = IoHelpers.Files(verifiedDirectory, "*");

Expand Down Expand Up @@ -215,20 +217,37 @@ void InitForDirectoryConvention(Namer namer, string typeAndMethod, Action<String
getFileNames = target =>
new(
target.Extension,
Path.Combine(directoryPrefix, $"{target.NameOrTarget}.received.{target.Extension}"),
Path.Combine(directoryPrefix, $"{target.NameOrTarget}{receivedUniqueness}.received.{target.Extension}"),
Path.Combine(directoryPrefix, $"{target.NameOrTarget}.verified.{target.Extension}"),
target.IsString);
getIndexedFileNames = (target, index) =>
new(
target.Extension,
Path.Combine(directoryPrefix, $"{target.NameOrTarget}#{index}.received.{target.Extension}"),
Path.Combine(directoryPrefix, $"{target.NameOrTarget}#{index}{receivedUniqueness}.received.{target.Extension}"),
Path.Combine(directoryPrefix, $"{target.NameOrTarget}#{index}.verified.{target.Extension}"),
target.IsString);

IoHelpers.DeleteFiles(directoryPrefix, "*.received.*");
IoHelpers.DeleteFiles(directoryPrefix, $"*{receivedUniqueness}.received.*");
}
}

// The directory convention derives both the received and the verified paths from the verified
// prefix, so, unlike the file convention, the received paths are shared by every target framework.
// When multiple frameworks are targeted, scope the received side to the runtime and version so the
// runs of each framework do not delete or overwrite each others received output. The verified paths
// are deliberately left alone, so accepting is unaffected. If the test already opted into
// UniqueForRuntimeAndVersion then the prefix separates the frameworks and nothing is needed.
static string ReceivedUniquenessForDirectoryConvention(Namer namer)
{
if (!VerifierSettings.TargetsMultipleFramework ||
namer.ResolveUniqueForRuntimeAndVersion())
{
return string.Empty;
}

return $".{Namer.RuntimeAndVersion}";
}

string PrefixForDirectoryConvention(Namer namer, string typeAndMethod, Action<StringBuilder>? verifiedParameters)
{
var uniquenessVerified = GetUniquenessVerified(PrefixUnique.SharedUniqueness(namer), namer);
Expand Down
Loading