using MrGameEng.Core; using Xunit; namespace MrGameEng.Core.Tests; public class HeadlessHostTests { private sealed class CountingScene : Scene { public int LoadCount; public int UnloadCount; public int UpdateCount; protected override void OnLoad() => LoadCount++; protected override void OnUnload() => UnloadCount++; public override void Update(GameClock clock) { UpdateCount++; base.Update(clock); } } [Fact] public void RunTicks_AdvancesClock_ByExactFixedStep() { var scene = new CountingScene(); using var host = new HeadlessHost(new HeadlessHostOptions { TicksPerSecond = 10f }, scene); host.RunTicks(30); Assert.Equal(0.1f, host.FixedDeltaTime, 3); Assert.Equal(30, host.TickCount); Assert.Equal(3.0, host.Context.Clock.TotalTime, 3); Assert.Equal(30, scene.UpdateCount); } [Fact] public void FirstTick_LoadsTheInitialScene() { var scene = new CountingScene(); using var host = new HeadlessHost(new HeadlessHostOptions(), scene); Assert.Equal(0, scene.LoadCount); host.Tick(); Assert.Equal(1, scene.LoadCount); Assert.Same(scene, host.Context.Scenes.Current); } [Fact] public void Dispose_UnloadsTheActiveScene() { var scene = new CountingScene(); var host = new HeadlessHost(new HeadlessHostOptions(), scene); host.Tick(); host.Dispose(); Assert.Equal(1, scene.UnloadCount); Assert.Null(host.Context.Scenes.Current); } [Fact] public void Run_StopsWhenCancelled() { using var cancellation = new CancellationTokenSource(); var scene = new CancellingScene(cancellation, afterTicks: 5); using var host = new HeadlessHost(new HeadlessHostOptions { Realtime = false }, scene); host.Run(cancellation.Token); Assert.Equal(5, scene.UpdateCount); } [Fact] public void TimeScale_StillApplies_OnTopOfFixedStep() { var scene = new CountingScene(); using var host = new HeadlessHost(new HeadlessHostOptions { TicksPerSecond = 10f }, scene); host.Context.Clock.TimeScale = 3f; host.RunTicks(10); Assert.Equal(3.0, host.Context.Clock.TotalTime, 3); Assert.Equal(1.0, host.Context.Clock.UnscaledTotalTime, 3); } [Fact] public void NonPositiveTickRate_Throws() { Assert.Throws(() => new HeadlessHost(new HeadlessHostOptions { TicksPerSecond = 0f }, new CountingScene()) ); } private sealed class CancellingScene(CancellationTokenSource cancellation, int afterTicks) : Scene { public int UpdateCount; protected override void OnLoad() { } public override void Update(GameClock clock) { UpdateCount++; if (UpdateCount >= afterTicks) { cancellation.Cancel(); } base.Update(clock); } } }