diff --git a/GameCli.Tests/ProgramSmokeTests.cs b/GameCli.Tests/ProgramSmokeTests.cs new file mode 100644 index 0000000..a3f6a2e --- /dev/null +++ b/GameCli.Tests/ProgramSmokeTests.cs @@ -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//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()}"); + } +}