536 lines
22 KiB
C#
536 lines
22 KiB
C#
// 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<long>();
|
|
|
|
var firstBatchTCS = new TaskCompletionSource<object>();
|
|
var secondBatchTCS = new TaskCompletionSource<object>();
|
|
var thirdBatchTCS = new TaskCompletionSource<object>();
|
|
|
|
var initialClient = new Mock<IClientProxy>();
|
|
initialClient.Setup(c => c.SendCoreAsync(It.IsAny<string>(), It.IsAny<object[]>(), It.IsAny<CancellationToken>()))
|
|
.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<IClientProxy>();
|
|
client.Setup(c => c.SendCoreAsync(It.IsAny<string>(), It.IsAny<object[]>(), It.IsAny<CancellationToken>()))
|
|
.Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[0]))
|
|
.Returns<string, object[], CancellationToken>((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<object>();
|
|
var secondBatchTCS = new TaskCompletionSource<object>();
|
|
var offlineClient = new CircuitClientProxy(new Mock<IClientProxy>(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<long>();
|
|
var onlineClient = new Mock<IClientProxy>();
|
|
onlineClient.Setup(c => c.SendCoreAsync(It.IsAny<string>(), It.IsAny<object[]>(), It.IsAny<CancellationToken>()))
|
|
.Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1]))
|
|
.Returns<string, object[], CancellationToken>((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task);
|
|
|
|
// This produces the initial batch (id = 2)
|
|
await renderer.RenderComponentAsync<AutoParameterTestComponent>(
|
|
ParameterView.FromDictionary(new Dictionary<string, object>
|
|
{
|
|
[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<Exception>();
|
|
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<object>();
|
|
var secondBatchTCS = new TaskCompletionSource<object>();
|
|
var offlineClient = new CircuitClientProxy(new Mock<IClientProxy>(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<long>();
|
|
var onlineClient = new Mock<IClientProxy>();
|
|
onlineClient.Setup(c => c.SendCoreAsync(It.IsAny<string>(), It.IsAny<object[]>(), It.IsAny<CancellationToken>()))
|
|
.Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1]))
|
|
.Returns<string, object[], CancellationToken>((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task);
|
|
|
|
// This produces the initial batch (id = 2)
|
|
await renderer.RenderComponentAsync<AutoParameterTestComponent>(
|
|
ParameterView.FromDictionary(new Dictionary<string, object>
|
|
{
|
|
[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<Exception>();
|
|
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<object>();
|
|
var secondBatchTCS = new TaskCompletionSource<object>();
|
|
var renderIds = new List<long>();
|
|
|
|
var onlineClient = new Mock<IClientProxy>();
|
|
onlineClient.Setup(c => c.SendCoreAsync(It.IsAny<string>(), It.IsAny<object[]>(), It.IsAny<CancellationToken>()))
|
|
.Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1]))
|
|
.Returns<string, object[], CancellationToken>((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<AutoParameterTestComponent>(
|
|
ParameterView.FromDictionary(new Dictionary<string, object>
|
|
{
|
|
[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<Exception>();
|
|
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<object>();
|
|
var secondBatchTCS = new TaskCompletionSource<object>();
|
|
var renderIds = new List<long>();
|
|
|
|
var onlineClient = new Mock<IClientProxy>();
|
|
onlineClient.Setup(c => c.SendCoreAsync(It.IsAny<string>(), It.IsAny<object[]>(), It.IsAny<CancellationToken>()))
|
|
.Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1]))
|
|
.Returns<string, object[], CancellationToken>((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<AutoParameterTestComponent>(
|
|
ParameterView.FromDictionary(new Dictionary<string, object>
|
|
{
|
|
[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<Exception>();
|
|
renderer.UnhandledException += (sender, e) =>
|
|
{
|
|
exceptions.Add(e);
|
|
};
|
|
|
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => 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<TComponent>(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<RenderFragment>(nameof(Content));
|
|
Trigger ??= parameters.GetValueOrDefault<Trigger>(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();
|
|
}
|
|
}
|
|
}
|
|
}
|