327 lines
9.1 KiB
C#
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;
|
|
}
|
|
}
|