Skip to content

Commit

Permalink
Merge branch 'dev' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
mclift authored Dec 23, 2024
2 parents 389f156 + a05ed25 commit 800849e
Show file tree
Hide file tree
Showing 14 changed files with 1,238 additions and 5 deletions.
27 changes: 27 additions & 0 deletions src/Stateless/DynamicTriggerBehaviour.Async.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Threading.Tasks;

namespace Stateless
{
public partial class StateMachine<TState, TTrigger>
{
internal class DynamicTriggerBehaviourAsync : TriggerBehaviour
{
readonly Func<object[], Task<TState>> _destination;
internal Reflection.DynamicTransitionInfo TransitionInfo { get; private set; }

public DynamicTriggerBehaviourAsync(TTrigger trigger, Func<object[], Task<TState>> destination,
TransitionGuard transitionGuard, Reflection.DynamicTransitionInfo info)
: base(trigger, transitionGuard)
{
_destination = destination ?? throw new ArgumentNullException(nameof(destination));
TransitionInfo = info ?? throw new ArgumentNullException(nameof(info));
}

public async Task<TState> GetDestinationState(TState source, object[] args)
{
return await _destination(args);
}
}
}
}
4 changes: 4 additions & 0 deletions src/Stateless/Reflection/StateInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ internal static void AddRelationships<TState, TTrigger>(StateInfo info, StateMac
{
dynamicTransitions.Add(((StateMachine<TState, TTrigger>.DynamicTriggerBehaviour)item).TransitionInfo);
}
foreach (var item in triggerBehaviours.Value.Where(behaviour => behaviour is StateMachine<TState, TTrigger>.DynamicTriggerBehaviourAsync))
{
dynamicTransitions.Add(((StateMachine<TState, TTrigger>.DynamicTriggerBehaviourAsync)item).TransitionInfo);
}
}

info.AddRelationships(superstate, substates, fixedTransitions, dynamicTransitions);
Expand Down
556 changes: 556 additions & 0 deletions src/Stateless/StateConfiguration.Async.cs

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions src/Stateless/StateMachine.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,15 @@ async Task InternalFireOneAsync(TTrigger trigger, params object[] args)
// Handle transition, and set new state
var transition = new Transition(source, handler.Destination, trigger, args);
await HandleReentryTriggerAsync(args, representativeState, transition);
break;
}
case DynamicTriggerBehaviourAsync asyncHandler:
{
var destination = await asyncHandler.GetDestinationState(source, args);
// 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 DynamicTriggerBehaviour handler:
Expand Down
12 changes: 12 additions & 0 deletions src/Stateless/StateMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,18 @@ private void InternalFireOne(TTrigger trigger, params object[] args)
HandleReentryTrigger(args, representativeState, transition);
break;
}
case DynamicTriggerBehaviourAsync asyncHandler:
{
asyncHandler.GetDestinationState(source, args)
.ContinueWith(t =>
{
var destination = t.Result;
// Handle transition, and set new state; reentry is permitted from dynamic trigger behaviours.
var transition = new Transition(source, destination, trigger, args);
return HandleTransitioningTriggerAsync(args, representativeState, transition);
});
break;
}
case DynamicTriggerBehaviour handler:
{
handler.GetDestinationState(source, args, out var destination);
Expand Down
72 changes: 72 additions & 0 deletions test/Stateless.Tests/DotGraphFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Xunit;
using Stateless.Reflection;
using Stateless.Graph;
using System.Threading.Tasks;

namespace Stateless.Tests
{
Expand Down Expand Up @@ -271,6 +272,27 @@ public void DestinationStateIsDynamic()

string dotGraph = UmlDotGraph.Format(sm.GetInfo());

#if WRITE_DOTS_TO_FOLDER
System.IO.File.WriteAllText(DestinationFolder + "DestinationStateIsDynamic.dot", dotGraph);
#endif

Assert.Equal(expected, dotGraph);
}

[Fact]
public void DestinationStateIsDynamicAsync()
{
var expected = Prefix(Style.UML)
+ Box(Style.UML, "A")
+ Decision(Style.UML, "Decision1", "Function")
+ Line("A", "Decision1", "X") + suffix;

var sm = new StateMachine<State, Trigger>(State.A);
sm.Configure(State.A)
.PermitDynamicAsync(Trigger.X, () => Task.FromResult(State.B));

string dotGraph = UmlDotGraph.Format(sm.GetInfo());

#if WRITE_DOTS_TO_FOLDER
System.IO.File.WriteAllText(DestinationFolder + "DestinationStateIsDynamic.dot", dotGraph);
#endif
Expand All @@ -293,6 +315,27 @@ public void DestinationStateIsCalculatedBasedOnTriggerParameters()

string dotGraph = UmlDotGraph.Format(sm.GetInfo());

#if WRITE_DOTS_TO_FOLDER
System.IO.File.WriteAllText(DestinationFolder + "DestinationStateIsCalculatedBasedOnTriggerParameters.dot", dotGraph);
#endif
Assert.Equal(expected, dotGraph);
}

[Fact]
public void DestinationStateIsCalculatedBasedOnTriggerParametersAsync()
{
var expected = Prefix(Style.UML)
+ Box(Style.UML, "A")
+ Decision(Style.UML, "Decision1", "Function")
+ Line("A", "Decision1", "X") + suffix;

var sm = new StateMachine<State, Trigger>(State.A);
var trigger = sm.SetTriggerParameters<int>(Trigger.X);
sm.Configure(State.A)
.PermitDynamicAsync(trigger, i =>Task.FromResult(i == 1 ? State.B : State.C));

string dotGraph = UmlDotGraph.Format(sm.GetInfo());

#if WRITE_DOTS_TO_FOLDER
System.IO.File.WriteAllText(DestinationFolder + "DestinationStateIsCalculatedBasedOnTriggerParameters.dot", dotGraph);
#endif
Expand Down Expand Up @@ -523,6 +566,35 @@ public void UmlWithDynamic()
Assert.Equal(expected, dotGraph);
}

[Fact]
public void UmlWithDynamicAsync()
{
var expected = Prefix(Style.UML)
+ Box(Style.UML, "A")
+ Box(Style.UML, "B")
+ Box(Style.UML, "C")
+ Decision(Style.UML, "Decision1", "Function")
+ Line("A", "Decision1", "X")
+ Line("Decision1", "B", "X [ChoseB]")
+ Line("Decision1", "C", "X [ChoseC]")
+ suffix;

var sm = new StateMachine<State, Trigger>(State.A);

sm.Configure(State.A)
.PermitDynamicAsync(Trigger.X, () => Task.FromResult(DestinationSelector()), null, new DynamicStateInfos { { State.B, "ChoseB" }, { State.C, "ChoseC" } });

sm.Configure(State.B);
sm.Configure(State.C);

string dotGraph = UmlDotGraph.Format(sm.GetInfo());
#if WRITE_DOTS_TO_FOLDER
System.IO.File.WriteAllText(DestinationFolder + "UmlWithDynamic.dot", dotGraph);
#endif

Assert.Equal(expected, dotGraph);
}

[Fact]
public void TransitionWithIgnoreAndEntry()
{
Expand Down
168 changes: 168 additions & 0 deletions test/Stateless.Tests/DynamicAsyncTriggerBehaviourAsyncFixture.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 DynamicAsyncTriggerBehaviourAsyncFixture
{
[Fact]
public async Task PermitDynamic_Selects_Expected_State_Async()
{
var sm = new StateMachine<State, Trigger>(State.A);
sm.Configure(State.A)
.PermitDynamicAsync(Trigger.X, async () => { await Task.Delay(100); return 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)
.PermitDynamicAsync(trigger, async (i) => { await Task.Delay(100); return 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)
.PermitDynamicAsync(Trigger.X, async () => { await Task.Delay(100); return 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)
.PermitDynamicAsync(Trigger.X, async () => { await Task.Delay(100); return 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)
.PermitDynamicIfAsync(trigger, async (i) =>{ await Task.Delay(100); return 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).PermitDynamicIfAsync(
trigger,
async (i, j) => { await Task.Yield(); return 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).PermitDynamicIfAsync(
trigger,
async (i, j, k) => { await Task.Delay(100); return 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)
.PermitDynamicIfAsync(trigger, async (i) => { await Task.Delay(100); return i > 0 ? State.C : State.B; }, guard: (i) => i == 2);

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).PermitDynamicIfAsync(
trigger,
async (i, j) => { await Task.Delay(100); return 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).PermitDynamicIfAsync(trigger,
async (i, j, k) => { await Task.Delay(100); return 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)
.PermitDynamicIfAsync(Trigger.X, async () =>{ await Task.Delay(100); return 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);
}
}
}
Loading

0 comments on commit 800849e

Please sign in to comment.