fix: await playout callbacks before refresh

This commit is contained in:
2026-07-11 05:03:31 +09:00
parent d8b9641a75
commit 3e3292b8a9
6 changed files with 358 additions and 32 deletions

View File

@@ -681,6 +681,144 @@ public sealed class DynamicK3dSessionTests
Assert.Equal(1, log.Names.Count(name => name == "Unload:5006"));
}
[Fact]
public async Task LifecycleCallbacks_SameNameReplacement_UnloadsOnlyAfterStopAll()
{
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
var options = SceneOptions(scenes.Path, "5001");
var log = new FakeComLog();
var player = new FakePlayer(log);
var retiredScene = new FakeScene(log, "5001");
var activeScene = new FakeScene(log, "5001");
var scenesToLoad = new Queue<FakeScene>([retiredScene, activeScene]);
var engine = new FakeEngine(log, player, retiredScene)
{
SceneResolver = _ => scenesToLoad.Dequeue()
};
var activator = new FakeActivator(log, engine);
var callbacks = new RecordingEventHandlerFactory();
var releaser = new FakeReleaser(
log,
(retiredScene, "5001-retired"),
(activeScene, "5001-active"),
(player, "Player"),
(engine, "Engine"),
(callbacks.Handler, "EventHandler"));
await using var dispatcher = new StaDispatcher(capacity: 3);
await dispatcher.InvokeAsync(
() =>
{
using var session = new DynamicK3dSession(
activator,
releaser,
new InstalledK3dInteropMethodInvoker(),
callbacks);
session.Connect(options);
session.Prepare(SceneCue(options, "5001"), options.LayoutIndex);
session.Play(options.LayoutIndex);
callbacks.Fire("OnScenePlayed", 1, 9, options.LayoutIndex);
Assert.Equal(
0,
session.ProcessPendingCallbacks(options.LayoutIndex).UnloadedSceneCount);
session.Prepare(SceneCue(options, "5001"), options.LayoutIndex);
session.Play(options.LayoutIndex);
callbacks.Fire("OnScenePlayed", 1, 9, options.LayoutIndex);
Assert.Equal(
0,
session.ProcessPendingCallbacks(options.LayoutIndex).UnloadedSceneCount);
Assert.DoesNotContain("Unload:5001", log.Names);
Assert.Contains("Release:5001-retired", log.Names);
Assert.DoesNotContain("Release:5001-active", log.Names);
session.TakeOut(options.LayoutIndex, PlayoutTakeOutScope.All);
callbacks.Fire("OnStopAll", 1);
Assert.Equal(
1,
session.ProcessPendingCallbacks(options.LayoutIndex).UnloadedSceneCount);
Assert.Equal(1, log.Names.Count(name => name == "Unload:5001"));
Assert.Contains("Release:5001-active", log.Names);
session.Disconnect();
return true;
},
TimeSpan.FromSeconds(5),
CancellationToken.None);
Assert.Empty(scenesToLoad);
Assert.Equal(1, log.Names.Count(name => name == "Unload:5001"));
Assert.Equal(1, log.Names.Count(name => name == "Release:5001-retired"));
Assert.Equal(1, log.Names.Count(name => name == "Release:5001-active"));
}
[Fact]
public async Task LifecycleCallbacks_SameNameReplacement_IgnoresSceneNameCasing()
{
using var scenes = TemporarySceneDirectory.Create("SceneA.t2s");
var options = SceneOptions(scenes.Path, "SceneA");
var log = new FakeComLog();
var player = new FakePlayer(log);
var retiredScene = new FakeScene(log, "SceneA");
var activeScene = new FakeScene(log, "scenea");
var scenesToLoad = new Queue<FakeScene>([retiredScene, activeScene]);
var engine = new FakeEngine(log, player, retiredScene)
{
SceneResolver = _ => scenesToLoad.Dequeue()
};
var activator = new FakeActivator(log, engine);
var callbacks = new RecordingEventHandlerFactory();
var releaser = new FakeReleaser(
log,
(retiredScene, "SceneA-retired"),
(activeScene, "scenea-active"),
(player, "Player"),
(engine, "Engine"),
(callbacks.Handler, "EventHandler"));
await using var dispatcher = new StaDispatcher(capacity: 3);
await dispatcher.InvokeAsync(
() =>
{
using var session = new DynamicK3dSession(
activator,
releaser,
new InstalledK3dInteropMethodInvoker(),
callbacks);
session.Connect(options);
session.Prepare(SceneCue(options, "SceneA"), options.LayoutIndex);
session.Play(options.LayoutIndex);
callbacks.Fire("OnScenePlayed", 1, 9, options.LayoutIndex);
session.ProcessPendingCallbacks(options.LayoutIndex);
session.Prepare(SceneCue(options, "scenea"), options.LayoutIndex);
session.Play(options.LayoutIndex);
callbacks.Fire("OnScenePlayed", 1, 9, options.LayoutIndex);
Assert.Equal(
0,
session.ProcessPendingCallbacks(options.LayoutIndex).UnloadedSceneCount);
Assert.DoesNotContain("Unload:SceneA", log.Names);
Assert.Contains("Release:SceneA-retired", log.Names);
session.TakeOut(options.LayoutIndex, PlayoutTakeOutScope.All);
callbacks.Fire("OnStopAll", 1);
Assert.Equal(
1,
session.ProcessPendingCallbacks(options.LayoutIndex).UnloadedSceneCount);
session.Disconnect();
return true;
},
TimeSpan.FromSeconds(5),
CancellationToken.None);
Assert.Empty(scenesToLoad);
Assert.DoesNotContain("Unload:SceneA", log.Names);
Assert.Equal(1, log.Names.Count(name => name == "Unload:scenea"));
Assert.Equal(1, log.Names.Count(name => name == "Release:SceneA-retired"));
Assert.Equal(1, log.Names.Count(name => name == "Release:scenea-active"));
}
[Fact]
public async Task StopAllSuccess_ClearsInterruptedPlayBeforeDisconnect()
{

View File

@@ -1295,12 +1295,16 @@ public sealed class TornadoPlayoutEngineTests
Assert.Null(engine.Status.OnAirSceneName);
}
[Fact]
public async Task Status_ExposesValidatedOperationTimeoutMilliseconds()
[Theory]
[InlineData(100)]
[InlineData(837)]
[InlineData(300_000)]
public async Task Status_ExposesValidatedOperationTimeoutMilliseconds(
int operationTimeoutMilliseconds)
{
using var scenes = TemporarySceneDirectory.Create("test-scene.t2s");
var options = TestOptions(scenes.Path, "test-scene");
options.OperationTimeoutMilliseconds = 837;
options.OperationTimeoutMilliseconds = operationTimeoutMilliseconds;
await using var engine = CreateEngine(
options,
new FakeK3dSessionFactory(),
@@ -1308,7 +1312,7 @@ public sealed class TornadoPlayoutEngineTests
new FakeTornadoProcessProbe(ProcessSnapshot("eligible-generation-a")),
liveAuthorized: false);
Assert.Equal(837, engine.Status.OperationTimeoutMilliseconds);
Assert.Equal(operationTimeoutMilliseconds, engine.Status.OperationTimeoutMilliseconds);
}
[Fact]
@@ -2186,6 +2190,64 @@ public sealed class TornadoPlayoutEngineTests
Assert.Equal(1, session.Calls.Count(call => call == "ProcessPendingCallbacks"));
}
[Fact]
public async Task PlayCompletionPending_StatusClearsOnlyAfterCallbackDrain()
{
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
var callbackReady = false;
var session = new FakeK3dSession
{
SupportsLifecycleCallbacks = true,
ClearPendingLifecycleCallbacksAfterProcess = false
};
session.PlayAction = _ =>
{
session.HasPendingPlayCallbacks = true;
session.HasPendingLifecycleCallbacks = true;
};
session.ProcessPendingCallbacksAction = _ =>
{
if (!callbackReady)
{
return new K3dCallbackDrainResult(0, 0, false);
}
session.HasPendingPlayCallbacks = false;
session.HasPendingLifecycleCallbacks = false;
return new K3dCallbackDrainResult(1, 1, false);
};
await using var engine = CreateEngine(
TestOptions(scenes.Path, "5001"),
new FakeK3dSessionFactory(() => session),
new FakeRegistrationProbe(),
new FakeTornadoProcessProbe(new TornadoProcessSnapshot(1, 1, 0)),
liveAuthorized: false);
Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
Assert.True((await engine.PrepareAsync(
Cue("5001"),
CancellationToken.None)).IsSuccess);
var observedPendingStates = new List<bool>();
engine.StatusChanged += (_, args) =>
observedPendingStates.Add(args.Current.IsPlayCompletionPending);
Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);
await engine.PollProcessOnceAsync(CancellationToken.None);
Assert.True(engine.Status.IsPlayCompletionPending);
Assert.Equal([true, true], observedPendingStates);
callbackReady = true;
await engine.PollProcessOnceAsync(CancellationToken.None);
Assert.False(engine.Status.IsPlayCompletionPending);
Assert.Equal([true, true, false], observedPendingStates);
Assert.True((await engine.TakeOutAsync(
PlayoutTakeOutScope.All,
CancellationToken.None)).IsSuccess);
Assert.True((await engine.DisconnectAsync(CancellationToken.None)).IsSuccess);
}
[Fact]
public async Task PendingPlayCallback_BlocksDelayedMultiNextUntilEachCompletionIsDrained()
{
@@ -2229,6 +2291,7 @@ public sealed class TornadoPlayoutEngineTests
CancellationToken.None)).IsSuccess);
Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);
Assert.True(session.HasPendingPlayCallbacks);
Assert.True(engine.Status.IsPlayCompletionPending);
var firstEarlyNext = await engine.NextAsync(
Cue("5006"),
@@ -2261,6 +2324,7 @@ public sealed class TornadoPlayoutEngineTests
callbackReady = true;
await engine.PollProcessOnceAsync(CancellationToken.None);
Assert.False(session.HasPendingPlayCallbacks);
Assert.False(engine.Status.IsPlayCompletionPending);
Assert.True((await engine.TakeOutAsync(
PlayoutTakeOutScope.All,
CancellationToken.None)).IsSuccess);
@@ -2373,6 +2437,7 @@ public sealed class TornadoPlayoutEngineTests
CancellationToken.None)).IsSuccess);
Assert.True((await engine.TakeInAsync(CancellationToken.None)).IsSuccess);
Assert.True(session.HasPendingPlayCallbacks);
Assert.True(engine.Status.IsPlayCompletionPending);
var result = await engine.TakeOutAsync(
PlayoutTakeOutScope.All,
@@ -2380,6 +2445,7 @@ public sealed class TornadoPlayoutEngineTests
Assert.True(result.IsSuccess);
Assert.Contains("TakeOut:All", session.Calls);
Assert.True(engine.Status.IsPlayCompletionPending);
}
[Theory]