From e1be34cc9295051b5a5f49e0a82a76c470d7edc9 Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Fri, 3 Apr 2015 10:47:14 +0100 Subject: [PATCH] Fix proper transition into super state - Big conceptual changes how state transitions are handled which now allows more easy ways to figure out what to do and where to go. - Exposing more info via State interface - Adding tests to samples - Fixes #30 --- .../state/AbstractSimpleState.java | 22 +- .../statemachine/state/AbstractState.java | 23 ++- .../statemachine/state/RegionState.java | 17 +- .../statemachine/state/State.java | 8 + .../statemachine/state/StateMachineState.java | 29 +-- .../support/AbstractStateMachine.java | 131 +++++++++--- .../java/demo/cdplayer/CdPlayerTests.java | 193 +++++++++++++++--- .../main/java/demo/showcase/Application.java | 43 +++- .../src/main/resources/statechartmodel.txt | 13 +- .../java/demo/showcase/ShowcaseTests.java | 76 ++++++- 10 files changed, 448 insertions(+), 107 deletions(-) diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractSimpleState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractSimpleState.java index 4b9421cd..f52feb76 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractSimpleState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractSimpleState.java @@ -52,7 +52,8 @@ public abstract class AbstractSimpleState extends AbstractState { * @param entryActions the entry actions * @param exitActions the exit actions */ - public AbstractSimpleState(S id, Collection deferred, Collection> entryActions, Collection> exitActions) { + public AbstractSimpleState(S id, Collection deferred, Collection> entryActions, + Collection> exitActions) { this(id, deferred, entryActions, exitActions, null); } @@ -86,8 +87,8 @@ public abstract class AbstractSimpleState extends AbstractState { * @param pseudoState the pseudo state * @param regions the regions */ - public AbstractSimpleState(S id, Collection deferred, Collection> entryActions, Collection> exitActions, - PseudoState pseudoState, Collection> regions) { + public AbstractSimpleState(S id, Collection deferred, Collection> entryActions, + Collection> exitActions, PseudoState pseudoState, Collection> regions) { super(id, deferred, entryActions, exitActions, pseudoState, regions); this.ids = new ArrayList(); this.ids.add(id); @@ -103,8 +104,8 @@ public abstract class AbstractSimpleState extends AbstractState { * @param pseudoState the pseudo state * @param submachine the submachine */ - public AbstractSimpleState(S id, Collection deferred, Collection> entryActions, Collection> exitActions, - PseudoState pseudoState, StateMachine submachine) { + public AbstractSimpleState(S id, Collection deferred, Collection> entryActions, + Collection> exitActions, PseudoState pseudoState, StateMachine submachine) { super(id, deferred, entryActions, exitActions, pseudoState, submachine); this.ids = new ArrayList(); this.ids.add(id); @@ -119,8 +120,8 @@ public abstract class AbstractSimpleState extends AbstractState { * @param exitActions the exit actions * @param pseudoState the pseudo state */ - public AbstractSimpleState(S id, Collection deferred, Collection> entryActions, Collection> exitActions, - PseudoState pseudoState) { + public AbstractSimpleState(S id, Collection deferred, Collection> entryActions, + Collection> exitActions, PseudoState pseudoState) { super(id, deferred, entryActions, exitActions, pseudoState); this.ids = new ArrayList(); this.ids.add(id); @@ -131,4 +132,11 @@ public abstract class AbstractSimpleState extends AbstractState { return Collections.unmodifiableCollection(ids); } + @Override + public Collection> getStates() { + ArrayList> states = new ArrayList>(); + states.add(this); + return states; + } + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java index b39d07fd..62bb99a7 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java @@ -70,7 +70,8 @@ public abstract class AbstractState implements State { * @param entryActions the entry actions * @param exitActions the exit actions */ - public AbstractState(S id, Collection deferred, Collection> entryActions, Collection> exitActions) { + public AbstractState(S id, Collection deferred, Collection> entryActions, + Collection> exitActions) { this(id, deferred, entryActions, exitActions, null); } @@ -83,8 +84,8 @@ public abstract class AbstractState implements State { * @param exitActions the exit actions * @param pseudoState the pseudo state */ - public AbstractState(S id, Collection deferred, Collection> entryActions, Collection> exitActions, - PseudoState pseudoState) { + public AbstractState(S id, Collection deferred, Collection> entryActions, + Collection> exitActions, PseudoState pseudoState) { this(id, deferred, entryActions, exitActions, pseudoState, null, null); } @@ -98,8 +99,8 @@ public abstract class AbstractState implements State { * @param pseudoState the pseudo state * @param submachine the submachine */ - public AbstractState(S id, Collection deferred, Collection> entryActions, Collection> exitActions, - PseudoState pseudoState, StateMachine submachine) { + public AbstractState(S id, Collection deferred, Collection> entryActions, + Collection> exitActions, PseudoState pseudoState, StateMachine submachine) { this(id, deferred, entryActions, exitActions, pseudoState, null, submachine); } @@ -113,8 +114,8 @@ public abstract class AbstractState implements State { * @param pseudoState the pseudo state * @param regions the regions */ - public AbstractState(S id, Collection deferred, Collection> entryActions, Collection> exitActions, - PseudoState pseudoState, Collection> regions) { + public AbstractState(S id, Collection deferred, Collection> entryActions, + Collection> exitActions, PseudoState pseudoState, Collection> regions) { this(id, deferred, entryActions, exitActions, pseudoState, regions, null); } @@ -129,8 +130,9 @@ public abstract class AbstractState implements State { * @param regions the regions * @param submachine the submachine */ - private AbstractState(S id, Collection deferred, Collection> entryActions, Collection> exitActions, - PseudoState pseudoState, Collection> regions, StateMachine submachine) { + private AbstractState(S id, Collection deferred, Collection> entryActions, + Collection> exitActions, PseudoState pseudoState, Collection> regions, + StateMachine submachine) { this.id = id; this.deferred = deferred; this.entryActions = entryActions; @@ -164,6 +166,9 @@ public abstract class AbstractState implements State { @Override public abstract Collection getIds(); + @Override + public abstract Collection> getStates(); + @Override public PseudoState getPseudoState() { return pseudoState; diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/RegionState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/RegionState.java index 804ac7c5..0113f668 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/RegionState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/RegionState.java @@ -75,8 +75,8 @@ public class RegionState extends AbstractState { * @param exitActions the exit actions * @param pseudoState the pseudo state */ - public RegionState(S id, Collection> regions, Collection deferred, Collection> entryActions, Collection> exitActions, - PseudoState pseudoState) { + public RegionState(S id, Collection> regions, Collection deferred, + Collection> entryActions, Collection> exitActions, PseudoState pseudoState) { super(id, deferred, entryActions, exitActions, pseudoState, regions); } @@ -89,7 +89,8 @@ public class RegionState extends AbstractState { * @param entryActions the entry actions * @param exitActions the exit actions */ - public RegionState(S id, Collection> regions, Collection deferred, Collection> entryActions, Collection> exitActions) { + public RegionState(S id, Collection> regions, Collection deferred, + Collection> entryActions, Collection> exitActions) { super(id, deferred, entryActions, exitActions, null, regions); } @@ -150,4 +151,14 @@ public class RegionState extends AbstractState { return ids; } + @Override + public Collection> getStates() { + ArrayList> states = new ArrayList>(); + states.add(this); + for (Region r : getRegions()) { + states.addAll(r.getStates()); + } + return states; + } + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/State.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/State.java index f495a15e..ca1b4db0 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/State.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/State.java @@ -70,6 +70,14 @@ public interface State { */ Collection getIds(); + /** + * Gets all possible states this state knows about including itself + * and substates. + * + * @return all state including itself and nested states + */ + Collection> getStates(); + /** * Gets a {@link PseudoState} attached to a {@code State}. * {@link PseudoState} is not required and thus this method return diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java index 7d273874..99c11034 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java @@ -22,7 +22,6 @@ import org.springframework.messaging.Message; import org.springframework.statemachine.StateContext; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.action.Action; -import org.springframework.statemachine.support.LifecycleObjectSupport; import org.springframework.statemachine.transition.Transition; import org.springframework.statemachine.transition.TransitionKind; @@ -86,7 +85,8 @@ public class StateMachineState extends AbstractState { * @param exitActions the exit actions * @param pseudoState the pseudo state */ - public StateMachineState(S id, StateMachine submachine, Collection deferred, Collection> entryActions, Collection> exitActions, + public StateMachineState(S id, StateMachine submachine, Collection deferred, + Collection> entryActions, Collection> exitActions, PseudoState pseudoState) { super(id, deferred, entryActions, exitActions, pseudoState, submachine); this.ids = new ArrayList(); @@ -102,7 +102,8 @@ public class StateMachineState extends AbstractState { * @param entryActions the entry actions * @param exitActions the exit actions */ - public StateMachineState(S id, StateMachine submachine, Collection deferred, Collection> entryActions, Collection> exitActions) { + public StateMachineState(S id, StateMachine submachine, Collection deferred, + Collection> entryActions, Collection> exitActions) { super(id, deferred, entryActions, exitActions, null, submachine); this.ids = new ArrayList(); this.ids.add(id); @@ -119,13 +120,22 @@ public class StateMachineState extends AbstractState { return ret; } + @Override + public Collection> getStates() { + ArrayList> states = new ArrayList>(); + states.add(this); + for (State s : getSubmachine().getStates()) { + states.addAll(s.getStates()); + } + return states; + } + @Override public void exit(E event, StateContext context) { - getSubmachine().getState().exit(event, context); // don't stop if it looks like we're coming back // stop would cause start with entry which would // enable default transition and state - if (context.getTransition().getSource().getId() != getSubmachine().getState().getId()) { + if (getSubmachine().getState() != null && context.getTransition().getSource().getId() != getSubmachine().getState().getId()) { getSubmachine().stop(); } Collection> actions = getExitActions(); @@ -144,14 +154,9 @@ public class StateMachineState extends AbstractState { action.execute(context); } } + if (getPseudoState() != null && getPseudoState().getKind() == PseudoStateKind.INITIAL) { - if (((LifecycleObjectSupport)getSubmachine()).isRunning()) { - getSubmachine().getState().entry(event, context); - } else { - getSubmachine().start(); - } - } else { - getSubmachine().getState().entry(event, context); + getSubmachine().start(); } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java index a388f7a8..b028054e 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java @@ -27,7 +27,6 @@ import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; import java.util.Queue; -import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import org.apache.commons.logging.Log; @@ -56,7 +55,6 @@ import org.springframework.statemachine.state.State; import org.springframework.statemachine.transition.Transition; import org.springframework.statemachine.transition.TransitionKind; import org.springframework.statemachine.trigger.DefaultTriggerContext; -import org.springframework.statemachine.trigger.EventTrigger; import org.springframework.statemachine.trigger.TimerTrigger; import org.springframework.statemachine.trigger.Trigger; import org.springframework.statemachine.trigger.TriggerListener; @@ -259,6 +257,19 @@ public abstract class AbstractStateMachine extends LifecycleObjectSupport return transitions; } + @Override + public String toString() { + ArrayList> all = new ArrayList>(); + for (State s : states) { + all.addAll(s.getStates()); + } + StringBuilder buf = new StringBuilder(); + for (State s : all) { + buf.append(s.getId() + " "); + } + return buf.toString(); + } + protected boolean acceptEvent(Message event) { boolean accepted = currentState.sendEvent(event); @@ -294,7 +305,7 @@ public abstract class AbstractStateMachine extends LifecycleObjectSupport } private void switchToState(State state, Message event, Transition transition) { - setCurrentState(state, event, transition); + setCurrentState(state, event, transition, true); // TODO: should handle triggerles transition some how differently for (Transition t : transitions) { @@ -307,19 +318,79 @@ public abstract class AbstractStateMachine extends LifecycleObjectSupport } - void setCurrentState(State state, Message event, Transition transition) { + private State findDeepParent(State state) { + for (State s : states) { + if (s.getStates().contains(state)) { + return s; + } + } + return null; + } + + void setCurrentState(State statex, Message event, Transition transition, boolean exit) { + + State state = statex; + State findDeep = findDeepParent(state); + boolean isTargetSubOf = false; + if (transition != null) { + isTargetSubOf = isSubstate(state, transition.getSource()); + if (isTargetSubOf && currentState == transition.getTarget()) { + state = transition.getSource(); + } + } + if (states.contains(state)) { - exitFromState(currentState, event, transition); + if (exit) { + exitCurrentState(state, event, transition); + } State notifyFrom = currentState; currentState = state; entryToState(state, event, transition); notifyStateChanged(notifyFrom, state); - } else if (currentState.isSubmachineState()) { - // TODO: should find a better way to trick setting state for submachine - // without a need to access package protected method via casting - StateMachine submachine = ((AbstractState)currentState).getSubmachine(); - ((AbstractStateMachine)submachine).setCurrentState(state, event, transition); + } else if (currentState != null && currentState.isSubmachineState()) { + if (findDeep != null) { + if (exit) { + exitCurrentState(state, event, transition); + } + if (currentState == findDeep) { + StateMachine submachine = ((AbstractState)currentState).getSubmachine(); + if (submachine.getState() == state) { + if (currentState == findDeep) { + if (isTargetSubOf) { + entryToState(currentState, event, transition); + } + currentState = findDeep; + ((AbstractStateMachine)submachine).setCurrentState(state, event, transition, false); + return; + } + } + } + currentState = findDeep; + entryToState(currentState, event, transition); + StateMachine submachine = ((AbstractState)currentState).getSubmachine(); + ((AbstractStateMachine)submachine).setCurrentState(state, event, transition, false); + } } + + } + + void exitCurrentState(State state, Message event, Transition transition) { + if (currentState == null) { + return; + } + if (currentState.isSubmachineState()) { + StateMachine submachine = ((AbstractState)currentState).getSubmachine(); + ((AbstractStateMachine)submachine).exitCurrentState(state, event, transition); + exitFromState(currentState, event, transition); + } else { + exitFromState(currentState, event, transition); + } + } + + private boolean isSubstate(State left, State right) { + Collection> c = left.getStates(); + c.remove(left); + return c.contains(right); } private void exitFromState(State state, Message event, Transition transition) { @@ -329,22 +400,22 @@ public abstract class AbstractStateMachine extends LifecycleObjectSupport new HashMap()); StateContext stateContext = new DefaultStateContext(messageHeaders, extendedState, transition, this); - // TODO: we use this trick not to double notify - State toNotify = null; - if (state.isSubmachineState()) { - StateMachine submachine = ((AbstractState)state).getSubmachine(); - if (((LifecycleObjectSupport)submachine).isRunning()) { - toNotify = submachine.getState(); - } + boolean isSubOfSource = isSubstate(transition.getSource(), currentState); + boolean isSubOfTarget = isSubstate(transition.getTarget(), currentState); + if (currentState == transition.getSource() && currentState == transition.getTarget()) { + } else if (!isSubOfSource && !isSubOfTarget && currentState == transition.getSource()) { + } else if (!isSubOfSource && !isSubOfTarget && currentState == transition.getTarget()) { + } else if (!isSubOfSource && !isSubOfTarget) { + return; } - if (toNotify != null) { - notifyStateExited(toNotify); + boolean isTargetSubOfSource = isSubstate(transition.getSource(), transition.getTarget()); + if (transition.getSource() == currentState && isTargetSubOfSource) { + return; } state.exit(event != null ? event.getPayload() : null, stateContext); notifyStateExited(state); - } } @@ -354,21 +425,21 @@ public abstract class AbstractStateMachine extends LifecycleObjectSupport MessageHeaders messageHeaders = event != null ? event.getHeaders() : new MessageHeaders( new HashMap()); StateContext stateContext = new DefaultStateContext(messageHeaders, extendedState, transition, this); - notifyStateEntered(state); - // TODO: we use this trick not to double notify - State toNotify = null; - if (state.isSubmachineState()) { - StateMachine submachine = ((AbstractState)state).getSubmachine(); - if (((LifecycleObjectSupport)submachine).isRunning()) { - toNotify = submachine.getState(); + if (transition != null) { + boolean isSubOfSource = isSubstate(transition.getSource(), currentState); + boolean isSubOfTarget = isSubstate(transition.getTarget(), currentState); + if (currentState == transition.getSource() && currentState == transition.getTarget()) { + } else if (!isSubOfSource && !isSubOfTarget && currentState == transition.getTarget()) { + } else if (isSubOfSource && !isSubOfTarget && currentState == transition.getTarget()) { + return; + } else if (!isSubOfSource && !isSubOfTarget) { + return; } } + notifyStateEntered(state); state.entry(event != null ? event.getPayload() : null, stateContext); - if (toNotify != null) { - notifyStateEntered(toNotify); - } } } diff --git a/spring-statemachine-samples/cdplayer/src/test/java/demo/cdplayer/CdPlayerTests.java b/spring-statemachine-samples/cdplayer/src/test/java/demo/cdplayer/CdPlayerTests.java index f02bbe4b..ec8f4d45 100644 --- a/spring-statemachine-samples/cdplayer/src/test/java/demo/cdplayer/CdPlayerTests.java +++ b/spring-statemachine-samples/cdplayer/src/test/java/demo/cdplayer/CdPlayerTests.java @@ -6,14 +6,24 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertThat; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.statemachine.EnumStateMachine; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.StateMachineSystemConstants; +import org.springframework.statemachine.listener.StateMachineListener; +import org.springframework.statemachine.listener.StateMachineListenerAdapter; +import org.springframework.statemachine.state.State; +import org.springframework.statemachine.transition.Transition; import demo.CommonConfiguration; import demo.cdplayer.Application.Events; @@ -29,95 +39,150 @@ public class CdPlayerTests { private Library library; + private TestListener listener; + @Test public void testInitialState() throws InterruptedException { + listener.stateChangedLatch.await(1, TimeUnit.SECONDS); + assertThat(listener.stateChangedCount, is(2)); assertThat(machine.getState().getIds(), contains(States.IDLE, States.CLOSED)); assertLcdStatusStartsWith("No CD"); } @Test public void testEjectTwice() throws Exception { + listener.reset(1, 0, 0); player.eject(); - Thread.sleep(100); + listener.stateChangedLatch.await(1, TimeUnit.SECONDS); + assertThat(listener.stateChangedCount, is(1)); assertThat(machine.getState().getIds(), contains(States.IDLE, States.OPEN)); + listener.reset(1, 0, 0); player.eject(); - Thread.sleep(100); + listener.stateChangedLatch.await(1, TimeUnit.SECONDS); + assertThat(listener.stateChangedCount, is(1)); assertThat(machine.getState().getIds(), contains(States.IDLE, States.CLOSED)); } @Test public void testPlayWithCdLoaded() throws Exception { + listener.reset(4, 0, 0); player.eject(); player.load(library.getCollection().get(0)); player.eject(); player.play(); - Thread.sleep(100); + listener.stateChangedLatch.await(5, TimeUnit.SECONDS); + assertThat(listener.stateChangedCount, is(4)); assertThat(machine.getState().getIds(), contains(States.BUSY, States.PLAYING)); assertLcdStatusContains("cd1"); } @Test - public void testPlayWithNoCdLoaded() { + public void testPlayWithNoCdLoaded() throws Exception { + listener.reset(0, 0, 0); player.play(); + listener.stateChangedLatch.await(1, TimeUnit.SECONDS); + assertThat(listener.stateChangedCount, is(0)); assertThat(machine.getState().getIds(), contains(States.IDLE, States.CLOSED)); assertLcdStatusStartsWith("No CD"); } @Test public void testPlayLcdTimeChanges() throws Exception { + listener.reset(4, 0, 0); player.eject(); player.load(library.getCollection().get(0)); player.eject(); player.play(); + listener.stateChangedLatch.await(1, TimeUnit.SECONDS); + assertThat(listener.stateChangedCount, is(4)); assertThat(machine.getState().getIds(), contains(States.BUSY, States.PLAYING)); assertLcdStatusContains("cd1"); - Thread.sleep(1000); - assertLcdStatusContains("00:01"); - Thread.sleep(1000); - assertLcdStatusContains("00:02"); - Thread.sleep(1000); - assertLcdStatusContains("00:03"); - } - @Test - public void testPlayPause() throws Exception { - player.eject(); - player.load(library.getCollection().get(0)); - player.eject(); - player.play(); - Thread.sleep(100); - assertThat(machine.getState().getIds(), contains(States.BUSY, States.PLAYING)); - assertLcdStatusContains("cd1"); - Thread.sleep(1000); - assertLcdStatusIs("cd1 00:01"); - Thread.sleep(1000); + listener.reset(0, 0, 0, 1); + listener.transitionLatch.await(1, TimeUnit.SECONDS); + assertThat(listener.transitionCount, is(1)); + assertLcdStatusContains("00:01"); + + listener.reset(0, 0, 0, 1); + listener.transitionLatch.await(1, TimeUnit.SECONDS); assertLcdStatusContains("00:02"); - player.pause(); - Thread.sleep(2000); - assertLcdStatusContains("00:02"); - player.pause(); - assertLcdStatusContains("00:03"); - Thread.sleep(1000); + assertThat(listener.transitionCount, is(1)); + + listener.reset(0, 0, 0, 2); + listener.transitionLatch.await(3, TimeUnit.SECONDS); + assertThat(listener.transitionCount, is(2)); assertLcdStatusContains("00:04"); } @Test - public void testPlayStop() throws Exception { + public void testPlayPause() throws Exception { + listener.reset(4, 0, 0); player.eject(); player.load(library.getCollection().get(0)); player.eject(); player.play(); - Thread.sleep(100); + listener.stateChangedLatch.await(2, TimeUnit.SECONDS); + assertThat(listener.stateChangedCount, is(4)); assertThat(machine.getState().getIds(), contains(States.BUSY, States.PLAYING)); + assertLcdStatusContains("cd1"); + + listener.reset(0, 0, 0, 1); + listener.transitionLatch.await(2, TimeUnit.SECONDS); + assertThat(listener.transitionCount, is(1)); + assertLcdStatusContains("00:01"); + + listener.reset(0, 0, 0, 1); + listener.transitionLatch.await(2, TimeUnit.SECONDS); + assertLcdStatusContains("00:02"); + assertThat(listener.transitionCount, is(1)); + + + listener.reset(1, 0, 0, 0); + player.pause(); + listener.stateChangedLatch.await(2, TimeUnit.SECONDS); + assertThat(listener.stateChangedCount, is(1)); + assertLcdStatusContains("00:02"); + + listener.reset(1, 0, 0, 1); + player.pause(); + listener.stateChangedLatch.await(2, TimeUnit.SECONDS); + listener.transitionLatch.await(2, TimeUnit.SECONDS); + assertThat(listener.stateChangedCount, is(1)); + assertThat(listener.transitionCount, is(1)); + assertLcdStatusContains("00:03"); + + listener.reset(0, 0, 0, 2); + listener.transitionLatch.await(2, TimeUnit.SECONDS); + assertThat(listener.transitionCount, is(2)); + assertLcdStatusContains("00:05"); + } + + @Test + public void testPlayStop() throws Exception { + listener.reset(4, 0, 0); + player.eject(); + player.load(library.getCollection().get(0)); + player.eject(); + player.play(); + + listener.stateChangedLatch.await(1, TimeUnit.SECONDS); + assertThat(listener.stateChangedCount, is(4)); + assertThat(machine.getState().getIds(), contains(States.BUSY, States.PLAYING)); + + listener.reset(2, 0, 0); player.stop(); - Thread.sleep(100); + listener.stateChangedLatch.await(1, TimeUnit.SECONDS); + assertThat(listener.stateChangedCount, is(2)); assertLcdStatusIs("cd1 "); } @Test public void testPlayDeckOpenNoCd() throws Exception { + listener.reset(2, 0, 0); player.eject(); player.play(); + listener.stateChangedLatch.await(1, TimeUnit.SECONDS); + assertThat(listener.stateChangedCount, is(2)); assertThat(machine.getState().getIds(), contains(States.IDLE, States.CLOSED)); } @@ -142,6 +207,7 @@ public class CdPlayerTests { machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); player = context.getBean(CdPlayer.class); library = context.getBean(Library.class); + listener = context.getBean(TestListener.class); machine.start(); } @@ -153,10 +219,21 @@ public class CdPlayerTests { machine = null; player = null; library = null; + listener = null; } static class TestConfig { + @Autowired + private StateMachine machine; + + @Bean + public StateMachineListener stateMachineListener() { + TestListener listener = new TestListener(); + machine.addStateListener(listener); + return listener; + } + @Bean public Library library() { // override library to make it easier to test @@ -171,4 +248,56 @@ public class CdPlayerTests { } + static class TestListener extends StateMachineListenerAdapter { + + volatile CountDownLatch stateChangedLatch = new CountDownLatch(1); + volatile CountDownLatch stateEnteredLatch = new CountDownLatch(2); + volatile CountDownLatch stateExitedLatch = new CountDownLatch(0); + volatile CountDownLatch transitionLatch = new CountDownLatch(0); + volatile int stateChangedCount = 0; + volatile int transitionCount = 0; + List> statesEntered = new ArrayList>(); + List> statesExited = new ArrayList>(); + + @Override + public void stateChanged(State from, State to) { + stateChangedLatch.countDown(); + stateChangedCount++; + } + + @Override + public void stateEntered(State state) { + statesEntered.add(state); + stateEnteredLatch.countDown(); + } + + @Override + public void stateExited(State state) { + statesExited.add(state); + stateExitedLatch.countDown(); + } + + @Override + public void transitionEnded(Transition transition) { + transitionLatch.countDown(); + transitionCount++; + } + + public void reset(int c1, int c2, int c3) { + reset(c1, c2, c3, 0); + } + + public void reset(int c1, int c2, int c3, int c4) { + stateChangedLatch = new CountDownLatch(c1); + stateEnteredLatch = new CountDownLatch(c2); + stateExitedLatch = new CountDownLatch(c3); + transitionLatch = new CountDownLatch(c4); + stateChangedCount = 0; + transitionCount = 0; + statesEntered.clear(); + statesExited.clear(); + } + + } + } diff --git a/spring-statemachine-samples/showcase/src/main/java/demo/showcase/Application.java b/spring-statemachine-samples/showcase/src/main/java/demo/showcase/Application.java index 82adb5bb..12294276 100644 --- a/spring-statemachine-samples/showcase/src/main/java/demo/showcase/Application.java +++ b/spring-statemachine-samples/showcase/src/main/java/demo/showcase/Application.java @@ -25,7 +25,7 @@ public class Application { throws Exception { states .withStates() - .initial(States.S0) + .initial(States.S0, fooAction()) .state(States.S0) .and() .withStates() @@ -95,9 +95,18 @@ public class Application { .withExternal() .source(States.S211).target(States.S0).event(Events.G) .and() - .withExternal() - .source(States.S21).target(States.S21).event(Events.H) - .guard(fooGuard()) + .withInternal() + .source(States.S0).event(Events.H) + .guard(foo0Guard()) + .action(fooAction()) + .and() + .withInternal() + .source(States.S2).event(Events.H) + .guard(foo1Guard()) + .action(fooAction()) + .and() + .withInternal() + .source(States.S1).event(Events.H) .and() .withExternal() .source(States.S11).target(States.S12).event(Events.I); @@ -105,8 +114,13 @@ public class Application { } @Bean - public FooGuard fooGuard() { - return new FooGuard(); + public FooGuard foo0Guard() { + return new FooGuard(0); + } + + @Bean + public FooGuard foo1Guard() { + return new FooGuard(1); } @Bean @@ -134,7 +148,14 @@ public class Application { @Override public void execute(StateContext context) { - context.getExtendedState().getVariables().put("foo", 1); + Object foo = context.getExtendedState().getVariables().get("foo"); + if (foo instanceof Integer && ((Integer)foo) == 0) { + context.getExtendedState().getVariables().put("foo", 1); + } else if (foo instanceof Integer && ((Integer)foo) == 1) { + context.getExtendedState().getVariables().put("foo", 0); + } else { + context.getExtendedState().getVariables().put("foo", 0); + } } } @@ -143,10 +164,16 @@ public class Application { // tag::snippetE[] private static class FooGuard implements Guard { + private final int match; + + public FooGuard(int match) { + this.match = match; + } + @Override public boolean evaluate(StateContext context) { Object foo = context.getExtendedState().getVariables().get("foo"); - return !(foo == null || !foo.equals(1)); + return !(foo == null || !foo.equals(match)); } } // end::snippetE[] diff --git a/spring-statemachine-samples/showcase/src/main/resources/statechartmodel.txt b/spring-statemachine-samples/showcase/src/main/resources/statechartmodel.txt index aa572b6d..63067a88 100644 --- a/spring-statemachine-samples/showcase/src/main/resources/statechartmodel.txt +++ b/spring-statemachine-samples/showcase/src/main/resources/statechartmodel.txt @@ -3,14 +3,17 @@ +---------------------------------------------------------------------------------------------+ | entry/ | | exit/ | +| H/[foo.equals(0)]; | +| | | +-------------------------+ +--------------------------------------------+ | | *-->| S1 | | S2 | | | +-------------------------+ +--------------------------------------------+ | -| | entry/ | C | entry/ H | | -| D | exit/ |----->| exit/ +-------------------+ | | -|<----------| | | | H[foo.equals(1)]; | | | -| | +---------------+ | C | +------------------------------+ | | | -| | *-->| S11 | |<-----| *-->| S21 |<--+ | | +| | entry/ | C | entry/ | | +| D | exit/ |----->| exit/ | | +|<----------| H/ | | H/[foo.equals(1)]; | | +| | | | | | +| | +---------------+ | C | +------------------------------+ | | +| | *-->| S11 | |<-----| *-->| S21 | | | | | +---------------+ | | +------------------------------+ | | | +--| | entry/ | | F | | entry/ | | | | A| | | exit/ |<---------| | exit/ | | | diff --git a/spring-statemachine-samples/showcase/src/test/java/demo/showcase/ShowcaseTests.java b/spring-statemachine-samples/showcase/src/test/java/demo/showcase/ShowcaseTests.java index ed561965..7a3a3caf 100644 --- a/spring-statemachine-samples/showcase/src/test/java/demo/showcase/ShowcaseTests.java +++ b/spring-statemachine-samples/showcase/src/test/java/demo/showcase/ShowcaseTests.java @@ -21,6 +21,7 @@ import org.springframework.statemachine.StateMachineSystemConstants; import org.springframework.statemachine.listener.StateMachineListener; import org.springframework.statemachine.listener.StateMachineListenerAdapter; import org.springframework.statemachine.state.State; +import org.springframework.statemachine.transition.Transition; import demo.CommonConfiguration; import demo.showcase.Application.Events; @@ -48,11 +49,12 @@ public class ShowcaseTests { @Test public void testA() throws Exception { - listener.reset(1, 2, 2); + listener.reset(1, 2, 2, 1); machine.sendEvent(Events.A); listener.stateChangedLatch.await(1, TimeUnit.SECONDS); listener.stateEnteredLatch.await(1, TimeUnit.SECONDS); listener.stateExitedLatch.await(1, TimeUnit.SECONDS); + listener.transitionLatch.await(1, TimeUnit.SECONDS); assertThat(machine.getState().getIds(), contains(States.S0, States.S1, States.S11)); assertThat(listener.statesEntered.size(), is(2)); assertThat(listener.statesEntered.get(0).getId(), is(States.S1)); @@ -60,6 +62,7 @@ public class ShowcaseTests { assertThat(listener.statesExited.size(), is(2)); assertThat(listener.statesExited.get(0).getId(), is(States.S11)); assertThat(listener.statesExited.get(1).getId(), is(States.S1)); + assertThat(listener.transitionCount, is(1)); } @Test @@ -91,6 +94,20 @@ public class ShowcaseTests { assertThat(listener.statesEntered.get(1).getId(), is(States.S11)); } + @Test + public void testD() throws Exception { + listener.reset(3, 3, 0); + machine.sendEvent(Events.D); + listener.stateChangedLatch.await(1, TimeUnit.SECONDS); + listener.stateEnteredLatch.await(1, TimeUnit.SECONDS); + assertThat(machine.getState().getIds(), contains(States.S0, States.S1, States.S11)); + assertThat(listener.statesEntered.size(), is(3)); + assertThat(listener.statesEntered.get(0).getId(), is(States.S0)); + assertThat(listener.statesEntered.get(1).getId(), is(States.S1)); + assertThat(listener.statesEntered.get(2).getId(), is(States.S11)); + assertThat(listener.statesExited.size(), is(3)); + } + @Test public void testCD() throws Exception { listener.reset(1, 3, 0); @@ -120,6 +137,46 @@ public class ShowcaseTests { assertThat(listener.statesExited.get(0).getId(), is(States.S11)); } + @Test + public void testH() throws Exception { + listener.reset(0, 0, 0, 1); + machine.sendEvent(Events.H); + listener.transitionLatch.await(1, TimeUnit.SECONDS); + assertThat(listener.transitionCount, is(1)); + assertThat(listener.transitions.get(0).getSource().getId(), is(States.S1)); + } + + @Test + public void testCH() throws Exception { + machine.sendEvent(Events.C); + listener.reset(0, 0, 0, 1); + machine.sendEvent(Events.H); + listener.transitionLatch.await(1, TimeUnit.SECONDS); + assertThat(listener.transitionCount, is(1)); + assertThat(listener.transitions.get(0).getSource().getId(), is(States.S0)); + } + + @Test + public void testACH() throws Exception { + machine.sendEvent(Events.A); + machine.sendEvent(Events.C); + listener.reset(0, 0, 0, 1); + machine.sendEvent(Events.H); + listener.transitionLatch.await(1, TimeUnit.SECONDS); + assertThat(listener.transitionCount, is(1)); + assertThat(listener.transitions.get(0).getSource().getId(), is(States.S2)); + } + + @Test + public void testE() throws Exception { + listener.reset(1, 2, 3, 0); + machine.sendEvent(Events.E); + listener.stateChangedLatch.await(1, TimeUnit.SECONDS); + assertThat(machine.getState().getIds(), contains(States.S0, States.S2, States.S21, States.S211)); + assertThat(listener.statesExited.size(), is(2)); + assertThat(listener.statesEntered.size(), is(3)); + } + static class Config { @Autowired @@ -138,8 +195,11 @@ public class ShowcaseTests { volatile CountDownLatch stateChangedLatch = new CountDownLatch(1); volatile CountDownLatch stateEnteredLatch = new CountDownLatch(3); volatile CountDownLatch stateExitedLatch = new CountDownLatch(0); + volatile CountDownLatch transitionLatch = new CountDownLatch(0); + volatile List> transitions = new ArrayList>(); List> statesEntered = new ArrayList>(); List> statesExited = new ArrayList>(); + volatile int transitionCount = 0; @Override public void stateChanged(State from, State to) { @@ -158,12 +218,26 @@ public class ShowcaseTests { stateExitedLatch.countDown(); } + @Override + public void transition(Transition transition) { + transitions.add(transition); + transitionLatch.countDown(); + transitionCount++; + } + public void reset(int c1, int c2, int c3) { + reset(c1, c2, c3, 0); + } + + public void reset(int c1, int c2, int c3, int c4) { stateChangedLatch = new CountDownLatch(c1); stateEnteredLatch = new CountDownLatch(c2); stateExitedLatch = new CountDownLatch(c3); + transitionLatch = new CountDownLatch(c4); statesEntered.clear(); statesExited.clear(); + transitionCount = 0; + transitions.clear(); } }