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