603 lines
22 KiB
C#
603 lines
22 KiB
C#
using System.Collections;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using MBN_STOCK_WEBVIEW.Core.Playout;
|
|
using MBN_STOCK_WEBVIEW.Playout.Configuration;
|
|
using MBN_STOCK_WEBVIEW.PlayoutSmoke;
|
|
|
|
namespace MBN_STOCK_WEBVIEW.Playout.Tests;
|
|
|
|
public sealed class IsolatedTestCommandTests : IDisposable
|
|
{
|
|
private readonly TemporarySceneDirectory _scenes =
|
|
TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");
|
|
|
|
[Fact]
|
|
public void Preflight_ValidSequenceBuildsExactBasenameCuesAndDisablesReconnect()
|
|
{
|
|
using var environment = ClearedPlayoutEnvironment();
|
|
using var config = CreateConfig(ValidOptions());
|
|
|
|
var plan = IsolatedTestCommandPreflight.Create(Invocation(
|
|
SmokeCommandKind.TestSequence,
|
|
config.Path));
|
|
|
|
Assert.Equal("5001.t2s", plan.PrepareCue!.SceneFile);
|
|
Assert.Equal("5001", plan.PrepareCue.SceneName);
|
|
Assert.Equal("5006.t2s", plan.NextCue!.SceneFile);
|
|
Assert.Equal("5006", plan.NextCue.SceneName);
|
|
Assert.Equal(TimeSpan.FromSeconds(1), plan.ObservationDuration);
|
|
Assert.False(plan.Options.ReconnectEnabled);
|
|
Assert.Equal(0, plan.Options.MaximumReconnectAttempts);
|
|
}
|
|
|
|
[Fact]
|
|
public void Preflight_TestConnectReusesTestConfigurationGatesWithoutScenes()
|
|
{
|
|
using var environment = ClearedPlayoutEnvironment();
|
|
using var config = CreateConfig(ValidOptions());
|
|
|
|
var plan = IsolatedTestCommandPreflight.Create(new SmokeCommandInvocation(
|
|
SmokeCommandKind.TestConnect,
|
|
config.Path));
|
|
|
|
Assert.Null(plan.PrepareCue);
|
|
Assert.Null(plan.NextCue);
|
|
Assert.Equal(TimeSpan.Zero, plan.ObservationDuration);
|
|
Assert.Equal(PlayoutMode.Test, plan.Options.Mode);
|
|
}
|
|
|
|
[Fact]
|
|
public void Preflight_RejectsAnyPlayoutEnvironmentOverrideBeforeReadingConfiguration()
|
|
{
|
|
using var environment = ClearedPlayoutEnvironment()
|
|
.Set("MBN_STOCK_PLAYOUT_FUTURE_OVERRIDE", "sensitive-value");
|
|
|
|
var exception = Assert.Throws<IsolatedTestPreflightException>(() =>
|
|
IsolatedTestCommandPreflight.Create(Invocation(
|
|
SmokeCommandKind.TestSequence,
|
|
"C:\\sensitive\\missing.local.json")));
|
|
|
|
Assert.Equal("environment-override-present", exception.ErrorCode);
|
|
Assert.DoesNotContain("sensitive", exception.Message, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public void Preflight_RejectsRelativeOrMissingConfigWithoutReflectingPath()
|
|
{
|
|
using var environment = ClearedPlayoutEnvironment();
|
|
const string unsafePath = ".\\private\\playout.local.json";
|
|
|
|
var exception = Assert.Throws<IsolatedTestPreflightException>(() =>
|
|
IsolatedTestCommandPreflight.Create(Invocation(
|
|
SmokeCommandKind.TestSequence,
|
|
unsafePath)));
|
|
|
|
Assert.Equal("config-file-rejected", exception.ErrorCode);
|
|
Assert.DoesNotContain(unsafePath, exception.Message, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(PlayoutMode.DryRun, false)]
|
|
[InlineData(PlayoutMode.Live, false)]
|
|
[InlineData(PlayoutMode.Test, true)]
|
|
public void Preflight_RejectsAnythingThatCouldEnableLiveOutput(
|
|
PlayoutMode mode,
|
|
bool trustedLiveOutputEnabled)
|
|
{
|
|
using var environment = ClearedPlayoutEnvironment();
|
|
var options = ValidOptions();
|
|
options.Mode = mode;
|
|
options.TrustedLiveOutputEnabled = trustedLiveOutputEnabled;
|
|
using var config = CreateConfig(options);
|
|
|
|
var exception = Assert.Throws<IsolatedTestPreflightException>(() =>
|
|
IsolatedTestCommandPreflight.Create(Invocation(
|
|
SmokeCommandKind.TestSequence,
|
|
config.Path)));
|
|
|
|
Assert.Equal("test-mode-required", exception.ErrorCode);
|
|
}
|
|
|
|
[Fact]
|
|
public void Preflight_ReusesAllowlistAndExistingFileValidationBeforeEngineCreation()
|
|
{
|
|
using var environment = ClearedPlayoutEnvironment();
|
|
var options = ValidOptions();
|
|
options.TestSceneAllowlist = ["5001"];
|
|
using var config = CreateConfig(options);
|
|
|
|
var exception = Assert.Throws<IsolatedTestPreflightException>(() =>
|
|
IsolatedTestCommandPreflight.Create(Invocation(
|
|
SmokeCommandKind.TestSequence,
|
|
config.Path)));
|
|
|
|
Assert.Equal("safety-gate-rejected", exception.ErrorCode);
|
|
Assert.DoesNotContain("5006", exception.Message, StringComparison.Ordinal);
|
|
Assert.DoesNotContain(_scenes.Path, exception.Message, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Executor_SuccessRunsExactSequenceWithObservationAndAllScopeTakeOut()
|
|
{
|
|
var engine = new RecordingPlayoutEngine();
|
|
var delays = new List<TimeSpan>();
|
|
|
|
var report = await IsolatedTestCommandExecutor.ExecuteAsync(
|
|
ExecutionPlan(SmokeCommandKind.TestSequence),
|
|
_ => engine,
|
|
(delay, _) =>
|
|
{
|
|
delays.Add(delay);
|
|
return Task.CompletedTask;
|
|
});
|
|
|
|
Assert.Equal(
|
|
[
|
|
"Connect",
|
|
"Prepare",
|
|
"TakeIn",
|
|
"Next",
|
|
"TakeOut:All",
|
|
"Disconnect",
|
|
"Dispose"
|
|
], engine.Calls);
|
|
Assert.Equal(
|
|
[
|
|
TimeSpan.FromSeconds(1),
|
|
TimeSpan.FromSeconds(1),
|
|
TimeSpan.FromSeconds(1)
|
|
],
|
|
delays);
|
|
Assert.True(report.Completed);
|
|
Assert.True(report.ComActivationAttempted);
|
|
Assert.Equal("accepted-unconfirmed", report.LastKtapConnectState);
|
|
Assert.True(report.KtapConnectAttempted);
|
|
Assert.True(report.KtapConnectAccepted);
|
|
Assert.True(report.NetworkMonitoringRecordExpected);
|
|
Assert.True(report.NetworkMonitoringCheckRequired);
|
|
Assert.Null(report.NetworkMonitoringVerified);
|
|
Assert.False(report.OutcomeUnknown);
|
|
Assert.Equal(6, report.Steps.Count);
|
|
Assert.True(report.Cleanup.AdapterDisposed);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Executor_TestConnectNeverIssuesSceneOrOutputMutation()
|
|
{
|
|
var engine = new RecordingPlayoutEngine();
|
|
|
|
var report = await IsolatedTestCommandExecutor.ExecuteAsync(
|
|
ExecutionPlan(SmokeCommandKind.TestConnect),
|
|
_ => engine,
|
|
(_, _) => throw new InvalidOperationException("No observation is allowed."));
|
|
|
|
Assert.Equal(["Connect", "Disconnect", "Dispose"], engine.Calls);
|
|
Assert.True(report.Completed);
|
|
Assert.True(report.ComActivationAttempted);
|
|
Assert.Equal("accepted-unconfirmed", report.LastKtapConnectState);
|
|
Assert.True(report.NetworkMonitoringRecordExpected);
|
|
Assert.True(report.NetworkMonitoringCheckRequired);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Executor_ConnectUnavailableDoesNotGuessWhetherComWasAttempted()
|
|
{
|
|
var engine = new RecordingPlayoutEngine()
|
|
.Return(PlayoutOperation.Connect, PlayoutResultCode.Unavailable);
|
|
|
|
var report = await IsolatedTestCommandExecutor.ExecuteAsync(
|
|
ExecutionPlan(SmokeCommandKind.TestConnect),
|
|
_ => engine,
|
|
(_, _) => Task.CompletedTask);
|
|
|
|
Assert.Null(report.ComActivationAttempted);
|
|
Assert.Equal("not-attempted", report.LastKtapConnectState);
|
|
Assert.False(report.NetworkMonitoringRecordExpected);
|
|
Assert.False(report.NetworkMonitoringCheckRequired);
|
|
Assert.Equal(["Connect", "Disconnect", "Dispose"], engine.Calls);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Executor_UnknownConnectOutcomeReportsUnknownActivationAndSkipsDisconnect()
|
|
{
|
|
var engine = new RecordingPlayoutEngine()
|
|
.Return(PlayoutOperation.Connect, PlayoutResultCode.OutcomeUnknown);
|
|
|
|
var report = await IsolatedTestCommandExecutor.ExecuteAsync(
|
|
ExecutionPlan(SmokeCommandKind.TestConnect),
|
|
_ => engine,
|
|
(_, _) => Task.CompletedTask);
|
|
|
|
Assert.Null(report.ComActivationAttempted);
|
|
Assert.Equal("attempted", report.LastKtapConnectState);
|
|
Assert.True(report.KtapConnectAttempted);
|
|
Assert.Null(report.KtapConnectAccepted);
|
|
Assert.Null(report.NetworkMonitoringRecordExpected);
|
|
Assert.True(report.NetworkMonitoringCheckRequired);
|
|
Assert.True(report.OutcomeUnknown);
|
|
Assert.Equal(["Connect", "Quarantine", "Dispose"], engine.Calls);
|
|
Assert.True(report.Cleanup.QuarantineCompleted);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Executor_EngineCreationFailureReportsNoComActivationAttempt()
|
|
{
|
|
var report = await IsolatedTestCommandExecutor.ExecuteAsync(
|
|
ExecutionPlan(SmokeCommandKind.TestConnect),
|
|
_ => throw new InvalidOperationException("sensitive factory failure"),
|
|
(_, _) => Task.CompletedTask);
|
|
|
|
Assert.False(report.ConnectRequestIssued);
|
|
Assert.False(report.ComActivationAttempted);
|
|
Assert.False(report.Completed);
|
|
Assert.False(report.OutcomeUnknown);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Executor_IntermediateFailureStopsPlaybackAndPerformsDisconnectCleanup()
|
|
{
|
|
var engine = new RecordingPlayoutEngine()
|
|
.Return(PlayoutOperation.Prepare, PlayoutResultCode.Failed);
|
|
|
|
var report = await IsolatedTestCommandExecutor.ExecuteAsync(
|
|
ExecutionPlan(SmokeCommandKind.TestSequence),
|
|
_ => engine,
|
|
(_, _) => Task.CompletedTask);
|
|
|
|
Assert.Equal(["Connect", "Prepare", "Disconnect", "Dispose"], engine.Calls);
|
|
Assert.False(report.Completed);
|
|
Assert.Equal("Prepare", report.StoppedAfter);
|
|
Assert.Contains(report.Steps, step =>
|
|
step.Phase == "cleanup" && step.Operation == "Disconnect");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Executor_OutcomeUnknownNeverAttemptsAutomaticDisconnectOrLaterCommand()
|
|
{
|
|
var engine = new RecordingPlayoutEngine()
|
|
.Return(PlayoutOperation.TakeIn, PlayoutResultCode.OutcomeUnknown);
|
|
|
|
var report = await IsolatedTestCommandExecutor.ExecuteAsync(
|
|
ExecutionPlan(SmokeCommandKind.TestSequence),
|
|
_ => engine,
|
|
(_, _) => Task.CompletedTask);
|
|
|
|
Assert.Equal(["Connect", "Prepare", "TakeIn", "Quarantine", "Dispose"], engine.Calls);
|
|
Assert.True(report.OutcomeUnknown);
|
|
Assert.True(report.OutputMayBeActive);
|
|
Assert.False(report.Cleanup.DisconnectAttempted);
|
|
Assert.True(report.Cleanup.AdapterDisposed);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Executor_QuarantineFailureSkipsDisposeToAvoidImplicitDisconnect()
|
|
{
|
|
var engine = new RecordingPlayoutEngine
|
|
{
|
|
ThrowOnQuarantine = true
|
|
}.Return(PlayoutOperation.TakeIn, PlayoutResultCode.OutcomeUnknown);
|
|
|
|
var report = await IsolatedTestCommandExecutor.ExecuteAsync(
|
|
ExecutionPlan(SmokeCommandKind.TestSequence),
|
|
_ => engine,
|
|
(_, _) => Task.CompletedTask);
|
|
|
|
Assert.Equal(["Connect", "Prepare", "TakeIn", "Quarantine"], engine.Calls);
|
|
Assert.True(report.Cleanup.QuarantineAttempted);
|
|
Assert.False(report.Cleanup.QuarantineCompleted);
|
|
Assert.False(report.Cleanup.AdapterDisposed);
|
|
Assert.False(report.Cleanup.DisconnectAttempted);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Executor_CancelledObservationStopsBeforeNextWithoutMarkingComOutcomeUnknown()
|
|
{
|
|
using var cancellation = new CancellationTokenSource();
|
|
var engine = new RecordingPlayoutEngine();
|
|
|
|
var report = await IsolatedTestCommandExecutor.ExecuteAsync(
|
|
ExecutionPlan(SmokeCommandKind.TestSequence),
|
|
_ => engine,
|
|
(_, token) =>
|
|
{
|
|
cancellation.Cancel();
|
|
return Task.FromCanceled(token);
|
|
},
|
|
cancellation.Token);
|
|
|
|
Assert.Equal(
|
|
["Connect", "Prepare", "TakeIn", "TakeOut:All", "Disconnect", "Dispose"],
|
|
engine.Calls);
|
|
Assert.Equal("Observation", report.StoppedAfter);
|
|
Assert.False(report.OutcomeUnknown);
|
|
Assert.False(report.OutputMayBeActive);
|
|
Assert.False(report.Completed);
|
|
Assert.Contains(report.Steps, step =>
|
|
step.Phase == "cleanup" && step.Operation == "TakeOut");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Executor_UnknownTakeOutCleanupSkipsAutomaticDisconnect()
|
|
{
|
|
using var cancellation = new CancellationTokenSource();
|
|
var engine = new RecordingPlayoutEngine()
|
|
.Return(PlayoutOperation.TakeOut, PlayoutResultCode.OutcomeUnknown);
|
|
|
|
var report = await IsolatedTestCommandExecutor.ExecuteAsync(
|
|
ExecutionPlan(SmokeCommandKind.TestSequence),
|
|
_ => engine,
|
|
(_, token) =>
|
|
{
|
|
cancellation.Cancel();
|
|
return Task.FromCanceled(token);
|
|
},
|
|
cancellation.Token);
|
|
|
|
Assert.Equal(
|
|
["Connect", "Prepare", "TakeIn", "TakeOut:All", "Quarantine", "Dispose"],
|
|
engine.Calls);
|
|
Assert.True(report.OutcomeUnknown);
|
|
Assert.True(report.OutputMayBeActive);
|
|
Assert.False(report.Cleanup.DisconnectAttempted);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(PlayoutResultCode.Failed)]
|
|
[InlineData(PlayoutResultCode.Unavailable)]
|
|
[InlineData(PlayoutResultCode.Cancelled)]
|
|
[InlineData(PlayoutResultCode.Rejected)]
|
|
public async Task Executor_NonSuccessfulCleanupTakeOutReportsPossibleActiveOutput(
|
|
PlayoutResultCode takeOutCode)
|
|
{
|
|
using var cancellation = new CancellationTokenSource();
|
|
var engine = new RecordingPlayoutEngine()
|
|
.Return(PlayoutOperation.TakeOut, takeOutCode);
|
|
|
|
var report = await IsolatedTestCommandExecutor.ExecuteAsync(
|
|
ExecutionPlan(SmokeCommandKind.TestSequence),
|
|
_ => engine,
|
|
(_, token) =>
|
|
{
|
|
cancellation.Cancel();
|
|
return Task.FromCanceled(token);
|
|
},
|
|
cancellation.Token);
|
|
|
|
Assert.Equal(
|
|
["Connect", "Prepare", "TakeIn", "TakeOut:All", "Quarantine", "Dispose"],
|
|
engine.Calls);
|
|
Assert.True(report.OutcomeUnknown);
|
|
Assert.True(report.OutputMayBeActive);
|
|
Assert.False(report.Cleanup.DisconnectAttempted);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(PlayoutResultCode.Failed)]
|
|
[InlineData(PlayoutResultCode.Unavailable)]
|
|
[InlineData(PlayoutResultCode.Cancelled)]
|
|
[InlineData(PlayoutResultCode.Rejected)]
|
|
public async Task Executor_NonSuccessfulSequenceTakeOutReportsPossibleActiveOutput(
|
|
PlayoutResultCode takeOutCode)
|
|
{
|
|
var engine = new RecordingPlayoutEngine()
|
|
.Return(PlayoutOperation.TakeOut, takeOutCode);
|
|
|
|
var report = await IsolatedTestCommandExecutor.ExecuteAsync(
|
|
ExecutionPlan(SmokeCommandKind.TestSequence),
|
|
_ => engine,
|
|
(_, _) => Task.CompletedTask);
|
|
|
|
Assert.Equal(
|
|
["Connect", "Prepare", "TakeIn", "Next", "TakeOut:All", "Quarantine", "Dispose"],
|
|
engine.Calls);
|
|
Assert.True(report.OutcomeUnknown);
|
|
Assert.True(report.OutputMayBeActive);
|
|
Assert.False(report.Cleanup.DisconnectAttempted);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Executor_ObservationFailureStillPerformsTakeOutCleanup()
|
|
{
|
|
var engine = new RecordingPlayoutEngine();
|
|
|
|
var report = await IsolatedTestCommandExecutor.ExecuteAsync(
|
|
ExecutionPlan(SmokeCommandKind.TestSequence),
|
|
_ => engine,
|
|
(_, _) => Task.FromException(new InvalidOperationException("fake observation failure")));
|
|
|
|
Assert.Equal(
|
|
["Connect", "Prepare", "TakeIn", "TakeOut:All", "Disconnect", "Dispose"],
|
|
engine.Calls);
|
|
Assert.Equal("Observation", report.StoppedAfter);
|
|
Assert.False(report.OutcomeUnknown);
|
|
Assert.False(report.OutputMayBeActive);
|
|
Assert.True(report.Cleanup.DisconnectAttempted);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Executor_ReportDoesNotContainEngineMessagesPathsOrSceneCodes()
|
|
{
|
|
var engine = new RecordingPlayoutEngine("C:\\private\\5001.t2s HRESULT 0x80004005");
|
|
|
|
var report = await IsolatedTestCommandExecutor.ExecuteAsync(
|
|
ExecutionPlan(SmokeCommandKind.TestSequence),
|
|
_ => engine,
|
|
(_, _) => Task.CompletedTask);
|
|
var json = JsonSerializer.Serialize(report);
|
|
|
|
Assert.DoesNotContain("private", json, StringComparison.OrdinalIgnoreCase);
|
|
Assert.DoesNotContain("5001", json, StringComparison.OrdinalIgnoreCase);
|
|
Assert.DoesNotContain("HRESULT", json, StringComparison.OrdinalIgnoreCase);
|
|
Assert.DoesNotContain("0x80004005", json, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
public void Dispose() => _scenes.Dispose();
|
|
|
|
private IsolatedTestCommandPlan ExecutionPlan(SmokeCommandKind kind) => new(
|
|
kind,
|
|
ValidOptions(),
|
|
kind == SmokeCommandKind.TestSequence ? new PlayoutCue("5001.t2s", "5001") : null,
|
|
kind == SmokeCommandKind.TestSequence ? new PlayoutCue("5006.t2s", "5006") : null,
|
|
kind == SmokeCommandKind.TestSequence ? TimeSpan.FromSeconds(1) : TimeSpan.Zero);
|
|
|
|
private SmokeCommandInvocation Invocation(SmokeCommandKind kind, string configPath) => new(
|
|
kind,
|
|
configPath,
|
|
"5001",
|
|
"5006",
|
|
1000);
|
|
|
|
private PlayoutOptions ValidOptions() => new()
|
|
{
|
|
Mode = PlayoutMode.Test,
|
|
Host = "127.0.0.1",
|
|
Port = 30001,
|
|
TcpMode = 1,
|
|
ClientPort = 0,
|
|
OutputChannel = 9,
|
|
LayoutIndex = 10,
|
|
SceneDirectory = _scenes.Path,
|
|
TestProcessWindowTitlePattern = "^Tornado2 TEST$",
|
|
TestSceneAllowlist = ["5001", "5006"],
|
|
TrustedLiveOutputEnabled = false,
|
|
ReconnectEnabled = true,
|
|
MaximumReconnectAttempts = 3
|
|
};
|
|
|
|
private static TemporaryJsonFile CreateConfig(PlayoutOptions options)
|
|
{
|
|
var serializerOptions = new JsonSerializerOptions();
|
|
serializerOptions.Converters.Add(new JsonStringEnumConverter());
|
|
return TemporaryJsonFile.Create(JsonSerializer.Serialize(options, serializerOptions));
|
|
}
|
|
|
|
private static EnvironmentScope ClearedPlayoutEnvironment()
|
|
{
|
|
var result = new EnvironmentScope();
|
|
foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables())
|
|
{
|
|
if (entry.Key is string name &&
|
|
name.StartsWith(
|
|
IsolatedTestCommandPreflight.EnvironmentVariablePrefix,
|
|
StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
result.Set(name, null);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private sealed class RecordingPlayoutEngine : IPlayoutEngine
|
|
{
|
|
private readonly Dictionary<PlayoutOperation, PlayoutResultCode> _codes = [];
|
|
private readonly string _message;
|
|
|
|
public RecordingPlayoutEngine(string message = "safe")
|
|
{
|
|
_message = message;
|
|
Status = new PlayoutStatus(
|
|
PlayoutMode.Test,
|
|
PlayoutConnectionState.Disconnected,
|
|
false,
|
|
true,
|
|
false,
|
|
false,
|
|
null,
|
|
null,
|
|
"safe",
|
|
DateTimeOffset.UnixEpoch,
|
|
1);
|
|
}
|
|
|
|
public List<string> Calls { get; } = [];
|
|
|
|
public bool ThrowOnQuarantine { get; init; }
|
|
|
|
public PlayoutStatus Status { get; private set; }
|
|
|
|
public event EventHandler<PlayoutStatusChangedEventArgs>? StatusChanged
|
|
{
|
|
add { }
|
|
remove { }
|
|
}
|
|
|
|
public RecordingPlayoutEngine Return(
|
|
PlayoutOperation operation,
|
|
PlayoutResultCode code)
|
|
{
|
|
_codes[operation] = code;
|
|
return this;
|
|
}
|
|
|
|
public Task<PlayoutResult> ConnectAsync(CancellationToken cancellationToken = default) =>
|
|
Result(PlayoutOperation.Connect, "Connect");
|
|
|
|
public Task<PlayoutResult> DisconnectAsync(CancellationToken cancellationToken = default) =>
|
|
Result(PlayoutOperation.Disconnect, "Disconnect");
|
|
|
|
public Task<PlayoutResult> PrepareAsync(
|
|
PlayoutCue cue,
|
|
CancellationToken cancellationToken = default) =>
|
|
Result(PlayoutOperation.Prepare, "Prepare");
|
|
|
|
public Task<PlayoutResult> TakeInAsync(CancellationToken cancellationToken = default) =>
|
|
Result(PlayoutOperation.TakeIn, "TakeIn");
|
|
|
|
public Task<PlayoutResult> NextAsync(
|
|
PlayoutCue cue,
|
|
CancellationToken cancellationToken = default) =>
|
|
Result(PlayoutOperation.Next, "Next");
|
|
|
|
public Task<PlayoutResult> TakeOutAsync(
|
|
PlayoutTakeOutScope scope,
|
|
CancellationToken cancellationToken = default) =>
|
|
Result(PlayoutOperation.TakeOut, $"TakeOut:{scope}");
|
|
|
|
public ValueTask DisposeAsync()
|
|
{
|
|
Calls.Add("Dispose");
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
|
|
public ValueTask QuarantineAsync()
|
|
{
|
|
Calls.Add("Quarantine");
|
|
if (ThrowOnQuarantine)
|
|
{
|
|
throw new InvalidOperationException("fake quarantine failure");
|
|
}
|
|
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
|
|
private Task<PlayoutResult> Result(PlayoutOperation operation, string call)
|
|
{
|
|
Calls.Add(call);
|
|
var code = _codes.GetValueOrDefault(operation, PlayoutResultCode.Success);
|
|
if (operation == PlayoutOperation.Connect)
|
|
{
|
|
Status = Status with
|
|
{
|
|
LastKtapConnectState = code switch
|
|
{
|
|
PlayoutResultCode.Success => PlayoutKtapConnectState.AcceptedUnconfirmed,
|
|
PlayoutResultCode.Failed => PlayoutKtapConnectState.Failed,
|
|
PlayoutResultCode.OutcomeUnknown or PlayoutResultCode.TimedOut =>
|
|
PlayoutKtapConnectState.Attempted,
|
|
_ => PlayoutKtapConnectState.NotAttempted
|
|
}
|
|
};
|
|
}
|
|
|
|
return Task.FromResult(new PlayoutResult(
|
|
operation,
|
|
code,
|
|
false,
|
|
_message,
|
|
DateTimeOffset.UnixEpoch));
|
|
}
|
|
}
|
|
}
|