diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/ensemble/DistributedStateMachine.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/ensemble/DistributedStateMachine.java index 06272211..f1429cc5 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/ensemble/DistributedStateMachine.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/ensemble/DistributedStateMachine.java @@ -219,11 +219,14 @@ public class DistributedStateMachine extends LifecycleObjectSupport implem stateContext.getMessageHeader(StateMachineSystemConstants.STATEMACHINE_IDENTIFIER))) { StateMachineContext 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( 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(stateContext.getStateMachine().getState() + .getId(), stateContext.getEvent(), stateContext.getMessageHeaders(), stateContext + .getStateMachine().getExtendedState())); } } return stateContext; diff --git a/spring-statemachine-test/src/main/java/org/springframework/statemachine/test/StateMachineTestPlan.java b/spring-statemachine-test/src/main/java/org/springframework/statemachine/test/StateMachineTestPlan.java index 7f67e27b..4e35abfc 100644 --- a/spring-statemachine-test/src/main/java/org/springframework/statemachine/test/StateMachineTestPlan.java +++ b/spring-statemachine-test/src/main/java/org/springframework/statemachine/test/StateMachineTestPlan.java @@ -127,6 +127,20 @@ public class StateMachineTestPlan { log.info("Sending test event " + step.sendEvent + " via machine " + machine); machine.sendEvent(step.sendEvent); } + } else if (step.sendMessage != null) { + ArrayList> sendVia = new ArrayList>(); + 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 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 { for (StateMachine stateMachine : stateMachines.values()) { Map variables = stateMachine.getExtendedState().getVariables(); for (Entry 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())); diff --git a/spring-statemachine-test/src/main/java/org/springframework/statemachine/test/StateMachineTestPlanBuilder.java b/spring-statemachine-test/src/main/java/org/springframework/statemachine/test/StateMachineTestPlanBuilder.java index 20a8cd6c..d3987b30 100644 --- a/spring-statemachine-test/src/main/java/org/springframework/statemachine/test/StateMachineTestPlanBuilder.java +++ b/spring-statemachine-test/src/main/java/org/springframework/statemachine/test/StateMachineTestPlanBuilder.java @@ -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 { public class StateMachineTestPlanStepBuilder { E sendEvent; + Message sendMessage; Object sendEventMachineId; boolean sendEventToAll = false; final Collection expectStates = new ArrayList(); @@ -189,6 +191,46 @@ public class StateMachineTestPlanBuilder { return this; } + /** + * Send a message {@code Message}. 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 event) { + return sendEvent(event, false); + } + + /** + * Send a message {@code Message}. 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 event, boolean sendToAll) { + this.sendMessage = event; + this.sendEventMachineId = null; + this.sendEventToAll = sendToAll; + return this; + } + + /** + * Send a message {@code Message} 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 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 { * @return the state machine test plan builder for chaining */ public StateMachineTestPlanBuilder and() { - steps.add(new StateMachineTestPlanStep(sendEvent, sendEventMachineId, sendEventToAll, expectStates, - expectStateChanged, expectStateEntered, expectStateExited, expectEventNotAccepted, + steps.add(new StateMachineTestPlanStep(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 { static class StateMachineTestPlanStep { E sendEvent; + Message sendMessage; Object sendEventMachineId; boolean sendEventToAll = false; final Collection expectStates; @@ -372,13 +415,14 @@ public class StateMachineTestPlanBuilder { final Collection expectVariableKeys; final Map expectVariables; - public StateMachineTestPlanStep(E sendEvent, Object sendEventMachineId, boolean sendEventToAll, - Collection expectStates, Integer expectStateChanged, Integer expectStateEntered, - Integer expectStateExited, Integer expectEventNotAccepted, Integer expectTransition, - Integer expectTransitionStarted, Integer expectTransitionEnded, Integer expectStateMachineStarted, - Integer expectStateMachineStopped, Collection expectVariableKeys, - Map expectVariables) { + public StateMachineTestPlanStep(E sendEvent, Message sendMessage, Object sendEventMachineId, + boolean sendEventToAll, Collection expectStates, Integer expectStateChanged, + Integer expectStateEntered, Integer expectStateExited, Integer expectEventNotAccepted, + Integer expectTransition, Integer expectTransitionStarted, Integer expectTransitionEnded, + Integer expectStateMachineStarted, Integer expectStateMachineStopped, + Collection expectVariableKeys, Map expectVariables) { this.sendEvent = sendEvent; + this.sendMessage = sendMessage; this.sendEventMachineId = sendEventMachineId; this.sendEventToAll = sendEventToAll; this.expectStates = expectStates; diff --git a/spring-statemachine-zookeeper/src/test/java/org/springframework/statemachine/zookeeper/ZookeeperStateMachineEnsembleTests.java b/spring-statemachine-zookeeper/src/test/java/org/springframework/statemachine/zookeeper/ZookeeperStateMachineEnsembleTests.java index bd766143..ce408d3c 100644 --- a/spring-statemachine-zookeeper/src/test/java/org/springframework/statemachine/zookeeper/ZookeeperStateMachineEnsembleTests.java +++ b/spring-statemachine-zookeeper/src/test/java/org/springframework/statemachine/zookeeper/ZookeeperStateMachineEnsembleTests.java @@ -90,6 +90,36 @@ public class ZookeeperStateMachineEnsembleTests extends AbstractZookeeperTests { ensemble.setState(new DefaultStateMachineContext("S2","E1", new HashMap(), new DefaultExtendedState())); } + @Test + public void testReadStateFromOther() throws Exception { + context.register(ZkServerConfig.class, BaseConfig.class); + context.refresh(); + + CuratorFramework curatorClient = + context.getBean("curatorClient", CuratorFramework.class); + + ZookeeperStateMachineEnsemble ensemble1 = + new ZookeeperStateMachineEnsemble(curatorClient, "/foo"); + ZookeeperStateMachineEnsemble ensemble2 = + new ZookeeperStateMachineEnsemble(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("S1","E1", new HashMap(), new DefaultExtendedState())); + assertThat(curatorClient.getData().forPath("/foo/data/current").length, greaterThan(0)); + + StateMachineContext 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); diff --git a/spring-statemachine-zookeeper/src/test/java/org/springframework/statemachine/zookeeper/ZookeeperStateMachineTests.java b/spring-statemachine-zookeeper/src/test/java/org/springframework/statemachine/zookeeper/ZookeeperStateMachineTests.java index 52e64c0c..48b5f47b 100644 --- a/spring-statemachine-zookeeper/src/test/java/org/springframework/statemachine/zookeeper/ZookeeperStateMachineTests.java +++ b/spring-statemachine-zookeeper/src/test/java/org/springframework/statemachine/zookeeper/ZookeeperStateMachineTests.java @@ -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 machine1 = + buildTestStateMachine2(curatorClient); + StateMachine machine2 = + buildTestStateMachine2(curatorClient); + + Message message = MessageBuilder + .withPayload("EV") + .setHeader("testVariable", "x1") + .build(); + + StateMachineTestPlan plan = + StateMachineTestPlanBuilder.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 machine1 = + buildTestStateMachine2(curatorClient); + StateMachine machine2 = + buildTestStateMachine2(curatorClient); + + Message message = MessageBuilder + .withPayload("EV") + .setHeader("testVariable", "x1") + .build(); + + StateMachineTestPlan plan = + StateMachineTestPlanBuilder.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 { private final int match; @@ -666,4 +757,16 @@ public class ZookeeperStateMachineTests extends AbstractZookeeperTests { } } + private static class SetVariableAction implements Action { + + @Override + public void execute(StateContext context) { + String testVariable = context.getMessageHeaders().get("testVariable", String.class); + if (testVariable != null) { + context.getExtendedState().getVariables().put("testVariable", testVariable); + } + } + + } + }