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

1111 lines
40 KiB
C#

using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using MBN_STOCK_WEBVIEW.Playout.Configuration;
using MBN_STOCK_WEBVIEW.Playout.Interop;
using MBN_STOCK_WEBVIEW.Playout.Runtime;
namespace MBN_STOCK_WEBVIEW.Playout.Tests;
public sealed class DynamicK3dSessionTests
{
[Theory]
[InlineData(0)]
[InlineData(-1)]
public async Task Connect_WhenKtapDoesNotReturnOne_FailsClosed(int connectResult)
{
using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
var options = ValidatedPlayoutOptions.Create(TestOptions(scenes.Path));
var log = new FakeComLog();
var engine = new FakeEngine(
log,
new FakePlayer(log),
new FakeScene(log),
connectResult);
var activator = new FakeActivator(log, engine);
var releaser = new FakeReleaser(
log,
(engine, "Engine"),
(activator.EventHandler, "EventHandler"));
await using var dispatcher = new StaDispatcher(capacity: 1);
var exception = await Assert.ThrowsAnyAsync<Exception>(
() => dispatcher.InvokeAsync(
() =>
{
using var session = new DynamicK3dSession(activator, releaser);
try
{
session.Connect(options);
}
catch
{
Assert.False(session.IsConnected);
Assert.Equal(
PlayoutKtapConnectState.Failed,
session.LastKtapConnectState);
throw;
}
return true;
},
TimeSpan.FromSeconds(5),
CancellationToken.None));
Assert.DoesNotContain(options.Host, exception.Message, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain(scenes.Path, exception.Message, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain(connectResult.ToString(), exception.Message, StringComparison.Ordinal);
Assert.DoesNotContain("GetScenePlayer", log.Names);
Assert.Equal(
new[]
{
$"Create:{K3dComConstants.KaEventHandlerClassGuid:B}",
$"Create:{K3dComConstants.KaEngineClassGuid:B}",
"KTAPConnect:1:127.0.0.1:30001:0",
"Disconnect",
"Release:Engine",
"Release:EventHandler"
},
log.Names);
Assert.Equal(1, log.Names.Count(name => name == "Disconnect"));
Assert.Single(log.ThreadIds.Distinct());
Assert.All(log.ApartmentStates, state => Assert.Equal(ApartmentState.STA, state));
}
[Fact]
public async Task Connect_WhenPlayerInitializationFails_DisconnectsOnceAndPreservesOriginalFailure()
{
using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
var options = ValidatedPlayoutOptions.Create(TestOptions(scenes.Path));
var log = new FakeComLog();
var originalFailure = new InvalidOperationException("fake original player failure");
var engine = new FakeEngine(
log,
new FakePlayer(log),
new FakeScene(log),
playerFailure: originalFailure,
disconnectFailure: new InvalidOperationException("fake cleanup failure"));
var activator = new FakeActivator(log, engine);
var releaser = new FakeReleaser(
log,
(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);
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[]
{
$"Create:{K3dComConstants.KaEventHandlerClassGuid:B}",
$"Create:{K3dComConstants.KaEngineClassGuid:B}",
"KTAPConnect:1:127.0.0.1:30001:0",
"GetScenePlayerOnChannel:9",
"Disconnect",
"Release:Engine",
"Release:EventHandler"
},
log.Names);
Assert.Equal(1, log.Names.Count(name => name == "Disconnect"));
Assert.Single(log.ThreadIds.Distinct());
Assert.All(log.ApartmentStates, state => Assert.Equal(ApartmentState.STA, state));
}
[Fact]
public async Task Connect_WhenKtapInvocationThrows_AttemptsDisconnectAndPreservesOriginalFailure()
{
using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
var options = ValidatedPlayoutOptions.Create(TestOptions(scenes.Path));
var log = new FakeComLog();
var originalFailure = new InvalidOperationException("fake original KTAP failure");
var engine = new FakeEngine(
log,
new FakePlayer(log),
new FakeScene(log),
connectFailure: originalFailure);
var activator = new FakeActivator(log, engine);
var releaser = new FakeReleaser(
log,
(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);
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[]
{
$"Create:{K3dComConstants.KaEventHandlerClassGuid:B}",
$"Create:{K3dComConstants.KaEngineClassGuid:B}",
"KTAPConnect:1:127.0.0.1:30001:0",
"Disconnect",
"Release:Engine",
"Release:EventHandler"
},
log.Names);
Assert.Equal(1, log.Names.Count(name => name == "Disconnect"));
}
[Fact]
public async Task GuardedConnect_WhenFinalTargetCheckFails_BlocksKtapAndNeverDisconnects()
{
using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
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 FakeActivator(log, engine);
var releaser = new FakeReleaser(
log,
(engine, "Engine"),
(activator.EventHandler, "EventHandler"));
await using var dispatcher = new StaDispatcher(capacity: 1);
await dispatcher.InvokeAsync(
() =>
{
using var session = new DynamicK3dSession(activator, releaser);
Assert.Throws<K3dConnectSafetyException>(() => session.ConnectGuarded(
options,
() =>
{
log.Add("FinalSafetyCheck");
return false;
},
new FakeK3dSdkDispatchLatch()));
Assert.Equal(
PlayoutKtapConnectState.NotAttempted,
session.LastKtapConnectState);
return true;
},
TimeSpan.FromSeconds(5),
CancellationToken.None);
Assert.Equal(
new[]
{
$"Create:{K3dComConstants.KaEventHandlerClassGuid:B}",
$"Create:{K3dComConstants.KaEngineClassGuid:B}",
"FinalSafetyCheck",
"Release:Engine",
"Release:EventHandler"
},
log.Names);
Assert.DoesNotContain(log.Names, name =>
name.StartsWith("KTAPConnect:", StringComparison.Ordinal));
Assert.DoesNotContain("Disconnect", log.Names);
}
[Fact]
public async Task GuardedConnect_WhenKtapThrows_ReleasesWithoutSdkDisconnect()
{
using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
var options = ValidatedPlayoutOptions.Create(TestOptions(scenes.Path));
var log = new FakeComLog();
var failure = new COMException("fake ambiguous guarded connect failure");
var engine = new FakeEngine(
log,
new FakePlayer(log),
new FakeScene(log),
connectFailure: failure);
var activator = new FakeActivator(log, engine);
var releaser = new FakeReleaser(
log,
(engine, "Engine"),
(activator.EventHandler, "EventHandler"));
await using var dispatcher = new StaDispatcher(capacity: 1);
await dispatcher.InvokeAsync(
() =>
{
using var session = new DynamicK3dSession(activator, releaser);
var exception = Assert.Throws<COMException>(
() => session.ConnectGuarded(
options,
() => true,
new FakeK3dSdkDispatchLatch()));
Assert.Same(failure, exception);
return true;
},
TimeSpan.FromSeconds(5),
CancellationToken.None);
Assert.Contains("KTAPConnect:1:127.0.0.1:30001:0", log.Names);
Assert.DoesNotContain("Disconnect", log.Names);
Assert.Equal(1, log.Names.Count(name => name == "Release:Engine"));
Assert.Equal(1, log.Names.Count(name => name == "Release:EventHandler"));
}
[Theory]
[InlineData(PlayoutTakeOutScope.All, "StopAll")]
[InlineData(PlayoutTakeOutScope.Layout, "CutOut:10")]
public async Task FakeLateBoundCom_UsesLegacyCallOrderOnOneStaThread(
PlayoutTakeOutScope takeOutScope,
string expectedTakeOutCall)
{
using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
var options = ValidatedPlayoutOptions.Create(TestOptions(scenes.Path));
var cue = options.ResolveCue(new PlayoutCue(
"test-scene.t2s",
"test-scene",
[
new PlayoutField("headline", "safe value", true),
new PlayoutField("badge", null, false)
],
FadeDuration: 12));
var log = new FakeComLog();
var player = new FakePlayer(log);
var scene = new FakeScene(log);
var engine = new FakeEngine(log, player, scene);
var activator = new FakeActivator(log, engine);
await using var dispatcher = new StaDispatcher(capacity: 4);
await dispatcher.InvokeAsync(
() =>
{
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);
session.Disconnect();
return true;
},
TimeSpan.FromSeconds(5),
CancellationToken.None);
Assert.Equal(
new[]
{
$"Create:{K3dComConstants.KaEventHandlerClassGuid:B}",
$"Create:{K3dComConstants.KaEngineClassGuid:B}",
"KTAPConnect:1:127.0.0.1:30001:0",
"GetScenePlayerOnChannel:9",
"LoadScene:test-scene",
"SetOutputChannelIndex:9",
"SetSceneEffectType:1:7:12",
"BeginTransaction",
"GetObject:headline",
"SetValue:headline:safe value",
"SetVisible:headline:1",
"GetObject:badge",
"SetVisible:badge:0",
"EndTransactionOnChannel:9",
"QueryVariables",
"Prepare:10",
"Play:10",
expectedTakeOutCall,
"Disconnect"
},
log.Names);
Assert.Single(log.ThreadIds.Distinct());
Assert.All(log.ApartmentStates, state => Assert.Equal(ApartmentState.STA, state));
}
[Fact]
public async Task Prepare_WithoutOutputChannel_UsesDefaultPlayerAndDoesNotSetSceneChannel()
{
using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
var unchanneledOptions = TestOptions(scenes.Path);
unchanneledOptions.Mode = PlayoutMode.Live;
unchanneledOptions.OutputChannel = null;
var options = ValidatedPlayoutOptions.Create(unchanneledOptions);
var cue = options.ResolveCue(new PlayoutCue(
"test-scene.t2s",
"test-scene",
[],
FadeDuration: 12));
var log = new FakeComLog();
var player = new FakePlayer(log);
var scene = new FakeScene(log);
var engine = new FakeEngine(log, player, scene);
var activator = new FakeActivator(log, engine);
await using var dispatcher = new StaDispatcher(capacity: 2);
await dispatcher.InvokeAsync(
() =>
{
using var session = new DynamicK3dSession(activator);
session.Connect(options);
session.Prepare(cue, options.LayoutIndex);
session.Disconnect();
return true;
},
TimeSpan.FromSeconds(5),
CancellationToken.None);
Assert.Equal(
new[]
{
$"Create:{K3dComConstants.KaEventHandlerClassGuid:B}",
$"Create:{K3dComConstants.KaEngineClassGuid:B}",
"KTAPConnect:1:127.0.0.1:30001:0",
"GetScenePlayer",
"LoadScene:test-scene",
"SetSceneEffectType:1:7:12",
"BeginTransaction",
"EndTransaction",
"QueryVariables",
"Prepare:10",
"Disconnect"
},
log.Names);
Assert.DoesNotContain(log.Names, name => name.StartsWith("SetOutputChannelIndex:", StringComparison.Ordinal));
}
[Fact]
public async Task SceneReplacement_5001To5006_RetainsAllScenesUntilDisconnect()
{
using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
var options = SceneOptions(scenes.Path, "5001", "5006");
var cue5001 = SceneCue(options, "5001");
var cue5006 = SceneCue(options, "5006");
var log = new FakeComLog();
var player = new FakePlayer(log);
var scene5001 = new FakeScene(log, "5001");
var scene5006 = new FakeScene(log, "5006");
var engine = new FakeEngine(log, player, scene5001)
{
SceneResolver = name => name == "5006" ? scene5006 : scene5001
};
var activator = new FakeActivator(log, engine);
var releaser = new FakeReleaser(
log,
(scene5001, "5001"),
(scene5006, "5006"),
(player, "Player"),
(engine, "Engine"),
(activator.EventHandler, "EventHandler"));
await using var dispatcher = new StaDispatcher(capacity: 4);
await dispatcher.InvokeAsync(
() =>
{
using var session = new DynamicK3dSession(activator, releaser);
session.Connect(options);
session.Prepare(cue5001, options.LayoutIndex);
session.Play(options.LayoutIndex);
session.Prepare(cue5006, options.LayoutIndex);
Assert.DoesNotContain("Unload:5001", log.Names);
session.Play(options.LayoutIndex);
session.TakeOut(options.LayoutIndex, PlayoutTakeOutScope.All);
Assert.DoesNotContain(log.Names, name =>
name.StartsWith("Release:500", StringComparison.Ordinal));
session.Disconnect();
return true;
},
TimeSpan.FromSeconds(5),
CancellationToken.None);
Assert.Equal(
new[]
{
"LoadScene:5001",
"Prepare:10",
"Play:10",
"LoadScene:5006",
"Prepare:10",
"Play:10",
"StopAll",
"Disconnect",
"Release:5001",
"Release:5006"
},
SceneLifecycleCalls(log));
}
[Fact]
public async Task Prepare_ReplacingUnplayedScene_RetainsBothScenesUntilDisconnect()
{
using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
var options = SceneOptions(scenes.Path, "5001", "5006");
var log = new FakeComLog();
var player = new FakePlayer(log);
var scene5001 = new FakeScene(log, "5001");
var scene5006 = new FakeScene(log, "5006");
var engine = new FakeEngine(log, player, scene5001)
{
SceneResolver = name => name == "5006" ? scene5006 : scene5001
};
var activator = new FakeActivator(log, engine);
var releaser = new FakeReleaser(
log,
(scene5001, "5001"),
(scene5006, "5006"),
(player, "Player"),
(engine, "Engine"),
(activator.EventHandler, "EventHandler"));
await using var dispatcher = new StaDispatcher(capacity: 2);
await dispatcher.InvokeAsync(
() =>
{
using var session = new DynamicK3dSession(activator, releaser);
session.Connect(options);
session.Prepare(SceneCue(options, "5001"), options.LayoutIndex);
session.Prepare(SceneCue(options, "5006"), options.LayoutIndex);
Assert.DoesNotContain(log.Names, name =>
name.StartsWith("Release:500", StringComparison.Ordinal));
session.Disconnect();
return true;
},
TimeSpan.FromSeconds(5),
CancellationToken.None);
Assert.Equal(
new[]
{
"LoadScene:5001",
"Prepare:10",
"LoadScene:5006",
"Prepare:10",
"Disconnect",
"Release:5006",
"Release:5001"
},
SceneLifecycleCalls(log));
Assert.DoesNotContain("Unload:5006", log.Names);
}
[Fact]
public async Task Prepare_WhenReplacementFails_RetainsFailedAndOnAirScenesUntilDisconnect()
{
using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
var options = SceneOptions(scenes.Path, "5001", "5006");
var originalFailure = new COMException("fake replacement failure");
var log = new FakeComLog();
var player = new FakePlayer(log);
var scene5001 = new FakeScene(log, "5001");
var scene5006 = new FakeScene(
log,
"5006",
queryVariablesFailure: originalFailure);
var engine = new FakeEngine(log, player, scene5001)
{
SceneResolver = name => name == "5006" ? scene5006 : scene5001
};
var activator = new FakeActivator(log, engine);
var releaser = new FakeReleaser(
log,
(scene5001, "5001"),
(scene5006, "5006"),
(player, "Player"),
(engine, "Engine"),
(activator.EventHandler, "EventHandler"));
await using var dispatcher = new StaDispatcher(capacity: 3);
await dispatcher.InvokeAsync(
() =>
{
using var session = new DynamicK3dSession(activator, releaser);
session.Connect(options);
session.Prepare(SceneCue(options, "5001"), options.LayoutIndex);
session.Play(options.LayoutIndex);
var exception = Assert.Throws<COMException>(
() => session.Prepare(SceneCue(options, "5006"), options.LayoutIndex));
Assert.Same(originalFailure, exception);
Assert.DoesNotContain("Unload:5001", log.Names);
Assert.DoesNotContain(log.Names, name =>
name.StartsWith("Release:500", StringComparison.Ordinal));
session.TakeOut(options.LayoutIndex, PlayoutTakeOutScope.All);
Assert.DoesNotContain(log.Names, name =>
name.StartsWith("Release:500", StringComparison.Ordinal));
session.Disconnect();
return true;
},
TimeSpan.FromSeconds(5),
CancellationToken.None);
Assert.Equal(
new[]
{
"LoadScene:5001",
"Prepare:10",
"Play:10",
"LoadScene:5006",
"StopAll",
"Disconnect",
"Release:5006",
"Release:5001"
},
SceneLifecycleCalls(log));
}
[Fact]
public async Task AsyncOutputSequence_DoesNotUnloadAndReleasesScenesOnlyAfterDisconnect()
{
using var scenes = TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
var options = SceneOptions(scenes.Path, "5001", "5006");
var log = new FakeComLog();
var player = new FakePlayer(log);
var scene5001 = new FakeScene(log, "5001");
var scene5006 = new FakeScene(log, "5006");
var engine = new FakeEngine(log, player, scene5001)
{
SceneResolver = name => name == "5006" ? scene5006 : scene5001
};
var activator = new FakeActivator(log, engine);
var releaser = new FakeReleaser(
log,
(scene5001, "5001"),
(scene5006, "5006"),
(player, "Player"),
(engine, "Engine"),
(activator.EventHandler, "EventHandler"));
await using var dispatcher = new StaDispatcher(capacity: 4);
await dispatcher.InvokeAsync(
() =>
{
using var session = new DynamicK3dSession(activator, releaser);
session.Connect(options);
session.Prepare(SceneCue(options, "5001"), options.LayoutIndex);
session.Play(options.LayoutIndex);
session.Prepare(SceneCue(options, "5006"), options.LayoutIndex);
session.Play(options.LayoutIndex);
session.TakeOut(options.LayoutIndex, PlayoutTakeOutScope.All);
Assert.DoesNotContain(log.Names, name =>
name.StartsWith("Release:500", StringComparison.Ordinal));
session.Disconnect();
return true;
},
TimeSpan.FromSeconds(5),
CancellationToken.None);
Assert.Equal(2, log.Names.Count(name => name == "Play:10"));
Assert.Equal(1, log.Names.Count(name => name == "StopAll"));
Assert.DoesNotContain(log.Names, name => name.StartsWith("Unload", StringComparison.Ordinal));
Assert.Equal(1, log.Names.Count(name => name == "Release:5001"));
Assert.Equal(1, log.Names.Count(name => name == "Release:5006"));
}
[Fact]
public void LateBoundActivatorContract_AcceptsOnlyAClsid()
{
var create = typeof(ILateBoundComActivator).GetMethod(nameof(ILateBoundComActivator.Create));
Assert.NotNull(create);
Assert.Equal(typeof(Guid), Assert.Single(create.GetParameters()).ParameterType);
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_AfterUnknownPlayOutcome_ReleasesReferencesWithoutSdkCleanup()
{
using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
var options = ValidatedPlayoutOptions.Create(TestOptions(scenes.Path));
var cue = SceneCue(options, "test-scene");
var playFailure = new COMException("fake ambiguous play failure");
var log = new FakeComLog();
var player = new FakePlayer(log, playFailure);
var scene = new FakeScene(log);
var engine = new FakeEngine(log, player, scene);
var activator = new FakeActivator(log, engine);
var releaser = new FakeReleaser(
log,
(scene, "Scene"),
(player, "Player"),
(engine, "Engine"),
(activator.EventHandler, "EventHandler"));
await using var dispatcher = new StaDispatcher(capacity: 1);
await dispatcher.InvokeAsync(
() =>
{
using var session = new DynamicK3dSession(activator, releaser);
session.Connect(options);
session.Prepare(cue, options.LayoutIndex);
var exception = Assert.Throws<COMException>(
() => session.Play(options.LayoutIndex));
Assert.Same(playFailure, exception);
session.Abandon();
return true;
},
TimeSpan.FromSeconds(5),
CancellationToken.None);
Assert.DoesNotContain("Disconnect", log.Names);
Assert.DoesNotContain(log.Names, name => name.StartsWith("Unload", StringComparison.Ordinal));
Assert.Equal(1, log.Names.Count(name => name == "Play:10"));
Assert.Equal(1, log.Names.Count(name => name == "Release:Scene"));
Assert.Equal(1, log.Names.Count(name => name == "Release:Player"));
Assert.Equal(1, log.Names.Count(name => name == "Release:Engine"));
Assert.Equal(1, log.Names.Count(name => name == "Release:EventHandler"));
}
private static PlayoutOptions TestOptions(string sceneDirectory) => new()
{
Mode = PlayoutMode.Test,
SceneDirectory = sceneDirectory,
OutputChannel = 9,
TestProcessWindowTitlePattern = "^Tornado2 TEST$",
TestSceneAllowlist = ["test-scene"]
};
private static ValidatedPlayoutOptions SceneOptions(
string sceneDirectory,
params string[] sceneNames)
{
var options = TestOptions(sceneDirectory);
options.TestSceneAllowlist = [.. sceneNames];
return ValidatedPlayoutOptions.Create(options);
}
private static PlayoutCue SceneCue(ValidatedPlayoutOptions options, string sceneName) =>
options.ResolveCue(new PlayoutCue(
$"{sceneName}.t2s",
sceneName,
[],
FadeDuration: 0));
private static string[] SceneLifecycleCalls(FakeComLog log) =>
log.Names.Where(name =>
name.StartsWith("LoadScene:", StringComparison.Ordinal) ||
name == "Prepare:10" ||
name == "Play:10" ||
name == "StopAll" ||
name == "CutOut:10" ||
name == "Disconnect" ||
name == "RollbackTransaction" ||
name.StartsWith("Unload:", StringComparison.Ordinal) ||
name.StartsWith("Release:500", StringComparison.Ordinal)).ToArray();
public sealed class FakeComLog
{
private readonly ConcurrentQueue<string> _names = new();
private readonly ConcurrentQueue<int> _threadIds = new();
private readonly ConcurrentQueue<ApartmentState> _apartmentStates = new();
public string[] Names => _names.ToArray();
public int[] ThreadIds => _threadIds.ToArray();
public ApartmentState[] ApartmentStates => _apartmentStates.ToArray();
public void Add(string name)
{
_names.Enqueue(name);
_threadIds.Enqueue(Environment.CurrentManagedThreadId);
_apartmentStates.Enqueue(Thread.CurrentThread.GetApartmentState());
}
}
private sealed class FakeActivator : ILateBoundComActivator
{
private readonly FakeComLog _log;
private readonly FakeEngine _engine;
private readonly object _eventHandler = new();
public FakeActivator(FakeComLog log, FakeEngine engine)
{
_log = log;
_engine = engine;
}
public object EventHandler => _eventHandler;
public object Create(Guid classId)
{
_log.Add($"Create:{classId:B}");
return classId switch
{
var value when value == K3dComConstants.KaEventHandlerClassGuid => _eventHandler,
var value when value == K3dComConstants.KaEngineClassGuid => _engine,
_ => throw new InvalidOperationException("Unexpected fake CLSID.")
};
}
}
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;
private readonly FakePlayer _player;
private readonly FakeScene _scene;
private readonly int _connectResult;
private readonly Exception? _playerFailure;
private readonly Exception? _disconnectFailure;
private readonly Exception? _connectFailure;
public FakeEngine(
FakeComLog log,
FakePlayer player,
FakeScene scene,
int connectResult = 1,
Exception? playerFailure = null,
Exception? disconnectFailure = null,
Exception? connectFailure = null)
{
_log = log;
_player = player;
_scene = scene;
_connectResult = connectResult;
_playerFailure = playerFailure;
_disconnectFailure = disconnectFailure;
_connectFailure = connectFailure;
}
public Func<string, FakeScene>? SceneResolver { get; init; }
public int KTAPConnect(
int tcpMode,
string host,
int hostPort,
int clientPort,
object eventHandler)
{
Assert.NotNull(eventHandler);
_log.Add($"KTAPConnect:{tcpMode}:{host}:{hostPort}:{clientPort}");
if (_connectFailure is not null)
{
throw _connectFailure;
}
return _connectResult;
}
public object GetScenePlayerOnChannel(int channel)
{
_log.Add($"GetScenePlayerOnChannel:{channel}");
if (_playerFailure is not null)
{
throw _playerFailure;
}
return _player;
}
public object GetScenePlayer()
{
_log.Add("GetScenePlayer");
if (_playerFailure is not null)
{
throw _playerFailure;
}
return _player;
}
public object LoadScene(string sceneFile, string sceneName)
{
Assert.True(System.IO.Path.IsPathFullyQualified(sceneFile));
_log.Add($"LoadScene:{sceneName}");
return SceneResolver?.Invoke(sceneName) ?? _scene;
}
public void BeginTransaction() => _log.Add("BeginTransaction");
public void EndTransaction() => _log.Add("EndTransaction");
public void EndTransactionOnChannel(int channel) =>
_log.Add($"EndTransactionOnChannel:{channel}");
public void RollbackTransaction() => _log.Add("RollbackTransaction");
public void Disconnect()
{
_log.Add("Disconnect");
if (_disconnectFailure is not null)
{
throw _disconnectFailure;
}
}
}
private sealed class FakeReleaser : ILateBoundComObjectReleaser
{
private readonly FakeComLog _log;
private readonly (object Value, string Name)[] _objects;
public FakeReleaser(
FakeComLog log,
params (object Value, string Name)[] objects)
{
_log = log;
_objects = objects;
}
public void Release(object value)
{
var match = Assert.Single(_objects, item => ReferenceEquals(item.Value, value));
_log.Add($"Release:{match.Name}");
}
}
public sealed class FakePlayer
{
private readonly FakeComLog _log;
private readonly Exception? _playFailure;
public FakePlayer(FakeComLog log, Exception? playFailure = null)
{
_log = log;
_playFailure = playFailure;
}
public void Prepare(int layoutIndex, object scene)
{
Assert.NotNull(scene);
_log.Add($"Prepare:{layoutIndex}");
}
public void Play(int layoutIndex)
{
_log.Add($"Play:{layoutIndex}");
if (_playFailure is not null)
{
throw _playFailure;
}
}
public void StopAll() => _log.Add("StopAll");
public void CutOut(int layoutIndex) => _log.Add($"CutOut:{layoutIndex}");
}
public sealed class FakeScene
{
private readonly FakeComLog _log;
private readonly string? _name;
private readonly Exception? _queryVariablesFailure;
private readonly Exception? _unloadFailure;
public FakeScene(
FakeComLog log,
string? name = null,
Exception? queryVariablesFailure = null,
Exception? unloadFailure = null)
{
_log = log;
_name = name;
_queryVariablesFailure = queryVariablesFailure;
_unloadFailure = unloadFailure;
}
public void SetOutputChannelIndex(int channel) =>
_log.Add($"SetOutputChannelIndex:{channel}");
public void SetSceneEffectType(int inEffect, int effectType, int duration) =>
_log.Add($"SetSceneEffectType:{inEffect}:{effectType}:{duration}");
public object GetObject(string objectName)
{
_log.Add($"GetObject:{objectName}");
return new FakeSceneObject(_log, objectName);
}
public void QueryVariables()
{
_log.Add("QueryVariables");
if (_queryVariablesFailure is not null)
{
throw _queryVariablesFailure;
}
}
public void Unload()
{
_log.Add(_name is null ? "Unload" : $"Unload:{_name}");
if (_unloadFailure is not null)
{
throw _unloadFailure;
}
}
}
public sealed class FakeSceneObject
{
private readonly FakeComLog _log;
private readonly string _name;
public FakeSceneObject(FakeComLog log, string name)
{
_log = log;
_name = name;
}
public void SetValue(string value) => _log.Add($"SetValue:{_name}:{value}");
public void SetVisible(int visible) => _log.Add($"SetVisible:{_name}:{visible}");
}
}