Fix internal transition for dist machine

- Change previous DistributedStateMachine fixes which didn't
  really work. Now explicitely getting state from machine to be set
  into ensemble for post internal transition.
- Now can send Message<E> via test plan instead of plain E.
- More testing for tweaked features.
This commit is contained in:
Janne Valkealahti
2015-08-08 10:43:54 +01:00
parent 7572a9ced3
commit 5347ea4dd9
5 changed files with 206 additions and 12 deletions

View File

@@ -219,11 +219,14 @@ public class DistributedStateMachine<S, E> extends LifecycleObjectSupport implem
stateContext.getMessageHeader(StateMachineSystemConstants.STATEMACHINE_IDENTIFIER))) {
StateMachineContext<S, E> current = ensemble.getState();
if (current != null) {
// TODO: it feels a bit wrong that this can be null so
// adding note here. feel this will come back to haunt us
ensemble.setState(new DefaultStateMachineContext<S, E>(
current.getState(), stateContext.getEvent(), stateContext
.getMessageHeaders(), stateContext.getStateMachine().getExtendedState()));
} else if (stateContext.getStateMachine().getState() != null) {
// if current ensemble state is null, get it from sm itself
ensemble.setState(new DefaultStateMachineContext<S, E>(stateContext.getStateMachine().getState()
.getId(), stateContext.getEvent(), stateContext.getMessageHeaders(), stateContext
.getStateMachine().getExtendedState()));
}
}
return stateContext;

View File

@@ -127,6 +127,20 @@ public class StateMachineTestPlan<S, E> {
log.info("Sending test event " + step.sendEvent + " via machine " + machine);
machine.sendEvent(step.sendEvent);
}
} else if (step.sendMessage != null) {
ArrayList<StateMachine<S, E>> sendVia = new ArrayList<StateMachine<S, E>>();
if (step.sendEventMachineId != null) {
sendVia.add(stateMachines.get(step.sendEventMachineId));
} else if (step.sendEventToAll) {
sendVia.addAll(stateMachines.values());
} else {
sendVia.add(stateMachines.values().iterator().next());
}
assertThat("Error finding machine to send via", sendVia, not(empty()));
for (StateMachine<S, E> machine : sendVia) {
log.info("Sending test event " + step.sendEvent + " via machine " + machine);
machine.sendEvent(step.sendMessage);
}
}
if (step.expectStateChanged != null) {
@@ -212,7 +226,7 @@ public class StateMachineTestPlan<S, E> {
for (StateMachine<S, E> stateMachine : stateMachines.values()) {
Map<Object, Object> variables = stateMachine.getExtendedState().getVariables();
for (Entry<Object, Object> entry : step.expectVariables.entrySet()) {
assertThat("Key " + entry.getKey() + " doesn exist in extended state variables",
assertThat("Key " + entry.getKey() + " doesn't exist in extended state variables",
variables.containsKey(entry.getKey()), is(true));
assertThat("Variable " + entry.getKey() + " doesn't match in extended state variables",
variables.get(entry.getKey()), is(entry.getValue()));

View File

@@ -22,6 +22,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.messaging.Message;
import org.springframework.statemachine.StateMachine;
/**
@@ -110,6 +111,7 @@ public class StateMachineTestPlanBuilder<S, E> {
public class StateMachineTestPlanStepBuilder {
E sendEvent;
Message<E> sendMessage;
Object sendEventMachineId;
boolean sendEventToAll = false;
final Collection<S> expectStates = new ArrayList<S>();
@@ -189,6 +191,46 @@ public class StateMachineTestPlanBuilder<S, E> {
return this;
}
/**
* Send a message {@code Message<E>}. In case multiple state machines
* exists, a random one will be chosen to send this event.
*
* @param event the event
* @return the state machine test plan step builder
*/
public StateMachineTestPlanStepBuilder sendEvent(Message<E> event) {
return sendEvent(event, false);
}
/**
* Send a message {@code Message<E>}. If {@code sendToAll} is set to {@code TRUE} event
* will be send to all existing machines.
*
* @param event the event
* @param sendToAll send to all machines
* @return the state machine test plan step builder
*/
public StateMachineTestPlanStepBuilder sendEvent(Message<E> event, boolean sendToAll) {
this.sendMessage = event;
this.sendEventMachineId = null;
this.sendEventToAll = sendToAll;
return this;
}
/**
* Send a message {@code Message<E>} into a state machine identified
* by {@code machineId}.
*
* @param event the event
* @param machineId the machine identifier for sending event
* @return the state machine test plan step builder
*/
public StateMachineTestPlanStepBuilder sendEvent(Message<E> event, Object machineId) {
this.sendMessage = event;
this.sendEventMachineId = machineId;
return this;
}
/**
* Expect variable to exist in extended state variables.
*
@@ -346,8 +388,8 @@ public class StateMachineTestPlanBuilder<S, E> {
* @return the state machine test plan builder for chaining
*/
public StateMachineTestPlanBuilder<S, E> and() {
steps.add(new StateMachineTestPlanStep<S, E>(sendEvent, sendEventMachineId, sendEventToAll, expectStates,
expectStateChanged, expectStateEntered, expectStateExited, expectEventNotAccepted,
steps.add(new StateMachineTestPlanStep<S, E>(sendEvent, sendMessage, sendEventMachineId, sendEventToAll,
expectStates, expectStateChanged, expectStateEntered, expectStateExited, expectEventNotAccepted,
expectTransition, expectTransitionStarted, expectTransitionEnded, expectStateMachineStarted,
expectStateMachineStopped, expectVariableKeys, expectVariables));
return StateMachineTestPlanBuilder.this;
@@ -357,6 +399,7 @@ public class StateMachineTestPlanBuilder<S, E> {
static class StateMachineTestPlanStep<S, E> {
E sendEvent;
Message<E> sendMessage;
Object sendEventMachineId;
boolean sendEventToAll = false;
final Collection<S> expectStates;
@@ -372,13 +415,14 @@ public class StateMachineTestPlanBuilder<S, E> {
final Collection<Object> expectVariableKeys;
final Map<Object, Object> expectVariables;
public StateMachineTestPlanStep(E sendEvent, Object sendEventMachineId, boolean sendEventToAll,
Collection<S> expectStates, Integer expectStateChanged, Integer expectStateEntered,
Integer expectStateExited, Integer expectEventNotAccepted, Integer expectTransition,
Integer expectTransitionStarted, Integer expectTransitionEnded, Integer expectStateMachineStarted,
Integer expectStateMachineStopped, Collection<Object> expectVariableKeys,
Map<Object, Object> expectVariables) {
public StateMachineTestPlanStep(E sendEvent, Message<E> sendMessage, Object sendEventMachineId,
boolean sendEventToAll, Collection<S> expectStates, Integer expectStateChanged,
Integer expectStateEntered, Integer expectStateExited, Integer expectEventNotAccepted,
Integer expectTransition, Integer expectTransitionStarted, Integer expectTransitionEnded,
Integer expectStateMachineStarted, Integer expectStateMachineStopped,
Collection<Object> expectVariableKeys, Map<Object, Object> expectVariables) {
this.sendEvent = sendEvent;
this.sendMessage = sendMessage;
this.sendEventMachineId = sendEventMachineId;
this.sendEventToAll = sendEventToAll;
this.expectStates = expectStates;

View File

@@ -90,6 +90,36 @@ public class ZookeeperStateMachineEnsembleTests extends AbstractZookeeperTests {
ensemble.setState(new DefaultStateMachineContext<String, String>("S2","E1", new HashMap<String, Object>(), new DefaultExtendedState()));
}
@Test
public void testReadStateFromOther() throws Exception {
context.register(ZkServerConfig.class, BaseConfig.class);
context.refresh();
CuratorFramework curatorClient =
context.getBean("curatorClient", CuratorFramework.class);
ZookeeperStateMachineEnsemble<String, String> ensemble1 =
new ZookeeperStateMachineEnsemble<String, String>(curatorClient, "/foo");
ZookeeperStateMachineEnsemble<String, String> ensemble2 =
new ZookeeperStateMachineEnsemble<String, String>(curatorClient, "/foo");
ensemble1.afterPropertiesSet();
ensemble2.afterPropertiesSet();
ensemble1.start();
ensemble2.start();
assertThat(curatorClient.checkExists().forPath("/foo/data/current"), notNullValue());
assertThat(curatorClient.getData().forPath("/foo/data/current").length, is(0));
ensemble1.setState(new DefaultStateMachineContext<String, String>("S1","E1", new HashMap<String, Object>(), new DefaultExtendedState()));
assertThat(curatorClient.getData().forPath("/foo/data/current").length, greaterThan(0));
StateMachineContext<String, String> context = ensemble2.getState();
assertThat(context, notNullValue());
assertThat(context.getState(), is("S1"));
assertThat(context.getEvent(), is("E1"));
}
@Test
public void testReceiveEvents() throws Exception {
context.register(ZkServerConfig.class, BaseConfig.class);

View File

@@ -31,6 +31,8 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.action.Action;
@@ -302,6 +304,83 @@ public class ZookeeperStateMachineTests extends AbstractZookeeperTests {
plan.test();
}
@Test
public void testExtendedStateVariables1() throws Exception {
context.register(ZkServerConfig.class, BaseConfig.class);
context.refresh();
CuratorFramework curatorClient =
context.getBean("curatorClient", CuratorFramework.class);
StateMachine<String, String> machine1 =
buildTestStateMachine2(curatorClient);
StateMachine<String, String> machine2 =
buildTestStateMachine2(curatorClient);
Message<String> message = MessageBuilder
.withPayload("EV")
.setHeader("testVariable", "x1")
.build();
StateMachineTestPlan<String, String> plan =
StateMachineTestPlanBuilder.<String, String>builder()
.defaultAwaitTime(2)
.stateMachine(machine1)
.stateMachine(machine2)
.step()
.expectStates("SI")
.and()
.step()
.sendEvent(message, machine1)
.expectTransition(1)
.expectVariable("testVariable", "x1")
.and()
.build();
plan.test();
}
@Test
public void testExtendedStateVariables2() throws Exception {
context.register(ZkServerConfig.class, BaseConfig.class);
context.refresh();
CuratorFramework curatorClient =
context.getBean("curatorClient", CuratorFramework.class);
StateMachine<String, String> machine1 =
buildTestStateMachine2(curatorClient);
StateMachine<String, String> machine2 =
buildTestStateMachine2(curatorClient);
Message<String> message = MessageBuilder
.withPayload("EV")
.setHeader("testVariable", "x1")
.build();
StateMachineTestPlan<String, String> plan =
StateMachineTestPlanBuilder.<String, String>builder()
.defaultAwaitTime(2)
.stateMachine(machine1)
.stateMachine(machine2)
.step()
.expectStates("SI")
.and()
.step()
.sendEvent("E1", machine1)
.expectStateChanged(1)
.expectStates("S1")
.and()
.step()
.sendEvent(message, machine1)
.expectTransition(1)
.expectVariable("testVariable", "x1")
.and()
.build();
plan.test();
}
@Test
@SuppressWarnings("unchecked")
public void testJoinLaterShouldSyncState() throws Exception {
@@ -618,7 +697,15 @@ public class ZookeeperStateMachineTests extends AbstractZookeeperTests {
.source("SI").target("S1").event("E1")
.and()
.withExternal()
.source("S1").target("S2").event("E2");
.source("S1").target("S2").event("E2")
.and()
.withInternal()
.source("SI").event("EV")
.action(setVariableAction())
.and()
.withInternal()
.source("S1").event("EV")
.action(setVariableAction());
return builder.build();
}
@@ -635,6 +722,10 @@ public class ZookeeperStateMachineTests extends AbstractZookeeperTests {
return new FooAction();
}
private static SetVariableAction setVariableAction() {
return new SetVariableAction();
}
private static class FooGuard implements Guard<String, String> {
private final int match;
@@ -666,4 +757,16 @@ public class ZookeeperStateMachineTests extends AbstractZookeeperTests {
}
}
private static class SetVariableAction implements Action<String, String> {
@Override
public void execute(StateContext<String, String> context) {
String testVariable = context.getMessageHeaders().get("testVariable", String.class);
if (testVariable != null) {
context.getExtendedState().getVariables().put("testVariable", testVariable);
}
}
}
}