// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.JSInterop; using Moq; using Xunit; namespace Microsoft.AspNetCore.Components.Web.Rendering { public class RemoteRendererTest { // Nothing should exceed the timeout in a successful run of the the tests, this is just here to catch // failures. private static readonly TimeSpan Timeout = Debugger.IsAttached ? System.Threading.Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10); [Fact] public void WritesAreBufferedWhenTheClientIsOffline() { // Arrange var serviceProvider = new ServiceCollection().BuildServiceProvider(); var renderer = GetRemoteRenderer(serviceProvider); var component = new TestComponent(builder => { builder.OpenElement(0, "my element"); builder.AddContent(1, "some text"); builder.CloseElement(); }); // Act var componentId = renderer.AssignRootComponentId(component); component.TriggerRender(); component.TriggerRender(); // Assert Assert.Equal(2, renderer._unacknowledgedRenderBatches.Count); } [Fact] public void NotAcknowledgingRenders_ProducesBatches_UpToTheLimit() { var serviceProvider = new ServiceCollection().BuildServiceProvider(); var renderer = GetRemoteRenderer(serviceProvider); var component = new TestComponent(builder => { builder.OpenElement(0, "my element"); builder.AddContent(1, "some text"); builder.CloseElement(); }); // Act var componentId = renderer.AssignRootComponentId(component); for (int i = 0; i < 20; i++) { component.TriggerRender(); } // Assert Assert.Equal(10, renderer._unacknowledgedRenderBatches.Count); } [Fact] public async Task NoNewBatchesAreCreated_WhenThereAreNoPendingRenderRequestsFromComponents() { var serviceProvider = new ServiceCollection().BuildServiceProvider(); var renderer = GetRemoteRenderer(serviceProvider); var component = new TestComponent(builder => { builder.OpenElement(0, "my element"); builder.AddContent(1, "some text"); builder.CloseElement(); }); // Act var componentId = renderer.AssignRootComponentId(component); for (var i = 0; i < 10; i++) { component.TriggerRender(); } await renderer.OnRenderCompletedAsync(2, null); // Assert Assert.Equal(9, renderer._unacknowledgedRenderBatches.Count); } [Fact] public async Task ProducesNewBatch_WhenABatchGetsAcknowledged() { var serviceProvider = new ServiceCollection().BuildServiceProvider(); var renderer = GetRemoteRenderer(serviceProvider); var i = 0; var component = new TestComponent(builder => { builder.AddContent(0, $"Value {i}"); }); // Act var componentId = renderer.AssignRootComponentId(component); for (i = 0; i < 20; i++) { component.TriggerRender(); } Assert.Equal(10, renderer._unacknowledgedRenderBatches.Count); await renderer.OnRenderCompletedAsync(2, null); // Assert Assert.Equal(10, renderer._unacknowledgedRenderBatches.Count); } [Fact] public async Task ProcessBufferedRenderBatches_WritesRenders() { // Arrange var @event = new ManualResetEventSlim(); var serviceProvider = new ServiceCollection().BuildServiceProvider(); var renderIds = new List(); var firstBatchTCS = new TaskCompletionSource(); var secondBatchTCS = new TaskCompletionSource(); var thirdBatchTCS = new TaskCompletionSource(); var initialClient = new Mock(); initialClient.Setup(c => c.SendCoreAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[0])) .Returns(firstBatchTCS.Task); var circuitClient = new CircuitClientProxy(initialClient.Object, "connection0"); var renderer = GetRemoteRenderer(serviceProvider, circuitClient); var component = new TestComponent(builder => { builder.OpenElement(0, "my element"); builder.AddContent(1, "some text"); builder.CloseElement(); }); var client = new Mock(); client.Setup(c => c.SendCoreAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[0])) .Returns((n, v, t) => (long)v[0] == 3 ? secondBatchTCS.Task : thirdBatchTCS.Task); var componentId = renderer.AssignRootComponentId(component); component.TriggerRender(); _ = renderer.OnRenderCompletedAsync(2, null); @event.Reset(); firstBatchTCS.SetResult(null); // Waiting is required here because the continuations of SetResult will not execute synchronously. @event.Wait(Timeout); circuitClient.SetDisconnected(); component.TriggerRender(); component.TriggerRender(); // Act circuitClient.Transfer(client.Object, "new-connection"); var task = renderer.ProcessBufferedRenderBatches(); foreach (var id in renderIds.ToArray()) { _ = renderer.OnRenderCompletedAsync(id, null); } secondBatchTCS.SetResult(null); thirdBatchTCS.SetResult(null); // Assert Assert.Equal(new long[] { 2, 3, 4 }, renderIds); Assert.True(task.Wait(3000), "One or more render batches weren't acknowledged"); await task; } [Fact] public async Task OnRenderCompletedAsync_DoesNotThrowWhenReceivedDuplicateAcks() { // Arrange var serviceProvider = new ServiceCollection().BuildServiceProvider(); var firstBatchTCS = new TaskCompletionSource(); var secondBatchTCS = new TaskCompletionSource(); var offlineClient = new CircuitClientProxy(new Mock(MockBehavior.Strict).Object, "offline-client"); offlineClient.SetDisconnected(); var renderer = GetRemoteRenderer(serviceProvider, offlineClient); RenderFragment initialContent = (builder) => { builder.OpenElement(0, "my element"); builder.AddContent(1, "some text"); builder.CloseElement(); }; var trigger = new Trigger(); var renderIds = new List(); var onlineClient = new Mock(); onlineClient.Setup(c => c.SendCoreAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1])) .Returns((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task); // This produces the initial batch (id = 2) await renderer.RenderComponentAsync( ParameterView.FromDictionary(new Dictionary { [nameof(AutoParameterTestComponent.Content)] = initialContent, [nameof(AutoParameterTestComponent.Trigger)] = trigger })); trigger.Component.Content = (builder) => { builder.OpenElement(0, "offline element"); builder.AddContent(1, "offline text"); builder.CloseElement(); }; // This produces an additional batch (id = 3) trigger.TriggerRender(); var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count; // Act offlineClient.Transfer(onlineClient.Object, "new-connection"); var task = renderer.ProcessBufferedRenderBatches(); var exceptions = new List(); renderer.UnhandledException += (sender, e) => { exceptions.Add(e); }; // Receive the ack for the initial batch _ = renderer.OnRenderCompletedAsync(2, null); // Receive the ack for the second batch _ = renderer.OnRenderCompletedAsync(3, null); firstBatchTCS.SetResult(null); secondBatchTCS.SetResult(null); // Repeat the ack for the third batch _ = renderer.OnRenderCompletedAsync(3, null); // Assert Assert.Empty(exceptions); } [Fact] public async Task OnRenderCompletedAsync_DoesNotThrowWhenThereAreNoPendingBatchesToAck() { // Arrange var serviceProvider = new ServiceCollection().BuildServiceProvider(); var firstBatchTCS = new TaskCompletionSource(); var secondBatchTCS = new TaskCompletionSource(); var offlineClient = new CircuitClientProxy(new Mock(MockBehavior.Strict).Object, "offline-client"); offlineClient.SetDisconnected(); var renderer = GetRemoteRenderer(serviceProvider, offlineClient); RenderFragment initialContent = (builder) => { builder.OpenElement(0, "my element"); builder.AddContent(1, "some text"); builder.CloseElement(); }; var trigger = new Trigger(); var renderIds = new List(); var onlineClient = new Mock(); onlineClient.Setup(c => c.SendCoreAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1])) .Returns((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task); // This produces the initial batch (id = 2) await renderer.RenderComponentAsync( ParameterView.FromDictionary(new Dictionary { [nameof(AutoParameterTestComponent.Content)] = initialContent, [nameof(AutoParameterTestComponent.Trigger)] = trigger })); trigger.Component.Content = (builder) => { builder.OpenElement(0, "offline element"); builder.AddContent(1, "offline text"); builder.CloseElement(); }; // This produces an additional batch (id = 3) trigger.TriggerRender(); var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count; // Act offlineClient.Transfer(onlineClient.Object, "new-connection"); var task = renderer.ProcessBufferedRenderBatches(); var exceptions = new List(); renderer.UnhandledException += (sender, e) => { exceptions.Add(e); }; // Receive the ack for the intial batch _ = renderer.OnRenderCompletedAsync(2, null); // Receive the ack for the second batch _ = renderer.OnRenderCompletedAsync(2, null); firstBatchTCS.SetResult(null); secondBatchTCS.SetResult(null); // Repeat the ack for the third batch _ = renderer.OnRenderCompletedAsync(3, null); // Assert Assert.Empty(exceptions); } [Fact] public async Task ConsumesAllPendingBatchesWhenReceivingAHigherSequenceBatchId() { // Arrange var serviceProvider = new ServiceCollection().BuildServiceProvider(); var firstBatchTCS = new TaskCompletionSource(); var secondBatchTCS = new TaskCompletionSource(); var renderIds = new List(); var onlineClient = new Mock(); onlineClient.Setup(c => c.SendCoreAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1])) .Returns((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task); var renderer = GetRemoteRenderer(serviceProvider, new CircuitClientProxy(onlineClient.Object, "online-client")); RenderFragment initialContent = (builder) => { builder.OpenElement(0, "my element"); builder.AddContent(1, "some text"); builder.CloseElement(); }; var trigger = new Trigger(); // This produces the initial batch (id = 2) await renderer.RenderComponentAsync( ParameterView.FromDictionary(new Dictionary { [nameof(AutoParameterTestComponent.Content)] = initialContent, [nameof(AutoParameterTestComponent.Trigger)] = trigger })); trigger.Component.Content = (builder) => { builder.OpenElement(0, "offline element"); builder.AddContent(1, "offline text"); builder.CloseElement(); }; // This produces an additional batch (id = 3) trigger.TriggerRender(); var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count; // Act var exceptions = new List(); renderer.UnhandledException += (sender, e) => { exceptions.Add(e); }; // Pretend that we missed the ack for the initial batch _ = renderer.OnRenderCompletedAsync(3, null); firstBatchTCS.SetResult(null); secondBatchTCS.SetResult(null); // Assert Assert.Empty(exceptions); Assert.Empty(renderer._unacknowledgedRenderBatches); } [Fact] public async Task ThrowsIfWeReceivedAnAcknowledgeForANeverProducedBatch() { // Arrange var serviceProvider = new ServiceCollection().BuildServiceProvider(); var firstBatchTCS = new TaskCompletionSource(); var secondBatchTCS = new TaskCompletionSource(); var renderIds = new List(); var onlineClient = new Mock(); onlineClient.Setup(c => c.SendCoreAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1])) .Returns((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task); var renderer = GetRemoteRenderer(serviceProvider, new CircuitClientProxy(onlineClient.Object, "online-client")); RenderFragment initialContent = (builder) => { builder.OpenElement(0, "my element"); builder.AddContent(1, "some text"); builder.CloseElement(); }; var trigger = new Trigger(); // This produces the initial batch (id = 2) await renderer.RenderComponentAsync( ParameterView.FromDictionary(new Dictionary { [nameof(AutoParameterTestComponent.Content)] = initialContent, [nameof(AutoParameterTestComponent.Trigger)] = trigger })); trigger.Component.Content = (builder) => { builder.OpenElement(0, "offline element"); builder.AddContent(1, "offline text"); builder.CloseElement(); }; // This produces an additional batch (id = 3) trigger.TriggerRender(); var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count; // Act var exceptions = new List(); renderer.UnhandledException += (sender, e) => { exceptions.Add(e); }; var exception = await Assert.ThrowsAsync(() => renderer.OnRenderCompletedAsync(4, null)); firstBatchTCS.SetResult(null); secondBatchTCS.SetResult(null); // Assert Assert.Equal( "Received an acknowledgement for batch with id '4' when the last batch produced was '3'.", exception.Message); } private TestRemoteRenderer GetRemoteRenderer(IServiceProvider serviceProvider, CircuitClientProxy circuitClient = null) { return new TestRemoteRenderer( serviceProvider, NullLoggerFactory.Instance, new CircuitOptions(), circuitClient ?? new CircuitClientProxy(), NullLogger.Instance); } private class TestRemoteRenderer : RemoteRenderer { public TestRemoteRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, CircuitOptions options, CircuitClientProxy client, ILogger logger) : base(serviceProvider, loggerFactory, options, client, logger) { } public async Task RenderComponentAsync(ParameterView initialParameters) { var component = InstantiateComponent(typeof(TComponent)); var componentId = AssignRootComponentId(component); await RenderRootComponentAsync(componentId, initialParameters); } } private class TestComponent : IComponent, IHandleAfterRender { private RenderHandle _renderHandle; private RenderFragment _renderFragment = (builder) => { builder.OpenElement(0, "my element"); builder.AddContent(1, "some text"); builder.CloseElement(); }; public TestComponent() { } public TestComponent(RenderFragment renderFragment) { _renderFragment = renderFragment; } public Action OnAfterRenderComplete { get; set; } public void Attach(RenderHandle renderHandle) { _renderHandle = renderHandle; } public Task OnAfterRenderAsync() { OnAfterRenderComplete?.Invoke(); return Task.CompletedTask; } public Task SetParametersAsync(ParameterView parameters) { TriggerRender(); return Task.CompletedTask; } public void TriggerRender() { var task = _renderHandle.Dispatcher.InvokeAsync(() => _renderHandle.Render(_renderFragment)); Assert.True(task.IsCompletedSuccessfully); } } private class AutoParameterTestComponent : IComponent { private RenderHandle _renderHandle; [Parameter] public RenderFragment Content { get; set; } [Parameter] public Trigger Trigger { get; set; } public void Attach(RenderHandle renderHandle) { _renderHandle = renderHandle; } public Task SetParametersAsync(ParameterView parameters) { Content = parameters.GetValueOrDefault(nameof(Content)); Trigger ??= parameters.GetValueOrDefault(nameof(Trigger)); Trigger.Component = this; TriggerRender(); return Task.CompletedTask; } public void TriggerRender() { var task = _renderHandle.Dispatcher.InvokeAsync(() => _renderHandle.Render(Content)); Assert.True(task.IsCompletedSuccessfully); } } private class Trigger { public AutoParameterTestComponent Component { get; set; } public void TriggerRender() { Component.TriggerRender(); } } } }