feat: add audited one-shot prepare gate

This commit is contained in:
2026-07-18 12:29:09 +09:00
parent 1302b1b92f
commit 5379511b8a
25 changed files with 6188 additions and 41 deletions

View File

@@ -4,6 +4,7 @@ using MBN_STOCK_WEBVIEW.Playout.Configuration;
using MBN_STOCK_WEBVIEW.Playout.Diagnostics;
using MBN_STOCK_WEBVIEW.Playout.Interop;
using MBN_STOCK_WEBVIEW.Playout.Runtime;
using MBN_STOCK_WEBVIEW.Playout.Safety;
namespace MBN_STOCK_WEBVIEW.Playout.Tests;
@@ -1678,6 +1679,128 @@ public sealed class DynamicK3dSessionTests
Assert.Equal(1, log.Names.Count(name => name == "Release:EventHandler"));
}
[Theory]
[InlineData("mutation")]
[InlineData("query")]
[InlineData("end")]
public async Task GateA_PrepareStageFailureNeverRollsBackDisconnectsOrRetries(
string failureStage)
{
const string capability =
"0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF";
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
var rawOptions = new PlayoutOptions
{
Mode = PlayoutMode.Live,
Host = "127.0.0.1",
Port = 30001,
TcpMode = 1,
LayoutIndex = 10,
SceneDirectory = scenes.Path,
TestSceneAllowlist = ["5001"],
TrustedLiveOutputEnabled = true,
ReconnectEnabled = false,
MaximumReconnectAttempts = 0,
MaximumAutomaticRefreshesPerTakeIn = 0,
LegacySceneFadeDuration = 6,
ConnectTimeoutMilliseconds = 500,
OperationTimeoutMilliseconds = 500,
DisconnectTimeoutMilliseconds = 500,
ProcessPollIntervalMilliseconds = 100
};
var options = ValidatedPlayoutOptions.Create(rawOptions);
var failure = new COMException($"fake Gate A {failureStage} failure");
var log = new FakeComLog();
var scene = new FakeScene(log, "5001");
var player = new FakePlayer(log);
var vendorEngine = new FakeEngine(log, player, scene);
switch (failureStage)
{
case "mutation":
scene.GetObjectFailure = failure;
break;
case "query":
scene.QueryVariablesFailure = failure;
break;
case "end":
vendorEngine.EndTransactionFailure = failure;
break;
default:
throw new InvalidOperationException("Unknown test failure stage.");
}
var activator = new FakeActivator(log, vendorEngine);
var releaser = new FakeReleaser(
log,
(scene, "5001"),
(player, "Player"),
(vendorEngine, "Engine"),
(activator.EventHandler, "EventHandler"));
var sessionFactory = new DelegateK3dSessionFactory(() =>
new DynamicK3dSession(
activator,
releaser,
new InstalledK3dInteropMethodInvoker(),
new ActivatorK3dEventHandlerFactory(activator),
rollbackOnPrepareFailure: false));
using var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(
capability);
var dispatcher = new StaDispatcher(options.QueueCapacity);
var engine = new TornadoPlayoutEngine(
options,
sessionFactory,
new FakeRegistrationProbe(),
new FakeTornadoProcessProbe(
ProcessSnapshot("approved-pgm"),
ProcessSnapshot("approved-pgm")),
dispatcher,
authorization,
TimeProvider.System,
startMonitor: false,
finalConnectSafetyCheck: () => true,
abandonOnTargetMismatch: true,
beforeSdkDispatchClaim: authorization.EnsureGateASdkDispatchAuthorized,
launchAuthorization: authorization);
try
{
Assert.True((await engine.ConnectAsync(CancellationToken.None)).IsSuccess);
Assert.True(authorization.TryClaimGateAPrepare(capability));
var cue = new PlayoutCue(
"5001.t2s",
"5001",
[new PlayoutField("headline", "test", true)],
FadeDuration: 6,
Mutations: [new PlayoutUseBackground(false)]);
var result = await engine.PrepareAsync(cue, CancellationToken.None);
var retry = await engine.PrepareAsync(cue, CancellationToken.None);
Assert.Equal(PlayoutResultCode.OutcomeUnknown, result.Code);
Assert.Equal(PlayoutResultCode.OutcomeUnknown, retry.Code);
}
finally
{
authorization.CompleteGateAPrepare();
await engine.DisposeAsync();
}
Assert.Equal(1, log.Names.Count(name => name == "BeginTransaction"));
Assert.DoesNotContain("RollbackTransaction", log.Names);
Assert.DoesNotContain("Disconnect", log.Names);
Assert.DoesNotContain("Prepare:10", log.Names);
Assert.Equal(1, log.Names.Count(name => name == "Release:5001"));
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 TornadoProcessSnapshot ProcessSnapshot(string generation) =>
new(1, 1, 0)
{
EligibleProcessGeneration = new TornadoProcessGeneration(generation)
};
private static PlayoutOptions TestOptions(string sceneDirectory) => new()
{
Mode = PlayoutMode.Test,
@@ -1715,6 +1838,12 @@ public sealed class DynamicK3dSessionTests
name.StartsWith("Unload:", StringComparison.Ordinal) ||
name.StartsWith("Release:500", StringComparison.Ordinal)).ToArray();
private sealed class DelegateK3dSessionFactory(Func<IK3dSession> create)
: IK3dSessionFactory
{
public IK3dSession Create() => create();
}
public sealed class FakeComLog
{
private readonly ConcurrentQueue<string> _names = new();
@@ -1865,6 +1994,8 @@ public sealed class DynamicK3dSessionTests
public Func<string, FakeScene>? SceneResolver { get; init; }
public Exception? EndTransactionFailure { get; set; }
public int KTAPConnect(
int tcpMode,
string host,
@@ -1913,10 +2044,23 @@ public sealed class DynamicK3dSessionTests
public void BeginTransaction() => _log.Add("BeginTransaction");
public void EndTransaction() => _log.Add("EndTransaction");
public void EndTransaction()
{
_log.Add("EndTransaction");
if (EndTransactionFailure is not null)
{
throw EndTransactionFailure;
}
}
public void EndTransactionOnChannel(int channel) =>
public void EndTransactionOnChannel(int channel)
{
_log.Add($"EndTransactionOnChannel:{channel}");
if (EndTransactionFailure is not null)
{
throw EndTransactionFailure;
}
}
public void RollbackTransaction() => _log.Add("RollbackTransaction");
@@ -2026,6 +2170,11 @@ public sealed class DynamicK3dSessionTests
public object GetObject(string objectName)
{
_log.Add($"GetObject:{objectName}");
if (GetObjectFailure is not null)
{
throw GetObjectFailure;
}
return new FakeSceneObject(_log, objectName);
}
@@ -2044,6 +2193,8 @@ public sealed class DynamicK3dSessionTests
set => _queryVariablesFailure = value;
}
public Exception? GetObjectFailure { get; set; }
public void Unload()
{
_log.Add(_name is null ? "Unload" : $"Unload:{_name}");