Merge pull request #13226 from aspnet/rynowak/interop-reliability

Improve reliability of reliability tests
This commit is contained in:
Kevin Pilch 2019-08-20 14:00:29 -07:00 committed by GitHub
commit 733218c652
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 588 additions and 588 deletions

View File

@ -649,12 +649,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
_intializationStarted = LoggerMessage.Define( _intializationStarted = LoggerMessage.Define(
LogLevel.Debug, LogLevel.Debug,
EventIds.InitializationFailed, EventIds.InitializationStarted,
"Circuit initialization started."); "Circuit initialization started.");
_intializationSucceded = LoggerMessage.Define( _intializationSucceded = LoggerMessage.Define(
LogLevel.Debug, LogLevel.Debug,
EventIds.InitializationFailed, EventIds.InitializationSucceeded,
"Circuit initialization succeeded."); "Circuit initialization succeeded.");
_intializationFailed = LoggerMessage.Define( _intializationFailed = LoggerMessage.Define(

View File

@ -0,0 +1,138 @@
// 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.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.E2ETest;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETests.ServerExecutionTests
{
public class ComponentHubInvalidEventTest : IgnitorTest<AspNetSiteServerFixture>
{
public ComponentHubInvalidEventTest(AspNetSiteServerFixture serverFixture, ITestOutputHelper output)
: base(serverFixture, output)
{
}
protected override void InitializeFixture(AspNetSiteServerFixture serverFixture)
{
serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
}
protected async override Task InitializeAsync()
{
var rootUri = ServerFixture.RootUri;
Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir")), "Couldn't connect to the app");
Assert.Single(Batches);
await Client.SelectAsync("test-selector-select", "BasicTestApp.CounterComponent");
Assert.Equal(2, Batches.Count);
}
[Fact]
public async Task DispatchingAnInvalidEventArgument_DoesNotProduceWarnings()
{
// Arrange
var expectedError = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " +
$"detailed exceptions in 'CircuitOptions.DetailedErrors'. Bad input data.";
var eventDescriptor = Serialize(new WebEventDescriptor()
{
BrowserRendererId = 0,
EventHandlerId = 3,
EventArgsType = "mouse",
});
// Act
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
"DispatchBrowserEvent",
eventDescriptor,
"{sadfadsf]"));
// Assert
var actualError = Assert.Single(Errors);
Assert.Equal(expectedError, actualError);
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
Assert.Contains(Logs, l => (l.LogLevel, l.Exception?.Message) == (LogLevel.Debug, "There was an error parsing the event arguments. EventId: '3'."));
}
[Fact]
public async Task DispatchingAnInvalidEvent_DoesNotTriggerWarnings()
{
// Arrange
var expectedError = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " +
$"detailed exceptions in 'CircuitOptions.DetailedErrors'. Failed to dispatch event.";
var eventDescriptor = Serialize(new WebEventDescriptor()
{
BrowserRendererId = 0,
EventHandlerId = 1990,
EventArgsType = "mouse",
});
var eventArgs = new MouseEventArgs
{
Type = "click",
Detail = 1,
ScreenX = 47,
ScreenY = 258,
ClientX = 47,
ClientY = 155,
};
// Act
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
"DispatchBrowserEvent",
eventDescriptor,
Serialize(eventArgs)));
// Assert
var actualError = Assert.Single(Errors);
Assert.Equal(expectedError, actualError);
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
Assert.Contains(Logs, l => (l.LogLevel, l.Message, l.Exception?.Message) ==
(LogLevel.Debug,
"There was an error dispatching the event '1990' to the application.",
"There is no event handler associated with this event. EventId: '1990'. (Parameter 'eventHandlerId')"));
}
[Fact]
public async Task DispatchingAnInvalidRenderAcknowledgement_DoesNotTriggerWarnings()
{
// Arrange
var expectedError = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " +
$"detailed exceptions in 'CircuitOptions.DetailedErrors'. Failed to complete render batch '1846'.";
Client.ConfirmRenderBatch = false;
await Client.ClickAsync("counter");
// Act
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
"OnRenderCompleted",
1846,
null));
// Assert
var actualError = Assert.Single(Errors);
Assert.Equal(expectedError, actualError);
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
var entry = Assert.Single(Logs, l => l.EventId.Name == "OnRenderCompletedFailed");
Assert.Equal(LogLevel.Debug, entry.LogLevel);
Assert.Matches("Failed to complete render batch '1846' in circuit host '.*'\\.", entry.Message);
Assert.Equal("Received an acknowledgement for batch with id '1846' when the last batch produced was '4'.", entry.Exception.Message);
}
private string Serialize<T>(T browserEventDescriptor) =>
JsonSerializer.Serialize(browserEventDescriptor, TestJsonSerializerOptionsProvider.Options);
}
}

View File

@ -2,9 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -13,58 +11,22 @@ using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.SignalR.Client; using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{ {
public class ComponentHubReliabilityTest : IClassFixture<AspNetSiteServerFixture>, IDisposable public class ComponentHubReliabilityTest : IgnitorTest<AspNetSiteServerFixture>
{ {
private static readonly TimeSpan DefaultLatencyTimeout = Debugger.IsAttached ? TimeSpan.MaxValue : TimeSpan.FromSeconds(10);
private readonly AspNetSiteServerFixture _serverFixture;
public ComponentHubReliabilityTest(AspNetSiteServerFixture serverFixture, ITestOutputHelper output) public ComponentHubReliabilityTest(AspNetSiteServerFixture serverFixture, ITestOutputHelper output)
: base(serverFixture, output)
{ {
_serverFixture = serverFixture; }
Output = output;
protected override void InitializeFixture(AspNetSiteServerFixture serverFixture)
{
serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost; serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
CreateDefaultConfiguration();
}
public BlazorClient Client { get; set; }
public ITestOutputHelper Output { get; set; }
private IList<Batch> Batches { get; set; } = new List<Batch>();
private IList<string> Errors { get; set; } = new List<string>();
private ConcurrentQueue<LogMessage> Logs { get; set; } = new ConcurrentQueue<LogMessage>();
public TestSink TestSink { get; set; }
private void CreateDefaultConfiguration()
{
Client = new BlazorClient() { DefaultLatencyTimeout = DefaultLatencyTimeout };
Client.RenderBatchReceived += (id, data) => Batches.Add(new Batch(id, data));
Client.OnCircuitError += (error) => Errors.Add(error);
Client.LoggerProvider = new XunitLoggerProvider(Output);
Client.FormatError = (error) =>
{
var logs = string.Join(Environment.NewLine, Logs);
return new Exception(error + Environment.NewLine + logs);
};
_ = _serverFixture.RootUri; // this is needed for the side-effects of getting the URI.
TestSink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
TestSink.MessageLogged += LogMessages;
}
private void LogMessages(WriteContext context)
{
var log = new LogMessage(context.LogLevel, context.EventId, context.Message, context.Exception);
Logs.Enqueue(log);
Output.WriteLine(log.ToString());
} }
[Fact] [Fact]
@ -72,10 +34,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{ {
// Arrange // Arrange
var expectedError = "The circuit host '.*?' has already been initialized."; var expectedError = "The circuit host '.*?' has already been initialized.";
var rootUri = _serverFixture.RootUri; var rootUri = ServerFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir"); var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app"); Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
Assert.Single(Batches); Assert.Single(Batches);
var descriptors = await Client.GetPrerenderDescriptors(baseUri); var descriptors = await Client.GetPrerenderDescriptors(baseUri);
// Act // Act
@ -96,7 +59,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{ {
// Arrange // Arrange
var expectedError = "The uris provided are invalid."; var expectedError = "The uris provided are invalid.";
var rootUri = _serverFixture.RootUri; var rootUri = ServerFixture.RootUri;
var uri = new Uri(rootUri, "/subdir"); var uri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(uri, connectAutomatically: false), "Couldn't connect to the app"); Assert.True(await Client.ConnectAsync(uri, connectAutomatically: false), "Couldn't connect to the app");
var descriptors = await Client.GetPrerenderDescriptors(uri); var descriptors = await Client.GetPrerenderDescriptors(uri);
@ -117,7 +80,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{ {
// Arrange // Arrange
var expectedError = "The circuit failed to initialize."; var expectedError = "The circuit failed to initialize.";
var rootUri = _serverFixture.RootUri; var rootUri = ServerFixture.RootUri;
var uri = new Uri(rootUri, "/subdir"); var uri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(uri, connectAutomatically: false), "Couldn't connect to the app"); Assert.True(await Client.ConnectAsync(uri, connectAutomatically: false), "Couldn't connect to the app");
var descriptors = await Client.GetPrerenderDescriptors(uri); var descriptors = await Client.GetPrerenderDescriptors(uri);
@ -138,7 +101,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{ {
// Arrange // Arrange
var expectedError = "Circuit not initialized."; var expectedError = "Circuit not initialized.";
var rootUri = _serverFixture.RootUri; var rootUri = ServerFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir"); var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false)); Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false));
Assert.Empty(Batches); Assert.Empty(Batches);
@ -164,7 +127,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{ {
// Arrange // Arrange
var expectedError = "Circuit not initialized."; var expectedError = "Circuit not initialized.";
var rootUri = _serverFixture.RootUri; var rootUri = ServerFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir"); var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false)); Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false));
Assert.Empty(Batches); Assert.Empty(Batches);
@ -188,7 +151,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{ {
// Arrange // Arrange
var expectedError = "Circuit not initialized."; var expectedError = "Circuit not initialized.";
var rootUri = _serverFixture.RootUri; var rootUri = ServerFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir"); var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false)); Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false));
Assert.Empty(Batches); Assert.Empty(Batches);
@ -206,125 +169,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Assert.Contains(Logs, l => (l.LogLevel, l.Message) == (LogLevel.Debug, "Call to 'DispatchBrowserEvent' received before the circuit host initialization")); Assert.Contains(Logs, l => (l.LogLevel, l.Message) == (LogLevel.Debug, "Call to 'DispatchBrowserEvent' received before the circuit host initialization"));
} }
private async Task GoToTestComponent(IList<Batch> batches)
{
var rootUri = _serverFixture.RootUri;
Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir")), "Couldn't connect to the app");
Assert.Single(batches);
await Client.SelectAsync("test-selector-select", "BasicTestApp.CounterComponent");
Assert.Equal(2, batches.Count);
}
[Fact]
public async Task DispatchingAnInvalidEventArgument_DoesNotProduceWarnings()
{
// Arrange
var expectedError = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " +
$"detailed exceptions in 'CircuitOptions.DetailedErrors'. Bad input data.";
var eventDescriptor = Serialize(new WebEventDescriptor()
{
BrowserRendererId = 0,
EventHandlerId = 3,
EventArgsType = "mouse",
});
await GoToTestComponent(Batches);
Assert.Equal(2, Batches.Count);
// Act
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
"DispatchBrowserEvent",
eventDescriptor,
"{sadfadsf]"));
// Assert
var actualError = Assert.Single(Errors);
Assert.Equal(expectedError, actualError);
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
Assert.Contains(Logs, l => (l.LogLevel, l.Exception?.Message) == (LogLevel.Debug, "There was an error parsing the event arguments. EventId: '3'."));
}
[Fact]
public async Task DispatchingAnInvalidEvent_DoesNotTriggerWarnings()
{
// Arrange
var expectedError = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " +
$"detailed exceptions in 'CircuitOptions.DetailedErrors'. Failed to dispatch event.";
var eventDescriptor = Serialize(new WebEventDescriptor()
{
BrowserRendererId = 0,
EventHandlerId = 1990,
EventArgsType = "mouse",
});
var eventArgs = new MouseEventArgs
{
Type = "click",
Detail = 1,
ScreenX = 47,
ScreenY = 258,
ClientX = 47,
ClientY = 155,
};
await GoToTestComponent(Batches);
Assert.Equal(2, Batches.Count);
// Act
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
"DispatchBrowserEvent",
eventDescriptor,
Serialize(eventArgs)));
// Assert
var actualError = Assert.Single(Errors);
Assert.Equal(expectedError, actualError);
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
Assert.Contains(Logs, l => (l.LogLevel, l.Message, l.Exception?.Message) ==
(LogLevel.Debug,
"There was an error dispatching the event '1990' to the application.",
"There is no event handler associated with this event. EventId: '1990'. (Parameter 'eventHandlerId')"));
}
[Fact]
public async Task DispatchingAnInvalidRenderAcknowledgement_DoesNotTriggerWarnings()
{
// Arrange
var expectedError = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " +
$"detailed exceptions in 'CircuitOptions.DetailedErrors'. Failed to complete render batch '1846'.";
await GoToTestComponent(Batches);
Assert.Equal(2, Batches.Count);
Client.ConfirmRenderBatch = false;
await Client.ClickAsync("counter");
// Act
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
"OnRenderCompleted",
1846,
null));
// Assert
var actualError = Assert.Single(Errors);
Assert.Equal(expectedError, actualError);
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
var entry = Assert.Single(Logs, l => l.EventId.Name == "OnRenderCompletedFailed");
Assert.Equal(LogLevel.Debug, entry.LogLevel);
Assert.Matches("Failed to complete render batch '1846' in circuit host '.*'\\.", entry.Message);
Assert.Equal("Received an acknowledgement for batch with id '1846' when the last batch produced was '4'.", entry.Exception.Message);
}
[Fact] [Fact]
public async Task CannotInvokeOnRenderCompletedBeforeInitialization() public async Task CannotInvokeOnRenderCompletedBeforeInitialization()
{ {
// Arrange // Arrange
var expectedError = "Circuit not initialized."; var expectedError = "Circuit not initialized.";
var rootUri = _serverFixture.RootUri; var rootUri = ServerFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir"); var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false)); Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false));
Assert.Empty(Batches); Assert.Empty(Batches);
@ -347,7 +197,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{ {
// Arrange // Arrange
var expectedError = "Circuit not initialized."; var expectedError = "Circuit not initialized.";
var rootUri = _serverFixture.RootUri; var rootUri = ServerFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir"); var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false)); Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false));
Assert.Empty(Batches); Assert.Empty(Batches);
@ -373,7 +223,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"For more details turn on detailed exceptions in 'CircuitOptions.DetailedErrors'. " + "For more details turn on detailed exceptions in 'CircuitOptions.DetailedErrors'. " +
"Location change to 'http://example.com' failed."; "Location change to 'http://example.com' failed.";
var rootUri = _serverFixture.RootUri; var rootUri = ServerFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir"); var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app"); Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
Assert.Single(Batches); Assert.Single(Batches);
@ -402,7 +252,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"For more details turn on detailed exceptions in 'CircuitOptions.DetailedErrors'. " + "For more details turn on detailed exceptions in 'CircuitOptions.DetailedErrors'. " +
"Location change failed."; "Location change failed.";
var rootUri = _serverFixture.RootUri; var rootUri = ServerFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir"); var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app"); Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
Assert.Single(Batches); Assert.Single(Batches);
@ -421,7 +271,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
var entry = Assert.Single(Logs, l => l.EventId.Name == "LocationChangeFailed"); var entry = Assert.Single(Logs, l => l.EventId.Name == "LocationChangeFailed");
Assert.Equal(LogLevel.Error, entry.LogLevel); Assert.Equal(LogLevel.Error, entry.LogLevel);
Assert.Matches($"Location change to '{new Uri(_serverFixture.RootUri, "/test")}' in circuit '.*' failed\\.", entry.Message); Assert.Matches($"Location change to '{new Uri(ServerFixture.RootUri, "/test")}' in circuit '.*' failed\\.", entry.Message);
} }
[Theory] [Theory]
@ -436,7 +286,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{ {
// Arrange // Arrange
var expectedError = "Unhandled exception in circuit .*"; var expectedError = "Unhandled exception in circuit .*";
var rootUri = _serverFixture.RootUri; var rootUri = ServerFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir"); var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app"); Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
Assert.Single(Batches); Assert.Single(Batches);
@ -467,7 +317,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{ {
// Arrange // Arrange
var expectedError = "Unhandled exception in circuit .*"; var expectedError = "Unhandled exception in circuit .*";
var rootUri = _serverFixture.RootUri; var rootUri = ServerFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir"); var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app"); Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
Assert.Single(Batches); Assert.Single(Batches);
@ -493,47 +343,5 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Logs, Logs,
e => LogLevel.Error == e.LogLevel && Regex.IsMatch(e.Message, expectedError)); e => LogLevel.Error == e.LogLevel && Regex.IsMatch(e.Message, expectedError));
} }
public void Dispose()
{
TestSink.MessageLogged -= LogMessages;
}
private string Serialize<T>(T browserEventDescriptor) =>
JsonSerializer.Serialize(browserEventDescriptor, TestJsonSerializerOptionsProvider.Options);
[DebuggerDisplay("{LogLevel.ToString(),nq} - {Message ?? \"null\",nq} - {Exception?.Message,nq}")]
private class LogMessage
{
public LogMessage(LogLevel logLevel, EventId eventId, string message, Exception exception)
{
LogLevel = logLevel;
EventId = eventId;
Message = message;
Exception = exception;
}
public LogLevel LogLevel { get; set; }
public EventId EventId { get; set; }
public string Message { get; set; }
public Exception Exception { get; set; }
public override string ToString()
{
return $"{LogLevel}: {EventId} {Message}{(Exception != null ? Environment.NewLine : "")}{Exception}";
}
}
private class Batch
{
public Batch(int id, byte[] data)
{
Id = id;
Data = data;
}
public int Id { get; }
public byte[] Data { get; }
}
} }
} }

View File

@ -0,0 +1,131 @@
// 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.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Ignitor;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components
{
// Base class for Ignitor-based tests.
public abstract class IgnitorTest<TFixture> : IClassFixture<TFixture>, IAsyncLifetime
where TFixture : ServerFixture
{
private static readonly TimeSpan DefaultTimeout = Debugger.IsAttached ? TimeSpan.MaxValue : TimeSpan.FromSeconds(30);
protected IgnitorTest(TFixture serverFixture, ITestOutputHelper output)
{
ServerFixture = serverFixture;
Output = output;
}
protected BlazorClient Client { get; private set; }
protected ConcurrentQueue<LogMessage> Logs { get; } = new ConcurrentQueue<LogMessage>();
protected ITestOutputHelper Output { get; }
protected TFixture ServerFixture { get; }
protected TimeSpan Timeout { get; set; } = DefaultTimeout;
private TestSink TestSink { get; set; }
protected IReadOnlyCollection<CapturedRenderBatch> Batches => Client?.Operations?.Batches;
protected IReadOnlyCollection<string> DotNetCompletions => Client?.Operations?.DotNetCompletions;
protected IReadOnlyCollection<string> Errors => Client?.Operations?.Errors;
protected IReadOnlyCollection<CapturedJSInteropCall> JSInteropCalls => Client?.Operations?.JSInteropCalls;
// Called to initialize the fixture as part of InitializeAsync.
protected virtual void InitializeFixture(TFixture serverFixture)
{
}
async Task IAsyncLifetime.InitializeAsync()
{
Client = new BlazorClient()
{
CaptureOperations = true,
DefaultOperationTimeout = Timeout,
};
Client.LoggerProvider = new XunitLoggerProvider(Output);
Client.FormatError = (error) =>
{
var logs = string.Join(Environment.NewLine, Logs);
return new Exception(error + Environment.NewLine + logs);
};
InitializeFixture(ServerFixture);
_ = ServerFixture.RootUri; // This is needed for the side-effects of starting the server.
if (ServerFixture is WebHostServerFixture hostFixture)
{
TestSink = hostFixture.Host.Services.GetRequiredService<TestSink>();
TestSink.MessageLogged += TestSink_MessageLogged;
}
await InitializeAsync();
}
async Task IAsyncLifetime.DisposeAsync()
{
if (TestSink != null)
{
TestSink.MessageLogged -= TestSink_MessageLogged;
}
await DisposeAsync();
}
protected virtual Task InitializeAsync()
{
return Task.CompletedTask;
}
protected virtual Task DisposeAsync()
{
return Task.CompletedTask;
}
private void TestSink_MessageLogged(WriteContext context)
{
var log = new LogMessage(context.LogLevel, context.EventId, context.Message, context.Exception);
Logs.Enqueue(log);
Output.WriteLine(log.ToString());
}
[DebuggerDisplay("{LogLevel.ToString(),nq} - {Message ?? \"null\",nq} - {Exception?.Message,nq}")]
protected sealed class LogMessage
{
public LogMessage(LogLevel logLevel, EventId eventId, string message, Exception exception)
{
LogLevel = logLevel;
EventId = eventId;
Message = message;
Exception = exception;
}
public LogLevel LogLevel { get; set; }
public EventId EventId { get; set; }
public string Message { get; set; }
public Exception Exception { get; set; }
public override string ToString()
{
return $"{LogLevel}: {EventId} {Message}{(Exception != null ? Environment.NewLine : "")}{Exception}";
}
}
}
}

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
@ -21,55 +20,26 @@ using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{ {
[Flaky("https://github.com/aspnet/AspNetCore/issues/13086", FlakyOn.All)] public class InteropReliabilityTests : IgnitorTest<AspNetSiteServerFixture>
public class InteropReliabilityTests : IClassFixture<AspNetSiteServerFixture>, IDisposable
{ {
private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromSeconds(30);
private readonly AspNetSiteServerFixture _serverFixture;
public InteropReliabilityTests(AspNetSiteServerFixture serverFixture, ITestOutputHelper output) public InteropReliabilityTests(AspNetSiteServerFixture serverFixture, ITestOutputHelper output)
: base(serverFixture, output)
{ {
_serverFixture = serverFixture; }
Output = output;
protected override void InitializeFixture(AspNetSiteServerFixture serverFixture)
{
serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost; serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
CreateDefaultConfiguration();
} }
public BlazorClient Client { get; set; } protected async override Task InitializeAsync()
public ITestOutputHelper Output { get; set; }
private IList<Batch> Batches { get; set; } = new List<Batch>();
private List<DotNetCompletion> DotNetCompletions = new List<DotNetCompletion>();
private List<JSInteropCall> JSInteropCalls = new List<JSInteropCall>();
private IList<string> Errors { get; set; } = new List<string>();
private ConcurrentQueue<LogMessage> Logs { get; set; } = new ConcurrentQueue<LogMessage>();
public TestSink TestSink { get; set; }
private void CreateDefaultConfiguration()
{ {
Client = new BlazorClient() { DefaultLatencyTimeout = DefaultLatencyTimeout }; var rootUri = ServerFixture.RootUri;
Client.RenderBatchReceived += (id, data) => Batches.Add(new Batch(id, data)); Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir")), "Couldn't connect to the app");
Client.DotNetInteropCompletion += (method) => DotNetCompletions.Add(new DotNetCompletion(method)); Assert.Single(Batches);
Client.JSInterop += (asyncHandle, identifier, argsJson) => JSInteropCalls.Add(new JSInteropCall(asyncHandle, identifier, argsJson));
Client.OnCircuitError += (error) => Errors.Add(error);
Client.LoggerProvider = new XunitLoggerProvider(Output);
Client.FormatError = (error) =>
{
var logs = string.Join(Environment.NewLine, Logs);
return new Exception(error + Environment.NewLine + logs);
};
_ = _serverFixture.RootUri; // this is needed for the side-effects of getting the URI. await Client.SelectAsync("test-selector-select", "BasicTestApp.ReliabilityComponent");
TestSink = _serverFixture.Host.Services.GetRequiredService<TestSink>(); Assert.Equal(2, Batches.Count);
TestSink.MessageLogged += LogMessages;
}
private void LogMessages(WriteContext context)
{
var log = new LogMessage(context.LogLevel, context.Message, context.Exception);
Logs.Enqueue(log);
Output.WriteLine(log.ToString());
} }
[Fact] [Fact]
@ -79,7 +49,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
var expectedError = "[\"1\"," + var expectedError = "[\"1\"," +
"false," + "false," +
"\"There was an exception invoking \\u0027WriteAllText\\u0027 on assembly \\u0027System.IO.FileSystem\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; "\"There was an exception invoking \\u0027WriteAllText\\u0027 on assembly \\u0027System.IO.FileSystem\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
await GoToTestComponent(Batches);
// Act // Act
await Client.InvokeDotNetMethod( await Client.InvokeDotNetMethod(
@ -90,7 +59,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
JsonSerializer.Serialize(new[] { ".\\log.txt", "log" })); JsonSerializer.Serialize(new[] { ".\\log.txt", "log" }));
// Assert // Assert
Assert.Single(DotNetCompletions, c => c.Message == expectedError); Assert.Single(DotNetCompletions, c => c == expectedError);
await ValidateClientKeepsWorking(Client, Batches); await ValidateClientKeepsWorking(Client, Batches);
} }
@ -102,8 +71,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"false," + "false," +
"\"There was an exception invoking \\u0027MadeUpMethod\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; "\"There was an exception invoking \\u0027MadeUpMethod\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
await GoToTestComponent(Batches);
// Act // Act
await Client.InvokeDotNetMethod( await Client.InvokeDotNetMethod(
"1", "1",
@ -113,7 +80,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
JsonSerializer.Serialize(new[] { ".\\log.txt", "log" })); JsonSerializer.Serialize(new[] { ".\\log.txt", "log" }));
// Assert // Assert
Assert.Single(DotNetCompletions, c => c.Message == expectedError); Assert.Single(DotNetCompletions, c => c == expectedError);
await ValidateClientKeepsWorking(Client, Batches); await ValidateClientKeepsWorking(Client, Batches);
} }
@ -125,18 +92,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"false," + "false," +
"\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; "\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
await GoToTestComponent(Batches);
// Act // Act
await Client.InvokeDotNetMethod( await Client.InvokeDotNetMethod(
"1", "1",
"Microsoft.AspNetCore.Components.Server", "Microsoft.AspNetCore.Components.Server",
"NotifyLocationChanged", "NotifyLocationChanged",
null, null,
JsonSerializer.Serialize(new[] { _serverFixture.RootUri })); JsonSerializer.Serialize(new[] { ServerFixture.RootUri }));
// Assert // Assert
Assert.Single(DotNetCompletions, c => c.Message == expectedError); Assert.Single(DotNetCompletions, c => c == expectedError);
await ValidateClientKeepsWorking(Client, Batches); await ValidateClientKeepsWorking(Client, Batches);
} }
@ -148,18 +113,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"false," + "false," +
"\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; "\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
await GoToTestComponent(Batches);
// Act // Act
await Client.InvokeDotNetMethod( await Client.InvokeDotNetMethod(
"1", "1",
"", "",
"NotifyLocationChanged", "NotifyLocationChanged",
null, null,
JsonSerializer.Serialize(new object[] { _serverFixture.RootUri + "counter", false })); JsonSerializer.Serialize(new object[] { ServerFixture.RootUri + "counter", false }));
// Assert // Assert
Assert.Single(DotNetCompletions, c => c.Message == expectedError); Assert.Single(DotNetCompletions, c => c == expectedError);
await ValidateClientKeepsWorking(Client, Batches); await ValidateClientKeepsWorking(Client, Batches);
} }
@ -171,18 +134,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"false," + "false," +
"\"There was an exception invoking \\u0027\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; "\"There was an exception invoking \\u0027\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
await GoToTestComponent(Batches);
// Act // Act
await Client.InvokeDotNetMethod( await Client.InvokeDotNetMethod(
"1", "1",
"Microsoft.AspNetCore.Components.Server", "Microsoft.AspNetCore.Components.Server",
"", "",
null, null,
JsonSerializer.Serialize(new object[] { _serverFixture.RootUri + "counter", false })); JsonSerializer.Serialize(new object[] { ServerFixture.RootUri + "counter", false }));
// Assert // Assert
Assert.Single(DotNetCompletions, c => c.Message == expectedError); Assert.Single(DotNetCompletions, c => c == expectedError);
await ValidateClientKeepsWorking(Client, Batches); await ValidateClientKeepsWorking(Client, Batches);
} }
@ -196,8 +157,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"false," + "false," +
"\"There was an exception invoking \\u0027Reverse\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; "\"There was an exception invoking \\u0027Reverse\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
await GoToTestComponent(Batches);
// Act // Act
await Client.InvokeDotNetMethod( await Client.InvokeDotNetMethod(
"1", "1",
@ -206,7 +165,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
null, null,
JsonSerializer.Serialize(Array.Empty<object>())); JsonSerializer.Serialize(Array.Empty<object>()));
Assert.Single(DotNetCompletions, c => c.Message == expectedDotNetObjectRef); Assert.Single(DotNetCompletions, c => c == expectedDotNetObjectRef);
await Client.InvokeDotNetMethod( await Client.InvokeDotNetMethod(
"1", "1",
@ -216,7 +175,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
JsonSerializer.Serialize(Array.Empty<object>())); JsonSerializer.Serialize(Array.Empty<object>()));
// Assert // Assert
Assert.Single(DotNetCompletions, c => c.Message == "[\"1\",true,\"tnatropmI\"]"); Assert.Single(DotNetCompletions, c => c == "[\"1\",true,\"tnatropmI\"]");
await Client.InvokeDotNetMethod( await Client.InvokeDotNetMethod(
"1", "1",
@ -225,7 +184,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
3, // non existing ref 3, // non existing ref
JsonSerializer.Serialize(Array.Empty<object>())); JsonSerializer.Serialize(Array.Empty<object>()));
Assert.Single(DotNetCompletions, c => c.Message == expectedError); Assert.Single(DotNetCompletions, c => c == expectedError);
await ValidateClientKeepsWorking(Client, Batches); await ValidateClientKeepsWorking(Client, Batches);
} }
@ -238,8 +197,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"false," + "false," +
"\"There was an exception invoking \\u0027ReceiveTrivial\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; "\"There was an exception invoking \\u0027ReceiveTrivial\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
await GoToTestComponent(Batches);
await Client.InvokeDotNetMethod( await Client.InvokeDotNetMethod(
"1", "1",
"BasicTestApp", "BasicTestApp",
@ -247,7 +204,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
null, null,
JsonSerializer.Serialize(Array.Empty<object>())); JsonSerializer.Serialize(Array.Empty<object>()));
Assert.Single(DotNetCompletions, c => c.Message == expectedImportantDotNetObjectRef); Assert.Single(DotNetCompletions, c => c == expectedImportantDotNetObjectRef);
// Act // Act
await Client.InvokeDotNetMethod( await Client.InvokeDotNetMethod(
@ -258,19 +215,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
JsonSerializer.Serialize(new object[] { new { __dotNetObject = 1 } })); JsonSerializer.Serialize(new object[] { new { __dotNetObject = 1 } }));
// Assert // Assert
Assert.Single(DotNetCompletions, c => c.Message == expectedError); Assert.Single(DotNetCompletions, c => c == expectedError);
await ValidateClientKeepsWorking(Client, Batches); await ValidateClientKeepsWorking(Client, Batches);
} }
[Fact] [Fact]
[Flaky("https://github.com/aspnet/AspNetCore/issues/13086", FlakyOn.AzP.Windows)]
public async Task ContinuesWorkingAfterInvalidAsyncReturnCallback() public async Task ContinuesWorkingAfterInvalidAsyncReturnCallback()
{ {
// Arrange // Arrange
var expectedError = "An exception occurred executing JS interop: The JSON value could not be converted to System.Int32. Path: $ | LineNumber: 0 | BytePositionInLine: 3.. See InnerException for more details."; var expectedError = "An exception occurred executing JS interop: The JSON value could not be converted to System.Int32. Path: $ | LineNumber: 0 | BytePositionInLine: 3.. See InnerException for more details.";
await GoToTestComponent(Batches);
// Act // Act
await Client.ClickAsync("triggerjsinterop-malformed"); await Client.ClickAsync("triggerjsinterop-malformed");
@ -278,11 +232,14 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Assert.NotEqual(default, call); Assert.NotEqual(default, call);
var id = call.AsyncHandle; var id = call.AsyncHandle;
await Client.HubConnection.InvokeAsync( await Client.ExpectRenderBatch(async () =>
"EndInvokeJSFromDotNet", {
id, await Client.HubConnection.InvokeAsync(
true, "EndInvokeJSFromDotNet",
$"[{id}, true, \"{{\"]"); id,
true,
$"[{id}, true, \"{{\"]");
});
var text = Assert.Single( var text = Assert.Single(
Client.FindElementById("errormessage-malformed").Children.OfType<TextNode>(), Client.FindElementById("errormessage-malformed").Children.OfType<TextNode>(),
@ -295,10 +252,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
public async Task JSInteropCompletionSuccess() public async Task JSInteropCompletionSuccess()
{ {
// Arrange // Arrange
await GoToTestComponent(Batches);
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
var logEvents = new List<(LogLevel logLevel, string)>();
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name));
// Act // Act
await Client.ClickAsync("triggerjsinterop-success"); await Client.ClickAsync("triggerjsinterop-success");
@ -307,27 +260,27 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Assert.NotEqual(default, call); Assert.NotEqual(default, call);
var id = call.AsyncHandle; var id = call.AsyncHandle;
await Client.HubConnection.InvokeAsync( await Client.ExpectRenderBatch(async () =>
"EndInvokeJSFromDotNet", {
id++, await Client.HubConnection.InvokeAsync(
true, "EndInvokeJSFromDotNet",
$"[{id}, true, null]"); id,
true,
$"[{id}, true, null]");
});
Assert.Single( Assert.Single(
Client.FindElementById("errormessage-success").Children.OfType<TextNode>(), Client.FindElementById("errormessage-success").Children.OfType<TextNode>(),
e => "" == e.TextContent); e => "" == e.TextContent);
Assert.Contains((LogLevel.Debug, "EndInvokeJSSucceeded"), logEvents); var entry = Assert.Single(Logs, l => l.EventId.Name == "EndInvokeJSSucceeded");
Assert.Equal(LogLevel.Debug, entry.LogLevel);
} }
[Fact] [Fact]
public async Task JSInteropThrowsInUserCode() public async Task JSInteropThrowsInUserCode()
{ {
// Arrange // Arrange
await GoToTestComponent(Batches);
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
var logEvents = new List<(LogLevel logLevel, string)>();
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name));
// Act // Act
await Client.ClickAsync("triggerjsinterop-failure"); await Client.ClickAsync("triggerjsinterop-failure");
@ -349,9 +302,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Client.FindElementById("errormessage-failure").Children.OfType<TextNode>(), Client.FindElementById("errormessage-failure").Children.OfType<TextNode>(),
e => "There was an error invoking sendFailureCallbackReturn" == e.TextContent); e => "There was an error invoking sendFailureCallbackReturn" == e.TextContent);
Assert.Contains((LogLevel.Debug, "EndInvokeJSFailed"), logEvents); var entry = Assert.Single(Logs, l => l.EventId.Name == "EndInvokeJSFailed");
Assert.Equal(LogLevel.Debug, entry.LogLevel);
Assert.DoesNotContain(logEvents, m => m.logLevel > LogLevel.Information); Assert.DoesNotContain(Logs, m => m.LogLevel > LogLevel.Information);
await ValidateClientKeepsWorking(Client, Batches); await ValidateClientKeepsWorking(Client, Batches);
} }
@ -360,10 +314,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
public async Task MalformedJSInteropCallbackDisposesCircuit() public async Task MalformedJSInteropCallbackDisposesCircuit()
{ {
// Arrange // Arrange
await GoToTestComponent(Batches);
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
var logEvents = new List<(LogLevel logLevel, string)>();
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name));
// Act // Act
await Client.ClickAsync("triggerjsinterop-malformed"); await Client.ClickAsync("triggerjsinterop-malformed");
@ -386,7 +336,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Client.FindElementById("errormessage-malformed").Children.OfType<TextNode>(), Client.FindElementById("errormessage-malformed").Children.OfType<TextNode>(),
e => "" == e.TextContent); e => "" == e.TextContent);
Assert.Contains((LogLevel.Debug, "EndInvokeDispatchException"), logEvents); var entry = Assert.Single(Logs, l => l.EventId.Name == "EndInvokeDispatchException");
Assert.Equal(LogLevel.Debug, entry.LogLevel);
await Client.ExpectCircuitErrorAndDisconnect(async () => await Client.ExpectCircuitErrorAndDisconnect(async () =>
{ {
@ -402,8 +353,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"false," + "false," +
"\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; "\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
await GoToTestComponent(Batches);
// Act // Act
await Client.InvokeDotNetMethod( await Client.InvokeDotNetMethod(
"1", "1",
@ -413,7 +362,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"[ \"invalidPayload\"}"); "[ \"invalidPayload\"}");
// Assert // Assert
Assert.Single(DotNetCompletions, c => c.Message == expectedError); Assert.Single(DotNetCompletions, c => c == expectedError);
await ValidateClientKeepsWorking(Client, Batches); await ValidateClientKeepsWorking(Client, Batches);
} }
@ -425,8 +374,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"false," + "false," +
"\"There was an exception invoking \\u0027ReceiveTrivial\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; "\"There was an exception invoking \\u0027ReceiveTrivial\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
await GoToTestComponent(Batches);
// Act // Act
await Client.InvokeDotNetMethod( await Client.InvokeDotNetMethod(
"1", "1",
@ -436,7 +383,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"[ { \"data\": {\"}} ]"); "[ { \"data\": {\"}} ]");
// Assert // Assert
Assert.Single(DotNetCompletions, c => c.Message == expectedError); Assert.Single(DotNetCompletions, c => c == expectedError);
await ValidateClientKeepsWorking(Client, Batches); await ValidateClientKeepsWorking(Client, Batches);
} }
@ -444,10 +391,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
public async Task DispatchingEventsWithInvalidPayloadsShutsDownCircuitGracefully() public async Task DispatchingEventsWithInvalidPayloadsShutsDownCircuitGracefully()
{ {
// Arrange // Arrange
await GoToTestComponent(Batches);
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
var logEvents = new List<(LogLevel logLevel, string)>();
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name));
// Act // Act
await Client.ExpectCircuitError(async () => await Client.ExpectCircuitError(async () =>
@ -458,9 +401,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
null); null);
}); });
Assert.Contains( var entry = Assert.Single(Logs, l => l.EventId.Name == "DispatchEventFailedToParseEventData");
(LogLevel.Debug, "DispatchEventFailedToParseEventData"), Assert.Equal(LogLevel.Debug, entry.LogLevel);
logEvents);
// Taking any other action will fail because the circuit is disposed. // Taking any other action will fail because the circuit is disposed.
await Client.ExpectCircuitErrorAndDisconnect(async () => await Client.ExpectCircuitErrorAndDisconnect(async () =>
@ -473,10 +415,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
public async Task DispatchingEventsWithInvalidEventDescriptor() public async Task DispatchingEventsWithInvalidEventDescriptor()
{ {
// Arrange // Arrange
await GoToTestComponent(Batches);
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
var logEvents = new List<(LogLevel logLevel, string)>();
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name));
// Act // Act
await Client.ExpectCircuitError(async () => await Client.ExpectCircuitError(async () =>
@ -487,9 +425,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"{}"); "{}");
}); });
Assert.Contains( var entry = Assert.Single(Logs, l => l.EventId.Name == "DispatchEventFailedToParseEventData");
(LogLevel.Debug, "DispatchEventFailedToParseEventData"), Assert.Equal(LogLevel.Debug, entry.LogLevel);
logEvents);
// Taking any other action will fail because the circuit is disposed. // Taking any other action will fail because the circuit is disposed.
await Client.ExpectCircuitErrorAndDisconnect(async () => await Client.ExpectCircuitErrorAndDisconnect(async () =>
@ -502,10 +439,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
public async Task DispatchingEventsWithInvalidEventArgs() public async Task DispatchingEventsWithInvalidEventArgs()
{ {
// Arrange // Arrange
await GoToTestComponent(Batches);
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
var logEvents = new List<(LogLevel logLevel, string eventIdName, Exception exception)>();
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name, wc.Exception));
// Act // Act
var browserDescriptor = new WebEventDescriptor() var browserDescriptor = new WebEventDescriptor()
@ -524,9 +457,9 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
}); });
Assert.Contains( Assert.Contains(
logEvents, Logs,
e => e.eventIdName == "DispatchEventFailedToParseEventData" && e.logLevel == LogLevel.Debug && e => e.EventId.Name == "DispatchEventFailedToParseEventData" && e.LogLevel == LogLevel.Debug &&
e.exception.Message == "There was an error parsing the event arguments. EventId: '6'."); e.Exception.Message == "There was an error parsing the event arguments. EventId: '6'.");
// Taking any other action will fail because the circuit is disposed. // Taking any other action will fail because the circuit is disposed.
await Client.ExpectCircuitErrorAndDisconnect(async () => await Client.ExpectCircuitErrorAndDisconnect(async () =>
@ -539,10 +472,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
public async Task DispatchingEventsWithInvalidEventHandlerId() public async Task DispatchingEventsWithInvalidEventHandlerId()
{ {
// Arrange // Arrange
await GoToTestComponent(Batches);
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
var logEvents = new List<(LogLevel logLevel, string eventIdName, Exception exception)>();
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name, wc.Exception));
// Act // Act
var mouseEventArgs = new MouseEventArgs() var mouseEventArgs = new MouseEventArgs()
@ -566,9 +495,9 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
}); });
Assert.Contains( Assert.Contains(
logEvents, Logs,
e => e.eventIdName == "DispatchEventFailedToDispatchEvent" && e.logLevel == LogLevel.Debug && e => e.EventId.Name == "DispatchEventFailedToDispatchEvent" && e.LogLevel == LogLevel.Debug &&
e.exception is ArgumentException ae && ae.Message.Contains("There is no event handler associated with this event. EventId: '1'.")); e.Exception is ArgumentException ae && ae.Message.Contains("There is no event handler associated with this event. EventId: '1'."));
// Taking any other action will fail because the circuit is disposed. // Taking any other action will fail because the circuit is disposed.
await Client.ExpectCircuitErrorAndDisconnect(async () => await Client.ExpectCircuitErrorAndDisconnect(async () =>
@ -581,21 +510,18 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
public async Task EventHandlerThrowsSyncExceptionTerminatesTheCircuit() public async Task EventHandlerThrowsSyncExceptionTerminatesTheCircuit()
{ {
// Arrange // Arrange
await GoToTestComponent(Batches);
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
var logEvents = new List<(LogLevel logLevel, string eventIdName, Exception exception)>();
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name, wc.Exception));
// Act // Act
await Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: false); await Client.ExpectCircuitError(async () =>
{
await Task.Delay(1000); await Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: false);
});
Assert.Contains( Assert.Contains(
logEvents, Logs,
e => LogLevel.Error == e.logLevel && e => LogLevel.Error == e.LogLevel &&
"CircuitUnhandledException" == e.eventIdName && "CircuitUnhandledException" == e.EventId.Name &&
"Handler threw an exception" == e.exception.Message); "Handler threw an exception" == e.Exception.Message);
// Now if you try to click again, you will get *forcibly* disconnected for trying to talk to // Now if you try to click again, you will get *forcibly* disconnected for trying to talk to
// a circuit that's gone. // a circuit that's gone.
@ -605,7 +531,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
}); });
} }
private Task ValidateClientKeepsWorking(BlazorClient Client, IList<Batch> batches) => private Task ValidateClientKeepsWorking(BlazorClient Client, IReadOnlyCollection<CapturedRenderBatch> batches) =>
ValidateClientKeepsWorking(Client, () => batches.Count); ValidateClientKeepsWorking(Client, () => batches.Count);
private async Task ValidateClientKeepsWorking(BlazorClient Client, Func<int> countAccessor) private async Task ValidateClientKeepsWorking(BlazorClient Client, Func<int> countAccessor)
@ -615,75 +541,5 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Assert.Equal(currentBatches + 1, countAccessor()); Assert.Equal(currentBatches + 1, countAccessor());
} }
private async Task GoToTestComponent(IList<Batch> batches)
{
var rootUri = _serverFixture.RootUri;
Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir")), "Couldn't connect to the app");
Assert.Single(batches);
await Client.SelectAsync("test-selector-select", "BasicTestApp.ReliabilityComponent");
Assert.Equal(2, batches.Count);
}
public void Dispose()
{
TestSink.MessageLogged -= LogMessages;
}
private class LogMessage
{
public LogMessage(LogLevel logLevel, string message, Exception exception)
{
LogLevel = logLevel;
Message = message;
Exception = exception;
}
public LogLevel LogLevel { get; set; }
public string Message { get; set; }
public Exception Exception { get; set; }
public override string ToString()
{
return $"{LogLevel}: {Message}{(Exception != null ? Environment.NewLine : "")}{Exception}";
}
}
private class Batch
{
public Batch(int id, byte[] data)
{
Id = id;
Data = data;
}
public int Id { get; }
public byte[] Data { get; }
}
private class DotNetCompletion
{
public DotNetCompletion(string message)
{
Message = message;
}
public string Message { get; }
}
private class JSInteropCall
{
public JSInteropCall(int asyncHandle, string identifier, string argsJson)
{
AsyncHandle = asyncHandle;
Identifier = identifier;
ArgsJson = argsJson;
}
public int AsyncHandle { get; }
public string Identifier { get; }
public string ArgsJson { get; }
}
} }
} }

View File

@ -2,55 +2,33 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ignitor; using Ignitor;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Xunit; using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{ {
public class RemoteRendererBufferLimitTest : IClassFixture<AspNetSiteServerFixture>, IDisposable public class RemoteRendererBufferLimitTest : IgnitorTest<AspNetSiteServerFixture>
{ {
private static readonly TimeSpan DefaultLatencyTimeout = Debugger.IsAttached ? TimeSpan.FromSeconds(60) : TimeSpan.FromMilliseconds(500); public RemoteRendererBufferLimitTest(AspNetSiteServerFixture serverFixture, ITestOutputHelper output)
: base(serverFixture, output)
private AspNetSiteServerFixture _serverFixture;
public RemoteRendererBufferLimitTest(AspNetSiteServerFixture serverFixture)
{ {
serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
_serverFixture = serverFixture;
// Needed here for side-effects
_ = _serverFixture.RootUri;
Client = new BlazorClient() { DefaultLatencyTimeout = DefaultLatencyTimeout };
Client.RenderBatchReceived += (id, data) => Batches.Add(new Batch(id, data));
Sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
Sink.MessageLogged += LogMessages;
} }
public BlazorClient Client { get; set; } protected override void InitializeFixture(AspNetSiteServerFixture serverFixture)
{
private IList<Batch> Batches { get; set; } = new List<Batch>(); serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
}
// We use a stack so that we can search the logs in reverse order
private ConcurrentStack<LogMessage> Logs { get; set; } = new ConcurrentStack<LogMessage>();
public TestSink Sink { get; private set; }
[Fact] [Fact]
public async Task DispatchedEventsWillKeepBeingProcessed_ButUpdatedWillBeDelayedUntilARenderIsAcknowledged() public async Task DispatchedEventsWillKeepBeingProcessed_ButUpdatedWillBeDelayedUntilARenderIsAcknowledged()
{ {
// Arrange // Arrange
var baseUri = new Uri(_serverFixture.RootUri, "/subdir"); var baseUri = new Uri(ServerFixture.RootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app"); Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
Assert.Single(Batches); Assert.Single(Batches);
@ -75,48 +53,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Client.ConfirmRenderBatch = true; Client.ConfirmRenderBatch = true;
// This will resume the render batches. // This will resume the render batches.
await Client.ExpectRenderBatch(() => Client.ConfirmBatch(Batches[^1].Id)); await Client.ExpectRenderBatch(() => Client.ConfirmBatch(Batches.Last().Id));
// Assert // Assert
Assert.Equal("12", ((TextNode)Client.FindElementById("the-count").Children.Single()).TextContent); Assert.Equal("12", ((TextNode)Client.FindElementById("the-count").Children.Single()).TextContent);
Assert.Equal(fullCount + 1, Batches.Count); Assert.Equal(fullCount + 1, Batches.Count);
} }
private void LogMessages(WriteContext context) => Logs.Push(new LogMessage(context.LogLevel, context.Message, context.Exception));
[DebuggerDisplay("{Message,nq}")]
private class LogMessage
{
public LogMessage(LogLevel logLevel, string message, Exception exception)
{
LogLevel = logLevel;
Message = message;
Exception = exception;
}
public LogLevel LogLevel { get; set; }
public string Message { get; set; }
public Exception Exception { get; set; }
}
private class Batch
{
public Batch(int id, byte[] data)
{
Id = id;
Data = data;
}
public int Id { get; }
public byte[] Data { get; }
}
public void Dispose()
{
if (Sink != null)
{
Sink.MessageLogged -= LogMessages;
}
}
} }
} }

View File

@ -77,7 +77,7 @@
{ {
int currentCount = 0; int currentCount = 0;
string errorMalformed = ""; string errorMalformed = "";
string errorSuccess = ""; string errorSuccess = "(this will be cleared)";
string errorFailure = ""; string errorFailure = "";
bool showConstructorThrow; bool showConstructorThrow;
bool showAttachThrow; bool showAttachThrow;
@ -116,6 +116,10 @@
var result = await JSRuntime.InvokeAsync<object>( var result = await JSRuntime.InvokeAsync<object>(
"sendSuccessCallbackReturn", "sendSuccessCallbackReturn",
Array.Empty<object>()); Array.Empty<object>());
// Make sure we trigger a render when the interop call completes.
// The test uses a render to synchronize.
errorSuccess = "";
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -2,8 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Diagnostics;
using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text.Json; using System.Text.Json;
@ -33,8 +31,20 @@ namespace Ignitor
}); });
} }
public TimeSpan? DefaultLatencyTimeout { get; set; } = TimeSpan.FromMilliseconds(500); public TimeSpan? DefaultConnectionTimeout { get; set; } = TimeSpan.FromSeconds(10);
public TimeSpan? DefaultConnectTimeout { get; set; } = TimeSpan.FromSeconds(10); public TimeSpan? DefaultOperationTimeout { get; set; } = TimeSpan.FromMilliseconds(500);
/// <summary>
/// Gets or sets a value that determines whether the client will capture data such
/// as render batches, interop calls, and errors for later inspection.
/// </summary>
public bool CaptureOperations { get; set; }
/// <summary>
/// Gets the collections of operation results that are captured when <see cref="CaptureOperations"/>
/// is true.
/// </summary>
public Operations Operations { get; private set; }
public Func<string, Exception> FormatError { get; set; } public Func<string, Exception> FormatError { get; set; }
@ -44,23 +54,23 @@ namespace Ignitor
private TaskCompletionSource<object> TaskCompletionSource { get; } private TaskCompletionSource<object> TaskCompletionSource { get; }
private CancellableOperation NextBatchReceived { get; set; } private CancellableOperation<CapturedRenderBatch> NextBatchReceived { get; set; }
private CancellableOperation NextErrorReceived { get; set; } private CancellableOperation<string> NextErrorReceived { get; set; }
private CancellableOperation NextDisconnect { get; set; } private CancellableOperation<Exception> NextDisconnect { get; set; }
private CancellableOperation NextJSInteropReceived { get; set; } private CancellableOperation<CapturedJSInteropCall> NextJSInteropReceived { get; set; }
private CancellableOperation NextDotNetInteropCompletionReceived { get; set; } private CancellableOperation<string> NextDotNetInteropCompletionReceived { get; set; }
public ILoggerProvider LoggerProvider { get; set; } public ILoggerProvider LoggerProvider { get; set; }
public bool ConfirmRenderBatch { get; set; } = true; public bool ConfirmRenderBatch { get; set; } = true;
public event Action<int, string, string> JSInterop; public event Action<CapturedJSInteropCall> JSInterop;
public event Action<int, byte[]> RenderBatchReceived; public event Action<CapturedRenderBatch> RenderBatchReceived;
public event Action<string> DotNetInteropCompletion; public event Action<string> DotNetInteropCompletion;
@ -70,66 +80,66 @@ namespace Ignitor
public ElementHive Hive { get; set; } = new ElementHive(); public ElementHive Hive { get; set; } = new ElementHive();
public bool ImplicitWait => DefaultLatencyTimeout != null; public bool ImplicitWait => DefaultOperationTimeout != null;
public HubConnection HubConnection { get; set; } public HubConnection HubConnection { get; set; }
public Task PrepareForNextBatch(TimeSpan? timeout) public Task<CapturedRenderBatch> PrepareForNextBatch(TimeSpan? timeout)
{ {
if (NextBatchReceived?.Completion != null) if (NextBatchReceived?.Completion != null)
{ {
throw new InvalidOperationException("Invalid state previous task not completed"); throw new InvalidOperationException("Invalid state previous task not completed");
} }
NextBatchReceived = new CancellableOperation(timeout); NextBatchReceived = new CancellableOperation<CapturedRenderBatch>(timeout);
return NextBatchReceived.Completion.Task; return NextBatchReceived.Completion.Task;
} }
public Task PrepareForNextJSInterop(TimeSpan? timeout) public Task<CapturedJSInteropCall> PrepareForNextJSInterop(TimeSpan? timeout)
{ {
if (NextJSInteropReceived?.Completion != null) if (NextJSInteropReceived?.Completion != null)
{ {
throw new InvalidOperationException("Invalid state previous task not completed"); throw new InvalidOperationException("Invalid state previous task not completed");
} }
NextJSInteropReceived = new CancellableOperation(timeout); NextJSInteropReceived = new CancellableOperation<CapturedJSInteropCall>(timeout);
return NextJSInteropReceived.Completion.Task; return NextJSInteropReceived.Completion.Task;
} }
public Task PrepareForNextDotNetInterop(TimeSpan? timeout) public Task<string> PrepareForNextDotNetInterop(TimeSpan? timeout)
{ {
if (NextDotNetInteropCompletionReceived?.Completion != null) if (NextDotNetInteropCompletionReceived?.Completion != null)
{ {
throw new InvalidOperationException("Invalid state previous task not completed"); throw new InvalidOperationException("Invalid state previous task not completed");
} }
NextDotNetInteropCompletionReceived = new CancellableOperation(timeout); NextDotNetInteropCompletionReceived = new CancellableOperation<string>(timeout);
return NextDotNetInteropCompletionReceived.Completion.Task; return NextDotNetInteropCompletionReceived.Completion.Task;
} }
public Task PrepareForNextCircuitError(TimeSpan? timeout) public Task<string> PrepareForNextCircuitError(TimeSpan? timeout)
{ {
if (NextErrorReceived?.Completion != null) if (NextErrorReceived?.Completion != null)
{ {
throw new InvalidOperationException("Invalid state previous task not completed"); throw new InvalidOperationException("Invalid state previous task not completed");
} }
NextErrorReceived = new CancellableOperation(timeout); NextErrorReceived = new CancellableOperation<string>(timeout);
return NextErrorReceived.Completion.Task; return NextErrorReceived.Completion.Task;
} }
public Task PrepareForNextDisconnect(TimeSpan? timeout) public Task<Exception> PrepareForNextDisconnect(TimeSpan? timeout)
{ {
if (NextDisconnect?.Completion != null) if (NextDisconnect?.Completion != null)
{ {
throw new InvalidOperationException("Invalid state previous task not completed"); throw new InvalidOperationException("Invalid state previous task not completed");
} }
NextDisconnect = new CancellableOperation(timeout); NextDisconnect = new CancellableOperation<Exception>(timeout);
return NextDisconnect.Completion.Task; return NextDisconnect.Completion.Task;
} }
@ -160,115 +170,162 @@ namespace Ignitor
return ExpectRenderBatch(() => elementNode.SelectAsync(HubConnection, value)); return ExpectRenderBatch(() => elementNode.SelectAsync(HubConnection, value));
} }
public async Task ExpectRenderBatch(Func<Task> action, TimeSpan? timeout = null) public async Task<CapturedRenderBatch> ExpectRenderBatch(Func<Task> action, TimeSpan? timeout = null)
{ {
var task = WaitForRenderBatch(timeout); var task = WaitForRenderBatch(timeout);
await action(); await action();
await task; return await task;
} }
public async Task ExpectJSInterop(Func<Task> action, TimeSpan? timeout = null) public async Task<CapturedJSInteropCall> ExpectJSInterop(Func<Task> action, TimeSpan? timeout = null)
{ {
var task = WaitForJSInterop(timeout); var task = WaitForJSInterop(timeout);
await action(); await action();
await task; return await task;
} }
public async Task ExpectDotNetInterop(Func<Task> action, TimeSpan? timeout = null) public async Task<string> ExpectDotNetInterop(Func<Task> action, TimeSpan? timeout = null)
{ {
var task = WaitForDotNetInterop(timeout); var task = WaitForDotNetInterop(timeout);
await action(); await action();
await task; return await task;
} }
public async Task ExpectCircuitError(Func<Task> action, TimeSpan? timeout = null) public async Task<string> ExpectCircuitError(Func<Task> action, TimeSpan? timeout = null)
{ {
var task = WaitForCircuitError(timeout); var task = WaitForCircuitError(timeout);
await action(); await action();
await task; return await task;
} }
public async Task ExpectCircuitErrorAndDisconnect(Func<Task> action, TimeSpan? timeout = null) public async Task<Exception> ExpectDisconnect(Func<Task> action, TimeSpan? timeout = null)
{
// NOTE: timeout is used for each operation individually.
await ExpectDisconnect(async () =>
{
await ExpectCircuitError(action, timeout);
}, timeout);
}
public async Task ExpectDisconnect(Func<Task> action, TimeSpan? timeout = null)
{ {
var task = WaitForDisconnect(timeout); var task = WaitForDisconnect(timeout);
await action(); await action();
await task; return await task;
} }
private Task WaitForRenderBatch(TimeSpan? timeout = null) public async Task<(string error, Exception exception)> ExpectCircuitErrorAndDisconnect(Func<Task> action, TimeSpan? timeout = null)
{
string error = null;
// NOTE: timeout is used for each operation individually.
var exception = await ExpectDisconnect(async () =>
{
error = await ExpectCircuitError(action, timeout);
}, timeout);
return (error, exception);
}
private async Task<CapturedRenderBatch> WaitForRenderBatch(TimeSpan? timeout = null)
{ {
if (ImplicitWait) if (ImplicitWait)
{ {
if (DefaultLatencyTimeout == null && timeout == null) if (DefaultOperationTimeout == null && timeout == null)
{ {
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed."); throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed.");
} }
return PrepareForNextBatch(timeout ?? DefaultLatencyTimeout); try
{
return await PrepareForNextBatch(timeout ?? DefaultOperationTimeout);
}
catch (OperationCanceledException)
{
throw FormatError("Timed out while waiting for batch.");
}
} }
return Task.CompletedTask; return null;
} }
private async Task WaitForJSInterop(TimeSpan? timeout = null) private async Task<CapturedJSInteropCall> WaitForJSInterop(TimeSpan? timeout = null)
{ {
if (ImplicitWait) if (ImplicitWait)
{ {
if (DefaultLatencyTimeout == null && timeout == null) if (DefaultOperationTimeout == null && timeout == null)
{ {
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed."); throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed.");
} }
await PrepareForNextJSInterop(timeout ?? DefaultLatencyTimeout); try
{
return await PrepareForNextJSInterop(timeout ?? DefaultOperationTimeout);
}
catch (OperationCanceledException)
{
throw FormatError("Timed out while waiting for JS Interop.");
}
} }
return null;
} }
private async Task WaitForDotNetInterop(TimeSpan? timeout = null) private async Task<string> WaitForDotNetInterop(TimeSpan? timeout = null)
{ {
if (ImplicitWait) if (ImplicitWait)
{ {
if (DefaultLatencyTimeout == null && timeout == null) if (DefaultOperationTimeout == null && timeout == null)
{ {
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed."); throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed.");
} }
await PrepareForNextDotNetInterop(timeout ?? DefaultLatencyTimeout); try
{
return await PrepareForNextDotNetInterop(timeout ?? DefaultOperationTimeout);
}
catch (OperationCanceledException)
{
throw FormatError("Timed out while waiting for .NET interop.");
}
} }
return null;
} }
private async Task WaitForCircuitError(TimeSpan? timeout = null) private async Task<string> WaitForCircuitError(TimeSpan? timeout = null)
{ {
if (ImplicitWait) if (ImplicitWait)
{ {
if (DefaultLatencyTimeout == null && timeout == null) if (DefaultOperationTimeout == null && timeout == null)
{ {
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed."); throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed.");
} }
await PrepareForNextCircuitError(timeout ?? DefaultLatencyTimeout); try
}
}
private async Task WaitForDisconnect(TimeSpan? timeout = null)
{
if (ImplicitWait)
{
if (DefaultLatencyTimeout == null && timeout == null)
{ {
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed."); return await PrepareForNextCircuitError(timeout ?? DefaultOperationTimeout);
}
catch (OperationCanceledException)
{
throw FormatError("Timed out while waiting for circuit error.");
} }
} }
await PrepareForNextDisconnect(timeout ?? DefaultLatencyTimeout); return null;
}
private async Task<Exception> WaitForDisconnect(TimeSpan? timeout = null)
{
if (ImplicitWait)
{
if (DefaultOperationTimeout == null && timeout == null)
{
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed.");
}
try
{
return await PrepareForNextDisconnect(timeout ?? DefaultOperationTimeout);
}
catch (OperationCanceledException)
{
throw FormatError("Timed out while waiting for disconnect.");
}
}
return null;
} }
public async Task<bool> ConnectAsync(Uri uri, bool connectAutomatically = true) public async Task<bool> ConnectAsync(Uri uri, bool connectAutomatically = true)
@ -294,6 +351,11 @@ namespace Ignitor
HubConnection.On<string>("JS.Error", OnError); HubConnection.On<string>("JS.Error", OnError);
HubConnection.Closed += OnClosedAsync; HubConnection.Closed += OnClosedAsync;
if (CaptureOperations)
{
Operations = new Operations();
}
if (!connectAutomatically) if (!connectAutomatically)
{ {
return true; return true;
@ -302,35 +364,41 @@ namespace Ignitor
var descriptors = await GetPrerenderDescriptors(uri); var descriptors = await GetPrerenderDescriptors(uri);
await ExpectRenderBatch( await ExpectRenderBatch(
async () => CircuitId = await HubConnection.InvokeAsync<string>("StartCircuit", uri, uri, descriptors), async () => CircuitId = await HubConnection.InvokeAsync<string>("StartCircuit", uri, uri, descriptors),
DefaultConnectTimeout); DefaultConnectionTimeout);
return CircuitId != null; return CircuitId != null;
} }
private void OnEndInvokeDotNet(string completion) private void OnEndInvokeDotNet(string message)
{ {
DotNetInteropCompletion?.Invoke(completion); Operations?.DotNetCompletions.Enqueue(message);
DotNetInteropCompletion?.Invoke(message);
NextDotNetInteropCompletionReceived?.Completion?.TrySetResult(null); NextDotNetInteropCompletionReceived?.Completion?.TrySetResult(null);
} }
private void OnBeginInvokeJS(int asyncHandle, string identifier, string argsJson) private void OnBeginInvokeJS(int asyncHandle, string identifier, string argsJson)
{ {
JSInterop?.Invoke(asyncHandle, identifier, argsJson); var call = new CapturedJSInteropCall(asyncHandle, identifier, argsJson);
Operations?.JSInteropCalls.Enqueue(call);
JSInterop?.Invoke(call);
NextJSInteropReceived?.Completion?.TrySetResult(null); NextJSInteropReceived?.Completion?.TrySetResult(null);
} }
private void OnRenderBatch(int batchId, byte[] batchData) private void OnRenderBatch(int id, byte[] data)
{ {
RenderBatchReceived?.Invoke(batchId, batchData); var capturedBatch = new CapturedRenderBatch(id, data);
var batch = RenderBatchReader.Read(batchData); Operations?.Batches.Enqueue(capturedBatch);
RenderBatchReceived?.Invoke(capturedBatch);
var batch = RenderBatchReader.Read(data);
Hive.Update(batch); Hive.Update(batch);
if (ConfirmRenderBatch) if (ConfirmRenderBatch)
{ {
_ = ConfirmBatch(batchId); _ = ConfirmBatch(id);
} }
NextBatchReceived?.Completion?.TrySetResult(null); NextBatchReceived?.Completion?.TrySetResult(null);
@ -343,8 +411,12 @@ namespace Ignitor
private void OnError(string error) private void OnError(string error)
{ {
Operations?.Errors.Enqueue(error);
OnCircuitError?.Invoke(error); OnCircuitError?.Invoke(error);
// If we get an error, forcibly terminate anything else we're waiting for. These
// tests should only encounter errors in specific situations, and this ensures that
// we fail with a good message.
var exception = FormatError?.Invoke(error) ?? new Exception(error); var exception = FormatError?.Invoke(error) ?? new Exception(error);
NextBatchReceived?.Completion?.TrySetException(exception); NextBatchReceived?.Completion?.TrySetException(exception);
NextDotNetInteropCompletionReceived?.Completion.TrySetException(exception); NextDotNetInteropCompletionReceived?.Completion.TrySetException(exception);
@ -415,7 +487,7 @@ namespace Ignitor
return element; return element;
} }
private class CancellableOperation private class CancellableOperation<TResult>
{ {
public CancellableOperation(TimeSpan? timeout) public CancellableOperation(TimeSpan? timeout)
{ {
@ -423,24 +495,32 @@ namespace Ignitor
Initialize(); Initialize();
} }
public TimeSpan? Timeout { get; }
public TaskCompletionSource<TResult> Completion { get; set; }
public CancellationTokenSource Cancellation { get; set; }
public CancellationTokenRegistration CancellationRegistration { get; set; }
private void Initialize() private void Initialize()
{ {
Completion = new TaskCompletionSource<object>(TaskContinuationOptions.RunContinuationsAsynchronously); Completion = new TaskCompletionSource<TResult>(TaskContinuationOptions.RunContinuationsAsynchronously);
Completion.Task.ContinueWith( Completion.Task.ContinueWith(
(task, state) => (task, state) =>
{ {
var operation = (CancellableOperation)state; var operation = (CancellableOperation<TResult>)state;
operation.Dispose(); operation.Dispose();
}, },
this, this,
TaskContinuationOptions.ExecuteSynchronously); // We need to execute synchronously to clean-up before anything else continues TaskContinuationOptions.ExecuteSynchronously); // We need to execute synchronously to clean-up before anything else continues
if (Timeout != null) if (Timeout != null && Timeout != System.Threading.Timeout.InfiniteTimeSpan && Timeout != TimeSpan.MaxValue)
{ {
Cancellation = new CancellationTokenSource(Timeout.Value); Cancellation = new CancellationTokenSource(Timeout.Value);
CancellationRegistration = Cancellation.Token.Register( CancellationRegistration = Cancellation.Token.Register(
(self) => (self) =>
{ {
var operation = (CancellableOperation)self; var operation = (CancellableOperation<TResult>)self;
operation.Completion.TrySetCanceled(operation.Cancellation.Token); operation.Completion.TrySetCanceled(operation.Cancellation.Token);
operation.Cancellation.Dispose(); operation.Cancellation.Dispose();
operation.CancellationRegistration.Dispose(); operation.CancellationRegistration.Dispose();
@ -455,14 +535,6 @@ namespace Ignitor
Cancellation.Dispose(); Cancellation.Dispose();
CancellationRegistration.Dispose(); CancellationRegistration.Dispose();
} }
public TimeSpan? Timeout { get; }
public TaskCompletionSource<object> Completion { get; set; }
public CancellationTokenSource Cancellation { get; set; }
public CancellationTokenRegistration CancellationRegistration { get; set; }
} }
private string[] ReadMarkers(string content) private string[] ReadMarkers(string content)

View File

@ -0,0 +1,19 @@
// 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.
namespace Ignitor
{
public class CapturedJSInteropCall
{
public CapturedJSInteropCall(int asyncHandle, string identifier, string argsJson)
{
AsyncHandle = asyncHandle;
Identifier = identifier;
ArgsJson = argsJson;
}
public int AsyncHandle { get; }
public string Identifier { get; }
public string ArgsJson { get; }
}
}

View File

@ -0,0 +1,17 @@
// 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.
namespace Ignitor
{
public class CapturedRenderBatch
{
public CapturedRenderBatch(int id, byte[] data)
{
Id = id;
Data = data;
}
public int Id { get; }
public byte[] Data { get; }
}
}

View File

@ -0,0 +1,18 @@
// 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.Collections.Concurrent;
namespace Ignitor
{
public sealed class Operations
{
public ConcurrentQueue<CapturedRenderBatch> Batches { get; } = new ConcurrentQueue<CapturedRenderBatch>();
public ConcurrentQueue<string> DotNetCompletions { get; } = new ConcurrentQueue<string>();
public ConcurrentQueue<string> Errors { get; } = new ConcurrentQueue<string>();
public ConcurrentQueue<CapturedJSInteropCall> JSInteropCalls { get; } = new ConcurrentQueue<CapturedJSInteropCall>();
}
}

View File

@ -2,10 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client; using Microsoft.AspNetCore.SignalR.Client;
@ -38,9 +34,9 @@ namespace Ignitor
var done = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); var done = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
// Click the counter button 1000 times // Click the counter button 1000 times
client.RenderBatchReceived += (int batchId, byte[] data) => client.RenderBatchReceived += (batch) =>
{ {
if (batchId < 1000) if (batch.Id < 1000)
{ {
var _ = client.ClickAsync("thecounter"); var _ = client.ClickAsync("thecounter");
} }
@ -56,8 +52,8 @@ namespace Ignitor
return 0; return 0;
} }
private static void OnJSInterop(int callId, string identifier, string argsJson) => private static void OnJSInterop(CapturedJSInteropCall call) =>
Console.WriteLine("JS Invoke: " + identifier + " (" + argsJson + ")"); Console.WriteLine("JS Invoke: " + call.Identifier + " (" + call.ArgsJson + ")");
public Program() public Program()
{ {