Improve reliability of reliability tests

Fixes: #13086

- Fixed some wrong logging (wrong EventIds)
- Added a base class for ignitor tests
- Centralized code for capturing data in ignitor
- Make `WaitForXyz` methods return the thing they found
- Make `WaitForXyz` methods report logs on timeout
- Fix synchronization problems with some tests
- Split invalid event tests into their own class for code reuse

The main thing here is that some of the JS interop reliability tests had
causality/synchronization problems. They were either not waiting for a
render batch, or they were not *triggering* a render batch.

In addition to that, I did a general consolidation of the infrastructure
for these tests to make sure that they are all using the same set of
practices. I found and fixed cases where tests were using timeouts that
were too small (inconsistency) or where they were using non-threadsafe
data structures (these tests always involve concurrency).
This commit is contained in:
Ryan Nowak 2019-08-17 18:34:10 -07:00
parent ff5c200345
commit be2a71855b
12 changed files with 588 additions and 588 deletions

View File

@ -649,12 +649,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
_intializationStarted = LoggerMessage.Define(
LogLevel.Debug,
EventIds.InitializationFailed,
EventIds.InitializationStarted,
"Circuit initialization started.");
_intializationSucceded = LoggerMessage.Define(
LogLevel.Debug,
EventIds.InitializationFailed,
EventIds.InitializationSucceeded,
"Circuit initialization succeeded.");
_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.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
@ -13,58 +11,22 @@ 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.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Xunit;
using Xunit.Abstractions;
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)
: base(serverFixture, output)
{
_serverFixture = serverFixture;
Output = output;
}
protected override void InitializeFixture(AspNetSiteServerFixture serverFixture)
{
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]
@ -72,10 +34,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
// Arrange
var expectedError = "The circuit host '.*?' has already been initialized.";
var rootUri = _serverFixture.RootUri;
var rootUri = ServerFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
Assert.Single(Batches);
var descriptors = await Client.GetPrerenderDescriptors(baseUri);
// Act
@ -96,7 +59,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
// Arrange
var expectedError = "The uris provided are invalid.";
var rootUri = _serverFixture.RootUri;
var rootUri = ServerFixture.RootUri;
var uri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(uri, connectAutomatically: false), "Couldn't connect to the app");
var descriptors = await Client.GetPrerenderDescriptors(uri);
@ -117,7 +80,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
// Arrange
var expectedError = "The circuit failed to initialize.";
var rootUri = _serverFixture.RootUri;
var rootUri = ServerFixture.RootUri;
var uri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(uri, connectAutomatically: false), "Couldn't connect to the app");
var descriptors = await Client.GetPrerenderDescriptors(uri);
@ -138,7 +101,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
// Arrange
var expectedError = "Circuit not initialized.";
var rootUri = _serverFixture.RootUri;
var rootUri = ServerFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false));
Assert.Empty(Batches);
@ -164,7 +127,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
// Arrange
var expectedError = "Circuit not initialized.";
var rootUri = _serverFixture.RootUri;
var rootUri = ServerFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false));
Assert.Empty(Batches);
@ -188,7 +151,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
// Arrange
var expectedError = "Circuit not initialized.";
var rootUri = _serverFixture.RootUri;
var rootUri = ServerFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false));
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"));
}
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]
public async Task CannotInvokeOnRenderCompletedBeforeInitialization()
{
// Arrange
var expectedError = "Circuit not initialized.";
var rootUri = _serverFixture.RootUri;
var rootUri = ServerFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false));
Assert.Empty(Batches);
@ -347,7 +197,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
// Arrange
var expectedError = "Circuit not initialized.";
var rootUri = _serverFixture.RootUri;
var rootUri = ServerFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false));
Assert.Empty(Batches);
@ -373,7 +223,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"For more details turn on detailed exceptions in 'CircuitOptions.DetailedErrors'. " +
"Location change to 'http://example.com' failed.";
var rootUri = _serverFixture.RootUri;
var rootUri = ServerFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
Assert.Single(Batches);
@ -402,7 +252,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"For more details turn on detailed exceptions in 'CircuitOptions.DetailedErrors'. " +
"Location change failed.";
var rootUri = _serverFixture.RootUri;
var rootUri = ServerFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
Assert.Single(Batches);
@ -421,7 +271,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
var entry = Assert.Single(Logs, l => l.EventId.Name == "LocationChangeFailed");
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]
@ -436,7 +286,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
// Arrange
var expectedError = "Unhandled exception in circuit .*";
var rootUri = _serverFixture.RootUri;
var rootUri = ServerFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
Assert.Single(Batches);
@ -467,7 +317,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
// Arrange
var expectedError = "Unhandled exception in circuit .*";
var rootUri = _serverFixture.RootUri;
var rootUri = ServerFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
Assert.Single(Batches);
@ -493,47 +343,5 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Logs,
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.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
@ -21,55 +20,26 @@ using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
[Flaky("https://github.com/aspnet/AspNetCore/issues/13086", FlakyOn.All)]
public class InteropReliabilityTests : IClassFixture<AspNetSiteServerFixture>, IDisposable
public class InteropReliabilityTests : IgnitorTest<AspNetSiteServerFixture>
{
private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromSeconds(30);
private readonly AspNetSiteServerFixture _serverFixture;
public InteropReliabilityTests(AspNetSiteServerFixture serverFixture, ITestOutputHelper output)
: base(serverFixture, output)
{
_serverFixture = serverFixture;
Output = output;
}
protected override void InitializeFixture(AspNetSiteServerFixture serverFixture)
{
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 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()
protected async override Task InitializeAsync()
{
Client = new BlazorClient() { DefaultLatencyTimeout = DefaultLatencyTimeout };
Client.RenderBatchReceived += (id, data) => Batches.Add(new Batch(id, data));
Client.DotNetInteropCompletion += (method) => DotNetCompletions.Add(new DotNetCompletion(method));
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);
};
var rootUri = ServerFixture.RootUri;
Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir")), "Couldn't connect to the app");
Assert.Single(Batches);
_ = _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.Message, context.Exception);
Logs.Enqueue(log);
Output.WriteLine(log.ToString());
await Client.SelectAsync("test-selector-select", "BasicTestApp.ReliabilityComponent");
Assert.Equal(2, Batches.Count);
}
[Fact]
@ -79,7 +49,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
var expectedError = "[\"1\"," +
"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\"]";
await GoToTestComponent(Batches);
// Act
await Client.InvokeDotNetMethod(
@ -90,7 +59,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
JsonSerializer.Serialize(new[] { ".\\log.txt", "log" }));
// Assert
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
Assert.Single(DotNetCompletions, c => c == expectedError);
await ValidateClientKeepsWorking(Client, Batches);
}
@ -102,8 +71,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"false," +
"\"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
await Client.InvokeDotNetMethod(
"1",
@ -113,7 +80,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
JsonSerializer.Serialize(new[] { ".\\log.txt", "log" }));
// Assert
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
Assert.Single(DotNetCompletions, c => c == expectedError);
await ValidateClientKeepsWorking(Client, Batches);
}
@ -125,18 +92,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"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\"]";
await GoToTestComponent(Batches);
// Act
await Client.InvokeDotNetMethod(
"1",
"Microsoft.AspNetCore.Components.Server",
"NotifyLocationChanged",
null,
JsonSerializer.Serialize(new[] { _serverFixture.RootUri }));
JsonSerializer.Serialize(new[] { ServerFixture.RootUri }));
// Assert
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
Assert.Single(DotNetCompletions, c => c == expectedError);
await ValidateClientKeepsWorking(Client, Batches);
}
@ -148,18 +113,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"false," +
"\"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
await Client.InvokeDotNetMethod(
"1",
"",
"NotifyLocationChanged",
null,
JsonSerializer.Serialize(new object[] { _serverFixture.RootUri + "counter", false }));
JsonSerializer.Serialize(new object[] { ServerFixture.RootUri + "counter", false }));
// Assert
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
Assert.Single(DotNetCompletions, c => c == expectedError);
await ValidateClientKeepsWorking(Client, Batches);
}
@ -171,18 +134,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"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\"]";
await GoToTestComponent(Batches);
// Act
await Client.InvokeDotNetMethod(
"1",
"Microsoft.AspNetCore.Components.Server",
"",
null,
JsonSerializer.Serialize(new object[] { _serverFixture.RootUri + "counter", false }));
JsonSerializer.Serialize(new object[] { ServerFixture.RootUri + "counter", false }));
// Assert
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
Assert.Single(DotNetCompletions, c => c == expectedError);
await ValidateClientKeepsWorking(Client, Batches);
}
@ -196,8 +157,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"false," +
"\"There was an exception invoking \\u0027Reverse\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
await GoToTestComponent(Batches);
// Act
await Client.InvokeDotNetMethod(
"1",
@ -206,7 +165,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
null,
JsonSerializer.Serialize(Array.Empty<object>()));
Assert.Single(DotNetCompletions, c => c.Message == expectedDotNetObjectRef);
Assert.Single(DotNetCompletions, c => c == expectedDotNetObjectRef);
await Client.InvokeDotNetMethod(
"1",
@ -216,7 +175,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
JsonSerializer.Serialize(Array.Empty<object>()));
// Assert
Assert.Single(DotNetCompletions, c => c.Message == "[\"1\",true,\"tnatropmI\"]");
Assert.Single(DotNetCompletions, c => c == "[\"1\",true,\"tnatropmI\"]");
await Client.InvokeDotNetMethod(
"1",
@ -225,7 +184,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
3, // non existing ref
JsonSerializer.Serialize(Array.Empty<object>()));
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
Assert.Single(DotNetCompletions, c => c == expectedError);
await ValidateClientKeepsWorking(Client, Batches);
}
@ -238,8 +197,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"false," +
"\"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(
"1",
"BasicTestApp",
@ -247,7 +204,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
null,
JsonSerializer.Serialize(Array.Empty<object>()));
Assert.Single(DotNetCompletions, c => c.Message == expectedImportantDotNetObjectRef);
Assert.Single(DotNetCompletions, c => c == expectedImportantDotNetObjectRef);
// Act
await Client.InvokeDotNetMethod(
@ -258,19 +215,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
JsonSerializer.Serialize(new object[] { new { __dotNetObject = 1 } }));
// Assert
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
Assert.Single(DotNetCompletions, c => c == expectedError);
await ValidateClientKeepsWorking(Client, Batches);
}
[Fact]
[Flaky("https://github.com/aspnet/AspNetCore/issues/13086", FlakyOn.AzP.Windows)]
public async Task ContinuesWorkingAfterInvalidAsyncReturnCallback()
{
// 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.";
await GoToTestComponent(Batches);
// Act
await Client.ClickAsync("triggerjsinterop-malformed");
@ -278,11 +232,14 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Assert.NotEqual(default, call);
var id = call.AsyncHandle;
await Client.HubConnection.InvokeAsync(
"EndInvokeJSFromDotNet",
id,
true,
$"[{id}, true, \"{{\"]");
await Client.ExpectRenderBatch(async () =>
{
await Client.HubConnection.InvokeAsync(
"EndInvokeJSFromDotNet",
id,
true,
$"[{id}, true, \"{{\"]");
});
var text = Assert.Single(
Client.FindElementById("errormessage-malformed").Children.OfType<TextNode>(),
@ -295,10 +252,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
public async Task JSInteropCompletionSuccess()
{
// 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
await Client.ClickAsync("triggerjsinterop-success");
@ -307,27 +260,27 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Assert.NotEqual(default, call);
var id = call.AsyncHandle;
await Client.HubConnection.InvokeAsync(
"EndInvokeJSFromDotNet",
id++,
true,
$"[{id}, true, null]");
await Client.ExpectRenderBatch(async () =>
{
await Client.HubConnection.InvokeAsync(
"EndInvokeJSFromDotNet",
id,
true,
$"[{id}, true, null]");
});
Assert.Single(
Client.FindElementById("errormessage-success").Children.OfType<TextNode>(),
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]
public async Task JSInteropThrowsInUserCode()
{
// 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
await Client.ClickAsync("triggerjsinterop-failure");
@ -349,9 +302,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Client.FindElementById("errormessage-failure").Children.OfType<TextNode>(),
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);
}
@ -360,10 +314,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
public async Task MalformedJSInteropCallbackDisposesCircuit()
{
// 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
await Client.ClickAsync("triggerjsinterop-malformed");
@ -386,7 +336,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Client.FindElementById("errormessage-malformed").Children.OfType<TextNode>(),
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 () =>
{
@ -402,8 +353,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"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\"]";
await GoToTestComponent(Batches);
// Act
await Client.InvokeDotNetMethod(
"1",
@ -413,7 +362,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"[ \"invalidPayload\"}");
// Assert
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
Assert.Single(DotNetCompletions, c => c == expectedError);
await ValidateClientKeepsWorking(Client, Batches);
}
@ -425,8 +374,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"false," +
"\"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
await Client.InvokeDotNetMethod(
"1",
@ -436,7 +383,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"[ { \"data\": {\"}} ]");
// Assert
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
Assert.Single(DotNetCompletions, c => c == expectedError);
await ValidateClientKeepsWorking(Client, Batches);
}
@ -444,10 +391,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
public async Task DispatchingEventsWithInvalidPayloadsShutsDownCircuitGracefully()
{
// 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
await Client.ExpectCircuitError(async () =>
@ -458,9 +401,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
null);
});
Assert.Contains(
(LogLevel.Debug, "DispatchEventFailedToParseEventData"),
logEvents);
var entry = Assert.Single(Logs, l => l.EventId.Name == "DispatchEventFailedToParseEventData");
Assert.Equal(LogLevel.Debug, entry.LogLevel);
// Taking any other action will fail because the circuit is disposed.
await Client.ExpectCircuitErrorAndDisconnect(async () =>
@ -473,10 +415,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
public async Task DispatchingEventsWithInvalidEventDescriptor()
{
// 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
await Client.ExpectCircuitError(async () =>
@ -487,9 +425,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
"{}");
});
Assert.Contains(
(LogLevel.Debug, "DispatchEventFailedToParseEventData"),
logEvents);
var entry = Assert.Single(Logs, l => l.EventId.Name == "DispatchEventFailedToParseEventData");
Assert.Equal(LogLevel.Debug, entry.LogLevel);
// Taking any other action will fail because the circuit is disposed.
await Client.ExpectCircuitErrorAndDisconnect(async () =>
@ -502,10 +439,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
public async Task DispatchingEventsWithInvalidEventArgs()
{
// 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
var browserDescriptor = new WebEventDescriptor()
@ -524,9 +457,9 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
});
Assert.Contains(
logEvents,
e => e.eventIdName == "DispatchEventFailedToParseEventData" && e.logLevel == LogLevel.Debug &&
e.exception.Message == "There was an error parsing the event arguments. EventId: '6'.");
Logs,
e => e.EventId.Name == "DispatchEventFailedToParseEventData" && e.LogLevel == LogLevel.Debug &&
e.Exception.Message == "There was an error parsing the event arguments. EventId: '6'.");
// Taking any other action will fail because the circuit is disposed.
await Client.ExpectCircuitErrorAndDisconnect(async () =>
@ -539,10 +472,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
public async Task DispatchingEventsWithInvalidEventHandlerId()
{
// 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
var mouseEventArgs = new MouseEventArgs()
@ -566,9 +495,9 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
});
Assert.Contains(
logEvents,
e => e.eventIdName == "DispatchEventFailedToDispatchEvent" && e.logLevel == LogLevel.Debug &&
e.exception is ArgumentException ae && ae.Message.Contains("There is no event handler associated with this event. EventId: '1'."));
Logs,
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'."));
// Taking any other action will fail because the circuit is disposed.
await Client.ExpectCircuitErrorAndDisconnect(async () =>
@ -581,21 +510,18 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
public async Task EventHandlerThrowsSyncExceptionTerminatesTheCircuit()
{
// 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
await Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: false);
await Task.Delay(1000);
await Client.ExpectCircuitError(async () =>
{
await Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: false);
});
Assert.Contains(
logEvents,
e => LogLevel.Error == e.logLevel &&
"CircuitUnhandledException" == e.eventIdName &&
"Handler threw an exception" == e.exception.Message);
Logs,
e => LogLevel.Error == e.LogLevel &&
"CircuitUnhandledException" == e.EventId.Name &&
"Handler threw an exception" == e.Exception.Message);
// Now if you try to click again, you will get *forcibly* disconnected for trying to talk to
// 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);
private async Task ValidateClientKeepsWorking(BlazorClient Client, Func<int> countAccessor)
@ -615,75 +541,5 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
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.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
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.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);
private AspNetSiteServerFixture _serverFixture;
public RemoteRendererBufferLimitTest(AspNetSiteServerFixture serverFixture)
public RemoteRendererBufferLimitTest(AspNetSiteServerFixture serverFixture, ITestOutputHelper output)
: base(serverFixture, output)
{
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; }
private IList<Batch> Batches { get; set; } = new List<Batch>();
// 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; }
protected override void InitializeFixture(AspNetSiteServerFixture serverFixture)
{
serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
}
[Fact]
public async Task DispatchedEventsWillKeepBeingProcessed_ButUpdatedWillBeDelayedUntilARenderIsAcknowledged()
{
// 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.Single(Batches);
@ -75,48 +53,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Client.ConfirmRenderBatch = true;
// This will resume the render batches.
await Client.ExpectRenderBatch(() => Client.ConfirmBatch(Batches[^1].Id));
await Client.ExpectRenderBatch(() => Client.ConfirmBatch(Batches.Last().Id));
// Assert
Assert.Equal("12", ((TextNode)Client.FindElementById("the-count").Children.Single()).TextContent);
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;
string errorMalformed = "";
string errorSuccess = "";
string errorSuccess = "(this will be cleared)";
string errorFailure = "";
bool showConstructorThrow;
bool showAttachThrow;
@ -116,6 +116,10 @@
var result = await JSRuntime.InvokeAsync<object>(
"sendSuccessCallbackReturn",
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)
{

View File

@ -2,8 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
@ -33,8 +31,20 @@ namespace Ignitor
});
}
public TimeSpan? DefaultLatencyTimeout { get; set; } = TimeSpan.FromMilliseconds(500);
public TimeSpan? DefaultConnectTimeout { get; set; } = TimeSpan.FromSeconds(10);
public TimeSpan? DefaultConnectionTimeout { 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; }
@ -44,23 +54,23 @@ namespace Ignitor
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 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;
@ -70,66 +80,66 @@ namespace Ignitor
public ElementHive Hive { get; set; } = new ElementHive();
public bool ImplicitWait => DefaultLatencyTimeout != null;
public bool ImplicitWait => DefaultOperationTimeout != null;
public HubConnection HubConnection { get; set; }
public Task PrepareForNextBatch(TimeSpan? timeout)
public Task<CapturedRenderBatch> PrepareForNextBatch(TimeSpan? timeout)
{
if (NextBatchReceived?.Completion != null)
{
throw new InvalidOperationException("Invalid state previous task not completed");
}
NextBatchReceived = new CancellableOperation(timeout);
NextBatchReceived = new CancellableOperation<CapturedRenderBatch>(timeout);
return NextBatchReceived.Completion.Task;
}
public Task PrepareForNextJSInterop(TimeSpan? timeout)
public Task<CapturedJSInteropCall> PrepareForNextJSInterop(TimeSpan? timeout)
{
if (NextJSInteropReceived?.Completion != null)
{
throw new InvalidOperationException("Invalid state previous task not completed");
}
NextJSInteropReceived = new CancellableOperation(timeout);
NextJSInteropReceived = new CancellableOperation<CapturedJSInteropCall>(timeout);
return NextJSInteropReceived.Completion.Task;
}
public Task PrepareForNextDotNetInterop(TimeSpan? timeout)
public Task<string> PrepareForNextDotNetInterop(TimeSpan? timeout)
{
if (NextDotNetInteropCompletionReceived?.Completion != null)
{
throw new InvalidOperationException("Invalid state previous task not completed");
}
NextDotNetInteropCompletionReceived = new CancellableOperation(timeout);
NextDotNetInteropCompletionReceived = new CancellableOperation<string>(timeout);
return NextDotNetInteropCompletionReceived.Completion.Task;
}
public Task PrepareForNextCircuitError(TimeSpan? timeout)
public Task<string> PrepareForNextCircuitError(TimeSpan? timeout)
{
if (NextErrorReceived?.Completion != null)
{
throw new InvalidOperationException("Invalid state previous task not completed");
}
NextErrorReceived = new CancellableOperation(timeout);
NextErrorReceived = new CancellableOperation<string>(timeout);
return NextErrorReceived.Completion.Task;
}
public Task PrepareForNextDisconnect(TimeSpan? timeout)
public Task<Exception> PrepareForNextDisconnect(TimeSpan? timeout)
{
if (NextDisconnect?.Completion != null)
{
throw new InvalidOperationException("Invalid state previous task not completed");
}
NextDisconnect = new CancellableOperation(timeout);
NextDisconnect = new CancellableOperation<Exception>(timeout);
return NextDisconnect.Completion.Task;
}
@ -160,115 +170,162 @@ namespace Ignitor
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);
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);
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);
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);
await action();
await task;
return await task;
}
public async Task ExpectCircuitErrorAndDisconnect(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)
public async Task<Exception> ExpectDisconnect(Func<Task> action, TimeSpan? timeout = null)
{
var task = WaitForDisconnect(timeout);
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 (DefaultLatencyTimeout == null && timeout == null)
if (DefaultOperationTimeout == null && timeout == null)
{
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 (DefaultLatencyTimeout == null && timeout == null)
if (DefaultOperationTimeout == null && timeout == null)
{
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 (DefaultLatencyTimeout == null && timeout == null)
if (DefaultOperationTimeout == null && timeout == null)
{
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 (DefaultLatencyTimeout == null && timeout == null)
if (DefaultOperationTimeout == null && timeout == null)
{
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed.");
}
await PrepareForNextCircuitError(timeout ?? DefaultLatencyTimeout);
}
}
private async Task WaitForDisconnect(TimeSpan? timeout = null)
{
if (ImplicitWait)
{
if (DefaultLatencyTimeout == null && timeout == null)
try
{
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)
@ -294,6 +351,11 @@ namespace Ignitor
HubConnection.On<string>("JS.Error", OnError);
HubConnection.Closed += OnClosedAsync;
if (CaptureOperations)
{
Operations = new Operations();
}
if (!connectAutomatically)
{
return true;
@ -302,35 +364,41 @@ namespace Ignitor
var descriptors = await GetPrerenderDescriptors(uri);
await ExpectRenderBatch(
async () => CircuitId = await HubConnection.InvokeAsync<string>("StartCircuit", uri, uri, descriptors),
DefaultConnectTimeout);
DefaultConnectionTimeout);
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);
}
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);
}
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);
if (ConfirmRenderBatch)
{
_ = ConfirmBatch(batchId);
_ = ConfirmBatch(id);
}
NextBatchReceived?.Completion?.TrySetResult(null);
@ -343,8 +411,12 @@ namespace Ignitor
private void OnError(string error)
{
Operations?.Errors.Enqueue(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);
NextBatchReceived?.Completion?.TrySetException(exception);
NextDotNetInteropCompletionReceived?.Completion.TrySetException(exception);
@ -415,7 +487,7 @@ namespace Ignitor
return element;
}
private class CancellableOperation
private class CancellableOperation<TResult>
{
public CancellableOperation(TimeSpan? timeout)
{
@ -423,24 +495,32 @@ namespace Ignitor
Initialize();
}
public TimeSpan? Timeout { get; }
public TaskCompletionSource<TResult> Completion { get; set; }
public CancellationTokenSource Cancellation { get; set; }
public CancellationTokenRegistration CancellationRegistration { get; set; }
private void Initialize()
{
Completion = new TaskCompletionSource<object>(TaskContinuationOptions.RunContinuationsAsynchronously);
Completion = new TaskCompletionSource<TResult>(TaskContinuationOptions.RunContinuationsAsynchronously);
Completion.Task.ContinueWith(
(task, state) =>
{
var operation = (CancellableOperation)state;
var operation = (CancellableOperation<TResult>)state;
operation.Dispose();
},
this,
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);
CancellationRegistration = Cancellation.Token.Register(
(self) =>
{
var operation = (CancellableOperation)self;
var operation = (CancellableOperation<TResult>)self;
operation.Completion.TrySetCanceled(operation.Cancellation.Token);
operation.Cancellation.Dispose();
operation.CancellationRegistration.Dispose();
@ -455,14 +535,6 @@ namespace Ignitor
Cancellation.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)

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.
using System;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
@ -38,9 +34,9 @@ namespace Ignitor
var done = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
// 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");
}
@ -56,8 +52,8 @@ namespace Ignitor
return 0;
}
private static void OnJSInterop(int callId, string identifier, string argsJson) =>
Console.WriteLine("JS Invoke: " + identifier + " (" + argsJson + ")");
private static void OnJSInterop(CapturedJSInteropCall call) =>
Console.WriteLine("JS Invoke: " + call.Identifier + " (" + call.ArgsJson + ")");
public Program()
{