diff --git a/MSBuild.slnx b/MSBuild.slnx index d807edb08b4..674708cf91d 100644 --- a/MSBuild.slnx +++ b/MSBuild.slnx @@ -74,6 +74,9 @@ + + + diff --git a/src/BuildCheck.UnitTests/EndToEndTests.cs b/src/BuildCheck.UnitTests/EndToEndTests.cs index 646dc27984f..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); - 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( @@ -196,21 +197,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\)"); @@ -270,7 +256,7 @@ public void CopyToOutputTest(bool skipUnchangedDuringCopy) const string entryProjectName = "EntryProject"; TransientTestFolder workFolder = _env.CreateFolder(createFolder: true); - CopyFilesRecursively(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path); + FileUtilities.CopyDirectory(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path); _env.SetCurrentDirectory(Path.Combine(workFolder.Path, entryProjectName)); @@ -382,7 +368,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); + FileUtilities.CopyDirectory(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path); ReplaceStringInFile(Path.Combine(workFolder.Path, $"{projectName}.csproj"), templateToReplace, tfmString); diff --git a/src/Framework/Properties/AssemblyInfo.cs b/src/Framework/Properties/AssemblyInfo.cs index 7473cdc18aa..b05a7c23e88 100644 --- a/src/Framework/Properties/AssemblyInfo.cs +++ b/src/Framework/Properties/AssemblyInfo.cs @@ -52,6 +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.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.EndToEnd.Tests.csproj b/src/MSBuild.EndToEnd.Tests/Microsoft.Build.EndToEnd.Tests.csproj new file mode 100644 index 00000000000..ba5daca9122 --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/Microsoft.Build.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..53fde409436 --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs @@ -0,0 +1,190 @@ +// 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.UnitTests; +using Microsoft.Build.UnitTests.Shared; +using Shouldly; +using Xunit; + +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; + + private readonly int _timeoutInMilliseconds = 180_000; + + // 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, TestSolutionAssetsFixture testAssetFixture) + { + _output = output; + _env = TestEnvironment.Create(output); + _testAssetDir = testAssetFixture.TestAssetDir; + } + + 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); + + FileUtilities.CopyDirectory(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, + nameof(TestSolutionAssetsFixture.NonSdkSingleProject) => TestSolutionAssetsFixture.NonSdkSingleProject, + nameof(TestSolutionAssetsFixture.NonSdkProjectWithDependencies) => TestSolutionAssetsFixture.NonSdkProjectWithDependencies, + _ => throw new ArgumentException($"Unknown test asset name: {testAssetName}", nameof(testAssetName)) + }; + } + + /// + /// Builds a test asset with the given MSBuild args and verifies success. + /// + 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, + timeoutMilliseconds: _timeoutInMilliseconds); + + success.ShouldBeTrue($"Build failed with args '{multithreadingArgs}' for {testAsset.SolutionFolder}. Output:\n{output}"); + + _output.WriteLine($"Built {testAsset.SolutionFolder} with arguments {multithreadingArgs}."); + } + + /// + /// Tests building projects with various multithreading flags. + /// + [Theory] + [InlineData(nameof(TestSolutionAssetsFixture.SingleProject), "/m:1 /mt")] + [InlineData(nameof(TestSolutionAssetsFixture.SingleProject), "/m:8 /mt")] + [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, + 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."); + new FileInfo(binlogPath).Length.ShouldBeGreaterThan(0, "Binary log file was created but is empty."); + + // 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}."); + } + + /// + /// Tests binary logging with multithreaded builds and verifies replay functionality. + /// + [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); + } + + /// + /// 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) + { + BuildAndVerify(testAssetName, multithreadingArgs); + } + + /// + /// Tests binary logging with non-SDK-style multithreaded builds and verifies replay functionality. + /// + [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/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/TestAssets/ProjectWithDependencies/ConsoleApp/ConsoleApp.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/ConsoleApp.csproj new file mode 100644 index 00000000000..e9c22e44cdd --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/ConsoleApp.csproj @@ -0,0 +1,11 @@ + + + Exe + net10.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..77cb2792044 --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Library1.csproj @@ -0,0 +1,11 @@ + + + net10.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..77cb2792044 --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Library2.csproj @@ -0,0 +1,11 @@ + + + net10.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..53502388eb3 --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Library3.csproj @@ -0,0 +1,6 @@ + + + net10.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..61e843a9623 --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Class4.cs @@ -0,0 +1,7 @@ +namespace Library4 +{ + public class Class4 + { + + } +} \ 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..53502388eb3 --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Library4.csproj @@ -0,0 +1,6 @@ + + + net10.0 + latest + + \ No newline at end of file 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/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..5c0a78df5ac --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/SingleProject.csproj @@ -0,0 +1,10 @@ + + + + Exe + net10.0 + enable + enable + + + \ No newline at end of file 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" + } +} diff --git a/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs new file mode 100644 index 00000000000..c5bd822538d --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs @@ -0,0 +1,53 @@ +// 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; +using Shouldly; + +namespace Microsoft.Build.EndToEndTests +{ + /// + /// Fixture for test solution assets that handles expensive initialization like NuGet restore. + /// Restore runs through bootstrap MSBuild, so failures here can surface real regressions + /// in the same code paths that the tests exercise. + /// + public class TestSolutionAssetsFixture + { + internal string TestAssetDir { get; } + + // Test solution asset definitions + 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, + ProjectWithDependencies + ]; + + + public TestSolutionAssetsFixture() + { + TestAssetDir = Path.Combine(Path.GetDirectoryName(typeof(TestSolutionAssetsFixture).Assembly.Location) ?? AppContext.BaseDirectory, "TestAssets"); + RestoreTestAssets(); + } + + private void RestoreTestAssets() + { + 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, timeoutMilliseconds: 120_000); + success.ShouldBeTrue($"Failed to restore test asset {asset.SolutionFolder}\\{asset.ProjectRelativePath}. Output:\n{output}"); + } + } + } +} diff --git a/src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs b/src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs new file mode 100644 index 00000000000..f7909df7544 --- /dev/null +++ b/src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs @@ -0,0 +1,31 @@ +// 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; + +namespace Microsoft.Build.EndToEndTests +{ + /// + /// Represents a test solution asset. + /// + internal readonly struct TestSolutionAsset + { + // Solution folder containing the test asset + internal string SolutionFolder { get; } + + // Path to main (entry) project file relative to the solution folder + 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). + /// + internal string ProjectPath => Path.Combine(SolutionFolder, ProjectRelativePath); + } +}