diff --git a/example/AlarmExample/Program.cs b/example/AlarmExample/Program.cs index 49a01a44..90ffd873 100644 --- a/example/AlarmExample/Program.cs +++ b/example/AlarmExample/Program.cs @@ -21,7 +21,7 @@ static void Main(string[] args) { Console.Write("> "); - input = Console.ReadLine(); + input = Console.ReadLine()!; if (!string.IsNullOrWhiteSpace(input)) switch (input.Split(" ")[0]) @@ -101,7 +101,7 @@ static void WriteFire(string input) Console.WriteLine($"{input.Split(" ")[1]} is not a valid AlarmCommand."); } } - catch (InvalidOperationException ex) + catch (InvalidOperationException) { Console.WriteLine($"{input.Split(" ")[1]} is not a valid AlarmCommand to the current state."); } diff --git a/src/Stateless/StateConfiguration.cs b/src/Stateless/StateConfiguration.cs index beec0168..cb35f4be 100644 --- a/src/Stateless/StateConfiguration.cs +++ b/src/Stateless/StateConfiguration.cs @@ -1162,8 +1162,10 @@ public StateConfiguration SubstateOf(TState superstate) /// dynamically by the supplied function. /// /// The accepted trigger. - /// Function to calculate the state - /// that the trigger will cause a transition to. + /// + /// Function to calculate the destination state; if the source and destination states are the same, it will be reentered and + /// any exit or entry logic will be invoked. + /// /// Optional description for the function to calculate the state /// Optional array of possible destination states (used by output formatters) /// The receiver. @@ -1190,8 +1192,10 @@ public StateConfiguration PermitDynamic(TTrigger trigger, Func destinati /// dynamically by the supplied function. /// /// The accepted trigger. - /// Function to calculate the state - /// that the trigger will cause a transition to. + /// + /// Function to calculate the destination state; if the source and destination states are the same, it will be reentered and + /// any exit or entry logic will be invoked. + /// /// Optional description of the function to calculate the state /// Optional list of possible target states. /// The receiver. @@ -1222,8 +1226,10 @@ public StateConfiguration PermitDynamic(TriggerWithParameters trig /// dynamically by the supplied function. /// /// The accepted trigger. - /// Function to calculate the state - /// that the trigger will cause a transition to. + /// + /// Function to calculate the destination state; if the source and destination states are the same, it will be reentered and + /// any exit or entry logic will be invoked. + /// /// Optional description of the function to calculate the state /// Optional list of possible target states. /// The receiver. @@ -1254,8 +1260,10 @@ public StateConfiguration PermitDynamic(TriggerWithParameters /// The accepted trigger. - /// Function to calculate the state - /// that the trigger will cause a transition to. + /// + /// Function to calculate the destination state; if the source and destination states are the same, it will be reentered and + /// any exit or entry logic will be invoked. + /// /// Optional description of the function to calculate the state /// Optional list of possible target states. /// The receiver. @@ -1288,8 +1296,10 @@ public StateConfiguration PermitDynamic(TriggerWithParamete /// dynamically by the supplied function. /// /// The accepted trigger. - /// Function to calculate the state - /// that the trigger will cause a transition to. + /// + /// Function to calculate the destination state; if the source and destination states are the same, it will be reentered and + /// any exit or entry logic will be invoked. + /// /// Function that must return true in order for the /// trigger to be accepted. /// Guard description @@ -1306,8 +1316,10 @@ public StateConfiguration PermitDynamicIf(TTrigger trigger, Func destina /// dynamically by the supplied function. /// /// The accepted trigger. - /// Function to calculate the state - /// that the trigger will cause a transition to. + /// + /// Function to calculate the destination state; if the source and destination states are the same, it will be reentered and + /// any exit or entry logic will be invoked. + /// /// Description of the function to calculate the state /// Function that must return true in order for the /// trigger to be accepted. @@ -1332,8 +1344,10 @@ public StateConfiguration PermitDynamicIf(TTrigger trigger, Func destina /// dynamically by the supplied function. /// /// The accepted trigger. - /// Function to calculate the state - /// that the trigger will cause a transition to. + /// + /// Function to calculate the destination state; if the source and destination states are the same, it will be reentered and + /// any exit or entry logic will be invoked. + /// /// Functions and their descriptions that must return true in order for the /// trigger to be accepted. /// Optional list of possible target states. @@ -1348,8 +1362,10 @@ public StateConfiguration PermitDynamicIf(TTrigger trigger, Func destina /// dynamically by the supplied function. /// /// The accepted trigger. - /// Function to calculate the state - /// that the trigger will cause a transition to. + /// + /// Function to calculate the destination state; if the source and destination states are the same, it will be reentered and + /// any exit or entry logic will be invoked. + /// /// Description of the function to calculate the state /// Functions and their descriptions that must return true in order for the /// trigger to be accepted. @@ -1373,8 +1389,10 @@ public StateConfiguration PermitDynamicIf(TTrigger trigger, Func destina /// dynamically by the supplied function. /// /// The accepted trigger. - /// Function to calculate the state - /// that the trigger will cause a transition to. + /// + /// Function to calculate the destination state; if the source and destination states are the same, it will be reentered and + /// any exit or entry logic will be invoked. + /// /// Function that must return true in order for the /// trigger to be accepted. /// Guard description @@ -1400,8 +1418,10 @@ public StateConfiguration PermitDynamicIf(TriggerWithParameters tr /// dynamically by the supplied function. /// /// The accepted trigger. - /// Function to calculate the state - /// that the trigger will cause a transition to. + /// + /// Function to calculate the destination state; if the source and destination states are the same, it will be reentered and + /// any exit or entry logic will be invoked. + /// /// The receiver. /// Type of the first trigger argument. public StateConfiguration PermitDynamicIf(TriggerWithParameters trigger, Func destinationStateSelector) @@ -1414,8 +1434,10 @@ public StateConfiguration PermitDynamicIf(TriggerWithParameters tr /// dynamically by the supplied function. /// /// The accepted trigger. - /// Function to calculate the state - /// that the trigger will cause a transition to. + /// + /// Function to calculate the destination state; if the source and destination states are the same, it will be reentered and + /// any exit or entry logic will be invoked. + /// /// Optional list of possible target states. /// Functions and their descriptions that must return true in order for the /// trigger to be accepted. @@ -1440,8 +1462,10 @@ public StateConfiguration PermitDynamicIf(TriggerWithParameters tr /// dynamically by the supplied function. /// /// The accepted trigger. - /// Function to calculate the state - /// that the trigger will cause a transition to. + /// + /// Function to calculate the destination state; if the source and destination states are the same, it will be reentered and + /// any exit or entry logic will be invoked. + /// /// Function that must return true in order for the /// trigger to be accepted. /// Guard description @@ -1469,8 +1493,10 @@ public StateConfiguration PermitDynamicIf(TriggerWithParameters /// The accepted trigger. - /// Function to calculate the state - /// that the trigger will cause a transition to. + /// + /// Function to calculate the destination state; if the source and destination states are the same, it will be reentered and + /// any exit or entry logic will be invoked. + /// /// Optional list of possible target states. /// Functions and their descriptions that must return true in order for the /// trigger to be accepted. @@ -1497,8 +1523,10 @@ public StateConfiguration PermitDynamicIf(TriggerWithParameters /// The accepted trigger. - /// Function to calculate the state - /// that the trigger will cause a transition to. + /// + /// Function to calculate the destination state; if the source and destination states are the same, it will be reentered and + /// any exit or entry logic will be invoked. + /// /// The receiver. /// Function that must return true in order for the /// trigger to be accepted. @@ -1528,8 +1556,10 @@ public StateConfiguration PermitDynamicIf(TriggerWithParame /// dynamically by the supplied function. /// /// The accepted trigger. - /// Function to calculate the state - /// that the trigger will cause a transition to. + /// + /// Function to calculate the destination state; if the source and destination states are the same, it will be reentered and + /// any exit or entry logic will be invoked. + /// /// Optional list of possible target states. /// The receiver. /// Functions ant their descriptions that must return true in order for the @@ -1558,8 +1588,10 @@ public StateConfiguration PermitDynamicIf(TriggerWithParame /// dynamically by the supplied function. /// /// The accepted trigger. - /// Function to calculate the state - /// that the trigger will cause a transition to. + /// + /// Function to calculate the destination state; if the source and destination states are the same, it will be reentered and + /// any exit or entry logic will be invoked. + /// /// Parameterized Function that must return true in order for the /// trigger to be accepted. /// Guard description @@ -1585,8 +1617,10 @@ public StateConfiguration PermitDynamicIf(TriggerWithParameters tr /// dynamically by the supplied function. /// /// The accepted trigger. - /// Function to calculate the state - /// that the trigger will cause a transition to. + /// + /// Function to calculate the destination state; if the source and destination states are the same, it will be reentered and + /// any exit or entry logic will be invoked. + /// /// Optional list of possible target states. /// Functions and their descriptions that must return true in order for the /// trigger to be accepted. @@ -1611,8 +1645,10 @@ public StateConfiguration PermitDynamicIf(TriggerWithParameters tr /// dynamically by the supplied function. /// /// The accepted trigger. - /// Function to calculate the state - /// that the trigger will cause a transition to. + /// + /// Function to calculate the destination state; if the source and destination states are the same, it will be reentered and + /// any exit or entry logic will be invoked. + /// /// Function that must return true in order for the /// trigger to be accepted. /// Guard description @@ -1640,8 +1676,10 @@ public StateConfiguration PermitDynamicIf(TriggerWithParameters /// The accepted trigger. - /// Function to calculate the state - /// that the trigger will cause a transition to. + /// + /// Function to calculate the destination state; if the source and destination states are the same, it will be reentered and + /// any exit or entry logic will be invoked. + /// /// Functions that must return true in order for the /// trigger to be accepted. /// Optional list of possible target states. @@ -1668,8 +1706,10 @@ public StateConfiguration PermitDynamicIf(TriggerWithParameters /// The accepted trigger. - /// Function to calculate the state - /// that the trigger will cause a transition to. + /// + /// Function to calculate the destination state; if the source and destination states are the same, it will be reentered and + /// any exit or entry logic will be invoked. + /// /// Function that must return true in order for the /// trigger to be accepted. /// Guard description @@ -1677,7 +1717,7 @@ public StateConfiguration PermitDynamicIf(TriggerWithParametersThe receiver. /// Type of the first trigger argument. /// Type of the second trigger argument. - /// + /// Type of the third trigger argument. public StateConfiguration PermitDynamicIf(TriggerWithParameters trigger, Func destinationStateSelector, Func guard, string guardDescription = null, Reflection.DynamicStateInfos possibleDestinationStates = null) { if (trigger == null) throw new ArgumentNullException(nameof(trigger)); @@ -1699,15 +1739,17 @@ public StateConfiguration PermitDynamicIf(TriggerWithParame /// dynamically by the supplied function. /// /// The accepted trigger. - /// Function to calculate the state - /// that the trigger will cause a transition to. + /// + /// Function to calculate the destination state; if the source and destination states are the same, it will be reentered and + /// any exit or entry logic will be invoked. + /// /// Functions that must return true in order for the /// trigger to be accepted. /// Optional list of possible target states. /// The receiver. /// Type of the first trigger argument. /// Type of the second trigger argument. - /// + /// Type of the third trigger argument. public StateConfiguration PermitDynamicIf(TriggerWithParameters trigger, Func destinationStateSelector, Tuple, string>[] guards, Reflection.DynamicStateInfos possibleDestinationStates = null) { if (trigger == null) throw new ArgumentNullException(nameof(trigger)); diff --git a/src/Stateless/StateMachine.Async.cs b/src/Stateless/StateMachine.Async.cs index 846842a5..919dedda 100644 --- a/src/Stateless/StateMachine.Async.cs +++ b/src/Stateless/StateMachine.Async.cs @@ -220,8 +220,19 @@ async Task InternalFireOneAsync(TTrigger trigger, params object[] args) break; } case DynamicTriggerBehaviour _ when result.Handler.ResultsInTransitionFrom(source, args, out var destination): - case TransitioningTriggerBehaviour _ when result.Handler.ResultsInTransitionFrom(source, args, out destination): { + // Handle transition, and set new state; reentry is permitted from dynamic trigger behaviours. + var transition = new Transition(source, destination, trigger, args); + await HandleTransitioningTriggerAsync(args, representativeState, transition); + + break; + } + case TransitioningTriggerBehaviour _ when result.Handler.ResultsInTransitionFrom(source, args, out var destination): + { + // If a trigger was found on a superstate that would cause unintended reentry, don't trigger. + if (source.Equals(destination)) + break; + // Handle transition, and set new state var transition = new Transition(source, destination, trigger, args); await HandleTransitioningTriggerAsync(args, representativeState, transition); diff --git a/src/Stateless/StateMachine.cs b/src/Stateless/StateMachine.cs index 5d5a0e5f..121d6ede 100644 --- a/src/Stateless/StateMachine.cs +++ b/src/Stateless/StateMachine.cs @@ -6,13 +6,13 @@ namespace Stateless { /// - /// Enum for the different modes used when Fire-ing a trigger + /// Enum for the different modes used when Fireing a trigger /// public enum FiringMode { /// Use immediate mode when the queuing of trigger events are not needed. Care must be taken when using this mode, as there is no run-to-completion guaranteed. Immediate, - /// Use the queued Fire-ing mode when run-to-completion is required. This is the recommended mode. + /// Use the queued Fireing mode when run-to-completion is required. This is the recommended mode. Queued } @@ -47,7 +47,7 @@ private class QueuedTrigger /// /// A function that will be called to read the current state value. /// An action that will be called to write new state values. - public StateMachine(Func stateAccessor, Action stateMutator) :this(stateAccessor, stateMutator, FiringMode.Queued) + public StateMachine(Func stateAccessor, Action stateMutator) : this(stateAccessor, stateMutator, FiringMode.Queued) { } @@ -414,32 +414,39 @@ void InternalFireOne(TTrigger trigger, params object[] args) // Handle special case, re-entry in superstate // Check if it is an internal transition, or a transition from one state to another. case ReentryTriggerBehaviour handler: - { - // Handle transition, and set new state - var transition = new Transition(source, handler.Destination, trigger, args); - HandleReentryTrigger(args, representativeState, transition); - break; - } + { + // Handle transition, and set new state + var transition = new Transition(source, handler.Destination, trigger, args); + HandleReentryTrigger(args, representativeState, transition); + break; + } case DynamicTriggerBehaviour _ when result.Handler.ResultsInTransitionFrom(source, args, out var destination): - case TransitioningTriggerBehaviour _ when result.Handler.ResultsInTransitionFrom(source, args, out destination): - { - //If a trigger was found on a superstate that would cause unintended reentry, don't trigger. - if (source.Equals(destination)) + { + // Handle transition, and set new state; reentry is permitted from dynamic trigger behaviours. + var transition = new Transition(source, destination, trigger, args); + HandleTransitioningTrigger(args, representativeState, transition); + break; + } + case TransitioningTriggerBehaviour _ when result.Handler.ResultsInTransitionFrom(source, args, out var destination): + { + // If a trigger was found on a superstate that would cause unintended reentry, don't trigger. + if (source.Equals(destination)) + break; - // Handle transition, and set new state - var transition = new Transition(source, destination, trigger, args); - HandleTransitioningTrigger(args, representativeState, transition); + // Handle transition, and set new state + var transition = new Transition(source, destination, trigger, args); + HandleTransitioningTrigger(args, representativeState, transition); - break; - } + break; + } case InternalTriggerBehaviour _: - { - // Internal transitions does not update the current state, but must execute the associated action. - var transition = new Transition(source, source, trigger, args); - CurrentRepresentation.InternalAction(transition, args); - break; - } + { + // Internal transitions does not update the current state, but must execute the associated action. + var transition = new Transition(source, source, trigger, args); + CurrentRepresentation.InternalAction(transition, args); + break; + } default: throw new InvalidOperationException("State machine configuration incorrect, no handler for trigger."); } @@ -471,7 +478,7 @@ private void HandleReentryTrigger(object[] args, StateRepresentation representat State = representation.UnderlyingState; } - private void HandleTransitioningTrigger( object[] args, StateRepresentation representativeState, Transition transition) + private void HandleTransitioningTrigger(object[] args, StateRepresentation representativeState, Transition transition) { transition = representativeState.Exit(transition); @@ -492,7 +499,7 @@ private void HandleTransitioningTrigger( object[] args, StateRepresentation repr _onTransitionCompletedEvent.Invoke(new Transition(transition.Source, State, transition.Trigger, transition.Parameters)); } - private StateRepresentation EnterState(StateRepresentation representation, Transition transition, object [] args) + private StateRepresentation EnterState(StateRepresentation representation, Transition transition, object[] args) { // Enter the new state representation.Enter(transition, args); diff --git a/test/Stateless.Tests/AsyncActionsFixture.cs b/test/Stateless.Tests/AsyncActionsFixture.cs index 397b5e4e..aed8ba58 100644 --- a/test/Stateless.Tests/AsyncActionsFixture.cs +++ b/test/Stateless.Tests/AsyncActionsFixture.cs @@ -1,15 +1,14 @@ #if TASKS using System; -using System.Threading.Tasks; using System.Collections.Generic; - -using Xunit; using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Xunit; namespace Stateless.Tests { - public class AsyncActionsFixture { [Fact] @@ -545,7 +544,7 @@ public async Task OnEntryFromAsync_WhenEnteringByAnotherTrigger_InvokesAction() [Fact] public async Task FireAsyncTriggerWithParametersArray() { - const string expectedParam = "42-Stateless-True-420.69-Y"; + const string expectedParam = "42-Stateless-True-123.45-Y"; string actualParam = null; var sm = new StateMachine(State.A); @@ -556,11 +555,11 @@ public async Task FireAsyncTriggerWithParametersArray() sm.Configure(State.B) .OnEntryAsync(t => { - actualParam = string.Join("-", t.Parameters); + actualParam = string.Join("-", t.Parameters.Select(x => string.Format(CultureInfo.InvariantCulture, "{0}", x))); return Task.CompletedTask; }); - await sm.FireAsync(Trigger.X, 42, "Stateless", true, 420.69, Trigger.Y); + await sm.FireAsync(Trigger.X, 42, "Stateless", true, 123.45, Trigger.Y); Assert.Equal(expectedParam, actualParam); } @@ -568,8 +567,7 @@ public async Task FireAsyncTriggerWithParametersArray() [Fact] public async Task FireAsync_TriggerWithMoreThanThreeParameters() { - var decimalSeparator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator; - string expectedParam = $"42-Stateless-True-420{decimalSeparator}69-Y"; + const string expectedParam = "42-Stateless-True-123.45-Y"; string actualParam = null; var sm = new StateMachine(State.A); @@ -580,16 +578,38 @@ public async Task FireAsync_TriggerWithMoreThanThreeParameters() sm.Configure(State.B) .OnEntryAsync(t => { - actualParam = string.Join("-", t.Parameters); + actualParam = string.Join("-", t.Parameters.Select(x => string.Format(CultureInfo.InvariantCulture, "{0}", x))); return Task.CompletedTask; }); var parameterizedX = sm.SetTriggerParameters(Trigger.X, typeof(int), typeof(string), typeof(bool), typeof(double), typeof(Trigger)); - await sm.FireAsync(parameterizedX, 42, "Stateless", true, 420.69, Trigger.Y); + await sm.FireAsync(parameterizedX, 42, "Stateless", true, 123.45, Trigger.Y); Assert.Equal(expectedParam, actualParam); } + + [Fact] + public async Task WhenInSubstate_TriggerSuperStateTwiceToSameSubstate_DoesNotReenterSubstate_Async() + { + var sm = new StateMachine(State.A); + var eCount = 0; + + sm.Configure(State.B) + .OnEntry(() => { eCount++; }) + .SubstateOf(State.C); + + sm.Configure(State.A) + .SubstateOf(State.C); + + sm.Configure(State.C) + .Permit(Trigger.X, State.B); + + await sm.FireAsync(Trigger.X); + await sm.FireAsync(Trigger.X); + + Assert.Equal(1, eCount); + } } } diff --git a/test/Stateless.Tests/DynamicTriggerBehaviourAsyncFixture.cs b/test/Stateless.Tests/DynamicTriggerBehaviourAsyncFixture.cs new file mode 100644 index 00000000..b2d09fee --- /dev/null +++ b/test/Stateless.Tests/DynamicTriggerBehaviourAsyncFixture.cs @@ -0,0 +1,168 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Stateless.Tests +{ + public class DynamicTriggerBehaviourAsyncFixture + { + [Fact] + public async Task PermitDynamic_Selects_Expected_State_Async() + { + var sm = new StateMachine(State.A); + sm.Configure(State.A) + .PermitDynamic(Trigger.X, () => State.B); + + await sm.FireAsync(Trigger.X); + + Assert.Equal(State.B, sm.State); + } + + [Fact] + public async Task PermitDynamic_With_TriggerParameter_Selects_Expected_State_Async() + { + var sm = new StateMachine(State.A); + var trigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A) + .PermitDynamic(trigger, i => i == 1 ? State.B : State.C); + + await sm.FireAsync(trigger, 1); + + Assert.Equal(State.B, sm.State); + } + + [Fact] + public async Task PermitDynamic_Permits_Reentry_Async() + { + var sm = new StateMachine(State.A); + var onExitInvoked = false; + var onEntryInvoked = false; + var onEntryFromInvoked = false; + sm.Configure(State.A) + .PermitDynamic(Trigger.X, () => State.A) + .OnEntry(() => onEntryInvoked = true) + .OnEntryFrom(Trigger.X, () => onEntryFromInvoked = true) + .OnExit(() => onExitInvoked = true); + + await sm.FireAsync(Trigger.X); + + Assert.True(onExitInvoked, "Expected OnExit to be invoked"); + Assert.True(onEntryInvoked, "Expected OnEntry to be invoked"); + Assert.True(onEntryFromInvoked, "Expected OnEntryFrom to be invoked"); + Assert.Equal(State.A, sm.State); + } + + [Fact] + public async Task PermitDynamic_Selects_Expected_State_Based_On_DestinationStateSelector_Function_Async() + { + var sm = new StateMachine(State.A); + var value = 'C'; + sm.Configure(State.A) + .PermitDynamic(Trigger.X, () => value == 'B' ? State.B : State.C); + + await sm.FireAsync(Trigger.X); + + Assert.Equal(State.C, sm.State); + } + + [Fact] + public async Task PermitDynamicIf_With_TriggerParameter_Permits_Transition_When_GuardCondition_Met_Async() + { + var sm = new StateMachine(State.A); + var trigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A) + .PermitDynamicIf(trigger, (i) => i == 1 ? State.C : State.B, (i) => i == 1); + + await sm.FireAsync(trigger, 1); + + Assert.Equal(State.C, sm.State); + } + + [Fact] + public async Task PermitDynamicIf_With_2_TriggerParameters_Permits_Transition_When_GuardCondition_Met_Async() + { + var sm = new StateMachine(State.A); + var trigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A).PermitDynamicIf( + trigger, + (i, j) => i == 1 && j == 2 ? State.C : State.B, + (i, j) => i == 1 && j == 2); + + await sm.FireAsync(trigger, 1, 2); + + Assert.Equal(State.C, sm.State); + } + + [Fact] + public async Task PermitDynamicIf_With_3_TriggerParameters_Permits_Transition_When_GuardCondition_Met_Async() + { + var sm = new StateMachine(State.A); + var trigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A).PermitDynamicIf( + trigger, + (i, j, k) => i == 1 && j == 2 && k == 3 ? State.C : State.B, + (i, j, k) => i == 1 && j == 2 && k == 3); + + await sm.FireAsync(trigger, 1, 2, 3); + + Assert.Equal(State.C, sm.State); + } + + [Fact] + public async Task PermitDynamicIf_With_TriggerParameter_Throws_When_GuardCondition_Not_Met_Async() + { + var sm = new StateMachine(State.A); + var trigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A) + .PermitDynamicIf(trigger, (i) => i > 0 ? State.C : State.B, (i) => i == 2 ? true : false); + + await Assert.ThrowsAsync(async () => await sm.FireAsync(trigger, 1)); + } + + [Fact] + public async Task PermitDynamicIf_With_2_TriggerParameters_Throws_When_GuardCondition_Not_Met_Async() + { + var sm = new StateMachine(State.A); + var trigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A).PermitDynamicIf( + trigger, + (i, j) => i > 0 ? State.C : State.B, + (i, j) => i == 2 && j == 3); + + await Assert.ThrowsAsync(async () => await sm.FireAsync(trigger, 1, 2)); + } + + [Fact] + public async Task PermitDynamicIf_With_3_TriggerParameters_Throws_When_GuardCondition_Not_Met_Async() + { + var sm = new StateMachine(State.A); + var trigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A).PermitDynamicIf(trigger, + (i, j, k) => i > 0 ? State.C : State.B, + (i, j, k) => i == 2 && j == 3 && k == 4); + + await Assert.ThrowsAsync(async () => await sm.FireAsync(trigger, 1, 2, 3)); + } + + [Fact] + public async Task PermitDynamicIf_Permits_Reentry_When_GuardCondition_Met_Async() + { + var sm = new StateMachine(State.A); + var onExitInvoked = false; + var onEntryInvoked = false; + var onEntryFromInvoked = false; + sm.Configure(State.A) + .PermitDynamicIf(Trigger.X, () => State.A, () => true) + .OnEntry(() => onEntryInvoked = true) + .OnEntryFrom(Trigger.X, () => onEntryFromInvoked = true) + .OnExit(() => onExitInvoked = true); + + await sm.FireAsync(Trigger.X); + + Assert.True(onExitInvoked, "Expected OnExit to be invoked"); + Assert.True(onEntryInvoked, "Expected OnEntry to be invoked"); + Assert.True(onEntryFromInvoked, "Expected OnEntryFrom to be invoked"); + Assert.Equal(State.A, sm.State); + } + } +} diff --git a/test/Stateless.Tests/DynamicTriggerBehaviourFixture.cs b/test/Stateless.Tests/DynamicTriggerBehaviourFixture.cs index 2a792155..893a3aec 100644 --- a/test/Stateless.Tests/DynamicTriggerBehaviourFixture.cs +++ b/test/Stateless.Tests/DynamicTriggerBehaviourFixture.cs @@ -1,12 +1,12 @@ -using System.Linq; +using System; using Xunit; namespace Stateless.Tests { - public class DynamicTriggerBehaviour + public class DynamicTriggerBehaviourFixture { [Fact] - public void DestinationStateIsDynamic() + public void PermitDynamic_Selects_Expected_State() { var sm = new StateMachine(State.A); sm.Configure(State.A) @@ -18,7 +18,7 @@ public void DestinationStateIsDynamic() } [Fact] - public void DestinationStateIsCalculatedBasedOnTriggerParameters() + public void PermitDynamic_With_TriggerParameter_Selects_Expected_State() { var sm = new StateMachine(State.A); var trigger = sm.SetTriggerParameters(Trigger.X); @@ -31,17 +31,137 @@ public void DestinationStateIsCalculatedBasedOnTriggerParameters() } [Fact] - public void Sdfsf() + public void PermitDynamic_Permits_Reentry() { var sm = new StateMachine(State.A); - var trigger = sm.SetTriggerParameters(Trigger.X); + var onExitInvoked = false; + var onEntryInvoked = false; + var onEntryFromInvoked = false; + sm.Configure(State.A) + .PermitDynamic(Trigger.X, () => State.A) + .OnEntry(() => onEntryInvoked = true) + .OnEntryFrom(Trigger.X, () => onEntryFromInvoked = true) + .OnExit(() => onExitInvoked = true); + + sm.Fire(Trigger.X); + + Assert.True(onExitInvoked, "Expected OnExit to be invoked"); + Assert.True(onEntryInvoked, "Expected OnEntry to be invoked"); + Assert.True(onEntryFromInvoked, "Expected OnEntryFrom to be invoked"); + Assert.Equal(State.A, sm.State); + } + + [Fact] + public void PermitDynamic_Selects_Expected_State_Based_On_DestinationStateSelector_Function() + { + var sm = new StateMachine(State.A); + var value = 'C'; sm.Configure(State.A) - .PermitDynamicIf(trigger, (i) => i == 1 ? State.C : State.B, (i) => i == 1 ? true : false); + .PermitDynamic(Trigger.X, () => value == 'B' ? State.B : State.C); + + sm.Fire(Trigger.X); - // Should not throw - sm.GetPermittedTriggers().ToList(); + Assert.Equal(State.C, sm.State); + } + + [Fact] + public void PermitDynamicIf_With_TriggerParameter_Permits_Transition_When_GuardCondition_Met() + { + var sm = new StateMachine(State.A); + var trigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A) + .PermitDynamicIf(trigger, (i) => i == 1 ? State.C : State.B, (i) => i == 1); sm.Fire(trigger, 1); + + Assert.Equal(State.C, sm.State); + } + + [Fact] + public void PermitDynamicIf_With_2_TriggerParameters_Permits_Transition_When_GuardCondition_Met() + { + var sm = new StateMachine(State.A); + var trigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A).PermitDynamicIf( + trigger, + (i, j) => i == 1 && j == 2 ? State.C : State.B, + (i, j) => i == 1 && j == 2); + + sm.Fire(trigger, 1, 2); + + Assert.Equal(State.C, sm.State); + } + + [Fact] + public void PermitDynamicIf_With_3_TriggerParameters_Permits_Transition_When_GuardCondition_Met() + { + var sm = new StateMachine(State.A); + var trigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A).PermitDynamicIf( + trigger, + (i, j, k) => i == 1 && j == 2 && k == 3 ? State.C : State.B, + (i, j, k) => i == 1 && j == 2 && k == 3); + + sm.Fire(trigger, 1, 2, 3); + + Assert.Equal(State.C, sm.State); + } + + [Fact] + public void PermitDynamicIf_With_TriggerParameter_Throws_When_GuardCondition_Not_Met() + { + var sm = new StateMachine(State.A); + var trigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A) + .PermitDynamicIf(trigger, (i) => i > 0 ? State.C : State.B, (i) => i == 2 ? true : false); + + Assert.Throws(() => sm.Fire(trigger, 1)); + } + + [Fact] + public void PermitDynamicIf_With_2_TriggerParameters_Throws_When_GuardCondition_Not_Met() + { + var sm = new StateMachine(State.A); + var trigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A).PermitDynamicIf( + trigger, + (i, j) => i > 0 ? State.C : State.B, + (i, j) => i == 2 && j == 3); + + Assert.Throws(() => sm.Fire(trigger, 1, 2)); + } + + [Fact] + public void PermitDynamicIf_With_3_TriggerParameters_Throws_When_GuardCondition_Not_Met() + { + var sm = new StateMachine(State.A); + var trigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A).PermitDynamicIf(trigger, + (i, j, k) => i > 0 ? State.C : State.B, + (i, j, k) => i == 2 && j == 3 && k == 4); + + Assert.Throws(() => sm.Fire(trigger, 1, 2, 3)); + } + + [Fact] + public void PermitDynamicIf_Permits_Reentry_When_GuardCondition_Met() + { + var sm = new StateMachine(State.A); + var onExitInvoked = false; + var onEntryInvoked = false; + var onEntryFromInvoked = false; + sm.Configure(State.A) + .PermitDynamicIf(Trigger.X, () => State.A, () => true) + .OnEntry(() => onEntryInvoked = true) + .OnEntryFrom(Trigger.X, () => onEntryFromInvoked = true) + .OnExit(() => onExitInvoked = true); + + sm.Fire(Trigger.X); + + Assert.True(onExitInvoked, "Expected OnExit to be invoked"); + Assert.True(onEntryInvoked, "Expected OnEntry to be invoked"); + Assert.True(onEntryFromInvoked, "Expected OnEntryFrom to be invoked"); + Assert.Equal(State.A, sm.State); } } } diff --git a/test/Stateless.Tests/ReflectionFixture.cs b/test/Stateless.Tests/ReflectionFixture.cs index aebe54e9..eff37206 100644 --- a/test/Stateless.Tests/ReflectionFixture.cs +++ b/test/Stateless.Tests/ReflectionFixture.cs @@ -892,6 +892,14 @@ StateConfiguration InternalPermit(TTrigger trigger, TState destinationState, str StateConfiguration InternalPermitDynamic(TTrigger trigger, Func destinationStateSelector, string guardDescription) */ } + + [Fact] + public void InvocationInfo_Description_Property_When_Method_Name_Is_Null_Returns_String_Literal_Null() + { + var invocationInfo = new InvocationInfo(null, null, InvocationInfo.Timing.Synchronous); + + Assert.Equal("", invocationInfo.Description); + } } }