File: ToolTask_Tests.cs
Web Access
Project: ..\..\..\src\Utilities.UnitTests\Microsoft.Build.Utilities.UnitTests.csproj (Microsoft.Build.Utilities.UnitTests)
// 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 System.IO;
using System.Text.RegularExpressions;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.UnitTests;
using Microsoft.Build.Utilities;
using Shouldly;
using Xunit;
using Xunit.Abstractions;
 
#nullable disable
 
namespace Microsoft.Build.UnitTests
{
    public sealed class ToolTask_Tests
    {
        private ITestOutputHelper _output;
 
        public ToolTask_Tests(ITestOutputHelper testOutput)
        {
            _output = testOutput;
        }
 
        private class MyTool : ToolTask, IDisposable
        {
            private string _fullToolName;
            private string _responseFileCommands = string.Empty;
            private string _commandLineCommands = string.Empty;
            private string _pathToToolUsed;
 
            public MyTool()
                : base()
            {
                _fullToolName = Path.Combine(
                    NativeMethodsShared.IsUnixLike ? "/bin" : Environment.GetFolderPath(Environment.SpecialFolder.System),
                    NativeMethodsShared.IsUnixLike ? "sh" : "cmd.exe");
            }
 
            public void Dispose()
            {
            }
 
            public string PathToToolUsed => _pathToToolUsed;
 
            public string MockResponseFileCommands
            {
                set => _responseFileCommands = value;
            }
 
            public string MockCommandLineCommands
            {
                set => _commandLineCommands = value;
            }
 
            public string FullToolName
            {
                set => _fullToolName = value;
            }
 
            /// <summary>
            /// Intercepted start info
            /// </summary>
            internal ProcessStartInfo StartInfo { get; private set; }
 
            /// <summary>
            /// Whether execute was called
            /// </summary>
            internal bool ExecuteCalled { get; private set; }
 
            internal Action<ProcessStartInfo> DoProcessStartInfoMutation { get; set; }
 
            protected override string ToolName => Path.GetFileName(_fullToolName);
 
            protected override string GenerateFullPathToTool() => _fullToolName;
 
            protected override string GenerateResponseFileCommands() => _responseFileCommands;
 
            protected override string GenerateCommandLineCommands() => _commandLineCommands;
 
            protected override ProcessStartInfo GetProcessStartInfo(string pathToTool, string commandLineCommands, string responseFileSwitch)
            {
                var basePSI = base.GetProcessStartInfo(pathToTool, commandLineCommands, responseFileSwitch);
                DoProcessStartInfoMutation?.Invoke(basePSI);
                return basePSI;
            }
 
            protected override void LogEventsFromTextOutput(string singleLine, MessageImportance messageImportance)
            {
                if (singleLine.Contains("BADTHINGHAPPENED"))
                {
                    // This is where a customer's tool task implementation could do its own
                    // parsing of the errors in the stdout/stderr output of the tool being wrapped.
                    Log.LogError(singleLine);
                }
                else
                {
                    base.LogEventsFromTextOutput(singleLine, messageImportance);
                }
            }
 
            protected override int ExecuteTool(string pathToTool, string responseFileCommands, string commandLineCommands)
            {
                Console.WriteLine("executetool");
                _pathToToolUsed = pathToTool;
                ExecuteCalled = true;
                if (!NativeMethodsShared.IsWindows && string.IsNullOrEmpty(responseFileCommands) && string.IsNullOrEmpty(commandLineCommands))
                {
                    // Unix makes sh interactive and it won't exit if there is nothing on the command line
                    commandLineCommands = " -c \"echo\"";
                }
 
                int result = base.ExecuteTool(pathToTool, responseFileCommands, commandLineCommands);
                StartInfo = GetProcessStartInfo(GenerateFullPathToTool(), NativeMethodsShared.IsWindows ? "/x" : string.Empty, null);
                return result;
            }
        }
 
        [Fact]
        public void Regress_Mutation_UserSuppliedToolPathIsLogged()
        {
            using (MyTool t = new MyTool())
            {
                MockEngine3 engine = new MockEngine3();
                t.BuildEngine = engine;
                t.ToolPath = NativeMethodsShared.IsWindows ? @"C:\MyAlternatePath" : "/MyAlternatePath";
 
                t.Execute();
 
                // The alternate path should be mentioned in the log.
                engine.AssertLogContains("MyAlternatePath");
            }
        }
 
        [Fact]
        public void Regress_Mutation_MissingExecutableIsLogged()
        {
            using (MyTool t = new MyTool())
            {
                MockEngine3 engine = new MockEngine3();
                t.BuildEngine = engine;
                t.ToolPath = NativeMethodsShared.IsWindows ? @"C:\MyAlternatePath" : "/MyAlternatePath";
 
                t.Execute().ShouldBeFalse();
 
                // There should be an error about invalid task location.
                engine.AssertLogContains("MSB6004");
            }
        }
 
        [Fact]
        public void Regress_Mutation_WarnIfCommandLineTooLong()
        {
            using (MyTool t = new MyTool())
            {
                MockEngine3 engine = new MockEngine3();
                t.BuildEngine = engine;
 
                // "cmd.exe" croaks big-time when given a very long command-line.  It pops up a message box on
                // Windows XP.  We can't have that!  So use "attrib.exe" for this exercise instead.
                string systemPath = NativeMethodsShared.IsUnixLike ? "/bin" : Environment.GetFolderPath(Environment.SpecialFolder.System);
                t.FullToolName = Path.Combine(systemPath, NativeMethodsShared.IsWindows ? "attrib.exe" : "ps");
 
                t.MockCommandLineCommands = new string('x', 32001);
 
                // It's only a warning, we still succeed
                t.Execute().ShouldBeTrue();
                t.ExitCode.ShouldBe(0);
                // There should be a warning about the command-line being too long.
                engine.AssertLogContains("MSB6002");
            }
        }
 
        /// <summary>
        /// Exercise the code in ToolTask's default implementation of HandleExecutionErrors.
        /// </summary>
        [Fact]
        public void HandleExecutionErrorsWhenToolDoesntLogError()
        {
            using (MyTool t = new MyTool())
            {
                MockEngine3 engine = new MockEngine3();
                t.BuildEngine = engine;
                t.MockCommandLineCommands = NativeMethodsShared.IsWindows ? "/C garbagegarbagegarbagegarbage.exe" : "-c garbagegarbagegarbagegarbage.exe";
 
                t.Execute().ShouldBeFalse();
                t.ExitCode.ShouldBe(NativeMethodsShared.IsWindows ? 1 : 127); // cmd.exe error code is 1, sh error code is 127
 
                // We just tried to run "cmd.exe /C garbagegarbagegarbagegarbage.exe".  This should fail,
                // but since "cmd.exe" doesn't log its errors in canonical format, no errors got
                // logged by the tool itself.  Therefore, ToolTask's default implementation of
                // HandleTaskExecutionErrors should have logged error MSB6006.
                engine.AssertLogContains("MSB6006");
            }
        }
 
        /// <summary>
        /// Exercise the code in ToolTask's default implementation of HandleExecutionErrors.
        /// </summary>
        [Fact]
        [Trait("Category", "netcore-osx-failing")]
        [Trait("Category", "netcore-linux-failing")]
        public void HandleExecutionErrorsWhenToolLogsError()
        {
            using (MyTool t = new MyTool())
            {
                MockEngine3 engine = new MockEngine3();
                t.BuildEngine = engine;
                t.MockCommandLineCommands = NativeMethodsShared.IsWindows
                                                ? "/C echo Main.cs(17,20): error CS0168: The variable 'foo' is declared but never used"
                                                : @"-c """"""echo Main.cs\(17,20\): error CS0168: The variable 'foo' is declared but never used""""""";
 
                t.Execute().ShouldBeFalse();
 
                // The above command logged a canonical error message.  Therefore ToolTask should
                // not log its own error beyond that.
                engine.AssertLogDoesntContain("MSB6006");
                engine.AssertLogContains("CS0168");
                engine.AssertLogContains("The variable 'foo' is declared but never used");
                t.ExitCode.ShouldBe(-1);
                engine.Errors.ShouldBe(1);
            }
        }
 
        /// <summary>
        /// ToolTask should never run String.Format on strings that are
        /// not meant to be formatted.
        /// </summary>
        [Fact]
        public void DoNotFormatTaskCommandOrMessage()
        {
            using MyTool t = new MyTool();
            MockEngine3 engine = new MockEngine3();
            t.BuildEngine = engine;
            // Unmatched curly would crash if they did
            t.MockCommandLineCommands = NativeMethodsShared.IsWindows
                                            ? "/C echo hello world {"
                                            : @"-c ""echo hello world {""";
            t.Execute();
            engine.AssertLogContains("echo hello world {");
            engine.Errors.ShouldBe(0);
        }
 
        /// <summary>
        /// Process notification encoding should be consistent with console code page.
        /// not meant to be formatted.
        /// </summary>
        [InlineData(0, "")]
        [InlineData(-1, "1>&2")]
        [Theory]
        public void ProcessNotificationEncodingConsistentWithConsoleCodePage(int exitCode, string errorPart)
        {
            using MyTool t = new MyTool();
            MockEngine engine = new MockEngine();
            t.BuildEngine = engine;
            t.UseCommandProcessor = true;
            t.LogStandardErrorAsError = true;
            t.EchoOff = true;
            t.UseUtf8Encoding = EncodingUtilities.UseUtf8Always;
            string content = "Building Custom Rule プロジェクト";
            string outputMessage = exitCode == 0 ? content : $"'{content}' {errorPart}";
            string commandLine = $"echo {outputMessage}";
            t.MockCommandLineCommands = commandLine;
            t.Execute();
            t.ExitCode.ShouldBe(exitCode);
 
            string log = engine.Log;
            string singleQuote = NativeMethodsShared.IsWindows ? "'" : string.Empty;
            string displayMessage = exitCode == 0 ? content : $"ERROR : {singleQuote}{content}{singleQuote}";
            string pattern = $"{commandLine}{Environment.NewLine}\\s*{displayMessage}";
            Regex regex = new Regex(pattern);
            regex.Matches(log).Count.ShouldBe(1, $"{log} doesn't contain the log matching the pattern: {pattern}");
        }
 
        /// <summary>
        /// When a message is logged to the standard error stream do not error is LogStandardErrorAsError is not true or set.
        /// </summary>
        [Fact]
        public void DoNotErrorWhenTextSentToStandardError()
        {
            using (MyTool t = new MyTool())
            {
                MockEngine3 engine = new MockEngine3();
                t.BuildEngine = engine;
                t.MockCommandLineCommands = NativeMethodsShared.IsWindows
                                                ? "/C Echo 'Who made you king anyways' 1>&2"
                                                : @"-c ""echo Who made you king anyways 1>&2""";
 
                t.Execute().ShouldBeTrue();
 
                engine.AssertLogDoesntContain("MSB");
                engine.AssertLogContains("Who made you king anyways");
                t.ExitCode.ShouldBe(0);
                engine.Errors.ShouldBe(0);
            }
        }
 
        /// <summary>
        /// When a message is logged to the standard output stream do not error is LogStandardErrorAsError is  true
        /// </summary>
        [Fact]
        public void DoNotErrorWhenTextSentToStandardOutput()
        {
            using (MyTool t = new MyTool())
            {
                MockEngine3 engine = new MockEngine3();
                t.BuildEngine = engine;
                t.LogStandardErrorAsError = true;
                t.MockCommandLineCommands = NativeMethodsShared.IsWindows
                                                ? "/C Echo 'Who made you king anyways'"
                                                : @"-c ""echo Who made you king anyways""";
 
                t.Execute().ShouldBeTrue();
 
                engine.AssertLogDoesntContain("MSB");
                engine.AssertLogContains("Who made you king anyways");
                t.ExitCode.ShouldBe(0);
                engine.Errors.ShouldBe(0);
            }
        }
 
        /// <summary>
        /// When a message is logged to the standard error stream error if LogStandardErrorAsError is true
        /// </summary>
        [Fact]
        public void ErrorWhenTextSentToStandardError()
        {
            using (MyTool t = new MyTool())
            {
                MockEngine3 engine = new MockEngine3();
                t.BuildEngine = engine;
                t.LogStandardErrorAsError = true;
                t.MockCommandLineCommands = NativeMethodsShared.IsWindows
                                                ? "/C Echo 'Who made you king anyways' 1>&2"
                                                : @"-c ""echo 'Who made you king anyways' 1>&2""";
 
                t.Execute().ShouldBeFalse();
 
                engine.AssertLogDoesntContain("MSB3073");
                engine.AssertLogContains("Who made you king anyways");
                t.ExitCode.ShouldBe(-1);
                engine.Errors.ShouldBe(1);
            }
        }
 
 
        /// <summary>
        /// When ToolExe is set, it is used instead of ToolName
        /// </summary>
        [Fact]
        public void ToolExeWinsOverToolName()
        {
            using (MyTool t = new MyTool())
            {
                MockEngine3 engine = new MockEngine3();
                t.BuildEngine = engine;
                t.FullToolName = NativeMethodsShared.IsWindows ? "c:\\baz\\foo.exe" : "/baz/foo.exe";
 
                t.ToolExe.ShouldBe("foo.exe");
                t.ToolExe = "bar.exe";
                t.ToolExe.ShouldBe("bar.exe");
            }
        }
 
        /// <summary>
        /// When ToolExe is set, it is appended to ToolPath instead
        /// of the regular tool name
        /// </summary>
        [Fact]
        public void ToolExeIsFoundOnToolPath()
        {
            string shellName = NativeMethodsShared.IsWindows ? "cmd.exe" : "sh";
            string copyName = NativeMethodsShared.IsWindows ? "xcopy.exe" : "cp";
            using (MyTool t = new MyTool())
            {
                MockEngine3 engine = new MockEngine3();
                t.BuildEngine = engine;
                t.FullToolName = shellName;
                string systemPath = NativeMethodsShared.IsUnixLike ? "/bin" : Environment.GetFolderPath(Environment.SpecialFolder.System);
                t.ToolPath = systemPath;
 
                t.Execute();
                t.PathToToolUsed.ShouldBe(Path.Combine(systemPath, shellName));
                engine.AssertLogContains(shellName);
                engine.Log = string.Empty;
 
                t.ToolExe = copyName;
                t.Execute();
                t.PathToToolUsed.ShouldBe(Path.Combine(systemPath, copyName));
                engine.AssertLogContains(copyName);
                engine.AssertLogDoesntContain(shellName);
            }
        }
 
        /// <summary>
        /// Task is not found on path - regress #499196
        /// </summary>
        [Fact]
        public void TaskNotFoundOnPath()
        {
            using (MyTool t = new MyTool())
            {
                MockEngine3 engine = new MockEngine3();
                t.BuildEngine = engine;
                t.FullToolName = "doesnotexist.exe";
 
                t.Execute().ShouldBeFalse();
                t.ExitCode.ShouldBe(-1);
                engine.Errors.ShouldBe(1);
 
                // Does not throw an exception
            }
        }
 
        /// <summary>
        /// Task is found on path.
        /// </summary>
        [Fact]
        public void TaskFoundOnPath()
        {
            using (MyTool t = new MyTool())
            {
                MockEngine3 engine = new MockEngine3();
                t.BuildEngine = engine;
                string toolName = NativeMethodsShared.IsWindows ? "cmd.exe" : "sh";
                t.FullToolName = toolName;
 
                t.Execute().ShouldBeTrue();
                t.ExitCode.ShouldBe(0);
                engine.Errors.ShouldBe(0);
 
                string systemPath = NativeMethodsShared.IsUnixLike ? "/bin" : Environment.GetFolderPath(Environment.SpecialFolder.System);
                engine.AssertLogContains(
                Path.Combine(systemPath, toolName));
            }
        }
 
        /// <summary>
        /// StandardOutputImportance set to Low should not show up in our log
        /// </summary>
        [Fact]
        public void OverrideStdOutImportanceToLow()
        {
            string tempFile = FileUtilities.GetTemporaryFileName();
            File.WriteAllText(tempFile, @"hello world");
 
            using (MyTool t = new MyTool())
            {
                MockEngine3 engine = new MockEngine3();
                engine.MinimumMessageImportance = MessageImportance.High;
 
                t.BuildEngine = engine;
                t.FullToolName = NativeMethodsShared.IsWindows ? "findstr.exe" : "grep";
                t.MockCommandLineCommands = "\"hello\" \"" + tempFile + "\"";
                t.StandardOutputImportance = "Low";
 
                t.Execute().ShouldBeTrue();
                t.ExitCode.ShouldBe(0);
                engine.Errors.ShouldBe(0);
 
                engine.AssertLogDoesntContain("hello world");
            }
            File.Delete(tempFile);
        }
 
        /// <summary>
        /// StandardOutputImportance set to High should show up in our log
        /// </summary>
        [Fact]
        public void OverrideStdOutImportanceToHigh()
        {
            string tempFile = FileUtilities.GetTemporaryFileName();
            File.WriteAllText(tempFile, @"hello world");
 
            using (MyTool t = new MyTool())
            {
                MockEngine3 engine = new MockEngine3();
                engine.MinimumMessageImportance = MessageImportance.High;
 
                t.BuildEngine = engine;
                t.FullToolName = NativeMethodsShared.IsWindows ? "findstr.exe" : "grep";
                t.MockCommandLineCommands = "\"hello\" \"" + tempFile + "\"";
                t.StandardOutputImportance = "High";
 
                t.Execute().ShouldBeTrue();
                t.ExitCode.ShouldBe(0);
                engine.Errors.ShouldBe(0);
 
                engine.AssertLogContains("hello world");
            }
            File.Delete(tempFile);
        }
 
        /// <summary>
        /// This is to ensure that somebody could write a task that implements ToolTask,
        /// wraps some .EXE tool, and still have the ability to parse the stdout/stderr
        /// himself.  This is so that in case the tool doesn't log its errors in canonical
        /// format, the task can still opt to do something reasonable with it.
        /// </summary>
        [Fact]
        public void ToolTaskCanChangeCanonicalErrorFormat()
        {
            string tempFile = FileUtilities.GetTemporaryFileName();
            File.WriteAllText(tempFile, @"
                Main.cs(17,20): warning CS0168: The variable 'foo' is declared but never used.
                BADTHINGHAPPENED: This is my custom error format that's not in canonical error format.
                ");
 
            using (MyTool t = new MyTool())
            {
                MockEngine3 engine = new MockEngine3();
                t.BuildEngine = engine;
                // The command we're giving is the command to spew the contents of the temp
                // file we created above.
                t.MockCommandLineCommands = NativeMethodsShared.IsWindows
                                                ? $"/C type \"{tempFile}\""
                                                : $"-c \"cat \'{tempFile}\'\"";
 
                t.Execute();
 
                // The above command logged a canonical warning, as well as a custom error.
                engine.AssertLogContains("CS0168");
                engine.AssertLogContains("The variable 'foo' is declared but never used");
                engine.AssertLogContains("BADTHINGHAPPENED");
                engine.AssertLogContains("This is my custom error format");
 
                engine.Warnings.ShouldBe(1); // "Expected one warning in log."
                engine.Errors.ShouldBe(1); // "Expected one error in log."
            }
 
            File.Delete(tempFile);
        }
 
        /// <summary>
        /// Passing env vars through the tooltask public property
        /// </summary>
        [Fact]
        public void EnvironmentVariablesToToolTask()
        {
            using MyTool task = new MyTool();
            task.BuildEngine = new MockEngine3();
            string userVarName = NativeMethodsShared.IsWindows ? "username" : "user";
            task.EnvironmentVariables = new[] { "a=b", "c=d", userVarName + "=x" /* built-in */, "path=" /* blank value */};
            bool result = task.Execute();
 
            result.ShouldBe(true);
            task.ExecuteCalled.ShouldBe(true);
 
            ProcessStartInfo startInfo = task.StartInfo;
 
            startInfo.Environment["a"].ShouldBe("b");
            startInfo.Environment["c"].ShouldBe("d");
            startInfo.Environment[userVarName].ShouldBe("x");
            startInfo.Environment["path"].ShouldBe(String.Empty);
 
            if (NativeMethodsShared.IsWindows)
            {
                Assert.Equal(
                        Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
                        startInfo.Environment["programfiles"],
                        true);
            }
        }
 
        /// <summary>
        /// Equals sign in value
        /// </summary>
        [Fact]
        public void EnvironmentVariablesToToolTaskEqualsSign()
        {
            using MyTool task = new MyTool();
            task.BuildEngine = new MockEngine3();
            task.EnvironmentVariables = new[] { "a=b=c" };
            bool result = task.Execute();
 
            result.ShouldBe(true);
            task.StartInfo.Environment["a"].ShouldBe("b=c");
        }
 
        /// <summary>
        /// No value provided
        /// </summary>
        [Fact]
        public void EnvironmentVariablesToToolTaskInvalid1()
        {
            using MyTool task = new MyTool();
            task.BuildEngine = new MockEngine3();
            task.EnvironmentVariables = new[] { "x" };
            bool result = task.Execute();
 
            result.ShouldBe(false);
            task.ExecuteCalled.ShouldBe(false);
        }
 
        /// <summary>
        /// Empty string provided
        /// </summary>
        [Fact]
        public void EnvironmentVariablesToToolTaskInvalid2()
        {
            using MyTool task = new MyTool();
            task.BuildEngine = new MockEngine3();
            task.EnvironmentVariables = new[] { "" };
            bool result = task.Execute();
 
            result.ShouldBe(false);
            task.ExecuteCalled.ShouldBe(false);
        }
 
        /// <summary>
        /// Empty name part provided
        /// </summary>
        [Fact]
        public void EnvironmentVariablesToToolTaskInvalid3()
        {
            using MyTool task = new MyTool();
            task.BuildEngine = new MockEngine3();
            task.EnvironmentVariables = new[] { "=a;b=c" };
            bool result = task.Execute();
 
            result.ShouldBe(false);
            task.ExecuteCalled.ShouldBe(false);
        }
 
        /// <summary>
        /// Not set should not wipe out other env vars
        /// </summary>
        [Fact]
        public void EnvironmentVariablesToToolTaskNotSet()
        {
            using MyTool task = new MyTool();
            task.BuildEngine = new MockEngine3();
            task.EnvironmentVariables = null;
            bool result = task.Execute();
 
            result.ShouldBe(true);
            task.ExecuteCalled.ShouldBe(true);
            Assert.True(task.StartInfo.Environment["PATH"].Length > 0);
        }
 
        /// <summary>
        /// Verifies that if a directory with the same name of the tool exists that the tool task correctly
        /// ignores the directory.
        /// </summary>
        [Fact]
        public void ToolPathIsFoundWhenDirectoryExistsWithNameOfTool()
        {
            string toolName = NativeMethodsShared.IsWindows ? "cmd" : "sh";
            string savedCurrentDirectory = Directory.GetCurrentDirectory();
 
            try
            {
                using (var env = TestEnvironment.Create())
                {
                    string tempDirectory = env.CreateFolder().Path;
                    env.SetCurrentDirectory(tempDirectory);
                    env.SetEnvironmentVariable("PATH", $"{tempDirectory}{Path.PathSeparator}{Environment.GetEnvironmentVariable("PATH")}");
                    Directory.SetCurrentDirectory(tempDirectory);
 
                    string directoryNamedSameAsTool = Directory.CreateDirectory(Path.Combine(tempDirectory, toolName)).FullName;
 
                    using MyTool task = new MyTool
                    {
                        BuildEngine = new MockEngine3(),
                        FullToolName = toolName,
                    };
                    bool result = task.Execute();
 
                    Assert.NotEqual(directoryNamedSameAsTool, task.PathToToolUsed);
 
                    result.ShouldBeTrue();
                }
            }
            finally
            {
                Directory.SetCurrentDirectory(savedCurrentDirectory);
            }
        }
 
        /// <summary>
        /// Confirms we can find a file on the PATH.
        /// </summary>
        [Fact]
        public void FindOnPathSucceeds()
        {
            string[] expectedCmdPath;
            string shellName;
            string cmdPath;
            if (NativeMethodsShared.IsWindows)
            {
                expectedCmdPath = new[] { Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe").ToUpperInvariant() };
                shellName = "cmd.exe";
                cmdPath = ToolTask.FindOnPath(shellName).ToUpperInvariant();
            }
            else
            {
                expectedCmdPath = new[] { "/bin/sh", "/usr/bin/sh" };
                shellName = "sh";
                cmdPath = ToolTask.FindOnPath(shellName);
            }
 
            cmdPath.ShouldBeOneOf(expectedCmdPath);
        }
 
        /// <summary>
        /// Equals sign in value
        /// </summary>
        [Fact]
        public void GetProcessStartInfoCanOverrideEnvironmentVariables()
        {
            using MyTool task = new MyTool();
            task.DoProcessStartInfoMutation = (p) => p.Environment.Remove("a");
 
            task.BuildEngine = new MockEngine3();
            task.EnvironmentVariables = new[] { "a=b" };
            bool result = task.Execute();
 
            result.ShouldBe(true);
            task.StartInfo.Environment.ContainsKey("a").ShouldBe(false);
        }
 
        [Fact]
        public void VisualBasicLikeEscapedQuotesInCommandAreNotMadeForwardSlashes()
        {
            using MyTool t = new MyTool();
            MockEngine3 engine = new MockEngine3();
            t.BuildEngine = engine;
            t.MockCommandLineCommands = NativeMethodsShared.IsWindows
                                            ? "/C echo \"hello \\\"world\\\"\""
                                            : "-c echo \"hello \\\"world\\\"\"";
            t.Execute();
            engine.AssertLogContains("echo \"hello \\\"world\\\"\"");
            engine.Errors.ShouldBe(0);
        }
 
        private sealed class MyToolWithCustomProcess : MyTool
        {
            protected override Process StartToolProcess(Process proc)
            {
#pragma warning disable CA2000 // Dispose objects before losing scope - caller needs the process
                Process customProcess = new Process();
#pragma warning restore CA2000
                customProcess.StartInfo = proc.StartInfo;
 
                customProcess.EnableRaisingEvents = true;
                customProcess.Exited += ReceiveExitNotification;
 
                customProcess.ErrorDataReceived += ReceiveStandardErrorData;
                customProcess.OutputDataReceived += ReceiveStandardOutputData;
                return base.StartToolProcess(customProcess);
            }
        }
 
        [Fact]
        public void UsesCustomProcess()
        {
            using (MyToolWithCustomProcess t = new MyToolWithCustomProcess())
            {
                MockEngine3 engine = new MockEngine3();
                t.BuildEngine = engine;
                t.MockCommandLineCommands = NativeMethodsShared.IsWindows
                    ? "/C echo hello_stdout & echo hello_stderr >&2"
                    : "-c \"echo hello_stdout ; echo hello_stderr >&2\"";
 
                t.Execute();
 
                engine.AssertLogContains("\nhello_stdout");
                engine.AssertLogContains("\nhello_stderr");
            }
        }
 
        /// <summary>
        /// Verifies that a ToolTask running under the command processor on Windows has autorun
        /// disabled or enabled depending on an escape hatch.
        /// </summary>
        [Theory]
        [InlineData("MSBUILDUSERAUTORUNINCMD", null, true)]
        [InlineData("MSBUILDUSERAUTORUNINCMD", "0", true)]
        [InlineData("MSBUILDUSERAUTORUNINCMD", "1", false)]
        [Trait("Category", "nonosxtests")]
        [Trait("Category", "nonlinuxtests")]
        public void ExecTaskDisablesAutoRun(string environmentVariableName, string environmentVariableValue, bool autoRunShouldBeDisabled)
        {
            using (TestEnvironment testEnvironment = TestEnvironment.Create())
            {
                testEnvironment.SetEnvironmentVariable(environmentVariableName, environmentVariableValue);
 
                ToolTaskThatGetsCommandLine task = new ToolTaskThatGetsCommandLine
                {
                    UseCommandProcessor = true
                };
 
                task.Execute();
 
                if (autoRunShouldBeDisabled)
                {
                    task.CommandLineCommands.ShouldContain("/D ");
                }
                else
                {
                    task.CommandLineCommands.ShouldNotContain("/D ");
                }
            }
        }
 
        /// <summary>
        /// A simple implementation of <see cref="ToolTask"/> that allows tests to verify the command-line that was generated.
        /// </summary>
        private sealed class ToolTaskThatGetsCommandLine : ToolTask
        {
            protected override string ToolName => "cmd.exe";
 
            protected override string GenerateFullPathToTool() => null;
 
            protected override int ExecuteTool(string pathToTool, string responseFileCommands, string commandLineCommands)
            {
                PathToTool = pathToTool;
                ResponseFileCommands = responseFileCommands;
                CommandLineCommands = commandLineCommands;
 
                return 0;
            }
            protected override void LogToolCommand(string message)
            {
            }
 
            public string CommandLineCommands { get; private set; }
 
            public string PathToTool { get; private set; }
 
            public string ResponseFileCommands { get; private set; }
        }
 
        [Theory]
        [InlineData("MSBUILDAVOIDUNICODE", null, false)]
        [InlineData("MSBUILDAVOIDUNICODE", "0", false)]
        [InlineData("MSBUILDAVOIDUNICODE", "1", true)]
        public void ToolTaskCanUseUnicode(string environmentVariableName, string environmentVariableValue, bool expectNormalizationToANSI)
        {
            using TestEnvironment testEnvironment = TestEnvironment.Create(_output);
 
            testEnvironment.SetEnvironmentVariable(environmentVariableName, environmentVariableValue);
 
            var output = testEnvironment.ExpectFile();
 
            MockEngine3 engine = new MockEngine3();
 
            var task = new ToolTaskThatNeedsUnicode
            {
                BuildEngine = engine,
                UseCommandProcessor = true,
                OutputPath = output.Path,
            };
 
            task.Execute();
 
            File.Exists(output.Path).ShouldBeTrue();
            if (NativeMethodsShared.IsUnixLike // treat all UNIXy OSes as capable of UTF-8 everywhere
                || !expectNormalizationToANSI)
            {
                File.ReadAllText(output.Path).ShouldContain("łoł");
            }
            else
            {
                File.ReadAllText(output.Path).ShouldContain("lol");
            }
        }
 
 
        private sealed class ToolTaskThatNeedsUnicode : ToolTask
        {
            protected override string ToolName => "cmd.exe";
 
            [Required]
            public string OutputPath { get; set; }
 
            public ToolTaskThatNeedsUnicode()
            {
                UseCommandProcessor = true;
            }
 
            protected override string GenerateFullPathToTool()
            {
                return "cmd.exe";
            }
 
            protected override string GenerateCommandLineCommands()
            {
                return $"echo łoł > {OutputPath}";
            }
        }
 
        /// <summary>
        /// Verifies the validation of the <see cref="ToolTask.TaskProcessTerminationTimeout" />.
        /// </summary>
        /// <param name="timeout">New value for <see cref="ToolTask.TaskProcessTerminationTimeout" />.</param>
        /// <param name="isInvalidValid">Is a task expected to be valid or not.</param>
        [Theory]
        [InlineData(int.MaxValue, false)]
        [InlineData(97, false)]
        [InlineData(0, false)]
        [InlineData(-1, false)]
        [InlineData(-2, true)]
        [InlineData(-101, true)]
        [InlineData(int.MinValue, true)]
        public void SetsTerminationTimeoutCorrectly(int timeout, bool isInvalidValid)
        {
            using var env = TestEnvironment.Create(_output);
 
            // Task under test:
            var task = new ToolTaskSetsTerminationTimeout
            {
                BuildEngine = new MockEngine3()
            };
 
            task.TerminationTimeout = timeout;
            task.ValidateParameters().ShouldBe(!isInvalidValid);
            task.TerminationTimeout.ShouldBe(timeout);
        }
 
        /// <summary>
        /// Verifies that a ToolTask instance can return correct results when executed multiple times with timeout.
        /// </summary>
        /// <param name="repeats">Specifies the number of repeats for external command execution.</param>
        /// <param name="initialDelay">Delay to generate on the first execution in milliseconds.</param>
        /// <param name="followupDelay">Delay to generate on follow-up execution in milliseconds.</param>
        /// <param name="timeout">Task timeout in milliseconds.</param>
        /// <remarks>
        /// These tests execute the same task instance multiple times, which will in turn run a shell command to sleep
        /// predefined amount of time. The first execution may time out, but all following ones won't. It is expected
        /// that all following executions return success.
        /// </remarks>
        [Theory]
        [InlineData(1, 1, 1, -1)] // Normal case, no repeat.
        [InlineData(3, 1, 1, -1)] // Repeat without timeout.
        [InlineData(3, 10000, 1, 1000)] // Repeat with timeout.
        public void ToolTaskThatTimeoutAndRetry(int repeats, int initialDelay, int followupDelay, int timeout)
        {
            using var env = TestEnvironment.Create(_output);
 
            MockEngine3 engine = new();
 
            // Task under test:
            var task = new ToolTaskThatSleeps
            {
                BuildEngine = engine,
                InitialDelay = initialDelay,
                FollowupDelay = followupDelay,
                Timeout = timeout
            };
 
            // Execute the same task instance multiple times. The index is one-based.
            bool result;
            for (int i = 1; i <= repeats; i++)
            {
                // Execute the task:
                result = task.Execute();
 
                _output.WriteLine(engine.Log);
 
                task.RepeatCount.ShouldBe(i);
 
                // The first execution may fail (timeout), but all following ones should succeed:
                if (i > 1)
                {
                    result.ShouldBeTrue();
                    task.ExitCode.ShouldBe(0);
                }
            }
        }
 
        /// <summary>
        /// A simple implementation of <see cref="ToolTask"/> to sleep for a while.
        /// </summary>
        /// <remarks>
        /// This task runs shell command to sleep for predefined, variable amount of time based on how many times the
        /// instance has been executed.
        /// </remarks>
        private sealed class ToolTaskThatSleeps : ToolTask
        {
            // Windows prompt command to sleep:
            private readonly string _windowsSleep = "/c start /wait timeout {0}";
 
            // UNIX command to sleep:
            private readonly string _unixSleep = "-c \"sleep {0}\"";
 
            // Full path to shell:
            private readonly string _pathToShell;
 
            public ToolTaskThatSleeps()
                : base()
            {
                // Determines shell to use: cmd for Windows, sh for UNIX-like systems:
                _pathToShell = NativeMethodsShared.IsUnixLike ? "/bin/sh" : "cmd.exe";
            }
 
            /// <summary>
            /// Gets or sets the delay for the first execution.
            /// </summary>
            /// <remarks>
            /// Defaults to 10 seconds.
            /// </remarks>
            public Int32 InitialDelay { get; set; } = 10000;
 
            /// <summary>
            /// Gets or sets the delay for the follow-up executions.
            /// </summary>
            /// <remarks>
            /// Defaults to 1 milliseconds.
            /// </remarks>
            public Int32 FollowupDelay { get; set; } = 1;
 
            /// <summary>
            /// Int32 output parameter for the repeat counter for test purpose.
            /// </summary>
            [Output]
            public Int32 RepeatCount { get; private set; } = 0;
 
            /// <summary>
            /// Gets the tool name (shell).
            /// </summary>
            protected override string ToolName => Path.GetFileName(_pathToShell);
 
            /// <summary>
            /// Gets the full path to shell.
            /// </summary>
            protected override string GenerateFullPathToTool() => _pathToShell;
 
            /// <summary>
            /// Generates a shell command to sleep different amount of time based on repeat counter.
            /// </summary>
            protected override string GenerateCommandLineCommands() =>
                NativeMethodsShared.IsUnixLike ?
                string.Format(_unixSleep, RepeatCount < 2 ? InitialDelay / 1000.0 : FollowupDelay / 1000.0) :
                string.Format(_windowsSleep, RepeatCount < 2 ? InitialDelay / 1000.0 : FollowupDelay / 1000.0);
 
            /// <summary>
            /// Ensures that test parameters make sense.
            /// </summary>
            protected internal override bool ValidateParameters() =>
                (InitialDelay > 0) && (FollowupDelay > 0) && base.ValidateParameters();
 
            /// <summary>
            /// Runs shell command to sleep for a while.
            /// </summary>
            /// <returns>
            /// true if the task runs successfully; false otherwise.
            /// </returns>
            public override bool Execute()
            {
                RepeatCount++;
                return base.Execute();
            }
        }
 
        /// <summary>
        /// A simple implementation of <see cref="ToolTask"/> to excercise <see cref="ToolTask.TaskProcessTerminationTimeout" />.
        /// </summary>
        private sealed class ToolTaskSetsTerminationTimeout : ToolTask
        {
            public ToolTaskSetsTerminationTimeout()
                : base()
            {
                base.TaskResources = AssemblyResources.PrimaryResources;
            }
 
            /// <summary>
            /// Gets or sets <see cref="ToolTask.TaskProcessTerminationTimeout" />.
            /// </summary>
            /// <remarks>
            /// This is just a proxy property to access <see cref="ToolTask.TaskProcessTerminationTimeout" />.
            /// </remarks>
            public int TerminationTimeout
            {
                get => TaskProcessTerminationTimeout;
                set => TaskProcessTerminationTimeout = value;
            }
 
            /// <summary>
            /// Gets the tool name (dummy).
            /// </summary>
            protected override string ToolName => string.Empty;
 
            /// <summary>
            /// Gets the full path to tool (dummy).
            /// </summary>
            protected override string GenerateFullPathToTool() => string.Empty;
 
            /// <summary>
            /// Does nothing.
            /// </summary>
            /// <returns>
            /// Always returns true.
            /// </returns>
            /// <remarks>
            /// This dummy tool task is not meant to run anything.
            /// </remarks>
            public override bool Execute() => true;
        }
    }
}