feat: harden isolated Tornado test workflow
This commit is contained in:
@@ -0,0 +1,564 @@
|
||||
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)], delays);
|
||||
Assert.True(report.Completed);
|
||||
Assert.True(report.ComActivationAttempted);
|
||||
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);
|
||||
}
|
||||
|
||||
[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(["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.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; }
|
||||
|
||||
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);
|
||||
return Task.FromResult(new PlayoutResult(
|
||||
operation,
|
||||
code,
|
||||
false,
|
||||
_message,
|
||||
DateTimeOffset.UnixEpoch));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user