diff --git a/src/Stateless/StateMachine.cs b/src/Stateless/StateMachine.cs index 121d6ede..bb9fa877 100644 --- a/src/Stateless/StateMachine.cs +++ b/src/Stateless/StateMachine.cs @@ -570,6 +570,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) @@ -579,8 +585,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. @@ -589,6 +651,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/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);