feat: verify Tornado PGM KTAP connection
This commit is contained in:
@@ -333,6 +333,15 @@ internal static class IsolatedTestCommandExecutor
|
||||
}
|
||||
|
||||
outputMayBeActive = false;
|
||||
// K3D output methods complete asynchronously through event callbacks.
|
||||
// The current adapter does not consume those callbacks yet, so keep
|
||||
// the connection alive for the same bounded observation window after
|
||||
// TAKE OUT before requesting Disconnect.
|
||||
if (!await ObserveAsync().ConfigureAwait(false))
|
||||
{
|
||||
stoppedAfter = "TakeOutObservation";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
disconnectAttempted = true;
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Text.Json;
|
||||
using MBN_STOCK_WEBVIEW.Core.Playout;
|
||||
using MBN_STOCK_WEBVIEW.Playout;
|
||||
using MBN_STOCK_WEBVIEW.Playout.Configuration;
|
||||
using MBN_STOCK_WEBVIEW.Playout.Diagnostics;
|
||||
using MBN_STOCK_WEBVIEW.Playout.Registration;
|
||||
using MBN_STOCK_WEBVIEW.PlayoutSmoke;
|
||||
|
||||
@@ -26,6 +27,8 @@ return parsed.Invocation.Kind switch
|
||||
SmokeCommandKind.TestPlan => TestPlan(parsed.Invocation),
|
||||
SmokeCommandKind.TestConnect or SmokeCommandKind.TestSequence =>
|
||||
await RunIsolatedTestCommandAsync(parsed.Invocation, cancellation.Token),
|
||||
SmokeCommandKind.PgmConnectDiagnostic =>
|
||||
await RunPgmConnectDiagnosticAsync(parsed.Invocation, cancellation.Token),
|
||||
_ => Usage("unknown-command")
|
||||
};
|
||||
|
||||
@@ -221,6 +224,104 @@ static async Task<int> RunIsolatedTestCommandAsync(
|
||||
return report.OutcomeUnknown ? 5 : 4;
|
||||
}
|
||||
|
||||
static async Task<int> RunPgmConnectDiagnosticAsync(
|
||||
SmokeCommandInvocation invocation,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (IsolatedTestCommandPreflight.HasPlayoutEnvironmentOverride(
|
||||
Environment.GetEnvironmentVariables()))
|
||||
{
|
||||
WritePgmDiagnosticFailure("environment-override-present");
|
||||
return 65;
|
||||
}
|
||||
|
||||
var request = new PgmConnectDiagnosticRequest(
|
||||
invocation.Host ?? string.Empty,
|
||||
invocation.Port ?? 0,
|
||||
invocation.ExpectedPgmWindowTitle ?? string.Empty);
|
||||
PgmConnectDiagnosticReport report;
|
||||
try
|
||||
{
|
||||
report = await new PgmConnectDiagnosticRunner()
|
||||
.ExecuteAsync(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
WritePgmDiagnosticFailure("internal-diagnostic-failure");
|
||||
return 70;
|
||||
}
|
||||
|
||||
WriteJson(new
|
||||
{
|
||||
command = "pgm-connect-diagnostic",
|
||||
report.RequestValidated,
|
||||
report.ProcessGatePassed,
|
||||
report.ListenerOwnershipConfirmed,
|
||||
report.RegistrationReady,
|
||||
report.ConnectRequestIssued,
|
||||
report.ComActivationAttempted,
|
||||
lastKtapConnectState = ToWireValue(report.LastKtapConnectState),
|
||||
report.KtapConnectAttempted,
|
||||
report.KtapConnectAccepted,
|
||||
report.KtapHelloObserved,
|
||||
report.NetworkMonitoringRecordExpected,
|
||||
report.NetworkMonitoringCheckRequired,
|
||||
report.NetworkMonitoringVerified,
|
||||
report.DisconnectAttempted,
|
||||
report.DisconnectCode,
|
||||
report.Completed,
|
||||
report.OutcomeUnknown,
|
||||
report.RenderCommandSurfaceExposed,
|
||||
report.RenderCommandAttempted,
|
||||
report.AutomaticReconnectEnabled,
|
||||
report.ErrorCode
|
||||
});
|
||||
|
||||
if (report.Completed && !report.OutcomeUnknown)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return report.OutcomeUnknown ? 5 : 4;
|
||||
}
|
||||
|
||||
static void WritePgmDiagnosticFailure(string errorCode) =>
|
||||
WriteJson(new
|
||||
{
|
||||
command = "pgm-connect-diagnostic",
|
||||
requestValidated = false,
|
||||
processGatePassed = false,
|
||||
listenerOwnershipConfirmed = false,
|
||||
registrationReady = false,
|
||||
connectRequestIssued = false,
|
||||
comActivationAttempted = false,
|
||||
lastKtapConnectState = "not-attempted",
|
||||
ktapConnectAttempted = false,
|
||||
ktapConnectAccepted = false,
|
||||
ktapHelloObserved = (bool?)null,
|
||||
networkMonitoringRecordExpected = false,
|
||||
networkMonitoringCheckRequired = false,
|
||||
networkMonitoringVerified = (bool?)null,
|
||||
disconnectAttempted = false,
|
||||
disconnectCode = "not-attempted",
|
||||
completed = false,
|
||||
outcomeUnknown = false,
|
||||
renderCommandSurfaceExposed = false,
|
||||
renderCommandAttempted = false,
|
||||
automaticReconnectEnabled = false,
|
||||
errorCode
|
||||
});
|
||||
|
||||
static string ToWireValue(PlayoutKtapConnectState state) => state switch
|
||||
{
|
||||
PlayoutKtapConnectState.NotAttempted => "not-attempted",
|
||||
PlayoutKtapConnectState.Attempted => "attempted",
|
||||
PlayoutKtapConnectState.AcceptedUnconfirmed => "accepted-unconfirmed",
|
||||
PlayoutKtapConnectState.Failed => "failed",
|
||||
_ => "not-attempted"
|
||||
};
|
||||
|
||||
static void WritePreflightFailure(string command, string errorCode) =>
|
||||
WriteJson(new
|
||||
{
|
||||
@@ -265,12 +366,14 @@ 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]");
|
||||
"Usage: MBN_STOCK_WEBVIEW.PlayoutSmoke [--probe|--dry-run|--test-plan|--test-connect|--test-sequence|--pgm-connect-diagnostic]");
|
||||
Console.Error.WriteLine(
|
||||
$"Test commands require {SmokeCommandLine.AcknowledgementFlag} and --config <absolute-path>.");
|
||||
Console.Error.WriteLine(
|
||||
"--test-plan and --test-sequence also require --prepare <scene-code> --next <scene-code> --observe-ms <1000..30000>.");
|
||||
Console.Error.WriteLine(
|
||||
"Only --test-connect and --test-sequence may request COM, after all Test safety gates pass.");
|
||||
"--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.");
|
||||
return 64;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ internal enum SmokeCommandKind
|
||||
DryRun,
|
||||
TestPlan,
|
||||
TestConnect,
|
||||
TestSequence
|
||||
TestSequence,
|
||||
PgmConnectDiagnostic
|
||||
}
|
||||
|
||||
internal sealed record SmokeCommandInvocation(
|
||||
@@ -15,7 +16,10 @@ internal sealed record SmokeCommandInvocation(
|
||||
string? ConfigPath = null,
|
||||
string? PrepareSceneCode = null,
|
||||
string? NextSceneCode = null,
|
||||
int? ObservationMilliseconds = null);
|
||||
int? ObservationMilliseconds = null,
|
||||
string? Host = null,
|
||||
int? Port = null,
|
||||
string? ExpectedPgmWindowTitle = null);
|
||||
|
||||
internal sealed record SmokeCommandParseResult(
|
||||
bool IsValid,
|
||||
@@ -26,6 +30,8 @@ internal static class SmokeCommandLine
|
||||
{
|
||||
internal const string AcknowledgementFlag =
|
||||
"--acknowledge-isolated-non-program-test-output";
|
||||
internal const string PgmConnectAcknowledgementFlag =
|
||||
"--i-understand-this-will-contact-current-pgm-tornado-via-ktap-connect-and-disconnect-only";
|
||||
|
||||
public static SmokeCommandParseResult Parse(IReadOnlyList<string> arguments)
|
||||
{
|
||||
@@ -50,6 +56,7 @@ internal static class SmokeCommandLine
|
||||
"--test-plan" => SmokeCommandKind.TestPlan,
|
||||
"--test-connect" => SmokeCommandKind.TestConnect,
|
||||
"--test-sequence" => SmokeCommandKind.TestSequence,
|
||||
"--pgm-connect-diagnostic" => SmokeCommandKind.PgmConnectDiagnostic,
|
||||
_ => SmokeCommandKind.Invalid
|
||||
};
|
||||
if (kind == SmokeCommandKind.Invalid)
|
||||
@@ -61,6 +68,9 @@ internal static class SmokeCommandLine
|
||||
string? prepareSceneCode = null;
|
||||
string? nextSceneCode = null;
|
||||
int? observationMilliseconds = null;
|
||||
string? host = null;
|
||||
int? port = null;
|
||||
string? expectedPgmWindowTitle = null;
|
||||
var acknowledged = false;
|
||||
|
||||
for (var index = 1; index < arguments.Count; index++)
|
||||
@@ -68,7 +78,8 @@ internal static class SmokeCommandLine
|
||||
var argument = arguments[index];
|
||||
switch (argument)
|
||||
{
|
||||
case AcknowledgementFlag:
|
||||
case AcknowledgementFlag when kind != SmokeCommandKind.PgmConnectDiagnostic:
|
||||
case PgmConnectAcknowledgementFlag when kind == SmokeCommandKind.PgmConnectDiagnostic:
|
||||
if (acknowledged)
|
||||
{
|
||||
return Invalid("duplicate-option");
|
||||
@@ -77,7 +88,54 @@ internal static class SmokeCommandLine
|
||||
acknowledged = true;
|
||||
break;
|
||||
|
||||
case "--config":
|
||||
case "--host" when kind == SmokeCommandKind.PgmConnectDiagnostic:
|
||||
if (host is not null)
|
||||
{
|
||||
return Invalid("duplicate-option");
|
||||
}
|
||||
|
||||
if (!TryReadValue(arguments, ref index, out host))
|
||||
{
|
||||
return Invalid("missing-option-value");
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "--port" when kind == SmokeCommandKind.PgmConnectDiagnostic:
|
||||
if (port is not null)
|
||||
{
|
||||
return Invalid("duplicate-option");
|
||||
}
|
||||
|
||||
if (!TryReadValue(arguments, ref index, out var portText) ||
|
||||
!int.TryParse(
|
||||
portText,
|
||||
System.Globalization.NumberStyles.None,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out var parsedPort) ||
|
||||
parsedPort is < 1 or > 65_535)
|
||||
{
|
||||
return Invalid("invalid-port");
|
||||
}
|
||||
|
||||
port = parsedPort;
|
||||
break;
|
||||
|
||||
case "--expected-pgm-window-title"
|
||||
when kind == SmokeCommandKind.PgmConnectDiagnostic:
|
||||
if (expectedPgmWindowTitle is not null)
|
||||
{
|
||||
return Invalid("duplicate-option");
|
||||
}
|
||||
|
||||
if (!TryReadValue(arguments, ref index, out expectedPgmWindowTitle))
|
||||
{
|
||||
return Invalid("missing-option-value");
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "--config" when kind != SmokeCommandKind.PgmConnectDiagnostic:
|
||||
if (configPath is not null)
|
||||
{
|
||||
return Invalid("duplicate-option");
|
||||
@@ -146,6 +204,22 @@ internal static class SmokeCommandLine
|
||||
return Invalid("missing-acknowledgement");
|
||||
}
|
||||
|
||||
if (kind == SmokeCommandKind.PgmConnectDiagnostic)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(host) ||
|
||||
port is null ||
|
||||
string.IsNullOrWhiteSpace(expectedPgmWindowTitle))
|
||||
{
|
||||
return Invalid("missing-required-option");
|
||||
}
|
||||
|
||||
return Valid(new SmokeCommandInvocation(
|
||||
kind,
|
||||
Host: host,
|
||||
Port: port,
|
||||
ExpectedPgmWindowTitle: expectedPgmWindowTitle));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(configPath))
|
||||
{
|
||||
return Invalid("missing-required-option");
|
||||
@@ -177,7 +251,10 @@ internal static class SmokeCommandLine
|
||||
configPath,
|
||||
prepareSceneCode,
|
||||
nextSceneCode,
|
||||
observationMilliseconds));
|
||||
observationMilliseconds,
|
||||
host,
|
||||
port,
|
||||
expectedPgmWindowTitle));
|
||||
}
|
||||
|
||||
internal static bool IsSafeSceneCode(string value)
|
||||
|
||||
Reference in New Issue
Block a user