@@ -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 ( ) ;
0 commit comments