From a65df9190dcbc6e801f1a453a46db02692203c89 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Mon, 19 Jan 2026 16:35:49 +0100 Subject: [PATCH 01/13] Add test for hanging child process for MTP --- .../MTPChildProcessHangTest.csproj | 17 +++++++ .../MTPChildProcessHangTest/Program.cs | 48 +++++++++++++++++++ .../Test/GivenDotnetTestBuildsAndRunsTests.cs | 15 ++++++ 3 files changed, 80 insertions(+) create mode 100644 test/TestAssets/TestProjects/MTPChildProcessHangTest/MTPChildProcessHangTest.csproj create mode 100644 test/TestAssets/TestProjects/MTPChildProcessHangTest/Program.cs diff --git a/test/TestAssets/TestProjects/MTPChildProcessHangTest/MTPChildProcessHangTest.csproj b/test/TestAssets/TestProjects/MTPChildProcessHangTest/MTPChildProcessHangTest.csproj new file mode 100644 index 000000000000..8699e5d53e86 --- /dev/null +++ b/test/TestAssets/TestProjects/MTPChildProcessHangTest/MTPChildProcessHangTest.csproj @@ -0,0 +1,17 @@ + + + + + $(CurrentTargetFramework) + Exe + + enable + enable + + false + + + + + + diff --git a/test/TestAssets/TestProjects/MTPChildProcessHangTest/Program.cs b/test/TestAssets/TestProjects/MTPChildProcessHangTest/Program.cs new file mode 100644 index 000000000000..3f750d1624cb --- /dev/null +++ b/test/TestAssets/TestProjects/MTPChildProcessHangTest/Program.cs @@ -0,0 +1,48 @@ +using System.Diagnostics; +using Microsoft.Testing.Extensions; +using Microsoft.Testing.Platform.Builder; +using Microsoft.Testing.Platform.Capabilities.TestFramework; +using Microsoft.Testing.Platform.Extensions.TestFramework; + +if (args.Length == 1 && args[0] == "hang") +{ + var @event = new ManualResetEvent(false); + @event.WaitOne(); + return 0; +} + +var builder = await TestApplication.CreateBuilderAsync(args); +builder.RegisterTestFramework(_ => new TestFrameworkCapabilities(), (_, _) => new MyTestFramework()); +using var testApp = await builder.BuildAsync(); +return await testApp.RunAsync(); + +internal class MyTestFramework : ITestFramework +{ + public string Uid => nameof(MyTestFramework); + public string Version => "1.0.0"; + public string DisplayName => nameof(MyTestFramework); + public string Description => DisplayName; + public Task CloseTestSessionAsync(CloseTestSessionContext context) + { + return Task.FromResult(new CloseTestSessionResult() { IsSuccess = true }); + } + public Task CreateTestSessionAsync(CreateTestSessionContext context) + => Task.FromResult(new CreateTestSessionResult() { IsSuccess = true }); + public Task ExecuteRequestAsync(ExecuteRequestContext context) + { + var fileName = Process.GetCurrentProcess().MainModule.FileName; + var p = Process.Start(new ProcessStartInfo(fileName, "hang") + { + RedirectStandardOutput = true, + RedirectStandardError = true, + }); + p.BeginOutputReadLine(); + p.BeginErrorReadLine(); + p.ErrorDataReceived += (sender, e) => { }; + p.OutputDataReceived += (sender, e) => { }; + context.Complete(); + return Task.CompletedTask; + } + public Task IsEnabledAsync() + => Task.FromResult(true); +} diff --git a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs index 307e257ed6c1..dfd72114db16 100644 --- a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs +++ b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs @@ -588,5 +588,20 @@ public void RunTestProjectWithEnvVariable(string configuration) result.ExitCode.Should().Be(ExitCodes.AtLeastOneTestFailed); } + + [InlineData(TestingConstants.Debug)] + [InlineData(TestingConstants.Release)] + [Theory] + public void DotnetTest_MTPChildProcessHangTestProject_ShouldNotHang(string configuration) + { + var testInstance = _testAssetsManager.CopyTestAsset("MTPChildProcessHangTest", Guid.NewGuid().ToString()) + .WithSource(); + + var result = new DotnetTestCommand(Log, disableNewOutput: false) + .WithWorkingDirectory(testInstance.Path) + .Execute(TestCommandDefinition.ConfigurationOption.Name, configuration); + + result.ExitCode.Should().Be(8); + } } } From fcac558db1cfb1e20c035d14d45115fdb23448b6 Mon Sep 17 00:00:00 2001 From: Youssef Victor Date: Tue, 20 Jan 2026 00:37:10 +0100 Subject: [PATCH 02/13] Create global.json --- .../TestProjects/MTPChildProcessHangTest/global.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 test/TestAssets/TestProjects/MTPChildProcessHangTest/global.json diff --git a/test/TestAssets/TestProjects/MTPChildProcessHangTest/global.json b/test/TestAssets/TestProjects/MTPChildProcessHangTest/global.json new file mode 100644 index 000000000000..9009caf0ba8f --- /dev/null +++ b/test/TestAssets/TestProjects/MTPChildProcessHangTest/global.json @@ -0,0 +1,5 @@ +{ + "test": { + "runner": "Microsoft.Testing.Platform" + } +} From 423b0142e755f127eb6b2f4339f93cc219de8cd3 Mon Sep 17 00:00:00 2001 From: Youssef Victor Date: Tue, 20 Jan 2026 01:59:26 +0100 Subject: [PATCH 03/13] Update Program.cs --- test/TestAssets/TestProjects/MTPChildProcessHangTest/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/TestAssets/TestProjects/MTPChildProcessHangTest/Program.cs b/test/TestAssets/TestProjects/MTPChildProcessHangTest/Program.cs index 3f750d1624cb..30683efa1f10 100644 --- a/test/TestAssets/TestProjects/MTPChildProcessHangTest/Program.cs +++ b/test/TestAssets/TestProjects/MTPChildProcessHangTest/Program.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using Microsoft.Testing.Extensions; using Microsoft.Testing.Platform.Builder; using Microsoft.Testing.Platform.Capabilities.TestFramework; using Microsoft.Testing.Platform.Extensions.TestFramework; From 4bd0e84c6e1b8c98d666c09fbe8a5b56b55b41e4 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Tue, 20 Jan 2026 12:51:53 +0100 Subject: [PATCH 04/13] Attempt to fix hang --- .../Commands/Test/MTP/TestApplication.cs | 68 +++++++++++++++++-- .../Test/GivenDotnetTestBuildsAndRunsTests.cs | 2 +- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs b/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs index 2ac6fb682cc2..0ea51a8b87a5 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs @@ -57,14 +57,37 @@ public async Task RunAsync() // Note: even with 'process.StandardOutput.ReadToEndAsync()' or 'process.BeginOutputReadLine()', we ended up with // many TP threads just doing synchronous IO, slowing down the progress of the test run. // We want to read requests coming through the pipe and sending responses back to the test app as fast as possible. - var stdOutTask = Task.Factory.StartNew(static standardOutput => ((StreamReader)standardOutput!).ReadToEnd(), process.StandardOutput, TaskCreationOptions.LongRunning); - var stdErrTask = Task.Factory.StartNew(static standardError => ((StreamReader)standardError!).ReadToEnd(), process.StandardError, TaskCreationOptions.LongRunning); + var stdOutBuilder = new StringBuilder(); + var stdErrBuilder = new StringBuilder(); - var outputAndError = await Task.WhenAll(stdOutTask, stdErrTask); - await process.WaitForExitAsync(); + var stdOutTask = Task.Factory.StartNew(() => + { + var stdOut = process.StandardOutput; + string? currentLine; + while ((currentLine = stdOut.ReadLine()) is not null) + { + stdOutBuilder.AppendLine(currentLine); + } + }, TaskCreationOptions.LongRunning); + + var stdErrTask = Task.Factory.StartNew(() => + { + var stdErr = process.StandardError; + string? currentLine; + while ((currentLine = stdErr.ReadLine()) is not null) + { + stdErrBuilder.AppendLine(currentLine); + } + }, TaskCreationOptions.LongRunning); + + await WaitForExitWithoutOutputAsync(process); + + // At this point, process already exited. Allow for 5 seconds to consume stdout/stderr. + // We might not be able to consume all the output if the test app has exited but left a child process alive. + await Task.WhenAll(stdOutTask, stdErrTask).WaitAsync(TimeSpan.FromSeconds(5)); var exitCode = process.ExitCode; - _handler.OnTestProcessExited(exitCode, outputAndError[0], outputAndError[1]); + _handler.OnTestProcessExited(exitCode, stdOutBuilder.ToString(), stdErrBuilder.ToString()); // This condition is to prevent considering the test app as successful when we didn't receive test session end. // We don't produce the exception if the exit code is already non-zero to avoid surfacing this exception when there is already a known failure. @@ -85,6 +108,41 @@ public async Task RunAsync() } } + private static async Task WaitForExitWithoutOutputAsync(Process process) + { + // Mostly copied from WaitForExitAsync from dotnet/runtime, with adjustments to match our needs here. + try + { + process.EnableRaisingEvents = true; + } + catch (InvalidOperationException) + { + if (process.HasExited) + { + return; + } + + throw; + } + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + EventHandler handler = (_, _) => tcs.TrySetResult(); + process.Exited += handler; + + try + { + if (!process.HasExited) + { + await tcs.Task.ConfigureAwait(false); + } + } + finally + { + process.Exited -= handler; + } + } + private ProcessStartInfo CreateProcessStartInfo() { var processStartInfo = new ProcessStartInfo diff --git a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs index dfd72114db16..f5054a28c203 100644 --- a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs +++ b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs @@ -601,7 +601,7 @@ public void DotnetTest_MTPChildProcessHangTestProject_ShouldNotHang(string confi .WithWorkingDirectory(testInstance.Path) .Execute(TestCommandDefinition.ConfigurationOption.Name, configuration); - result.ExitCode.Should().Be(8); + result.ExitCode.Should().Be(ExitCodes.ZeroTests); } } } From 06ddbb3a5672bffe31351ab9b4ab281e11760dc1 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Tue, 10 Feb 2026 16:52:51 +0100 Subject: [PATCH 05/13] Handle the 5 sec timeout --- src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs b/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs index 0ea51a8b87a5..c0c5c8651549 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs @@ -84,7 +84,13 @@ public async Task RunAsync() // At this point, process already exited. Allow for 5 seconds to consume stdout/stderr. // We might not be able to consume all the output if the test app has exited but left a child process alive. - await Task.WhenAll(stdOutTask, stdErrTask).WaitAsync(TimeSpan.FromSeconds(5)); + try + { + await Task.WhenAll(stdOutTask, stdErrTask).WaitAsync(TimeSpan.FromSeconds(5)); + } + catch (TimeoutException) + { + } var exitCode = process.ExitCode; _handler.OnTestProcessExited(exitCode, stdOutBuilder.ToString(), stdErrBuilder.ToString()); From bf322f59467532fed2249237bf5c68dc3898152e Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Tue, 10 Feb 2026 17:18:41 +0100 Subject: [PATCH 06/13] Ignore test --- .../CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs index f5054a28c203..6c705ee63ed0 100644 --- a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs +++ b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs @@ -591,7 +591,7 @@ public void RunTestProjectWithEnvVariable(string configuration) [InlineData(TestingConstants.Debug)] [InlineData(TestingConstants.Release)] - [Theory] + [Theory(Skip = "Works manually. The test here still hangs because the test infra redirects stdout/stderr and will be waiting for the hanging process.")] public void DotnetTest_MTPChildProcessHangTestProject_ShouldNotHang(string configuration) { var testInstance = _testAssetsManager.CopyTestAsset("MTPChildProcessHangTest", Guid.NewGuid().ToString()) From 587f2fda36dd8cbf44ec461eea98e34b4969ea54 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Tue, 10 Feb 2026 18:38:35 +0100 Subject: [PATCH 07/13] Fix build --- .../CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs index 6c705ee63ed0..77ab8803bd0d 100644 --- a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs +++ b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs @@ -594,7 +594,7 @@ public void RunTestProjectWithEnvVariable(string configuration) [Theory(Skip = "Works manually. The test here still hangs because the test infra redirects stdout/stderr and will be waiting for the hanging process.")] public void DotnetTest_MTPChildProcessHangTestProject_ShouldNotHang(string configuration) { - var testInstance = _testAssetsManager.CopyTestAsset("MTPChildProcessHangTest", Guid.NewGuid().ToString()) + var testInstance = TestAssetsManager.CopyTestAsset("MTPChildProcessHangTest", Guid.NewGuid().ToString()) .WithSource(); var result = new DotnetTestCommand(Log, disableNewOutput: false) From 1d9adece2563b102cd49b9a7ce13f3296b9e7e5c Mon Sep 17 00:00:00 2001 From: Youssef Victor Date: Tue, 10 Feb 2026 22:52:39 +0100 Subject: [PATCH 08/13] Fix build --- .../CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs index 77ab8803bd0d..ed4bccc48bf1 100644 --- a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs +++ b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs @@ -599,7 +599,7 @@ public void DotnetTest_MTPChildProcessHangTestProject_ShouldNotHang(string confi var result = new DotnetTestCommand(Log, disableNewOutput: false) .WithWorkingDirectory(testInstance.Path) - .Execute(TestCommandDefinition.ConfigurationOption.Name, configuration); + .Execute("-c", configuration); result.ExitCode.Should().Be(ExitCodes.ZeroTests); } From dc5c3bdd0ffdf0a9351d6bdbeb910dd18f4f4413 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Wed, 11 Feb 2026 10:31:03 +0100 Subject: [PATCH 09/13] Switch to ConcurrentQueue --- src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs b/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs index c0c5c8651549..7618a1222b1b 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Concurrent; using System.Diagnostics; using System.Globalization; using System.IO; @@ -57,8 +58,10 @@ public async Task RunAsync() // Note: even with 'process.StandardOutput.ReadToEndAsync()' or 'process.BeginOutputReadLine()', we ended up with // many TP threads just doing synchronous IO, slowing down the progress of the test run. // We want to read requests coming through the pipe and sending responses back to the test app as fast as possible. - var stdOutBuilder = new StringBuilder(); - var stdErrBuilder = new StringBuilder(); + // We are using ConcurrentQueue to avoid thread-safety issues for the timeout case. + // In the timeout case, we leave stdOutTask and stdErrTask running, just we stop observing them. + var stdOutBuilder = new ConcurrentQueue(); + var stdErrBuilder = new ConcurrentQueue(); var stdOutTask = Task.Factory.StartNew(() => { @@ -66,7 +69,7 @@ public async Task RunAsync() string? currentLine; while ((currentLine = stdOut.ReadLine()) is not null) { - stdOutBuilder.AppendLine(currentLine); + stdOutBuilder.Enqueue(currentLine); } }, TaskCreationOptions.LongRunning); @@ -76,7 +79,7 @@ public async Task RunAsync() string? currentLine; while ((currentLine = stdErr.ReadLine()) is not null) { - stdErrBuilder.AppendLine(currentLine); + stdErrBuilder.Enqueue(currentLine); } }, TaskCreationOptions.LongRunning); @@ -93,7 +96,7 @@ public async Task RunAsync() } var exitCode = process.ExitCode; - _handler.OnTestProcessExited(exitCode, stdOutBuilder.ToString(), stdErrBuilder.ToString()); + _handler.OnTestProcessExited(exitCode, string.Join(Environment.NewLine, stdOutBuilder), string.Join(Environment.NewLine, stdErrBuilder)); // This condition is to prevent considering the test app as successful when we didn't receive test session end. // We don't produce the exception if the exit code is already non-zero to avoid surfacing this exception when there is already a known failure. From 4df0d09c8860cb3dd998a17c46f73ecc54e14bd2 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Wed, 11 Feb 2026 12:25:43 +0100 Subject: [PATCH 10/13] Unskip test, adjust test infra to allow not capturing stdout/stderr --- .../Commands/Test/MTP/TestApplication.cs | 1 + .../Commands/SdkCommandSpec.cs | 2 + .../Commands/TestCommand.cs | 46 ++++++++++++------- .../Test/GivenDotnetTestBuildsAndRunsTests.cs | 3 +- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs b/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs index 7618a1222b1b..cc2aa2252f1a 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs @@ -120,6 +120,7 @@ public async Task RunAsync() private static async Task WaitForExitWithoutOutputAsync(Process process) { // Mostly copied from WaitForExitAsync from dotnet/runtime, with adjustments to match our needs here. + // Basically, we don't want to wait for the output streams, and we also simplify logic around CancellationToken as we don't need to pass one. try { process.EnableRaisingEvents = true; diff --git a/test/Microsoft.NET.TestFramework/Commands/SdkCommandSpec.cs b/test/Microsoft.NET.TestFramework/Commands/SdkCommandSpec.cs index 4b13f122d4ac..9bc513f6ff6c 100644 --- a/test/Microsoft.NET.TestFramework/Commands/SdkCommandSpec.cs +++ b/test/Microsoft.NET.TestFramework/Commands/SdkCommandSpec.cs @@ -19,6 +19,8 @@ public class SdkCommandSpec public bool RedirectStandardInput { get; set; } + public bool DisableOutputAndErrorRedirection { get; set; } + private string EscapeArgs() { // Note: this doesn't handle invoking .cmd files via "cmd /c" on Windows, which probably won't be necessary here diff --git a/test/Microsoft.NET.TestFramework/Commands/TestCommand.cs b/test/Microsoft.NET.TestFramework/Commands/TestCommand.cs index 62eafc09d6ca..56cf1002d2ae 100644 --- a/test/Microsoft.NET.TestFramework/Commands/TestCommand.cs +++ b/test/Microsoft.NET.TestFramework/Commands/TestCommand.cs @@ -22,6 +22,8 @@ public abstract class TestCommand public bool RedirectStandardInput { get; set; } + public bool DisableOutputAndErrorRedirection { get; set; } + // These only work via Execute(), not when using GetProcessStartInfo() public Action? CommandOutputHandler { get; set; } public Action? ProcessStartedHandler { get; set; } @@ -47,6 +49,12 @@ public TestCommand WithWorkingDirectory(string workingDirectory) return this; } + public TestCommand WithDisableOutputAndErrorRedirection() + { + DisableOutputAndErrorRedirection = true; + return this; + } + public TestCommand WithStandardInput(string stdin) { Debug.Assert(ProcessStartedHandler == null); @@ -107,6 +115,7 @@ private SdkCommandSpec CreateCommandSpec(IEnumerable args) } commandSpec.RedirectStandardInput = RedirectStandardInput; + commandSpec.DisableOutputAndErrorRedirection = DisableOutputAndErrorRedirection; return commandSpec; } @@ -147,24 +156,27 @@ public virtual CommandResult Execute(IEnumerable args) var spec = CreateCommandSpec(args); var command = spec - .ToCommand(_doNotEscapeArguments) - .CaptureStdOut() - .CaptureStdErr(); + .ToCommand(_doNotEscapeArguments); - command.OnOutputLine(line => + if (!spec.DisableOutputAndErrorRedirection) { - Log.WriteLine($"》{line}"); - CommandOutputHandler?.Invoke(line); - }); - - command.OnErrorLine(line => - { - Log.WriteLine($"❌{line}"); - }); - - if (StandardOutputEncoding is not null) - { - command.StandardOutputEncoding(StandardOutputEncoding); + command + .CaptureStdOut() + .CaptureStdErr() + .OnOutputLine(line => + { + Log.WriteLine($"》{line}"); + CommandOutputHandler?.Invoke(line); + }) + .OnErrorLine(line => + { + Log.WriteLine($"❌{line}"); + }); + + if (StandardOutputEncoding is not null) + { + command.StandardOutputEncoding(StandardOutputEncoding); + } } string fileToShow = Path.GetFileNameWithoutExtension(spec.FileName!).Equals("dotnet", StringComparison.OrdinalIgnoreCase) ? @@ -173,7 +185,7 @@ public virtual CommandResult Execute(IEnumerable args) var display = $"{fileToShow} {string.Join(" ", spec.Arguments)}"; Log.WriteLine($"Executing '{display}':"); - var result = ((Command)command).Execute(ProcessStartedHandler); + var result = command.Execute(ProcessStartedHandler); Log.WriteLine($"Command '{display}' exited with exit code {result.ExitCode}."); if (Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT") is string uploadRoot) diff --git a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs index ed4bccc48bf1..6200513107b6 100644 --- a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs +++ b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs @@ -591,7 +591,7 @@ public void RunTestProjectWithEnvVariable(string configuration) [InlineData(TestingConstants.Debug)] [InlineData(TestingConstants.Release)] - [Theory(Skip = "Works manually. The test here still hangs because the test infra redirects stdout/stderr and will be waiting for the hanging process.")] + [Theory] public void DotnetTest_MTPChildProcessHangTestProject_ShouldNotHang(string configuration) { var testInstance = TestAssetsManager.CopyTestAsset("MTPChildProcessHangTest", Guid.NewGuid().ToString()) @@ -599,6 +599,7 @@ public void DotnetTest_MTPChildProcessHangTestProject_ShouldNotHang(string confi var result = new DotnetTestCommand(Log, disableNewOutput: false) .WithWorkingDirectory(testInstance.Path) + .WithDisableOutputAndErrorRedirection() .Execute("-c", configuration); result.ExitCode.Should().Be(ExitCodes.ZeroTests); From 3a8e36e589b0af3f134b9b6118ec997f1da44b42 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Wed, 11 Feb 2026 14:24:53 +0100 Subject: [PATCH 11/13] Try simplification --- .../Commands/Test/MTP/TestApplication.cs | 40 ++----------------- 1 file changed, 3 insertions(+), 37 deletions(-) diff --git a/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs b/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs index cc2aa2252f1a..d5d733a17044 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs @@ -83,7 +83,9 @@ public async Task RunAsync() } }, TaskCreationOptions.LongRunning); - await WaitForExitWithoutOutputAsync(process); + // WaitForExitAsync only waits for process exit (and doesn't wait for output) for our usage here. + // If we use BeginOutputReadLine/BeginErrorReadLine, it will also wait for output which can deadlock. + await process.WaitForExitAsync(); // At this point, process already exited. Allow for 5 seconds to consume stdout/stderr. // We might not be able to consume all the output if the test app has exited but left a child process alive. @@ -117,42 +119,6 @@ public async Task RunAsync() } } - private static async Task WaitForExitWithoutOutputAsync(Process process) - { - // Mostly copied from WaitForExitAsync from dotnet/runtime, with adjustments to match our needs here. - // Basically, we don't want to wait for the output streams, and we also simplify logic around CancellationToken as we don't need to pass one. - try - { - process.EnableRaisingEvents = true; - } - catch (InvalidOperationException) - { - if (process.HasExited) - { - return; - } - - throw; - } - - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - EventHandler handler = (_, _) => tcs.TrySetResult(); - process.Exited += handler; - - try - { - if (!process.HasExited) - { - await tcs.Task.ConfigureAwait(false); - } - } - finally - { - process.Exited -= handler; - } - } - private ProcessStartInfo CreateProcessStartInfo() { var processStartInfo = new ProcessStartInfo From 78626d0d8bda92dca196dce9720cec6eca4b9f7d Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Wed, 11 Feb 2026 14:26:04 +0100 Subject: [PATCH 12/13] Add comment to clarify --- .../CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs index 6200513107b6..5b550cb7c824 100644 --- a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs +++ b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs @@ -599,6 +599,8 @@ public void DotnetTest_MTPChildProcessHangTestProject_ShouldNotHang(string confi var result = new DotnetTestCommand(Log, disableNewOutput: false) .WithWorkingDirectory(testInstance.Path) + // We need to disable output and error redirection so that the test infra doesn't + // hang because of a hanging child process that keeps out/err open. .WithDisableOutputAndErrorRedirection() .Execute("-c", configuration); From 0378ccf8554744e387bdd4da2851b16304ed0f49 Mon Sep 17 00:00:00 2001 From: Youssef Victor Date: Sat, 7 Mar 2026 12:31:48 +0100 Subject: [PATCH 13/13] Fix test for 10.0.3xx branch --- .../CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs index 5b550cb7c824..dd2b8a359546 100644 --- a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs +++ b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs @@ -594,7 +594,7 @@ public void RunTestProjectWithEnvVariable(string configuration) [Theory] public void DotnetTest_MTPChildProcessHangTestProject_ShouldNotHang(string configuration) { - var testInstance = TestAssetsManager.CopyTestAsset("MTPChildProcessHangTest", Guid.NewGuid().ToString()) + var testInstance = _testAssetsManager.CopyTestAsset("MTPChildProcessHangTest", Guid.NewGuid().ToString()) .WithSource(); var result = new DotnetTestCommand(Log, disableNewOutput: false)