feat: add safe Tornado K3D playout adapter
This commit is contained in:
169
tests/MBN_STOCK_WEBVIEW.Playout.Tests/StaDispatcherTests.cs
Normal file
169
tests/MBN_STOCK_WEBVIEW.Playout.Tests/StaDispatcherTests.cs
Normal file
@@ -0,0 +1,169 @@
|
||||
using System.Collections.Concurrent;
|
||||
using MBN_STOCK_WEBVIEW.Playout.Runtime;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.Playout.Tests;
|
||||
|
||||
public sealed class StaDispatcherTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task InvokeAsync_RunsCallbacksOnOneStaThreadInFifoOrder()
|
||||
{
|
||||
await using var dispatcher = new StaDispatcher(capacity: 4);
|
||||
using var firstStarted = new ManualResetEventSlim();
|
||||
using var releaseFirst = new ManualResetEventSlim();
|
||||
var calls = new ConcurrentQueue<(int Order, int ThreadId, ApartmentState Apartment)>();
|
||||
|
||||
var first = dispatcher.InvokeAsync(
|
||||
() =>
|
||||
{
|
||||
calls.Enqueue((
|
||||
1,
|
||||
Environment.CurrentManagedThreadId,
|
||||
Thread.CurrentThread.GetApartmentState()));
|
||||
firstStarted.Set();
|
||||
Assert.True(releaseFirst.Wait(TimeSpan.FromSeconds(5)));
|
||||
return 1;
|
||||
},
|
||||
TimeSpan.FromSeconds(10),
|
||||
CancellationToken.None);
|
||||
Assert.True(firstStarted.Wait(TimeSpan.FromSeconds(5)));
|
||||
|
||||
var second = dispatcher.InvokeAsync(
|
||||
() =>
|
||||
{
|
||||
calls.Enqueue((
|
||||
2,
|
||||
Environment.CurrentManagedThreadId,
|
||||
Thread.CurrentThread.GetApartmentState()));
|
||||
return 2;
|
||||
},
|
||||
TimeSpan.FromSeconds(10),
|
||||
CancellationToken.None);
|
||||
var third = dispatcher.InvokeAsync(
|
||||
() =>
|
||||
{
|
||||
calls.Enqueue((
|
||||
3,
|
||||
Environment.CurrentManagedThreadId,
|
||||
Thread.CurrentThread.GetApartmentState()));
|
||||
return 3;
|
||||
},
|
||||
TimeSpan.FromSeconds(10),
|
||||
CancellationToken.None);
|
||||
|
||||
releaseFirst.Set();
|
||||
var results = await Task.WhenAll(first, second, third);
|
||||
Assert.Equal(new[] { 1, 2, 3 }, results);
|
||||
|
||||
var recorded = calls.ToArray();
|
||||
Assert.Equal([1, 2, 3], recorded.Select(call => call.Order));
|
||||
Assert.Single(recorded.Select(call => call.ThreadId).Distinct());
|
||||
Assert.All(recorded, call => Assert.Equal(ApartmentState.STA, call.Apartment));
|
||||
Assert.Equal(recorded[0].ThreadId, dispatcher.ManagedThreadId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_CancelledWhileQueued_DoesNotRunCallback()
|
||||
{
|
||||
await using var dispatcher = new StaDispatcher(capacity: 2);
|
||||
using var firstStarted = new ManualResetEventSlim();
|
||||
using var releaseFirst = new ManualResetEventSlim();
|
||||
var cancelledCallbackRan = false;
|
||||
|
||||
var first = dispatcher.InvokeAsync(
|
||||
() =>
|
||||
{
|
||||
firstStarted.Set();
|
||||
Assert.True(releaseFirst.Wait(TimeSpan.FromSeconds(5)));
|
||||
return 1;
|
||||
},
|
||||
TimeSpan.FromSeconds(10),
|
||||
CancellationToken.None);
|
||||
Assert.True(firstStarted.Wait(TimeSpan.FromSeconds(5)));
|
||||
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
var cancelled = dispatcher.InvokeAsync(
|
||||
() =>
|
||||
{
|
||||
cancelledCallbackRan = true;
|
||||
return 2;
|
||||
},
|
||||
TimeSpan.FromSeconds(10),
|
||||
cancellation.Token);
|
||||
cancellation.Cancel();
|
||||
releaseFirst.Set();
|
||||
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => cancelled);
|
||||
Assert.Equal(1, await first);
|
||||
Assert.False(cancelledCallbackRan);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_WhenBoundedQueueIsFull_CanCancelWithoutRunning()
|
||||
{
|
||||
await using var dispatcher = new StaDispatcher(capacity: 1);
|
||||
using var firstStarted = new ManualResetEventSlim();
|
||||
using var releaseFirst = new ManualResetEventSlim();
|
||||
var thirdCallbackRan = false;
|
||||
|
||||
var first = dispatcher.InvokeAsync(
|
||||
() =>
|
||||
{
|
||||
firstStarted.Set();
|
||||
Assert.True(releaseFirst.Wait(TimeSpan.FromSeconds(5)));
|
||||
return 1;
|
||||
},
|
||||
TimeSpan.FromSeconds(10),
|
||||
CancellationToken.None);
|
||||
Assert.True(firstStarted.Wait(TimeSpan.FromSeconds(5)));
|
||||
|
||||
var second = dispatcher.InvokeAsync(
|
||||
() => 2,
|
||||
TimeSpan.FromSeconds(10),
|
||||
CancellationToken.None);
|
||||
using var cancellation = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
|
||||
var third = dispatcher.InvokeAsync(
|
||||
() =>
|
||||
{
|
||||
thirdCallbackRan = true;
|
||||
return 3;
|
||||
},
|
||||
TimeSpan.FromSeconds(10),
|
||||
cancellation.Token);
|
||||
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => third);
|
||||
Assert.False(thirdCallbackRan);
|
||||
releaseFirst.Set();
|
||||
var results = await Task.WhenAll(first, second);
|
||||
Assert.Equal(new[] { 1, 2 }, results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_TimeoutQuarantinesDispatcherAndRejectsLaterWork()
|
||||
{
|
||||
await using var dispatcher = new StaDispatcher(capacity: 2);
|
||||
using var callbackStarted = new ManualResetEventSlim();
|
||||
using var releaseCallback = new ManualResetEventSlim();
|
||||
|
||||
var timedOut = dispatcher.InvokeAsync(
|
||||
() =>
|
||||
{
|
||||
callbackStarted.Set();
|
||||
Assert.True(releaseCallback.Wait(TimeSpan.FromSeconds(5)));
|
||||
return 1;
|
||||
},
|
||||
TimeSpan.FromMilliseconds(100),
|
||||
CancellationToken.None);
|
||||
Assert.True(callbackStarted.Wait(TimeSpan.FromSeconds(5)));
|
||||
|
||||
await Assert.ThrowsAsync<StaOperationTimedOutException>(() => timedOut);
|
||||
Assert.True(dispatcher.IsQuarantined);
|
||||
await Assert.ThrowsAsync<StaDispatcherQuarantinedException>(
|
||||
() => dispatcher.InvokeAsync(
|
||||
() => 2,
|
||||
TimeSpan.FromSeconds(1),
|
||||
CancellationToken.None));
|
||||
|
||||
releaseCallback.Set();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user