Add support for completion transitions

- Currently into as internal new feature, add state
  completed concecept and use it in various places.
- Main focus for this commit is to add support using
  anonymous transitions with state do actions which
  requires proper completion schematics.
- Internal functionality here will probably expose to
  user level in future releases using various other
  concepts.
- Backport #466
- Relates #504
This commit is contained in:
jvalkeal
2018-02-12 13:30:37 +02:00
parent 6743d42792
commit a4d43131ea
23 changed files with 962 additions and 67 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016 the original author or authors.
* Copyright 2016-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -51,7 +51,7 @@ public class LinkedRegionsTests extends AbstractBuildTests {
StateMachineTestPlan<String, String> plan =
StateMachineTestPlanBuilder.<String, String>builder()
.stateMachine(stateMachine)
.step().expectStateChanged(19).expectStates("S3").and()
.step().expectStateChanged(15).expectStates("S3").and()
.build();
plan.test();
assertThat(listener.statesEntered, not(hasItem(startsWith("JOIN"))));

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -66,7 +66,7 @@ public class OrderedComposite<S> {
*
* @param item item
*/
public void add(S item) {
public synchronized void add(S item) {
if (item instanceof Ordered) {
if (!ordered.contains(item)) {
ordered.add(item);
@@ -89,7 +89,7 @@ public class OrderedComposite<S> {
*
* @param item item
*/
public void remove(S item) {
public synchronized void remove(S item) {
ordered.remove(item);
unordered.remove(item);
Collections.sort(ordered, comparator);
@@ -120,4 +120,8 @@ public class OrderedComposite<S> {
return result.iterator();
}
@Override
public String toString() {
return "OrderedComposite list=" + list.size() + " hash=" + hashCode();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 the original author or authors.
* Copyright 2017-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,17 +20,22 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.messaging.Message;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateContext.Stage;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.action.ActionListener;
import org.springframework.statemachine.action.CompositeActionListener;
import org.springframework.statemachine.listener.StateMachineListener;
import org.springframework.statemachine.listener.StateMachineListenerAdapter;
import org.springframework.statemachine.region.Region;
import org.springframework.statemachine.support.LifecycleObjectSupport;
import org.springframework.statemachine.trigger.Trigger;
@@ -46,7 +51,6 @@ import org.springframework.statemachine.trigger.Trigger;
public abstract class AbstractState<S, E> extends LifecycleObjectSupport implements State<S, E> {
private static final Log log = LogFactory.getLog(AbstractState.class);
private final S id;
private final PseudoState<S, E> pseudoState;
private final Collection<E> deferred;
@@ -59,6 +63,7 @@ public abstract class AbstractState<S, E> extends LifecycleObjectSupport impleme
private final CompositeStateListener<S, E> stateListener = new CompositeStateListener<S, E>();
private final List<ScheduledFuture<?>> cancellableActions = new ArrayList<>();
private CompositeActionListener<S, E> actionListener;
private final List<StateMachineListener<S, E>> completionListeners = new CopyOnWriteArrayList<>();
/**
* Instantiates a new abstract state.
@@ -196,6 +201,18 @@ public abstract class AbstractState<S, E> extends LifecycleObjectSupport impleme
@Override
public void exit(StateContext<S, E> context) {
if (submachine != null) {
for (StateMachineListener<S, E> l : completionListeners) {
submachine.removeStateListener(l);
}
} else if (!regions.isEmpty()) {
for (Region<S, E> region : regions) {
for (StateMachineListener<S, E> l : completionListeners) {
region.removeStateListener(l);
}
}
}
completionListeners.clear();
cancelStateActions();
stateListener.onExit(context);
disarmTriggers();
@@ -203,6 +220,45 @@ public abstract class AbstractState<S, E> extends LifecycleObjectSupport impleme
@Override
public void entry(StateContext<S, E> context) {
if (submachine != null) {
final StateMachineListener<S, E> l = new StateMachineListenerAdapter<S, E>() {
@Override
public void stateContext(StateContext<S, E> stateContext) {
if (stateContext.getStage() == Stage.STATEMACHINE_STOP) {
if (stateContext.getStateMachine() == submachine && submachine.isComplete()) {
completionListeners.remove(this);
submachine.removeStateListener(this);
if (completionListeners.isEmpty()) {
notifyStateOnComplete(stateContext);
}
}
}
}
};
submachine.addStateListener(l);
} else if (!regions.isEmpty()) {
for (final Region<S, E> region : regions) {
final StateMachineListener<S, E> l = new StateMachineListenerAdapter<S, E>() {
@Override
public void stateContext(StateContext<S, E> stateContext) {
if (stateContext.getStage() == Stage.STATEMACHINE_STOP) {
if (stateContext.getStateMachine() == region && region.isComplete()) {
completionListeners.remove(this);
region.removeStateListener(this);
if (completionListeners.isEmpty()) {
notifyStateOnComplete(stateContext);
}
}
}
}
};
completionListeners.add(l);
region.addStateListener(l);
}
}
stateListener.onEntry(context);
armTriggers();
scheduleStateActions(context);
@@ -378,12 +434,19 @@ public abstract class AbstractState<S, E> extends LifecycleObjectSupport impleme
* @param context the context
*/
protected void scheduleStateActions(StateContext<S, E> context) {
AtomicInteger completionCount = null;
if (isSimple()) {
completionCount = new AtomicInteger(stateActions.size());
}
for (Action<S, E> action : stateActions) {
ScheduledFuture<?> future = scheduleAction(action, context);
ScheduledFuture<?> future = scheduleAction(action, context, completionCount);
if (future != null) {
cancellableActions.add(future);
}
}
if (isSimple() && stateActions.size() == 0) {
notifyStateOnComplete(context);
}
}
/**
@@ -409,9 +472,10 @@ public abstract class AbstractState<S, E> extends LifecycleObjectSupport impleme
*
* @param action the action
* @param context the context
* @param completionCount the completion count tracker
* @return the scheduled future
*/
protected ScheduledFuture<?> scheduleAction(final Action<S, E> action, final StateContext<S, E> context) {
protected ScheduledFuture<?> scheduleAction(final Action<S, E> action, final StateContext<S, E> context, final AtomicInteger completionCount) {
TaskScheduler taskScheduler = getTaskScheduler();
if (taskScheduler == null) {
log.error("Unable to schedule action as taskSchedule is not set, action=[" + action + "]");
@@ -422,11 +486,18 @@ public abstract class AbstractState<S, E> extends LifecycleObjectSupport impleme
@Override
public void run() {
executeAction(action, context);
if (completionCount != null && completionCount.decrementAndGet() <= 0) {
notifyStateOnComplete(context);
}
}
}, new Date());
return future;
}
protected void notifyStateOnComplete(StateContext<S, E> context) {
stateListener.onComplete(context);
}
@Override
public String toString() {
return "AbstractState [id=" + id + ", pseudoState=" + pseudoState + ", deferred=" + deferred + ", entryActions="

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016 the original author or authors.
* Copyright 2016-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -44,4 +44,11 @@ public class CompositeStateListener<S, E> extends AbstractCompositeListener<Stat
iterator.next().onExit(context);
}
}
@Override
public void onComplete(StateContext<S, E> context) {
for (Iterator<StateListener<S, E>> iterator = getListeners().reverse(); iterator.hasNext();) {
iterator.next().onComplete(context);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,7 +24,6 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.guard.Guard;
import org.springframework.statemachine.state.PseudoStateContext.PseudoAction;
import org.springframework.statemachine.support.StateMachineUtils;
import org.springframework.util.Assert;
/**
@@ -112,31 +111,22 @@ public class JoinPseudoState<S, E> extends AbstractPseudoState<S, E> {
this.track = new ArrayList<State<S,E>>(joins);
for (State<S, E> tt : joins) {
final State<S, E> t = tt;
t.addStateListener(new StateListener<S, E>() {
t.addStateListener(new StateListenerAdapter<S, E>() {
@Override
public void onEntry(StateContext<S, E> context) {
if (context.getTransition() != null && StateMachineUtils
.isPseudoState(context.getTransition().getTarget(), PseudoStateKind.END)) {
if (!notified && track.size() > 0) {
track.remove(t);
if (track.size() == 0) {
notified = true;
notifyContext(new DefaultPseudoStateContext<S, E>(JoinPseudoState.this, PseudoAction.JOIN_COMPLETED));
}
}
}
}
@Override
public void onExit(StateContext<S, E> context) {
if (!notified && track.size() > 0) {
public void onComplete(StateContext<S, E> context) {
boolean trackSizeZero = false;
synchronized (track) {
track.remove(t);
if (track.size() == 0) {
notified = true;
notifyContext(new DefaultPseudoStateContext<S, E>(JoinPseudoState.this, PseudoAction.JOIN_COMPLETED));
trackSizeZero = true;
}
}
if (!notified && trackSizeZero) {
log.debug("Join complete");
notified = true;
notifyContext(new DefaultPseudoStateContext<S, E>(JoinPseudoState.this, PseudoAction.JOIN_COMPLETED));
}
}
});
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -153,7 +153,6 @@ public class ObjectState<S, E> extends AbstractSimpleState<S, E> {
@Override
public void entry(StateContext<S, E> context) {
super.entry(context);
for (Action<S, E> action : getEntryActions()) {
try {
executeAction(action, context);
@@ -161,6 +160,7 @@ public class ObjectState<S, E> extends AbstractSimpleState<S, E> {
log.error("Action execution resulted error", e);
}
}
super.entry(context);
}
@Override

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2016 the original author or authors.
* Copyright 2015-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016 the original author or authors.
* Copyright 2016-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -40,4 +40,11 @@ public interface StateListener<S, E> {
* @param context the state context
*/
void onExit(StateContext<S, E> context);
/**
* Called when {@link State} want to notify of its completion.
*
* @param context the state context
*/
void onComplete(StateContext<S, E> context);
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.state;
import org.springframework.statemachine.StateContext;
/**
* Adapter implementation of {@link StateListener} implementing all
* methods which extended implementation can override.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public class StateListenerAdapter<S, E> implements StateListener<S, E> {
@Override
public void onEntry(StateContext<S, E> context) {
}
@Override
public void onExit(StateContext<S, E> context) {
}
@Override
public void onComplete(StateContext<S, E> context) {
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 the original author or authors.
* Copyright 2015-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -49,6 +49,7 @@ import org.springframework.statemachine.state.PseudoStateContext;
import org.springframework.statemachine.state.PseudoStateKind;
import org.springframework.statemachine.state.PseudoStateListener;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.state.StateListenerAdapter;
import org.springframework.statemachine.support.StateMachineExecutor.StateMachineExecutorTransit;
import org.springframework.statemachine.transition.InitialTransition;
import org.springframework.statemachine.transition.Transition;
@@ -260,7 +261,14 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
}
}
for (State<S, E> state : states) {
for (final State<S, E> state : states) {
state.addStateListener(new StateListenerAdapter<S, E>() {
public void onComplete(StateContext<S, E> context) {
((AbstractStateMachine<S, E>)getRelayStateMachine()).executeTriggerlessTransitions(AbstractStateMachine.this, context, state);
};
});
if (state.isSubmachineState()) {
StateMachine<S, E> submachine = ((AbstractState<S, E>)state).getSubmachine();
submachine.addStateListener(new StateMachineListenerRelay());
@@ -358,10 +366,6 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
}
}
protected StateMachineExecutor<S, E> getStateMachineExecutor() {
return stateMachineExecutor;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
// last change to set factory because this maybe be called per
@@ -801,6 +805,24 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
this.id = id;
}
protected void executeTriggerlessTransitions(StateMachine<S, E> stateMachine, StateContext<S, E> stateContext, State<S, E> state) {
this.stateMachineExecutor.executeTriggerlessTransitions(stateContext, state);
State<S, E> cs = currentState;
if (cs != null && cs.isOrthogonal()) {
Collection<Region<S, E>> regions = ((AbstractState<S, E>)cs).getRegions();
for (Region<S, E> region : regions) {
((AbstractStateMachine<S, E>)region).executeTriggerlessTransitions(this, stateContext, state);
}
} else if (cs != null && cs.isSubmachineState()) {
StateMachine<S, E> submachine = ((AbstractState<S, E>)cs).getSubmachine();
((AbstractStateMachine<S, E>)submachine).executeTriggerlessTransitions(this, stateContext, state);
}
}
protected StateMachineExecutor<S, E> getStateMachineExecutor() {
return stateMachineExecutor;
}
protected synchronized boolean acceptEvent(Message<E> message) {
if ((currentState != null && currentState.shouldDefer(message))) {
log.info("Current state " + currentState + " deferred event " + message);
@@ -1012,12 +1034,18 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
if (states.contains(state)) {
if (exit) {
exitCurrentState(state, message, transition, stateMachine, sources, targets);
try {
exitCurrentState(state, message, transition, stateMachine, sources, targets);
} catch (Throwable t) {
log.error("Error calling exitCurrentState", t);
}
}
State<S, E> notifyFrom = currentState;
currentState = state;
entryToState(state, message, transition, stateMachine);
notifyStateChanged(buildStateContext(Stage.STATE_CHANGED, message, null, getRelayStateMachine(), notifyFrom, state));
if (!StateMachineUtils.isPseudoState(state, PseudoStateKind.JOIN)) {
notifyStateChanged(buildStateContext(Stage.STATE_CHANGED, message, null, getRelayStateMachine(), notifyFrom, state));
}
nonDeepStatePresent = true;
if (!isRunning() && !isComplete()) {
start();
@@ -1029,7 +1057,9 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
State<S, E> notifyFrom = currentState;
currentState = findDeep;
entryToState(findDeep, message, transition, stateMachine);
notifyStateChanged(buildStateContext(Stage.STATE_CHANGED, message, null, getRelayStateMachine(), notifyFrom, findDeep));
if (!StateMachineUtils.isPseudoState(state, PseudoStateKind.JOIN)) {
notifyStateChanged(buildStateContext(Stage.STATE_CHANGED, message, null, getRelayStateMachine(), notifyFrom, findDeep));
}
if (!isRunning() && !isComplete()) {
start();
}
@@ -1214,6 +1244,7 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
if (state == null) {
return;
}
log.debug("Trying Enter state=[" + state + "]");
if (log.isTraceEnabled()) {
log.trace("Trying Enter state=[" + state + "]");
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2017 the original author or authors.
* Copyright 2015-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -46,6 +46,7 @@ import org.springframework.statemachine.StateMachineSystemConstants;
import org.springframework.statemachine.state.JoinPseudoState;
import org.springframework.statemachine.state.PseudoStateKind;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.transition.AbstractTransition;
import org.springframework.statemachine.transition.Transition;
import org.springframework.statemachine.transition.TransitionConflictPolicy;
import org.springframework.statemachine.trigger.DefaultTriggerContext;
@@ -102,7 +103,9 @@ public class DefaultStateMachineExecutor<S, E> extends LifecycleObjectSupport im
private final ReentrantLock lock = new ReentrantLock();
private final TransitionComparator<S, E> transitionComparator;;
private final TransitionComparator<S, E> transitionComparator;
private final TransitionConflictPolicy transitionConflictPolicy;
/**
* Instantiates a new default state machine executor.
@@ -128,6 +131,7 @@ public class DefaultStateMachineExecutor<S, E> extends LifecycleObjectSupport im
this.initialTransition = initialTransition;
this.initialEvent = initialEvent;
this.transitionComparator = new TransitionComparator<S, E>(transitionConflictPolicy);
this.transitionConflictPolicy = transitionConflictPolicy;
// anonymous transitions are fixed, sort those now
this.triggerlessTransitions.sort(transitionComparator);
registerTriggerListener();
@@ -204,6 +208,10 @@ public class DefaultStateMachineExecutor<S, E> extends LifecycleObjectSupport im
private final Set<State<S, E>> joinSyncStates = new HashSet<>();
private boolean handleTriggerTrans(List<Transition<S, E>> trans, Message<E> queuedMessage) {
return handleTriggerTrans(trans, queuedMessage, null);
}
private boolean handleTriggerTrans(List<Transition<S, E>> trans, Message<E> queuedMessage, State<S, E> completion) {
boolean transit = false;
for (Transition<S, E> t : trans) {
if (t == null) {
@@ -221,6 +229,16 @@ public class DefaultStateMachineExecutor<S, E> extends LifecycleObjectSupport im
continue;
}
if (transitionConflictPolicy != TransitionConflictPolicy.PARENT && completion != null && !source.getId().equals(completion.getId())) {
if (source.isOrthogonal()) {
continue;
}
else if (!StateMachineUtils.isSubstate(source, completion)) {
continue;
}
}
// special handling of join
if (StateMachineUtils.isPseudoState(t.getTarget(), PseudoStateKind.JOIN)) {
if (joinSyncStates.isEmpty()) {
@@ -429,14 +447,30 @@ public class DefaultStateMachineExecutor<S, E> extends LifecycleObjectSupport im
trans.sort(transitionComparator);
handleTriggerTrans(trans, queuedMessage);
}
List<Transition<S, E>> transWithGuards = new ArrayList<>();
for (Transition<S, E> t : triggerlessTransitions) {
if (((AbstractTransition<S, E>)t).getGuard() != null) {
transWithGuards.add(t);
}
}
if (stateMachine.getState() != null) {
// loop triggerless transitions here so that
// all "chained" transitions will get queue message
boolean transit = false;
do {
transit = handleTriggerTrans(triggerlessTransitions, queuedMessage);
transit = handleTriggerTrans(transWithGuards, queuedMessage);
} while (transit);
}
}
@Override
public void executeTriggerlessTransitions(StateContext<S, E> context, State<S, E> state) {
if (stateMachine.getState() != null) {
handleTriggerTrans(triggerlessTransitions, context.getMessage(), state);
}
}
private synchronized boolean processDeferList() {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -133,7 +133,12 @@ public abstract class LifecycleObjectSupport implements InitializingBean, Dispos
@Override
public final void stop() {
this.lifecycleLock.lock();
if (!this.lifecycleLock.tryLock()) {
if (log.isDebugEnabled()) {
log.debug("already stopping " + this);
}
return;
}
try {
if (this.running) {
this.doStop();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2017 the original author or authors.
* Copyright 2015-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@ import org.springframework.messaging.Message;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.access.StateMachineAccess;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.transition.Transition;
import org.springframework.statemachine.trigger.Trigger;
@@ -56,6 +57,14 @@ public interface StateMachineExecutor<S, E> {
*/
void queueDeferredEvent(Message<E> message);
/**
* Execute and check all triggerless transitions.
*
* @param context the state context
* @param state the state
*/
void executeTriggerlessTransitions(StateContext<S, E> context, State<S, E> state);
/**
* Execute {@code StateMachineExecutor} logic.
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2017 the original author or authors.
* Copyright 2015-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -115,6 +115,11 @@ public abstract class AbstractTransition<S, E> implements Transition<S, E> {
return true;
}
@Override
public Guard<S, E> getGuard() {
return guard;
}
@Override
public TransitionKind getKind() {
return kind;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2017 the original author or authors.
* Copyright 2015-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.statemachine.transition;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.action.ActionListener;
import org.springframework.statemachine.guard.Guard;
import org.springframework.statemachine.security.SecurityRule;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.trigger.Trigger;
@@ -64,6 +65,13 @@ public interface Transition<S, E> {
*/
State<S,E> getTarget();
/**
* Gets the guard of this transition.
*
* @return the guard
*/
Guard<S, E> getGuard();
/**
* Gets the transition actions.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -89,6 +89,11 @@ public abstract class AbstractStateMachineTests {
TASKS, T1, T1E, T2, T2E, T3, T3E
}
public static enum TestStates4 {
READY, DONE,
TASKS, T1, T1E, T2, T2E
}
public static enum TestEvents2 {
PLAY, STOP, PAUSE, EJECT, LOAD
}
@@ -273,4 +278,25 @@ public abstract class AbstractStateMachineTests {
}
public static class TestStateMachineListener4 extends StateMachineListenerAdapter<TestStates4, TestEvents> {
public volatile CountDownLatch stateChangedLatch = new CountDownLatch(6);
public volatile CountDownLatch stateMachineStartedLatch = new CountDownLatch(1);
@Override
public void stateChanged(State<TestStates4, TestEvents> from, State<TestStates4, TestEvents> to) {
stateChangedLatch.countDown();
}
@Override
public void stateMachineStarted(StateMachine<TestStates4, TestEvents> stateMachine) {
stateMachineStartedLatch.countDown();
}
public void reset(int c1, int c2) {
stateChangedLatch = new CountDownLatch(c1);
stateMachineStartedLatch = new CountDownLatch(c2);
}
}
}

View File

@@ -0,0 +1,419 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.state;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.statemachine.AbstractStateMachineTests;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.StateMachineSystemConstants;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
public class CompletionEventTests extends AbstractStateMachineTests {
@Override
protected AnnotationConfigApplicationContext buildContext() {
return new AnnotationConfigApplicationContext();
}
@SuppressWarnings({ "unchecked" })
@Test
public void testSimpleStateWithStateActionCompletes() throws Exception {
context.register(Config1.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
StateMachine<String,String> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
TestCountAction testAction2 = context.getBean("testAction2", TestCountAction.class);
machine.start();
machine.sendEvent(MessageBuilder.withPayload("E1").build());
assertThat(testAction2.latch.await(2, TimeUnit.SECONDS), is(true));
assertThat(testAction2.count, is(1));
Thread.sleep(1000);
assertThat(machine.getState().getId(), is("S3"));
}
@SuppressWarnings({ "unchecked" })
@Test
public void testSimpleStateWithStateActionCompletesThreading() throws Exception {
context.register(Config1.class, BaseConfig2.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
StateMachine<String,String> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
TestCountAction testAction2 = context.getBean("testAction2", TestCountAction.class);
machine.start();
Thread.sleep(1000);
machine.sendEvent(MessageBuilder.withPayload("E1").build());
assertThat(testAction2.latch.await(2, TimeUnit.SECONDS), is(true));
assertThat(testAction2.count, is(1));
Thread.sleep(1000);
assertThat(machine.getState().getId(), is("S3"));
}
@SuppressWarnings({ "unchecked" })
@Test
public void testSimpleStateWithoutStateActionCompletes() throws Exception {
context.register(Config2.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
StateMachine<String,String> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
machine.start();
assertThat(machine.getState().getId(), is("S1"));
machine.sendEvent(MessageBuilder.withPayload("E1").build());
assertThat(machine.getState().getId(), is("S3"));
}
public void testSubmachineWithStateActionCompletes() throws Exception {
}
@SuppressWarnings({ "unchecked" })
@Test
public void testSubmachineWithoutStateActionCompletes() throws Exception {
context.register(Config3.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
StateMachine<String,String> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
machine.start();
assertThat(machine.getState().getId(), is("S1"));
machine.sendEvent(MessageBuilder.withPayload("E1").build());
assertThat(machine.getState().getId(), is("S3"));
}
@SuppressWarnings({ "unchecked" })
@Test
public void testSubmachineWithoutStateActionCompletes2() throws Exception {
context.register(Config5.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
StateMachine<String,String> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
machine.start();
assertThat(machine.getState().getId(), is("S1"));
machine.sendEvent(MessageBuilder.withPayload("E1").build());
assertThat(machine.getState().getId(), is("S3"));
}
@SuppressWarnings({ "unchecked" })
@Test
public void testSubmachineWithoutStateActionCompletesThreading() throws Exception {
context.register(Config3.class, BaseConfig2.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
StateMachine<String,String> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
machine.start();
Thread.sleep(200);
assertThat(machine.getState().getId(), is("S1"));
machine.sendEvent(MessageBuilder.withPayload("E1").build());
Thread.sleep(200);
assertThat(machine.getState().getId(), is("S3"));
}
public void testRegionWithStateActionCompletes() throws Exception {
}
@SuppressWarnings({ "unchecked" })
@Test
public void testRegionWithoutStateActionCompletes() throws Exception {
context.register(Config4.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
StateMachine<String,String> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
machine.start();
assertThat(machine.getState().getId(), is("S1"));
machine.sendEvent(MessageBuilder.withPayload("E1").build());
assertThat(machine.getState().getId(), is("S3"));
}
@Configuration
@EnableStateMachine
static class Config1 extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial("S1")
.stateDo("S2", testAction2())
.state("S3");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withExternal()
.source("S1")
.target("S2")
.event("E1")
.and()
.withExternal()
.source("S2")
.target("S3");
}
@Bean
public TestCountAction testAction2() {
return new TestCountAction() {
@Override
public void execute(StateContext<String, String> context) {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
super.execute(context);
}
};
}
}
@Configuration
@EnableStateMachine
static class Config2 extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial("S1")
.state("S2")
.state("S3");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withExternal()
.source("S1")
.target("S2")
.event("E1")
.and()
.withExternal()
.source("S2")
.target("S3");
}
}
@Configuration
@EnableStateMachine
static class Config3 extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial("S1")
.state("S2")
.state("S3")
.and()
.withStates()
.parent("S2")
.initial("S21")
.state("S22")
.end("S23");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withExternal()
.source("S1")
.target("S2")
.event("E1")
.and()
.withExternal()
.source("S21")
.target("S22")
.and()
.withExternal()
.source("S22")
.target("S23")
.and()
.withExternal()
.source("S2")
.target("S3");
}
}
@Configuration
@EnableStateMachine
static class Config4 extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial("S1")
.state("S2")
.state("S3")
.and()
.withStates()
.parent("S2")
.initial("S201")
.state("S202")
.end("S203")
.and()
.withStates()
.parent("S2")
.initial("S211")
.state("S212")
.end("S213");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withExternal()
.source("S1")
.target("S2")
.event("E1")
.and()
.withExternal()
.source("S201")
.target("S202")
.and()
.withExternal()
.source("S202")
.target("S203")
.and()
.withExternal()
.source("S211")
.target("S212")
.and()
.withExternal()
.source("S212")
.target("S213")
.and()
.withExternal()
.source("S2")
.target("S3");
}
}
@Configuration
@EnableStateMachine
static class Config5 extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial("S1")
.state("S2")
.state("S3")
.and()
.withStates()
.parent("S2")
.initial("S21")
.state("S22")
.end("S23")
.and()
.withStates()
.parent("S22")
.initial("S221")
.state("S222")
.end("S223");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withExternal()
.source("S1")
.target("S2")
.event("E1")
.and()
.withExternal()
.source("S21")
.target("S22")
.and()
.withExternal()
.source("S22")
.target("S23")
.and()
.withExternal()
.source("S221")
.target("S222")
.and()
.withExternal()
.source("S222")
.target("S223")
.and()
.withExternal()
.source("S2")
.target("S3");
}
}
private static class TestCountAction implements Action<String, String> {
int count = 0;
StateContext<String, String> context;
CountDownLatch latch = new CountDownLatch(1);
public TestCountAction() {
count = 0;
}
@Override
public void execute(StateContext<String, String> context) {
this.context = context;
count++;
latch.countDown();
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2017 the original author or authors.
* Copyright 2015-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,12 +16,14 @@
package org.springframework.statemachine.state;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.util.EnumSet;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
@@ -165,6 +167,73 @@ public class EndStateTests extends AbstractStateMachineTests {
assertThat(machine.getState().getIds(), contains(TestStates.S2));
}
@Test
public void testEndStatesWithRegionsCompletionCompletes() throws InterruptedException {
context.register(Config8.class, BaseConfig2.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
@SuppressWarnings("unchecked")
ObjectStateMachine<TestStates4,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
TestStateMachineListener4 listener = new TestStateMachineListener4();
machine.addStateListener(listener);
machine.start();
assertThat(listener.stateMachineStartedLatch.await(2, TimeUnit.SECONDS), is(true));
machine.sendEvent(TestEvents.E1);
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(machine.getState().getIds(), contains(TestStates4.DONE));
}
@Test
public void testEndStatesWithSubmachineCompletionCompletes() throws InterruptedException {
context.register(Config9.class, BaseConfig2.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
@SuppressWarnings("unchecked")
ObjectStateMachine<TestStates4,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
TestStateMachineListener4 listener = new TestStateMachineListener4();
machine.addStateListener(listener);
machine.start();
assertThat(listener.stateMachineStartedLatch.await(2, TimeUnit.SECONDS), is(true));
machine.sendEvent(TestEvents.E1);
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(machine.getState().getIds(), contains(TestStates4.DONE));
}
@Test
public void testEndStatesWithRegionsCompletionCompletes2() throws InterruptedException {
context.register(Config10.class, BaseConfig2.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
@SuppressWarnings("unchecked")
ObjectStateMachine<TestStates4,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
TestStateMachineListener4 listener = new TestStateMachineListener4();
machine.addStateListener(listener);
listener.reset(1, 1);
machine.start();
assertThat(listener.stateMachineStartedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(machine.getState().getIds(), contains(TestStates4.READY));
listener.reset(3, 0);
machine.sendEvent(TestEvents.E1);
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates4.TASKS, TestStates4.T1, TestStates4.T2));
listener.reset(1, 0);
machine.sendEvent(TestEvents.E2);
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates4.TASKS, TestStates4.T1E, TestStates4.T2));
listener.reset(2, 0);
machine.sendEvent(TestEvents.E3);
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(machine.getState().getIds(), contains(TestStates4.DONE));
}
@Configuration
@EnableStateMachine
static class Config1 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@@ -496,4 +565,131 @@ public class EndStateTests extends AbstractStateMachineTests {
.event(TestEvents.E2);
}
}
@Configuration
@EnableStateMachine
static class Config8 extends EnumStateMachineConfigurerAdapter<TestStates4, TestEvents> {
@Override
public void configure(StateMachineStateConfigurer<TestStates4, TestEvents> states)
throws Exception {
states
.withStates()
.initial(TestStates4.READY)
.state(TestStates4.TASKS)
.state(TestStates4.DONE)
.and()
.withStates()
.parent(TestStates4.TASKS)
.initial(TestStates4.T1)
.end(TestStates4.T1E)
.and()
.withStates()
.parent(TestStates4.TASKS)
.initial(TestStates4.T2)
.end(TestStates4.T2E);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates4, TestEvents> transitions)
throws Exception {
transitions
.withExternal()
.source(TestStates4.READY).target(TestStates4.TASKS)
.event(TestEvents.E1)
.and()
.withExternal()
.source(TestStates4.T1).target(TestStates4.T1E)
.and()
.withExternal()
.source(TestStates4.T2).target(TestStates4.T2E)
.and()
.withExternal()
.source(TestStates4.TASKS).target(TestStates4.DONE);
}
}
@Configuration
@EnableStateMachine
static class Config9 extends EnumStateMachineConfigurerAdapter<TestStates4, TestEvents> {
@Override
public void configure(StateMachineStateConfigurer<TestStates4, TestEvents> states)
throws Exception {
states
.withStates()
.initial(TestStates4.READY)
.state(TestStates4.TASKS)
.state(TestStates4.DONE)
.and()
.withStates()
.parent(TestStates4.TASKS)
.initial(TestStates4.T1)
.end(TestStates4.T1E);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates4, TestEvents> transitions)
throws Exception {
transitions
.withExternal()
.source(TestStates4.READY).target(TestStates4.TASKS)
.event(TestEvents.E1)
.and()
.withExternal()
.source(TestStates4.T1).target(TestStates4.T1E)
.and()
.withExternal()
.source(TestStates4.TASKS).target(TestStates4.DONE);
}
}
@Configuration
@EnableStateMachine
static class Config10 extends EnumStateMachineConfigurerAdapter<TestStates4, TestEvents> {
@Override
public void configure(StateMachineStateConfigurer<TestStates4, TestEvents> states)
throws Exception {
states
.withStates()
.initial(TestStates4.READY)
.state(TestStates4.TASKS)
.state(TestStates4.DONE)
.and()
.withStates()
.parent(TestStates4.TASKS)
.initial(TestStates4.T1)
.end(TestStates4.T1E)
.and()
.withStates()
.parent(TestStates4.TASKS)
.initial(TestStates4.T2)
.end(TestStates4.T2E);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates4, TestEvents> transitions)
throws Exception {
transitions
.withExternal()
.source(TestStates4.READY).target(TestStates4.TASKS)
.event(TestEvents.E1)
.and()
.withExternal()
.source(TestStates4.T1).target(TestStates4.T1E)
.event(TestEvents.E2)
.and()
.withExternal()
.source(TestStates4.T2).target(TestStates4.T2E)
.event(TestEvents.E3)
.and()
.withExternal()
.source(TestStates4.TASKS).target(TestStates4.DONE);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2017 the original author or authors.
* Copyright 2015-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -37,6 +37,7 @@ import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.access.StateMachineAccessor;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.action.ActionListener;
import org.springframework.statemachine.guard.Guard;
import org.springframework.statemachine.listener.StateMachineListener;
import org.springframework.statemachine.security.SecurityRule;
import org.springframework.statemachine.state.EnumState;
@@ -118,6 +119,11 @@ public class StateContextExpressionMethodsTests {
return new EnumState<SpelStates, SpelEvents>(SpelStates.S2);
}
@Override
public Guard<SpelStates, SpelEvents> getGuard() {
return null;
}
@Override
public Collection<Action<SpelStates, SpelEvents>> getActions() {
return null;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2017 the original author or authors.
* Copyright 2015-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -348,7 +348,7 @@ public class TransitionTests extends AbstractStateMachineTests {
listener.reset(3);
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).setHeader("testHeader", "testValue").build());
assertThat(testAction1.latch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.s20Latch.getCount(), is(1L));
assertThat(listener.s20Latch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedCount, is(3));
@@ -832,13 +832,11 @@ public class TransitionTests extends AbstractStateMachineTests {
@Override
public void execute(StateContext<TestStates, TestEvents> context) {
log.info("XXX11");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
testHeader = context.getMessageHeaders().get("testHeader", String.class);
log.info("XXX12");
latch.countDown();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,6 +25,8 @@ import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -38,6 +40,7 @@ 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 org.springframework.util.StringUtils;
import demo.CommonConfiguration;
import demo.tasks.Application.Events;
@@ -45,6 +48,8 @@ import demo.tasks.Application.States;
public class TasksTests {
private final static Log log = LogFactory.getLog(TasksTests.class);
private AnnotationConfigApplicationContext context;
private StateMachine<States,Events> machine;
@@ -61,9 +66,9 @@ public class TasksTests {
@Test
public void testRunOnce() throws InterruptedException {
listener.reset(8, 0, 0);
listener.reset(8, 8, 0);
tasks.run();
assertThat(listener.stateChangedLatch.await(8, TimeUnit.SECONDS), is(true));
assertThat(listener.stateEnteredLatch.await(8, TimeUnit.SECONDS), is(true));
assertThat(machine.getState().getIds(), contains(States.READY));
Map<Object, Object> variables = machine.getExtendedState().getVariables();
assertThat(variables.size(), is(3));
@@ -71,23 +76,38 @@ public class TasksTests {
@Test
public void testRunTwice() throws InterruptedException {
listener.reset(8, 0, 0);
listener.reset(8, 8, 0);
tasks.run();
assertThat(listener.stateChangedLatch.await(8, TimeUnit.SECONDS), is(true));
assertThat(listener.stateEnteredLatch.await(8, TimeUnit.SECONDS), is(true));
assertThat(machine.getState().getIds(), contains(States.READY));
Map<Object, Object> variables = machine.getExtendedState().getVariables();
assertThat(variables.size(), is(3));
listener.reset(8, 0, 0);
listener.reset(8, 8, 0);
tasks.run();
assertThat(listener.stateChangedLatch.await(8, TimeUnit.SECONDS), is(true));
assertThat(listener.stateEnteredLatch.await(8, TimeUnit.SECONDS), is(true));
assertThat(machine.getState().getIds(), contains(States.READY));
variables = machine.getExtendedState().getVariables();
assertThat(variables.size(), is(3));
}
@Test
public void testRunSmoke() throws InterruptedException {
for (int i = 0; i < 20; i++) {
log.info("testRunSmoke SMOKE START " + i);
listener.reset(8, 8, 0);
tasks.run();
boolean await = listener.stateEnteredLatch.await(8, TimeUnit.SECONDS);
String reason = "Machine was " + machine + " " + StringUtils.collectionToCommaDelimitedString(listener.statesEntered);
assertThat(reason , await, is(true));
assertThat(machine.getState().getIds(), contains(States.READY));
log.info("testRunSmoke SMOKE STOP " + i);
}
}
@Test
public void testFailAutomaticFix() throws InterruptedException {
listener.reset(10, 0, 0);

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %t %m%n</pattern>
<charset>utf8</charset>
</encoder>
</appender>
<root level="WARN">
<appender-ref ref="CONSOLE" />
</root>
<logger name="org.springframework.statemachine" level="DEBUG"/>
</configuration>