독립 검증 도구 추가와 앱 상태 책임 분리
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
using System.Text.Json;
|
||||
using ProjectQuint.Probes;
|
||||
|
||||
var json = new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||
try
|
||||
{
|
||||
var action = args.Length == 0 ? "list" : args[0];
|
||||
var options = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
string? output = null;
|
||||
for (var index = 1; index < args.Length; index += 2)
|
||||
{
|
||||
if (index + 1 >= args.Length)
|
||||
throw new ArgumentException("Every option requires a value.");
|
||||
if (args[index] == "--output" && output == null)
|
||||
output = args[index + 1];
|
||||
else if (args[index] is "--notes" or "--seed" or "--fps" or "--iterations" &&
|
||||
int.TryParse(args[index + 1], out var value) && options.TryAdd(args[index], value))
|
||||
continue;
|
||||
else
|
||||
throw new ArgumentException("Unknown, duplicated or invalid option.");
|
||||
}
|
||||
var notes = options.GetValueOrDefault("--notes", 10_000);
|
||||
var seed = options.GetValueOrDefault("--seed", 42);
|
||||
var fps = options.GetValueOrDefault("--fps", 240);
|
||||
var iterations = options.GetValueOrDefault("--iterations", 20);
|
||||
if (seed < 0)
|
||||
throw new ArgumentException("Seed must be nonnegative.");
|
||||
object report;
|
||||
switch (action)
|
||||
{
|
||||
case "list":
|
||||
if (options.Count != 0 || output != null)
|
||||
throw new ArgumentException("The list command accepts no options.");
|
||||
report = new
|
||||
{
|
||||
schemaVersion = 1,
|
||||
component = "headless-core",
|
||||
commands = new[] { "list", "replay", "benchmark", "test (wrapper)" },
|
||||
input = "seeded synthetic four-lane notes; no music or external library",
|
||||
options = new[] { "--notes", "--seed", "--fps", "--iterations (benchmark)", "--output" },
|
||||
};
|
||||
break;
|
||||
case "replay":
|
||||
if (options.ContainsKey("--iterations"))
|
||||
throw new ArgumentException("Iterations applies only to benchmarks.");
|
||||
var workload = SyntheticWorkload.Create(notes, (uint)seed);
|
||||
report = new
|
||||
{
|
||||
schemaVersion = 1, complete = true, notes, seed, frameRate = fps,
|
||||
eventCount = workload.Events.Length,
|
||||
result = ReplayRunner.Run(workload, fps),
|
||||
};
|
||||
break;
|
||||
case "benchmark":
|
||||
report = CoreBenchmark.Run(notes, (uint)seed, fps, iterations, checkpoint =>
|
||||
{
|
||||
Console.Error.WriteLine($"benchmark {checkpoint.Samples.Length}/{iterations}");
|
||||
if (output != null)
|
||||
Save(output, checkpoint);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unknown command. Use list, replay, benchmark or test.");
|
||||
}
|
||||
if (output != null)
|
||||
Save(output, report);
|
||||
Console.WriteLine(JsonSerializer.Serialize(report, json));
|
||||
return 0;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
// Paths in I/O exceptions can identify the local machine; keep structured failures generic.
|
||||
Console.Error.WriteLine(JsonSerializer.Serialize(new
|
||||
{
|
||||
error = error.GetType().Name,
|
||||
detail = error is ArgumentException ? error.Message : "The probe failed; no complete result was produced.",
|
||||
}));
|
||||
return 1;
|
||||
}
|
||||
|
||||
void Save(string path, object report)
|
||||
{
|
||||
var destination = Path.GetFullPath(path);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
|
||||
var temporary = destination + ".tmp";
|
||||
File.WriteAllText(temporary, JsonSerializer.Serialize(report, json) + "\n");
|
||||
File.Move(temporary, destination, overwrite: true);
|
||||
}
|
||||
Reference in New Issue
Block a user