feat: verify Tornado PGM cut sequence

This commit is contained in:
2026-07-10 16:05:06 +09:00
parent 930424b752
commit d12a5d8095
15 changed files with 4344 additions and 74 deletions

View File

@@ -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;
}

View File

@@ -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)