From 27886d0c28bad8e0b0ea8381b378beebb55f46b9 Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Fri, 19 Apr 2024 21:19:34 +0100 Subject: [PATCH 01/11] #565 Permit state reentry from dynamic transitions. --- src/Stateless/StateConfiguration.cs | 130 +++++++++++------ src/Stateless/StateMachine.cs | 11 +- .../DynamicTriggerBehaviourFixture.cs | 136 ++++++++++++++++-- 3 files changed, 223 insertions(+), 54 deletions(-) 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.cs b/src/Stateless/StateMachine.cs index 5d5a0e5f..db0d808b 100644 --- a/src/Stateless/StateMachine.cs +++ b/src/Stateless/StateMachine.cs @@ -421,9 +421,16 @@ void InternalFireOne(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): { - //If a trigger was found on a superstate that would cause unintended reentry, don't trigger. + // 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; diff --git a/test/Stateless.Tests/DynamicTriggerBehaviourFixture.cs b/test/Stateless.Tests/DynamicTriggerBehaviourFixture.cs index 2a792155..9fbf5d39 100644 --- a/test/Stateless.Tests/DynamicTriggerBehaviourFixture.cs +++ b/test/Stateless.Tests/DynamicTriggerBehaviourFixture.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System; using Xunit; namespace Stateless.Tests @@ -6,7 +6,7 @@ namespace Stateless.Tests public class DynamicTriggerBehaviour { [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); } } } From 1f833521ee2c2b8aac76d69eb0e9639c11fb3694 Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Fri, 19 Apr 2024 22:02:04 +0100 Subject: [PATCH 02/11] Housekeeping --- example/AlarmExample/Program.cs | 4 ++-- src/Stateless/StateMachine.cs | 4 ++-- test/Stateless.Tests/AsyncActionsFixture.cs | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) 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/StateMachine.cs b/src/Stateless/StateMachine.cs index db0d808b..63c63636 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 } diff --git a/test/Stateless.Tests/AsyncActionsFixture.cs b/test/Stateless.Tests/AsyncActionsFixture.cs index e74d3e76..48a67d39 100644 --- a/test/Stateless.Tests/AsyncActionsFixture.cs +++ b/test/Stateless.Tests/AsyncActionsFixture.cs @@ -544,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); @@ -559,7 +559,7 @@ public async Task FireAsyncTriggerWithParametersArray() 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); } @@ -567,7 +567,7 @@ public async Task FireAsyncTriggerWithParametersArray() [Fact] public async Task FireAsync_TriggerWithMoreThanThreeParameters() { - 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); @@ -584,7 +584,7 @@ public async Task FireAsync_TriggerWithMoreThanThreeParameters() 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); } From dec8c3d780d5a7c561f12e930e66fcabe2abb9f3 Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Sun, 21 Apr 2024 11:51:51 +0100 Subject: [PATCH 03/11] Fix localisation bug in tests. --- test/Stateless.Tests/AsyncActionsFixture.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/Stateless.Tests/AsyncActionsFixture.cs b/test/Stateless.Tests/AsyncActionsFixture.cs index 48a67d39..ebfcf293 100644 --- a/test/Stateless.Tests/AsyncActionsFixture.cs +++ b/test/Stateless.Tests/AsyncActionsFixture.cs @@ -1,14 +1,15 @@ #if TASKS using System; -using System.Threading.Tasks; using System.Collections.Generic; - +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Xunit; namespace Stateless.Tests { - public class AsyncActionsFixture { [Fact] @@ -555,11 +556,12 @@ 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, 123.45, Trigger.Y); + Console.WriteLine(Thread.CurrentThread.CurrentCulture); Assert.Equal(expectedParam, actualParam); } @@ -578,7 +580,7 @@ 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; }); From 18d71415efed88c77b5d41fbb22e3f68cd9b2650 Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Sun, 21 Apr 2024 11:55:10 +0100 Subject: [PATCH 04/11] Remove debug code. --- test/Stateless.Tests/AsyncActionsFixture.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/Stateless.Tests/AsyncActionsFixture.cs b/test/Stateless.Tests/AsyncActionsFixture.cs index ebfcf293..efb7208c 100644 --- a/test/Stateless.Tests/AsyncActionsFixture.cs +++ b/test/Stateless.Tests/AsyncActionsFixture.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Xunit; @@ -561,7 +560,6 @@ public async Task FireAsyncTriggerWithParametersArray() }); await sm.FireAsync(Trigger.X, 42, "Stateless", true, 123.45, Trigger.Y); - Console.WriteLine(Thread.CurrentThread.CurrentCulture); Assert.Equal(expectedParam, actualParam); } From 7c6511cfbc82854ae064d90cc0daa4848750aba7 Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Mon, 22 Apr 2024 21:19:21 +0100 Subject: [PATCH 05/11] #565 Permit state reentry from dynamic transitions with FireAsync; retrofit #544 onto FireAsync. --- src/Stateless/StateMachine.Async.cs | 13 +- src/Stateless/StateMachine.cs | 60 +++---- test/Stateless.Tests/AsyncActionsFixture.cs | 22 +++ .../DynamicTriggerBehaviourAsyncFixture.cs | 168 ++++++++++++++++++ .../DynamicTriggerBehaviourFixture.cs | 2 +- 5 files changed, 233 insertions(+), 32 deletions(-) create mode 100644 test/Stateless.Tests/DynamicTriggerBehaviourAsyncFixture.cs 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 63c63636..121d6ede 100644 --- a/src/Stateless/StateMachine.cs +++ b/src/Stateless/StateMachine.cs @@ -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,39 +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): - { - // 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); + { + // 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; + } + 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."); } @@ -478,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); @@ -499,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 efb7208c..aed8ba58 100644 --- a/test/Stateless.Tests/AsyncActionsFixture.cs +++ b/test/Stateless.Tests/AsyncActionsFixture.cs @@ -588,6 +588,28 @@ public async Task FireAsync_TriggerWithMoreThanThreeParameters() 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 9fbf5d39..893a3aec 100644 --- a/test/Stateless.Tests/DynamicTriggerBehaviourFixture.cs +++ b/test/Stateless.Tests/DynamicTriggerBehaviourFixture.cs @@ -3,7 +3,7 @@ namespace Stateless.Tests { - public class DynamicTriggerBehaviour + public class DynamicTriggerBehaviourFixture { [Fact] public void PermitDynamic_Selects_Expected_State() From 750c8959457ba6dd75000bc372fc0091aa9048e5 Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Fri, 19 Apr 2024 21:19:34 +0100 Subject: [PATCH 06/11] #565 Permit state reentry from dynamic transitions. --- src/Stateless/StateConfiguration.cs | 130 +++++++++++------ src/Stateless/StateMachine.cs | 11 +- .../DynamicTriggerBehaviourFixture.cs | 136 ++++++++++++++++-- 3 files changed, 223 insertions(+), 54 deletions(-) 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.cs b/src/Stateless/StateMachine.cs index 5d5a0e5f..db0d808b 100644 --- a/src/Stateless/StateMachine.cs +++ b/src/Stateless/StateMachine.cs @@ -421,9 +421,16 @@ void InternalFireOne(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): { - //If a trigger was found on a superstate that would cause unintended reentry, don't trigger. + // 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; diff --git a/test/Stateless.Tests/DynamicTriggerBehaviourFixture.cs b/test/Stateless.Tests/DynamicTriggerBehaviourFixture.cs index 2a792155..9fbf5d39 100644 --- a/test/Stateless.Tests/DynamicTriggerBehaviourFixture.cs +++ b/test/Stateless.Tests/DynamicTriggerBehaviourFixture.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System; using Xunit; namespace Stateless.Tests @@ -6,7 +6,7 @@ namespace Stateless.Tests public class DynamicTriggerBehaviour { [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); } } } From 1cf007c567b67db0a77ba6cbeb4c3f124e4ea32e Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Fri, 19 Apr 2024 22:02:04 +0100 Subject: [PATCH 07/11] Merged from dev --- example/AlarmExample/Program.cs | 4 ++-- src/Stateless/StateMachine.cs | 4 ++-- test/Stateless.Tests/AsyncActionsFixture.cs | 9 ++++----- 3 files changed, 8 insertions(+), 9 deletions(-) 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/StateMachine.cs b/src/Stateless/StateMachine.cs index db0d808b..63c63636 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 } diff --git a/test/Stateless.Tests/AsyncActionsFixture.cs b/test/Stateless.Tests/AsyncActionsFixture.cs index 397b5e4e..8295c4c4 100644 --- a/test/Stateless.Tests/AsyncActionsFixture.cs +++ b/test/Stateless.Tests/AsyncActionsFixture.cs @@ -545,7 +545,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); @@ -560,7 +560,7 @@ public async Task FireAsyncTriggerWithParametersArray() 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 +568,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); @@ -586,7 +585,7 @@ public async Task FireAsync_TriggerWithMoreThanThreeParameters() 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); } From 75494cb3058becdb5059c83e78c2ef608b6ada03 Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Sun, 21 Apr 2024 11:51:51 +0100 Subject: [PATCH 08/11] Fix localisation bug in tests. --- test/Stateless.Tests/AsyncActionsFixture.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/Stateless.Tests/AsyncActionsFixture.cs b/test/Stateless.Tests/AsyncActionsFixture.cs index 8295c4c4..51f8398e 100644 --- a/test/Stateless.Tests/AsyncActionsFixture.cs +++ b/test/Stateless.Tests/AsyncActionsFixture.cs @@ -1,15 +1,16 @@ #if TASKS using System; -using System.Threading.Tasks; using System.Collections.Generic; - +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Xunit; using System.Globalization; namespace Stateless.Tests { - public class AsyncActionsFixture { [Fact] @@ -556,11 +557,12 @@ 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, 123.45, Trigger.Y); + Console.WriteLine(Thread.CurrentThread.CurrentCulture); Assert.Equal(expectedParam, actualParam); } @@ -579,7 +581,7 @@ 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; }); From 4c36aed7e3e6106defa4a84b101ea5a3cc545788 Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Sun, 21 Apr 2024 11:55:10 +0100 Subject: [PATCH 09/11] Remove debug code. --- test/Stateless.Tests/AsyncActionsFixture.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/Stateless.Tests/AsyncActionsFixture.cs b/test/Stateless.Tests/AsyncActionsFixture.cs index 51f8398e..f0f8d46a 100644 --- a/test/Stateless.Tests/AsyncActionsFixture.cs +++ b/test/Stateless.Tests/AsyncActionsFixture.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Xunit; using System.Globalization; @@ -562,7 +561,6 @@ public async Task FireAsyncTriggerWithParametersArray() }); await sm.FireAsync(Trigger.X, 42, "Stateless", true, 123.45, Trigger.Y); - Console.WriteLine(Thread.CurrentThread.CurrentCulture); Assert.Equal(expectedParam, actualParam); } From 81d3b072b14c1bc0beaeb168128fc546aa3ba1ab Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Mon, 22 Apr 2024 21:19:21 +0100 Subject: [PATCH 10/11] #565 Permit state reentry from dynamic transitions with FireAsync; retrofit #544 onto FireAsync. --- src/Stateless/StateMachine.Async.cs | 13 +- src/Stateless/StateMachine.cs | 60 +++---- test/Stateless.Tests/AsyncActionsFixture.cs | 22 +++ .../DynamicTriggerBehaviourAsyncFixture.cs | 168 ++++++++++++++++++ .../DynamicTriggerBehaviourFixture.cs | 2 +- 5 files changed, 233 insertions(+), 32 deletions(-) create mode 100644 test/Stateless.Tests/DynamicTriggerBehaviourAsyncFixture.cs 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 63c63636..121d6ede 100644 --- a/src/Stateless/StateMachine.cs +++ b/src/Stateless/StateMachine.cs @@ -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,39 +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): - { - // 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); + { + // 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; + } + 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."); } @@ -478,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); @@ -499,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 f0f8d46a..0dd48a28 100644 --- a/test/Stateless.Tests/AsyncActionsFixture.cs +++ b/test/Stateless.Tests/AsyncActionsFixture.cs @@ -589,6 +589,28 @@ public async Task FireAsync_TriggerWithMoreThanThreeParameters() 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 9fbf5d39..893a3aec 100644 --- a/test/Stateless.Tests/DynamicTriggerBehaviourFixture.cs +++ b/test/Stateless.Tests/DynamicTriggerBehaviourFixture.cs @@ -3,7 +3,7 @@ namespace Stateless.Tests { - public class DynamicTriggerBehaviour + public class DynamicTriggerBehaviourFixture { [Fact] public void PermitDynamic_Selects_Expected_State() From 9da05491f41fb18c1b8da169030f1505a1924f2b Mon Sep 17 00:00:00 2001 From: Mike Clift Date: Mon, 22 Apr 2024 21:45:44 +0100 Subject: [PATCH 11/11] Add test coverage for incoming commit --- test/Stateless.Tests/AsyncActionsFixture.cs | 1 - test/Stateless.Tests/ReflectionFixture.cs | 8 ++++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/test/Stateless.Tests/AsyncActionsFixture.cs b/test/Stateless.Tests/AsyncActionsFixture.cs index 0dd48a28..aed8ba58 100644 --- a/test/Stateless.Tests/AsyncActionsFixture.cs +++ b/test/Stateless.Tests/AsyncActionsFixture.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Threading.Tasks; using Xunit; -using System.Globalization; namespace Stateless.Tests { 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); + } } }