diff --git a/.gitignore b/.gitignore index a57193e23f..28bf4034f8 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/docs/mdsource/naming.source.md b/docs/mdsource/naming.source.md index f1b57df8af..21e5958b72 100644 --- a/docs/mdsource/naming.source.md +++ b/docs/mdsource/naming.source.md @@ -324,7 +324,17 @@ eg. add the following to `.gitignore` When a test project uses more than one `TargetFrameworks` (eg `net48;net7.0`) 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 diff --git a/docs/naming.md b/docs/naming.md index d865aef3d5..37e70bb40f 100644 --- a/docs/naming.md +++ b/docs/naming.md @@ -899,7 +899,17 @@ eg. add the following to `.gitignore` When a test project uses more than one `TargetFrameworks` (eg `net48;net7.0`) 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 diff --git a/src/Verify.Tests/AotCompatibilityTests.cs b/src/Verify.Tests/AotCompatibilityTests.cs index 4514ffe93f..6b77c3945e 100644 --- a/src/Verify.Tests/AotCompatibilityTests.cs +++ b/src/Verify.Tests/AotCompatibilityTests.cs @@ -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); @@ -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 diff --git a/src/Verify.Tests/Naming/UniqueDirectoryReceivedTests.cs b/src/Verify.Tests/Naming/UniqueDirectoryReceivedTests.cs new file mode 100644 index 0000000000..b2a56d40e5 --- /dev/null +++ b/src/Verify.Tests/Naming/UniqueDirectoryReceivedTests.cs @@ -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( + () => 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( + () => 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); + } + } +} \ No newline at end of file diff --git a/src/Verify.Tests/NewLineTests.TrailingNewlinesRaw.verified.txt b/src/Verify.Tests/NewLineTests.TrailingNewlinesRaw.verified.txt deleted file mode 100644 index 7a2a9ee4f5..0000000000 --- a/src/Verify.Tests/NewLineTests.TrailingNewlinesRaw.verified.txt +++ /dev/null @@ -1 +0,0 @@ -a diff --git a/src/Verify/Verifier/InnerVerifier.cs b/src/Verify/Verifier/InnerVerifier.cs index 0ff3d38507..51ca3a5460 100644 --- a/src/Verify/Verifier/InnerVerifier.cs +++ b/src/Verify/Verifier/InnerVerifier.cs @@ -179,10 +179,12 @@ void InitForDirectoryConvention(Namer namer, string typeAndMethod, Action 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? verifiedParameters) { var uniquenessVerified = GetUniquenessVerified(PrefixUnique.SharedUniqueness(namer), namer);