File: Infrastructure\TestCommand.cs
Web Access
Project: src\test\ProjectTemplates\Microsoft.Extensions.AI.Templates.IntegrationTests\Microsoft.Extensions.AI.Templates.Tests.csproj (Microsoft.Extensions.AI.Templates.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;
 
namespace Microsoft.Extensions.AI.Templates.Tests;
 
public abstract class TestCommand
{
    public string? FileName { get; set; }
 
    public string? WorkingDirectory { get; set; }
 
    public TimeSpan? Timeout { get; set; }
 
    public List<string> Arguments { get; } = [];
 
    public Dictionary<string, string> EnvironmentVariables = [];
 
    public virtual async Task<TestCommandResult> ExecuteAsync(ITestOutputHelper outputHelper)
    {
        if (string.IsNullOrEmpty(FileName))
        {
            throw new InvalidOperationException($"The {nameof(TestCommand)} did not specify an executable file name.");
        }
 
        var processStartInfo = new ProcessStartInfo(FileName, Arguments)
        {
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            CreateNoWindow = true,
            UseShellExecute = false,
        };
 
        if (WorkingDirectory is not null)
        {
            processStartInfo.WorkingDirectory = WorkingDirectory;
        }
 
        foreach (var (key, value) in EnvironmentVariables)
        {
            processStartInfo.EnvironmentVariables[key] = value;
        }
 
        var exitedTcs = new TaskCompletionSource();
        var standardOutputBuilder = new StringBuilder();
        var standardErrorBuilder = new StringBuilder();
 
        using var process = new Process
        {
            StartInfo = processStartInfo,
        };
 
        process.EnableRaisingEvents = true;
        process.OutputDataReceived += MakeOnDataReceivedHandler(standardOutputBuilder);
        process.ErrorDataReceived += MakeOnDataReceivedHandler(standardErrorBuilder);
        process.Exited += (sender, args) =>
        {
            exitedTcs.SetResult();
        };
 
        DataReceivedEventHandler MakeOnDataReceivedHandler(StringBuilder outputBuilder) => (sender, args) =>
        {
            if (args.Data is null)
            {
                return;
            }
 
            lock (outputBuilder)
            {
                outputBuilder.AppendLine(args.Data);
            }
 
            lock (outputHelper)
            {
                outputHelper.WriteLine(args.Data);
            }
        };
 
        outputHelper.WriteLine($"Executing '{processStartInfo.FileName} {string.Join(" ", Arguments)}' in working directory '{processStartInfo.WorkingDirectory}'");
 
        using var timeoutCts = new CancellationTokenSource();
        if (Timeout is { } timeout)
        {
            timeoutCts.CancelAfter(timeout);
        }
 
        var startTimestamp = Stopwatch.GetTimestamp();
 
        try
        {
            process.Start();
            process.BeginOutputReadLine();
            process.BeginErrorReadLine();
 
            await exitedTcs.Task.WaitAsync(timeoutCts.Token).ConfigureAwait(false);
            await process.WaitForExitAsync(timeoutCts.Token).ConfigureAwait(false);
 
            var elapsedTime = Stopwatch.GetElapsedTime(startTimestamp);
            outputHelper.WriteLine($"Process ran for {elapsedTime} seconds.");
 
            return new(standardOutputBuilder, standardErrorBuilder, process.ExitCode);
        }
        catch (Exception ex)
        {
            outputHelper.WriteLine($"An exception occurred: {ex}");
            throw;
        }
        finally
        {
            if (!process.TryGetHasExited())
            {
                var elapsedTime = Stopwatch.GetElapsedTime(startTimestamp);
                outputHelper.WriteLine($"The process has been running for {elapsedTime} seconds. Terminating the process.");
                process.Kill(entireProcessTree: true);
            }
        }
    }
}