feat: harden isolated Tornado test workflow

This commit is contained in:
2026-07-10 11:27:40 +09:00
parent 5a8c7028dc
commit fc932b27f6
36 changed files with 3919 additions and 226 deletions

View File

@@ -4,14 +4,29 @@ using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Playout;
using MBN_STOCK_WEBVIEW.Playout.Configuration;
using MBN_STOCK_WEBVIEW.Playout.Registration;
using MBN_STOCK_WEBVIEW.PlayoutSmoke;
var command = args.Length == 0 ? "--probe" : args[0].ToLowerInvariant();
return command switch
var parsed = SmokeCommandLine.Parse(args);
if (!parsed.IsValid)
{
"--probe" => Probe(),
"--dry-run" => await DryRunAsync(),
_ => Usage()
return Usage(parsed.ErrorCode);
}
using var cancellation = new CancellationTokenSource();
Console.CancelKeyPress += (_, eventArgs) =>
{
eventArgs.Cancel = true;
cancellation.Cancel();
};
return parsed.Invocation.Kind switch
{
SmokeCommandKind.Probe => Probe(),
SmokeCommandKind.DryRun => await DryRunAsync(),
SmokeCommandKind.TestPlan => TestPlan(parsed.Invocation),
SmokeCommandKind.TestConnect or SmokeCommandKind.TestSequence =>
await RunIsolatedTestCommandAsync(parsed.Invocation, cancellation.Token),
_ => Usage("unknown-command")
};
static int Probe()
@@ -42,8 +57,7 @@ static int Probe()
tornadoProcessCount++;
var title = process.MainWindowTitle;
if (title.Contains("PGM", StringComparison.OrdinalIgnoreCase) ||
title.Contains("PROGRAM", StringComparison.OrdinalIgnoreCase))
if (SmokeCommandLine.IsUnsafeTornadoWindowTitle(title))
{
programWindowDetected = true;
}
@@ -58,9 +72,12 @@ static int Probe()
}
}
Console.WriteLine(JsonSerializer.Serialize(new
WriteJson(new
{
command = "probe",
architecture = Environment.Is64BitProcess ? "x64" : "x86",
connectRequestIssued = false,
comActivationAttempted = false,
runtimeRegistration = new
{
ready = runtimeRegistration.IsReady,
@@ -76,9 +93,9 @@ static int Probe()
programWindowDetected
},
safety = programWindowDetected
? "PROGRAM window detected; no COM activation was attempted."
? "PROGRAM or ambiguous Tornado window detected; no COM activation was attempted."
: "Read-only probe complete; no COM activation was attempted."
}, new JsonSerializerOptions { WriteIndented = true }));
});
return runtimeRegistration.IsReady && Environment.Is64BitProcess
? 0
@@ -101,24 +118,124 @@ static async Task<int> DryRunAsync()
await engine.DisconnectAsync()
};
Console.WriteLine(JsonSerializer.Serialize(new
WriteJson(new
{
command = "dry-run",
mode = engine.Status.Mode.ToString(),
state = engine.Status.State.ToString(),
connectRequestIssued = false,
comActivationAttempted = false,
commands = results.Select(result => new
{
operation = result.Operation.ToString(),
code = result.Code.ToString(),
message = result.Message
})
}, new JsonSerializerOptions { WriteIndented = true }));
});
return results.All(result => result.IsSuccess) ? 0 : 3;
}
static int Usage()
static int TestPlan(SmokeCommandInvocation invocation)
{
Console.Error.WriteLine("Usage: MBN_STOCK_WEBVIEW.PlayoutSmoke [--probe|--dry-run]");
Console.Error.WriteLine("This tool never enables Test or Live mode and never activates K3D COM in dry-run.");
try
{
var plan = IsolatedTestCommandPreflight.Create(invocation);
WriteJson(new
{
command = "test-plan",
mode = "Test",
preflightValidated = true,
runtimeProcessGateChecked = false,
connectRequestIssued = false,
comActivationAttempted = false,
sceneCount = plan.PrepareCue is null ? 0 : 2,
automaticReconnectEnabled = plan.Options.ReconnectEnabled,
safety = "Configuration and scene assets validated only; no engine or COM was created."
});
return 0;
}
catch (IsolatedTestPreflightException exception)
{
WritePreflightFailure("test-plan", exception.ErrorCode);
return 65;
}
catch
{
WritePreflightFailure("test-plan", "internal-preflight-failure");
return 70;
}
}
static async Task<int> RunIsolatedTestCommandAsync(
SmokeCommandInvocation invocation,
CancellationToken cancellationToken)
{
var command = invocation.Kind == SmokeCommandKind.TestConnect
? "test-connect"
: "test-sequence";
IsolatedTestCommandPlan plan;
try
{
plan = IsolatedTestCommandPreflight.Create(invocation);
}
catch (IsolatedTestPreflightException exception)
{
WritePreflightFailure(command, exception.ErrorCode);
return 65;
}
catch
{
WritePreflightFailure(command, "internal-preflight-failure");
return 70;
}
var report = await IsolatedTestCommandExecutor.ExecuteAsync(plan, cancellationToken);
WriteJson(report);
if (report.Completed && !report.OutcomeUnknown)
{
return 0;
}
return report.OutcomeUnknown ? 5 : 4;
}
static void WritePreflightFailure(string command, string errorCode) =>
WriteJson(new
{
command,
mode = "Test",
preflightValidated = false,
runtimeProcessGateChecked = false,
connectRequestIssued = false,
comActivationAttempted = false,
errorCode,
safety = "Rejected before engine creation; no COM activation was attempted."
});
static void WriteJson<T>(T value) =>
Console.WriteLine(JsonSerializer.Serialize(value, new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
}));
static int Usage(string? errorCode)
{
WriteJson(new
{
command = "invalid",
connectRequestIssued = false,
comActivationAttempted = false,
errorCode = errorCode ?? "invalid-command-line"
});
Console.Error.WriteLine(
"Usage: MBN_STOCK_WEBVIEW.PlayoutSmoke [--probe|--dry-run|--test-plan|--test-connect|--test-sequence]");
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.");
return 64;
}