feat: advance legacy UI behavior parity
This commit is contained in:
@@ -72,6 +72,177 @@ public sealed class LegacyPlayoutNativeContractTests
|
||||
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()
|
||||
{
|
||||
@@ -143,6 +314,58 @@ public sealed class LegacyPlayoutNativeContractTests
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user