/* * Copyright 2015 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.event; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import java.util.ArrayList; import java.util.EnumSet; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.junit.Test; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.Message; import org.springframework.statemachine.AbstractStateMachineTests; import org.springframework.statemachine.ObjectStateMachine; import org.springframework.statemachine.StateMachineSystemConstants; import org.springframework.statemachine.config.EnableStateMachine; import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; import org.springframework.statemachine.listener.StateMachineListenerAdapter; /** * Tests from state machine app context events. * * @author Janne Valkealahti * */ public class StateMachineEventTests extends AbstractStateMachineTests { @Override protected AnnotationConfigApplicationContext buildContext() { return new AnnotationConfigApplicationContext(); } @Test public void testContextEvents() throws Exception { context.register(BaseConfig.class, Config1.class); context.refresh(); assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); TestEventListener listener = context.getBean(TestEventListener.class); @SuppressWarnings("unchecked") ObjectStateMachine<TestStates,TestEvents> machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); machine.start(); assertThat(machine, notNullValue()); machine.sendEvent(TestEvents.E1); machine.sendEvent(TestEvents.E2); machine.sendEvent(TestEvents.E3); machine.sendEvent(TestEvents.E4); machine.sendEvent(TestEvents.EF); // 6 events instead of 5, first one is initial transition // to SI where source state is null assertThat(listener.onEventLatch.await(5, TimeUnit.SECONDS), is(true)); assertThat(listener.stateChangedEvents.size(), is(6)); } @Test public void testEventNotAccepted() throws Exception { context.register(BaseConfig.class, Config1.class); context.refresh(); assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); TestEventListener eventListener = context.getBean(TestEventListener.class); TestListener listener = new TestListener(); @SuppressWarnings("unchecked") ObjectStateMachine<TestStates,TestEvents> machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); eventListener.reset(2, 0); machine.addStateListener(listener); machine.start(); assertThat(machine, notNullValue()); machine.sendEvent(TestEvents.E1); assertThat(eventListener.onEventLatch.await(2, TimeUnit.SECONDS), is(true)); assertThat(eventListener.stateChangedEvents.size(), is(2)); eventListener.reset(0, 1); machine.sendEvent(TestEvents.E3); assertThat(eventListener.onEventLatch.await(2, TimeUnit.SECONDS), is(true)); assertThat(eventListener.eventNotAcceptedEvents.size(), is(1)); assertThat(eventListener.eventNotAcceptedEvents.get(0), instanceOf(OnEventNotAcceptedEvent.class)); assertThat(((OnEventNotAcceptedEvent)eventListener.eventNotAcceptedEvents.get(0)).getEvent().getPayload(), is(TestEvents.E3)); assertThat(listener.eventNotAcceptedLatch.await(2, TimeUnit.SECONDS), is(true)); assertThat(listener.eventNotAccepted.size(), is(1)); } @Test public void testSubmachineHandlesEvent() throws Exception { context.register(BaseConfig.class, Config2.class); context.refresh(); assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); @SuppressWarnings("unchecked") ObjectStateMachine<TestStates,TestEvents> machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); machine.start(); assertThat(machine, notNullValue()); assertThat(machine.getState().getIds(), contains(TestStates.S1, TestStates.S10)); machine.sendEvent(TestEvents.E1); assertThat(machine.getState().getIds(), contains(TestStates.S1, TestStates.S12)); } @Test public void testEventNotAcceptedS1() throws Exception { context.register(BaseConfig.class, Config3.class); context.refresh(); assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); @SuppressWarnings("unchecked") ObjectStateMachine<TestStates,TestEvents> machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); TestListener listener = new TestListener(); machine.addStateListener(listener); machine.start(); assertThat(machine.getState().getIds(), contains(TestStates.S1, TestStates.S11, TestStates.S111)); listener.reset(1); boolean accepted = machine.sendEvent(TestEvents.E2); assertThat(accepted, is(false)); assertThat(machine.getState().getIds(), contains(TestStates.S1, TestStates.S11, TestStates.S111)); assertThat(listener.eventNotAcceptedLatch.await(1, TimeUnit.SECONDS), is(true)); assertThat(listener.eventNotAccepted.size(), is(1)); } @Test public void testEventAcceptedS1() throws Exception { context.register(BaseConfig.class, Config3.class); context.refresh(); assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); @SuppressWarnings("unchecked") ObjectStateMachine<TestStates,TestEvents> machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); TestListener listener = new TestListener(); machine.addStateListener(listener); machine.start(); assertThat(machine.getState().getIds(), contains(TestStates.S1, TestStates.S11, TestStates.S111)); listener.reset(1); boolean accepted = machine.sendEvent(TestEvents.E1); assertThat(accepted, is(true)); assertThat(machine.getState().getIds(), contains(TestStates.S1, TestStates.S11, TestStates.S111)); assertThat(listener.eventNotAcceptedLatch.await(1, TimeUnit.SECONDS), is(false)); assertThat(listener.eventNotAccepted.size(), is(0)); } @Test public void testEventAcceptedS1NoS1Transition() throws Exception { context.register(BaseConfig.class, Config4.class); context.refresh(); assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); @SuppressWarnings("unchecked") ObjectStateMachine<TestStates,TestEvents> machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); TestListener listener = new TestListener(); machine.addStateListener(listener); machine.start(); assertThat(machine.getState().getIds(), contains(TestStates.S1, TestStates.S11, TestStates.S111)); listener.reset(1); boolean accepted = machine.sendEvent(TestEvents.E1); assertThat(accepted, is(true)); assertThat(machine.getState().getIds(), contains(TestStates.S1, TestStates.S11, TestStates.S111)); assertThat(listener.eventNotAcceptedLatch.await(1, TimeUnit.SECONDS), is(false)); assertThat(listener.eventNotAccepted.size(), is(0)); } @Test public void testEventAcceptedS1GuardAllow() throws Exception { context.register(BaseConfig.class, Config3.class); context.refresh(); assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); @SuppressWarnings("unchecked") ObjectStateMachine<TestStates,TestEvents> machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); TestListener listener = new TestListener(); machine.addStateListener(listener); machine.start(); assertThat(machine.getState().getIds(), contains(TestStates.S1, TestStates.S11, TestStates.S111)); machine.getExtendedState().getVariables().put("S1E1", true); listener.reset(1); boolean accepted = machine.sendEvent(TestEvents.E1); assertThat(accepted, is(true)); assertThat(machine.getState().getIds(), contains(TestStates.S2)); assertThat(listener.eventNotAcceptedLatch.await(1, TimeUnit.SECONDS), is(false)); assertThat(listener.eventNotAccepted.size(), is(0)); } @Test public void testEventAcceptedS11GuardAllow() throws Exception { context.register(BaseConfig.class, Config3.class); context.refresh(); assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); @SuppressWarnings("unchecked") ObjectStateMachine<TestStates,TestEvents> machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); TestListener listener = new TestListener(); machine.addStateListener(listener); machine.start(); assertThat(machine.getState().getIds(), contains(TestStates.S1, TestStates.S11, TestStates.S111)); machine.getExtendedState().getVariables().put("S11E1", true); listener.reset(1); boolean accepted = machine.sendEvent(TestEvents.E1); assertThat(accepted, is(true)); assertThat(machine.getState().getIds(), contains(TestStates.S1, TestStates.S12)); assertThat(listener.eventNotAcceptedLatch.await(1, TimeUnit.SECONDS), is(false)); assertThat(listener.eventNotAccepted.size(), is(0)); } @Test public void testEventAcceptedS111GuardAllow() throws Exception { context.register(BaseConfig.class, Config3.class); context.refresh(); assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); @SuppressWarnings("unchecked") ObjectStateMachine<TestStates,TestEvents> machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); TestListener listener = new TestListener(); machine.addStateListener(listener); machine.start(); assertThat(machine.getState().getIds(), contains(TestStates.S1, TestStates.S11, TestStates.S111)); machine.getExtendedState().getVariables().put("S111E1", true); listener.reset(1); boolean accepted = machine.sendEvent(TestEvents.E1); assertThat(accepted, is(true)); assertThat(machine.getState().getIds(), contains(TestStates.S1, TestStates.S11, TestStates.S112)); assertThat(listener.eventNotAcceptedLatch.await(1, TimeUnit.SECONDS), is(false)); assertThat(listener.eventNotAccepted.size(), is(0)); } @Configuration @EnableStateMachine static class Config1 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> { @Override public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception { states .withStates() .initial(TestStates.SI) .states(EnumSet.allOf(TestStates.class)) .end(TestStates.SF); } @Override public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception { transitions .withExternal() .source(TestStates.SI) .target(TestStates.S1) .event(TestEvents.E1) .and() .withExternal() .source(TestStates.S1) .target(TestStates.S2) .event(TestEvents.E2) .and() .withExternal() .source(TestStates.S2) .target(TestStates.S3) .event(TestEvents.E3) .and() .withExternal() .source(TestStates.S3) .target(TestStates.S4) .event(TestEvents.E4) .and() .withExternal() .source(TestStates.S4) .target(TestStates.SF) .event(TestEvents.EF); } @Bean public TestEventListener testEventListener() { return new TestEventListener(); } } @Configuration @EnableStateMachine static class Config2 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> { @Override public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception { states .withStates() .initial(TestStates.S1) .state(TestStates.S1) .and() .withStates() .parent(TestStates.S1) .initial(TestStates.S10) .state(TestStates.S10) .state(TestStates.S11) .state(TestStates.S12) .state(TestStates.S13); } @Override public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception { transitions .withExternal() .source(TestStates.S10) .target(TestStates.S11) .event(TestEvents.E1) .and() .withExternal() .state(TestStates.S1) .source(TestStates.S10) .target(TestStates.S12) .event(TestEvents.E1) .and() .withExternal() .state(TestStates.S1) .source(TestStates.S10) .target(TestStates.S13) .event(TestEvents.E2); } } @Configuration @EnableStateMachine static class Config3 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> { @Override public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception { states .withStates() .initial(TestStates.S1) .state(TestStates.S2) .and() .withStates() .parent(TestStates.S1) .initial(TestStates.S11) .state(TestStates.S12) .and() .withStates() .parent(TestStates.S11) .initial(TestStates.S111) .state(TestStates.S112); } @Override public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception { transitions .withExternal() .source(TestStates.S1) .target(TestStates.S2) .event(TestEvents.E1) .guardExpression("extendedState.variables.containsKey('S1E1')") .and() .withExternal() .source(TestStates.S11) .target(TestStates.S12) .event(TestEvents.E1) .guardExpression("extendedState.variables.containsKey('S11E1')") .and() .withExternal() .source(TestStates.S111) .target(TestStates.S112) .event(TestEvents.E1) .guardExpression("extendedState.variables.containsKey('S111E1')"); } } @Configuration @EnableStateMachine static class Config4 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> { @Override public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception { states .withStates() .initial(TestStates.S1) .state(TestStates.S2) .and() .withStates() .parent(TestStates.S1) .initial(TestStates.S11) .state(TestStates.S12) .and() .withStates() .parent(TestStates.S11) .initial(TestStates.S111) .state(TestStates.S112); } @Override public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception { transitions .withExternal() .source(TestStates.S11) .target(TestStates.S12) .event(TestEvents.E1) .guardExpression("extendedState.variables.containsKey('S11E1')") .and() .withExternal() .source(TestStates.S111) .target(TestStates.S112) .event(TestEvents.E1) .guardExpression("extendedState.variables.containsKey('S111E1')"); } } static class TestEventListener implements ApplicationListener<StateMachineEvent> { volatile CountDownLatch onEventLatch = new CountDownLatch(6); volatile ArrayList<StateMachineEvent> stateChangedEvents = new ArrayList<StateMachineEvent>(); volatile ArrayList<StateMachineEvent> eventNotAcceptedEvents = new ArrayList<StateMachineEvent>(); @Override public void onApplicationEvent(StateMachineEvent event) { if (event instanceof OnStateChangedEvent) { stateChangedEvents.add(event); onEventLatch.countDown(); } else if (event instanceof OnEventNotAcceptedEvent) { eventNotAcceptedEvents.add(event); onEventLatch.countDown(); } } public void reset(int c1,int c2) { onEventLatch = new CountDownLatch(c1); eventNotAcceptedEvents = new ArrayList<StateMachineEvent>(); stateChangedEvents.clear(); eventNotAcceptedEvents.clear(); } } static class TestListener extends StateMachineListenerAdapter<TestStates, TestEvents> { volatile CountDownLatch eventNotAcceptedLatch = new CountDownLatch(1); volatile ArrayList<Message<TestEvents>> eventNotAccepted = new ArrayList<Message<TestEvents>>(); @Override public void eventNotAccepted(Message<TestEvents> event) { eventNotAccepted.add(event); eventNotAcceptedLatch.countDown(); } public void reset(int c1) { eventNotAcceptedLatch = new CountDownLatch(c1); eventNotAccepted.clear(); } } }