feat: verify Tornado PGM cut sequence
This commit is contained in:
@@ -29,6 +29,8 @@ return parsed.Invocation.Kind switch
|
||||
await RunIsolatedTestCommandAsync(parsed.Invocation, cancellation.Token),
|
||||
SmokeCommandKind.PgmConnectDiagnostic =>
|
||||
await RunPgmConnectDiagnosticAsync(parsed.Invocation, cancellation.Token),
|
||||
SmokeCommandKind.PgmCutsSequence =>
|
||||
await RunPgmCutsSequenceAsync(parsed.Invocation, cancellation.Token),
|
||||
_ => Usage("unknown-command")
|
||||
};
|
||||
|
||||
@@ -286,6 +288,75 @@ static async Task<int> RunPgmConnectDiagnosticAsync(
|
||||
return report.OutcomeUnknown ? 5 : 4;
|
||||
}
|
||||
|
||||
static async Task<int> RunPgmCutsSequenceAsync(
|
||||
SmokeCommandInvocation invocation,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (IsolatedTestCommandPreflight.HasPlayoutEnvironmentOverride(
|
||||
Environment.GetEnvironmentVariables()))
|
||||
{
|
||||
WritePgmCutsSequenceFailure("environment-override-present");
|
||||
return 65;
|
||||
}
|
||||
|
||||
var request = new PgmCutsSequenceRequest(
|
||||
invocation.Host ?? string.Empty,
|
||||
invocation.Port ?? 0,
|
||||
invocation.ExpectedPgmWindowTitle ?? string.Empty,
|
||||
invocation.SceneRoot ?? string.Empty,
|
||||
invocation.ObservationMilliseconds ?? 0);
|
||||
PgmCutsSequenceReport report;
|
||||
try
|
||||
{
|
||||
report = await new PgmCutsSequenceRunner()
|
||||
.ExecuteAsync(
|
||||
request,
|
||||
stage =>
|
||||
{
|
||||
WriteProgressJson(new
|
||||
{
|
||||
@event = "observation-start",
|
||||
stage = ToObservationWireValue(stage)
|
||||
});
|
||||
},
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
WritePgmCutsSequenceFailure("internal-sequence-failure");
|
||||
return 70;
|
||||
}
|
||||
|
||||
WriteJson(new
|
||||
{
|
||||
command = "pgm-cuts-sequence",
|
||||
report.RequestValidated,
|
||||
report.HashPinsPresent,
|
||||
report.TargetGatePassed,
|
||||
report.EngineCreated,
|
||||
report.RenderCommandAttempted,
|
||||
report.DisconnectAttempted,
|
||||
report.QuarantineAttempted,
|
||||
report.QuarantineCompleted,
|
||||
report.Completed,
|
||||
report.OutcomeUnknown,
|
||||
commands = report.Commands.Select(command => new
|
||||
{
|
||||
stage = ToSequenceStageWireValue(command.Stage),
|
||||
code = command.Code.ToString()
|
||||
}),
|
||||
report.ErrorCode
|
||||
});
|
||||
|
||||
if (report.Completed && !report.OutcomeUnknown)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return report.OutcomeUnknown ? 5 : 4;
|
||||
}
|
||||
|
||||
static void WritePgmDiagnosticFailure(string errorCode) =>
|
||||
WriteJson(new
|
||||
{
|
||||
@@ -313,6 +384,24 @@ static void WritePgmDiagnosticFailure(string errorCode) =>
|
||||
errorCode
|
||||
});
|
||||
|
||||
static void WritePgmCutsSequenceFailure(string errorCode) =>
|
||||
WriteJson(new
|
||||
{
|
||||
command = "pgm-cuts-sequence",
|
||||
requestValidated = false,
|
||||
hashPinsPresent = false,
|
||||
targetGatePassed = false,
|
||||
engineCreated = false,
|
||||
renderCommandAttempted = false,
|
||||
disconnectAttempted = false,
|
||||
quarantineAttempted = false,
|
||||
quarantineCompleted = false,
|
||||
completed = false,
|
||||
outcomeUnknown = false,
|
||||
commands = Array.Empty<object>(),
|
||||
errorCode
|
||||
});
|
||||
|
||||
static string ToWireValue(PlayoutKtapConnectState state) => state switch
|
||||
{
|
||||
PlayoutKtapConnectState.NotAttempted => "not-attempted",
|
||||
@@ -322,6 +411,25 @@ static string ToWireValue(PlayoutKtapConnectState state) => state switch
|
||||
_ => "not-attempted"
|
||||
};
|
||||
|
||||
static string ToObservationWireValue(PgmCutsObservationStage stage) => stage switch
|
||||
{
|
||||
PgmCutsObservationStage.First => "first",
|
||||
PgmCutsObservationStage.Next => "next",
|
||||
PgmCutsObservationStage.TakeOut => "take-out",
|
||||
_ => "unknown"
|
||||
};
|
||||
|
||||
static string ToSequenceStageWireValue(PgmCutsSequenceStage stage) => stage switch
|
||||
{
|
||||
PgmCutsSequenceStage.Connect => "connect",
|
||||
PgmCutsSequenceStage.PrepareFirst => "prepare-first",
|
||||
PgmCutsSequenceStage.TakeIn => "take-in",
|
||||
PgmCutsSequenceStage.Next => "next",
|
||||
PgmCutsSequenceStage.TakeOut => "take-out",
|
||||
PgmCutsSequenceStage.Disconnect => "disconnect",
|
||||
_ => "unknown"
|
||||
};
|
||||
|
||||
static void WritePreflightFailure(string command, string errorCode) =>
|
||||
WriteJson(new
|
||||
{
|
||||
@@ -349,6 +457,15 @@ static void WriteJson<T>(T value) =>
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
}));
|
||||
|
||||
static void WriteProgressJson<T>(T value)
|
||||
{
|
||||
Console.WriteLine(JsonSerializer.Serialize(value, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
}));
|
||||
Console.Out.Flush();
|
||||
}
|
||||
|
||||
static int Usage(string? errorCode)
|
||||
{
|
||||
WriteJson(new
|
||||
@@ -366,7 +483,7 @@ static int Usage(string? errorCode)
|
||||
errorCode = errorCode ?? "invalid-command-line"
|
||||
});
|
||||
Console.Error.WriteLine(
|
||||
"Usage: MBN_STOCK_WEBVIEW.PlayoutSmoke [--probe|--dry-run|--test-plan|--test-connect|--test-sequence|--pgm-connect-diagnostic]");
|
||||
"Usage: MBN_STOCK_WEBVIEW.PlayoutSmoke [--probe|--dry-run|--test-plan|--test-connect|--test-sequence|--pgm-connect-diagnostic|--pgm-cuts-sequence]");
|
||||
Console.Error.WriteLine(
|
||||
$"Test commands require {SmokeCommandLine.AcknowledgementFlag} and --config <absolute-path>.");
|
||||
Console.Error.WriteLine(
|
||||
@@ -375,5 +492,7 @@ static int Usage(string? errorCode)
|
||||
"--test-connect and --test-sequence may request render-capable COM after all Test safety gates pass.");
|
||||
Console.Error.WriteLine(
|
||||
$"--pgm-connect-diagnostic requires {SmokeCommandLine.PgmConnectAcknowledgementFlag}, --host, --port and --expected-pgm-window-title; it exposes no render command.");
|
||||
Console.Error.WriteLine(
|
||||
$"--pgm-cuts-sequence requires {SmokeCommandLine.PgmCutsSequenceAcknowledgementFlag}, --host, --port, --expected-pgm-window-title and --scene-root; --observe-ms may only be 5000.");
|
||||
return 64;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ internal enum SmokeCommandKind
|
||||
TestPlan,
|
||||
TestConnect,
|
||||
TestSequence,
|
||||
PgmConnectDiagnostic
|
||||
PgmConnectDiagnostic,
|
||||
PgmCutsSequence
|
||||
}
|
||||
|
||||
internal sealed record SmokeCommandInvocation(
|
||||
@@ -19,7 +20,8 @@ internal sealed record SmokeCommandInvocation(
|
||||
int? ObservationMilliseconds = null,
|
||||
string? Host = null,
|
||||
int? Port = null,
|
||||
string? ExpectedPgmWindowTitle = null);
|
||||
string? ExpectedPgmWindowTitle = null,
|
||||
string? SceneRoot = null);
|
||||
|
||||
internal sealed record SmokeCommandParseResult(
|
||||
bool IsValid,
|
||||
@@ -32,6 +34,8 @@ internal static class SmokeCommandLine
|
||||
"--acknowledge-isolated-non-program-test-output";
|
||||
internal const string PgmConnectAcknowledgementFlag =
|
||||
"--i-understand-this-will-contact-current-pgm-tornado-via-ktap-connect-and-disconnect-only";
|
||||
internal const string PgmCutsSequenceAcknowledgementFlag =
|
||||
"--i-understand-this-will-render-current-pgm-tornado-cuts-5001-then-5006-and-take-them-out";
|
||||
|
||||
public static SmokeCommandParseResult Parse(IReadOnlyList<string> arguments)
|
||||
{
|
||||
@@ -57,6 +61,7 @@ internal static class SmokeCommandLine
|
||||
"--test-connect" => SmokeCommandKind.TestConnect,
|
||||
"--test-sequence" => SmokeCommandKind.TestSequence,
|
||||
"--pgm-connect-diagnostic" => SmokeCommandKind.PgmConnectDiagnostic,
|
||||
"--pgm-cuts-sequence" => SmokeCommandKind.PgmCutsSequence,
|
||||
_ => SmokeCommandKind.Invalid
|
||||
};
|
||||
if (kind == SmokeCommandKind.Invalid)
|
||||
@@ -71,6 +76,7 @@ internal static class SmokeCommandLine
|
||||
string? host = null;
|
||||
int? port = null;
|
||||
string? expectedPgmWindowTitle = null;
|
||||
string? sceneRoot = null;
|
||||
var acknowledged = false;
|
||||
|
||||
for (var index = 1; index < arguments.Count; index++)
|
||||
@@ -78,8 +84,10 @@ internal static class SmokeCommandLine
|
||||
var argument = arguments[index];
|
||||
switch (argument)
|
||||
{
|
||||
case AcknowledgementFlag when kind != SmokeCommandKind.PgmConnectDiagnostic:
|
||||
case AcknowledgementFlag when kind is not (
|
||||
SmokeCommandKind.PgmConnectDiagnostic or SmokeCommandKind.PgmCutsSequence):
|
||||
case PgmConnectAcknowledgementFlag when kind == SmokeCommandKind.PgmConnectDiagnostic:
|
||||
case PgmCutsSequenceAcknowledgementFlag when kind == SmokeCommandKind.PgmCutsSequence:
|
||||
if (acknowledged)
|
||||
{
|
||||
return Invalid("duplicate-option");
|
||||
@@ -88,7 +96,8 @@ internal static class SmokeCommandLine
|
||||
acknowledged = true;
|
||||
break;
|
||||
|
||||
case "--host" when kind == SmokeCommandKind.PgmConnectDiagnostic:
|
||||
case "--host" when kind is
|
||||
SmokeCommandKind.PgmConnectDiagnostic or SmokeCommandKind.PgmCutsSequence:
|
||||
if (host is not null)
|
||||
{
|
||||
return Invalid("duplicate-option");
|
||||
@@ -101,7 +110,8 @@ internal static class SmokeCommandLine
|
||||
|
||||
break;
|
||||
|
||||
case "--port" when kind == SmokeCommandKind.PgmConnectDiagnostic:
|
||||
case "--port" when kind is
|
||||
SmokeCommandKind.PgmConnectDiagnostic or SmokeCommandKind.PgmCutsSequence:
|
||||
if (port is not null)
|
||||
{
|
||||
return Invalid("duplicate-option");
|
||||
@@ -122,7 +132,8 @@ internal static class SmokeCommandLine
|
||||
break;
|
||||
|
||||
case "--expected-pgm-window-title"
|
||||
when kind == SmokeCommandKind.PgmConnectDiagnostic:
|
||||
when kind is
|
||||
SmokeCommandKind.PgmConnectDiagnostic or SmokeCommandKind.PgmCutsSequence:
|
||||
if (expectedPgmWindowTitle is not null)
|
||||
{
|
||||
return Invalid("duplicate-option");
|
||||
@@ -135,7 +146,21 @@ internal static class SmokeCommandLine
|
||||
|
||||
break;
|
||||
|
||||
case "--config" when kind != SmokeCommandKind.PgmConnectDiagnostic:
|
||||
case "--scene-root" when kind == SmokeCommandKind.PgmCutsSequence:
|
||||
if (sceneRoot is not null)
|
||||
{
|
||||
return Invalid("duplicate-option");
|
||||
}
|
||||
|
||||
if (!TryReadValue(arguments, ref index, out sceneRoot))
|
||||
{
|
||||
return Invalid("missing-option-value");
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "--config" when kind is not (
|
||||
SmokeCommandKind.PgmConnectDiagnostic or SmokeCommandKind.PgmCutsSequence):
|
||||
if (configPath is not null)
|
||||
{
|
||||
return Invalid("duplicate-option");
|
||||
@@ -194,6 +219,26 @@ internal static class SmokeCommandLine
|
||||
observationMilliseconds = observationValue;
|
||||
break;
|
||||
|
||||
case "--observe-ms" when kind == SmokeCommandKind.PgmCutsSequence:
|
||||
if (observationMilliseconds is not null)
|
||||
{
|
||||
return Invalid("duplicate-option");
|
||||
}
|
||||
|
||||
if (!TryReadValue(arguments, ref index, out var pgmObservationText) ||
|
||||
!int.TryParse(
|
||||
pgmObservationText,
|
||||
System.Globalization.NumberStyles.None,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out var pgmObservationValue) ||
|
||||
pgmObservationValue != 5_000)
|
||||
{
|
||||
return Invalid("invalid-observation-duration");
|
||||
}
|
||||
|
||||
observationMilliseconds = pgmObservationValue;
|
||||
break;
|
||||
|
||||
default:
|
||||
return Invalid("unknown-option");
|
||||
}
|
||||
@@ -220,6 +265,28 @@ internal static class SmokeCommandLine
|
||||
ExpectedPgmWindowTitle: expectedPgmWindowTitle));
|
||||
}
|
||||
|
||||
if (kind == SmokeCommandKind.PgmCutsSequence)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(host) ||
|
||||
!System.Net.IPAddress.TryParse(host, out var literalHost) ||
|
||||
!System.Net.IPAddress.IsLoopback(literalHost) ||
|
||||
port is null ||
|
||||
string.IsNullOrWhiteSpace(expectedPgmWindowTitle) ||
|
||||
string.IsNullOrWhiteSpace(sceneRoot) ||
|
||||
!Path.IsPathFullyQualified(sceneRoot))
|
||||
{
|
||||
return Invalid("missing-or-invalid-required-option");
|
||||
}
|
||||
|
||||
return Valid(new SmokeCommandInvocation(
|
||||
kind,
|
||||
ObservationMilliseconds: observationMilliseconds ?? 5_000,
|
||||
Host: host,
|
||||
Port: port,
|
||||
ExpectedPgmWindowTitle: expectedPgmWindowTitle,
|
||||
SceneRoot: sceneRoot));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(configPath))
|
||||
{
|
||||
return Invalid("missing-required-option");
|
||||
@@ -254,7 +321,8 @@ internal static class SmokeCommandLine
|
||||
observationMilliseconds,
|
||||
host,
|
||||
port,
|
||||
expectedPgmWindowTitle));
|
||||
expectedPgmWindowTitle,
|
||||
sceneRoot));
|
||||
}
|
||||
|
||||
internal static bool IsSafeSceneCode(string value)
|
||||
|
||||
Reference in New Issue
Block a user