385 lines
16 KiB
C#
385 lines
16 KiB
C#
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
|
|
|
|
public sealed class LegacyPlayoutNativeContractTests
|
|
{
|
|
private static readonly string RepositoryRoot = FindRepositoryRoot();
|
|
private static readonly string AppRoot = Path.Combine(
|
|
RepositoryRoot,
|
|
"src",
|
|
"MBN_STOCK_WEBVIEW.LegacyParityApp");
|
|
private static readonly string Playout = File.ReadAllText(
|
|
Path.Combine(AppRoot, "MainWindow.Playout.cs"));
|
|
private static readonly string Window = File.ReadAllText(
|
|
Path.Combine(AppRoot, "MainWindow.xaml.cs"));
|
|
private static readonly string Project = File.ReadAllText(
|
|
Path.Combine(AppRoot, "MBN_STOCK_WEBVIEW.LegacyParityApp.csproj"));
|
|
|
|
[Fact]
|
|
public void ParityAppComposesTheVerifiedThirtyFiveSceneRuntime()
|
|
{
|
|
Assert.Contains("LegacySceneRuntimeFactory.cs", Project, StringComparison.Ordinal);
|
|
Assert.Contains("LegacySceneRuntimeFactory.Create(", Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("trustedManualSource: _manualNetSellStore", Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("trustedViSource: _manualViStore", Playout,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void StartupDefaultBackgroundDependsOnAbsentLocalConfigAndSurfacesWarning()
|
|
{
|
|
Assert.Contains("File.Exists(", Playout, StringComparison.Ordinal);
|
|
Assert.Contains("PlayoutOptionsLoader.DefaultPath", Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("LegacyParityStartupCompositionResolver.Resolve(", Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("_playoutInitializationWarning ?? status?.Message", Playout,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void OperatorCommandsUseTheNativeWorkflowAndCSharpPlaylist()
|
|
{
|
|
Assert.Contains("_controller.CreatePlayoutRequest(", Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("_compositionOptions!.Current.FadeDuration", Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("workflow.PrepareAsync(", Playout, StringComparison.Ordinal);
|
|
Assert.Contains("workflow.TakeSelectedAsync(", Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain("workflow.TakeInAsync(", Playout, StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"string.IsNullOrWhiteSpace(workflow.State.PreparedCutCode)",
|
|
Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("workflow.NextAsync(", Playout, StringComparison.Ordinal);
|
|
Assert.Contains("workflow.TakeOutAsync(", Playout, StringComparison.Ordinal);
|
|
Assert.Contains("_controller.ReflectSuccessfulPlayout(", Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("workflow.State.CurrentEntryId", Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("LegacyOperatorPlayoutCommand.Prepare or", Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"command != LegacyOperatorPlayoutCommand.TakeOut",
|
|
Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("LegacyWorkflowNextKind.PlaylistNext", Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("LegacySceneRefreshPolicy.TryGetInterval", Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("WaitForPlayCompletionAsync", Playout, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void AutomaticRefreshUsesTheCoreSchedulerAndGenerationOwnedState()
|
|
{
|
|
Assert.Contains("new LegacyRefreshScheduler(", Playout, StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"LegacyRefreshEpoch<LegacyRefreshRuntimeStatus>",
|
|
Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("_refreshEpoch.ReadState()", Playout, StringComparison.Ordinal);
|
|
Assert.Contains("_refreshEpoch.Replace(", Playout, StringComparison.Ordinal);
|
|
Assert.Contains("_refreshEpoch.TryUpdate(", Playout, StringComparison.Ordinal);
|
|
Assert.Contains("_refreshEpoch.TryComplete(", Playout, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("Task.Delay(", Playout, StringComparison.Ordinal);
|
|
|
|
foreach (var obsoleteRawField in new[]
|
|
{
|
|
"_refreshCancellation",
|
|
"_refreshCts",
|
|
"_refreshStateGate",
|
|
"_refreshActive",
|
|
"_refreshCompletedCount",
|
|
"_refreshMaximumCount",
|
|
"_refreshLimitReached",
|
|
"_refreshNextAtUtc",
|
|
"_refreshLastSuccessAtUtc",
|
|
"_refreshFaultCode",
|
|
"_refreshMessage"
|
|
})
|
|
{
|
|
Assert.DoesNotContain(obsoleteRawField, Playout, StringComparison.Ordinal);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ZeroRefreshLimitReturnsBeforeCreatingAnEpochOrLoop()
|
|
{
|
|
var start = Slice(
|
|
Playout,
|
|
"private void StartRefreshLoop(LegacyPlayoutWorkflow workflow)",
|
|
"private void StopRefreshLoop()");
|
|
var limitCheck = RequiredIndex(start, "if (scheduler.HasReachedMaximum)");
|
|
var linkedEpoch = RequiredIndex(
|
|
start,
|
|
"CancellationTokenSource.CreateLinkedTokenSource(");
|
|
var loopAssignment = RequiredIndex(
|
|
start,
|
|
"_refreshTask = RunRefreshLoopAsync(workflow, scheduler, cancellation);");
|
|
|
|
Assert.True(limitCheck < linkedEpoch);
|
|
Assert.True(linkedEpoch < loopAssignment);
|
|
var zeroLimitBranch = start[limitCheck..linkedEpoch];
|
|
Assert.Contains("_refreshTask = Task.CompletedTask;", zeroLimitBranch,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("_refreshEpoch.Stop(", zeroLimitBranch, StringComparison.Ordinal);
|
|
Assert.Contains("return;", zeroLimitBranch, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("RunRefreshLoopAsync(", zeroLimitBranch,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void AutomaticRefreshSerializesDbThenPlayoutAndPublishesBothBusyStates()
|
|
{
|
|
var loop = Slice(
|
|
Playout,
|
|
"private async Task RunRefreshLoopAsync(",
|
|
"private bool TrySetRefreshFault(");
|
|
var intentWait = RequiredIndex(loop, "await _intentGate.WaitAsync(token)");
|
|
var intentBusy = RequiredIndex(loop, "_intentBusy = true;", intentWait);
|
|
var intentBusyState = RequiredIndex(loop, "QueueOperatorState();", intentBusy);
|
|
var playoutWait = RequiredIndex(
|
|
loop,
|
|
"await _playoutCommandGate.WaitAsync(token)",
|
|
intentBusyState);
|
|
var playoutBusy = RequiredIndex(
|
|
loop,
|
|
"Interlocked.Exchange(ref _playoutBusy, 1);",
|
|
playoutWait);
|
|
var playoutBusyState = RequiredIndex(loop, "QueueOperatorState();", playoutBusy);
|
|
var refreshDispatch = RequiredIndex(
|
|
loop,
|
|
"workflow.RefreshOnAirAsync",
|
|
playoutBusyState);
|
|
|
|
Assert.True(intentWait < intentBusy);
|
|
Assert.True(intentBusy < intentBusyState);
|
|
Assert.True(intentBusyState < playoutWait);
|
|
Assert.True(playoutWait < playoutBusy);
|
|
Assert.True(playoutBusy < playoutBusyState);
|
|
Assert.True(playoutBusyState < refreshDispatch);
|
|
}
|
|
|
|
[Fact]
|
|
public void KnownRefreshInputFailuresStopWithoutQuarantineButUnknownsDoQuarantine()
|
|
{
|
|
var loop = Normalize(Slice(
|
|
Playout,
|
|
"private async Task RunRefreshLoopAsync(",
|
|
"private bool TrySetRefreshFault("));
|
|
var databaseFailure = Slice(
|
|
loop,
|
|
"catch (DatabaseInfrastructureException)",
|
|
"catch (LegacySceneDataException)");
|
|
var sceneDataFailure = Slice(
|
|
loop,
|
|
"catch (LegacySceneDataException)",
|
|
"catch (ArgumentException)");
|
|
var argumentFailure = Slice(
|
|
loop,
|
|
"catch (ArgumentException)",
|
|
"catch\n {");
|
|
var unknownFailure = Slice(
|
|
loop,
|
|
"catch\n {",
|
|
"finally");
|
|
|
|
AssertKnownFailureDoesNotQuarantine(
|
|
databaseFailure,
|
|
"DATABASE_UNAVAILABLE");
|
|
AssertKnownFailureDoesNotQuarantine(
|
|
sceneDataFailure,
|
|
"SCENE_DATA_INVALID");
|
|
AssertKnownFailureDoesNotQuarantine(
|
|
argumentFailure,
|
|
"SCENE_ARGUMENT_INVALID");
|
|
|
|
Assert.Contains("REFRESH_OUTCOME_UNKNOWN", unknownFailure,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("QuarantinePlayoutAsync", unknownFailure,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"result.Code is PlayoutResultCode.OutcomeUnknown or\n" +
|
|
" PlayoutResultCode.TimedOut",
|
|
loop,
|
|
StringComparison.Ordinal);
|
|
AssertEveryFaultBranchQuarantines(loop, "PLAY_CALLBACK_TIMEOUT");
|
|
}
|
|
|
|
[Fact]
|
|
public void ExternalIdleStatusResetsNormalRefreshState()
|
|
{
|
|
var handler = Slice(
|
|
Playout,
|
|
"private void OnPlayoutStatusChanged(",
|
|
"private async Task<LegacyOperatorSnapshot> ExecuteOperatorPlayoutAsync(");
|
|
Assert.Contains(
|
|
"string.IsNullOrWhiteSpace(args.Current.PreparedSceneName)",
|
|
handler,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"string.IsNullOrWhiteSpace(args.Current.OnAirSceneName)",
|
|
handler,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"args.Current.State == PlayoutConnectionState.OutcomeUnknown",
|
|
handler,
|
|
StringComparison.Ordinal);
|
|
var stop = RequiredIndex(handler, "StopRefreshLoop();");
|
|
var normalElse = RequiredIndex(handler, "else", stop);
|
|
var reset = RequiredIndex(handler, "ResetRefreshState();", normalElse);
|
|
Assert.True(stop < normalElse && normalElse < reset);
|
|
|
|
var resetHelper = Slice(
|
|
Playout,
|
|
"private void ResetRefreshState()",
|
|
"private async Task RunRefreshLoopAsync(");
|
|
Assert.Contains(
|
|
"_refreshEpoch.Stop(_ => LegacyRefreshRuntimeStatus.Empty",
|
|
resetHelper,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void UnknownOutcomeQuarantinesWithoutAutomaticCommandRetry()
|
|
{
|
|
Assert.Contains("PlayoutResultCode.OutcomeUnknown", Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("PlayoutResultCode.TimedOut", Playout, StringComparison.Ordinal);
|
|
Assert.Contains("QuarantinePlayoutAsync", Playout, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("MaximumReconnectAttempts", Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain("ReconnectAsync", Playout, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void OriginalDissolveComboAcceptsOnlyItsZeroBasedTwentyItemIndex()
|
|
{
|
|
Assert.Contains("fadeDuration is < 0 or > 19", Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain("fadeDuration is < 0 or > 60", Playout,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void HomeAndEndStopAutomaticRefreshBeforeChangingTheCandidateRow()
|
|
{
|
|
Assert.Contains(
|
|
"LegacySelectPlaylistBoundaryIntent boundary =>\n" +
|
|
" SelectPlaylistBoundaryAndStopRefresh(boundary.Last)",
|
|
Window.Replace("\r\n", "\n", StringComparison.Ordinal),
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"private LegacyOperatorSnapshot SelectPlaylistBoundaryAndStopRefresh(bool last)",
|
|
Playout,
|
|
StringComparison.Ordinal);
|
|
var helperStart = Playout.IndexOf(
|
|
"private LegacyOperatorSnapshot SelectPlaylistBoundaryAndStopRefresh(bool last)",
|
|
StringComparison.Ordinal);
|
|
var helperEnd = Playout.IndexOf(
|
|
"private async Task<LegacyOperatorSnapshot> ToggleBackgroundAsync",
|
|
helperStart,
|
|
StringComparison.Ordinal);
|
|
Assert.True(helperStart >= 0 && helperEnd > helperStart);
|
|
var helper = Playout[helperStart..helperEnd];
|
|
Assert.True(
|
|
helper.IndexOf("StopRefreshLoop();", StringComparison.Ordinal) <
|
|
helper.IndexOf("_controller.SelectPlaylistBoundary(last)", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void PreparedProgramAndUnknownWriteOutcomesBlockOperatorMutations()
|
|
{
|
|
Assert.Contains("CanAcceptOperatorMutation()", Window, StringComparison.Ordinal);
|
|
Assert.Contains("IsOperatorMutationQuarantined()", Window,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("ObserveOperatorMutationQuarantine(state)", Window,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("ManualFinancial?.WritesQuarantined", Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("NamedPlaylist?.IsWriteQuarantined", Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("ManualLists?.WritesQuarantined", Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("OperatorCatalog?.WritesQuarantined", Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("LegacyConfirmManualListIntent", Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("LegacyCutPointerDownIntent", Playout,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("LegacyClearCutSelectionIntent", Playout,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
private static void AssertKnownFailureDoesNotQuarantine(
|
|
string failureBlock,
|
|
string expectedFaultCode)
|
|
{
|
|
Assert.Contains(expectedFaultCode, failureBlock, StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"QuarantinePlayoutAsync",
|
|
failureBlock,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
private static void AssertEveryFaultBranchQuarantines(
|
|
string source,
|
|
string faultCode)
|
|
{
|
|
var occurrence = 0;
|
|
var count = 0;
|
|
while ((occurrence = source.IndexOf(
|
|
faultCode,
|
|
occurrence,
|
|
StringComparison.Ordinal)) >= 0)
|
|
{
|
|
var branchEnd = source.IndexOf("return;", occurrence, StringComparison.Ordinal);
|
|
Assert.True(branchEnd > occurrence, $"{faultCode} branch has no return.");
|
|
Assert.Contains(
|
|
"QuarantinePlayoutAsync",
|
|
source[occurrence..branchEnd],
|
|
StringComparison.Ordinal);
|
|
count++;
|
|
occurrence = branchEnd + "return;".Length;
|
|
}
|
|
|
|
Assert.True(count > 0, $"No {faultCode} branch was found.");
|
|
}
|
|
|
|
private static string Slice(string source, string startMarker, string endMarker)
|
|
{
|
|
var start = RequiredIndex(source, startMarker);
|
|
var end = RequiredIndex(source, endMarker, start + startMarker.Length);
|
|
return source[start..end];
|
|
}
|
|
|
|
private static int RequiredIndex(string source, string marker, int startIndex = 0)
|
|
{
|
|
var index = source.IndexOf(marker, startIndex, StringComparison.Ordinal);
|
|
Assert.True(index >= 0, $"Required source marker was not found: {marker}");
|
|
return index;
|
|
}
|
|
|
|
private static string Normalize(string source) =>
|
|
source.Replace("\r\n", "\n", StringComparison.Ordinal);
|
|
|
|
private static string FindRepositoryRoot()
|
|
{
|
|
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (current is not null)
|
|
{
|
|
if (File.Exists(Path.Combine(current.FullName, "MBN_STOCK_WEBVIEW.sln")))
|
|
{
|
|
return current.FullName;
|
|
}
|
|
|
|
current = current.Parent;
|
|
}
|
|
|
|
throw new DirectoryNotFoundException("Repository root was not found.");
|
|
}
|
|
}
|