diff --git a/src/Utilities.UnitTests/ToolTask_Tests.cs b/src/Utilities.UnitTests/ToolTask_Tests.cs index f1d08123d54..06811207770 100644 --- a/src/Utilities.UnitTests/ToolTask_Tests.cs +++ b/src/Utilities.UnitTests/ToolTask_Tests.cs @@ -117,7 +117,7 @@ protected override int ExecuteTool(string pathToTool, string responseFileCommand StartInfo = GetProcessStartInfo(GenerateFullPathToTool(), NativeMethodsShared.IsWindows ? "/x" : string.Empty, null); return result; } - }; + } [Fact] public void Regress_Mutation_UserSuppliedToolPathIsLogged() @@ -825,5 +825,135 @@ protected override string GenerateCommandLineCommands() return $"echo łoł > {OutputPath}"; } } + + /// + /// Verifies that a ToolTask instance can return correct results when executed multiple times with timeout. + /// + /// Specifies the number of repeats for external command execution. + /// Delay to generate on the first execution in milliseconds. + /// Delay to generate on follow-up execution in milliseconds. + /// Task timeout in milliseconds. + /// + /// 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. + /// + [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); + + // Task under test: + var task = new ToolTaskThatSleeps + { + BuildEngine = new MockEngine(), + 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(); + 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); + } + } + } + + /// + /// A simple implementation of to sleep for a while. + /// + /// + /// This task runs shell command to sleep for predefined, variable amount of time based on how many times the + /// instance has been executed. + /// + private sealed class ToolTaskThatSleeps : ToolTask + { + // PowerShell command to sleep: + private readonly string _powerShellSleep = "-ExecutionPolicy RemoteSigned -Command \"Start-Sleep -Milliseconds {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: PowerShell for Windows, sh for UNIX-like systems: + _pathToShell = NativeMethodsShared.IsUnixLike ? "/bin/sh" : FindOnPath("PowerShell.exe"); + } + + /// + /// Gets or sets the delay for the first execution. + /// + /// + /// Defaults to 10 seconds. + /// + public Int32 InitialDelay { get; set; } = 10000; + + /// + /// Gets or sets the delay for the follow-up executions. + /// + /// + /// Defaults to 1 milliseconds. + /// + public Int32 FollowupDelay { get; set; } = 1; + + /// + /// Int32 output parameter for the repeat counter for test purpose. + /// + [Output] + public Int32 RepeatCount { get; private set; } = 0; + + /// + /// Gets the tool name (shell). + /// + protected override string ToolName => Path.GetFileName(_pathToShell); + + /// + /// Gets the full path to shell. + /// + protected override string GenerateFullPathToTool() => _pathToShell; + + /// + /// Generates a shell command to sleep different amount of time based on repeat counter. + /// + protected override string GenerateCommandLineCommands() => + NativeMethodsShared.IsUnixLike ? + string.Format(_unixSleep, RepeatCount < 2 ? InitialDelay / 1000.0 : FollowupDelay / 1000.0) : + string.Format(_powerShellSleep, RepeatCount < 2 ? InitialDelay : FollowupDelay); + + /// + /// Ensures that test parameters make sense. + /// + protected internal override bool ValidateParameters() => + (InitialDelay > 0) && (FollowupDelay > 0) && base.ValidateParameters(); + + /// + /// Runs shell command to sleep for a while. + /// + /// + /// true if the task runs successfully; false otherwise. + /// + public override bool Execute() + { + RepeatCount++; + return base.Execute(); + } + } } } diff --git a/src/Utilities/ToolTask.cs b/src/Utilities/ToolTask.cs index 2243faedfca..0abb6213e67 100644 --- a/src/Utilities/ToolTask.cs +++ b/src/Utilities/ToolTask.cs @@ -670,6 +670,7 @@ protected virtual int ExecuteTool( _standardOutputDataAvailable = new ManualResetEvent(false); _toolExited = new ManualResetEvent(false); + _terminatedTool = false; _toolTimeoutExpired = new ManualResetEvent(false); _eventsDisposed = false;