Files
MBN_STOCK_WEBVIEW/tools/MBN_STOCK_WEBVIEW.PlayoutSmoke/SmokeCommandLine.cs

403 lines
14 KiB
C#

namespace MBN_STOCK_WEBVIEW.PlayoutSmoke;
internal enum SmokeCommandKind
{
Invalid,
Probe,
DryRun,
TestPlan,
TestConnect,
TestSequence,
PgmConnectDiagnostic,
PgmCutsSequence
}
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,
string? SceneRoot = 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";
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)
{
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,
"--pgm-cuts-sequence" => SmokeCommandKind.PgmCutsSequence,
_ => 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;
string? sceneRoot = null;
var acknowledged = false;
for (var index = 1; index < arguments.Count; index++)
{
var argument = arguments[index];
switch (argument)
{
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");
}
acknowledged = true;
break;
case "--host" when kind is
SmokeCommandKind.PgmConnectDiagnostic or SmokeCommandKind.PgmCutsSequence:
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 is
SmokeCommandKind.PgmConnectDiagnostic or SmokeCommandKind.PgmCutsSequence:
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 is
SmokeCommandKind.PgmConnectDiagnostic or SmokeCommandKind.PgmCutsSequence:
if (expectedPgmWindowTitle is not null)
{
return Invalid("duplicate-option");
}
if (!TryReadValue(arguments, ref index, out expectedPgmWindowTitle))
{
return Invalid("missing-option-value");
}
break;
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");
}
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;
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");
}
}
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 (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");
}
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,
sceneRoot));
}
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);
}