Skip to content

Commit

Permalink
Merge pull request #571 from mclift/565-reentry-from-dynamic-transitions
Browse files Browse the repository at this point in the history
Permit state reentry from dynamic transitions.
  • Loading branch information
mclift authored Apr 24, 2024
2 parents 5fa762b + 99947d2 commit 67b18fa
Show file tree
Hide file tree
Showing 8 changed files with 469 additions and 93 deletions.
4 changes: 2 additions & 2 deletions example/AlarmExample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -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.");
}
Expand Down
130 changes: 86 additions & 44 deletions src/Stateless/StateConfiguration.cs

Large diffs are not rendered by default.

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
59 changes: 33 additions & 26 deletions src/Stateless/StateMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
namespace Stateless
{
/// <summary>
/// Enum for the different modes used when Fire-ing a trigger
/// Enum for the different modes used when <c>Fire</c>ing a trigger
/// </summary>
public enum FiringMode
{
/// <summary> 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.</summary>
Immediate,
/// <summary> Use the queued Fire-ing mode when run-to-completion is required. This is the recommended mode.</summary>
/// <summary> Use the queued <c>Fire</c>ing mode when run-to-completion is required. This is the recommended mode.</summary>
Queued
}

Expand Down 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,32 +414,39 @@ void InternalFireOne(TTrigger trigger, params object[] args)
// Handle special case, re-entry in superstate
// Check if it is an internal transition, or a transition from one state to another.
case ReentryTriggerBehaviour handler:
{
// Handle transition, and set new state
var transition = new Transition(source, handler.Destination, trigger, args);
HandleReentryTrigger(args, representativeState, transition);
break;
}
{
// Handle transition, and set new state
var transition = new Transition(source, handler.Destination, trigger, args);
HandleReentryTrigger(args, representativeState, transition);
break;
}
case DynamicTriggerBehaviour _ when result.Handler.ResultsInTransitionFrom(source, args, out var destination):
case TransitioningTriggerBehaviour _ when result.Handler.ResultsInTransitionFrom(source, args, out destination):
{
//If a trigger was found on a superstate that would cause unintended reentry, don't trigger.
if (source.Equals(destination))
{
// Handle transition, and set new state; reentry is permitted from dynamic trigger behaviours.
var transition = new Transition(source, destination, trigger, args);
HandleTransitioningTrigger(args, representativeState, transition);

break;
}
case TransitioningTriggerBehaviour _ when result.Handler.ResultsInTransitionFrom(source, args, out var destination):
{
// If a trigger was found on a superstate that would cause unintended reentry, don't trigger.
if (source.Equals(destination))
break;

// Handle transition, and set new state
var transition = new Transition(source, destination, trigger, args);
HandleTransitioningTrigger(args, representativeState, transition);
// Handle transition, and set new state
var transition = new Transition(source, destination, trigger, args);
HandleTransitioningTrigger(args, representativeState, transition);

break;
}
break;
}
case InternalTriggerBehaviour _:
{
// Internal transitions does not update the current state, but must execute the associated action.
var transition = new Transition(source, source, trigger, args);
CurrentRepresentation.InternalAction(transition, args);
break;
}
{
// Internal transitions does not update the current state, but must execute the associated action.
var transition = new Transition(source, source, trigger, args);
CurrentRepresentation.InternalAction(transition, args);
break;
}
default:
throw new InvalidOperationException("State machine configuration incorrect, no handler for trigger.");
}
Expand Down Expand Up @@ -471,7 +478,7 @@ private void HandleReentryTrigger(object[] args, StateRepresentation representat
State = representation.UnderlyingState;
}

private void HandleTransitioningTrigger( object[] args, StateRepresentation representativeState, Transition transition)
private void HandleTransitioningTrigger(object[] args, StateRepresentation representativeState, Transition transition)
{
transition = representativeState.Exit(transition);

Expand All @@ -492,7 +499,7 @@ private void HandleTransitioningTrigger( object[] args, StateRepresentation repr
_onTransitionCompletedEvent.Invoke(new Transition(transition.Source, State, transition.Trigger, transition.Parameters));
}

private StateRepresentation EnterState(StateRepresentation representation, Transition transition, object [] args)
private StateRepresentation EnterState(StateRepresentation representation, Transition transition, object[] args)
{
// Enter the new state
representation.Enter(transition, args);
Expand Down
42 changes: 31 additions & 11 deletions test/Stateless.Tests/AsyncActionsFixture.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
#if TASKS

using System;
using System.Threading.Tasks;
using System.Collections.Generic;

using Xunit;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Xunit;

namespace Stateless.Tests
{

public class AsyncActionsFixture
{
[Fact]
Expand Down Expand Up @@ -545,7 +544,7 @@ public async Task OnEntryFromAsync_WhenEnteringByAnotherTrigger_InvokesAction()
[Fact]
public async Task FireAsyncTriggerWithParametersArray()
{
const string expectedParam = "42-Stateless-True-420.69-Y";
const string expectedParam = "42-Stateless-True-123.45-Y";
string actualParam = null;

var sm = new StateMachine<State, Trigger>(State.A);
Expand All @@ -556,20 +555,19 @@ public async Task FireAsyncTriggerWithParametersArray()
sm.Configure(State.B)
.OnEntryAsync(t =>
{
actualParam = string.Join("-", t.Parameters);
actualParam = string.Join("-", t.Parameters.Select(x => string.Format(CultureInfo.InvariantCulture, "{0}", x)));
return Task.CompletedTask;
});

await sm.FireAsync(Trigger.X, 42, "Stateless", true, 420.69, Trigger.Y);
await sm.FireAsync(Trigger.X, 42, "Stateless", true, 123.45, Trigger.Y);

Assert.Equal(expectedParam, actualParam);
}

[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, Trigger>(State.A);
Expand All @@ -580,16 +578,38 @@ public async Task FireAsync_TriggerWithMoreThanThreeParameters()
sm.Configure(State.B)
.OnEntryAsync(t =>
{
actualParam = string.Join("-", t.Parameters);
actualParam = string.Join("-", t.Parameters.Select(x => string.Format(CultureInfo.InvariantCulture, "{0}", x)));
return Task.CompletedTask;
});

var parameterizedX = sm.SetTriggerParameters(Trigger.X, typeof(int), typeof(string), typeof(bool), typeof(double), typeof(Trigger));

await sm.FireAsync(parameterizedX, 42, "Stateless", true, 420.69, Trigger.Y);
await sm.FireAsync(parameterizedX, 42, "Stateless", true, 123.45, Trigger.Y);

Assert.Equal(expectedParam, actualParam);
}

[Fact]
public async Task WhenInSubstate_TriggerSuperStateTwiceToSameSubstate_DoesNotReenterSubstate_Async()
{
var sm = new StateMachine<State, 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
Loading

0 comments on commit 67b18fa

Please sign in to comment.