|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Globalization;
namespace Microsoft.AspNetCore.Components.Rendering;
public class RendererSynchronizationContextTest
{
// Nothing should exceed the timeout in a successful run of the the tests, this is just here to catch
// failures.
public TimeSpan Timeout = Debugger.IsAttached ? System.Threading.Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10);
[Fact]
public void Post_RunsAsynchronously_WhenNotBusy()
{
// Arrange
var context = new RendererSynchronizationContext();
var thread = Thread.CurrentThread;
Thread capturedThread = null;
var e = new ManualResetEventSlim();
// Act
context.Post((_) =>
{
capturedThread = Thread.CurrentThread;
e.Set();
}, null);
// Assert
Assert.True(e.Wait(Timeout), "timeout");
Assert.NotSame(thread, capturedThread);
}
[Fact]
public void Post_RunsAsynchronously_WhenNotBusy_Exception()
{
// Arrange
var context = new RendererSynchronizationContext();
Exception exception = null;
context.UnhandledException += (sender, e) =>
{
exception = (InvalidTimeZoneException)e.ExceptionObject;
};
// Act
context.Post((_) =>
{
throw new InvalidTimeZoneException();
}, null);
// Assert
//
// Use another item to 'push through' the throwing one
context.Send((_) => { }, null);
Assert.NotNull(exception);
}
[Fact]
public async Task Post_CanRunAsynchronously_WhenBusy()
{
// Arrange
var context = new RendererSynchronizationContext();
var thread = Thread.CurrentThread;
Thread capturedThread = null;
var e1 = new ManualResetEventSlim();
var e2 = new ManualResetEventSlim();
var e3 = new ManualResetEventSlim();
var task = Task.Run(() =>
{
context.Send((_) =>
{
e1.Set();
Assert.True(e2.Wait(Timeout), "timeout");
}, null);
});
Assert.True(e1.Wait(Timeout), "timeout");
// Act
context.Post((_) =>
{
capturedThread = Thread.CurrentThread;
e3.Set();
}, null);
// Assert
Assert.False(e2.IsSet);
e2.Set(); // Unblock the first item
await task;
Assert.True(e3.Wait(Timeout), "timeout");
Assert.NotSame(thread, capturedThread);
}
[Fact]
public async Task Post_CanRunAsynchronously_CaptureExecutionContext()
{
// Arrange
var context = new RendererSynchronizationContext();
// CultureInfo uses the execution context.
CultureInfo.CurrentCulture = new CultureInfo("en-GB");
CultureInfo capturedCulture = null;
SynchronizationContext capturedContext = null;
var e1 = new ManualResetEventSlim();
var e2 = new ManualResetEventSlim();
var e3 = new ManualResetEventSlim();
var task = Task.Run(() =>
{
context.Send((_) =>
{
e1.Set();
Assert.True(e2.Wait(Timeout), "timeout");
}, null);
});
Assert.True(e1.Wait(Timeout), "timeout");
// Act
SynchronizationContext original = SynchronizationContext.Current;
try
{
SynchronizationContext.SetSynchronizationContext(context);
context.Post((_) =>
{
capturedCulture = CultureInfo.CurrentCulture;
capturedContext = SynchronizationContext.Current;
e3.Set();
}, null);
}
finally
{
SynchronizationContext.SetSynchronizationContext(original);
}
// Assert
Assert.False(e2.IsSet);
e2.Set(); // Unblock the first item
await task;
Assert.True(e3.Wait(Timeout), "timeout");
Assert.Same(CultureInfo.CurrentCulture, capturedCulture);
Assert.Same(context, capturedContext);
}
[Fact]
public async Task Post_CanRunAsynchronously_WhenBusy_Exception()
{
// Arrange
var context = new RendererSynchronizationContext();
Exception exception = null;
context.UnhandledException += (sender, e) =>
{
exception = (InvalidTimeZoneException)e.ExceptionObject;
};
var e1 = new ManualResetEventSlim();
var e2 = new ManualResetEventSlim();
var task = Task.Run(() =>
{
context.Send((_) =>
{
e1.Set();
Assert.True(e2.Wait(Timeout), "timeout");
}, null);
});
Assert.True(e1.Wait(Timeout), "timeout");
// Act
context.Post((_) =>
{
throw new InvalidTimeZoneException();
}, null);
// Assert
Assert.False(e2.IsSet);
e2.Set(); // Unblock the first item
await task;
// Use another item to 'push through' the throwing one
context.Send((_) => { }, null);
Assert.NotNull(exception);
}
[Fact]
public async Task Post_BackgroundWorkItem_CanProcessMoreItemsInline()
{
// Arrange
var context = new RendererSynchronizationContext();
Thread capturedThread = null;
var e1 = new ManualResetEventSlim();
var e2 = new ManualResetEventSlim();
var e3 = new ManualResetEventSlim();
var e4 = new ManualResetEventSlim();
var e5 = new ManualResetEventSlim();
var e6 = new ManualResetEventSlim();
// Force task2 to execute in the background
var task1 = Task.Run(() => context.Send((_) =>
{
e1.Set();
Assert.True(e2.Wait(Timeout), "timeout");
}, null));
Assert.True(e1.Wait(Timeout), "timeout");
var task2 = Task.Run(() =>
{
context.Send((_) =>
{
e3.Set();
Assert.True(e4.Wait(Timeout), "timeout");
capturedThread = Thread.CurrentThread;
}, null);
});
e2.Set();
await task1;
Assert.True(e3.Wait(Timeout), "timeout");
// Act
//
// Now task2 is 'running' in the sync context. Schedule more work items - they will be
// run immediately after the second item
context.Post((_) =>
{
e5.Set();
Assert.Same(Thread.CurrentThread, capturedThread);
}, null);
context.Post((_) =>
{
e6.Set();
Assert.Same(Thread.CurrentThread, capturedThread);
}, null);
// Assert
e4.Set();
await task2;
Assert.True(e5.Wait(Timeout), "timeout");
Assert.True(e6.Wait(Timeout), "timeout");
}
[Fact]
public void Post_CapturesContext()
{
// Arrange
var context = new RendererSynchronizationContext();
var e1 = new ManualResetEventSlim();
// CultureInfo uses the execution context.
CultureInfo.CurrentCulture = new CultureInfo("en-GB");
CultureInfo capturedCulture = null;
SynchronizationContext capturedContext = null;
// Act
context.Post(async (_) =>
{
await Task.Yield();
capturedCulture = CultureInfo.CurrentCulture;
capturedContext = SynchronizationContext.Current;
e1.Set();
}, null);
// Assert
Assert.True(e1.Wait(Timeout), "timeout");
Assert.Same(CultureInfo.CurrentCulture, capturedCulture);
Assert.Same(context, capturedContext);
}
[Fact]
public void Send_CanRunSynchronously()
{
// Arrange
var context = new RendererSynchronizationContext();
var thread = Thread.CurrentThread;
Thread capturedThread = null;
// Act
context.Send((_) =>
{
capturedThread = Thread.CurrentThread;
}, null);
// Assert
Assert.Same(thread, capturedThread);
}
[Fact]
public void Send_CanRunSynchronously_Exception()
{
// Arrange
var context = new RendererSynchronizationContext();
// Act & Assert
Assert.Throws<InvalidTimeZoneException>(() => context.Send((_) =>
{
throw new InvalidTimeZoneException();
}, null));
}
[Fact]
public async Task Send_BlocksWhenOtherWorkRunning()
{
// Arrange
var context = new RendererSynchronizationContext();
var e1 = new ManualResetEventSlim();
var e2 = new ManualResetEventSlim();
var e3 = new ManualResetEventSlim();
var e4 = new ManualResetEventSlim();
// Force task2 to execute in the background
var task1 = Task.Run(() =>
{
context.Send((_) =>
{
e1.Set();
Assert.True(e2.Wait(Timeout), "timeout");
}, null);
});
Assert.True(e1.Wait(Timeout), "timeout");
// Act
//
// Dispatch this on the background thread because otherwise it would block.
var task2 = Task.Run(() =>
{
e3.Set();
context.Send((_) =>
{
e4.Set();
}, null);
});
// Assert
Assert.True(e3.Wait(Timeout), "timeout");
Assert.True(e3.IsSet);
// Unblock the first item
e2.Set();
await task1;
await task2;
Assert.True(e4.IsSet);
}
[Fact]
public void Send_CapturesContext()
{
// Arrange
var context = new RendererSynchronizationContext();
var e1 = new ManualResetEventSlim();
// CultureInfo uses the execution context.
CultureInfo.CurrentCulture = new CultureInfo("en-GB");
CultureInfo capturedCulture = null;
SynchronizationContext capturedContext = null;
// Act
context.Send(async (_) =>
{
await Task.Yield();
capturedCulture = CultureInfo.CurrentCulture;
capturedContext = SynchronizationContext.Current;
e1.Set();
}, null);
// Assert
Assert.True(e1.Wait(Timeout), "timeout");
Assert.Same(CultureInfo.CurrentCulture, capturedCulture);
Assert.Same(context, capturedContext);
}
[Fact]
public async Task InvokeAsync_Action_CanRunSynchronously_WhenNotBusy()
{
// Arrange
var context = new RendererSynchronizationContext();
var thread = Thread.CurrentThread;
Thread capturedThread = null;
// Act
var task = context.InvokeAsync(() =>
{
capturedThread = Thread.CurrentThread;
});
// Assert
await task;
Assert.Same(thread, capturedThread);
}
[Fact]
public async Task InvokeAsync_Action_CanRunAsynchronously_WhenBusy()
{
// Arrange
var context = new RendererSynchronizationContext();
var thread = Thread.CurrentThread;
Thread capturedThread = null;
var e1 = new ManualResetEventSlim();
var e2 = new ManualResetEventSlim();
var e3 = new ManualResetEventSlim();
var task1 = Task.Run(() =>
{
context.Send((_) =>
{
e1.Set();
Assert.True(e2.Wait(Timeout), "timeout");
}, null);
});
Assert.True(e1.Wait(Timeout), "timeout");
var task2 = context.InvokeAsync(() =>
{
capturedThread = Thread.CurrentThread;
e3.Set();
});
// Assert
Assert.False(e2.IsSet);
e2.Set(); // Unblock the first item
await task1;
Assert.True(e3.Wait(Timeout), "timeout");
await task2;
Assert.NotSame(thread, capturedThread);
}
[Fact]
public async Task InvokeAsync_Action_CanRethrowExceptions()
{
// Arrange
var context = new RendererSynchronizationContext();
// Act
var task = context.InvokeAsync((Action)(() =>
{
throw new InvalidTimeZoneException();
}));
// Assert
await Assert.ThrowsAsync<InvalidTimeZoneException>(async () => await task);
}
[Fact]
public async Task InvokeAsync_Action_CanReportCancellation()
{
// Arrange
var context = new RendererSynchronizationContext();
// Act
var task = context.InvokeAsync((Action)(() =>
{
throw new OperationCanceledException();
}));
// Assert
Assert.Equal(TaskStatus.Canceled, task.Status);
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await task);
}
[Fact]
public async Task InvokeAsync_FuncT_CanRunSynchronously_WhenNotBusy()
{
// Arrange
var context = new RendererSynchronizationContext();
var thread = Thread.CurrentThread;
// Act
var task = context.InvokeAsync(() =>
{
return Thread.CurrentThread;
});
// Assert
Assert.Same(thread, await task);
}
[Fact]
public async Task InvokeAsync_FuncT_CanRunAsynchronously_WhenBusy()
{
// Arrange
var context = new RendererSynchronizationContext();
var thread = Thread.CurrentThread;
var e1 = new ManualResetEventSlim();
var e2 = new ManualResetEventSlim();
var e3 = new ManualResetEventSlim();
var task1 = Task.Run(() =>
{
context.Send((_) =>
{
e1.Set();
Assert.True(e2.Wait(Timeout), "timeout");
}, null);
});
Assert.True(e1.Wait(Timeout), "timeout");
var task2 = context.InvokeAsync(() =>
{
e3.Set();
return Thread.CurrentThread;
});
// Assert
Assert.False(e2.IsSet);
e2.Set(); // Unblock the first item
await task1;
Assert.True(e3.Wait(Timeout), "timeout");
Assert.NotSame(thread, await task2);
}
[Fact]
public async Task InvokeAsync_FuncT_CanRethrowExceptions()
{
// Arrange
var context = new RendererSynchronizationContext();
// Act
var task = context.InvokeAsync<string>((Func<string>)(() =>
{
throw new InvalidTimeZoneException();
}));
// Assert
await Assert.ThrowsAsync<InvalidTimeZoneException>(async () => await task);
}
[Fact]
public async Task InvokeAsync_FuncT_CanReportCancellation()
{
// Arrange
var context = new RendererSynchronizationContext();
// Act
var task = context.InvokeAsync<string>((Func<string>)(() =>
{
throw new OperationCanceledException();
}));
// Assert
Assert.Equal(TaskStatus.Canceled, task.Status);
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await task);
}
[Fact]
public async Task InvokeAsync_FuncTask_CanRunSynchronously_WhenNotBusy()
{
// Arrange
var context = new RendererSynchronizationContext();
var thread = Thread.CurrentThread;
Thread capturedThread = null;
// Act
var task = context.InvokeAsync(() =>
{
capturedThread = Thread.CurrentThread;
return Task.CompletedTask;
});
// Assert
await task;
Assert.Same(thread, capturedThread);
}
[Fact]
public async Task InvokeAsync_FuncTask_CanRunAsynchronously_WhenBusy()
{
// Arrange
var context = new RendererSynchronizationContext();
var thread = Thread.CurrentThread;
Thread capturedThread = null;
var e1 = new ManualResetEventSlim();
var e2 = new ManualResetEventSlim();
var e3 = new ManualResetEventSlim();
var task1 = Task.Run(() =>
{
context.Send((_) =>
{
e1.Set();
Assert.True(e2.Wait(Timeout), "timeout");
}, null);
});
Assert.True(e1.Wait(Timeout), "timeout");
var task2 = context.InvokeAsync(() =>
{
capturedThread = Thread.CurrentThread;
e3.Set();
return Task.CompletedTask;
});
// Assert
Assert.False(e2.IsSet);
e2.Set(); // Unblock the first item
await task1;
Assert.True(e3.Wait(Timeout), "timeout");
await task2;
Assert.NotSame(thread, capturedThread);
}
[Fact]
public async Task InvokeAsync_FuncTask_CanRethrowExceptions()
{
// Arrange
var context = new RendererSynchronizationContext();
// Act
var task = context.InvokeAsync(() =>
{
throw new InvalidTimeZoneException();
});
// Assert
await Assert.ThrowsAsync<InvalidTimeZoneException>(async () => await task);
}
[Fact]
public async Task InvokeAsync_FuncTask_CanReportCancellation()
{
// Arrange
var context = new RendererSynchronizationContext();
// Act
var task = context.InvokeAsync(() =>
{
throw new OperationCanceledException();
});
// Assert
Assert.Equal(TaskStatus.Canceled, task.Status);
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await task);
}
[Fact]
public async Task InvokeAsync_FuncTaskT_CanRunSynchronously_WhenNotBusy()
{
// Arrange
var context = new RendererSynchronizationContext();
var thread = Thread.CurrentThread;
// Act
var task = context.InvokeAsync(() =>
{
return Task.FromResult(Thread.CurrentThread);
});
// Assert
Assert.Same(thread, await task);
}
[Fact]
public async Task InvokeAsync_FuncTaskT_CanRunAsynchronously_WhenBusy()
{
// Arrange
var context = new RendererSynchronizationContext();
var thread = Thread.CurrentThread;
var e1 = new ManualResetEventSlim();
var e2 = new ManualResetEventSlim();
var e3 = new ManualResetEventSlim();
var task1 = Task.Run(() =>
{
context.Send((_) =>
{
e1.Set();
Assert.True(e2.Wait(Timeout), "timeout");
}, null);
});
Assert.True(e1.Wait(Timeout), "timeout");
var task2 = context.InvokeAsync(() =>
{
e3.Set();
return Task.FromResult(Thread.CurrentThread);
});
// Assert
Assert.False(e2.IsSet);
e2.Set(); // Unblock the first item
await task1;
Assert.True(e3.Wait(Timeout), "timeout");
Assert.NotSame(thread, await task2);
}
[Fact]
public async Task InvokeAsync_FuncTaskT_CanRethrowExceptions()
{
// Arrange
var context = new RendererSynchronizationContext();
// Act
var task = context.InvokeAsync<string>((Func<Task<string>>)(() =>
{
throw new InvalidTimeZoneException();
}));
// Assert
await Assert.ThrowsAsync<InvalidTimeZoneException>(async () => await task);
}
[Fact]
public async Task InvokeAsync_FuncTaskT_CanReportCancellation()
{
// Arrange
var context = new RendererSynchronizationContext();
// Act
var task = context.InvokeAsync<string>((Func<Task<string>>)(() =>
{
throw new OperationCanceledException();
}));
// Assert
Assert.Equal(TaskStatus.Canceled, task.Status);
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await task);
}
[Fact]
public async Task InvokeAsync_SyncWorkInAsyncTaskIsCompletedFirst()
{
// Simplified version of ServerComponentRenderingTest.CanDispatchAsyncWorkToSyncContext
var expected = "First Second Third Fourth Fifth";
var context = new RendererSynchronizationContext();
string actual;
// Act
await Task.Yield();
actual = "First";
// this test assumes RendererSynchronizationContext optimization, which makes it synchronous execution.
// with multi-threading runtime and WebAssemblyDispatcher `InvokeAsync` will be executed asynchronously ordering it differently.
// See https://github.com/dotnet/aspnetcore/pull/52724#issuecomment-1895566632
var invokeTask = context.InvokeAsync(async () =>
{
// When the sync context is idle, queued work items start synchronously
actual += " Second";
await Task.Delay(250);
actual += " Fourth";
});
actual += " Third";
await invokeTask;
actual += " Fifth";
// Assert
Assert.Equal(expected, actual);
}
}
|