diff --git a/.github/workflows/BuildAndTestOnPullRequests.yml b/.github/workflows/BuildAndTestOnPullRequests.yml index 5f843ed1..b995b1dc 100644 --- a/.github/workflows/BuildAndTestOnPullRequests.yml +++ b/.github/workflows/BuildAndTestOnPullRequests.yml @@ -10,7 +10,7 @@ jobs: Build_Stateless_solution: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install dependencies run: dotnet restore diff --git a/CHANGELOG.md b/CHANGELOG.md index 45f0d51f..6f103ce9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 5.16.0 - 2024.05.24 +### Changed + - Permit state reentry from dynamic transitions [#565] + - This is a change in behavior from v5.15.0 (see [#544]); this version restores the previous behavior for `PermitDynamic` that allows reentry; + if reentry is not the desired behavior, consider using a guard condition with `PermitDynamicIf`. + - Remove getDestination, and use Destination property instead (internal refactor) [#575] +### Added + - Add overloads to `FireAsync` to support parameterized trigger arguments [#570] + - Add overloads to `CanFire` to support parameterized trigger arguments [#574] +### Fixed + - Prevent `NullReferenceException` in the `InvocationInfo` class [#566] + ## 5.15.0 - 2023.12.29 ### Changed - Updated net6.0 build target to net8.0 [#551] @@ -210,6 +222,11 @@ Version 5.10.0 is now listed as the newest, since it has the highest version num ### Removed ### Fixed +[#575]: https://github.com/dotnet-state-machine/stateless/pull/575 +[#574]: https://github.com/dotnet-state-machine/stateless/pull/574 +[#570]: https://github.com/dotnet-state-machine/stateless/pull/570 +[#566]: https://github.com/dotnet-state-machine/stateless/pull/566 +[#565]: https://github.com/dotnet-state-machine/stateless/issues/565 [#551]: https://github.com/dotnet-state-machine/stateless/pull/551 [#557]: https://github.com/dotnet-state-machine/stateless/issues/557 [#553]: https://github.com/dotnet-state-machine/stateless/issues/553 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/DynamicTriggerBehaviour.cs b/src/Stateless/DynamicTriggerBehaviour.cs index 99e550d4..ed694f9e 100644 --- a/src/Stateless/DynamicTriggerBehaviour.cs +++ b/src/Stateless/DynamicTriggerBehaviour.cs @@ -17,10 +17,9 @@ public DynamicTriggerBehaviour(TTrigger trigger, Func destinat TransitionInfo = info ?? throw new ArgumentNullException(nameof(info)); } - public override bool ResultsInTransitionFrom(TState source, object[] args, out TState destination) + public void GetDestinationState(TState source, object[] args, out TState destination) { destination = _destination(args); - return true; } } } diff --git a/src/Stateless/IgnoredTriggerBehaviour.cs b/src/Stateless/IgnoredTriggerBehaviour.cs index 753dc819..cb396f1e 100644 --- a/src/Stateless/IgnoredTriggerBehaviour.cs +++ b/src/Stateless/IgnoredTriggerBehaviour.cs @@ -8,12 +8,6 @@ public IgnoredTriggerBehaviour(TTrigger trigger, TransitionGuard transitionGuard : base(trigger, transitionGuard) { } - - public override bool ResultsInTransitionFrom(TState source, object[] args, out TState destination) - { - destination = default(TState); - return false; - } } } } diff --git a/src/Stateless/InternalTriggerBehaviour.cs b/src/Stateless/InternalTriggerBehaviour.cs index 3966a945..328ef769 100644 --- a/src/Stateless/InternalTriggerBehaviour.cs +++ b/src/Stateless/InternalTriggerBehaviour.cs @@ -14,12 +14,6 @@ protected InternalTriggerBehaviour(TTrigger trigger, TransitionGuard guard) : ba public abstract void Execute(Transition transition, object[] args); public abstract Task ExecuteAsync(Transition transition, object[] args); - public override bool ResultsInTransitionFrom(TState source, object[] args, out TState destination) - { - destination = source; - return false; - } - public class Sync : InternalTriggerBehaviour { public Action InternalAction { get; } diff --git a/src/Stateless/ReentryTriggerBehaviour.cs b/src/Stateless/ReentryTriggerBehaviour.cs index 29ecc344..95df24e9 100644 --- a/src/Stateless/ReentryTriggerBehaviour.cs +++ b/src/Stateless/ReentryTriggerBehaviour.cs @@ -14,13 +14,6 @@ public ReentryTriggerBehaviour(TTrigger trigger, TState destination, TransitionG { _destination = destination; } - - public override bool ResultsInTransitionFrom(TState source, object[] args, out TState destination) - { - destination = _destination; - return true; - } } - } } diff --git a/src/Stateless/Reflection/InvocationInfo.cs b/src/Stateless/Reflection/InvocationInfo.cs index 9f0270fa..5cec4727 100644 --- a/src/Stateless/Reflection/InvocationInfo.cs +++ b/src/Stateless/Reflection/InvocationInfo.cs @@ -63,9 +63,11 @@ public string Description { if (_description != null) return _description; + if (MethodName == null) + return ""; if (MethodName.IndexOfAny(new char[] { '<', '>', '`' }) >= 0) return DefaultFunctionDescription; - return MethodName ?? ""; + return MethodName; } } 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 c4e61e98..f69d0065 100644 --- a/src/Stateless/StateMachine.Async.cs +++ b/src/Stateless/StateMachine.Async.cs @@ -46,6 +46,19 @@ public Task FireAsync(TTrigger trigger) return InternalFireAsync(trigger, new object[0]); } + /// + /// Transition from the current state via the specified trigger in async fashion. + /// The target state is determined by the configuration of the current state. + /// Actions associated with leaving the current state and entering the new one + /// will be invoked. + /// + /// The trigger to fire. + /// A variable-length parameters list containing arguments. + public Task FireAsync(TTrigger trigger, params object[] args) + { + return InternalFireAsync(trigger, args); + } + /// /// Transition from the current state via the specified trigger in async fashion. /// The target state is determined by the configuration of the current state. @@ -206,13 +219,25 @@ async Task InternalFireOneAsync(TTrigger trigger, params object[] args) await HandleReentryTriggerAsync(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): + case DynamicTriggerBehaviour handler: { - // Handle transition, and set new state + handler.GetDestinationState(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); await HandleTransitioningTriggerAsync(args, representativeState, transition); + break; + } + case TransitioningTriggerBehaviour handler: + { + // If a trigger was found on a superstate that would cause unintended reentry, don't trigger. + if (source.Equals(handler.Destination)) + break; + + // Handle transition, and set new state + var transition = new Transition(source, handler.Destination, trigger, args); + await HandleTransitioningTriggerAsync(args, representativeState, transition); + break; } case InternalTriggerBehaviour itb: diff --git a/src/Stateless/StateMachine.cs b/src/Stateless/StateMachine.cs index 5d5a0e5f..a28c5f48 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) { } @@ -390,11 +390,13 @@ private void InternalFireQueued(TTrigger trigger, params object[] args) /// /// /// - void InternalFireOne(TTrigger trigger, params object[] args) + private void InternalFireOne(TTrigger trigger, params object[] args) { // If this is a trigger with parameters, we must validate the parameter(s) if (_triggerConfiguration.TryGetValue(trigger, out TriggerWithParameters configuration)) + { configuration.ValidateParameters(args); + } var source = State; var representativeState = GetRepresentation(source); @@ -414,32 +416,40 @@ 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; - } - 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 + var transition = new Transition(source, handler.Destination, trigger, args); + HandleReentryTrigger(args, representativeState, transition); break; + } + case DynamicTriggerBehaviour handler: + { + handler.GetDestinationState(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 - var transition = new Transition(source, destination, trigger, args); - HandleTransitioningTrigger(args, representativeState, transition); + break; + } + case TransitioningTriggerBehaviour handler: + { + // If a trigger was found on a superstate that would cause unintended reentry, don't trigger. + if (source.Equals(handler.Destination)) + break; - break; - } + // Handle transition, and set new state + var transition = new Transition(source, handler.Destination, trigger, args); + HandleTransitioningTrigger(args, representativeState, transition); + + 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 +481,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 +502,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); @@ -563,6 +573,12 @@ public bool IsInState(TState state) /// Returns true if can be fired /// in the current state. /// + /// + /// When the trigger is configured with parameters, the default value of each of the trigger parameter's types will be used + /// to evaluate whether it can fire, which may not be the desired behavior; to check if a trigger can be fired with specific arguments, + /// use the overload of CanFire<TArg1[, TArg2[ ,TArg3]]>(TriggerWithParameters<TArg1[, TArg2[ ,TArg3]]>, ...) that + /// matches the type arguments of your trigger. + /// /// Trigger to test. /// True if the trigger can be fired, false otherwise. public bool CanFire(TTrigger trigger) @@ -572,8 +588,64 @@ public bool CanFire(TTrigger trigger) /// /// Returns true if can be fired - /// in the current state. + /// in the current state using the supplied trigger argument. + /// + /// Type of the first trigger argument. + /// Trigger to test. + /// The first argument. + /// True if the trigger can be fired, false otherwise. + public bool CanFire(TriggerWithParameters trigger, TArg0 arg0) + { + if (trigger == null) throw new ArgumentNullException(nameof(trigger)); + + return CurrentRepresentation.CanHandle(trigger.Trigger, arg0); + } + + /// + /// Returns true if can be fired + /// in the current state using the supplied trigger arguments. + /// + /// Type of the first trigger argument. + /// Type of the second trigger argument. + /// Trigger to test. + /// The first argument. + /// The second argument. + /// True if the trigger can be fired, false otherwise. + public bool CanFire(TriggerWithParameters trigger, TArg0 arg0, TArg1 arg1) + { + if (trigger == null) throw new ArgumentNullException(nameof(trigger)); + + return CurrentRepresentation.CanHandle(trigger.Trigger, arg0, arg1); + } + + /// + /// Returns true if can be fired + /// in the current state using the supplied trigger arguments. + /// + /// Type of the first trigger argument. + /// Type of the second trigger argument. + /// Type of the third trigger argument. + /// Trigger to test. + /// The first argument. + /// The second argument. + /// The third argument. + /// True if the trigger can be fired, false otherwise. + public bool CanFire(TriggerWithParameters trigger, TArg0 arg0, TArg1 arg1, TArg2 arg2) + { + if (trigger == null) throw new ArgumentNullException(nameof(trigger)); + + return CurrentRepresentation.CanHandle(trigger.Trigger, arg0, arg1, arg2); + } + + /// + /// Returns true if can be fired in the current state. /// + /// + /// When the trigger is configured with parameters, the default value of each of the trigger parameter's types will be used + /// to evaluate whether it can fire, which may not be the desired behavior; to check if a trigger can be fired with specific arguments, + /// use the overload of CanFire<TArg1[, TArg2[ ,TArg3]]>(TriggerWithParameters<TArg1[, TArg2[ ,TArg3]]>, ...) that + /// matches the type arguments of your trigger. + /// /// Trigger to test. /// Guard descriptions of unmet guards. If given trigger is not configured for current state, this will be null. /// True if the trigger can be fired, false otherwise. @@ -582,6 +654,60 @@ public bool CanFire(TTrigger trigger, out ICollection unmetGuards) return CurrentRepresentation.CanHandle(trigger, new object[] { }, out unmetGuards); } + /// + /// Returns true if can be fired + /// in the current state using the supplied trigger argument. + /// + /// Type of the first trigger argument. + /// Trigger to test. + /// The first argument. + /// Guard descriptions of unmet guards. If given trigger is not configured for current state, this will be null. + /// True if the trigger can be fired, false otherwise. + public bool CanFire(TriggerWithParameters trigger, TArg0 arg0, out ICollection unmetGuards) + { + if (trigger == null) throw new ArgumentNullException(nameof(trigger)); + + return CurrentRepresentation.CanHandle(trigger.Trigger, new object[] { arg0 }, out unmetGuards); + } + + /// + /// Returns true if can be fired + /// in the current state using the supplied trigger arguments. + /// + /// Type of the first trigger argument. + /// Type of the second trigger argument. + /// Trigger to test. + /// The first argument. + /// The second argument. + /// Guard descriptions of unmet guards. If given trigger is not configured for current state, this will be null. + /// True if the trigger can be fired, false otherwise. + public bool CanFire(TriggerWithParameters trigger, TArg0 arg0, TArg1 arg1, out ICollection unmetGuards) + { + if (trigger == null) throw new ArgumentNullException(nameof(trigger)); + + return CurrentRepresentation.CanHandle(trigger.Trigger, new object[] { arg0, arg1 }, out unmetGuards); + } + + /// + /// Returns true if can be fired + /// in the current state using the supplied trigger arguments. + /// + /// Type of the first trigger argument. + /// Type of the second trigger argument. + /// Type of the third trigger argument. + /// Trigger to test. + /// The first argument. + /// The second argument. + /// The third argument. + /// Guard descriptions of unmet guards. If given trigger is not configured for current state, this will be null. + /// True if the trigger can be fired, false otherwise. + public bool CanFire(TriggerWithParameters trigger, TArg0 arg0, TArg1 arg1, TArg2 arg2, out ICollection unmetGuards) + { + if (trigger == null) throw new ArgumentNullException(nameof(trigger)); + + return CurrentRepresentation.CanHandle(trigger.Trigger, new object[] { arg0, arg1, arg2 }, out unmetGuards); + } + /// /// A human-readable representation of the state machine. /// diff --git a/src/Stateless/Stateless.csproj b/src/Stateless/Stateless.csproj index e9cc3285..a3d0771c 100644 --- a/src/Stateless/Stateless.csproj +++ b/src/Stateless/Stateless.csproj @@ -8,7 +8,7 @@ Create state machines and lightweight state machine-based workflows directly in .NET code Copyright © Stateless Contributors 2009-$([System.DateTime]::Now.ToString(yyyy)) en-US - 5.15.0 + 5.16.0 Stateless Contributors true true diff --git a/src/Stateless/TransitioningTriggerBehaviour.cs b/src/Stateless/TransitioningTriggerBehaviour.cs index 269b95e1..417a8cf2 100644 --- a/src/Stateless/TransitioningTriggerBehaviour.cs +++ b/src/Stateless/TransitioningTriggerBehaviour.cs @@ -12,12 +12,6 @@ public TransitioningTriggerBehaviour(TTrigger trigger, TState destination, Trans { Destination = destination; } - - public override bool ResultsInTransitionFrom(TState source, object[] args, out TState destination) - { - destination = Destination; - return true; - } } } } diff --git a/src/Stateless/TriggerBehaviour.cs b/src/Stateless/TriggerBehaviour.cs index 331ec520..f878e6e1 100644 --- a/src/Stateless/TriggerBehaviour.cs +++ b/src/Stateless/TriggerBehaviour.cs @@ -37,7 +37,7 @@ protected TriggerBehaviour(TTrigger trigger, TransitionGuard guard) internal ICollection> Guards =>_guard.Guards; /// - /// GuardConditionsMet is true if all of the guard functions return true + /// GuardConditionsMet is true if all the guard functions return true /// or if there are no guard functions /// public bool GuardConditionsMet(params object[] args) => _guard.GuardConditionsMet(args); @@ -47,8 +47,6 @@ protected TriggerBehaviour(TTrigger trigger, TransitionGuard guard) /// whose guard function returns false /// public ICollection UnmetGuardConditions(object[] args) => _guard.UnmetGuardConditions(args); - - public abstract bool ResultsInTransitionFrom(TState source, object[] args, out TState destination); } } } diff --git a/test/Stateless.Tests/AsyncActionsFixture.cs b/test/Stateless.Tests/AsyncActionsFixture.cs index 79ff95f5..aed8ba58 100644 --- a/test/Stateless.Tests/AsyncActionsFixture.cs +++ b/test/Stateless.Tests/AsyncActionsFixture.cs @@ -1,14 +1,14 @@ #if TASKS using System; -using System.Threading.Tasks; using System.Collections.Generic; - +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; using Xunit; namespace Stateless.Tests { - public class AsyncActionsFixture { [Fact] @@ -541,10 +541,33 @@ public async Task OnEntryFromAsync_WhenEnteringByAnotherTrigger_InvokesAction() Assert.False(wasInvoked); } + [Fact] + public async Task FireAsyncTriggerWithParametersArray() + { + const string expectedParam = "42-Stateless-True-123.45-Y"; + string actualParam = null; + + var sm = new StateMachine(State.A); + + sm.Configure(State.A) + .Permit(Trigger.X, State.B); + + sm.Configure(State.B) + .OnEntryAsync(t => + { + 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); + + Assert.Equal(expectedParam, actualParam); + } + [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); @@ -555,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/IgnoredTriggerBehaviourFixture.cs b/test/Stateless.Tests/IgnoredTriggerBehaviourFixture.cs index 63d120ab..de516805 100644 --- a/test/Stateless.Tests/IgnoredTriggerBehaviourFixture.cs +++ b/test/Stateless.Tests/IgnoredTriggerBehaviourFixture.cs @@ -8,8 +8,12 @@ public class IgnoredTriggerBehaviourFixture [Fact] public void StateRemainsUnchanged() { - var ignored = new StateMachine.IgnoredTriggerBehaviour(Trigger.X, null); - Assert.False(ignored.ResultsInTransitionFrom(State.B, new object[0], out _)); + var sm = new StateMachine(State.A); + sm.Configure(State.A).Ignore(Trigger.X); + + sm.Fire(Trigger.X); + + Assert.Equal(State.A, sm.State); } [Fact] @@ -21,7 +25,7 @@ public void ExposesCorrectUnderlyingTrigger() Assert.Equal(Trigger.X, ignored.Trigger); } - protected bool False(params object[] args) + private bool False(params object[] args) { return false; } @@ -35,7 +39,7 @@ public void WhenGuardConditionFalse_IsGuardConditionMetIsFalse() Assert.False(ignored.GuardConditionsMet()); } - protected bool True(params object[] args) + private bool True(params object[] args) { return true; } @@ -48,6 +52,7 @@ public void WhenGuardConditionTrue_IsGuardConditionMetIsTrue() Assert.True(ignored.GuardConditionsMet()); } + [Fact] public void IgnoredTriggerMustBeIgnoredSync() { 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); + } } } diff --git a/test/Stateless.Tests/StateMachineFixture.cs b/test/Stateless.Tests/StateMachineFixture.cs index f89b8560..91cb42ac 100644 --- a/test/Stateless.Tests/StateMachineFixture.cs +++ b/test/Stateless.Tests/StateMachineFixture.cs @@ -115,7 +115,7 @@ public void WhenInSubstate_TriggerSuperStateTwiceToSameSubstate_DoesNotReenterSu var eCount = 0; sm.Configure(State.B) - .OnEntry(() => { eCount++;}) + .OnEntry(() => { eCount++; }) .SubstateOf(State.C); sm.Configure(State.A) @@ -721,6 +721,185 @@ public void ExceptionWhenPermitIfHasMultipleNonExclusiveGuards() Assert.Throws(() => sm.Fire(x, 2)); } + [Fact] + public void CanFire_When_Transition_Is_Conditional_On_Default_Value_And_Trigger_Parameters_Are_Omitted_Returns_True() + { + // This test verifies behavior in CanFire that may be considered a bug. + // When a PermitIf transition is configured with a parameterized trigger and the trigger is fired without parameters, + // CanFire will test the transition with the default values of the omitted trigger parameters' types. + var sm = new StateMachine(State.A); + var valueTrigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A).PermitIf(valueTrigger, State.B, i => i == default); + + Assert.True(sm.CanFire(Trigger.X)); + } + + [Fact] + public void CanFire_When_Transition_Is_Conditional_On_Non_Default_Value_And_Trigger_Parameters_Are_Omitted_Returns_False() + { + // This test verifies behavior in CanFire that may be considered a bug. + // When a PermitIf transition is configured with a parameterized trigger and the trigger is fired without parameters, + // CanFire will test the transition with the default values of the omitted trigger parameters' types. + var sm = new StateMachine(State.A); + var valueTrigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A).PermitIf(valueTrigger, State.B, i => i == 1); + + Assert.False(sm.CanFire(Trigger.X)); + } + + [Fact] + public void CanFire_When_Transition_Is_Conditional_And_One_Trigger_Parameter_Is_Used_And_Condition_Is_Met_Returns_True() + { + var sm = new StateMachine(State.A); + var valueTrigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A).PermitIf(valueTrigger, State.B, i => i == 1); + + Assert.True(sm.CanFire(valueTrigger, 1)); + } + + [Fact] + public void CanFire_When_Transition_Is_Conditional_And_One_Trigger_Parameter_Is_Used_And_Condition_Is_NotMet_Returns_False() + { + var sm = new StateMachine(State.A); + var valueTrigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A).PermitIf(valueTrigger, State.B, i => i != 1); + + Assert.False(sm.CanFire(valueTrigger, 1)); + } + + [Fact] + public void CanFire_When_Transition_Is_Conditional_And_Two_Trigger_Parameters_Are_Used_And_Condition_Is_Met_Returns_True() + { + var sm = new StateMachine(State.A); + var valueTrigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A).PermitIf(valueTrigger, State.B, (i, s) => i == 1 && s.Equals("X", StringComparison.Ordinal)); + + Assert.True(sm.CanFire(valueTrigger, 1, "X")); + } + + [Fact] + public void CanFire_When_Transition_Is_Conditional_And_Two_Trigger_Parameters_Are_Used_And_Condition_Is_Not_Met_Returns_False() + { + var sm = new StateMachine(State.A); + var valueTrigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A).PermitIf(valueTrigger, State.B, (i, s) => i != 1 && s.Equals("Y", StringComparison.Ordinal)); + + Assert.False(sm.CanFire(valueTrigger, 1, "X")); + } + + [Fact] + public void CanFire_When_Transition_Is_Conditional_And_Three_Trigger_Parameters_Are_Used_And_Condition_Is_Met_Returns_True() + { + var sm = new StateMachine(State.A); + var valueTrigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A).PermitIf(valueTrigger, State.B, (i, s, b) => i == 1 && s.Equals("X", StringComparison.Ordinal) && b); + + Assert.True(sm.CanFire(valueTrigger, 1, "X", true)); + } + + [Fact] + public void CanFire_When_Transition_Is_Conditional_And_Three_Trigger_Parameters_Are_Used_And_Condition_Is_Not_Met_Returns_False() + { + var sm = new StateMachine(State.A); + var valueTrigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A).PermitIf(valueTrigger, State.B, (i, s, b) => i != 1 && s.Equals("Y", StringComparison.Ordinal) && !b); + + Assert.False(sm.CanFire(valueTrigger, 1, "X", true)); + } + + [Fact] + public void CanFire_When_Transition_Is_Contidional_And_Has_One_Trigger_Parameter_And_A_Guard_Condition_Is_Met_Returns_Empty_Collection() + { + var sm = new StateMachine(State.A); + var valueTrigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A) + .PermitIf(valueTrigger, State.B, i => i == 1, "i equal to 1") + .PermitIf(valueTrigger, State.B, i => i == 2, "i equal to 2"); + + bool result = sm.CanFire(valueTrigger, 1, out ICollection unmetGuards); + + Assert.True(result); + Assert.False(unmetGuards.Any()); + } + + [Fact] + public void CanFire_When_Transition_Is_Contidional_And_Has_One_Trigger_Parameter_And_No_Guard_Conditions_Are_Met_Returns_Guard_Conditions() + { + var sm = new StateMachine(State.A); + var valueTrigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A) + .PermitIf(valueTrigger, State.B, i => i == 1, "i equal to 1") + .PermitIf(valueTrigger, State.B, i => i == 2, "i equal to 2"); + + bool result = sm.CanFire(valueTrigger, 3, out ICollection unmetGuards); + + Assert.Collection(unmetGuards, + item => Assert.Equal("i equal to 1", item), + item => Assert.Equal("i equal to 2", item)); + } + + [Fact] + public void CanFire_When_Transition_Is_Contidional_And_Has_Two_Trigger_Parameters_And_A_Guard_Condition_Is_Met_Returns_Empty_Collection() + { + var sm = new StateMachine(State.A); + var valueTrigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A) + .PermitIf(valueTrigger, State.B, (i, s) => i == 1 && s == "X", "i equal to 1 and s equal to 'X'") + .PermitIf(valueTrigger, State.B, (i, s) => i == 2 && s == "X", "i equal to 2 and s equal to 'Y'"); + + bool result = sm.CanFire(valueTrigger, 1, "X", out ICollection unmetGuards); + + Assert.True(result); + Assert.False(unmetGuards.Any()); + } + + [Fact] + public void CanFire_When_Transition_Is_Contidional_And_Has_Two_Trigger_Parameters_And_No_Guard_Conditions_Are_Met_Returns_Guard_Conditions() + { + var sm = new StateMachine(State.A); + var valueTrigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A) + .PermitIf(valueTrigger, State.B, (i, s) => i == 1 && s == "X", "i equal to 1 and s equal to 'X'") + .PermitIf(valueTrigger, State.B, (i, s) => i == 2 && s == "X", "i equal to 2 and s equal to 'Y'"); + + bool result = sm.CanFire(valueTrigger, 3, "Z", out ICollection unmetGuards); + + Assert.Collection(unmetGuards, + item => Assert.Equal("i equal to 1 and s equal to 'X'", item), + item => Assert.Equal("i equal to 2 and s equal to 'Y'", item)); + } + + [Fact] + public void CanFire_When_Transition_Is_Contidional_And_Has_Three_Trigger_Parameters_And_A_Guard_Condition_Is_Met_Returns_Empty_Collection() + { + var sm = new StateMachine(State.A); + var valueTrigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A) + .PermitIf(valueTrigger, State.B, (i, s, b) => i == 1 && s == "X", "i equal to 1 and s equal to 'X' and boolean is true") + .PermitIf(valueTrigger, State.B, (i, s, b) => i == 2 && s == "X", "i equal to 2 and s equal to 'Y' and boolean is true"); + + bool result = sm.CanFire(valueTrigger, 1, "X", true, out ICollection unmetGuards); + + Assert.True(result); + Assert.False(unmetGuards.Any()); + } + + [Fact] + public void CanFire_When_Transition_Is_Contidional_And_Has_Three_Trigger_Parameters_And_No_Guard_Conditions_Are_Met_Returns_Guard_Conditions() + { + var sm = new StateMachine(State.A); + var valueTrigger = sm.SetTriggerParameters(Trigger.X); + sm.Configure(State.A) + .PermitIf(valueTrigger, State.B, (i, s, b) => i == 1 && s == "X", "i equal to 1 and s equal to 'X' and boolean is true") + .PermitIf(valueTrigger, State.B, (i, s, b) => i == 2 && s == "X", "i equal to 2 and s equal to 'Y' and boolean is true"); + + bool result = sm.CanFire(valueTrigger, 3, "Z", false, out ICollection unmetGuards); + + Assert.Collection(unmetGuards, + item => Assert.Equal("i equal to 1 and s equal to 'X' and boolean is true", item), + item => Assert.Equal("i equal to 2 and s equal to 'Y' and boolean is true", item)); + } + [Fact] public void TransitionWhenPermitDyanmicIfHasMultipleExclusiveGuards() { @@ -1048,7 +1227,7 @@ public void CanFire_GetUnmetGuardDescriptionsIfGuardFails() const string guardDescription = "Guard failed"; var sm = new StateMachine(State.A); sm.Configure(State.A) - .PermitIf(Trigger.X, State.B, ()=> false, guardDescription); + .PermitIf(Trigger.X, State.B, () => false, guardDescription); bool result = sm.CanFire(Trigger.X, out ICollection unmetGuards); diff --git a/test/Stateless.Tests/SynchronizationContextFixture.cs b/test/Stateless.Tests/SynchronizationContextFixture.cs index 26eaccf6..c4f8cacd 100644 --- a/test/Stateless.Tests/SynchronizationContextFixture.cs +++ b/test/Stateless.Tests/SynchronizationContextFixture.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -90,7 +89,7 @@ public async Task Activation_of_state_with_superstate_should_retain_SyncContext( sm.Configure(State.A) .OnActivateAsync(CaptureThenLoseSyncContext) .SubstateOf(State.B); - ; + sm.Configure(State.B) .OnActivateAsync(CaptureThenLoseSyncContext); @@ -110,7 +109,7 @@ public async Task Deactivation_of_state_with_superstate_should_retain_SyncContex sm.Configure(State.A) .OnDeactivateAsync(CaptureThenLoseSyncContext) .SubstateOf(State.B); - ; + sm.Configure(State.B) .OnDeactivateAsync(CaptureThenLoseSyncContext); diff --git a/test/Stateless.Tests/TransitioningTriggerBehaviourFixture.cs b/test/Stateless.Tests/TransitioningTriggerBehaviourFixture.cs index 6c739560..486113ff 100644 --- a/test/Stateless.Tests/TransitioningTriggerBehaviourFixture.cs +++ b/test/Stateless.Tests/TransitioningTriggerBehaviourFixture.cs @@ -8,8 +8,7 @@ public class TransitioningTriggerBehaviourFixture public void TransitionsToDestinationState() { var transitioning = new StateMachine.TransitioningTriggerBehaviour(Trigger.X, State.C, null); - Assert.True(transitioning.ResultsInTransitionFrom(State.B, new object[0], out State destination)); - Assert.Equal(State.C, destination); + Assert.Equal(State.C, transitioning.Destination); } } }