feat: add safe Tornado K3D playout adapter
This commit is contained in:
280
tests/MBN_STOCK_WEBVIEW.Playout.Tests/DynamicK3dSessionTests.cs
Normal file
280
tests/MBN_STOCK_WEBVIEW.Playout.Tests/DynamicK3dSessionTests.cs
Normal file
@@ -0,0 +1,280 @@
|
||||
using System.Collections.Concurrent;
|
||||
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);
|
||||
await using var dispatcher = new StaDispatcher(capacity: 1);
|
||||
|
||||
var exception = await Assert.ThrowsAnyAsync<Exception>(
|
||||
() => dispatcher.InvokeAsync(
|
||||
() =>
|
||||
{
|
||||
using var session = new DynamicK3dSession(activator);
|
||||
session.Connect(options);
|
||||
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);
|
||||
}
|
||||
|
||||
[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);
|
||||
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",
|
||||
"SetSceneEffectType:10:7:12",
|
||||
"BeginTransaction",
|
||||
"GetObject:headline",
|
||||
"SetValue:headline:safe value",
|
||||
"SetVisible:headline:1",
|
||||
"GetObject:badge",
|
||||
"SetVisible:badge:0",
|
||||
"QueryVariables",
|
||||
"EndTransaction",
|
||||
"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 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);
|
||||
}
|
||||
|
||||
private static PlayoutOptions TestOptions(string sceneDirectory) => new()
|
||||
{
|
||||
Mode = PlayoutMode.Test,
|
||||
SceneDirectory = sceneDirectory,
|
||||
OutputChannel = 9,
|
||||
TestProcessWindowTitlePattern = "^Tornado2 TEST$",
|
||||
TestSceneAllowlist = ["test-scene"]
|
||||
};
|
||||
|
||||
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 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.")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class FakeEngine
|
||||
{
|
||||
private readonly FakeComLog _log;
|
||||
private readonly FakePlayer _player;
|
||||
private readonly FakeScene _scene;
|
||||
private readonly int _connectResult;
|
||||
|
||||
public FakeEngine(
|
||||
FakeComLog log,
|
||||
FakePlayer player,
|
||||
FakeScene scene,
|
||||
int connectResult = 1)
|
||||
{
|
||||
_log = log;
|
||||
_player = player;
|
||||
_scene = scene;
|
||||
_connectResult = connectResult;
|
||||
}
|
||||
|
||||
public int KTAPConnect(
|
||||
int tcpMode,
|
||||
string host,
|
||||
int hostPort,
|
||||
int clientPort,
|
||||
object eventHandler)
|
||||
{
|
||||
Assert.NotNull(eventHandler);
|
||||
_log.Add($"KTAPConnect:{tcpMode}:{host}:{hostPort}:{clientPort}");
|
||||
return _connectResult;
|
||||
}
|
||||
|
||||
public object GetScenePlayerOnChannel(int channel)
|
||||
{
|
||||
_log.Add($"GetScenePlayerOnChannel:{channel}");
|
||||
return _player;
|
||||
}
|
||||
|
||||
public object LoadScene(string sceneFile, string sceneName)
|
||||
{
|
||||
Assert.True(System.IO.Path.IsPathFullyQualified(sceneFile));
|
||||
_log.Add($"LoadScene:{sceneName}");
|
||||
return _scene;
|
||||
}
|
||||
|
||||
public void BeginTransaction() => _log.Add("BeginTransaction");
|
||||
|
||||
public void EndTransaction() => _log.Add("EndTransaction");
|
||||
|
||||
public void Disconnect() => _log.Add("Disconnect");
|
||||
}
|
||||
|
||||
public sealed class FakePlayer
|
||||
{
|
||||
private readonly FakeComLog _log;
|
||||
|
||||
public FakePlayer(FakeComLog log)
|
||||
{
|
||||
_log = log;
|
||||
}
|
||||
|
||||
public void Prepare(int layoutIndex, object scene)
|
||||
{
|
||||
Assert.NotNull(scene);
|
||||
_log.Add($"Prepare:{layoutIndex}");
|
||||
}
|
||||
|
||||
public void Play(int layoutIndex) => _log.Add($"Play:{layoutIndex}");
|
||||
|
||||
public void StopAll() => _log.Add("StopAll");
|
||||
|
||||
public void CutOut(int layoutIndex) => _log.Add($"CutOut:{layoutIndex}");
|
||||
}
|
||||
|
||||
public sealed class FakeScene
|
||||
{
|
||||
private readonly FakeComLog _log;
|
||||
|
||||
public FakeScene(FakeComLog log)
|
||||
{
|
||||
_log = log;
|
||||
}
|
||||
|
||||
public void SetSceneEffectType(int layoutIndex, int effectType, int duration) =>
|
||||
_log.Add($"SetSceneEffectType:{layoutIndex}:{effectType}:{duration}");
|
||||
|
||||
public object GetObject(string objectName)
|
||||
{
|
||||
_log.Add($"GetObject:{objectName}");
|
||||
return new FakeSceneObject(_log, objectName);
|
||||
}
|
||||
|
||||
public void QueryVariables() => _log.Add("QueryVariables");
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user