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( () => 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 _names = new(); private readonly ConcurrentQueue _threadIds = new(); private readonly ConcurrentQueue _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}"); } }