File: MultithreadedExecution_Tests.cs
Web Access
Project: ..\..\..\src\MSBuild.EndToEnd.Tests\Microsoft.Build.EndToEnd.Tests.csproj (Microsoft.Build.EndToEnd.Tests)
// 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
{
    /// <summary>
    /// Tests for multithreaded MSBuild execution scenarios using test assets.
    /// </summary>
    public class MultithreadedExecution_Tests : IClassFixture<TestSolutionAssetsFixture>, 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();
        }
 
        /// <summary>
        /// Prepares an isolated copy of test assets in a temporary directory for each test run.
        /// This ensures fresh builds and proper test isolation.
        /// </summary>
        /// <param name="testAsset">Test asset</param>
        /// <returns>TestSolutionAsset for the copied asset in a temporary folder.</returns>
        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);
        }
        
        /// <summary>
        /// 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.
        /// </summary>
        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))
            };
        }
 
        /// <summary>
        /// Builds a test asset with the given MSBuild args and verifies success.
        /// </summary>
        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}.");
        }
 
        /// <summary>
        /// Tests building projects with various multithreading flags.
        /// </summary>
        [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);
        }
 
        /// <summary>
        /// Builds a test asset with binary logging, then replays the binlog and verifies both succeed.
        /// </summary>
        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}.");
        }
 
        /// <summary>
        /// Tests binary logging with multithreaded builds and verifies replay functionality.
        /// </summary>
        [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);
        }
 
        /// <summary>
        /// Tests building non-SDK-style projects with multithreading flags.
        /// </summary>
        [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);
        }
 
        /// <summary>
        /// Tests binary logging with non-SDK-style multithreaded builds and verifies replay functionality.
        /// </summary>
        [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);
        }
    }
}