File: RunnerUtilities.cs
Web Access
Project: ..\..\..\src\UnitTests.Shared\Microsoft.Build.UnitTests.Shared.csproj (Microsoft.Build.UnitTests.Shared)
// 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.Diagnostics;
using Microsoft.Build.Shared;
using System.IO;
using System.Reflection;
using Microsoft.Build.Framework;
using Xunit.Abstractions;
using System.Linq;
 
#nullable disable
 
namespace Microsoft.Build.UnitTests.Shared
{
    public static class RunnerUtilities
    {
        public static string PathToCurrentlyRunningMsBuildExe => BuildEnvironmentHelper.Instance.CurrentMSBuildExePath;
#if !FEATURE_RUN_EXE_IN_TESTS
        private static readonly string s_dotnetExePath = EnvironmentProvider.GetDotnetExePath();
 
        public static void ApplyDotnetHostPathEnvironmentVariable(TestEnvironment testEnvironment)
        {
            // Built msbuild.dll executed by dotnet.exe needs this environment variable for msbuild tasks such as RoslynCodeTaskFactory.
            testEnvironment.SetEnvironmentVariable("DOTNET_HOST_PATH", s_dotnetExePath);
        }
#endif
 
        /// <summary>
        /// Invoke the currently running msbuild and return the stdout, stderr, and process exit status.
        /// This method may invoke msbuild via other runtimes.
        /// </summary>
        public static string ExecMSBuild(string msbuildParameters, out bool successfulExit, ITestOutputHelper outputHelper = null)
        {
            return ExecMSBuild(PathToCurrentlyRunningMsBuildExe, msbuildParameters, out successfulExit, outputHelper: outputHelper);
        }
 
        /// <summary>
        /// Invoke msbuild.exe with the given parameters and return the stdout, stderr, and process exit status.
        /// This method may invoke msbuild via other runtimes.
        /// </summary>
        public static string ExecMSBuild(string pathToMsBuildExe, string msbuildParameters, out bool successfulExit, bool shellExecute = false, ITestOutputHelper outputHelper = null)
        {
#if FEATURE_RUN_EXE_IN_TESTS
            var pathToExecutable = pathToMsBuildExe;
#else
            var pathToExecutable = s_dotnetExePath;
            msbuildParameters = FileUtilities.EnsureDoubleQuotes(pathToMsBuildExe) + " " + msbuildParameters;
#endif
 
            return RunProcessAndGetOutput(pathToExecutable, msbuildParameters, out successfulExit, shellExecute, outputHelper);
        }
 
        public static string ExecBootstrapedMSBuild(
            string msbuildParameters,
            out bool successfulExit,
            bool shellExecute = false,
            ITestOutputHelper outputHelper = null,
            bool attachProcessId = true,
            int timeoutMilliseconds = 30_000)
        {
            BootstrapLocationAttribute attribute = Assembly.GetExecutingAssembly().GetCustomAttribute<BootstrapLocationAttribute>()
                                                   ?? throw new InvalidOperationException("This test assembly does not have the BootstrapLocationAttribute");
 
            string binaryFolder = attribute.BootstrapMsBuildBinaryLocation;
#if NET
            string pathToExecutable = EnvironmentProvider.GetDotnetExePathFromFolder(binaryFolder);
            msbuildParameters = Path.Combine(binaryFolder, "sdk", attribute.BootstrapSdkVersion, "MSBuild.dll") + " " + msbuildParameters;
#else
            string pathToExecutable = Path.Combine(binaryFolder, "MSBuild.exe");
#endif
            return RunProcessAndGetOutput(pathToExecutable, msbuildParameters, out successfulExit, shellExecute, outputHelper, attachProcessId, timeoutMilliseconds);
        }
 
        private static void AdjustForShellExecution(ref string pathToExecutable, ref string arguments)
        {
            if (NativeMethodsShared.IsWindows)
            {
                var comSpec = Environment.GetEnvironmentVariable("ComSpec");
 
                // /D: Do not load AutoRun configuration from the registry (perf)
                arguments = $"/D /C \"{pathToExecutable} {arguments}\"";
                pathToExecutable = comSpec;
            }
            else
            {
                throw new NotImplementedException();
            }
        }
 
        /// <summary>
        /// Run the process and get stdout and stderr.
        /// </summary>
        public static string RunProcessAndGetOutput(
            string process,
            string parameters,
            out bool successfulExit,
            bool shellExecute = false,
            ITestOutputHelper outputHelper = null,
            bool attachProcessId = true,
            int timeoutMilliseconds = 30_000)
        {
            if (shellExecute)
            {
                // we adjust the psi data manually because on net core using ProcessStartInfo.UseShellExecute throws NotImplementedException
                AdjustForShellExecution(ref process, ref parameters);
            }
 
            var psi = new ProcessStartInfo(process)
            {
                CreateNoWindow = true,
                RedirectStandardInput = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                Arguments = parameters
            };
            string output = string.Empty;
            int pid = -1;
 
            using (var p = new Process { EnableRaisingEvents = true, StartInfo = psi })
            {
                DataReceivedEventHandler handler = delegate (object sender, DataReceivedEventArgs args)
                {
                    if (args != null && args.Data != null)
                    {
                        WriteOutput(args.Data);
                        output += args.Data + "\r\n";
                    }
                };
 
                p.OutputDataReceived += handler;
                p.ErrorDataReceived += handler;
 
                WriteOutput($"Executing [{process} {parameters}]");
                WriteOutput("==== OUTPUT ====");
                p.Start();
                p.BeginOutputReadLine();
                p.BeginErrorReadLine();
                p.StandardInput.Dispose();
 
                TimeSpan timeout = TimeSpan.FromMilliseconds(timeoutMilliseconds);
                if (Traits.Instance.DebugUnitTests)
                {
                    p.WaitForExit();
                }
                else if (!p.WaitForExit(timeoutMilliseconds))
                {
                    // Let's not create a unit test for which we need more than requested timeout to execute.
                    // Please consider carefully if you would like to increase the timeout.
                    p.KillTree(1000);
                    throw new TimeoutException($"Test failed due to timeout: process {p.Id} is active for more than {timeout.TotalSeconds} sec.");
                }
 
                // We need the WaitForExit call without parameters because our processing of output/error streams is not synchronous.
                // See https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexit?view=net-6.0#system-diagnostics-process-waitforexit(system-int32).
                // The overload WaitForExit() waits for the error and output to be handled. The WaitForExit(int timeout) overload does not, so we could lose the data.
                p.WaitForExit();
 
                pid = p.Id;
                successfulExit = p.ExitCode == 0;
            }
 
            if (attachProcessId)
            {
                output += "Process ID is " + pid + "\r\n";
                WriteOutput("Process ID is " + pid + "\r\n");
                WriteOutput("==============");
            }
 
            return output;
 
            void WriteOutput(string data)
            {
                outputHelper?.WriteLine(data);
                Console.WriteLine(data);
            }
        }
    }
}