Files
MBN_STOCK_WEBVIEW/tests/MBN_STOCK_WEBVIEW.Playout.Tests/PlayoutFakes.cs

327 lines
9.1 KiB
C#

using System.Collections.Concurrent;
using System.Text.RegularExpressions;
using MBN_STOCK_WEBVIEW.Playout.Configuration;
using MBN_STOCK_WEBVIEW.Playout.Interop;
using MBN_STOCK_WEBVIEW.Playout.Registration;
using MBN_STOCK_WEBVIEW.Playout.Runtime;
using MBN_STOCK_WEBVIEW.Playout.Safety;
namespace MBN_STOCK_WEBVIEW.Playout.Tests;
internal sealed class FakeK3dSessionFactory : IK3dSessionFactory
{
private readonly Func<FakeK3dSession> _create;
private int _createCount;
public FakeK3dSessionFactory(Func<FakeK3dSession>? create = null)
{
_create = create ?? (() => new FakeK3dSession());
}
public int CreateCount => Volatile.Read(ref _createCount);
public ConcurrentQueue<FakeK3dSession> Sessions { get; } = new();
public IK3dSession Create()
{
Interlocked.Increment(ref _createCount);
var session = _create();
Sessions.Enqueue(session);
return session;
}
}
internal sealed class FakeK3dSdkDispatchLatch(bool allow = true) : IK3dSdkDispatchLatch
{
public IDisposable? TryClaim() => allow ? new Claim() : null;
private sealed class Claim : IDisposable
{
public void Dispose()
{
}
}
}
internal sealed class FakeK3dSession : IK3dGuardedConnectSession
{
private int _disposed;
private int _ktapDispatchGate;
private int _ktapDispatchCount;
public bool IsConnected { get; private set; }
public PlayoutKtapConnectState LastKtapConnectState { get; private set; }
public bool SupportsLifecycleCallbacks { get; set; }
public bool? KtapHelloObserved { get; set; }
public bool HasPendingLifecycleCallbacks { get; set; }
public bool HasPendingPlayCallbacks { get; set; }
public bool HasPendingTakeOutCallbacks { get; set; }
public bool ClearPendingLifecycleCallbacksAfterProcess { get; set; } = true;
public ConcurrentQueue<string> Calls { get; } = new();
public ConcurrentQueue<int> ThreadIds { get; } = new();
public ConcurrentQueue<ApartmentState> ApartmentStates { get; } = new();
public Action<ValidatedPlayoutOptions>? ConnectAction { get; set; }
public Action? BeforeKtapDispatchAction { get; set; }
public int KtapDispatchCount => Volatile.Read(ref _ktapDispatchCount);
public Action? DisconnectAction { get; set; }
public Action<PlayoutCue, int>? PrepareAction { get; set; }
public Action<PlayoutCue, int>? UpdateOnAirAction { get; set; }
public Action<int>? PlayAction { get; set; }
public Action<int, PlayoutTakeOutScope>? TakeOutAction { get; set; }
public Func<int, K3dCallbackDrainResult>? ProcessPendingCallbacksAction { get; set; }
public Action? AbandonAction { get; set; }
public Action<string>? CallObserver { get; set; }
public void Connect(ValidatedPlayoutOptions options)
=> ConnectCore(options, finalSafetyCheck: null);
public void ConnectGuarded(
ValidatedPlayoutOptions options,
Func<bool> finalSafetyCheck,
IK3dSdkDispatchLatch dispatchLatch)
{
ArgumentNullException.ThrowIfNull(finalSafetyCheck);
ArgumentNullException.ThrowIfNull(dispatchLatch);
ConnectCore(options, finalSafetyCheck, dispatchLatch);
}
private void ConnectCore(
ValidatedPlayoutOptions options,
Func<bool>? finalSafetyCheck,
IK3dSdkDispatchLatch? dispatchLatch = null)
{
Record("Connect");
BeforeKtapDispatchAction?.Invoke();
if (finalSafetyCheck is not null && !finalSafetyCheck())
{
throw new K3dConnectSafetyException();
}
using (var dispatchClaim = dispatchLatch?.TryClaim())
{
if (dispatchLatch is not null && dispatchClaim is null)
{
throw new K3dConnectSafetyException();
}
if (Interlocked.CompareExchange(ref _ktapDispatchGate, 1, 0) != 0)
{
throw new InvalidOperationException("Fake KTAP dispatch was prevented.");
}
LastKtapConnectState = PlayoutKtapConnectState.Attempted;
Interlocked.Increment(ref _ktapDispatchCount);
try
{
ConnectAction?.Invoke(options);
}
catch
{
LastKtapConnectState = PlayoutKtapConnectState.Failed;
throw;
}
}
LastKtapConnectState = PlayoutKtapConnectState.AcceptedUnconfirmed;
IsConnected = true;
}
public bool TryPreventKtapConnect()
{
var previous = Interlocked.CompareExchange(ref _ktapDispatchGate, 2, 0);
if (previous == 1 && LastKtapConnectState == PlayoutKtapConnectState.NotAttempted)
{
LastKtapConnectState = PlayoutKtapConnectState.Attempted;
}
return previous == 0;
}
public void Disconnect()
{
Record("Disconnect");
DisconnectAction?.Invoke();
IsConnected = false;
}
public void Prepare(PlayoutCue cue, int layoutIndex)
{
Record($"Prepare:{cue.SceneName}");
PrepareAction?.Invoke(cue, layoutIndex);
}
public void UpdateOnAir(PlayoutCue cue, int layoutIndex)
{
Record($"UpdateOnAir:{cue.SceneName}");
UpdateOnAirAction?.Invoke(cue, layoutIndex);
}
public void Play(int layoutIndex)
{
Record("Play");
PlayAction?.Invoke(layoutIndex);
}
public void TakeOut(int layoutIndex, PlayoutTakeOutScope scope)
{
Record($"TakeOut:{scope}");
TakeOutAction?.Invoke(layoutIndex, scope);
}
public K3dCallbackDrainResult ProcessPendingCallbacks(int layoutIndex)
{
Record("ProcessPendingCallbacks");
var result = ProcessPendingCallbacksAction?.Invoke(layoutIndex)
?? new K3dCallbackDrainResult(0, 0, KtapHelloObserved == true);
if (ClearPendingLifecycleCallbacksAfterProcess)
{
HasPendingLifecycleCallbacks = false;
HasPendingPlayCallbacks = false;
HasPendingTakeOutCallbacks = false;
}
return result;
}
public void Abandon()
{
TryPreventKtapConnect();
if (Interlocked.Exchange(ref _disposed, 1) == 0)
{
Record("Abandon");
AbandonAction?.Invoke();
IsConnected = false;
}
}
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) == 0)
{
Record("Dispose");
IsConnected = false;
}
}
private void Record(string name)
{
Calls.Enqueue(name);
ThreadIds.Enqueue(Environment.CurrentManagedThreadId);
ApartmentStates.Enqueue(Thread.CurrentThread.GetApartmentState());
CallObserver?.Invoke(name);
}
}
internal sealed class FakeRegistrationProbe : IK3dRegistrationProbe
{
private int _probeCount;
public FakeRegistrationProbe(K3dRegistrationReport? report = null)
{
Report = report ?? ReadyReport;
}
public static K3dRegistrationReport ReadyReport { get; } = new(
K3dRegistrationIssue.None,
Is64BitProcess: true,
HasWin64TypeLibrary: true,
IsWin64BinaryAmd64: true,
IsEngineClassReady: true,
IsEventHandlerClassReady: true,
Message: "fake K3D registration ready");
public K3dRegistrationReport Report { get; set; }
public int ProbeCount => Volatile.Read(ref _probeCount);
public K3dRegistrationReport Probe()
{
Interlocked.Increment(ref _probeCount);
return Report;
}
}
internal sealed class FakeTornadoProcessProbe : ITornadoProcessProbe
{
private readonly object _sync = new();
private readonly Queue<TornadoProcessSnapshot> _snapshots = new();
private TornadoProcessSnapshot _last;
public FakeTornadoProcessProbe(params TornadoProcessSnapshot[] snapshots)
{
_last = snapshots.LastOrDefault()
?? new TornadoProcessSnapshot(0, 0, 0);
foreach (var snapshot in snapshots)
{
_snapshots.Enqueue(snapshot);
}
}
public int CaptureCount { get; private set; }
public Regex? LastPattern { get; private set; }
public TornadoProcessSnapshot Capture(Regex? testWindowPattern)
{
lock (_sync)
{
CaptureCount++;
LastPattern = testWindowPattern;
if (_snapshots.Count > 0)
{
_last = _snapshots.Dequeue();
}
return _last;
}
}
}
internal sealed class FakeLiveAuthorization : ILiveAuthorization
{
public FakeLiveAuthorization(bool isAuthorizedForThisLaunch)
{
IsAuthorizedForThisLaunch = isAuthorizedForThisLaunch;
}
public bool IsAuthorizedForThisLaunch { get; set; }
}
internal sealed class TrackingStaDispatcherFactory : IStaDispatcherFactory
{
private int _createCount;
public int CreateCount => Volatile.Read(ref _createCount);
public ConcurrentQueue<IStaDispatcher> Dispatchers { get; } = new();
public IStaDispatcher Create(int capacity)
{
Interlocked.Increment(ref _createCount);
var dispatcher = new StaDispatcher(capacity);
Dispatchers.Enqueue(dispatcher);
return dispatcher;
}
}