test(gamecli): end-to-end smoke tests exercising the built binary

This commit is contained in:
meelstorm
2026-07-17 16:52:57 +00:00
committed by EugeneTes
parent 6340218f88
commit 0b22d67316

View File

@@ -0,0 +1,139 @@
using System.Diagnostics;
using System.Text.Json;
using GameCli;
using Xunit;
namespace GameCli.Tests;
public class ProgramSmokeTests
{
private static string GetProjectPath()
{
// Locate the GameCli binary relative to the test binary's output dir.
// Tests run from GameCli.Tests/bin/<Config>/net8.0/, so ../../../.. gets to repo root.
var testDir = AppContext.BaseDirectory;
var repoRoot = Path.GetFullPath(Path.Combine(testDir, "..", "..", "..", ".."));
var projectPath = Path.Combine(repoRoot, "GameCli", "GameCli.csproj");
Assert.True(File.Exists(projectPath),
$"could not find GameCli.csproj at {projectPath}");
return projectPath;
}
private static Process StartGameCli()
{
var projectPath = GetProjectPath();
var psi = new ProcessStartInfo
{
FileName = "dotnet",
ArgumentList = { "run", "--project", projectPath, "-c", "Release", "--no-build" },
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
};
var p = Process.Start(psi)
?? throw new InvalidOperationException("failed to start GameCli");
return p;
}
[Fact]
public void FirstOutputLine_IsInitHandshake()
{
// Ensure release build exists so --no-build works reliably.
RunOrFail("dotnet", $"build -c Release \"{GetProjectPath()}\"");
using var p = StartGameCli();
try
{
var line = p.StandardOutput.ReadLine();
Assert.NotNull(line);
var init = JsonSerializer.Deserialize(line!, ProtocolJsonContext.Default.InitMessage);
Assert.NotNull(init?.Init);
Assert.Equal(Physics.Dt, init!.Init.Dt);
Assert.Equal(Observation.Dim, init.Init.ObsDim);
Assert.Equal(4, init.Init.NActions);
}
finally
{
p.StandardInput.Close();
p.WaitForExit(3000);
}
}
[Fact]
public void StepRoundTrip_ReturnsSevenDimObservation()
{
RunOrFail("dotnet", $"build -c Release \"{GetProjectPath()}\"");
using var p = StartGameCli();
try
{
_ = p.StandardOutput.ReadLine(); // skip init
p.StandardInput.WriteLine("""{"action":0,"target":[0.5,0.5]}""");
p.StandardInput.Flush();
var line = p.StandardOutput.ReadLine();
Assert.NotNull(line);
var output = JsonSerializer.Deserialize(line!, ProtocolJsonContext.Default.StepOutput);
Assert.NotNull(output);
Assert.Equal(7, output!.Obs.Length);
Assert.Equal(1, output.Step);
}
finally
{
p.StandardInput.Close();
p.WaitForExit(3000);
}
}
[Fact]
public void ResetCommand_ResetsStepCounter()
{
RunOrFail("dotnet", $"build -c Release \"{GetProjectPath()}\"");
using var p = StartGameCli();
try
{
_ = p.StandardOutput.ReadLine(); // init
// Take three steps.
for (int i = 0; i < 3; i++)
{
p.StandardInput.WriteLine("""{"action":0,"target":[0.5,0.5]}""");
}
p.StandardInput.Flush();
string? last = null;
for (int i = 0; i < 3; i++) last = p.StandardOutput.ReadLine();
var afterThree = JsonSerializer.Deserialize(last!, ProtocolJsonContext.Default.StepOutput);
Assert.Equal(3, afterThree!.Step);
// Reset. Expect step=0 on the next output line.
p.StandardInput.WriteLine("""{"cmd":"reset","seed":42}""");
p.StandardInput.Flush();
var afterReset = JsonSerializer.Deserialize(
p.StandardOutput.ReadLine()!, ProtocolJsonContext.Default.StepOutput);
Assert.Equal(0, afterReset!.Step);
}
finally
{
p.StandardInput.Close();
p.WaitForExit(3000);
}
}
private static void RunOrFail(string file, string args)
{
var psi = new ProcessStartInfo(file, args)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
};
using var p = Process.Start(psi)!;
p.WaitForExit(120_000);
if (p.ExitCode != 0)
throw new Exception($"{file} {args} exited {p.ExitCode}\nstderr:\n{p.StandardError.ReadToEnd()}");
}
}