File: RemoteTestOutputHelper.cs
Web Access
Project: src\tests\Aspire.Hosting.Azure.Tests\Aspire.Hosting.Azure.Tests.csproj (Aspire.Hosting.Azure.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.DotNet.RemoteExecutor;
 
namespace Aspire.Hosting.Azure.Tests;
 
// The remote process cannot use the parent's ITestOutputHelper directly. Write its logs to the
// child console so StartAndWait can redirect them to the parent test output helper.
internal sealed class RemoteTestOutputHelper : ITestOutputHelper
{
    public string Output => string.Empty;
 
    public void Write(string message) => Console.Write(message);
 
    public void Write(string format, params object[] args) => Console.Write(format, args);
 
    public void WriteLine(string message) => Console.WriteLine(message);
 
    public void WriteLine(string format, params object[] args) => Console.WriteLine(format, args);
 
    public static RemoteInvokeOptions CreateRemoteInvokeOptions()
    {
        var options = new RemoteInvokeOptions { Start = false };
        options.StartInfo.RedirectStandardError = true;
        options.StartInfo.RedirectStandardOutput = true;
        return options;
    }
 
    public static void StartAndWait(RemoteInvokeHandle handle, ITestOutputHelper testOutputHelper)
    {
        handle.Process.OutputDataReceived += (_, eventArgs) =>
        {
            if (eventArgs.Data is not null)
            {
                testOutputHelper.WriteLine($"[RemoteExecutor] {eventArgs.Data}");
            }
        };
        handle.Process.ErrorDataReceived += (_, eventArgs) =>
        {
            if (eventArgs.Data is not null)
            {
                testOutputHelper.WriteLine($"[RemoteExecutor] ERROR: {eventArgs.Data}");
            }
        };
 
        handle.Process.Start();
        handle.Process.BeginErrorReadLine();
        handle.Process.BeginOutputReadLine();
 
        if (!handle.Process.WaitForExit(handle.Options.TimeOut))
        {
            var message = $"Timed out after {handle.Options.TimeOut}ms waiting for remote process {handle.AssemblyName}!{handle.ClassName}.{handle.MethodName}.";
            testOutputHelper.WriteLine($"[RemoteExecutor] ERROR: {message}");
 
            // The non-zero exit code is expected because the timeout path terminates the child.
            // RemoteInvokeHandle.Dispose still checks for remote exceptions and releases its resources.
            handle.Options.CheckExitCode = false;
            try
            {
                handle.Process.Kill(entireProcessTree: true);
            }
            catch (InvalidOperationException)
            {
                // The process exited between WaitForExit returning false and Kill.
            }
 
            handle.Process.WaitForExit();
            throw new RemoteExecutionException(message);
        }
 
        // The timed overload waits for the process but not its asynchronous output handlers.
        handle.Process.WaitForExit();
    }
}