335 lines
11 KiB
C#
335 lines
11 KiB
C#
namespace MBN_STOCK_WEBVIEW.PlayoutSmoke;
|
|
|
|
internal enum SmokeCommandKind
|
|
{
|
|
Invalid,
|
|
Probe,
|
|
DryRun,
|
|
TestPlan,
|
|
TestConnect,
|
|
TestSequence,
|
|
PgmConnectDiagnostic
|
|
}
|
|
|
|
internal sealed record SmokeCommandInvocation(
|
|
SmokeCommandKind Kind,
|
|
string? ConfigPath = null,
|
|
string? PrepareSceneCode = null,
|
|
string? NextSceneCode = null,
|
|
int? ObservationMilliseconds = null,
|
|
string? Host = null,
|
|
int? Port = null,
|
|
string? ExpectedPgmWindowTitle = null);
|
|
|
|
internal sealed record SmokeCommandParseResult(
|
|
bool IsValid,
|
|
SmokeCommandInvocation Invocation,
|
|
string? ErrorCode = null);
|
|
|
|
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)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(arguments);
|
|
if (arguments.Count == 0)
|
|
{
|
|
return Valid(new SmokeCommandInvocation(SmokeCommandKind.Probe));
|
|
}
|
|
|
|
if (arguments.Count == 1)
|
|
{
|
|
return arguments[0].ToLowerInvariant() switch
|
|
{
|
|
"--probe" => Valid(new SmokeCommandInvocation(SmokeCommandKind.Probe)),
|
|
"--dry-run" => Valid(new SmokeCommandInvocation(SmokeCommandKind.DryRun)),
|
|
_ => Invalid("unknown-command")
|
|
};
|
|
}
|
|
|
|
var kind = arguments[0].ToLowerInvariant() switch
|
|
{
|
|
"--test-plan" => SmokeCommandKind.TestPlan,
|
|
"--test-connect" => SmokeCommandKind.TestConnect,
|
|
"--test-sequence" => SmokeCommandKind.TestSequence,
|
|
"--pgm-connect-diagnostic" => SmokeCommandKind.PgmConnectDiagnostic,
|
|
_ => SmokeCommandKind.Invalid
|
|
};
|
|
if (kind == SmokeCommandKind.Invalid)
|
|
{
|
|
return Invalid("unknown-command");
|
|
}
|
|
|
|
string? configPath = null;
|
|
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++)
|
|
{
|
|
var argument = arguments[index];
|
|
switch (argument)
|
|
{
|
|
case AcknowledgementFlag when kind != SmokeCommandKind.PgmConnectDiagnostic:
|
|
case PgmConnectAcknowledgementFlag when kind == SmokeCommandKind.PgmConnectDiagnostic:
|
|
if (acknowledged)
|
|
{
|
|
return Invalid("duplicate-option");
|
|
}
|
|
|
|
acknowledged = true;
|
|
break;
|
|
|
|
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");
|
|
}
|
|
|
|
if (!TryReadValue(arguments, ref index, out configPath))
|
|
{
|
|
return Invalid("missing-option-value");
|
|
}
|
|
|
|
break;
|
|
|
|
case "--prepare" when kind is SmokeCommandKind.TestPlan or SmokeCommandKind.TestSequence:
|
|
if (prepareSceneCode is not null)
|
|
{
|
|
return Invalid("duplicate-option");
|
|
}
|
|
|
|
if (!TryReadValue(arguments, ref index, out prepareSceneCode))
|
|
{
|
|
return Invalid("missing-option-value");
|
|
}
|
|
|
|
break;
|
|
|
|
case "--next" when kind is SmokeCommandKind.TestPlan or SmokeCommandKind.TestSequence:
|
|
if (nextSceneCode is not null)
|
|
{
|
|
return Invalid("duplicate-option");
|
|
}
|
|
|
|
if (!TryReadValue(arguments, ref index, out nextSceneCode))
|
|
{
|
|
return Invalid("missing-option-value");
|
|
}
|
|
|
|
break;
|
|
|
|
case "--observe-ms" when kind is SmokeCommandKind.TestPlan or SmokeCommandKind.TestSequence:
|
|
if (observationMilliseconds is not null)
|
|
{
|
|
return Invalid("duplicate-option");
|
|
}
|
|
|
|
if (!TryReadValue(arguments, ref index, out var observationText) ||
|
|
!int.TryParse(
|
|
observationText,
|
|
System.Globalization.NumberStyles.None,
|
|
System.Globalization.CultureInfo.InvariantCulture,
|
|
out var observationValue) ||
|
|
observationValue is < 1_000 or > 30_000)
|
|
{
|
|
return Invalid("invalid-observation-duration");
|
|
}
|
|
|
|
observationMilliseconds = observationValue;
|
|
break;
|
|
|
|
default:
|
|
return Invalid("unknown-option");
|
|
}
|
|
}
|
|
|
|
if (!acknowledged)
|
|
{
|
|
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");
|
|
}
|
|
|
|
if (kind is SmokeCommandKind.TestPlan or SmokeCommandKind.TestSequence)
|
|
{
|
|
if (prepareSceneCode is null || nextSceneCode is null || observationMilliseconds is null)
|
|
{
|
|
return Invalid("missing-required-option");
|
|
}
|
|
|
|
if (!IsSafeSceneCode(prepareSceneCode) || !IsSafeSceneCode(nextSceneCode))
|
|
{
|
|
return Invalid("unsafe-scene-code");
|
|
}
|
|
|
|
if (string.Equals(
|
|
prepareSceneCode,
|
|
nextSceneCode,
|
|
StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return Invalid("duplicate-scene-code");
|
|
}
|
|
}
|
|
|
|
return Valid(new SmokeCommandInvocation(
|
|
kind,
|
|
configPath,
|
|
prepareSceneCode,
|
|
nextSceneCode,
|
|
observationMilliseconds,
|
|
host,
|
|
port,
|
|
expectedPgmWindowTitle));
|
|
}
|
|
|
|
internal static bool IsSafeSceneCode(string value)
|
|
{
|
|
if (value.Length is < 1 or > 64)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
foreach (var character in value)
|
|
{
|
|
if (!((character >= 'A' && character <= 'Z') ||
|
|
(character >= 'a' && character <= 'z') ||
|
|
(character >= '0' && character <= '9') ||
|
|
character is '_' or '-'))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
internal static bool IsUnsafeTornadoWindowTitle(string? title)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(title) ||
|
|
title.Contains("PGM", StringComparison.OrdinalIgnoreCase) ||
|
|
title.Contains("PROGRAM", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
const string marker = "TEST";
|
|
for (var start = 0; start <= title.Length - marker.Length; start++)
|
|
{
|
|
if (!title.AsSpan(start, marker.Length).Equals(
|
|
marker.AsSpan(),
|
|
StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var beforeIsBoundary = start == 0 || !char.IsLetterOrDigit(title[start - 1]);
|
|
var after = start + marker.Length;
|
|
var afterIsBoundary = after == title.Length || !char.IsLetterOrDigit(title[after]);
|
|
if (beforeIsBoundary && afterIsBoundary)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool TryReadValue(
|
|
IReadOnlyList<string> arguments,
|
|
ref int index,
|
|
out string? value)
|
|
{
|
|
if (index + 1 >= arguments.Count ||
|
|
string.IsNullOrWhiteSpace(arguments[index + 1]) ||
|
|
arguments[index + 1].StartsWith("--", StringComparison.Ordinal))
|
|
{
|
|
value = null;
|
|
return false;
|
|
}
|
|
|
|
value = arguments[++index];
|
|
return true;
|
|
}
|
|
|
|
private static SmokeCommandParseResult Valid(SmokeCommandInvocation invocation) =>
|
|
new(true, invocation);
|
|
|
|
private static SmokeCommandParseResult Invalid(string errorCode) =>
|
|
new(false, new SmokeCommandInvocation(SmokeCommandKind.Invalid), errorCode);
|
|
}
|