using FluentAssertions; using Xunit; namespace OECS.Tests; public class SingletonTests { private struct GameConfig { public float Gravity; public int MaxPlayers; } private struct TimeState { public float Elapsed; } [Fact] public void SetSingleton_GetSingleton_RoundTrips() { var world = new World(); world.SetSingleton(new GameConfig { Gravity = 9.8f, MaxPlayers = 4 }); ref var config = ref world.GetSingleton(); config.Gravity.Should().Be(9.8f); config.MaxPlayers.Should().Be(4); } [Fact] public void SetSingleton_ReplacesExisting() { var world = new World(); world.SetSingleton(new GameConfig { Gravity = 9.8f, MaxPlayers = 4 }); world.SetSingleton(new GameConfig { Gravity = 1.6f, MaxPlayers = 2 }); ref var config = ref world.GetSingleton(); config.Gravity.Should().Be(1.6f); config.MaxPlayers.Should().Be(2); } [Fact] public void HasSingleton_ReturnsCorrectly() { var world = new World(); world.HasSingleton().Should().BeFalse(); world.SetSingleton(new GameConfig()); world.HasSingleton().Should().BeTrue(); } [Fact] public void RemoveSingleton_RemovesIt() { var world = new World(); world.SetSingleton(new GameConfig { Gravity = 9.8f, MaxPlayers = 4 }); world.HasSingleton().Should().BeTrue(); world.RemoveSingleton(); world.HasSingleton().Should().BeFalse(); } [Fact] public void RemoveSingleton_NoOp_WhenNotPresent() { var world = new World(); // Should not throw. world.RemoveSingleton(); } [Fact] public void SingletonEntity_DoesNotAppearInNormalQueries() { var world = new World(); // Set a singleton with the same component type that a regular entity has. world.SetSingleton(new GameConfig { Gravity = 9.8f, MaxPlayers = 4 }); var regular = world.CreateEntity(); world.AddComponent(regular, new GameConfig { Gravity = 1.6f, MaxPlayers = 2 }); var query = world.Query().With().Build(); var results = new List(); world.ForEach(query, (Entity e, ref GameConfig cfg) => { results.Add(e); }); // Only the regular entity should appear, not the singleton. results.Should().BeEquivalentTo([regular]); } [Fact] public void SingletonEntity_IsAlive() { var world = new World(); world.SetSingleton(new GameConfig()); world.IsAlive(World.SingletonEntity).Should().BeTrue(); } [Fact] public void Singleton_SurvivesTickExecution() { var world = new World(); var group = new SystemGroup(world); world.SetSingleton(new TimeState { Elapsed = 0f }); // System that reads and updates the singleton. group.Add(new TimeSystem(world)); group.RunTimed(0.5f); group.RunTimed(0.5f); ref var time = ref world.GetSingleton(); time.Elapsed.Should().Be(1.0f); } [Fact] public void MultipleSingletonTypes_AreIndependent() { var world = new World(); world.SetSingleton(new GameConfig { Gravity = 9.8f, MaxPlayers = 4 }); world.SetSingleton(new TimeState { Elapsed = 42f }); ref var config = ref world.GetSingleton(); ref var time = ref world.GetSingleton(); config.Gravity.Should().Be(9.8f); time.Elapsed.Should().Be(42f); } [Fact] public void Singleton_Mutation_ViaRef_IsVisible() { var world = new World(); world.SetSingleton(new GameConfig { Gravity = 0, MaxPlayers = 0 }); ref var config = ref world.GetSingleton(); config.Gravity = 9.8f; ref var config2 = ref world.GetSingleton(); config2.Gravity.Should().Be(9.8f); } private class TimeSystem : ITickedSystem { private readonly World _world; public TimeSystem(World world) { _world = world; } public void Run(World world) => Run(world, Tick.Logical()); public void Run(World world, Tick tick) { ref var time = ref _world.GetSingleton(); time.Elapsed += tick.DeltaTime; } } }