StateMachine tests and support for child states.
MODIFY
WalletWasabi.Fluent/State/StateMachine.cs
WalletWasabi.Fluent/State/StateMachine.cs
@@ -37,6 +37,35 @@ public class StateMachine<TState, TTrigger> where TTrigger : Enum where TState :
return this;
}
+ public bool IsAncestorOf(TState state, TState parent)
+ {
+ if (_states.ContainsKey(state))
+ {
+ StateContext current = _states[state];
+
+ while (true)
+ {
+ if (current.StateId.Equals(parent))
+ {
+ return true;
+ }
+
+ if (current.Parent is { })
+ {
+ current = current.Parent;
+ }
+ else
+ {
+ return false;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public bool IsInState(TState state) => IsAncestorOf(State, state);
+
public StateContext Configure(TState state)
{
return _states[state];
@@ -46,7 +75,14 @@ public class StateMachine<TState, TTrigger> where TTrigger : Enum where TState :
{
if (_currentState.CanTransit(trigger))
{
- Goto(trigger, _currentState.GetDestination(trigger));
+ var destination = _currentState.GetDestination(trigger);
+
+ if (_states.ContainsKey(destination) && _states[destination].Parent is { } parent && !IsInState(parent.StateId))
+ {
+ Goto(trigger, parent.StateId);
+ }
+
+ Goto(trigger, destination);
}
else if (_currentState.Parent is { } && _currentState.Parent.CanTransit(trigger))
{
@@ -68,7 +104,7 @@ public class StateMachine<TState, TTrigger> where TTrigger : Enum where TState :
{
if (_states.ContainsKey(state))
{
- if (exit)
+ if (exit && !IsAncestorOf(state, _currentState.StateId))
{
_currentState.Exit();
}

MODIFY
WalletWasabi.Tests/UnitTests/StateMachineTests.cs
WalletWasabi.Tests/UnitTests/StateMachineTests.cs
@@ -176,6 +176,90 @@ public class StateMachineTests
Assert.Throws<InvalidOperationException>(ConfigureForReEntry);
}
+ [Fact]
+ public void Transition_direct_to_child_state_enters_via_parent()
+ {
+ StateMachine<JukeBoxState, JukeBoxTrigger> sut =
+ new StateMachine<JukeBoxState, JukeBoxTrigger>(JukeBoxState.Playing);
+
+ sut.Configure(JukeBoxState.Playing)
+ .Permit(JukeBoxTrigger.Pause, JukeBoxState.PausedChild);
+
+ bool pausedEntered = false;
+ bool pausedChildEntered = false;
+ int enteredPausedCount = 0;
+
+ sut.Configure(JukeBoxState.Paused)
+ .Permit(JukeBoxTrigger.Play, JukeBoxState.Playing)
+ .OnEntry(() =>
+ {
+ pausedEntered = true;
+ enteredPausedCount++;
+ });
+
+ sut.Configure(JukeBoxState.PausedChild)
+ .SubstateOf(JukeBoxState.Paused)
+ .OnEntry(() =>
+ {
+ pausedChildEntered = true;
+ enteredPausedCount++;
+ });
+
+ sut.Start();
+
+ sut.Fire(JukeBoxTrigger.Pause);
+
+ Assert.Equal(JukeBoxState.PausedChild, sut.State);
+ Assert.True(sut.IsInState(JukeBoxState.Paused));
+
+ Assert.True(pausedEntered);
+ Assert.True(pausedChildEntered);
+ Assert.Equal(2, enteredPausedCount);
+ }
+
+ [Fact]
+ public void Transition_direct_to_parent_state_exits_parents()
+ {
+ StateMachine<JukeBoxState, JukeBoxTrigger> sut =
+ new StateMachine<JukeBoxState, JukeBoxTrigger>(JukeBoxState.Playing);
+
+ sut.Configure(JukeBoxState.Playing)
+ .Permit(JukeBoxTrigger.Pause, JukeBoxState.PausedChild);
+
+ bool pausedExited = false;
+ bool pausedChildExited = false;
+ int exitedPausedCount = 0;
+
+ sut.Configure(JukeBoxState.Paused)
+ .Permit(JukeBoxTrigger.Play, JukeBoxState.Playing)
+ .OnExit(() =>
+ {
+ pausedExited = true;
+ exitedPausedCount++;
+ });
+
+ sut.Configure(JukeBoxState.PausedChild)
+ .SubstateOf(JukeBoxState.Paused)
+ .OnExit(() =>
+ {
+ pausedChildExited = true;
+ exitedPausedCount++;
+ });
+
+ sut.Start();
+
+ sut.Fire(JukeBoxTrigger.Pause);
+
+ Assert.Equal(JukeBoxState.PausedChild, sut.State);
+ Assert.True(sut.IsInState(JukeBoxState.Paused));
+
+ sut.Fire(JukeBoxTrigger.Play);
+
+ Assert.Equal(JukeBoxState.Playing, sut.State);
+ Assert.False(sut.IsInState(JukeBoxState.Paused));
+ Assert.False(sut.IsInState(JukeBoxState.PausedChild));
+ }
+
private enum PhoneState
{
Disconnected,
@@ -190,4 +274,18 @@ public class StateMachineTests
ReleaseOnHold,
Ping
}
+
+ private enum JukeBoxTrigger
+ {
+ Play,
+ Pause
+ }
+
+ private enum JukeBoxState
+ {
+ Playing,
+ Paused,
+ PausedChild
+ }
+
}
\ No newline at end of file

StateMachine tests and support for child states.