diff --git a/GFramework.Core.Tests/state/StateMachineTests.cs b/GFramework.Core.Tests/state/StateMachineTests.cs new file mode 100644 index 0000000..db557b9 --- /dev/null +++ b/GFramework.Core.Tests/state/StateMachineTests.cs @@ -0,0 +1,328 @@ +using GFramework.Core.Abstractions.state; +using GFramework.Core.state; +using NUnit.Framework; + +namespace GFramework.Core.Tests.state; + +[TestFixture] +public class StateMachineTests +{ + [SetUp] + public void SetUp() + { + _stateMachine = new StateMachine(); + } + + private StateMachine _stateMachine = null!; + + [Test] + public void Current_Should_BeNull_When_NoState_Active() + { + Assert.That(_stateMachine.Current, Is.Null); + } + + [Test] + public void Register_Should_AddState_To_StatesDictionary() + { + var state = new TestStateV2(); + _stateMachine.Register(state); + + Assert.That(_stateMachine.ContainsState(), Is.True); + } + + [Test] + public void ChangeTo_Should_SetCurrentState() + { + var state = new TestStateV2(); + _stateMachine.Register(state); + _stateMachine.ChangeTo(); + + Assert.That(_stateMachine.Current, Is.SameAs(state)); + } + + [Test] + public void ChangeTo_Should_Invoke_OnEnter() + { + var state = new TestStateV2(); + _stateMachine.Register(state); + _stateMachine.ChangeTo(); + + Assert.That(state.EnterCalled, Is.True); + Assert.That(state.EnterFrom, Is.Null); + } + + [Test] + public void ChangeTo_When_CurrentStateExists_Should_Invoke_OnExit() + { + var state1 = new TestStateV2(); + var state2 = new TestStateV3(); + _stateMachine.Register(state1); + _stateMachine.Register(state2); + + _stateMachine.ChangeTo(); + _stateMachine.ChangeTo(); + + Assert.That(state1.ExitCalled, Is.True); + Assert.That(state1.ExitTo, Is.SameAs(state2)); + } + + [Test] + public void ChangeTo_When_CurrentStateExists_Should_Invoke_OnEnter() + { + var state1 = new TestStateV2(); + var state2 = new TestStateV3(); + _stateMachine.Register(state1); + _stateMachine.Register(state2); + + _stateMachine.ChangeTo(); + _stateMachine.ChangeTo(); + + Assert.That(state2.EnterCalled, Is.True); + Assert.That(state2.EnterFrom, Is.SameAs(state1)); + } + + [Test] + public void ChangeTo_ToSameState_Should_NotInvoke_Callbacks() + { + var state = new TestStateV2(); + _stateMachine.Register(state); + _stateMachine.ChangeTo(); + + var enterCount = state.EnterCallCount; + var exitCount = state.ExitCallCount; + + _stateMachine.ChangeTo(); + + Assert.That(state.EnterCallCount, Is.EqualTo(enterCount)); + Assert.That(state.ExitCallCount, Is.EqualTo(exitCount)); + } + + [Test] + public void ChangeTo_ToUnregisteredState_Should_ThrowInvalidOperationException() + { + Assert.Throws(() => _stateMachine.ChangeTo()); + } + + [Test] + public void CanChangeTo_WhenStateNotRegistered_Should_ReturnFalse() + { + var result = _stateMachine.CanChangeTo(); + Assert.That(result, Is.False); + } + + [Test] + public void CanChangeTo_WhenStateRegistered_Should_ReturnTrue() + { + var state = new TestStateV2(); + _stateMachine.Register(state); + + var result = _stateMachine.CanChangeTo(); + Assert.That(result, Is.True); + } + + [Test] + public void CanChangeTo_WhenCurrentStateDeniesTransition_Should_ReturnFalse() + { + var state1 = new TestStateV2 { AllowTransition = false }; + var state2 = new TestStateV3(); + _stateMachine.Register(state1); + _stateMachine.Register(state2); + _stateMachine.ChangeTo(); + + var result = _stateMachine.CanChangeTo(); + Assert.That(result, Is.False); + } + + [Test] + public void ChangeTo_WhenCurrentStateDeniesTransition_Should_NotChange() + { + var state1 = new TestStateV2 { AllowTransition = false }; + var state2 = new TestStateV3(); + _stateMachine.Register(state1); + _stateMachine.Register(state2); + _stateMachine.ChangeTo(); + + var oldState = _stateMachine.Current; + _stateMachine.ChangeTo(); + + Assert.That(_stateMachine.Current, Is.SameAs(oldState)); + Assert.That(_stateMachine.Current, Is.SameAs(state1)); + Assert.That(state2.EnterCalled, Is.False); + } + + [Test] + public void Unregister_Should_RemoveState_FromDictionary() + { + var state = new TestStateV2(); + _stateMachine.Register(state); + _stateMachine.Unregister(); + + Assert.That(_stateMachine.ContainsState(), Is.False); + } + + [Test] + public void Unregister_WhenStateIsActive_Should_Invoke_OnExit_AndClearCurrent() + { + var state = new TestStateV2(); + _stateMachine.Register(state); + _stateMachine.ChangeTo(); + _stateMachine.Unregister(); + + Assert.That(state.ExitCalled, Is.True); + Assert.That(state.ExitTo, Is.Null); + Assert.That(_stateMachine.Current, Is.Null); + } + + [Test] + public void Unregister_WhenStateNotActive_Should_Not_Invoke_OnExit() + { + var state1 = new TestStateV2(); + var state2 = new TestStateV3(); + _stateMachine.Register(state1); + _stateMachine.Register(state2); + _stateMachine.ChangeTo(); + + _stateMachine.Unregister(); + + Assert.That(state1.ExitCalled, Is.False); + Assert.That(_stateMachine.Current, Is.SameAs(state1)); + } + + [Test] + public void MultipleStateChanges_Should_Invoke_Callbacks_Correctly() + { + var state1 = new TestStateV2(); + var state2 = new TestStateV3(); + var state3 = new TestStateV4(); + _stateMachine.Register(state1); + _stateMachine.Register(state2); + _stateMachine.Register(state3); + + _stateMachine.ChangeTo(); + _stateMachine.ChangeTo(); + _stateMachine.ChangeTo(); + + Assert.That(state1.EnterCalled, Is.True); + Assert.That(state1.ExitCalled, Is.True); + Assert.That(state2.EnterCalled, Is.True); + Assert.That(state2.ExitCalled, Is.True); + Assert.That(state3.EnterCalled, Is.True); + Assert.That(state3.ExitCalled, Is.False); + } + + [Test] + public void ChangeTo_Should_Respect_CanTransitionTo_Logic() + { + var state1 = new TestStateV2(); + var state2 = new TestStateV3(); + var state3 = new TestStateV4(); + _stateMachine.Register(state1); + _stateMachine.Register(state2); + _stateMachine.Register(state3); + + _stateMachine.ChangeTo(); + _stateMachine.ChangeTo(); + + Assert.That(state1.EnterCalled, Is.True); + Assert.That(state1.ExitCalled, Is.True); + Assert.That(state2.EnterCalled, Is.True); + } +} + +public sealed class TestStateV2 : IState +{ + public bool AllowTransition { get; set; } = true; + public bool EnterCalled { get; private set; } + public bool ExitCalled { get; private set; } + public int EnterCallCount { get; private set; } + public int ExitCallCount { get; private set; } + public IState? EnterFrom { get; private set; } + public IState? ExitTo { get; private set; } + + public void OnEnter(IState? from) + { + EnterCalled = true; + EnterCallCount++; + EnterFrom = from; + } + + public void OnExit(IState? to) + { + ExitCalled = true; + ExitCallCount++; + ExitTo = to; + } + + public bool CanTransitionTo(IState target) + { + return AllowTransition; + } +} + +public sealed class TestStateV3 : IState +{ + public bool EnterCalled { get; private set; } + public bool ExitCalled { get; private set; } + public int EnterCallCount { get; private set; } + public int ExitCallCount { get; private set; } + public IState? EnterFrom { get; private set; } + public IState? ExitTo { get; private set; } + + public void OnEnter(IState? from) + { + EnterCalled = true; + EnterCallCount++; + EnterFrom = from; + } + + public void OnExit(IState? to) + { + ExitCalled = true; + ExitCallCount++; + ExitTo = to; + } + + public bool CanTransitionTo(IState target) + { + return true; + } +} + +public sealed class TestStateV4 : IState +{ + public bool EnterCalled { get; private set; } + public bool ExitCalled { get; private set; } + public int EnterCallCount { get; private set; } + public int ExitCallCount { get; private set; } + public IState? EnterFrom { get; private set; } + public IState? ExitTo { get; private set; } + + public void OnEnter(IState? from) + { + EnterCalled = true; + EnterCallCount++; + EnterFrom = from; + } + + public void OnExit(IState? to) + { + ExitCalled = true; + ExitCallCount++; + ExitTo = to; + } + + public bool CanTransitionTo(IState target) + { + return true; + } +} + +public static class StateMachineExtensions +{ + public static bool ContainsState(this StateMachine stateMachine) where T : IState + { + return stateMachine.GetType().GetField("States", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)? + .GetValue(stateMachine) is System.Collections.Generic.Dictionary states && + states.ContainsKey(typeof(T)); + } +} diff --git a/GFramework.Core.Tests/state/StateTests.cs b/GFramework.Core.Tests/state/StateTests.cs new file mode 100644 index 0000000..dc07404 --- /dev/null +++ b/GFramework.Core.Tests/state/StateTests.cs @@ -0,0 +1,288 @@ +using GFramework.Core.Abstractions.state; +using GFramework.Core.state; +using NUnit.Framework; + +namespace GFramework.Core.Tests.state; + +[TestFixture] +public class StateTests +{ + [Test] + public void State_Should_Implement_IState_Interface() + { + var state = new ConcreteStateV2(); + + Assert.That(state is IState); + } + + [Test] + public void OnEnter_Should_BeCalled_When_State_Enters() + { + var state = new ConcreteStateV2(); + var otherState = new ConcreteStateV3(); + + state.OnEnter(otherState); + + Assert.That(state.EnterCalled, Is.True); + Assert.That(state.EnterFrom, Is.SameAs(otherState)); + } + + [Test] + public void OnEnter_WithNull_Should_Set_EnterFrom_ToNull() + { + var state = new ConcreteStateV2(); + + state.OnEnter(null); + + Assert.That(state.EnterCalled, Is.True); + Assert.That(state.EnterFrom, Is.Null); + } + + [Test] + public void OnExit_Should_BeCalled_When_State_Exits() + { + var state = new ConcreteStateV2(); + var otherState = new ConcreteStateV3(); + + state.OnExit(otherState); + + Assert.That(state.ExitCalled, Is.True); + Assert.That(state.ExitTo, Is.SameAs(otherState)); + } + + [Test] + public void OnExit_WithNull_Should_Set_ExitTo_ToNull() + { + var state = new ConcreteStateV2(); + + state.OnExit(null); + + Assert.That(state.ExitCalled, Is.True); + Assert.That(state.ExitTo, Is.Null); + } + + [Test] + public void CanTransitionTo_WithAllowTrue_Should_ReturnTrue() + { + var state = new ConcreteStateV2 { AllowTransitions = true }; + var target = new ConcreteStateV3(); + + var result = state.CanTransitionTo(target); + + Assert.That(result, Is.True); + } + + [Test] + public void CanTransitionTo_WithAllowFalse_Should_ReturnFalse() + { + var state = new ConcreteStateV2 { AllowTransitions = false }; + var target = new ConcreteStateV3(); + + var result = state.CanTransitionTo(target); + + Assert.That(result, Is.False); + } + + [Test] + public void CanTransitionTo_Should_Receive_TargetState() + { + var state = new ConcreteStateV2 { AllowTransitions = true }; + var target = new ConcreteStateV3(); + IState? receivedTarget = null; + + state.CanTransitionToAction = s => receivedTarget = s; + state.CanTransitionTo(target); + + Assert.That(receivedTarget, Is.SameAs(target)); + } + + [Test] + public void State_WithComplexTransitionRules_Should_Work() + { + var state1 = new ConditionalStateV2 { AllowedTransitions = new[] { typeof(ConcreteStateV3) } }; + var state2 = new ConcreteStateV3(); + var state3 = new ConcreteStateV4(); + + Assert.That(state1.CanTransitionTo(state2), Is.True); + Assert.That(state1.CanTransitionTo(state3), Is.False); + } + + [Test] + public void MultipleStates_Should_WorkTogether() + { + var state1 = new ConcreteStateV2(); + var state2 = new ConcreteStateV3(); + var state3 = new ConcreteStateV4(); + + state1.OnEnter(null); + state2.OnEnter(state1); + state3.OnEnter(state2); + + Assert.That(state1.EnterCalled, Is.True); + Assert.That(state2.EnterCalled, Is.True); + Assert.That(state3.EnterCalled, Is.True); + + Assert.That(state2.EnterFrom, Is.SameAs(state1)); + Assert.That(state3.EnterFrom, Is.SameAs(state2)); + } + + [Test] + public void State_Should_Track_MultipleTransitions() + { + var state = new TrackingStateV2(); + var other = new ConcreteStateV3(); + + state.OnEnter(other); + state.OnExit(other); + state.OnEnter(other); + state.OnExit(null); + + Assert.That(state.EnterCallCount, Is.EqualTo(2)); + Assert.That(state.ExitCallCount, Is.EqualTo(2)); + } + + [Test] + public void State_Should_Handle_SameState_Transition() + { + var state1 = new ConcreteStateV2(); + var state2 = new ConcreteStateV3(); + var state3 = new ConcreteStateV2(); + + state1.OnEnter(null); + state2.OnEnter(state1); + state3.OnEnter(state2); + + Assert.That(state1.EnterFrom, Is.Null); + Assert.That(state2.EnterFrom, Is.SameAs(state1)); + Assert.That(state3.EnterFrom, Is.SameAs(state2)); + } +} + +public sealed class ConcreteStateV2 : IState +{ + public bool AllowTransitions { get; set; } = true; + public bool EnterCalled { get; private set; } + public bool ExitCalled { get; private set; } + public IState? EnterFrom { get; private set; } + public IState? ExitTo { get; private set; } + public Action? CanTransitionToAction { get; set; } + + public void OnEnter(IState? from) + { + EnterCalled = true; + EnterFrom = from; + } + + public void OnExit(IState? to) + { + ExitCalled = true; + ExitTo = to; + } + + public bool CanTransitionTo(IState target) + { + CanTransitionToAction?.Invoke(target); + return AllowTransitions; + } +} + +public sealed class ConcreteStateV3 : IState +{ + public bool EnterCalled { get; private set; } + public bool ExitCalled { get; private set; } + public IState? EnterFrom { get; private set; } + public IState? ExitTo { get; private set; } + + public void OnEnter(IState? from) + { + EnterCalled = true; + EnterFrom = from; + } + + public void OnExit(IState? to) + { + ExitCalled = true; + ExitTo = to; + } + + public bool CanTransitionTo(IState target) + { + return true; + } +} + +public sealed class ConcreteStateV4 : IState +{ + public bool EnterCalled { get; private set; } + public bool ExitCalled { get; private set; } + public IState? EnterFrom { get; private set; } + public IState? ExitTo { get; private set; } + + public void OnEnter(IState? from) + { + EnterCalled = true; + EnterFrom = from; + } + + public void OnExit(IState? to) + { + ExitCalled = true; + ExitTo = to; + } + + public bool CanTransitionTo(IState target) + { + return true; + } +} + +public sealed class ConditionalStateV2 : IState +{ + public Type[] AllowedTransitions { get; set; } = Array.Empty(); + public bool EnterCalled { get; private set; } + public bool ExitCalled { get; private set; } + public IState? EnterFrom { get; private set; } + public IState? ExitTo { get; private set; } + + public void OnEnter(IState? from) + { + EnterCalled = true; + EnterFrom = from; + } + + public void OnExit(IState? to) + { + ExitCalled = true; + ExitTo = to; + } + + public bool CanTransitionTo(IState target) + { + return AllowedTransitions.Contains(target.GetType()); + } +} + +public sealed class TrackingStateV2 : IState +{ + public int EnterCallCount { get; private set; } + public int ExitCallCount { get; private set; } + public IState? EnterFrom { get; private set; } + public IState? ExitTo { get; private set; } + + public void OnEnter(IState? from) + { + EnterCallCount++; + EnterFrom = from; + } + + public void OnExit(IState? to) + { + ExitCallCount++; + ExitTo = to; + } + + public bool CanTransitionTo(IState target) + { + return true; + } +}