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

494 lines
17 KiB
C#

using System.Collections.Concurrent;
using System.Net;
using System.Text.Json;
using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Playout.Diagnostics;
using MBN_STOCK_WEBVIEW.Playout.Interop;
namespace MBN_STOCK_WEBVIEW.Playout.Tests;
public sealed class PgmConnectDiagnosticTests
{
[Fact]
public void RealContactDiagnosticApi_IsNotPublic()
{
Assert.False(typeof(PgmConnectDiagnosticRunner).IsVisible);
Assert.False(typeof(PgmConnectDiagnosticRequest).IsVisible);
Assert.False(typeof(PgmConnectDiagnosticReport).IsVisible);
Assert.Null(typeof(PgmConnectDiagnosticRunner).GetMethod(
nameof(PgmConnectDiagnosticRunner.ExecuteAsync),
System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.Public));
}
[Theory]
[InlineData("localhost", 30001, "PGM")]
[InlineData("192.0.2.1", 30001, "PGM")]
[InlineData("127.0.0.1", 0, "PGM")]
[InlineData("127.0.0.1", 30001, "TEST")]
[InlineData("127.0.0.1", 30001, "CONPROGRAM")]
public void RequestValidation_RejectsUnsafeEndpointOrWindow(
string host,
int port,
string title)
{
var valid = PgmConnectDiagnosticRunner.TryValidateRequest(
new PgmConnectDiagnosticRequest(host, port, title),
out _);
Assert.False(valid);
}
[Fact]
public async Task Runner_SuccessUsesOnlyConnectThenDisconnectAndReportsUnconfirmedEvidence()
{
var snapshot = ReadySnapshot(10);
var safety = new FakeSafetyProbe(snapshot, snapshot, snapshot, snapshot);
var session = new FakeConnectOnlySession();
var factory = new FakeConnectOnlySessionFactory(session);
var runner = new PgmConnectDiagnosticRunner(
safety,
factory,
new FakeRegistrationProbe(),
new TrackingStaDispatcherFactory());
var request = new PgmConnectDiagnosticRequest("127.0.0.1", 30001, "PGM");
var report = await runner.ExecuteAsync(request);
Assert.Equal(["Connect", "Disconnect"], session.Calls);
Assert.True(report.Completed);
Assert.False(report.OutcomeUnknown);
Assert.True(report.ConnectRequestIssued);
Assert.True(report.ComActivationAttempted);
Assert.Equal(PlayoutKtapConnectState.AcceptedUnconfirmed, report.LastKtapConnectState);
Assert.True(report.KtapConnectAttempted);
Assert.True(report.KtapConnectAccepted);
Assert.Null(report.KtapHelloObserved);
Assert.True(report.NetworkMonitoringRecordExpected);
Assert.True(report.NetworkMonitoringCheckRequired);
Assert.Null(report.NetworkMonitoringVerified);
Assert.Equal("success", report.DisconnectCode);
Assert.False(report.RenderCommandSurfaceExposed);
Assert.False(report.RenderCommandAttempted);
Assert.False(report.AutomaticReconnectEnabled);
var json = JsonSerializer.Serialize(report);
Assert.DoesNotContain("127.0.0.1", json, StringComparison.Ordinal);
Assert.DoesNotContain("30001", json, StringComparison.Ordinal);
Assert.DoesNotContain("PGM", json, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Runner_TargetRejectedStopsBeforeRegistrationAndCom()
{
var rejected = new PgmDiagnosticTargetSnapshot(false, 0, 0, false, null);
var factory = new FakeConnectOnlySessionFactory(new FakeConnectOnlySession());
var registration = new FakeRegistrationProbe();
var runner = new PgmConnectDiagnosticRunner(
new FakeSafetyProbe(rejected),
factory,
registration,
new TrackingStaDispatcherFactory());
var report = await runner.ExecuteAsync(
new PgmConnectDiagnosticRequest("127.0.0.1", 30001, "PGM"));
Assert.False(report.Completed);
Assert.False(report.ConnectRequestIssued);
Assert.False(report.ComActivationAttempted);
Assert.Equal(0, factory.CreateCount);
Assert.Equal(0, registration.ProbeCount);
Assert.Equal("pgm-target-rejected", report.ErrorCode);
}
[Fact]
public async Task Runner_UnapprovedInstalledInteropIsDistinctFromComActivationFailure()
{
var snapshot = ReadySnapshot(10);
var runner = new PgmConnectDiagnosticRunner(
new FakeSafetyProbe(snapshot),
new UnavailableInteropSessionFactory(),
new FakeRegistrationProbe(),
new TrackingStaDispatcherFactory());
var report = await runner.ExecuteAsync(
new PgmConnectDiagnosticRequest("127.0.0.1", 30001, "PGM"));
Assert.False(report.Completed);
Assert.False(report.ComActivationAttempted);
Assert.False(report.KtapConnectAttempted);
Assert.False(report.OutcomeUnknown);
Assert.Equal("not-attempted", report.DisconnectCode);
Assert.Equal("installed-interop-metadata-unavailable", report.ErrorCode);
}
[Fact]
public async Task Runner_TargetChangeAfterAcceptanceAbandonsWithoutDisconnect()
{
var baseline = ReadySnapshot(10);
var changed = ReadySnapshot(11);
var session = new FakeConnectOnlySession();
var runner = new PgmConnectDiagnosticRunner(
new FakeSafetyProbe(baseline, baseline, baseline, changed),
new FakeConnectOnlySessionFactory(session),
new FakeRegistrationProbe(),
new TrackingStaDispatcherFactory());
var report = await runner.ExecuteAsync(
new PgmConnectDiagnosticRequest("127.0.0.1", 30001, "PGM"));
Assert.Equal(["Connect", "Abandon"], session.Calls);
Assert.False(report.Completed);
Assert.True(report.OutcomeUnknown);
Assert.True(report.KtapConnectAttempted);
Assert.DoesNotContain("Disconnect", session.Calls);
Assert.Equal("pgm-target-changed-after-connect", report.ErrorCode);
}
[Fact]
public async Task DynamicSession_InvokesOnlyKtapConnectAndDisconnect()
{
var log = new ConcurrentQueue<string>();
var session = new DynamicKtapConnectOnlySession(new RecordingComBinding(log));
await using var dispatcher = new Runtime.StaDispatcher(1);
await dispatcher.InvokeAsync(
() =>
{
session.Connect("127.0.0.1", 30001, static () => true);
session.Disconnect();
return true;
},
TimeSpan.FromSeconds(2),
CancellationToken.None);
Assert.Equal(
[
"CreateEventHandler",
"CreateEngine",
"KTAPConnect",
"Disconnect",
"Release",
"Release"
],
log);
Assert.Equal(PlayoutKtapConnectState.AcceptedUnconfirmed, session.LastKtapConnectState);
}
[Fact]
public async Task DynamicSession_FinalSafetyCheckRunsAfterActivationAndBeforeKtap()
{
var log = new ConcurrentQueue<string>();
var session = new DynamicKtapConnectOnlySession(new RecordingComBinding(log));
await using var dispatcher = new Runtime.StaDispatcher(1);
await Assert.ThrowsAsync<PgmDiagnosticSafetyException>(() =>
dispatcher.InvokeAsync(
() =>
{
session.Connect(
"127.0.0.1",
30001,
() =>
{
log.Enqueue("FinalSafetyCheck");
return false;
});
return true;
},
TimeSpan.FromSeconds(2),
CancellationToken.None));
Assert.Equal(
[
"CreateEventHandler",
"CreateEngine",
"FinalSafetyCheck",
"Release",
"Release"
],
log);
Assert.DoesNotContain("KTAPConnect", log);
Assert.Equal(PlayoutKtapConnectState.NotAttempted, session.LastKtapConnectState);
}
[Fact]
public async Task Runner_ConnectTimeoutPreventsLateKtapDispatch()
{
var snapshot = ReadySnapshot(10);
using var safety = new BlockingSecondCaptureSafetyProbe(snapshot);
var session = new FakeConnectOnlySession();
var dispatcherFactory = new TrackingStaDispatcherFactory();
var runner = new PgmConnectDiagnosticRunner(
safety,
new FakeConnectOnlySessionFactory(session),
new FakeRegistrationProbe(),
dispatcherFactory,
TimeSpan.FromMilliseconds(50),
TimeSpan.FromSeconds(1));
var execution = runner.ExecuteAsync(
new PgmConnectDiagnosticRequest("127.0.0.1", 30001, "PGM"));
try
{
Assert.True(safety.SecondCaptureEntered.Wait(TimeSpan.FromSeconds(2)));
Assert.True(session.PreventionAttempted.Wait(TimeSpan.FromSeconds(2)));
}
finally
{
safety.ReleaseSecondCapture.Set();
}
var report = await execution.WaitAsync(TimeSpan.FromSeconds(3));
Assert.True(session.ConnectCompleted.Wait(TimeSpan.FromSeconds(2)));
Assert.Equal("connect-timeout", report.ErrorCode);
Assert.False(report.Completed);
Assert.False(report.OutcomeUnknown);
Assert.False(report.KtapConnectAttempted);
Assert.Equal(PlayoutKtapConnectState.NotAttempted, report.LastKtapConnectState);
Assert.False(session.ComActivationAttempted);
Assert.Equal(0, session.DispatchCount);
Assert.Contains("Connect", session.Calls);
}
[Fact]
public void ConnectOnlySessionSurfaceHasNoRenderCommand()
{
var methodNames = typeof(IKtapConnectOnlySession)
.GetMethods()
.Select(method => method.Name)
.ToArray();
string[] forbidden =
[
"GetScenePlayer",
"LoadScene",
"Prepare",
"Play",
"PlayDirect",
"TakeIn",
"Next",
"TakeOut",
"CutOut",
"Stop",
"StopAll"
];
Assert.DoesNotContain(methodNames, name =>
forbidden.Contains(name, StringComparer.OrdinalIgnoreCase));
Assert.Contains("Connect", methodNames);
Assert.Contains("Disconnect", methodNames);
}
[Fact]
public void ListenerPortDecoderUsesNetworkByteOrder()
{
const ushort port = 30001;
var encoded = unchecked((uint)(ushort)IPAddress.HostToNetworkOrder((short)port));
Assert.Equal(port, WindowsTcpListenerOwnershipProbe.DecodePort(encoded));
}
private static PgmDiagnosticTargetSnapshot ReadySnapshot(int processId) => new(
true,
1,
1,
true,
new PgmDiagnosticTargetIdentity(processId, 100));
private sealed class FakeSafetyProbe : IPgmDiagnosticSafetyProbe
{
private readonly Queue<PgmDiagnosticTargetSnapshot> _snapshots;
private PgmDiagnosticTargetSnapshot _last;
public FakeSafetyProbe(params PgmDiagnosticTargetSnapshot[] snapshots)
{
_snapshots = new Queue<PgmDiagnosticTargetSnapshot>(snapshots);
_last = snapshots.Last();
}
public PgmDiagnosticTargetSnapshot Capture(
PgmConnectDiagnosticRequest request,
IPAddress parsedHost)
{
if (_snapshots.Count > 0)
{
_last = _snapshots.Dequeue();
}
return _last;
}
}
private sealed class BlockingSecondCaptureSafetyProbe : IPgmDiagnosticSafetyProbe, IDisposable
{
private readonly PgmDiagnosticTargetSnapshot _snapshot;
private int _captureCount;
public BlockingSecondCaptureSafetyProbe(PgmDiagnosticTargetSnapshot snapshot)
{
_snapshot = snapshot;
}
public ManualResetEventSlim SecondCaptureEntered { get; } = new(false);
public ManualResetEventSlim ReleaseSecondCapture { get; } = new(false);
public PgmDiagnosticTargetSnapshot Capture(
PgmConnectDiagnosticRequest request,
IPAddress parsedHost)
{
if (Interlocked.Increment(ref _captureCount) == 2)
{
SecondCaptureEntered.Set();
ReleaseSecondCapture.Wait();
}
return _snapshot;
}
public void Dispose()
{
ReleaseSecondCapture.Set();
SecondCaptureEntered.Dispose();
ReleaseSecondCapture.Dispose();
}
}
private sealed class FakeConnectOnlySessionFactory : IKtapConnectOnlySessionFactory
{
private readonly IKtapConnectOnlySession _session;
public FakeConnectOnlySessionFactory(IKtapConnectOnlySession session)
{
_session = session;
}
public int CreateCount { get; private set; }
public IKtapConnectOnlySession Create()
{
CreateCount++;
return _session;
}
}
private sealed class UnavailableInteropSessionFactory : IKtapConnectOnlySessionFactory
{
public IKtapConnectOnlySession Create() =>
throw new InstalledK3dInteropMetadataUnavailableException();
}
private sealed class FakeConnectOnlySession : IKtapConnectOnlySession
{
private int _dispatchGate;
private int _dispatchCount;
private int _abandonRecorded;
public ConcurrentQueue<string> Calls { get; } = new();
public ManualResetEventSlim PreventionAttempted { get; } = new(false);
public ManualResetEventSlim ConnectCompleted { get; } = new(false);
public int DispatchCount => Volatile.Read(ref _dispatchCount);
public bool ComActivationAttempted { get; private set; }
public PlayoutKtapConnectState LastKtapConnectState { get; private set; }
public bool TryPreventKtapConnect()
{
var previous = Interlocked.CompareExchange(ref _dispatchGate, 2, 0);
if (previous == 1 && LastKtapConnectState == PlayoutKtapConnectState.NotAttempted)
{
LastKtapConnectState = PlayoutKtapConnectState.Attempted;
}
// Publish the test synchronization point only after the dispatch gate
// is closed. Signalling first lets the blocked safety probe resume and
// race KTAP dispatch before this prevention attempt has taken effect.
PreventionAttempted.Set();
return previous == 0;
}
public void Connect(string host, int port, Func<bool> finalSafetyCheck)
{
Calls.Enqueue("Connect");
try
{
if (Volatile.Read(ref _dispatchGate) != 0)
{
throw new InvalidOperationException();
}
ComActivationAttempted = true;
if (!finalSafetyCheck())
{
throw new PgmDiagnosticSafetyException();
}
if (Interlocked.CompareExchange(ref _dispatchGate, 1, 0) != 0)
{
throw new InvalidOperationException();
}
Interlocked.Increment(ref _dispatchCount);
LastKtapConnectState = PlayoutKtapConnectState.AcceptedUnconfirmed;
}
finally
{
ConnectCompleted.Set();
}
}
public void Disconnect() => Calls.Enqueue("Disconnect");
public void Abandon()
{
TryPreventKtapConnect();
if (Interlocked.Exchange(ref _abandonRecorded, 1) == 0)
{
Calls.Enqueue("Abandon");
}
}
}
private sealed class RecordingComBinding : IK3dConnectOnlyComBinding
{
private readonly ConcurrentQueue<string> _log;
public RecordingComBinding(ConcurrentQueue<string> log)
{
_log = log;
}
public object CreateEventHandler()
{
_log.Enqueue("CreateEventHandler");
return new object();
}
public object CreateEngine()
{
_log.Enqueue("CreateEngine");
return new object();
}
public int KtapConnect(
object engine,
string host,
int port,
object eventHandler)
{
_log.Enqueue("KTAPConnect");
return 1;
}
public void Disconnect(object engine) => _log.Enqueue("Disconnect");
public void Release(object value) => _log.Enqueue("Release");
}
}