diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/Program.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/Program.cs index d403dfee35..481015709d 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/Program.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/Program.cs @@ -8,7 +8,6 @@ // Opt-out telemetry Environment.SetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", "1"); -CommandLine.MaxOutstandingCommands = Environment.ProcessorCount; DotnetCli.DoNotRetry = Debugger.IsAttached; ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); @@ -29,5 +28,4 @@ CompositeExtensionFactory slowestTestCompositeServiceFacto builder.TestHost.AddTestSessionLifetimeHandle(slowestTestCompositeServiceFactory); using ITestApplication app = await builder.BuildAsync(); int returnValue = await app.RunAsync(); -Console.WriteLine($"Process started: {CommandLine.TotalProcessesAttempt}"); return returnValue; diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Program.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Program.cs index ede99dd24b..4d8c63a599 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Program.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Program.cs @@ -8,7 +8,6 @@ // Opt-out telemetry Environment.SetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", "1"); -CommandLine.MaxOutstandingCommands = Environment.ProcessorCount; DotnetCli.DoNotRetry = Debugger.IsAttached; ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); @@ -30,5 +29,4 @@ CompositeExtensionFactory slowestTestCompositeServiceFacto builder.TestHost.AddTestSessionLifetimeHandle(slowestTestCompositeServiceFactory); using ITestApplication app = await builder.BuildAsync(); int returnValue = await app.RunAsync(); -Console.WriteLine($"Process started: {CommandLine.TotalProcessesAttempt}"); return returnValue; diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ServerMode/ServerModeTestsBase.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ServerMode/ServerModeTestsBase.cs index bc573cc7b7..05d8d83e30 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ServerMode/ServerModeTestsBase.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/ServerMode/ServerModeTestsBase.cs @@ -54,7 +54,7 @@ protected async Task StartAsServerAndConnectToTheClientAs EnvironmentVariables = environmentVariables, }; - IProcessHandle processHandler = ProcessFactory.Start(processConfig, cleanDefaultEnvironmentVariableIfCustomAreProvided: false); + (IProcessHandle processHandler, _) = ProcessFactory.Start(processConfig, cleanDefaultEnvironmentVariableIfCustomAreProvided: false); TcpClient? tcpClient; using CancellationTokenSource cancellationTokenSource = new(TimeSpan.FromSeconds(60)); diff --git a/test/Utilities/Microsoft.Testing.TestInfrastructure/CommandLine.cs b/test/Utilities/Microsoft.Testing.TestInfrastructure/CommandLine.cs index 51bf29848b..48f5ba177b 100644 --- a/test/Utilities/Microsoft.Testing.TestInfrastructure/CommandLine.cs +++ b/test/Utilities/Microsoft.Testing.TestInfrastructure/CommandLine.cs @@ -7,13 +7,6 @@ namespace Microsoft.Testing.TestInfrastructure; public sealed class CommandLine : IDisposable { - private static int s_totalProcessesAttempt; - [SuppressMessage("Style", "IDE0032:Use auto property", Justification = "It's causing some runtime issue")] - private static int s_maxOutstandingCommand = Environment.ProcessorCount; - private static SemaphoreSlim s_maxOutstandingCommands_semaphore = new(s_maxOutstandingCommand, s_maxOutstandingCommand); - - public static int TotalProcessesAttempt => s_totalProcessesAttempt; - private readonly List _errorOutputLines = []; private readonly List _standardOutputLines = []; private IProcessHandle? _process; @@ -26,18 +19,6 @@ public sealed class CommandLine : IDisposable public string ErrorOutput => string.Join(Environment.NewLine, _errorOutputLines); - public static int MaxOutstandingCommands - { - get => s_maxOutstandingCommand; - - set - { - s_maxOutstandingCommand = value; - s_maxOutstandingCommands_semaphore.Dispose(); - s_maxOutstandingCommands_semaphore = new SemaphoreSlim(s_maxOutstandingCommand, s_maxOutstandingCommand); - } - } - public async Task RunAsync( string commandLine, IDictionary? environmentVariables = null, @@ -75,30 +56,22 @@ public async Task RunAsyncAndReturnExitCodeAsync( bool cleanDefaultEnvironmentVariableIfCustomAreProvided = false, CancellationToken cancellationToken = default) { - await s_maxOutstandingCommands_semaphore.WaitAsync(cancellationToken); - try + (string command, string arguments) = GetCommandAndArguments(commandLine); + _errorOutputLines.Clear(); + _standardOutputLines.Clear(); + ProcessConfiguration startInfo = new(command) { - Interlocked.Increment(ref s_totalProcessesAttempt); - (string command, string arguments) = GetCommandAndArguments(commandLine); - _errorOutputLines.Clear(); - _standardOutputLines.Clear(); - ProcessConfiguration startInfo = new(command) - { - Arguments = arguments, - EnvironmentVariables = environmentVariables, - OnErrorOutput = (_, o) => _errorOutputLines.Add(ClearBOM(o)), - OnStandardOutput = (_, o) => _standardOutputLines.Add(ClearBOM(o)), - WorkingDirectory = workingDirectory, - }; - _process = ProcessFactory.Start(startInfo, cleanDefaultEnvironmentVariableIfCustomAreProvided); - - using CancellationTokenRegistration registration = cancellationToken.Register(() => _process.Kill()); - return await _process.WaitForExitAsync(cancellationToken); - } - finally - { - s_maxOutstandingCommands_semaphore.Release(); - } + Arguments = arguments, + EnvironmentVariables = environmentVariables, + OnErrorOutput = (_, o) => _errorOutputLines.Add(ClearBOM(o)), + OnStandardOutput = (_, o) => _standardOutputLines.Add(ClearBOM(o)), + WorkingDirectory = workingDirectory, + }; + (_process, Task outputAndErrorTask) = ProcessFactory.Start(startInfo, cleanDefaultEnvironmentVariableIfCustomAreProvided); + + using CancellationTokenRegistration registration = cancellationToken.Register(() => _process.Kill()); + await outputAndErrorTask; + return await _process.WaitForExitAsync(cancellationToken); } /// @@ -113,5 +86,11 @@ private static string ClearBOM(string outputLine) return firstChar == byteOrderMark ? outputLine[1..] : outputLine; } - public void Dispose() => _process?.Kill(); + public void Dispose() + { + if (_process?.HasExited == false) + { + _process.Kill(); + } + } } diff --git a/test/Utilities/Microsoft.Testing.TestInfrastructure/DotnetCli.cs b/test/Utilities/Microsoft.Testing.TestInfrastructure/DotnetCli.cs index 022edcbdea..f931e29bf7 100644 --- a/test/Utilities/Microsoft.Testing.TestInfrastructure/DotnetCli.cs +++ b/test/Utilities/Microsoft.Testing.TestInfrastructure/DotnetCli.cs @@ -29,22 +29,6 @@ public static class DotnetCli private static int s_binlogCounter; - [SuppressMessage("Style", "IDE0032:Use auto property", Justification = "It's causing some runtime bug")] - private static int s_maxOutstandingCommand = Environment.ProcessorCount; - private static SemaphoreSlim s_maxOutstandingCommands_semaphore = new(s_maxOutstandingCommand, s_maxOutstandingCommand); - - public static int MaxOutstandingCommands - { - get => s_maxOutstandingCommand; - - set - { - s_maxOutstandingCommand = value; - s_maxOutstandingCommands_semaphore.Dispose(); - s_maxOutstandingCommands_semaphore = new SemaphoreSlim(s_maxOutstandingCommand, s_maxOutstandingCommand); - } - } - public static bool DoNotRetry { get; set; } public static async Task RunAsync( @@ -61,69 +45,61 @@ public static async Task RunAsync( [CallerMemberName] string callerMemberName = "", CancellationToken cancellationToken = default) { - await s_maxOutstandingCommands_semaphore.WaitAsync(cancellationToken); - try + environmentVariables ??= []; + foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables()) { - environmentVariables ??= []; - foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables()) + // Skip all unwanted environment variables. + string? key = entry.Key.ToString(); + if (WellKnownEnvironmentVariables.ToSkipEnvironmentVariables.Contains(key, StringComparer.OrdinalIgnoreCase)) { - // Skip all unwanted environment variables. - string? key = entry.Key.ToString(); - if (WellKnownEnvironmentVariables.ToSkipEnvironmentVariables.Contains(key, StringComparer.OrdinalIgnoreCase)) - { - continue; - } + continue; + } - if (disableCodeCoverage) + if (disableCodeCoverage) + { + // Disable the code coverage during the build. + if (CodeCoverageEnvironmentVariables.Contains(key, StringComparer.OrdinalIgnoreCase)) { - // Disable the code coverage during the build. - if (CodeCoverageEnvironmentVariables.Contains(key, StringComparer.OrdinalIgnoreCase)) - { - continue; - } + continue; } - - // We use TryAdd to let tests "overwrite" existing environment variables. - // Consider that the given dictionary has "TESTINGPLATFORM_UI_LANGUAGE" as a key. - // And also Environment.GetEnvironmentVariables() is returning TESTINGPLATFORM_UI_LANGUAGE. - // In that case, we do a "TryAdd" which effectively means the value from the original dictionary wins. - environmentVariables.TryAdd(key!, entry.Value!.ToString()!); } - if (disableTelemetry) - { - environmentVariables.Add("DOTNET_CLI_TELEMETRY_OPTOUT", "1"); - } + // We use TryAdd to let tests "overwrite" existing environment variables. + // Consider that the given dictionary has "TESTINGPLATFORM_UI_LANGUAGE" as a key. + // And also Environment.GetEnvironmentVariables() is returning TESTINGPLATFORM_UI_LANGUAGE. + // In that case, we do a "TryAdd" which effectively means the value from the original dictionary wins. + environmentVariables.TryAdd(key!, entry.Value!.ToString()!); + } + + if (disableTelemetry) + { + environmentVariables.Add("DOTNET_CLI_TELEMETRY_OPTOUT", "1"); + } - environmentVariables["NUGET_PACKAGES"] = nugetGlobalPackagesFolder; + environmentVariables["NUGET_PACKAGES"] = nugetGlobalPackagesFolder; - string extraArgs = warnAsError ? " -p:MSBuildTreatWarningsAsErrors=true" : string.Empty; - extraArgs += suppressPreviewDotNetMessage ? " -p:SuppressNETCoreSdkPreviewMessage=true" : string.Empty; - if (args.IndexOf("-- ", StringComparison.Ordinal) is int platformArgsIndex && platformArgsIndex > 0) - { - args = args.Insert(platformArgsIndex, extraArgs + " "); - } - else - { - args += extraArgs; - } + string extraArgs = warnAsError ? " -p:MSBuildTreatWarningsAsErrors=true" : string.Empty; + extraArgs += suppressPreviewDotNetMessage ? " -p:SuppressNETCoreSdkPreviewMessage=true" : string.Empty; + if (args.IndexOf("-- ", StringComparison.Ordinal) is int platformArgsIndex && platformArgsIndex > 0) + { + args = args.Insert(platformArgsIndex, extraArgs + " "); + } + else + { + args += extraArgs; + } - if (DoNotRetry) - { - return await CallTheMuxerAsync(args, environmentVariables, workingDirectory, failIfReturnValueIsNotZero, callerMemberName, cancellationToken); - } - else - { - IEnumerable delay = Backoff.ExponentialBackoff(TimeSpan.FromSeconds(3), retryCount, factor: 1.5); - return await Policy - .Handle() - .WaitAndRetryAsync(delay) - .ExecuteAsync(async ct => await CallTheMuxerAsync(args, environmentVariables, workingDirectory, failIfReturnValueIsNotZero, callerMemberName, ct), cancellationToken); - } + if (DoNotRetry) + { + return await CallTheMuxerAsync(args, environmentVariables, workingDirectory, failIfReturnValueIsNotZero, callerMemberName, cancellationToken); } - finally + else { - s_maxOutstandingCommands_semaphore.Release(); + IEnumerable delay = Backoff.ExponentialBackoff(TimeSpan.FromSeconds(3), retryCount, factor: 1.5); + return await Policy + .Handle() + .WaitAndRetryAsync(delay) + .ExecuteAsync(async ct => await CallTheMuxerAsync(args, environmentVariables, workingDirectory, failIfReturnValueIsNotZero, callerMemberName, ct), cancellationToken); } } diff --git a/test/Utilities/Microsoft.Testing.TestInfrastructure/IProcessHandle.cs b/test/Utilities/Microsoft.Testing.TestInfrastructure/IProcessHandle.cs index 9307e6fdcc..d5cc832c68 100644 --- a/test/Utilities/Microsoft.Testing.TestInfrastructure/IProcessHandle.cs +++ b/test/Utilities/Microsoft.Testing.TestInfrastructure/IProcessHandle.cs @@ -15,6 +15,8 @@ public interface IProcessHandle TextReader StandardOutput { get; } + bool HasExited { get; } + void Dispose(); void Kill(); diff --git a/test/Utilities/Microsoft.Testing.TestInfrastructure/ProcessFactory.cs b/test/Utilities/Microsoft.Testing.TestInfrastructure/ProcessFactory.cs index a7a7b0a8fa..a7c1550c19 100644 --- a/test/Utilities/Microsoft.Testing.TestInfrastructure/ProcessFactory.cs +++ b/test/Utilities/Microsoft.Testing.TestInfrastructure/ProcessFactory.cs @@ -5,7 +5,7 @@ namespace Microsoft.Testing.TestInfrastructure; public static class ProcessFactory { - public static IProcessHandle Start(ProcessConfiguration config, bool cleanDefaultEnvironmentVariableIfCustomAreProvided = false) + public static (IProcessHandle Handle, Task OutputAndErrorTask) Start(ProcessConfiguration config, bool cleanDefaultEnvironmentVariableIfCustomAreProvided = false) { string fullPath = config.FileName; // Path.GetFullPath(startInfo.FileName); string workingDirectory = config.WorkingDirectory @@ -61,32 +61,34 @@ public static IProcessHandle Start(ProcessConfiguration config, bool cleanDefaul process.Exited += (s, e) => config.OnExit.Invoke(processHandle, process.ExitCode); } - if (config.OnStandardOutput != null) + if (!process.Start()) { - process.OutputDataReceived += (s, e) => + throw new InvalidOperationException("Process failed to start"); + } + + Task outputTask = Task.Factory.StartNew( + () => { - if (!string.IsNullOrWhiteSpace(e.Data)) + while (process.StandardOutput.ReadLine() is string line) { - config.OnStandardOutput(processHandle, e.Data); + if (!string.IsNullOrWhiteSpace(line)) + { + config.OnStandardOutput?.Invoke(processHandle, line); + } } - }; - } + }, TaskCreationOptions.LongRunning); - if (config.OnErrorOutput != null) - { - process.ErrorDataReceived += (s, e) => + Task errorTask = Task.Factory.StartNew( + () => { - if (!string.IsNullOrWhiteSpace(e.Data)) + while (process.StandardError.ReadLine() is string line) { - config.OnErrorOutput(processHandle, e.Data); + if (!string.IsNullOrWhiteSpace(line)) + { + config.OnErrorOutput?.Invoke(processHandle, line); + } } - }; - } - - if (!process.Start()) - { - throw new InvalidOperationException("Process failed to start"); - } + }, TaskCreationOptions.LongRunning); try { @@ -100,16 +102,6 @@ public static IProcessHandle Start(ProcessConfiguration config, bool cleanDefaul processHandleInfo.Id = process.Id; - if (config.OnStandardOutput != null) - { - process.BeginOutputReadLine(); - } - - if (config.OnErrorOutput != null) - { - process.BeginErrorReadLine(); - } - - return processHandle; + return (processHandle, Task.WhenAll(outputTask, errorTask)); } } diff --git a/test/Utilities/Microsoft.Testing.TestInfrastructure/ProcessHandle.cs b/test/Utilities/Microsoft.Testing.TestInfrastructure/ProcessHandle.cs index 31d4fc729c..18414926ca 100644 --- a/test/Utilities/Microsoft.Testing.TestInfrastructure/ProcessHandle.cs +++ b/test/Utilities/Microsoft.Testing.TestInfrastructure/ProcessHandle.cs @@ -24,6 +24,8 @@ internal ProcessHandle(Process process, ProcessHandleInfo processHandleInfo) public TextReader StandardOutput => _process.StandardOutput; + public bool HasExited => _process.HasExited; + public int ExitCode => _process.ExitCode; public async Task WaitForExitAsync(CancellationToken cancellationToken = default) diff --git a/test/Utilities/Microsoft.Testing.TestInfrastructure/TestHost.cs b/test/Utilities/Microsoft.Testing.TestInfrastructure/TestHost.cs index 91f9c992db..7113f56211 100644 --- a/test/Utilities/Microsoft.Testing.TestInfrastructure/TestHost.cs +++ b/test/Utilities/Microsoft.Testing.TestInfrastructure/TestHost.cs @@ -10,10 +10,6 @@ public sealed class TestHost { private readonly string _testHostModuleName; - [SuppressMessage("Style", "IDE0032:Use auto property", Justification = "It's causing runtime bug")] - private static int s_maxOutstandingExecutions = Environment.ProcessorCount; - private static SemaphoreSlim s_maxOutstandingExecutions_semaphore = new(s_maxOutstandingExecutions, s_maxOutstandingExecutions); - private TestHost(string testHostFullName, string testHostModuleName) { FullName = testHostFullName; @@ -21,18 +17,6 @@ private TestHost(string testHostFullName, string testHostModuleName) _testHostModuleName = testHostModuleName; } - public static int MaxOutstandingExecutions - { - get => s_maxOutstandingExecutions; - - set - { - s_maxOutstandingExecutions = value; - s_maxOutstandingExecutions_semaphore.Dispose(); - s_maxOutstandingExecutions_semaphore = new SemaphoreSlim(s_maxOutstandingExecutions, s_maxOutstandingExecutions); - } - } - public string FullName { get; } public string DirectoryName { get; } @@ -43,68 +27,60 @@ public async Task ExecuteAsync( bool disableTelemetry = true, CancellationToken cancellationToken = default) { - await s_maxOutstandingExecutions_semaphore.WaitAsync(cancellationToken); - try + if (command?.StartsWith(_testHostModuleName, StringComparison.OrdinalIgnoreCase) ?? false) { - if (command?.StartsWith(_testHostModuleName, StringComparison.OrdinalIgnoreCase) ?? false) - { - throw new InvalidOperationException($"Command should not start with module name '{_testHostModuleName}'."); - } + throw new InvalidOperationException($"Command should not start with module name '{_testHostModuleName}'."); + } - environmentVariables ??= []; + environmentVariables ??= []; - if (disableTelemetry) - { - environmentVariables.Add("DOTNET_CLI_TELEMETRY_OPTOUT", "1"); - } + if (disableTelemetry) + { + environmentVariables.Add("DOTNET_CLI_TELEMETRY_OPTOUT", "1"); + } - foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables()) + foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables()) + { + // Skip all unwanted environment variables. + string? key = entry.Key.ToString(); + if (WellKnownEnvironmentVariables.ToSkipEnvironmentVariables.Contains(key, StringComparer.OrdinalIgnoreCase)) { - // Skip all unwanted environment variables. - string? key = entry.Key.ToString(); - if (WellKnownEnvironmentVariables.ToSkipEnvironmentVariables.Contains(key, StringComparer.OrdinalIgnoreCase)) - { - continue; - } - - // We use TryAdd to let tests "overwrite" existing environment variables. - // Consider that the given dictionary has "TESTINGPLATFORM_UI_LANGUAGE" as a key. - // And also Environment.GetEnvironmentVariables() is returning TESTINGPLATFORM_UI_LANGUAGE. - // In that case, we do a "TryAdd" which effectively means the value from the original dictionary wins. - environmentVariables.TryAdd(key!, entry!.Value!.ToString()!); + continue; } - // Define DOTNET_ROOT to point to the dotnet we install for this repository, to avoid - // computer configuration having impact on our tests. - environmentVariables.Add("DOTNET_ROOT", $"{RootFinder.Find()}/.dotnet"); - - string finalArguments = command ?? string.Empty; - - IEnumerable delay = Backoff.ExponentialBackoff(TimeSpan.FromSeconds(3), retryCount: 5, factor: 1.5); - return await Policy - .Handle() - .WaitAndRetryAsync(delay) - .ExecuteAsync( - async ct => - { - CommandLine commandLine = new(); - // Disable ANSI rendering so tests have easier time parsing the output. - // Disable progress so tests don't mix progress with overall progress, and with test process output. - int exitCode = await commandLine.RunAsyncAndReturnExitCodeAsync( - $"{FullName} --no-ansi --no-progress {finalArguments}", - environmentVariables: environmentVariables, - workingDirectory: null, - cleanDefaultEnvironmentVariableIfCustomAreProvided: true, - cancellationToken: ct); - string fullCommand = command is not null ? $"{FullName} {command}" : FullName; - return new TestHostResult(fullCommand, exitCode, commandLine.StandardOutput, commandLine.StandardOutputLines, commandLine.ErrorOutput, commandLine.ErrorOutputLines); - }, - cancellationToken); - } - finally - { - s_maxOutstandingExecutions_semaphore.Release(); + // We use TryAdd to let tests "overwrite" existing environment variables. + // Consider that the given dictionary has "TESTINGPLATFORM_UI_LANGUAGE" as a key. + // And also Environment.GetEnvironmentVariables() is returning TESTINGPLATFORM_UI_LANGUAGE. + // In that case, we do a "TryAdd" which effectively means the value from the original dictionary wins. + environmentVariables.TryAdd(key!, entry!.Value!.ToString()!); } + + // Define DOTNET_ROOT to point to the dotnet we install for this repository, to avoid + // computer configuration having impact on our tests. + environmentVariables.Add("DOTNET_ROOT", $"{RootFinder.Find()}/.dotnet"); + + string finalArguments = command ?? string.Empty; + + IEnumerable delay = Backoff.ExponentialBackoff(TimeSpan.FromSeconds(3), retryCount: 5, factor: 1.5); + return await Policy + .Handle() + .WaitAndRetryAsync(delay) + .ExecuteAsync( + async ct => + { + CommandLine commandLine = new(); + // Disable ANSI rendering so tests have easier time parsing the output. + // Disable progress so tests don't mix progress with overall progress, and with test process output. + int exitCode = await commandLine.RunAsyncAndReturnExitCodeAsync( + $"{FullName} --no-ansi --no-progress {finalArguments}", + environmentVariables: environmentVariables, + workingDirectory: null, + cleanDefaultEnvironmentVariableIfCustomAreProvided: true, + cancellationToken: ct); + string fullCommand = command is not null ? $"{FullName} {command}" : FullName; + return new TestHostResult(fullCommand, exitCode, commandLine.StandardOutput, commandLine.StandardOutputLines, commandLine.ErrorOutput, commandLine.ErrorOutputLines); + }, + cancellationToken); } public static TestHost LocateFrom(string rootFolder, string testHostModuleNameWithoutExtension)