| File: Utils\TestProcessRunner.cs | Web Access |
| Project: src\tests\Aspire.Hosting.TestUtilities\Aspire.Hosting.TestUtilities.csproj (Aspire.Hosting.TestUtilities) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.Dcp.Process; namespace Aspire.Hosting.Tests.Utils; internal sealed class TestProcessRunner : IProcessRunner { private readonly object _lock = new(); private readonly Queue<TestProcessRun> _runs = []; private readonly List<ProcessSpec> _processSpecs = []; private readonly List<TestProcessDisposable> _disposables = []; public IReadOnlyList<ProcessSpec> ProcessSpecs { get { lock (_lock) { return [.. _processSpecs]; } } } public IReadOnlyList<TestProcessDisposable> Disposables { get { lock (_lock) { return [.. _disposables]; } } } public TaskCompletionSource<ProcessSpec> RunStarted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); public void EnqueueResult( int exitCode = 0, IReadOnlyList<string>? output = null, IReadOnlyList<string>? error = null, int? totalOutputLineCount = null, IReadOnlyList<TestProcessOutput>? outputEvents = null) { lock (_lock) { _runs.Enqueue(TestProcessRun.Result(exitCode, output, error, totalOutputLineCount, outputEvents)); } } public void EnqueueException(Exception exception) { lock (_lock) { _runs.Enqueue(TestProcessRun.Failed(exception)); } } public void EnqueuePending(Task<ProcessResult> processResult) { lock (_lock) { _runs.Enqueue(TestProcessRun.Pending(processResult)); } } public (Task<ProcessResult>, IAsyncDisposable) Run(ProcessSpec processSpec) { TestProcessRun run; TestProcessDisposable disposable; lock (_lock) { _processSpecs.Add(processSpec); disposable = new TestProcessDisposable(); _disposables.Add(disposable); run = _runs.Count > 0 ? _runs.Dequeue() : TestProcessRun.Result(); } RunStarted.TrySetResult(processSpec); if (run.FailureException is { } exception) { throw exception; } if (run.PendingResult is { } pendingResult) { return (pendingResult, disposable); } foreach (var output in run.OutputEvents) { if (output.IsError) { processSpec.OnErrorData?.Invoke(output.Value); } else { processSpec.OnOutputData?.Invoke(output.Value); } } var processOutput = run.OutputEvents.Select(static output => output.Value).ToArray(); var processResult = new ProcessResult(run.ExitCode, processOutput, run.TotalOutputLineCount); return (Task.FromResult(processResult), disposable); } private sealed record TestProcessRun( int ExitCode, IReadOnlyList<TestProcessOutput> OutputEvents, int? TotalOutputLineCount, Exception? FailureException, Task<ProcessResult>? PendingResult) { public static TestProcessRun Result( int exitCode = 0, IReadOnlyList<string>? output = null, IReadOnlyList<string>? error = null, int? totalOutputLineCount = null, IReadOnlyList<TestProcessOutput>? outputEvents = null) { outputEvents ??= [ .. (output ?? Array.Empty<string>()).Select(static value => new TestProcessOutput(IsError: false, Value: value)), .. (error ?? Array.Empty<string>()).Select(static value => new TestProcessOutput(IsError: true, Value: value)) ]; return new TestProcessRun(exitCode, outputEvents, totalOutputLineCount, null, null); } public static TestProcessRun Failed(Exception exception) => new(0, [], null, exception, null); public static TestProcessRun Pending(Task<ProcessResult> pendingResult) => new(0, [], null, null, pendingResult); } } internal sealed record TestProcessOutput(bool IsError, string Value); internal sealed class TestProcessDisposable : IAsyncDisposable { public int DisposeCallCount { get; private set; } public ValueTask DisposeAsync() { DisposeCallCount++; return ValueTask.CompletedTask; } }