Files

96 lines
3.7 KiB
C#

using System.IO;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using Backend;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;
namespace Backend.Tests;
public class EndToEndSmokeTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public EndToEndSmokeTests(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(b =>
{
// Prevent MVC picking up assemblies; force env vars if needed.
b.UseEnvironment("Development");
});
}
[Fact]
public async Task WebSocket_ReceivesInitAndStateFrames()
{
// Prerequisites: publish/GameCli/GameCli and models/ppo_lander.onnx must exist.
var repoRoot = FindRepoRoot();
Assert.True(File.Exists(Path.Combine(repoRoot, "publish", "GameCli", "GameCli")),
"run `dotnet publish GameCli -c Release -o publish/GameCli` before this test");
Assert.True(File.Exists(Path.Combine(repoRoot, "models", "ppo_lander.onnx")),
"run `python Training/export_onnx.py ...` before this test");
var client = _factory.Server.CreateWebSocketClient();
var baseUri = _factory.Server.BaseAddress;
var wsUri = new UriBuilder(baseUri) { Scheme = "ws", Path = "/ws/game" }.Uri;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
using var ws = await client.ConnectAsync(wsUri, cts.Token);
var initText = await ReceiveTextAsync(ws, cts.Token);
using var initDoc = JsonDocument.Parse(initText);
Assert.Equal("init", initDoc.RootElement.GetProperty("type").GetString());
// Send a cursor and receive at least one state frame.
var cursorPayload = """{"type":"cursor","x":0.3,"y":0.7}""";
await ws.SendAsync(Encoding.UTF8.GetBytes(cursorPayload),
WebSocketMessageType.Text, true, cts.Token);
// Read up to N frames looking for a state frame; the ticker runs at 50 Hz.
for (int i = 0; i < 100; i++)
{
var text = await ReceiveTextAsync(ws, cts.Token);
using var doc = JsonDocument.Parse(text);
if (doc.RootElement.GetProperty("type").GetString() == "state")
{
Assert.True(doc.RootElement.TryGetProperty("x", out _));
Assert.True(doc.RootElement.TryGetProperty("y", out _));
Assert.True(doc.RootElement.TryGetProperty("angle", out _));
Assert.True(doc.RootElement.TryGetProperty("engine", out _));
Assert.True(doc.RootElement.TryGetProperty("target", out _));
Assert.True(doc.RootElement.TryGetProperty("step", out _));
return;
}
}
Assert.Fail("did not receive a state frame in 100 messages");
}
private static async Task<string> ReceiveTextAsync(WebSocket ws, CancellationToken ct)
{
var buffer = new byte[16 * 1024];
var sb = new StringBuilder();
WebSocketReceiveResult result;
do
{
result = await ws.ReceiveAsync(buffer, ct);
sb.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));
} while (!result.EndOfMessage);
return sb.ToString();
}
private static string FindRepoRoot()
{
var dir = AppContext.BaseDirectory;
while (dir is not null)
{
if (File.Exists(Path.Combine(dir, "GameCli.sln"))) return dir;
var parent = Directory.GetParent(dir);
if (parent is null) break;
dir = parent.FullName;
}
throw new InvalidOperationException("repo root not found");
}
}