fix: add truthful KTAP monitoring diagnostics

This commit is contained in:
2026-07-10 12:17:42 +09:00
parent fc932b27f6
commit 306d3a70a1
15 changed files with 764 additions and 33 deletions

View File

@@ -39,6 +39,9 @@ public sealed class DynamicK3dSessionTests
catch
{
Assert.False(session.IsConnected);
Assert.Equal(
PlayoutKtapConnectState.Failed,
session.LastKtapConnectState);
throw;
}
@@ -86,19 +89,29 @@ public sealed class DynamicK3dSessionTests
(engine, "Engine"),
(activator.EventHandler, "EventHandler"));
await using var dispatcher = new StaDispatcher(capacity: 1);
var lastKtapConnectState = PlayoutKtapConnectState.NotAttempted;
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => dispatcher.InvokeAsync(
() =>
{
using var session = new DynamicK3dSession(activator, releaser);
session.Connect(options);
try
{
session.Connect(options);
}
finally
{
lastKtapConnectState = session.LastKtapConnectState;
}
return true;
},
TimeSpan.FromSeconds(5),
CancellationToken.None));
Assert.Same(originalFailure, exception);
Assert.Equal(PlayoutKtapConnectState.AcceptedUnconfirmed, lastKtapConnectState);
Assert.Equal(
new[]
{
@@ -134,19 +147,29 @@ public sealed class DynamicK3dSessionTests
(engine, "Engine"),
(activator.EventHandler, "EventHandler"));
await using var dispatcher = new StaDispatcher(capacity: 1);
var lastKtapConnectState = PlayoutKtapConnectState.NotAttempted;
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => dispatcher.InvokeAsync(
() =>
{
using var session = new DynamicK3dSession(activator, releaser);
session.Connect(options);
try
{
session.Connect(options);
}
finally
{
lastKtapConnectState = session.LastKtapConnectState;
}
return true;
},
TimeSpan.FromSeconds(5),
CancellationToken.None));
Assert.Same(originalFailure, exception);
Assert.Equal(PlayoutKtapConnectState.Failed, lastKtapConnectState);
Assert.Equal(
new[]
{
@@ -190,6 +213,9 @@ public sealed class DynamicK3dSessionTests
{
using var session = new DynamicK3dSession(activator);
session.Connect(options);
Assert.Equal(
PlayoutKtapConnectState.AcceptedUnconfirmed,
session.LastKtapConnectState);
session.Prepare(cue, options.LayoutIndex);
session.Play(options.LayoutIndex);
session.TakeOut(options.LayoutIndex, takeOutScope);
@@ -236,6 +262,88 @@ public sealed class DynamicK3dSessionTests
Assert.Equal(typeof(object), create.ReturnType);
}
[Fact]
public async Task Connect_WhenComActivationFails_ReportsKtapNotAttempted()
{
using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
var options = ValidatedPlayoutOptions.Create(TestOptions(scenes.Path));
var originalFailure = new InvalidOperationException("fake activation failure");
var lastKtapConnectState = PlayoutKtapConnectState.Failed;
await using var dispatcher = new StaDispatcher(capacity: 1);
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => dispatcher.InvokeAsync(
() =>
{
using var session = new DynamicK3dSession(
new ThrowingActivator(originalFailure));
try
{
session.Connect(options);
}
finally
{
lastKtapConnectState = session.LastKtapConnectState;
}
return true;
},
TimeSpan.FromSeconds(5),
CancellationToken.None));
Assert.Same(originalFailure, exception);
Assert.Equal(PlayoutKtapConnectState.NotAttempted, lastKtapConnectState);
}
[Fact]
public async Task PreventKtapConnect_DuringActivation_BlocksLateDispatch()
{
using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
using var activationStarted = new ManualResetEventSlim();
using var releaseActivation = new ManualResetEventSlim();
var options = ValidatedPlayoutOptions.Create(TestOptions(scenes.Path));
var log = new FakeComLog();
var engine = new FakeEngine(log, new FakePlayer(log), new FakeScene(log));
var activator = new BlockingEngineActivator(
log,
engine,
activationStarted,
releaseActivation);
var session = new DynamicK3dSession(activator);
await using var dispatcher = new StaDispatcher(capacity: 1);
try
{
var connectTask = dispatcher.InvokeAsync(
() =>
{
using (session)
{
session.Connect(options);
}
return true;
},
TimeSpan.FromSeconds(5),
CancellationToken.None);
Assert.True(activationStarted.Wait(TimeSpan.FromSeconds(2)));
Assert.True(session.TryPreventKtapConnect());
releaseActivation.Set();
await Assert.ThrowsAsync<InvalidOperationException>(() => connectTask);
Assert.Equal(
PlayoutKtapConnectState.NotAttempted,
session.LastKtapConnectState);
Assert.DoesNotContain(log.Names, name =>
name.StartsWith("KTAPConnect:", StringComparison.Ordinal));
}
finally
{
releaseActivation.Set();
}
}
[Fact]
public async Task Abandon_ReleasesEveryComReferenceWithoutDisconnect()
{
@@ -325,6 +433,61 @@ public sealed class DynamicK3dSessionTests
}
}
private sealed class ThrowingActivator : ILateBoundComActivator
{
private readonly Exception _exception;
public ThrowingActivator(Exception exception)
{
_exception = exception;
}
public object Create(Guid classId) => throw _exception;
}
private sealed class BlockingEngineActivator : ILateBoundComActivator
{
private readonly FakeComLog _log;
private readonly FakeEngine _engine;
private readonly ManualResetEventSlim _activationStarted;
private readonly ManualResetEventSlim _releaseActivation;
private readonly object _eventHandler = new();
public BlockingEngineActivator(
FakeComLog log,
FakeEngine engine,
ManualResetEventSlim activationStarted,
ManualResetEventSlim releaseActivation)
{
_log = log;
_engine = engine;
_activationStarted = activationStarted;
_releaseActivation = releaseActivation;
}
public object Create(Guid classId)
{
_log.Add($"Create:{classId:B}");
if (classId == K3dComConstants.KaEventHandlerClassGuid)
{
return _eventHandler;
}
if (classId != K3dComConstants.KaEngineClassGuid)
{
throw new InvalidOperationException("Unexpected fake CLSID.");
}
_activationStarted.Set();
if (!_releaseActivation.Wait(TimeSpan.FromSeconds(5)))
{
throw new TimeoutException("fake activation coordination timeout");
}
return _engine;
}
}
public sealed class FakeEngine
{
private readonly FakeComLog _log;