feat: verify Tornado PGM KTAP connection
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
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;
|
||||
@@ -233,15 +234,16 @@ public sealed class DynamicK3dSessionTests
|
||||
"KTAPConnect:1:127.0.0.1:30001:0",
|
||||
"GetScenePlayerOnChannel:9",
|
||||
"LoadScene:test-scene",
|
||||
"SetSceneEffectType:10:7:12",
|
||||
"SetOutputChannelIndex:9",
|
||||
"SetSceneEffectType:1:7:12",
|
||||
"BeginTransaction",
|
||||
"GetObject:headline",
|
||||
"SetValue:headline:safe value",
|
||||
"SetVisible:headline:1",
|
||||
"GetObject:badge",
|
||||
"SetVisible:badge:0",
|
||||
"EndTransactionOnChannel:9",
|
||||
"QueryVariables",
|
||||
"EndTransaction",
|
||||
"Prepare:10",
|
||||
"Play:10",
|
||||
expectedTakeOutCall,
|
||||
@@ -252,6 +254,285 @@ public sealed class DynamicK3dSessionTests
|
||||
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()
|
||||
{
|
||||
@@ -345,17 +626,20 @@ public sealed class DynamicK3dSessionTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Abandon_ReleasesEveryComReferenceWithoutDisconnect()
|
||||
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);
|
||||
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"));
|
||||
@@ -366,6 +650,10 @@ public sealed class DynamicK3dSessionTests
|
||||
{
|
||||
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;
|
||||
},
|
||||
@@ -373,6 +661,9 @@ public sealed class DynamicK3dSessionTests
|
||||
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"));
|
||||
@@ -387,6 +678,34 @@ public sealed class DynamicK3dSessionTests
|
||||
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();
|
||||
@@ -516,6 +835,8 @@ public sealed class DynamicK3dSessionTests
|
||||
_connectFailure = connectFailure;
|
||||
}
|
||||
|
||||
public Func<string, FakeScene>? SceneResolver { get; init; }
|
||||
|
||||
public int KTAPConnect(
|
||||
int tcpMode,
|
||||
string host,
|
||||
@@ -544,17 +865,33 @@ public sealed class DynamicK3dSessionTests
|
||||
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 _scene;
|
||||
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");
|
||||
@@ -588,10 +925,12 @@ public sealed class DynamicK3dSessionTests
|
||||
public sealed class FakePlayer
|
||||
{
|
||||
private readonly FakeComLog _log;
|
||||
private readonly Exception? _playFailure;
|
||||
|
||||
public FakePlayer(FakeComLog log)
|
||||
public FakePlayer(FakeComLog log, Exception? playFailure = null)
|
||||
{
|
||||
_log = log;
|
||||
_playFailure = playFailure;
|
||||
}
|
||||
|
||||
public void Prepare(int layoutIndex, object scene)
|
||||
@@ -600,7 +939,14 @@ public sealed class DynamicK3dSessionTests
|
||||
_log.Add($"Prepare:{layoutIndex}");
|
||||
}
|
||||
|
||||
public void Play(int layoutIndex) => _log.Add($"Play:{layoutIndex}");
|
||||
public void Play(int layoutIndex)
|
||||
{
|
||||
_log.Add($"Play:{layoutIndex}");
|
||||
if (_playFailure is not null)
|
||||
{
|
||||
throw _playFailure;
|
||||
}
|
||||
}
|
||||
|
||||
public void StopAll() => _log.Add("StopAll");
|
||||
|
||||
@@ -610,14 +956,27 @@ public sealed class DynamicK3dSessionTests
|
||||
public sealed class FakeScene
|
||||
{
|
||||
private readonly FakeComLog _log;
|
||||
private readonly string? _name;
|
||||
private readonly Exception? _queryVariablesFailure;
|
||||
private readonly Exception? _unloadFailure;
|
||||
|
||||
public FakeScene(FakeComLog log)
|
||||
public FakeScene(
|
||||
FakeComLog log,
|
||||
string? name = null,
|
||||
Exception? queryVariablesFailure = null,
|
||||
Exception? unloadFailure = null)
|
||||
{
|
||||
_log = log;
|
||||
_name = name;
|
||||
_queryVariablesFailure = queryVariablesFailure;
|
||||
_unloadFailure = unloadFailure;
|
||||
}
|
||||
|
||||
public void SetSceneEffectType(int layoutIndex, int effectType, int duration) =>
|
||||
_log.Add($"SetSceneEffectType:{layoutIndex}:{effectType}:{duration}");
|
||||
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)
|
||||
{
|
||||
@@ -625,7 +984,23 @@ public sealed class DynamicK3dSessionTests
|
||||
return new FakeSceneObject(_log, objectName);
|
||||
}
|
||||
|
||||
public void QueryVariables() => _log.Add("QueryVariables");
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user