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
This commit is contained in:
Janne Valkealahti
2015-04-03 10:47:14 +01:00
parent 85cc2df29e
commit e1be34cc92
10 changed files with 448 additions and 107 deletions

View File

@@ -52,7 +52,8 @@ public abstract class AbstractSimpleState<S, E> extends AbstractState<S, E> {
* @param entryActions the entry actions
* @param exitActions the exit actions
*/
public AbstractSimpleState(S id, Collection<E> deferred, Collection<? extends Action<S, E>> entryActions, Collection<? extends Action<S, E>> exitActions) {
public AbstractSimpleState(S id, Collection<E> deferred, Collection<? extends Action<S, E>> entryActions,
Collection<? extends Action<S, E>> exitActions) {
this(id, deferred, entryActions, exitActions, null);
}
@@ -86,8 +87,8 @@ public abstract class AbstractSimpleState<S, E> extends AbstractState<S, E> {
* @param pseudoState the pseudo state
* @param regions the regions
*/
public AbstractSimpleState(S id, Collection<E> deferred, Collection<? extends Action<S, E>> entryActions, Collection<? extends Action<S, E>> exitActions,
PseudoState pseudoState, Collection<Region<S, E>> regions) {
public AbstractSimpleState(S id, Collection<E> deferred, Collection<? extends Action<S, E>> entryActions,
Collection<? extends Action<S, E>> exitActions, PseudoState pseudoState, Collection<Region<S, E>> regions) {
super(id, deferred, entryActions, exitActions, pseudoState, regions);
this.ids = new ArrayList<S>();
this.ids.add(id);
@@ -103,8 +104,8 @@ public abstract class AbstractSimpleState<S, E> extends AbstractState<S, E> {
* @param pseudoState the pseudo state
* @param submachine the submachine
*/
public AbstractSimpleState(S id, Collection<E> deferred, Collection<? extends Action<S, E>> entryActions, Collection<? extends Action<S, E>> exitActions,
PseudoState pseudoState, StateMachine<S, E> submachine) {
public AbstractSimpleState(S id, Collection<E> deferred, Collection<? extends Action<S, E>> entryActions,
Collection<? extends Action<S, E>> exitActions, PseudoState pseudoState, StateMachine<S, E> submachine) {
super(id, deferred, entryActions, exitActions, pseudoState, submachine);
this.ids = new ArrayList<S>();
this.ids.add(id);
@@ -119,8 +120,8 @@ public abstract class AbstractSimpleState<S, E> extends AbstractState<S, E> {
* @param exitActions the exit actions
* @param pseudoState the pseudo state
*/
public AbstractSimpleState(S id, Collection<E> deferred, Collection<? extends Action<S, E>> entryActions, Collection<? extends Action<S, E>> exitActions,
PseudoState pseudoState) {
public AbstractSimpleState(S id, Collection<E> deferred, Collection<? extends Action<S, E>> entryActions,
Collection<? extends Action<S, E>> exitActions, PseudoState pseudoState) {
super(id, deferred, entryActions, exitActions, pseudoState);
this.ids = new ArrayList<S>();
this.ids.add(id);
@@ -131,4 +132,11 @@ public abstract class AbstractSimpleState<S, E> extends AbstractState<S, E> {
return Collections.unmodifiableCollection(ids);
}
@Override
public Collection<State<S, E>> getStates() {
ArrayList<State<S, E>> states = new ArrayList<State<S, E>>();
states.add(this);
return states;
}
}

View File

@@ -70,7 +70,8 @@ public abstract class AbstractState<S, E> implements State<S, E> {
* @param entryActions the entry actions
* @param exitActions the exit actions
*/
public AbstractState(S id, Collection<E> deferred, Collection<? extends Action<S, E>> entryActions, Collection<? extends Action<S, E>> exitActions) {
public AbstractState(S id, Collection<E> deferred, Collection<? extends Action<S, E>> entryActions,
Collection<? extends Action<S, E>> exitActions) {
this(id, deferred, entryActions, exitActions, null);
}
@@ -83,8 +84,8 @@ public abstract class AbstractState<S, E> implements State<S, E> {
* @param exitActions the exit actions
* @param pseudoState the pseudo state
*/
public AbstractState(S id, Collection<E> deferred, Collection<? extends Action<S, E>> entryActions, Collection<? extends Action<S, E>> exitActions,
PseudoState pseudoState) {
public AbstractState(S id, Collection<E> deferred, Collection<? extends Action<S, E>> entryActions,
Collection<? extends Action<S, E>> exitActions, PseudoState pseudoState) {
this(id, deferred, entryActions, exitActions, pseudoState, null, null);
}
@@ -98,8 +99,8 @@ public abstract class AbstractState<S, E> implements State<S, E> {
* @param pseudoState the pseudo state
* @param submachine the submachine
*/
public AbstractState(S id, Collection<E> deferred, Collection<? extends Action<S, E>> entryActions, Collection<? extends Action<S, E>> exitActions,
PseudoState pseudoState, StateMachine<S, E> submachine) {
public AbstractState(S id, Collection<E> deferred, Collection<? extends Action<S, E>> entryActions,
Collection<? extends Action<S, E>> exitActions, PseudoState pseudoState, StateMachine<S, E> submachine) {
this(id, deferred, entryActions, exitActions, pseudoState, null, submachine);
}
@@ -113,8 +114,8 @@ public abstract class AbstractState<S, E> implements State<S, E> {
* @param pseudoState the pseudo state
* @param regions the regions
*/
public AbstractState(S id, Collection<E> deferred, Collection<? extends Action<S, E>> entryActions, Collection<? extends Action<S, E>> exitActions,
PseudoState pseudoState, Collection<Region<S, E>> regions) {
public AbstractState(S id, Collection<E> deferred, Collection<? extends Action<S, E>> entryActions,
Collection<? extends Action<S, E>> exitActions, PseudoState pseudoState, Collection<Region<S, E>> regions) {
this(id, deferred, entryActions, exitActions, pseudoState, regions, null);
}
@@ -129,8 +130,9 @@ public abstract class AbstractState<S, E> implements State<S, E> {
* @param regions the regions
* @param submachine the submachine
*/
private AbstractState(S id, Collection<E> deferred, Collection<? extends Action<S, E>> entryActions, Collection<? extends Action<S, E>> exitActions,
PseudoState pseudoState, Collection<Region<S, E>> regions, StateMachine<S, E> submachine) {
private AbstractState(S id, Collection<E> deferred, Collection<? extends Action<S, E>> entryActions,
Collection<? extends Action<S, E>> exitActions, PseudoState pseudoState, Collection<Region<S, E>> regions,
StateMachine<S, E> submachine) {
this.id = id;
this.deferred = deferred;
this.entryActions = entryActions;
@@ -164,6 +166,9 @@ public abstract class AbstractState<S, E> implements State<S, E> {
@Override
public abstract Collection<S> getIds();
@Override
public abstract Collection<State<S, E>> getStates();
@Override
public PseudoState getPseudoState() {
return pseudoState;

View File

@@ -75,8 +75,8 @@ public class RegionState<S, E> extends AbstractState<S, E> {
* @param exitActions the exit actions
* @param pseudoState the pseudo state
*/
public RegionState(S id, Collection<Region<S, E>> regions, Collection<E> deferred, Collection<Action<S, E>> entryActions, Collection<Action<S, E>> exitActions,
PseudoState pseudoState) {
public RegionState(S id, Collection<Region<S, E>> regions, Collection<E> deferred,
Collection<Action<S, E>> entryActions, Collection<Action<S, E>> exitActions, PseudoState pseudoState) {
super(id, deferred, entryActions, exitActions, pseudoState, regions);
}
@@ -89,7 +89,8 @@ public class RegionState<S, E> extends AbstractState<S, E> {
* @param entryActions the entry actions
* @param exitActions the exit actions
*/
public RegionState(S id, Collection<Region<S, E>> regions, Collection<E> deferred, Collection<Action<S, E>> entryActions, Collection<Action<S, E>> exitActions) {
public RegionState(S id, Collection<Region<S, E>> regions, Collection<E> deferred,
Collection<Action<S, E>> entryActions, Collection<Action<S, E>> exitActions) {
super(id, deferred, entryActions, exitActions, null, regions);
}
@@ -150,4 +151,14 @@ public class RegionState<S, E> extends AbstractState<S, E> {
return ids;
}
@Override
public Collection<State<S, E>> getStates() {
ArrayList<State<S, E>> states = new ArrayList<State<S, E>>();
states.add(this);
for (Region<S, E> r : getRegions()) {
states.addAll(r.getStates());
}
return states;
}
}

View File

@@ -70,6 +70,14 @@ public interface State<S, E> {
*/
Collection<S> getIds();
/**
* Gets all possible states this state knows about including itself
* and substates.
*
* @return all state including itself and nested states
*/
Collection<State<S, E>> getStates();
/**
* Gets a {@link PseudoState} attached to a {@code State}.
* {@link PseudoState} is not required and thus this method return

View File

@@ -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<S, E> extends AbstractState<S, E> {
* @param exitActions the exit actions
* @param pseudoState the pseudo state
*/
public StateMachineState(S id, StateMachine<S, E> submachine, Collection<E> deferred, Collection<? extends Action<S, E>> entryActions, Collection<? extends Action<S, E>> exitActions,
public StateMachineState(S id, StateMachine<S, E> submachine, Collection<E> deferred,
Collection<? extends Action<S, E>> entryActions, Collection<? extends Action<S, E>> exitActions,
PseudoState pseudoState) {
super(id, deferred, entryActions, exitActions, pseudoState, submachine);
this.ids = new ArrayList<S>();
@@ -102,7 +102,8 @@ public class StateMachineState<S, E> extends AbstractState<S, E> {
* @param entryActions the entry actions
* @param exitActions the exit actions
*/
public StateMachineState(S id, StateMachine<S, E> submachine, Collection<E> deferred, Collection<? extends Action<S, E>> entryActions, Collection<? extends Action<S, E>> exitActions) {
public StateMachineState(S id, StateMachine<S, E> submachine, Collection<E> deferred,
Collection<? extends Action<S, E>> entryActions, Collection<? extends Action<S, E>> exitActions) {
super(id, deferred, entryActions, exitActions, null, submachine);
this.ids = new ArrayList<S>();
this.ids.add(id);
@@ -119,13 +120,22 @@ public class StateMachineState<S, E> extends AbstractState<S, E> {
return ret;
}
@Override
public Collection<State<S, E>> getStates() {
ArrayList<State<S, E>> states = new ArrayList<State<S, E>>();
states.add(this);
for (State<S, E> s : getSubmachine().getStates()) {
states.addAll(s.getStates());
}
return states;
}
@Override
public void exit(E event, StateContext<S, E> 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<? extends Action<S, E>> actions = getExitActions();
@@ -144,14 +154,9 @@ public class StateMachineState<S, E> extends AbstractState<S, E> {
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();
}
}

View File

@@ -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<S, E> extends LifecycleObjectSupport
return transitions;
}
@Override
public String toString() {
ArrayList<State<S, E>> all = new ArrayList<State<S,E>>();
for (State<S, E> s : states) {
all.addAll(s.getStates());
}
StringBuilder buf = new StringBuilder();
for (State<S, E> s : all) {
buf.append(s.getId() + " ");
}
return buf.toString();
}
protected boolean acceptEvent(Message<E> event) {
boolean accepted = currentState.sendEvent(event);
@@ -294,7 +305,7 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
}
private void switchToState(State<S,E> state, Message<E> event, Transition<S,E> transition) {
setCurrentState(state, event, transition);
setCurrentState(state, event, transition, true);
// TODO: should handle triggerles transition some how differently
for (Transition<S,E> t : transitions) {
@@ -307,19 +318,79 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
}
void setCurrentState(State<S, E> state, Message<E> event, Transition<S, E> transition) {
private State<S, E> findDeepParent(State<S, E> state) {
for (State<S, E> s : states) {
if (s.getStates().contains(state)) {
return s;
}
}
return null;
}
void setCurrentState(State<S, E> statex, Message<E> event, Transition<S, E> transition, boolean exit) {
State<S, E> state = statex;
State<S, E> 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<S, E> 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<S, E> submachine = ((AbstractState<S, E>)currentState).getSubmachine();
((AbstractStateMachine<S, E>)submachine).setCurrentState(state, event, transition);
} else if (currentState != null && currentState.isSubmachineState()) {
if (findDeep != null) {
if (exit) {
exitCurrentState(state, event, transition);
}
if (currentState == findDeep) {
StateMachine<S, E> submachine = ((AbstractState<S, E>)currentState).getSubmachine();
if (submachine.getState() == state) {
if (currentState == findDeep) {
if (isTargetSubOf) {
entryToState(currentState, event, transition);
}
currentState = findDeep;
((AbstractStateMachine<S, E>)submachine).setCurrentState(state, event, transition, false);
return;
}
}
}
currentState = findDeep;
entryToState(currentState, event, transition);
StateMachine<S, E> submachine = ((AbstractState<S, E>)currentState).getSubmachine();
((AbstractStateMachine<S, E>)submachine).setCurrentState(state, event, transition, false);
}
}
}
void exitCurrentState(State<S, E> state, Message<E> event, Transition<S, E> transition) {
if (currentState == null) {
return;
}
if (currentState.isSubmachineState()) {
StateMachine<S, E> submachine = ((AbstractState<S, E>)currentState).getSubmachine();
((AbstractStateMachine<S, E>)submachine).exitCurrentState(state, event, transition);
exitFromState(currentState, event, transition);
} else {
exitFromState(currentState, event, transition);
}
}
private boolean isSubstate(State<S, E> left, State<S, E> right) {
Collection<State<S, E>> c = left.getStates();
c.remove(left);
return c.contains(right);
}
private void exitFromState(State<S, E> state, Message<E> event, Transition<S, E> transition) {
@@ -329,22 +400,22 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
new HashMap<String, Object>());
StateContext<S, E> stateContext = new DefaultStateContext<S, E>(messageHeaders, extendedState, transition, this);
// TODO: we use this trick not to double notify
State<S, E> toNotify = null;
if (state.isSubmachineState()) {
StateMachine<S, E> submachine = ((AbstractState<S, E>)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<S, E> extends LifecycleObjectSupport
MessageHeaders messageHeaders = event != null ? event.getHeaders() : new MessageHeaders(
new HashMap<String, Object>());
StateContext<S, E> stateContext = new DefaultStateContext<S, E>(messageHeaders, extendedState, transition, this);
notifyStateEntered(state);
// TODO: we use this trick not to double notify
State<S, E> toNotify = null;
if (state.isSubmachineState()) {
StateMachine<S, E> submachine = ((AbstractState<S, E>)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);
}
}
}

View File

@@ -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<States,Events> machine;
@Bean
public StateMachineListener<States, Events> 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<States, Events> {
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<State<States, Events>> statesEntered = new ArrayList<State<States,Events>>();
List<State<States, Events>> statesExited = new ArrayList<State<States,Events>>();
@Override
public void stateChanged(State<States, Events> from, State<States, Events> to) {
stateChangedLatch.countDown();
stateChangedCount++;
}
@Override
public void stateEntered(State<States, Events> state) {
statesEntered.add(state);
stateEnteredLatch.countDown();
}
@Override
public void stateExited(State<States, Events> state) {
statesExited.add(state);
stateExitedLatch.countDown();
}
@Override
public void transitionEnded(Transition<States, Events> 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();
}
}
}

View File

@@ -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<States, Events> 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<States, Events> {
private final int match;
public FooGuard(int match) {
this.match = match;
}
@Override
public boolean evaluate(StateContext<States, Events> context) {
Object foo = context.getExtendedState().getVariables().get("foo");
return !(foo == null || !foo.equals(1));
return !(foo == null || !foo.equals(match));
}
}
// end::snippetE[]

View File

@@ -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/ | | |

View File

@@ -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<Transition<States, Events>> transitions = new ArrayList<Transition<States,Events>>();
List<State<States, Events>> statesEntered = new ArrayList<State<States,Events>>();
List<State<States, Events>> statesExited = new ArrayList<State<States,Events>>();
volatile int transitionCount = 0;
@Override
public void stateChanged(State<States, Events> from, State<States, Events> to) {
@@ -158,12 +218,26 @@ public class ShowcaseTests {
stateExitedLatch.countDown();
}
@Override
public void transition(Transition<States, Events> 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();
}
}