From 02fe445d0c130a0a7e0ee467dc6964f7631964cb Mon Sep 17 00:00:00 2001 From: AR-May <67507805+AR-May@users.noreply.github.com> Date: Thu, 8 Jan 2026 13:12:13 +0100 Subject: [PATCH 01/16] 1st version --- MSBuild.sln | 30 ++++- ...ft.Build.CommandLine.EndToEnd.Tests.csproj | 31 +++++ .../MultithreadedExecution_Tests.cs | 109 ++++++++++++++++++ .../ConsoleApp/ConsoleApp.csproj | 11 ++ .../ConsoleApp/Program.cs | 9 ++ .../Library1/Class1.cs | 7 ++ .../Library1/Library1.csproj | 11 ++ .../Library2/Class2.cs | 7 ++ .../Library2/Library2.csproj | 11 ++ .../Library3/Class3.cs | 7 ++ .../Library3/Library3.csproj | 6 + .../Library4/Class4.cs | 7 ++ .../Library4/Library4.csproj | 6 + .../TestAssets/SingleProject/Program.cs | 12 ++ .../SingleProject/SingleProject.csproj | 10 ++ .../TestAssetsFixture.cs | 56 +++++++++ src/MSBuild/AssemblyInfo.cs | 1 + 17 files changed, 329 insertions(+), 2 deletions(-) create mode 100644 src/MSBuild.EndToEnd.Tests/Microsoft.Build.CommandLine.EndToEnd.Tests.csproj create mode 100644 src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/ConsoleApp.csproj create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/Program.cs create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Class1.cs create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Library1.csproj create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Class2.cs create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Library2.csproj create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Class3.cs create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Library3.csproj create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Class4.cs create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Library4.csproj create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/Program.cs create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/SingleProject.csproj create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs diff --git a/MSBuild.sln b/MSBuild.sln index caac39c6344..90c6df304ae 100644 --- a/MSBuild.sln +++ b/MSBuild.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 +# Visual Studio Version 18 +VisualStudioVersion = 18.3.11305.148 MinimumVisualStudioVersion = 17.0.31903.59 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4900B3B8-4310-4D5B-B1F7-2FDF9199765F}" ProjectSection(SolutionItems) = preProject @@ -78,6 +78,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Build.BuildCheck. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Build.Templates", "template_feed\Microsoft.Build.Templates.csproj", "{A86EE74A-AEF0-42ED-A5A7-7A54BC0773D8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Build.CommandLine.EndToEnd.Tests", "src\MSBuild.EndToEnd.Tests\Microsoft.Build.CommandLine.EndToEnd.Tests.csproj", "{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -862,6 +864,30 @@ Global {A86EE74A-AEF0-42ED-A5A7-7A54BC0773D8}.Release|x64.Build.0 = Release|Any CPU {A86EE74A-AEF0-42ED-A5A7-7A54BC0773D8}.Release|x86.ActiveCfg = Release|Any CPU {A86EE74A-AEF0-42ED-A5A7-7A54BC0773D8}.Release|x86.Build.0 = Release|Any CPU + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Debug|ARM64.ActiveCfg = Debug|arm64 + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Debug|ARM64.Build.0 = Debug|arm64 + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Debug|x64.ActiveCfg = Debug|x64 + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Debug|x64.Build.0 = Debug|x64 + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Debug|x86.ActiveCfg = Debug|Any CPU + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Debug|x86.Build.0 = Debug|Any CPU + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.MachineIndependent|Any CPU.ActiveCfg = MachineIndependent|Any CPU + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.MachineIndependent|Any CPU.Build.0 = MachineIndependent|Any CPU + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.MachineIndependent|ARM64.ActiveCfg = MachineIndependent|arm64 + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.MachineIndependent|ARM64.Build.0 = MachineIndependent|arm64 + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.MachineIndependent|x64.ActiveCfg = MachineIndependent|x64 + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.MachineIndependent|x64.Build.0 = MachineIndependent|x64 + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.MachineIndependent|x86.ActiveCfg = MachineIndependent|Any CPU + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.MachineIndependent|x86.Build.0 = MachineIndependent|Any CPU + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Release|Any CPU.Build.0 = Release|Any CPU + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Release|ARM64.ActiveCfg = Release|arm64 + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Release|ARM64.Build.0 = Release|arm64 + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Release|x64.ActiveCfg = Release|x64 + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Release|x64.Build.0 = Release|x64 + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Release|x86.ActiveCfg = Release|Any CPU + {29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/MSBuild.EndToEnd.Tests/Microsoft.Build.CommandLine.EndToEnd.Tests.csproj b/src/MSBuild.EndToEnd.Tests/Microsoft.Build.CommandLine.EndToEnd.Tests.csproj new file mode 100644 index 00000000000..655efccecab --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/Microsoft.Build.CommandLine.EndToEnd.Tests.csproj @@ -0,0 +1,31 @@ + + + + $(RuntimeOutputTargetFrameworks) + $(RuntimeOutputPlatformTarget) + false + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs b/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs new file mode 100644 index 00000000000..a03f919389b --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Logging; +using Microsoft.Build.Shared; +using Microsoft.Build.UnitTests; +using Microsoft.Build.UnitTests.Shared; +using Shouldly; +using Xunit; +using Xunit.Abstractions; + +#nullable disable + +namespace Microsoft.Build.EndToEndTests +{ + /// + /// Tests for multithreaded MSBuild execution scenarios using test assets. + /// + public class MultithreadedExecution_Tests : IClassFixture, IDisposable + { + private readonly ITestOutputHelper _output; + private readonly TestEnvironment _env; + private readonly string _testAssetDir; + + public MultithreadedExecution_Tests(ITestOutputHelper output, TestAssetsFixture testAssetFixture) + { + _output = output; + _env = TestEnvironment.Create(output); + _testAssetDir = testAssetFixture.TestAssetDir; + } + + public void Dispose() + { + _env.Dispose(); + } + + /// + /// Tests building projects with various multithreading flags. + /// + [Theory] + [InlineData(TestAssetsFixture.SingleProjectPath, "/m:1 /mt")] + [InlineData(TestAssetsFixture.SingleProjectPath, "/m:8 /mt")] + [InlineData(TestAssetsFixture.ProjectWithDependencies, "/m:1 /mt")] + [InlineData(TestAssetsFixture.ProjectWithDependencies, "/m:2 /mt")] + [InlineData(TestAssetsFixture.ProjectWithDependencies, "/m:8 /mt")] + public void MultithreadedBuild_Success(string projectRelativePath, string multithreadingArgs) + { + string projectPath = Path.Combine(_testAssetDir, projectRelativePath); + + // Ensure test asset exists - fail if missing + File.Exists(projectPath).ShouldBeTrue($"Test asset not found: {projectPath}."); + + string output = RunnerUtilities.ExecBootstrapedMSBuild( + $"\"{projectPath}\" {multithreadingArgs} /v:minimal", + out bool success); + + success.ShouldBeTrue($"Build failed with args '{multithreadingArgs}' for {projectRelativePath}. Output:\\n{output}"); + + _output.WriteLine($"Built {Path.GetFileNameWithoutExtension(projectRelativePath)} with arguments {multithreadingArgs}."); + } + + /// + /// Tests binary logging with multithreaded builds and verifies replay functionality. + /// + [Theory] + [InlineData(TestAssetsFixture.ProjectWithDependencies, "/m:8 /mt")] + public void MultithreadedBuild_BinaryLogging(string projectRelativePath, string multithreadingArgs) + { + string projectPath = Path.Combine(_testAssetDir, projectRelativePath); + + // Ensure test asset exists - fail if missing + File.Exists(projectPath).ShouldBeTrue($"Test asset not found: {projectPath}."); + + var tempFolder = _env.CreateFolder(); + string binlogPath = Path.Combine(tempFolder.Path, "build.binlog"); + + try + { + // Build with binary logging + string output = RunnerUtilities.ExecBootstrapedMSBuild( + $"\"{projectPath}\" {multithreadingArgs} /bl:\"{binlogPath}\" /v:minimal", + out bool success); + + success.ShouldBeTrue($"Build failed with args '{multithreadingArgs}' for {projectRelativePath}. Output:\\n{output}."); + + // Verify binary log was created and has content + File.Exists(binlogPath).ShouldBeTrue("Binary log file was not created."); + + // Test binlog replay + string replayOutput = RunnerUtilities.ExecBootstrapedMSBuild($"\"{binlogPath}\" /v:minimal", out bool replaySuccess); + + replaySuccess.ShouldBeTrue($"Binlog replay failed. Output:\\n{replayOutput}"); + + _output.WriteLine($"Built and replayed {Path.GetFileNameWithoutExtension(projectRelativePath)} with arguments {multithreadingArgs}."); + } + finally + { + if (File.Exists(binlogPath)) + { + File.Delete(binlogPath); + } + } + } + } +} diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/ConsoleApp.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/ConsoleApp.csproj new file mode 100644 index 00000000000..3b215f6b962 --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/ConsoleApp.csproj @@ -0,0 +1,11 @@ + + + Exe + net8.0 + latest + + + + + + \ No newline at end of file diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/Program.cs b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/Program.cs new file mode 100644 index 00000000000..26d73d8bcaf --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/Program.cs @@ -0,0 +1,9 @@ +namespace ConsoleApp +{ + class Program + { + static void Main(string[] args) + { + } + } +} diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Class1.cs b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Class1.cs new file mode 100644 index 00000000000..c8224d76a9a --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Class1.cs @@ -0,0 +1,7 @@ +namespace Library1 +{ + public class Class1 + { + + } +} \ No newline at end of file diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Library1.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Library1.csproj new file mode 100644 index 00000000000..1586309c905 --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Library1.csproj @@ -0,0 +1,11 @@ + + + net8.0 + latest + + + + + + + \ No newline at end of file diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Class2.cs b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Class2.cs new file mode 100644 index 00000000000..7de56b98ddb --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Class2.cs @@ -0,0 +1,7 @@ +namespace Library2 +{ + public class Class2 + { + + } +} \ No newline at end of file diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Library2.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Library2.csproj new file mode 100644 index 00000000000..1586309c905 --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Library2.csproj @@ -0,0 +1,11 @@ + + + net8.0 + latest + + + + + + + \ No newline at end of file diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Class3.cs b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Class3.cs new file mode 100644 index 00000000000..50a703c482b --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Class3.cs @@ -0,0 +1,7 @@ +namespace Library3 +{ + public class Class3 + { + + } +} \ No newline at end of file diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Library3.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Library3.csproj new file mode 100644 index 00000000000..2f9f4165538 --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Library3.csproj @@ -0,0 +1,6 @@ + + + net8.0 + latest + + \ No newline at end of file diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Class4.cs b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Class4.cs new file mode 100644 index 00000000000..50a703c482b --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Class4.cs @@ -0,0 +1,7 @@ +namespace Library3 +{ + public class Class3 + { + + } +} \ No newline at end of file diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Library4.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Library4.csproj new file mode 100644 index 00000000000..2f9f4165538 --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Library4.csproj @@ -0,0 +1,6 @@ + + + net8.0 + latest + + \ No newline at end of file diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/Program.cs b/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/Program.cs new file mode 100644 index 00000000000..6d3de979e33 --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace ConsoleApp +{ + class Program + { + static void Main(string[] args) + { + + } + } +} \ No newline at end of file diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/SingleProject.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/SingleProject.csproj new file mode 100644 index 00000000000..c28018dbfdd --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/SingleProject.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + \ No newline at end of file diff --git a/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs new file mode 100644 index 00000000000..75b41bd0886 --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using Microsoft.Build.UnitTests.Shared; + +#nullable disable + +namespace Microsoft.Build.EndToEndTests +{ + /// + /// Fixture for test assets that handles expensive initialization like NuGet restore. + /// + public class TestAssetsFixture : IDisposable + { + public string TestAssetDir { get; } + + // Public constants for common project paths + public const string SingleProjectPath = "SingleProject\\SingleProject.csproj"; + public const string ProjectWithDependencies = "ProjectWithDependencies\\ConsoleApp\\ConsoleApp.csproj"; + + private static readonly string[] ProjectsToRestore = + [ + SingleProjectPath, + ProjectWithDependencies + ]; + + public TestAssetsFixture() + { + TestAssetDir = Path.Combine(AppContext.BaseDirectory, "TestAssets"); + RestoreTestAssets(); + } + + private void RestoreTestAssets() + { + foreach (string projectPath in ProjectsToRestore) + { + string fullPath = Path.Combine(TestAssetDir, projectPath); + + if (File.Exists(fullPath)) + { + RunnerUtilities.ExecBootstrapedMSBuild($"\"{fullPath}\" /t:Restore /v:minimal", out bool success); + if (!success) + { + System.Diagnostics.Debug.WriteLine($"Warning: Failed to restore {fullPath}"); + } + } + } + } + public void Dispose() + { + // Clean up if needed + } + } +} diff --git a/src/MSBuild/AssemblyInfo.cs b/src/MSBuild/AssemblyInfo.cs index f93e8a6db00..5cee7eb069e 100644 --- a/src/MSBuild/AssemblyInfo.cs +++ b/src/MSBuild/AssemblyInfo.cs @@ -8,6 +8,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +[assembly: InternalsVisibleTo("Microsoft.Build.CommandLine.EndToEnd.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.CommandLine.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.Utilities.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] From 7064f6653290a1f6deb01aa8882bcae0da7d89e8 Mon Sep 17 00:00:00 2001 From: Alina Mayorova <67507805+AR-May@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:01:05 +0100 Subject: [PATCH 02/16] fix path to the asset --- src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs index 75b41bd0886..5ebc8dcedc5 100644 --- a/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs +++ b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs @@ -28,7 +28,7 @@ public class TestAssetsFixture : IDisposable public TestAssetsFixture() { - TestAssetDir = Path.Combine(AppContext.BaseDirectory, "TestAssets"); + TestAssetDir = Path.Combine(Path.GetDirectoryName(typeof(TestAssetsFixture).Assembly.Location) ?? AppContext.BaseDirectory, "TestAssets"); RestoreTestAssets(); } From 880f29cadab8982091564d41702f2fc61a1cb7d7 Mon Sep 17 00:00:00 2001 From: Alina Mayorova <67507805+AR-May@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:08:35 +0100 Subject: [PATCH 03/16] change binlog test so that it takes less time --- src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs b/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs index a03f919389b..68f8d12a8b4 100644 --- a/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs +++ b/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs @@ -67,7 +67,7 @@ public void MultithreadedBuild_Success(string projectRelativePath, string multit /// Tests binary logging with multithreaded builds and verifies replay functionality. /// [Theory] - [InlineData(TestAssetsFixture.ProjectWithDependencies, "/m:8 /mt")] + [InlineData(TestAssetsFixture.SingleProjectPath, "/m:8 /mt")] public void MultithreadedBuild_BinaryLogging(string projectRelativePath, string multithreadingArgs) { string projectPath = Path.Combine(_testAssetDir, projectRelativePath); From 32dce8d1d702f19c865b87d36ee3f1ad8f5b714d Mon Sep 17 00:00:00 2001 From: Alina Mayorova <67507805+AR-May@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:25:04 +0100 Subject: [PATCH 04/16] ensure tests isolation --- .../MultithreadedExecution_Tests.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs b/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs index 68f8d12a8b4..d1b3313ecb1 100644 --- a/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs +++ b/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs @@ -26,6 +26,12 @@ public class MultithreadedExecution_Tests : IClassFixture, ID private readonly TestEnvironment _env; private readonly string _testAssetDir; + // Common parameters for all multithreaded tests: + // /nodereuse:false - Prevents MSBuild server processes from persisting between tests, + // ensuring proper test isolation and avoiding potential timeouts + // /v:minimal - Reduces log verbosity for cleaner test output and better performance + private const string CommonMSBuildArgs = "/nodereuse:false /v:minimal"; + public MultithreadedExecution_Tests(ITestOutputHelper output, TestAssetsFixture testAssetFixture) { _output = output; @@ -55,7 +61,7 @@ public void MultithreadedBuild_Success(string projectRelativePath, string multit File.Exists(projectPath).ShouldBeTrue($"Test asset not found: {projectPath}."); string output = RunnerUtilities.ExecBootstrapedMSBuild( - $"\"{projectPath}\" {multithreadingArgs} /v:minimal", + $"\"{projectPath}\" {multithreadingArgs} {CommonMSBuildArgs}", out bool success); success.ShouldBeTrue($"Build failed with args '{multithreadingArgs}' for {projectRelativePath}. Output:\\n{output}"); @@ -82,7 +88,7 @@ public void MultithreadedBuild_BinaryLogging(string projectRelativePath, string { // Build with binary logging string output = RunnerUtilities.ExecBootstrapedMSBuild( - $"\"{projectPath}\" {multithreadingArgs} /bl:\"{binlogPath}\" /v:minimal", + $"\"{projectPath}\" {multithreadingArgs} /bl:\"{binlogPath}\" {CommonMSBuildArgs}", out bool success); success.ShouldBeTrue($"Build failed with args '{multithreadingArgs}' for {projectRelativePath}. Output:\\n{output}."); @@ -91,7 +97,7 @@ public void MultithreadedBuild_BinaryLogging(string projectRelativePath, string File.Exists(binlogPath).ShouldBeTrue("Binary log file was not created."); // Test binlog replay - string replayOutput = RunnerUtilities.ExecBootstrapedMSBuild($"\"{binlogPath}\" /v:minimal", out bool replaySuccess); + string replayOutput = RunnerUtilities.ExecBootstrapedMSBuild($"\"{binlogPath}\" {CommonMSBuildArgs}", out bool replaySuccess); replaySuccess.ShouldBeTrue($"Binlog replay failed. Output:\\n{replayOutput}"); From 3d78e85060f2bcab41a11080b47a677a50082427 Mon Sep 17 00:00:00 2001 From: Alina Mayorova <67507805+AR-May@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:14:39 +0100 Subject: [PATCH 05/16] Isolate test asset --- src/BuildCheck.UnitTests/EndToEndTests.cs | 21 +-- .../MultithreadedExecution_Tests.cs | 121 +++++++++++------- .../TestAssetsFixture.cs | 29 +++-- .../TestSolutionAsset.cs | 32 +++++ src/UnitTests.Shared/FileSystemUtilities.cs | 35 +++++ 5 files changed, 159 insertions(+), 79 deletions(-) create mode 100644 src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs create mode 100644 src/UnitTests.Shared/FileSystemUtilities.cs diff --git a/src/BuildCheck.UnitTests/EndToEndTests.cs b/src/BuildCheck.UnitTests/EndToEndTests.cs index d251e8f5ecc..27fdadbd20d 100644 --- a/src/BuildCheck.UnitTests/EndToEndTests.cs +++ b/src/BuildCheck.UnitTests/EndToEndTests.cs @@ -162,7 +162,7 @@ private EmbedResourceTestOutput RunEmbeddedResourceTest(string resourceXmlToAdd, const string templateToReplace = "###EmbeddedResourceToAdd"; TransientTestFolder workFolder = _env.CreateFolder(createFolder: true); - CopyFilesRecursively(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path); + FileSystemUtilities.CopyFilesRecursively(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path); ReplaceStringInFile(Path.Combine(workFolder.Path, referencedProjectName, $"{referencedProjectName}.csproj"), templateToReplace, resourceXmlToAdd); File.Copy( @@ -198,21 +198,6 @@ void ReplaceStringInFile(string filePath, string original, string replacement) } } - private static void CopyFilesRecursively(string sourcePath, string targetPath) - { - // First Create all directories - foreach (string dirPath in Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories)) - { - Directory.CreateDirectory(dirPath.Replace(sourcePath, targetPath)); - } - - // Then copy all the files & Replaces any files with the same name - foreach (string newPath in Directory.GetFiles(sourcePath, "*", SearchOption.AllDirectories)) - { - File.Copy(newPath, newPath.Replace(sourcePath, targetPath), true); - } - } - private static int GetWarningsCount(string output) { Regex regex = new Regex(@"(\d+) Warning\(s\)"); @@ -272,7 +257,7 @@ public void CopyToOutputTest(bool skipUnchangedDuringCopy) const string entryProjectName = "EntryProject"; TransientTestFolder workFolder = _env.CreateFolder(createFolder: true); - CopyFilesRecursively(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path); + FileSystemUtilities.CopyFilesRecursively(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path); _env.SetCurrentDirectory(Path.Combine(workFolder.Path, entryProjectName)); @@ -377,7 +362,7 @@ public void TFMConfusionCheckTest(string tfmString, string cliSuffix, bool shoul const string templateToReplace = "###TFM"; TransientTestFolder workFolder = _env.CreateFolder(createFolder: true); - CopyFilesRecursively(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path); + FileSystemUtilities.CopyFilesRecursively(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path); ReplaceStringInFile(Path.Combine(workFolder.Path, $"{projectName}.csproj"), templateToReplace, tfmString); diff --git a/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs b/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs index d1b3313ecb1..afa94c2d9e2 100644 --- a/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs +++ b/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs @@ -20,7 +20,7 @@ namespace Microsoft.Build.EndToEndTests /// /// Tests for multithreaded MSBuild execution scenarios using test assets. /// - public class MultithreadedExecution_Tests : IClassFixture, IDisposable + public class MultithreadedExecution_Tests : IClassFixture, IDisposable { private readonly ITestOutputHelper _output; private readonly TestEnvironment _env; @@ -32,7 +32,7 @@ public class MultithreadedExecution_Tests : IClassFixture, ID // /v:minimal - Reduces log verbosity for cleaner test output and better performance private const string CommonMSBuildArgs = "/nodereuse:false /v:minimal"; - public MultithreadedExecution_Tests(ITestOutputHelper output, TestAssetsFixture testAssetFixture) + public MultithreadedExecution_Tests(ITestOutputHelper output, TestSolutionAssetsFixture testAssetFixture) { _output = output; _env = TestEnvironment.Create(output); @@ -44,72 +44,99 @@ public void Dispose() _env.Dispose(); } + /// + /// Prepares an isolated copy of test assets in a temporary directory for each test run. + /// This ensures fresh builds and proper test isolation. + /// + /// Test asset + /// TestSolutionAsset for the copied asset in a temporary folder. + private TestSolutionAsset PrepareIsolatedTestAssets(TestSolutionAsset testAsset) + { + string sourceAssetDir = Path.Combine(_testAssetDir, testAsset.SolutionFolder); + + // Ensure source test asset exists + Directory.Exists(sourceAssetDir).ShouldBeTrue($"Test asset not found: {sourceAssetDir}."); + + // Create isolated copy of entire test asset directory structure + TransientTestFolder workFolder = _env.CreateFolder(createFolder: true); + + FileSystemUtilities.CopyFilesRecursively(sourceAssetDir, workFolder.Path); + + // Return TestSolutionAsset with temp folder and project file + return new TestSolutionAsset(workFolder.Path, testAsset.ProjectRelativePath); + } + + /// + /// Helper method to resolve TestSolutionAsset instances by name. + /// This is the easiest way to work around the limitation that [InlineData] cannot pass complex objects like TestSolutionAsset directly. + /// + private static TestSolutionAsset GetTestAssetByName(string testAssetName) + { + return testAssetName switch + { + nameof(TestSolutionAssetsFixture.SingleProject) => TestSolutionAssetsFixture.SingleProject, + nameof(TestSolutionAssetsFixture.ProjectWithDependencies) => TestSolutionAssetsFixture.ProjectWithDependencies, + _ => throw new ArgumentException($"Unknown test asset name: {testAssetName}", nameof(testAssetName)) + }; + } + /// /// Tests building projects with various multithreading flags. /// [Theory] - [InlineData(TestAssetsFixture.SingleProjectPath, "/m:1 /mt")] - [InlineData(TestAssetsFixture.SingleProjectPath, "/m:8 /mt")] - [InlineData(TestAssetsFixture.ProjectWithDependencies, "/m:1 /mt")] - [InlineData(TestAssetsFixture.ProjectWithDependencies, "/m:2 /mt")] - [InlineData(TestAssetsFixture.ProjectWithDependencies, "/m:8 /mt")] - public void MultithreadedBuild_Success(string projectRelativePath, string multithreadingArgs) + [InlineData(nameof(TestSolutionAssetsFixture.SingleProject), "/m:1 /mt")] + [InlineData(nameof(TestSolutionAssetsFixture.SingleProject), "/m:8 /mt")] + [InlineData(nameof(TestSolutionAssetsFixture.ProjectWithDependencies), "/m:1 /mt")] + [InlineData(nameof(TestSolutionAssetsFixture.ProjectWithDependencies), "/m:2 /mt")] + [InlineData(nameof(TestSolutionAssetsFixture.ProjectWithDependencies), "/m:8 /mt")] + public void MultithreadedBuild_Success(string testAssetName, string multithreadingArgs) { - string projectPath = Path.Combine(_testAssetDir, projectRelativePath); + // Resolve TestSolutionAsset from name + TestSolutionAsset testAsset = GetTestAssetByName(testAssetName); - // Ensure test asset exists - fail if missing - File.Exists(projectPath).ShouldBeTrue($"Test asset not found: {projectPath}."); + // Prepare isolated copy of test assets to ensure fresh builds + TestSolutionAsset isolatedAsset = PrepareIsolatedTestAssets(testAsset); string output = RunnerUtilities.ExecBootstrapedMSBuild( - $"\"{projectPath}\" {multithreadingArgs} {CommonMSBuildArgs}", + $"\"{isolatedAsset.ProjectPath}\" {multithreadingArgs} {CommonMSBuildArgs}", out bool success); - success.ShouldBeTrue($"Build failed with args '{multithreadingArgs}' for {projectRelativePath}. Output:\\n{output}"); + success.ShouldBeTrue($"Build failed with args '{multithreadingArgs}' for {testAsset.SolutionFolder}. Output:\\n{output}"); - _output.WriteLine($"Built {Path.GetFileNameWithoutExtension(projectRelativePath)} with arguments {multithreadingArgs}."); + _output.WriteLine($"Built {testAsset.SolutionFolder} with arguments {multithreadingArgs}."); } /// /// Tests binary logging with multithreaded builds and verifies replay functionality. /// [Theory] - [InlineData(TestAssetsFixture.SingleProjectPath, "/m:8 /mt")] - public void MultithreadedBuild_BinaryLogging(string projectRelativePath, string multithreadingArgs) + [InlineData(nameof(TestSolutionAssetsFixture.SingleProject), "/m:8 /mt")] + public void MultithreadedBuild_BinaryLogging(string testAssetName, string multithreadingArgs) { - string projectPath = Path.Combine(_testAssetDir, projectRelativePath); + // Resolve TestSolutionAsset from name + TestSolutionAsset testAsset = GetTestAssetByName(testAssetName); - // Ensure test asset exists - fail if missing - File.Exists(projectPath).ShouldBeTrue($"Test asset not found: {projectPath}."); - - var tempFolder = _env.CreateFolder(); - string binlogPath = Path.Combine(tempFolder.Path, "build.binlog"); + // Prepare isolated copy of test assets to ensure fresh builds + TestSolutionAsset isolatedAsset = PrepareIsolatedTestAssets(testAsset); + + string binlogPath = Path.Combine(isolatedAsset.SolutionFolder, "build.binlog"); - try - { - // Build with binary logging - string output = RunnerUtilities.ExecBootstrapedMSBuild( - $"\"{projectPath}\" {multithreadingArgs} /bl:\"{binlogPath}\" {CommonMSBuildArgs}", - out bool success); + // Build with binary logging + string output = RunnerUtilities.ExecBootstrapedMSBuild( + $"\"{isolatedAsset.ProjectPath}\" {multithreadingArgs} /bl:\"{binlogPath}\" {CommonMSBuildArgs}", + out bool success); - success.ShouldBeTrue($"Build failed with args '{multithreadingArgs}' for {projectRelativePath}. Output:\\n{output}."); - - // Verify binary log was created and has content - File.Exists(binlogPath).ShouldBeTrue("Binary log file was not created."); - - // Test binlog replay - string replayOutput = RunnerUtilities.ExecBootstrapedMSBuild($"\"{binlogPath}\" {CommonMSBuildArgs}", out bool replaySuccess); - - replaySuccess.ShouldBeTrue($"Binlog replay failed. Output:\\n{replayOutput}"); - - _output.WriteLine($"Built and replayed {Path.GetFileNameWithoutExtension(projectRelativePath)} with arguments {multithreadingArgs}."); - } - finally - { - if (File.Exists(binlogPath)) - { - File.Delete(binlogPath); - } - } + success.ShouldBeTrue($"Build failed with args '{multithreadingArgs}' for {testAsset.SolutionFolder}. Output:\\n{output}."); + + // Verify binary log was created and has content + File.Exists(binlogPath).ShouldBeTrue("Binary log file was not created."); + + // Test binlog replay + string replayOutput = RunnerUtilities.ExecBootstrapedMSBuild($"\"{binlogPath}\" {CommonMSBuildArgs}", out bool replaySuccess); + + replaySuccess.ShouldBeTrue($"Binlog replay failed. Output:\\n{replayOutput}"); + + _output.WriteLine($"Built and replayed {testAsset.SolutionFolder} with arguments {multithreadingArgs}."); } } } diff --git a/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs index 5ebc8dcedc5..7efc6ad7216 100644 --- a/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs +++ b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs @@ -10,44 +10,45 @@ namespace Microsoft.Build.EndToEndTests { /// - /// Fixture for test assets that handles expensive initialization like NuGet restore. + /// Fixture for test solution assets that handles expensive initialization like NuGet restore. /// - public class TestAssetsFixture : IDisposable + public class TestSolutionAssetsFixture : IDisposable { public string TestAssetDir { get; } - // Public constants for common project paths - public const string SingleProjectPath = "SingleProject\\SingleProject.csproj"; - public const string ProjectWithDependencies = "ProjectWithDependencies\\ConsoleApp\\ConsoleApp.csproj"; + // Test solution asset definitions + public static readonly TestSolutionAsset SingleProject = new("SingleProject", "SingleProject.csproj"); + public static readonly TestSolutionAsset ProjectWithDependencies = new("ProjectWithDependencies", "ConsoleApp\\ConsoleApp.csproj"); - private static readonly string[] ProjectsToRestore = + private static readonly TestSolutionAsset[] AssetsToRestore = [ - SingleProjectPath, + SingleProject, ProjectWithDependencies ]; - public TestAssetsFixture() + public TestSolutionAssetsFixture() { - TestAssetDir = Path.Combine(Path.GetDirectoryName(typeof(TestAssetsFixture).Assembly.Location) ?? AppContext.BaseDirectory, "TestAssets"); + TestAssetDir = Path.Combine(Path.GetDirectoryName(typeof(TestSolutionAssetsFixture).Assembly.Location) ?? AppContext.BaseDirectory, "TestAssets"); RestoreTestAssets(); } private void RestoreTestAssets() { - foreach (string projectPath in ProjectsToRestore) + foreach (var asset in AssetsToRestore) { - string fullPath = Path.Combine(TestAssetDir, projectPath); + string projectPath = Path.Combine(TestAssetDir, asset.ProjectPath); - if (File.Exists(fullPath)) + if (File.Exists(projectPath)) { - RunnerUtilities.ExecBootstrapedMSBuild($"\"{fullPath}\" /t:Restore /v:minimal", out bool success); + RunnerUtilities.ExecBootstrapedMSBuild($"\"{projectPath}\" /t:Restore /v:minimal", out bool success); if (!success) { - System.Diagnostics.Debug.WriteLine($"Warning: Failed to restore {fullPath}"); + System.Diagnostics.Debug.WriteLine($"Warning: Failed to restore {projectPath}"); } } } } + public void Dispose() { // Clean up if needed diff --git a/src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs b/src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs new file mode 100644 index 00000000000..03abe9fce67 --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; + +#nullable disable + +namespace Microsoft.Build.EndToEndTests +{ + /// + /// Represents a test solution asset. + /// + public readonly struct TestSolutionAsset + { + // Solution folder containing the test asset + public string SolutionFolder { get; } + + // Path to main (entry) project file relative to the solution folder + public string ProjectRelativePath { get; } + + public TestSolutionAsset(string solutionFolder, string projectFile) + { + SolutionFolder = solutionFolder; + ProjectRelativePath = projectFile; + } + + /// + /// Gets the full relative path from TestAssets root to the project file. + /// + public string ProjectPath => Path.Combine(SolutionFolder, ProjectRelativePath); + } +} \ No newline at end of file diff --git a/src/UnitTests.Shared/FileSystemUtilities.cs b/src/UnitTests.Shared/FileSystemUtilities.cs new file mode 100644 index 00000000000..b66bd353b08 --- /dev/null +++ b/src/UnitTests.Shared/FileSystemUtilities.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; + +#nullable disable + +namespace Microsoft.Build.UnitTests.Shared +{ + /// + /// File system utilities for unit tests. + /// + public static class FileSystemUtilities + { + /// + /// Recursively copies all files and directories from source to target path. + /// + /// Source directory path + /// Target directory path + public static void CopyFilesRecursively(string sourcePath, string targetPath) + { + // First Create all directories + foreach (string dirPath in Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories)) + { + Directory.CreateDirectory(dirPath.Replace(sourcePath, targetPath)); + } + + // Then copy all the files & Replaces any files with the same name + foreach (string newPath in Directory.GetFiles(sourcePath, "*", SearchOption.AllDirectories)) + { + File.Copy(newPath, newPath.Replace(sourcePath, targetPath), true); + } + } + } +} \ No newline at end of file From 29f2f92fcb4d106ca3d91c9666cf4abde51dd8ed Mon Sep 17 00:00:00 2001 From: Alina Mayorova <67507805+AR-May@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:26:07 +0100 Subject: [PATCH 06/16] Revert vs version change --- MSBuild.sln | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MSBuild.sln b/MSBuild.sln index 90c6df304ae..d5f7f3e7622 100644 --- a/MSBuild.sln +++ b/MSBuild.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 18 -VisualStudioVersion = 18.3.11305.148 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 17.0.31903.59 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4900B3B8-4310-4D5B-B1F7-2FDF9199765F}" ProjectSection(SolutionItems) = preProject From 2b18fc19d8dc0ed75b0c1de1839b528101fc8f0a Mon Sep 17 00:00:00 2001 From: Alina Mayorova <67507805+AR-May@users.noreply.github.com> Date: Mon, 12 Jan 2026 18:00:15 +0100 Subject: [PATCH 07/16] Fix target framework in tests --- .../ProjectWithDependencies/ConsoleApp/ConsoleApp.csproj | 2 +- .../TestAssets/ProjectWithDependencies/Library1/Library1.csproj | 2 +- .../TestAssets/ProjectWithDependencies/Library2/Library2.csproj | 2 +- .../TestAssets/ProjectWithDependencies/Library3/Library3.csproj | 2 +- .../TestAssets/ProjectWithDependencies/Library4/Library4.csproj | 2 +- .../TestAssets/SingleProject/SingleProject.csproj | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/ConsoleApp.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/ConsoleApp.csproj index 3b215f6b962..e9c22e44cdd 100644 --- a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/ConsoleApp.csproj +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/ConsoleApp.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net10.0 latest diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Library1.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Library1.csproj index 1586309c905..77cb2792044 100644 --- a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Library1.csproj +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Library1.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0 latest diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Library2.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Library2.csproj index 1586309c905..77cb2792044 100644 --- a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Library2.csproj +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Library2.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0 latest diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Library3.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Library3.csproj index 2f9f4165538..53502388eb3 100644 --- a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Library3.csproj +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Library3.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0 latest \ No newline at end of file diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Library4.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Library4.csproj index 2f9f4165538..53502388eb3 100644 --- a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Library4.csproj +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Library4.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0 latest \ No newline at end of file diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/SingleProject.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/SingleProject.csproj index c28018dbfdd..5c0a78df5ac 100644 --- a/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/SingleProject.csproj +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/SingleProject.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable From 23daaf8cd48dd6893d613bdde23ae9fa758cc5e5 Mon Sep 17 00:00:00 2001 From: Alina Mayorova <67507805+AR-May@users.noreply.github.com> Date: Fri, 16 Jan 2026 17:46:01 +0100 Subject: [PATCH 08/16] Test assets fixture should fail if restore fails. --- src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs index 7efc6ad7216..dd7007e0d37 100644 --- a/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs +++ b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs @@ -4,6 +4,7 @@ using System; using System.IO; using Microsoft.Build.UnitTests.Shared; +using Shouldly; #nullable disable @@ -38,14 +39,10 @@ private void RestoreTestAssets() { string projectPath = Path.Combine(TestAssetDir, asset.ProjectPath); - if (File.Exists(projectPath)) - { - RunnerUtilities.ExecBootstrapedMSBuild($"\"{projectPath}\" /t:Restore /v:minimal", out bool success); - if (!success) - { - System.Diagnostics.Debug.WriteLine($"Warning: Failed to restore {projectPath}"); - } - } + File.Exists(projectPath).ShouldBeTrue($"Test asset project not found: {projectPath}"); + + string output = RunnerUtilities.ExecBootstrapedMSBuild($"\"{projectPath}\" /t:Restore /v:minimal", out bool success); + success.ShouldBeTrue($"Failed to restore test asset {asset.SolutionFolder}\\{asset.ProjectRelativePath}. Output:\n{output}"); } } From 637bcfb7c66a2fd49680b341a0f745ac2366c333 Mon Sep 17 00:00:00 2001 From: Alina Mayorova <67507805+AR-May@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:50:05 +0100 Subject: [PATCH 09/16] Fix path separator - this will fix unix tests, --- src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs index dd7007e0d37..30099928f72 100644 --- a/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs +++ b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs @@ -19,7 +19,7 @@ public class TestSolutionAssetsFixture : IDisposable // Test solution asset definitions public static readonly TestSolutionAsset SingleProject = new("SingleProject", "SingleProject.csproj"); - public static readonly TestSolutionAsset ProjectWithDependencies = new("ProjectWithDependencies", "ConsoleApp\\ConsoleApp.csproj"); + public static readonly TestSolutionAsset ProjectWithDependencies = new("ProjectWithDependencies", "ConsoleApp/ConsoleApp.csproj"); private static readonly TestSolutionAsset[] AssetsToRestore = [ From b8d161fb42ba137522c49acc8f89ba80f7bdc148 Mon Sep 17 00:00:00 2001 From: Alina Mayorova <67507805+AR-May@users.noreply.github.com> Date: Tue, 20 Jan 2026 09:35:29 +0100 Subject: [PATCH 10/16] Increase timeout for the tests --- .../MultithreadedExecution_Tests.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs b/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs index afa94c2d9e2..b5db3e9ce51 100644 --- a/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs +++ b/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs @@ -26,6 +26,8 @@ public class MultithreadedExecution_Tests : IClassFixture Date: Tue, 21 Apr 2026 09:31:59 +0200 Subject: [PATCH 11/16] Add global.json to use bootstrap layout's SDK --- .../TestAssets/ProjectWithDependencies/global.json | 9 +++++++++ .../TestAssets/SingleProject/global.json | 9 +++++++++ 2 files changed, 18 insertions(+) create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/global.json create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/global.json diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/global.json b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/global.json new file mode 100644 index 00000000000..611555aea5f --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/global.json @@ -0,0 +1,9 @@ +{ + "sdk": { + // This global.json is needed to prevent builds running in tests using the bootstrap layout from walking + // up the repo tree and resolving our sdk.paths, instead of the bootstrap layout's SDK. + // See https://github.com/dotnet/runtime/issues/118488 for details. + "allowPrerelease": true, + "rollForward": "latestMajor" + } +} diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/global.json b/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/global.json new file mode 100644 index 00000000000..611555aea5f --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/global.json @@ -0,0 +1,9 @@ +{ + "sdk": { + // This global.json is needed to prevent builds running in tests using the bootstrap layout from walking + // up the repo tree and resolving our sdk.paths, instead of the bootstrap layout's SDK. + // See https://github.com/dotnet/runtime/issues/118488 for details. + "allowPrerelease": true, + "rollForward": "latestMajor" + } +} From 13e7d14a78b8611451c799bc5ce965e9a7b65475 Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Tue, 21 Apr 2026 11:31:46 +0200 Subject: [PATCH 12/16] Add non-sdk style tests --- .../MultithreadedExecution_Tests.cs | 63 +++++++++++++++++++ .../ConsoleApp/ConsoleApp.csproj | 16 +++++ .../ConsoleApp/Program.cs | 15 +++++ .../Library1/Class1.cs | 6 ++ .../Library1/Library1.csproj | 11 ++++ .../Library2/Class2.cs | 6 ++ .../Library2/Library2.csproj | 11 ++++ .../NonSdkSingleProject.csproj | 12 ++++ .../TestAssets/NonSdkSingleProject/Program.cs | 11 ++++ .../TestAssetsFixture.cs | 3 + 10 files changed, 154 insertions(+) create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/ConsoleApp/ConsoleApp.csproj create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/ConsoleApp/Program.cs create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library1/Class1.cs create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library1/Library1.csproj create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library2/Class2.cs create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library2/Library2.csproj create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkSingleProject/NonSdkSingleProject.csproj create mode 100644 src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkSingleProject/Program.cs diff --git a/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs b/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs index 939848070ce..a8154c680dc 100644 --- a/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs +++ b/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs @@ -77,6 +77,8 @@ private static TestSolutionAsset GetTestAssetByName(string testAssetName) { nameof(TestSolutionAssetsFixture.SingleProject) => TestSolutionAssetsFixture.SingleProject, nameof(TestSolutionAssetsFixture.ProjectWithDependencies) => TestSolutionAssetsFixture.ProjectWithDependencies, + nameof(TestSolutionAssetsFixture.NonSdkSingleProject) => TestSolutionAssetsFixture.NonSdkSingleProject, + nameof(TestSolutionAssetsFixture.NonSdkProjectWithDependencies) => TestSolutionAssetsFixture.NonSdkProjectWithDependencies, _ => throw new ArgumentException($"Unknown test asset name: {testAssetName}", nameof(testAssetName)) }; } @@ -144,5 +146,66 @@ public void MultithreadedBuild_BinaryLogging(string testAssetName, string multit _output.WriteLine($"Built and replayed {testAsset.SolutionFolder} with arguments {multithreadingArgs}."); } + + /// + /// Tests building non-SDK-style projects with multithreading flags. + /// + [WindowsOnlyTheory] + [InlineData(nameof(TestSolutionAssetsFixture.NonSdkSingleProject), "/m:1 /mt")] + [InlineData(nameof(TestSolutionAssetsFixture.NonSdkSingleProject), "/m:8 /mt")] + [InlineData(nameof(TestSolutionAssetsFixture.NonSdkProjectWithDependencies), "/m:1 /mt")] + [InlineData(nameof(TestSolutionAssetsFixture.NonSdkProjectWithDependencies), "/m:2 /mt")] + [InlineData(nameof(TestSolutionAssetsFixture.NonSdkProjectWithDependencies), "/m:8 /mt")] + public void MultithreadedBuild_NonSdkStyle_Success(string testAssetName, string multithreadingArgs) + { + TestSolutionAsset testAsset = GetTestAssetByName(testAssetName); + TestSolutionAsset isolatedAsset = PrepareIsolatedTestAssets(testAsset); + + string output = RunnerUtilities.ExecBootstrapedMSBuild( + $"\"{isolatedAsset.ProjectPath}\" {multithreadingArgs} {CommonMSBuildArgs}", + out bool success, + timeoutMilliseconds: _timeoutInMilliseconds); + + success.ShouldBeTrue($"Build failed with args '{multithreadingArgs}' for {testAsset.SolutionFolder}. Output:\\n{output}"); + + _output.WriteLine($"Built {testAsset.SolutionFolder} with arguments {multithreadingArgs}."); + } + + /// + /// Tests binary logging with non-sdk style multithreaded builds and verifies replay functionality. + /// + [WindowsOnlyTheory] + [InlineData(nameof(TestSolutionAssetsFixture.NonSdkSingleProject), "/m:8 /mt")] + public void MultithreadedBuild_NonSdkStyle_BinaryLogging(string testAssetName, string multithreadingArgs) + { + // Resolve TestSolutionAsset from name + TestSolutionAsset testAsset = GetTestAssetByName(testAssetName); + + // Prepare isolated copy of test assets to ensure fresh builds + TestSolutionAsset isolatedAsset = PrepareIsolatedTestAssets(testAsset); + + string binlogPath = Path.Combine(isolatedAsset.SolutionFolder, "build.binlog"); + + // Build with binary logging + string output = RunnerUtilities.ExecBootstrapedMSBuild( + $"\"{isolatedAsset.ProjectPath}\" {multithreadingArgs} /bl:\"{binlogPath}\" {CommonMSBuildArgs}", + out bool success, + timeoutMilliseconds: _timeoutInMilliseconds); + + success.ShouldBeTrue($"Build failed with args '{multithreadingArgs}' for {testAsset.SolutionFolder}. Output:\\n{output}."); + + // Verify binary log was created and has content + File.Exists(binlogPath).ShouldBeTrue("Binary log file was not created."); + + // Test binlog replay + string replayOutput = RunnerUtilities.ExecBootstrapedMSBuild( + $"\"{binlogPath}\" {CommonMSBuildArgs}", + out bool replaySuccess, + timeoutMilliseconds: _timeoutInMilliseconds); + + replaySuccess.ShouldBeTrue($"Binlog replay failed. Output:\\n{replayOutput}"); + + _output.WriteLine($"Built and replayed {testAsset.SolutionFolder} with arguments {multithreadingArgs}."); + } } } diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/ConsoleApp/ConsoleApp.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/ConsoleApp/ConsoleApp.csproj new file mode 100644 index 00000000000..56476a6a79c --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/ConsoleApp/ConsoleApp.csproj @@ -0,0 +1,16 @@ + + + + + Exe + v4.7.2 + + + + + + + + + + diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/ConsoleApp/Program.cs b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/ConsoleApp/Program.cs new file mode 100644 index 00000000000..c106ee47fe5 --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/ConsoleApp/Program.cs @@ -0,0 +1,15 @@ +using System; +using Library1; +using Library2; + +namespace NonSdkConsoleApp +{ + class Program + { + static void Main(string[] args) + { + var c1 = new Class1(); + var c2 = new Class2(); + } + } +} diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library1/Class1.cs b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library1/Class1.cs new file mode 100644 index 00000000000..13c5247c524 --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library1/Class1.cs @@ -0,0 +1,6 @@ +namespace Library1 +{ + public class Class1 + { + } +} diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library1/Library1.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library1/Library1.csproj new file mode 100644 index 00000000000..9b68e2f1f6c --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library1/Library1.csproj @@ -0,0 +1,11 @@ + + + + Library + v4.7.2 + + + + + + diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library2/Class2.cs b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library2/Class2.cs new file mode 100644 index 00000000000..72f3dba396e --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library2/Class2.cs @@ -0,0 +1,6 @@ +namespace Library2 +{ + public class Class2 + { + } +} diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library2/Library2.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library2/Library2.csproj new file mode 100644 index 00000000000..d51282c86c1 --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library2/Library2.csproj @@ -0,0 +1,11 @@ + + + + Library + v4.7.2 + + + + + + diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkSingleProject/NonSdkSingleProject.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkSingleProject/NonSdkSingleProject.csproj new file mode 100644 index 00000000000..d6a90b59173 --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkSingleProject/NonSdkSingleProject.csproj @@ -0,0 +1,12 @@ + + + + + Exe + v4.7.2 + + + + + + diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkSingleProject/Program.cs b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkSingleProject/Program.cs new file mode 100644 index 00000000000..5ab00dec12b --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkSingleProject/Program.cs @@ -0,0 +1,11 @@ +using System; + +namespace NonSdkSingleProject +{ + class Program + { + static void Main(string[] args) + { + } + } +} diff --git a/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs index 30099928f72..94895add9fb 100644 --- a/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs +++ b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs @@ -20,6 +20,8 @@ public class TestSolutionAssetsFixture : IDisposable // Test solution asset definitions public static readonly TestSolutionAsset SingleProject = new("SingleProject", "SingleProject.csproj"); public static readonly TestSolutionAsset ProjectWithDependencies = new("ProjectWithDependencies", "ConsoleApp/ConsoleApp.csproj"); + public static readonly TestSolutionAsset NonSdkSingleProject = new("NonSdkSingleProject", "NonSdkSingleProject.csproj"); + public static readonly TestSolutionAsset NonSdkProjectWithDependencies = new("NonSdkProjectWithDependencies", "ConsoleApp/ConsoleApp.csproj"); private static readonly TestSolutionAsset[] AssetsToRestore = [ @@ -27,6 +29,7 @@ public class TestSolutionAssetsFixture : IDisposable ProjectWithDependencies ]; + public TestSolutionAssetsFixture() { TestAssetDir = Path.Combine(Path.GetDirectoryName(typeof(TestSolutionAssetsFixture).Assembly.Location) ?? AppContext.BaseDirectory, "TestAssets"); From b69dafa48af8fc2d72cd2af495a89f336edd94e6 Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Tue, 21 Apr 2026 12:59:16 +0200 Subject: [PATCH 13/16] Clean up tests --- src/BuildCheck.UnitTests/EndToEndTests.cs | 7 +- .../MultithreadedExecution_Tests.cs | 118 +++++++----------- .../Library4/Class4.cs | 4 +- .../TestAssetsFixture.cs | 2 - .../TestSolutionAsset.cs | 2 - src/UnitTests.Shared/FileSystemUtilities.cs | 35 ------ 6 files changed, 53 insertions(+), 115 deletions(-) delete mode 100644 src/UnitTests.Shared/FileSystemUtilities.cs diff --git a/src/BuildCheck.UnitTests/EndToEndTests.cs b/src/BuildCheck.UnitTests/EndToEndTests.cs index 97dcdd4e432..542a1a2aefb 100644 --- a/src/BuildCheck.UnitTests/EndToEndTests.cs +++ b/src/BuildCheck.UnitTests/EndToEndTests.cs @@ -9,6 +9,7 @@ using System.Text.RegularExpressions; using System.Xml; using Microsoft.Build.Experimental.BuildCheck; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.Build.UnitTests; using Microsoft.Build.UnitTests.Shared; @@ -160,7 +161,7 @@ private EmbedResourceTestOutput RunEmbeddedResourceTest(string resourceXmlToAdd, const string templateToReplace = "###EmbeddedResourceToAdd"; TransientTestFolder workFolder = _env.CreateFolder(createFolder: true); - FileSystemUtilities.CopyFilesRecursively(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path); + FileUtilities.CopyDirectory(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path); ReplaceStringInFile(Path.Combine(workFolder.Path, referencedProjectName, $"{referencedProjectName}.csproj"), templateToReplace, resourceXmlToAdd); File.Copy( @@ -255,7 +256,7 @@ public void CopyToOutputTest(bool skipUnchangedDuringCopy) const string entryProjectName = "EntryProject"; TransientTestFolder workFolder = _env.CreateFolder(createFolder: true); - FileSystemUtilities.CopyFilesRecursively(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path); + FileUtilities.CopyDirectory(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path); _env.SetCurrentDirectory(Path.Combine(workFolder.Path, entryProjectName)); @@ -367,7 +368,7 @@ public void TFMConfusionCheckTest(string tfmString, string cliSuffix, bool shoul const string templateToReplace = "###TFM"; TransientTestFolder workFolder = _env.CreateFolder(createFolder: true); - FileSystemUtilities.CopyFilesRecursively(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path); + FileUtilities.CopyDirectory(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path); ReplaceStringInFile(Path.Combine(workFolder.Path, $"{projectName}.csproj"), templateToReplace, tfmString); diff --git a/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs b/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs index a8154c680dc..79923f4349e 100644 --- a/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs +++ b/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs @@ -5,15 +5,11 @@ using System.IO; using System.Linq; using Microsoft.Build.Framework; -using Microsoft.Build.Logging; -using Microsoft.Build.Shared; using Microsoft.Build.UnitTests; using Microsoft.Build.UnitTests.Shared; using Shouldly; using Xunit; -#nullable disable - namespace Microsoft.Build.EndToEndTests { /// @@ -61,7 +57,7 @@ private TestSolutionAsset PrepareIsolatedTestAssets(TestSolutionAsset testAsset) // Create isolated copy of entire test asset directory structure TransientTestFolder workFolder = _env.CreateFolder(createFolder: true); - FileSystemUtilities.CopyFilesRecursively(sourceAssetDir, workFolder.Path); + FileUtilities.CopyDirectory(sourceAssetDir, workFolder.Path); // Return TestSolutionAsset with temp folder and project file return new TestSolutionAsset(workFolder.Path, testAsset.ProjectRelativePath); @@ -84,128 +80,108 @@ private static TestSolutionAsset GetTestAssetByName(string testAssetName) } /// - /// Tests building projects with various multithreading flags. + /// Builds a test asset with the given MSBuild args and verifies success and output assemblies. /// - [Theory] - [InlineData(nameof(TestSolutionAssetsFixture.SingleProject), "/m:1 /mt")] - [InlineData(nameof(TestSolutionAssetsFixture.SingleProject), "/m:8 /mt")] - [InlineData(nameof(TestSolutionAssetsFixture.ProjectWithDependencies), "/m:1 /mt")] - [InlineData(nameof(TestSolutionAssetsFixture.ProjectWithDependencies), "/m:2 /mt")] - [InlineData(nameof(TestSolutionAssetsFixture.ProjectWithDependencies), "/m:8 /mt")] - public void MultithreadedBuild_Success(string testAssetName, string multithreadingArgs) + private void BuildAndVerify(string testAssetName, string multithreadingArgs) { // Resolve TestSolutionAsset from name TestSolutionAsset testAsset = GetTestAssetByName(testAssetName); - // Prepare isolated copy of test assets to ensure fresh builds TestSolutionAsset isolatedAsset = PrepareIsolatedTestAssets(testAsset); string output = RunnerUtilities.ExecBootstrapedMSBuild( - $"\"{isolatedAsset.ProjectPath}\" {multithreadingArgs} {CommonMSBuildArgs}", - out bool success, + $"\"{isolatedAsset.ProjectPath}\" {multithreadingArgs} {CommonMSBuildArgs}", + out bool success, timeoutMilliseconds: _timeoutInMilliseconds); - success.ShouldBeTrue($"Build failed with args '{multithreadingArgs}' for {testAsset.SolutionFolder}. Output:\\n{output}"); - + success.ShouldBeTrue($"Build failed with args '{multithreadingArgs}' for {testAsset.SolutionFolder}. Output:\n{output}"); + _output.WriteLine($"Built {testAsset.SolutionFolder} with arguments {multithreadingArgs}."); } /// - /// Tests binary logging with multithreaded builds and verifies replay functionality. + /// Tests building projects with various multithreading flags. /// [Theory] + [InlineData(nameof(TestSolutionAssetsFixture.SingleProject), "/m:1 /mt")] [InlineData(nameof(TestSolutionAssetsFixture.SingleProject), "/m:8 /mt")] - public void MultithreadedBuild_BinaryLogging(string testAssetName, string multithreadingArgs) + [InlineData(nameof(TestSolutionAssetsFixture.SingleProject), "/mt")] + [InlineData(nameof(TestSolutionAssetsFixture.ProjectWithDependencies), "/m:1 /mt")] + [InlineData(nameof(TestSolutionAssetsFixture.ProjectWithDependencies), "/m:2 /mt")] + [InlineData(nameof(TestSolutionAssetsFixture.ProjectWithDependencies), "/m:8 /mt")] + public void MultithreadedBuild_Success(string testAssetName, string multithreadingArgs) + { + BuildAndVerify(testAssetName, multithreadingArgs); + } + + /// + /// Builds a test asset with binary logging, then replays the binlog and verifies both succeed. + /// + private void BuildWithBinlogAndVerifyReplay(string testAssetName, string multithreadingArgs) { // Resolve TestSolutionAsset from name TestSolutionAsset testAsset = GetTestAssetByName(testAssetName); // Prepare isolated copy of test assets to ensure fresh builds TestSolutionAsset isolatedAsset = PrepareIsolatedTestAssets(testAsset); - + string binlogPath = Path.Combine(isolatedAsset.SolutionFolder, "build.binlog"); // Build with binary logging string output = RunnerUtilities.ExecBootstrapedMSBuild( - $"\"{isolatedAsset.ProjectPath}\" {multithreadingArgs} /bl:\"{binlogPath}\" {CommonMSBuildArgs}", - out bool success, + $"\"{isolatedAsset.ProjectPath}\" {multithreadingArgs} /bl:\"{binlogPath}\" {CommonMSBuildArgs}", + out bool success, timeoutMilliseconds: _timeoutInMilliseconds); - success.ShouldBeTrue($"Build failed with args '{multithreadingArgs}' for {testAsset.SolutionFolder}. Output:\\n{output}."); - + success.ShouldBeTrue($"Build failed with args '{multithreadingArgs}' for {testAsset.SolutionFolder}. Output:\n{output}."); + // Verify binary log was created and has content File.Exists(binlogPath).ShouldBeTrue("Binary log file was not created."); - + // Test binlog replay string replayOutput = RunnerUtilities.ExecBootstrapedMSBuild( - $"\"{binlogPath}\" {CommonMSBuildArgs}", - out bool replaySuccess, + $"\"{binlogPath}\" {CommonMSBuildArgs}", + out bool replaySuccess, timeoutMilliseconds: _timeoutInMilliseconds); - - replaySuccess.ShouldBeTrue($"Binlog replay failed. Output:\\n{replayOutput}"); - + + replaySuccess.ShouldBeTrue($"Binlog replay failed. Output:\n{replayOutput}"); + _output.WriteLine($"Built and replayed {testAsset.SolutionFolder} with arguments {multithreadingArgs}."); } + /// + /// Tests binary logging with multithreaded builds and verifies replay functionality. + /// + [Theory] + [InlineData(nameof(TestSolutionAssetsFixture.SingleProject), "/m:8 /mt")] + public void MultithreadedBuild_BinaryLogging(string testAssetName, string multithreadingArgs) + { + BuildWithBinlogAndVerifyReplay(testAssetName, multithreadingArgs); + } + /// /// Tests building non-SDK-style projects with multithreading flags. /// [WindowsOnlyTheory] [InlineData(nameof(TestSolutionAssetsFixture.NonSdkSingleProject), "/m:1 /mt")] [InlineData(nameof(TestSolutionAssetsFixture.NonSdkSingleProject), "/m:8 /mt")] + [InlineData(nameof(TestSolutionAssetsFixture.NonSdkSingleProject), "/mt")] [InlineData(nameof(TestSolutionAssetsFixture.NonSdkProjectWithDependencies), "/m:1 /mt")] [InlineData(nameof(TestSolutionAssetsFixture.NonSdkProjectWithDependencies), "/m:2 /mt")] [InlineData(nameof(TestSolutionAssetsFixture.NonSdkProjectWithDependencies), "/m:8 /mt")] public void MultithreadedBuild_NonSdkStyle_Success(string testAssetName, string multithreadingArgs) { - TestSolutionAsset testAsset = GetTestAssetByName(testAssetName); - TestSolutionAsset isolatedAsset = PrepareIsolatedTestAssets(testAsset); - - string output = RunnerUtilities.ExecBootstrapedMSBuild( - $"\"{isolatedAsset.ProjectPath}\" {multithreadingArgs} {CommonMSBuildArgs}", - out bool success, - timeoutMilliseconds: _timeoutInMilliseconds); - - success.ShouldBeTrue($"Build failed with args '{multithreadingArgs}' for {testAsset.SolutionFolder}. Output:\\n{output}"); - - _output.WriteLine($"Built {testAsset.SolutionFolder} with arguments {multithreadingArgs}."); + BuildAndVerify(testAssetName, multithreadingArgs); } /// - /// Tests binary logging with non-sdk style multithreaded builds and verifies replay functionality. + /// Tests binary logging with non-SDK-style multithreaded builds and verifies replay functionality. /// [WindowsOnlyTheory] [InlineData(nameof(TestSolutionAssetsFixture.NonSdkSingleProject), "/m:8 /mt")] public void MultithreadedBuild_NonSdkStyle_BinaryLogging(string testAssetName, string multithreadingArgs) { - // Resolve TestSolutionAsset from name - TestSolutionAsset testAsset = GetTestAssetByName(testAssetName); - - // Prepare isolated copy of test assets to ensure fresh builds - TestSolutionAsset isolatedAsset = PrepareIsolatedTestAssets(testAsset); - - string binlogPath = Path.Combine(isolatedAsset.SolutionFolder, "build.binlog"); - - // Build with binary logging - string output = RunnerUtilities.ExecBootstrapedMSBuild( - $"\"{isolatedAsset.ProjectPath}\" {multithreadingArgs} /bl:\"{binlogPath}\" {CommonMSBuildArgs}", - out bool success, - timeoutMilliseconds: _timeoutInMilliseconds); - - success.ShouldBeTrue($"Build failed with args '{multithreadingArgs}' for {testAsset.SolutionFolder}. Output:\\n{output}."); - - // Verify binary log was created and has content - File.Exists(binlogPath).ShouldBeTrue("Binary log file was not created."); - - // Test binlog replay - string replayOutput = RunnerUtilities.ExecBootstrapedMSBuild( - $"\"{binlogPath}\" {CommonMSBuildArgs}", - out bool replaySuccess, - timeoutMilliseconds: _timeoutInMilliseconds); - - replaySuccess.ShouldBeTrue($"Binlog replay failed. Output:\\n{replayOutput}"); - - _output.WriteLine($"Built and replayed {testAsset.SolutionFolder} with arguments {multithreadingArgs}."); + BuildWithBinlogAndVerifyReplay(testAssetName, multithreadingArgs); } } } diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Class4.cs b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Class4.cs index 50a703c482b..61e843a9623 100644 --- a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Class4.cs +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Class4.cs @@ -1,6 +1,6 @@ -namespace Library3 +namespace Library4 { - public class Class3 + public class Class4 { } diff --git a/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs index 94895add9fb..a704434ebe6 100644 --- a/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs +++ b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs @@ -6,8 +6,6 @@ using Microsoft.Build.UnitTests.Shared; using Shouldly; -#nullable disable - namespace Microsoft.Build.EndToEndTests { /// diff --git a/src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs b/src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs index 03abe9fce67..11821cee43c 100644 --- a/src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs +++ b/src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs @@ -3,8 +3,6 @@ using System.IO; -#nullable disable - namespace Microsoft.Build.EndToEndTests { /// diff --git a/src/UnitTests.Shared/FileSystemUtilities.cs b/src/UnitTests.Shared/FileSystemUtilities.cs deleted file mode 100644 index b66bd353b08..00000000000 --- a/src/UnitTests.Shared/FileSystemUtilities.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.IO; - -#nullable disable - -namespace Microsoft.Build.UnitTests.Shared -{ - /// - /// File system utilities for unit tests. - /// - public static class FileSystemUtilities - { - /// - /// Recursively copies all files and directories from source to target path. - /// - /// Source directory path - /// Target directory path - public static void CopyFilesRecursively(string sourcePath, string targetPath) - { - // First Create all directories - foreach (string dirPath in Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories)) - { - Directory.CreateDirectory(dirPath.Replace(sourcePath, targetPath)); - } - - // Then copy all the files & Replaces any files with the same name - foreach (string newPath in Directory.GetFiles(sourcePath, "*", SearchOption.AllDirectories)) - { - File.Copy(newPath, newPath.Replace(sourcePath, targetPath), true); - } - } - } -} \ No newline at end of file From 7e330694d40891152392342ec791b45009baae48 Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Thu, 23 Apr 2026 13:08:18 +0200 Subject: [PATCH 14/16] Address review feedback: - Rename csproj from Microsoft.Build.CommandLine.EndToEnd.Tests to Microsoft.Build.EndToEnd.Tests - Delete InternalsVisibleTo in Build and MSBuild assemblies - Increase restore timeout to 120s to reduce CI flakiness - Route fixture output via IMessageSink for diagnostics - Remove unnecessary IDisposable from TestSolutionAssetsFixture - Fix inaccurate XML doc comments on BuildAndVerify and ProjectPath - Add binlog non-empty assertion - Increase test timeout to 180s --- MSBuild.slnx | 2 +- pr-description.md | 26 +++++++++++++++++++ src/Build/AssemblyInfo.cs | 1 - src/Framework/Properties/AssemblyInfo.cs | 2 +- ... => Microsoft.Build.EndToEnd.Tests.csproj} | 0 .../MultithreadedExecution_Tests.cs | 5 ++-- .../TestAssetsFixture.cs | 25 ++++++++++-------- .../TestSolutionAsset.cs | 3 ++- src/MSBuild/AssemblyInfo.cs | 1 - 9 files changed, 47 insertions(+), 18 deletions(-) create mode 100644 pr-description.md rename src/MSBuild.EndToEnd.Tests/{Microsoft.Build.CommandLine.EndToEnd.Tests.csproj => Microsoft.Build.EndToEnd.Tests.csproj} (100%) diff --git a/MSBuild.slnx b/MSBuild.slnx index b5ff8a1ebc6..674708cf91d 100644 --- a/MSBuild.slnx +++ b/MSBuild.slnx @@ -74,7 +74,7 @@ - + diff --git a/pr-description.md b/pr-description.md new file mode 100644 index 00000000000..ccddb603e63 --- /dev/null +++ b/pr-description.md @@ -0,0 +1,26 @@ +Fixes #12640 + +### Context + +End-to-end tests for multithreaded (`/mt`) MSBuild execution were missing. The existing test coverage only had unit-level tests for task routing but no E2E validation of building real projects with `/mt`. + +### Changes Made + +- Added new `MSBuild.EndToEnd.Tests` test project with SDK-style and non-SDK-style test assets. +- SDK assets: single console app and multi-project solution (ConsoleApp + 4 libraries). +- Non-SDK assets: single `.NET Framework 4.7.2` project and multi-project solution (ConsoleApp + 2 libraries), Windows-only. +- Tests cover `/m:1 /mt`, `/m:2 /mt`, `/m:8 /mt`, and `/mt` alone. +- Binlog build + replay tests for both SDK and non-SDK. +- Added `global.json` to SDK test assets to prevent bootstrap SDK resolution hijacking (see dotnet/runtime#118488). +- Added `InternalsVisibleTo` for the new test assembly in `Microsoft.Build`, `Microsoft.Build.Framework`, and `MSBuild`. +- Added project to `MSBuild.slnx`. +- Replaced local `CopyFilesRecursively` with existing `FileUtilities.CopyDirectory`. +- Fixed copy-paste bug in `Library4/Class4.cs`. + +### Testing + +End-to-end tests in `MultithreadedExecution_Tests.cs`: +- `MultithreadedBuild_Success` — SDK-style builds with various `/mt` combinations. +- `MultithreadedBuild_BinaryLogging` — SDK-style build with binlog + replay. +- `MultithreadedBuild_NonSdkStyle_Success` — non-SDK builds (Windows-only). +- `MultithreadedBuild_NonSdkStyle_BinaryLogging` — non-SDK binlog + replay (Windows-only). diff --git a/src/Build/AssemblyInfo.cs b/src/Build/AssemblyInfo.cs index 195f095e990..8cf576e4636 100644 --- a/src/Build/AssemblyInfo.cs +++ b/src/Build/AssemblyInfo.cs @@ -23,7 +23,6 @@ [assembly: InternalsVisibleTo("Microsoft.Build.BuildCheck.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.UnitTests.Shared, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.UnitTests.Shared, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] -[assembly: InternalsVisibleTo("Microsoft.Build.CommandLine.EndToEnd.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.Tasks.Cop, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.BuildCheck.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] // DO NOT expose Internals to "Microsoft.Build.UnitTests.OM.OrcasCompatibility" as this assembly is supposed to only see public interface diff --git a/src/Framework/Properties/AssemblyInfo.cs b/src/Framework/Properties/AssemblyInfo.cs index f571187cb34..b05a7c23e88 100644 --- a/src/Framework/Properties/AssemblyInfo.cs +++ b/src/Framework/Properties/AssemblyInfo.cs @@ -52,7 +52,7 @@ [assembly: InternalsVisibleTo("Microsoft.Build.Engine.OM.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.Utilities.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.CommandLine.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] -[assembly: InternalsVisibleTo("Microsoft.Build.CommandLine.EndToEnd.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] +[assembly: InternalsVisibleTo("Microsoft.Build.EndToEnd.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.Tasks.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.UnitTests.Shared, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] diff --git a/src/MSBuild.EndToEnd.Tests/Microsoft.Build.CommandLine.EndToEnd.Tests.csproj b/src/MSBuild.EndToEnd.Tests/Microsoft.Build.EndToEnd.Tests.csproj similarity index 100% rename from src/MSBuild.EndToEnd.Tests/Microsoft.Build.CommandLine.EndToEnd.Tests.csproj rename to src/MSBuild.EndToEnd.Tests/Microsoft.Build.EndToEnd.Tests.csproj diff --git a/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs b/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs index 79923f4349e..fd0240a80f6 100644 --- a/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs +++ b/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs @@ -21,7 +21,7 @@ public class MultithreadedExecution_Tests : IClassFixture - /// Builds a test asset with the given MSBuild args and verifies success and output assemblies. + /// Builds a test asset with the given MSBuild args and verifies success. /// private void BuildAndVerify(string testAssetName, string multithreadingArgs) { @@ -137,6 +137,7 @@ private void BuildWithBinlogAndVerifyReplay(string testAssetName, string multith // Verify binary log was created and has content File.Exists(binlogPath).ShouldBeTrue("Binary log file was not created."); + new FileInfo(binlogPath).Length.ShouldBeGreaterThan(0, "Binary log file was created but is empty."); // Test binlog replay string replayOutput = RunnerUtilities.ExecBootstrapedMSBuild( diff --git a/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs index a704434ebe6..3437242757f 100644 --- a/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs +++ b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs @@ -5,13 +5,17 @@ using System.IO; using Microsoft.Build.UnitTests.Shared; using Shouldly; +using Xunit.Sdk; +using Xunit.v3; namespace Microsoft.Build.EndToEndTests { /// /// Fixture for test solution assets that handles expensive initialization like NuGet restore. + /// Restore uses bootstrap MSBuild so it exercises the same code path as the tests themselves, + /// which is important when restore-related tasks become multithreadable. /// - public class TestSolutionAssetsFixture : IDisposable + public class TestSolutionAssetsFixture { public string TestAssetDir { get; } @@ -28,28 +32,27 @@ public class TestSolutionAssetsFixture : IDisposable ]; - public TestSolutionAssetsFixture() + public TestSolutionAssetsFixture(IMessageSink messageSink) { TestAssetDir = Path.Combine(Path.GetDirectoryName(typeof(TestSolutionAssetsFixture).Assembly.Location) ?? AppContext.BaseDirectory, "TestAssets"); - RestoreTestAssets(); + RestoreTestAssets(messageSink); } - private void RestoreTestAssets() + private void RestoreTestAssets(IMessageSink messageSink) { foreach (var asset in AssetsToRestore) { string projectPath = Path.Combine(TestAssetDir, asset.ProjectPath); File.Exists(projectPath).ShouldBeTrue($"Test asset project not found: {projectPath}"); - - string output = RunnerUtilities.ExecBootstrapedMSBuild($"\"{projectPath}\" /t:Restore /v:minimal", out bool success); + + messageSink.OnMessage(new DiagnosticMessage($"Started restoring test asset: {asset.ProjectPath}")); + + string output = RunnerUtilities.ExecBootstrapedMSBuild($"\"{projectPath}\" /t:Restore /v:minimal", out bool success, timeoutMilliseconds: 120_000); success.ShouldBeTrue($"Failed to restore test asset {asset.SolutionFolder}\\{asset.ProjectRelativePath}. Output:\n{output}"); - } - } - public void Dispose() - { - // Clean up if needed + messageSink.OnMessage(new DiagnosticMessage($"Finished restoring test asset: {asset.ProjectPath}")); + } } } } diff --git a/src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs b/src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs index 11821cee43c..d3306369310 100644 --- a/src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs +++ b/src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs @@ -23,7 +23,8 @@ public TestSolutionAsset(string solutionFolder, string projectFile) } /// - /// Gets the full relative path from TestAssets root to the project file. + /// Gets the path to the project file. This is relative when used as a test asset definition, + /// or absolute when used as an isolated test instance (after PrepareIsolatedTestAssets). /// public string ProjectPath => Path.Combine(SolutionFolder, ProjectRelativePath); } diff --git a/src/MSBuild/AssemblyInfo.cs b/src/MSBuild/AssemblyInfo.cs index 0a084fddeb1..c0407dd5a2d 100644 --- a/src/MSBuild/AssemblyInfo.cs +++ b/src/MSBuild/AssemblyInfo.cs @@ -8,7 +8,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -[assembly: InternalsVisibleTo("Microsoft.Build.CommandLine.EndToEnd.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.CommandLine.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.Utilities.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] From 2f040b0c4735affc493b3206d11d20729eb052f1 Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Thu, 23 Apr 2026 13:17:24 +0200 Subject: [PATCH 15/16] Remove IMessageSink --- src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs index 3437242757f..99247ebc4d2 100644 --- a/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs +++ b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs @@ -5,8 +5,6 @@ using System.IO; using Microsoft.Build.UnitTests.Shared; using Shouldly; -using Xunit.Sdk; -using Xunit.v3; namespace Microsoft.Build.EndToEndTests { @@ -32,13 +30,13 @@ public class TestSolutionAssetsFixture ]; - public TestSolutionAssetsFixture(IMessageSink messageSink) + public TestSolutionAssetsFixture() { TestAssetDir = Path.Combine(Path.GetDirectoryName(typeof(TestSolutionAssetsFixture).Assembly.Location) ?? AppContext.BaseDirectory, "TestAssets"); - RestoreTestAssets(messageSink); + RestoreTestAssets(); } - private void RestoreTestAssets(IMessageSink messageSink) + private void RestoreTestAssets() { foreach (var asset in AssetsToRestore) { @@ -46,12 +44,8 @@ private void RestoreTestAssets(IMessageSink messageSink) File.Exists(projectPath).ShouldBeTrue($"Test asset project not found: {projectPath}"); - messageSink.OnMessage(new DiagnosticMessage($"Started restoring test asset: {asset.ProjectPath}")); - string output = RunnerUtilities.ExecBootstrapedMSBuild($"\"{projectPath}\" /t:Restore /v:minimal", out bool success, timeoutMilliseconds: 120_000); success.ShouldBeTrue($"Failed to restore test asset {asset.SolutionFolder}\\{asset.ProjectRelativePath}. Output:\n{output}"); - - messageSink.OnMessage(new DiagnosticMessage($"Finished restoring test asset: {asset.ProjectPath}")); } } } From 199b07dbc82a3fb90e7b53f658045440d6ddd7cd Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Tue, 28 Apr 2026 14:36:52 +0200 Subject: [PATCH 16/16] Remove pr-description.md file Make TestSolutionAsset and TestAssetsFixture internal Improve comments in TestAssetsFixture Add project with dependencies to binaryLogging tests. --- pr-description.md | 26 ------------------- .../MultithreadedExecution_Tests.cs | 2 ++ .../TestAssetsFixture.cs | 15 ++++++----- .../TestSolutionAsset.cs | 16 ++++++------ 4 files changed, 18 insertions(+), 41 deletions(-) delete mode 100644 pr-description.md diff --git a/pr-description.md b/pr-description.md deleted file mode 100644 index ccddb603e63..00000000000 --- a/pr-description.md +++ /dev/null @@ -1,26 +0,0 @@ -Fixes #12640 - -### Context - -End-to-end tests for multithreaded (`/mt`) MSBuild execution were missing. The existing test coverage only had unit-level tests for task routing but no E2E validation of building real projects with `/mt`. - -### Changes Made - -- Added new `MSBuild.EndToEnd.Tests` test project with SDK-style and non-SDK-style test assets. -- SDK assets: single console app and multi-project solution (ConsoleApp + 4 libraries). -- Non-SDK assets: single `.NET Framework 4.7.2` project and multi-project solution (ConsoleApp + 2 libraries), Windows-only. -- Tests cover `/m:1 /mt`, `/m:2 /mt`, `/m:8 /mt`, and `/mt` alone. -- Binlog build + replay tests for both SDK and non-SDK. -- Added `global.json` to SDK test assets to prevent bootstrap SDK resolution hijacking (see dotnet/runtime#118488). -- Added `InternalsVisibleTo` for the new test assembly in `Microsoft.Build`, `Microsoft.Build.Framework`, and `MSBuild`. -- Added project to `MSBuild.slnx`. -- Replaced local `CopyFilesRecursively` with existing `FileUtilities.CopyDirectory`. -- Fixed copy-paste bug in `Library4/Class4.cs`. - -### Testing - -End-to-end tests in `MultithreadedExecution_Tests.cs`: -- `MultithreadedBuild_Success` — SDK-style builds with various `/mt` combinations. -- `MultithreadedBuild_BinaryLogging` — SDK-style build with binlog + replay. -- `MultithreadedBuild_NonSdkStyle_Success` — non-SDK builds (Windows-only). -- `MultithreadedBuild_NonSdkStyle_BinaryLogging` — non-SDK binlog + replay (Windows-only). diff --git a/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs b/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs index fd0240a80f6..53fde409436 100644 --- a/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs +++ b/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs @@ -155,6 +155,7 @@ private void BuildWithBinlogAndVerifyReplay(string testAssetName, string multith /// [Theory] [InlineData(nameof(TestSolutionAssetsFixture.SingleProject), "/m:8 /mt")] + [InlineData(nameof(TestSolutionAssetsFixture.ProjectWithDependencies), "/m:8 /mt")] public void MultithreadedBuild_BinaryLogging(string testAssetName, string multithreadingArgs) { BuildWithBinlogAndVerifyReplay(testAssetName, multithreadingArgs); @@ -180,6 +181,7 @@ public void MultithreadedBuild_NonSdkStyle_Success(string testAssetName, string /// [WindowsOnlyTheory] [InlineData(nameof(TestSolutionAssetsFixture.NonSdkSingleProject), "/m:8 /mt")] + [InlineData(nameof(TestSolutionAssetsFixture.NonSdkProjectWithDependencies), "/m:8 /mt")] public void MultithreadedBuild_NonSdkStyle_BinaryLogging(string testAssetName, string multithreadingArgs) { BuildWithBinlogAndVerifyReplay(testAssetName, multithreadingArgs); diff --git a/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs index 99247ebc4d2..c5bd822538d 100644 --- a/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs +++ b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs @@ -10,19 +10,20 @@ namespace Microsoft.Build.EndToEndTests { /// /// Fixture for test solution assets that handles expensive initialization like NuGet restore. - /// Restore uses bootstrap MSBuild so it exercises the same code path as the tests themselves, - /// which is important when restore-related tasks become multithreadable. + /// Restore runs through bootstrap MSBuild, so failures here can surface real regressions + /// in the same code paths that the tests exercise. /// public class TestSolutionAssetsFixture { - public string TestAssetDir { get; } + internal string TestAssetDir { get; } // Test solution asset definitions - public static readonly TestSolutionAsset SingleProject = new("SingleProject", "SingleProject.csproj"); - public static readonly TestSolutionAsset ProjectWithDependencies = new("ProjectWithDependencies", "ConsoleApp/ConsoleApp.csproj"); - public static readonly TestSolutionAsset NonSdkSingleProject = new("NonSdkSingleProject", "NonSdkSingleProject.csproj"); - public static readonly TestSolutionAsset NonSdkProjectWithDependencies = new("NonSdkProjectWithDependencies", "ConsoleApp/ConsoleApp.csproj"); + internal static readonly TestSolutionAsset SingleProject = new("SingleProject", "SingleProject.csproj"); + internal static readonly TestSolutionAsset ProjectWithDependencies = new("ProjectWithDependencies", "ConsoleApp/ConsoleApp.csproj"); + internal static readonly TestSolutionAsset NonSdkSingleProject = new("NonSdkSingleProject", "NonSdkSingleProject.csproj"); + internal static readonly TestSolutionAsset NonSdkProjectWithDependencies = new("NonSdkProjectWithDependencies", "ConsoleApp/ConsoleApp.csproj"); + // Non-SDK projects do not require NuGet restore. private static readonly TestSolutionAsset[] AssetsToRestore = [ SingleProject, diff --git a/src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs b/src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs index d3306369310..f7909df7544 100644 --- a/src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs +++ b/src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs @@ -8,24 +8,24 @@ namespace Microsoft.Build.EndToEndTests /// /// Represents a test solution asset. /// - public readonly struct TestSolutionAsset + internal readonly struct TestSolutionAsset { // Solution folder containing the test asset - public string SolutionFolder { get; } + internal string SolutionFolder { get; } // Path to main (entry) project file relative to the solution folder - public string ProjectRelativePath { get; } - - public TestSolutionAsset(string solutionFolder, string projectFile) + internal string ProjectRelativePath { get; } + + internal TestSolutionAsset(string solutionFolder, string projectFile) { SolutionFolder = solutionFolder; ProjectRelativePath = projectFile; } - + /// /// Gets the path to the project file. This is relative when used as a test asset definition, /// or absolute when used as an isolated test instance (after PrepareIsolatedTestAssets). /// - public string ProjectPath => Path.Combine(SolutionFolder, ProjectRelativePath); + internal string ProjectPath => Path.Combine(SolutionFolder, ProjectRelativePath); } -} \ No newline at end of file +}