diff --git a/src/Utilities.UnitTests/ToolTask_Tests.cs b/src/Utilities.UnitTests/ToolTask_Tests.cs
index 77af6815973..b1842f49f2e 100644
--- a/src/Utilities.UnitTests/ToolTask_Tests.cs
+++ b/src/Utilities.UnitTests/ToolTask_Tests.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Globalization;
using System.IO;
using System.Resources;
using System.Text.RegularExpressions;
@@ -21,7 +22,7 @@ namespace Microsoft.Build.UnitTests
{
public sealed class ToolTask_Tests
{
- private ITestOutputHelper _output;
+ private readonly ITestOutputHelper _output;
public ToolTask_Tests(ITestOutputHelper testOutput)
{
@@ -995,50 +996,61 @@ public void SetsTerminationTimeoutCorrectly(int timeout, bool isInvalidValid)
/// 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.
+ /// Whether the first execution should be forced to time out before later retries succeed.
///
- /// These tests execute the same task instance multiple times, which will in turn run a shell command to sleep
+ /// These tests execute the same task instance multiple times, which will in turn run a command to sleep for a
/// 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)
+ [InlineData(1, false)]
+ [InlineData(3, false)]
+ [InlineData(3, true)]
+ public void ToolTaskThatTimeoutAndRetry(int repeats, bool timeoutOnFirstExecution)
{
using var env = TestEnvironment.Create(_output);
+ int fastDelayMilliseconds = 100;
+ int slowDelayMilliseconds = 5_000;
+ int timeoutMilliseconds = 2_000;
+
MockEngine3 engine = new();
// Task under test:
var task = new ToolTaskThatSleeps
{
BuildEngine = engine,
- InitialDelay = initialDelay,
- FollowupDelay = followupDelay,
- Timeout = timeout
+ InitialDelay = timeoutOnFirstExecution ? slowDelayMilliseconds : fastDelayMilliseconds,
+ FollowupDelay = fastDelayMilliseconds,
+ Timeout = timeoutOnFirstExecution ? timeoutMilliseconds : System.Threading.Timeout.Infinite
};
// Execute the same task instance multiple times. The index is one-based.
- bool result;
- for (int i = 1; i <= repeats; i++)
+ for (int attempt = 1; attempt <= repeats; attempt++)
{
- // Execute the task:
- result = task.Execute();
+ bool shouldSucceed = attempt > 1 || !timeoutOnFirstExecution;
+ bool result = task.Execute();
- _output.WriteLine(engine.Log);
+ _output.WriteLine(
+ $"Attempt {attempt}/{repeats}: expectedSuccess={shouldSucceed}, actualSuccess={result}, exitCode={task.ExitCode}.");
+
+ if (!string.IsNullOrEmpty(engine.Log))
+ {
+ _output.WriteLine(engine.Log);
+ engine.Log = string.Empty;
+ }
- task.RepeatCount.ShouldBe(i);
+ task.RepeatCount.ShouldBe(attempt);
+ result.ShouldBe(shouldSucceed);
- // The first execution may fail (timeout), but all following ones should succeed:
- if (i > 1)
+ if (shouldSucceed)
{
- result.ShouldBeTrue();
task.ExitCode.ShouldBe(0);
}
+ else
+ {
+ task.ExitCode.ShouldNotBe(0);
+ }
}
}
@@ -1108,25 +1120,21 @@ public void ToolTaskCapturesAllOutputWithFix()
/// 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.
+ /// This task invokes a direct sleep tool with a variable delay based on how many times the instance has been
+ /// executed, which avoids the flakiness of nesting the wait inside a shell.
///
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;
+ private readonly string _pathToTool;
public ToolTaskThatSleeps()
: base()
{
- // Determines shell to use: cmd for Windows, sh for UNIX-like systems:
- _pathToShell = NativeMethodsShared.IsUnixLike ? "/bin/sh" : "cmd.exe";
+ // timeout.exe exits immediately when ToolTask redirects stdin on Windows, so use ping.exe
+ // as the built-in blocking process for the timeout/retry scenario.
+ _pathToTool = NativeMethodsShared.IsUnixLike
+ ? "sleep"
+ : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "ping.exe");
}
///
@@ -1141,9 +1149,9 @@ public ToolTaskThatSleeps()
/// Gets or sets the delay for the follow-up executions.
///
///
- /// Defaults to 1 milliseconds.
+ /// Defaults to 100 milliseconds.
///
- public Int32 FollowupDelay { get; set; } = 1;
+ public Int32 FollowupDelay { get; set; } = 100;
///
/// Int32 output parameter for the repeat counter for test purpose.
@@ -1152,22 +1160,26 @@ public ToolTaskThatSleeps()
public Int32 RepeatCount { get; private set; } = 0;
///
- /// Gets the tool name (shell).
+ /// Gets the tool name.
///
- protected override string ToolName => Path.GetFileName(_pathToShell);
+ protected override string ToolName => Path.GetFileName(_pathToTool);
///
- /// Gets the full path to shell.
+ /// Gets the full path to the sleep tool.
///
- protected override string GenerateFullPathToTool() => _pathToShell;
+ protected override string GenerateFullPathToTool() => _pathToTool;
///
- /// Generates a shell command to sleep different amount of time based on repeat counter.
+ /// Generates the arguments to sleep for a 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(_windowsSleep, RepeatCount < 2 ? InitialDelay / 1000.0 : FollowupDelay / 1000.0);
+ protected override string GenerateCommandLineCommands()
+ {
+ int delay = RepeatCount < 2 ? InitialDelay : FollowupDelay;
+
+ return NativeMethodsShared.IsUnixLike
+ ? (delay / 1000.0).ToString("0.###", CultureInfo.InvariantCulture)
+ : $"-n {Math.Max(2, (int)Math.Ceiling(delay / 1000.0) + 1).ToString(CultureInfo.InvariantCulture)} 127.0.0.1";
+ }
///
/// Ensures that test parameters make sense.