Skip to content

Commit 8890b5c

Browse files
authored
Re-run WriteVerifyAttributes when the values change (#1891)
* Re-run WriteVerifyAttributes when the values change The up-to-date check compared only $(MSBuildAllProjects) timestamps against the generated file, but the content comes from $(SolutionDir), $(SolutionName) and $(TargetFrameworks). So rebuilding with a different /p:SolutionDir or /p:SolutionName, which is exactly what the DiscoverSolutionInfo warnings tell users to pass, or building the same project from another solution, skipped the target and CoreCompile and left stale metadata in the assembly for AttributeReader and DerivePaths to read at test time. Only a full Rebuild or touching the csproj cleared it. The values are now written to a cache file with WriteOnlyWhenDifferent, and that file is an input, so its timestamp moves only on a real change. Same pattern as the SDK's GenerateAssemblyInfo. Verified by building, then rebuilding with /p:SolutionName=OtherSolution: before this the emitted Verify.SolutionName stayed at the old value, now it updates. * Update SolutionDiscoveryTests.cs
1 parent 40ecfe5 commit 8890b5c

2 files changed

Lines changed: 114 additions & 22 deletions

File tree

src/Verify.Tests/SolutionDiscoveryTests.cs

Lines changed: 87 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -294,23 +294,83 @@ public async Task ExplicitSolutionName_OverridesDiscovery()
294294
Assert.Equal(explicitSolutionName, solutionName);
295295
}
296296

297-
static string CreateMinimalCsprojContent()
297+
[Fact]
298+
public async Task RebuildFromAnotherSolution_UpdatesMetadata()
299+
{
300+
using var directory = new TempDirectory();
301+
var tempDir = directory.Path;
302+
303+
// Create directory structure
304+
var projectDir = Path.Combine(tempDir, "TestProject");
305+
Directory.CreateDirectory(projectDir);
306+
307+
// Two solution directories the same project can be built from
308+
var firstSolutionDir = Path.Combine(tempDir, "First") + Path.DirectorySeparatorChar;
309+
var secondSolutionDir = Path.Combine(tempDir, "Second") + Path.DirectorySeparatorChar;
310+
Directory.CreateDirectory(firstSolutionDir);
311+
Directory.CreateDirectory(secondSolutionDir);
312+
313+
// Create .csproj file. It does not reference Verify.csproj: SolutionDir is a global
314+
// property, so it would flow into that build too, and this repo derives its strong name
315+
// key path from SolutionDir. Only the imported props are needed here anyway.
316+
var csprojPath = Path.Combine(projectDir, "TestProject.csproj");
317+
await File.WriteAllTextAsync(csprojPath, CreateMinimalCsprojContent(referenceVerify: false));
318+
319+
// Build against the first solution
320+
var (success, output) = await BuildProject(csprojPath, firstSolutionDir, "FirstSolution");
321+
Assert.True(success, $"Build failed: {output}");
322+
323+
var assemblyPath = GetAssemblyPath(projectDir);
324+
var (solutionDir, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath);
325+
326+
Assert.Equal(firstSolutionDir, solutionDir);
327+
Assert.Equal("FirstSolution", solutionName);
328+
329+
// Rebuild the same intermediate directory against the second solution. No file the
330+
// up-to-date check can see has changed, so only the attributes cache can stop
331+
// WriteVerifyAttributes being skipped and the baked in metadata going stale.
332+
(success, output) = await BuildProject(csprojPath, secondSolutionDir, "SecondSolution");
333+
Assert.True(success, $"Build failed: {output}");
334+
335+
(solutionDir, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath);
336+
337+
Assert.Equal(secondSolutionDir, solutionDir);
338+
Assert.Equal("SecondSolution", solutionName);
339+
Assert.DoesNotContain(skipMessage, output);
340+
341+
// Build against the second solution again. The values are unchanged, so the target has
342+
// to go back to being skipped rather than regenerating on every build.
343+
(success, output) = await BuildProject(csprojPath, secondSolutionDir, "SecondSolution");
344+
Assert.True(success, $"Build failed: {output}");
345+
Assert.Contains(skipMessage, output);
346+
}
347+
348+
// MSBuild message, in the language BuildProject pins the build to
349+
const string skipMessage = "Skipping target \"WriteVerifyAttributes\" because all output files are up-to-date";
350+
351+
static string CreateMinimalCsprojContent(bool referenceVerify = true)
298352
{
299353
// Get the path to Verify.csproj and Verify.props relative to test project
300354
var verifyProjectPath = Path.Combine(ProjectFiles.SolutionDirectory, "Verify", "Verify.csproj");
301355

302356
var verifyPropsPath = Path.Combine(ProjectFiles.SolutionDirectory, "Verify", "buildTransitive", "Verify.props");
303357

358+
var reference = referenceVerify
359+
? $"""
360+
<ItemGroup>
361+
<ProjectReference Include="{verifyProjectPath}" />
362+
</ItemGroup>
363+
"""
364+
: "";
365+
304366
return $"""
305367
<Project Sdk="Microsoft.NET.Sdk">
306368
<PropertyGroup>
307369
<TargetFramework>net10.0</TargetFramework>
308370
<OutputType>Library</OutputType>
309371
<AssemblyName>TestProject</AssemblyName>
310372
</PropertyGroup>
311-
<ItemGroup>
312-
<ProjectReference Include="{verifyProjectPath}" />
313-
</ItemGroup>
373+
{reference}
314374
<Import Project="{verifyPropsPath}" />
315375
</Project>
316376
""";
@@ -341,28 +401,38 @@ static string CreateMinimalSlnContent() =>
341401

342402
static async Task<(bool success, string output)> BuildProject(string csprojPath, string? solutionDir = null, string? solutionName = null)
343403
{
344-
var args = $"build \"{csprojPath}\" --configuration Release --verbosity normal";
345-
346-
if (solutionDir != null)
347-
{
348-
args += $" \"/p:SolutionDir={solutionDir}\"";
349-
}
350-
351-
if (solutionName != null)
352-
{
353-
args += $" \"/p:SolutionName={solutionName}\"";
354-
}
355-
356404
var startInfo = new ProcessStartInfo
357405
{
358406
FileName = "dotnet",
359-
Arguments = args,
360407
RedirectStandardOutput = true,
361408
RedirectStandardError = true,
362409
UseShellExecute = false,
363410
CreateNoWindow = true
364411
};
365412

413+
// MSBuild localizes its messages, and the assertions above match the English text
414+
startInfo.Environment["DOTNET_CLI_UI_LANGUAGE"] = "en";
415+
416+
// ArgumentList quotes each value, so a SolutionDir ending in a separator is not
417+
// mangled by that separator escaping the closing quote
418+
var arguments = startInfo.ArgumentList;
419+
arguments.Add("build");
420+
arguments.Add(csprojPath);
421+
arguments.Add("--configuration");
422+
arguments.Add("Release");
423+
arguments.Add("--verbosity");
424+
arguments.Add("normal");
425+
426+
if (solutionDir != null)
427+
{
428+
arguments.Add($"/p:SolutionDir={solutionDir}");
429+
}
430+
431+
if (solutionName != null)
432+
{
433+
arguments.Add($"/p:SolutionName={solutionName}");
434+
}
435+
366436
using var process = Process.Start(startInfo)!;
367437

368438
var outputTask = process.StandardOutput.ReadToEndAsync();

src/Verify/buildTransitive/Verify.props

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,21 @@
9090
</PropertyGroup>
9191
</Target>
9292

93-
<Target Name="WriteVerifyAttributes"
93+
<!-- The generated values come from properties, not from the files the up-to-date check can
94+
see, so they are written to a cache file with WriteOnlyWhenDifferent: its timestamp
95+
moves only when a value actually changes, which is what makes WriteVerifyAttributes
96+
below re-run. This mirrors the SDK's GenerateAssemblyInfo.
97+
Without it, only $(MSBuildAllProjects) timestamps were compared, so rebuilding with a
98+
different /p:SolutionDir or /p:SolutionName (exactly what the DiscoverSolutionInfo
99+
warnings above tell users to pass), or building the same project from another
100+
solution, skipped the target and left stale Verify.SolutionDirectory and
101+
Verify.SolutionName metadata in the assembly until a full Rebuild. -->
102+
<Target Name="WriteVerifyAttributesCache"
94103
Condition="$(Language) == 'VB' or $(Language) == 'C#' or $(Language) == 'F#' or $(Language) == 'X#'"
95-
DependsOnTargets="DiscoverSolutionInfo"
96-
BeforeTargets="BeforeCompile;CoreCompile"
97-
Inputs="$(MSBuildAllProjects)"
98-
Outputs="$(IntermediateOutputPath)$(VerifyAttributesFile)">
104+
DependsOnTargets="DiscoverSolutionInfo">
99105
<PropertyGroup>
100106
<VerifyAttributesFilePath>$(IntermediateOutputPath)$(VerifyAttributesFile)</VerifyAttributesFilePath>
107+
<VerifyAttributesCacheFile>$(IntermediateOutputPath)Verify.Attributes.cache</VerifyAttributesCacheFile>
101108
<!-- IntermediateOutputPath is relative to the project for the default layout, but absolute for
102109
others (eg artifacts output), so combine rather than concatenate. -->
103110
<VerifyIntermediateDirectory>$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(IntermediateOutputPath)'))</VerifyIntermediateDirectory>
@@ -132,6 +139,21 @@
132139
<!-- Ensure not part of Compile, as a workaround for https://github.com/dotnet/sdk/issues/114 -->
133140
<Compile Remove="$(VerifyAttributesFilePath)" />
134141
</ItemGroup>
142+
<MakeDir Directories="$(IntermediateOutputPath)" />
143+
<WriteLinesToFile File="$(VerifyAttributesCacheFile)"
144+
Lines="@(VerifyAttributes->'%(_Parameter1)=%(_Parameter2)')"
145+
Overwrite="true"
146+
WriteOnlyWhenDifferent="true" />
147+
<ItemGroup>
148+
<FileWrites Include="$(VerifyAttributesCacheFile)" />
149+
</ItemGroup>
150+
</Target>
151+
<Target Name="WriteVerifyAttributes"
152+
Condition="$(Language) == 'VB' or $(Language) == 'C#' or $(Language) == 'F#' or $(Language) == 'X#'"
153+
DependsOnTargets="WriteVerifyAttributesCache"
154+
BeforeTargets="BeforeCompile;CoreCompile"
155+
Inputs="$(MSBuildAllProjects);$(IntermediateOutputPath)Verify.Attributes.cache"
156+
Outputs="$(IntermediateOutputPath)$(VerifyAttributesFile)">
135157
<WriteCodeFragment AssemblyAttributes="@(VerifyAttributes)"
136158
Language="$(Language)"
137159
OutputFile="$(VerifyAttributesFilePath)">

0 commit comments

Comments
 (0)