| File: Shared\TerminalTestProducer.cs | Web Access |
| Project: src\tests\Aspire.Dashboard.Tests\Aspire.Dashboard.Tests.csproj (Aspire.Dashboard.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.Collections.Concurrent; using System.IO.Pipelines; using Hex1b; using Hex1b.Automation; using Hex1b.Reflow; namespace Aspire.Dashboard.Tests.Shared; internal sealed class TerminalTestProducer : IAsyncDisposable { private readonly ConcurrentBag<Task<Hmp1ClientHandle>> _connections = []; private readonly ConcurrentBag<Task> _disconnections = []; private readonly CancellationTokenSource _stopping = new(); private readonly Hex1bTerminal _terminal; private int _disposed; public TerminalTestProducer(int width, int height, int scrollback) { Workload = new Hex1bAppWorkloadAdapter(); Presentation = new Hmp1PresentationAdapter(width, height) .WithReflow(GhosttyReflowStrategy.Instance); _terminal = Hex1bTerminal.CreateBuilder() .WithWorkload(Workload) .WithPresentation(Presentation) .WithDimensions(width, height) .WithScrollback(scrollback) .Build(); } public Hex1bAppWorkloadAdapter Workload { get; } public Hmp1PresentationAdapter Presentation { get; } public int Width => Workload.Width; public int Height => Workload.Height; public int ConnectionCount => _connections.Count; public Stream Connect() { ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this); var toClient = new Pipe(); var toServer = new Pipe(); var disconnected = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var server = new DuplexStream(toServer.Reader.AsStream(), toClient.Writer.AsStream(), onDispose: null); var client = new DuplexStream(toClient.Reader.AsStream(), toServer.Writer.AsStream(), () => disconnected.TrySetResult()); _disconnections.Add(disconnected.Task); _connections.Add(Presentation.AddClient(server, _stopping.Token)); return client; } public async Task WaitForProducerTextAsync(string text, CancellationToken cancellationToken) { using var snapshot = await new Hex1bTerminalInputSequenceBuilder() .WaitUntil(snapshot => snapshot.ContainsText(text), TimeSpan.FromSeconds(10), "Terminal producer output was not applied.") .Build() .ApplyAsync(_terminal, cancellationToken); } public Task WaitForPeerHandshakesAsync(CancellationToken cancellationToken) => Task.WhenAll(_connections).WaitAsync(cancellationToken); public Task WaitForAttachmentsReleasedAsync(CancellationToken cancellationToken) => Task.WhenAll(_disconnections).WaitAsync(cancellationToken); internal Task CancelConnectionsAsync() => _stopping.CancelAsync(); public async ValueTask DisposeAsync() { if (Interlocked.Exchange(ref _disposed, 1) != 0) { return; } await _stopping.CancelAsync(); try { foreach (var connection in _connections) { try { await using var handle = await connection; } catch (Exception ex) when (ex is OperationCanceledException or IOException or ObjectDisposedException) { // A cancelled request can end during the producer's initial handshake. } } } finally { await _terminal.DisposeAsync(); _stopping.Dispose(); } } private sealed class DuplexStream(Stream input, Stream output, Action? onDispose) : Stream { private int _disposed; public override bool CanRead => input.CanRead; public override bool CanWrite => output.CanWrite; public override bool CanSeek => false; public override long Length => throw new NotSupportedException(); public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } public override int Read(byte[] buffer, int offset, int count) => input.Read(buffer, offset, count); public override void Write(byte[] buffer, int offset, int count) => output.Write(buffer, offset, count); public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) => input.ReadAsync(buffer, cancellationToken); public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) => output.WriteAsync(buffer, cancellationToken); public override void Flush() => output.Flush(); public override Task FlushAsync(CancellationToken cancellationToken) => output.FlushAsync(cancellationToken); public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); protected override void Dispose(bool disposing) { if (disposing && Interlocked.Exchange(ref _disposed, 1) == 0) { input.Dispose(); output.Dispose(); onDispose?.Invoke(); } base.Dispose(disposing); } } }