diff --git a/src/Verify.Tests/SolutionDiscoveryTests.cs b/src/Verify.Tests/SolutionDiscoveryTests.cs index 6d4a59e9e2..674a0da2b9 100644 --- a/src/Verify.Tests/SolutionDiscoveryTests.cs +++ b/src/Verify.Tests/SolutionDiscoveryTests.cs @@ -294,13 +294,75 @@ public async Task ExplicitSolutionName_OverridesDiscovery() Assert.Equal(explicitSolutionName, solutionName); } - static string CreateMinimalCsprojContent() + [Fact] + public async Task RebuildFromAnotherSolution_UpdatesMetadata() + { + using var directory = new TempDirectory(); + var tempDir = directory.Path; + + // Create directory structure + var projectDir = Path.Combine(tempDir, "TestProject"); + Directory.CreateDirectory(projectDir); + + // Two solution directories the same project can be built from + var firstSolutionDir = Path.Combine(tempDir, "First") + Path.DirectorySeparatorChar; + var secondSolutionDir = Path.Combine(tempDir, "Second") + Path.DirectorySeparatorChar; + Directory.CreateDirectory(firstSolutionDir); + Directory.CreateDirectory(secondSolutionDir); + + // Create .csproj file. It does not reference Verify.csproj: SolutionDir is a global + // property, so it would flow into that build too, and this repo derives its strong name + // key path from SolutionDir. Only the imported props are needed here anyway. + var csprojPath = Path.Combine(projectDir, "TestProject.csproj"); + await File.WriteAllTextAsync(csprojPath, CreateMinimalCsprojContent(referenceVerify: false)); + + // Build against the first solution + var (success, output) = await BuildProject(csprojPath, firstSolutionDir, "FirstSolution"); + Assert.True(success, $"Build failed: {output}"); + + var assemblyPath = GetAssemblyPath(projectDir); + var (solutionDir, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath); + + Assert.Equal(firstSolutionDir, solutionDir); + Assert.Equal("FirstSolution", solutionName); + + // Rebuild the same intermediate directory against the second solution. No file the + // up-to-date check can see has changed, so only the attributes cache can stop + // WriteVerifyAttributes being skipped and the baked in metadata going stale. + (success, output) = await BuildProject(csprojPath, secondSolutionDir, "SecondSolution"); + Assert.True(success, $"Build failed: {output}"); + + (solutionDir, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath); + + Assert.Equal(secondSolutionDir, solutionDir); + Assert.Equal("SecondSolution", solutionName); + Assert.DoesNotContain(skipMessage, output); + + // Build against the second solution again. The values are unchanged, so the target has + // to go back to being skipped rather than regenerating on every build. + (success, output) = await BuildProject(csprojPath, secondSolutionDir, "SecondSolution"); + Assert.True(success, $"Build failed: {output}"); + Assert.Contains(skipMessage, output); + } + + // MSBuild message, in the language BuildProject pins the build to + const string skipMessage = "Skipping target \"WriteVerifyAttributes\" because all output files are up-to-date"; + + static string CreateMinimalCsprojContent(bool referenceVerify = true) { // Get the path to Verify.csproj and Verify.props relative to test project var verifyProjectPath = Path.Combine(ProjectFiles.SolutionDirectory, "Verify", "Verify.csproj"); var verifyPropsPath = Path.Combine(ProjectFiles.SolutionDirectory, "Verify", "buildTransitive", "Verify.props"); + var reference = referenceVerify + ? $""" + + + + """ + : ""; + return $""" @@ -308,9 +370,7 @@ static string CreateMinimalCsprojContent() Library TestProject - - - + {reference} """; @@ -341,28 +401,38 @@ static string CreateMinimalSlnContent() => static async Task<(bool success, string output)> BuildProject(string csprojPath, string? solutionDir = null, string? solutionName = null) { - var args = $"build \"{csprojPath}\" --configuration Release --verbosity normal"; - - if (solutionDir != null) - { - args += $" \"/p:SolutionDir={solutionDir}\""; - } - - if (solutionName != null) - { - args += $" \"/p:SolutionName={solutionName}\""; - } - var startInfo = new ProcessStartInfo { FileName = "dotnet", - Arguments = args, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; + // MSBuild localizes its messages, and the assertions above match the English text + startInfo.Environment["DOTNET_CLI_UI_LANGUAGE"] = "en"; + + // ArgumentList quotes each value, so a SolutionDir ending in a separator is not + // mangled by that separator escaping the closing quote + var arguments = startInfo.ArgumentList; + arguments.Add("build"); + arguments.Add(csprojPath); + arguments.Add("--configuration"); + arguments.Add("Release"); + arguments.Add("--verbosity"); + arguments.Add("normal"); + + if (solutionDir != null) + { + arguments.Add($"/p:SolutionDir={solutionDir}"); + } + + if (solutionName != null) + { + arguments.Add($"/p:SolutionName={solutionName}"); + } + using var process = Process.Start(startInfo)!; var outputTask = process.StandardOutput.ReadToEndAsync(); diff --git a/src/Verify/buildTransitive/Verify.props b/src/Verify/buildTransitive/Verify.props index d2a0e208df..2ac2d64015 100644 --- a/src/Verify/buildTransitive/Verify.props +++ b/src/Verify/buildTransitive/Verify.props @@ -90,14 +90,21 @@ - + + DependsOnTargets="DiscoverSolutionInfo"> $(IntermediateOutputPath)$(VerifyAttributesFile) + $(IntermediateOutputPath)Verify.Attributes.cache $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(IntermediateOutputPath)')) @@ -132,6 +139,21 @@ + + + + + + +