| File: tests\Shared\TestDashboardClient.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.Collections.Immutable; using System.Runtime.CompilerServices; using System.Threading.Channels; using Aspire.Dashboard.Model; using Aspire.Dashboard.ServiceClient; using Aspire.DashboardService.Proto.V1; using Google.Protobuf.WellKnownTypes; namespace Aspire.Dashboard.Tests.Shared; public class TestDashboardClient : IDashboardClient { private DashboardConnectionState _connectionState = DashboardConnectionState.Connected; private readonly Func<string, Channel<IReadOnlyList<ResourceLogLine>>>? _consoleLogsChannelProvider; private readonly Func<Channel<IReadOnlyList<ResourceViewModelChange>>>? _resourceChannelProvider; private readonly Func<Channel<WatchInteractionsResponseUpdate>>? _interactionChannelProvider; private readonly Func<Channel<WatchTerminalsUpdate>>? _terminalChannelProvider; private readonly Func<string, CancellationToken, Task>? _closeTerminal; private readonly Func<string, CancellationToken, Task<Stream>>? _attachTerminal; private readonly Channel<ResourceCommandResponseViewModel>? _resourceCommandsChannel; private readonly Func<string, string, CommandViewModel, ExecuteResourceCommandOptions, CancellationToken, Task<ResourceCommandResponseViewModel>>? _executeResourceCommand; private readonly Channel<WatchInteractionsRequestUpdate>? _sendInteractionUpdateChannel; private readonly IList<ResourceViewModel>? _initialResources; private int _terminalSubscriptionCount; private int _activeTerminalSubscriptionCount; private int _resourceSubscriptionCount; public bool IsEnabled { get; } public bool IsReadOnly { get; set; } public Task WhenConnected { get; } public string ApplicationName { get; } = "TestApp"; public string? MinRequiredVersion => null; public DashboardConnectionState ConnectionState => _connectionState; public ConcurrentQueue<(IReadOnlyList<string> ResourceNames, DateTime ClearDate)> ClearedConsoleLogs { get; } = new(); public ConcurrentQueue<string> ClosedTerminals { get; } = new(); public Action? OnTerminalSubscriptionDisposed { get; set; } public Action? OnResourceSubscriptionDisposed { get; set; } public Func<WatchTerminalsUpdate, Task>? BeforeTerminalUpdateAsync { get; set; } public Action<WatchTerminalsUpdate>? OnTerminalUpdateProcessed { get; set; } public int TerminalSubscriptionCount => Volatile.Read(ref _terminalSubscriptionCount); public int ActiveTerminalSubscriptionCount => Volatile.Read(ref _activeTerminalSubscriptionCount); public int ResourceSubscriptionCount => Volatile.Read(ref _resourceSubscriptionCount); public event Action<DashboardConnectionState>? ConnectionStateChanged; public Task ReconnectAsync() => Task.CompletedTask; public TestDashboardClient( bool? isEnabled = false, string? applicationName = null, Func<string, Channel<IReadOnlyList<ResourceLogLine>>>? consoleLogsChannelProvider = null, Func<Channel<IReadOnlyList<ResourceViewModelChange>>>? resourceChannelProvider = null, Func<Channel<WatchInteractionsResponseUpdate>>? interactionChannelProvider = null, Channel<ResourceCommandResponseViewModel>? resourceCommandsChannel = null, Func<string, string, CommandViewModel, ExecuteResourceCommandOptions, CancellationToken, Task<ResourceCommandResponseViewModel>>? executeResourceCommand = null, Channel<WatchInteractionsRequestUpdate>? sendInteractionUpdateChannel = null, IList<ResourceViewModel>? initialResources = null, Task? whenConnected = null, bool isReadOnly = false, Func<Channel<WatchTerminalsUpdate>>? terminalChannelProvider = null, Func<string, CancellationToken, Task>? closeTerminal = null, Func<string, CancellationToken, Task<Stream>>? attachTerminal = null) { IsEnabled = isEnabled ?? false; IsReadOnly = isReadOnly; ApplicationName = applicationName ?? "TestApp"; WhenConnected = whenConnected ?? Task.CompletedTask; _consoleLogsChannelProvider = consoleLogsChannelProvider; _resourceChannelProvider = resourceChannelProvider; _interactionChannelProvider = interactionChannelProvider; _resourceCommandsChannel = resourceCommandsChannel; _executeResourceCommand = executeResourceCommand; _sendInteractionUpdateChannel = sendInteractionUpdateChannel; _initialResources = initialResources; _terminalChannelProvider = terminalChannelProvider; _closeTerminal = closeTerminal; _attachTerminal = attachTerminal; } public ValueTask DisposeAsync() { return default; } public void SetConnectionState(DashboardConnectionState state) { _connectionState = state; ConnectionStateChanged?.Invoke(state); } public Task<ResourceCommandResponseViewModel> ExecuteResourceCommandAsync(string resourceName, string resourceType, CommandViewModel command, ExecuteResourceCommandOptions options, CancellationToken cancellationToken) { if (_executeResourceCommand is not null) { return _executeResourceCommand(resourceName, resourceType, command, options, cancellationToken); } if (_resourceCommandsChannel == null) { throw new InvalidOperationException("No resource command channel set."); } return _resourceCommandsChannel.Reader.ReadAsync(cancellationToken).AsTask(); } public Task<string> UploadFileAsync(Stream fileStream, string fileName, long expectedSize, int interactionId, string inputName, CancellationToken cancellationToken) { return Task.FromResult(Guid.NewGuid().ToString("N")); } public Task<Stream> AttachTerminalAsync(string terminalId, CancellationToken cancellationToken) { return _attachTerminal?.Invoke(terminalId, cancellationToken) ?? Task.FromResult<Stream>(new MemoryStream()); } public async IAsyncEnumerable<WatchTerminalsUpdate> SubscribeTerminalsAsync([EnumeratorCancellation] CancellationToken cancellationToken) { Interlocked.Increment(ref _terminalSubscriptionCount); Interlocked.Increment(ref _activeTerminalSubscriptionCount); try { if (_terminalChannelProvider is { } provider) { await foreach (var update in provider().Reader.ReadAllAsync(cancellationToken)) { // Allow a test to hold an already-received update while its subscriber is being replaced. if (BeforeTerminalUpdateAsync is { } beforeUpdate) { await beforeUpdate(update); } yield return update; // Resuming after yield confirms the subscriber finished handling this update, even if it // intentionally did not render. Tests can synchronize without depending on UI side effects. OnTerminalUpdateProcessed?.Invoke(update); } } } finally { Interlocked.Decrement(ref _activeTerminalSubscriptionCount); OnTerminalSubscriptionDisposed?.Invoke(); } } public Task CloseTerminalAsync(string terminalId, CancellationToken cancellationToken) { ClosedTerminals.Enqueue(terminalId); return _closeTerminal?.Invoke(terminalId, cancellationToken) ?? Task.CompletedTask; } public async IAsyncEnumerable<IReadOnlyList<ResourceLogLine>> SubscribeConsoleLogs(string resourceName, [EnumeratorCancellation] CancellationToken cancellationToken) { if (_consoleLogsChannelProvider == null) { throw new InvalidOperationException("No channel provider set."); } var channel = _consoleLogsChannelProvider(resourceName); await foreach (var item in channel.Reader.ReadAllAsync(cancellationToken)) { yield return item; } } public async IAsyncEnumerable<IReadOnlyList<ResourceLogLine>> GetConsoleLogs(string resourceName, [EnumeratorCancellation] CancellationToken cancellationToken) { if (_consoleLogsChannelProvider == null) { throw new InvalidOperationException("No channel provider set."); } var channel = _consoleLogsChannelProvider(resourceName); await foreach (var item in channel.Reader.ReadAllAsync(cancellationToken)) { yield return item; } } public Task ClearConsoleLogsAsync(IReadOnlyList<string> resourceNames, DateTime clearDate) { ClearedConsoleLogs.Enqueue((resourceNames, clearDate)); return Task.CompletedTask; } public Task<ResourceViewModelSubscription> SubscribeResourcesAsync(CancellationToken cancellationToken) { if (_resourceChannelProvider == null) { throw new InvalidOperationException("No channel provider set."); } var channel = _resourceChannelProvider(); Interlocked.Increment(ref _resourceSubscriptionCount); return Task.FromResult(new ResourceViewModelSubscription(_initialResources?.ToImmutableArray() ?? [], BuildSubscription(channel, cancellationToken))); async IAsyncEnumerable<IReadOnlyList<ResourceViewModelChange>> BuildSubscription(Channel<IReadOnlyList<ResourceViewModelChange>> channel, [EnumeratorCancellation] CancellationToken cancellationToken) { try { await foreach (var item in channel.Reader.ReadAllAsync(cancellationToken)) { yield return item; } } finally { OnResourceSubscriptionDisposed?.Invoke(); } } } public IAsyncEnumerable<WatchInteractionsResponseUpdate> SubscribeInteractionsAsync(CancellationToken cancellationToken) { if (_interactionChannelProvider == null) { throw new InvalidOperationException("No channel provider set."); } var channel = _interactionChannelProvider(); return BuildSubscription(channel, cancellationToken); async static IAsyncEnumerable<WatchInteractionsResponseUpdate> BuildSubscription(Channel<WatchInteractionsResponseUpdate> channel, [EnumeratorCancellation] CancellationToken cancellationToken) { await foreach (var item in channel.Reader.ReadAllAsync(cancellationToken)) { yield return item; } } } public async Task SendInteractionRequestAsync(WatchInteractionsRequestUpdate request, CancellationToken cancellationToken) { if (_sendInteractionUpdateChannel == null) { throw new InvalidOperationException("No resource command channel set."); } await _sendInteractionUpdateChannel.Writer.WriteAsync(request, cancellationToken); } public ResourceViewModel? GetResource(string resourceName) => null; public IReadOnlyList<ResourceViewModel> GetResources() => _initialResources?.ToList() ?? []; }