using System.IO.Pipes; using System.Text.Json; using XFEEDToolBox.Core.Navigation; using XFEExtension.NetCore.XUnit.Attributes; namespace XFEEDToolBox.Test; [TestFixture] public class NativeGameInputChannelTests { private sealed class Fixture:IAsyncDisposable { private readonly CancellationTokenSource stop=new(); private readonly Task navigation,interaction; public readonly int Pid=Random.Shared.Next(300000000,400000000); public int Reads,Writes; public bool AlwaysOther,Verified=true,Stale; public Fixture(){navigation=Serve("Navigation");interaction=Serve("Interaction");} private async Task Serve(string service) { try { while(!stop.IsCancellationRequested) { await using var server=new NamedPipeServerStream($"XFEEDToolBox.{service}.{Pid}",PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous); await server.WaitForConnectionAsync(stop.Token);var buffer=new byte[4096];var count=await server.ReadAsync(buffer,stop.Token); using var request=JsonDocument.Parse(buffer.AsMemory(0,count));object result; if(service=="Navigation") result=new{entityId="42",localPlayerShipVerified=Verified,shipHealth=new{entityId="42",verified=Verified,observedAtUnixMilliseconds=DateTimeOffset.UtcNow.AddSeconds(Stale?-2:0).ToUnixTimeMilliseconds()}}; else if(request.RootElement.GetProperty("op").GetString()=="read") result=new{sequence=Interlocked.Increment(ref Reads),entityId=AlwaysOther||Reads<=2?"99":"42",observedAtUnixMilliseconds=DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),hardpoints=false,cargoScoop=false,landingGear=false,appliedGeneration=0}; else {Interlocked.Increment(ref Writes);result=new{generation=1,queued=true};} var bytes=JsonSerializer.SerializeToUtf8Bytes(new{ok=true,result});await server.WriteAsync(bytes,stop.Token);await server.WriteAsync(new byte[]{10},stop.Token);server.WaitForPipeDrain(); } } catch(OperationCanceledException)when(stop.IsCancellationRequested){} } public async ValueTask DisposeAsync(){stop.Cancel();await Task.WhenAll(navigation,interaction);stop.Dispose();} public NativeGameInputChannel Channel()=>new(Pid,new string('a',64),"unused"); } [Test]public async Task OtherShipSamplesAreSkippedUntilTheVerifiedMainShipIsObserved() { await using var fixture=new Fixture();var value=await fixture.Channel().ReadAsync(CancellationToken.None); Check.Equal(42UL,value.EntityId);Check.That(fixture.Reads>=3);Check.Equal(0,fixture.Writes); } [Test]public async Task PersistentOtherShipCannotProduceAPlayerInputFrame() { await using var fixture=new Fixture{AlwaysOther=true}; try{await fixture.Channel().ReadAsync(CancellationToken.None);throw new Exception("Expected unavailable telemetry");} catch(FlightTelemetryUnavailableException){} Check.Equal(0,fixture.Writes); } [Test]public async Task UnverifiedOrStaleMainShipBlocksBothObservationAndWrites() { foreach(var stale in new[]{false,true}) { await using var fixture=new Fixture{Verified=stale,Stale=stale};var channel=fixture.Channel(); try{await channel.ReadAsync(CancellationToken.None);throw new Exception("Expected unavailable telemetry");}catch(FlightTelemetryUnavailableException){} try{await channel.SetAsync(new(1,42,DateTimeOffset.UtcNow,false,false,false),"landingGear",false,CancellationToken.None);throw new Exception("Expected unavailable telemetry");}catch(FlightTelemetryUnavailableException){} Check.Equal(0,fixture.Reads);Check.Equal(0,fixture.Writes); } } [Test]public async Task ChangedMainShipBlocksAnOldInputCommandBeforeItsPipeWrite() { await using var fixture=new Fixture(); try{await fixture.Channel().SetAsync(new(1,99,DateTimeOffset.UtcNow,false,false,false),"landingGear",false,CancellationToken.None);throw new Exception("Expected ship rejection");} catch(InvalidOperationException){} Check.Equal(0,fixture.Writes); } }