Skip to content

Commit

Permalink
dotnet-state-machine#565 Permit state reentry from dynamic transition…
Browse files Browse the repository at this point in the history
…s with FireAsync; retrofit dotnet-state-machine#544 onto FireAsync.
  • Loading branch information
mclift committed Apr 22, 2024
1 parent 18d7141 commit 7c6511c
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 32 deletions.
13 changes: 12 additions & 1 deletion src/Stateless/StateMachine.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
60 changes: 30 additions & 30 deletions src/Stateless/StateMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ private class QueuedTrigger
/// </summary>
/// <param name="stateAccessor">A function that will be called to read the current state value.</param>
/// <param name="stateMutator">An action that will be called to write new state values.</param>
public StateMachine(Func<TState> stateAccessor, Action<TState> stateMutator) :this(stateAccessor, stateMutator, FiringMode.Queued)
public StateMachine(Func<TState> stateAccessor, Action<TState> stateMutator) : this(stateAccessor, stateMutator, FiringMode.Queued)
{
}

Expand Down Expand Up @@ -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.");
}
Expand Down Expand Up @@ -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);

Expand All @@ -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);
Expand Down
22 changes: 22 additions & 0 deletions test/Stateless.Tests/AsyncActionsFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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, Trigger>(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);
}
}
}

Expand Down
168 changes: 168 additions & 0 deletions test/Stateless.Tests/DynamicTriggerBehaviourAsyncFixture.cs
Original file line number Diff line number Diff line change
@@ -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, Trigger>(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, Trigger>(State.A);
var trigger = sm.SetTriggerParameters<int>(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, Trigger>(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, Trigger>(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, Trigger>(State.A);
var trigger = sm.SetTriggerParameters<int>(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, Trigger>(State.A);
var trigger = sm.SetTriggerParameters<int, int>(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, Trigger>(State.A);
var trigger = sm.SetTriggerParameters<int, int, int>(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, Trigger>(State.A);
var trigger = sm.SetTriggerParameters<int>(Trigger.X);
sm.Configure(State.A)
.PermitDynamicIf(trigger, (i) => i > 0 ? State.C : State.B, (i) => i == 2 ? true : false);

await Assert.ThrowsAsync<InvalidOperationException>(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, Trigger>(State.A);
var trigger = sm.SetTriggerParameters<int, int>(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<InvalidOperationException>(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, Trigger>(State.A);
var trigger = sm.SetTriggerParameters<int, int, int>(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<InvalidOperationException>(async () => await sm.FireAsync(trigger, 1, 2, 3));
}

[Fact]
public async Task PermitDynamicIf_Permits_Reentry_When_GuardCondition_Met_Async()
{
var sm = new StateMachine<State, Trigger>(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);
}
}
}
2 changes: 1 addition & 1 deletion test/Stateless.Tests/DynamicTriggerBehaviourFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Stateless.Tests
{
public class DynamicTriggerBehaviour
public class DynamicTriggerBehaviourFixture
{
[Fact]
public void PermitDynamic_Selects_Expected_State()
Expand Down

0 comments on commit 7c6511c

Please sign in to comment.