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
+}