| File: Utils\TestAppHostTerminalViewer.cs | Web Access |
| Project: src\tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj (Aspire.Hosting.Tests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Hex1b; using Hex1b.Automation; using Hex1b.Reflow; using Microsoft.AspNetCore.InternalTesting; #pragma warning disable ASPIRETERMINAL001 // Test consumer of the experimental AppHost terminal API. namespace Aspire.Hosting.Tests.Utils; internal sealed class TestAppHostTerminalViewer : IAsyncDisposable { private readonly CancellationTokenSource _attachmentCts = new(); private readonly CancellationTokenSource _clientCts = new(); private readonly TaskCompletionSource<IHmp1ConnectionHandle> _connected = new(TaskCreationOptions.RunContinuationsAsynchronously); private readonly TestDuplexStream _serverStream; private readonly TestDuplexStream _clientStream; private readonly Hex1bTerminal _client; private readonly Task _attachment; private readonly Task _run; private bool _disposed; private TestAppHostTerminalViewer(TerminalService service, string terminalId) { (_serverStream, _clientStream) = TestDuplexStream.CreatePair(); _client = Hex1bTerminal.CreateBuilder() .WithHeadless() .WithReflow(GhosttyReflowStrategy.Instance) .WithDimensions(80, 24) .WithHmp1Stream(_clientStream, options => { options.DefaultRole = Hmp1Role.Secondary; options.OnConnected = (e, _) => { _connected.TrySetResult(e.Connection); return Task.CompletedTask; }; }) .Build(); _attachment = service.AttachAsync(terminalId, _serverStream, _ => Task.CompletedTask, _attachmentCts.Token); _run = _client.RunAsync(_clientCts.Token); } public static async Task<TestAppHostTerminalViewer> ConnectAsync(TerminalService service, string terminalId) { var viewer = new TestAppHostTerminalViewer(service, terminalId); try { var completed = await Task.WhenAny(viewer._connected.Task, viewer._run, viewer._attachment).DefaultTimeout(); await completed; Assert.True(viewer._connected.Task.IsCompletedSuccessfully, "The HMP1 connection ended before the handshake completed."); return viewer; } catch { await viewer.DisposeAsync(); throw; } } public Task WaitForTextAsync(string text) => new Hex1bTerminalAutomator(_client, TimeSpan.FromSeconds(30)).WaitUntilTextAsync(text); public Task SendTextAsync(string text) => new Hex1bTerminalAutomator(_client, TimeSpan.FromSeconds(30)).TypeAsync(text); public async Task ResizeAsync(int width, int height) { var connection = await _connected.Task.DefaultTimeout(); await connection.RequestPrimaryAsync(width, height).DefaultTimeout(); using var snapshot = await new Hex1bTerminalInputSequenceBuilder() .WaitUntil(snapshot => snapshot.Width == width && snapshot.Height == height, TimeSpan.FromSeconds(30), "The producer did not acknowledge the requested dimensions.") .Build().ApplyAsync(_client, _clientCts.Token).DefaultTimeout(); } public async Task DisconnectPeerAsync() { await StopClientAsync(); // No attachment cancellation: the server must detect the peer's EOF and release the RPC itself. await _attachment.DefaultTimeout(); } private async Task StopClientAsync() { await _clientCts.CancelAsync(); try { await _run.DefaultTimeout(); } catch (OperationCanceledException) when (_clientCts.IsCancellationRequested) { } await _client.DisposeAsync(); } public async ValueTask DisposeAsync() { if (_disposed) { return; } _disposed = true; try { await _attachmentCts.CancelAsync(); await _attachment.DefaultTimeout(); } finally { await StopClientAsync(); _serverStream.Dispose(); _clientStream.Dispose(); _attachmentCts.Dispose(); _clientCts.Dispose(); } } }