package org.squirrelframework.foundation.fsm.threadsafe; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.squirrelframework.foundation.fsm.AnonymousAction; import org.squirrelframework.foundation.fsm.StateMachineBuilderFactory; import org.squirrelframework.foundation.fsm.StateMachineData; import org.squirrelframework.foundation.fsm.UntypedStateMachine; import org.squirrelframework.foundation.fsm.UntypedStateMachineBuilder; import org.squirrelframework.foundation.fsm.annotation.AsyncExecute; import org.squirrelframework.foundation.fsm.annotation.OnTransitionBegin; @RunWith(Parameterized.class) public class ConcurrentEventTest { private UntypedStateMachineBuilder builder = null; private static final int TIME_INTERVAL = 50; // repeat 10 times for each test case @Parameterized.Parameters public static List<Object[]> data() { return Arrays.asList(new Object[10][0]); } @Before public void setUp() throws Exception { builder = StateMachineBuilderFactory.create(ConcurrentSimpleStateMachine.class); } @SuppressWarnings("rawtypes") @Test public void testConcurrentEvents() { // test concurrent read/write/test/dump state machine final CountDownLatch actionCondition = new CountDownLatch(1); final CountDownLatch eventCondition = new CountDownLatch(5); final AtomicReference<Object> testStateRef = new AtomicReference<Object>(); final AtomicReference<Object> dumpDataRef = new AtomicReference<Object>(); final AtomicReference<Object> readStateRef = new AtomicReference<Object>(); builder.transition().from("A").to("B").on("FIRST").perform( new AnonymousAction<UntypedStateMachine, Object, Object, Object>() { @Override public void execute(Object from, Object to, Object event, Object context, UntypedStateMachine stateMachine) { actionCondition.countDown(); try { TimeUnit.MILLISECONDS.sleep(TIME_INTERVAL); } catch (InterruptedException e) { e.printStackTrace(); } } }); builder.transition().from("B").to("C").on("SECOND"); builder.transition().from("A").to("D").on("SECOND"); builder.transition().from("C").to("E").on("SECOND"); final UntypedStateMachine fsm = builder.newStateMachine("A"); // thread 1 start process event "FIRST" new Thread(new Runnable() { @Override public void run() { fsm.fire("FIRST"); eventCondition.countDown(); } }, "Test-Thread-1").start(); // thread 2 read fsm state while processing event "FIRST" new Thread(new Runnable() { @Override public void run() { try { actionCondition.await(); } catch (InterruptedException e) { e.printStackTrace(); } readStateRef.set(fsm.getCurrentState()); eventCondition.countDown(); } }, "Test-Thread-2").start(); // thread 3 test event "SECOND" while processing event "FIRST" new Thread(new Runnable() { @Override public void run() { try { actionCondition.await(); } catch (InterruptedException e) { e.printStackTrace(); } testStateRef.set(fsm.test("SECOND")); eventCondition.countDown(); } }, "Test-Thread-3").start(); // thread 4 dump data while processing event "FIRST" new Thread(new Runnable() { @Override public void run() { try { actionCondition.await(); } catch (InterruptedException e) { e.printStackTrace(); } dumpDataRef.set(fsm.dumpSavedData()); eventCondition.countDown(); } }, "Test-Thread-4").start(); // thread 5 process event "SECOND" while processing event "FIRST" new Thread(new Runnable() { @Override public void run() { try { actionCondition.await(); } catch (InterruptedException e) { e.printStackTrace(); } fsm.fire("SECOND"); eventCondition.countDown(); } }, "Test-Thread-5").start(); // wait for all threads finish processing events then check result try { eventCondition.await(1000, TimeUnit.MILLISECONDS); TimeUnit.MILLISECONDS.sleep(TIME_INTERVAL); } catch (InterruptedException e) { fail(); } assertEquals(fsm.getCurrentState(), "C"); assertEquals(readStateRef.get(), "C"); assertEquals(testStateRef.get(), "E"); assertNotNull(dumpDataRef.get()); assertEquals(((StateMachineData.Reader)dumpDataRef.get()).currentState(), "C"); Object testAgain = fsm.test("SECOND"); assertEquals(testAgain, "E"); } @Test @SuppressWarnings("unused") public void testConcurrentAddRemoveListener() { final CountDownLatch l1Condition = new CountDownLatch(1); final CountDownLatch l3Condition = new CountDownLatch(1); final CountDownLatch eventCondition = new CountDownLatch(2); class MockCallSequence { @Mock MockCallSequence mock; public void listener1() { mock.listener1(); } public void listener2() { mock.listener2(); } public void listener3() { mock.listener3(); } } class Listener1 { MockCallSequence callSequence; @OnTransitionBegin public void onTransitionBegin() { l3Condition.countDown(); try { l1Condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } callSequence.listener1(); } } class Listener2 { MockCallSequence callSequence; @OnTransitionBegin public void onTransitionBegin() { callSequence.listener2(); } } class Listener3 { MockCallSequence callSequence; @OnTransitionBegin public void onTransitionBegin() { callSequence.listener3(); } } MockCallSequence callSequence = new MockCallSequence(); final Listener1 l1 = new Listener1(); l1.callSequence = callSequence; final Listener2 l2 = new Listener2(); l2.callSequence = callSequence; final Listener3 l3 = new Listener3(); l3.callSequence = callSequence; MockitoAnnotations.initMocks(callSequence); builder.transition().from("A").to("B").on("FIRST"); builder.transition().from("B").to("C").on("SECOND"); final UntypedStateMachine fsm = builder.newStateMachine("A"); fsm.addDeclarativeListener(l1); fsm.addDeclarativeListener(l2); InOrder inOrder = Mockito.inOrder(callSequence.mock); // thread 1 fire event "FIRST" new Thread(new Runnable() { @Override public void run() { fsm.fire("FIRST"); eventCondition.countDown(); } }, "Test-Thread-1").start(); // thread 2 add listener and fire event "SECOND" during thread 1 processing event "FIRST" new Thread(new Runnable() { @Override public void run() { try { l3Condition.await(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } fsm.addDeclarativeListener(l3); l1Condition.countDown(); fsm.fire("SECOND"); eventCondition.countDown(); } }, "Test-Thread-2").start(); // wait for all threads finish processing events then check result try { eventCondition.await(1000, TimeUnit.MILLISECONDS); TimeUnit.MILLISECONDS.sleep(TIME_INTERVAL); } catch (InterruptedException e) { fail(); } inOrder.verify(callSequence.mock, Mockito.times(1)).listener1(); inOrder.verify(callSequence.mock, Mockito.times(1)).listener2(); inOrder.verify(callSequence.mock, Mockito.times(1)).listener1(); inOrder.verify(callSequence.mock, Mockito.times(1)).listener2(); inOrder.verify(callSequence.mock, Mockito.times(1)).listener3(); } @Test @SuppressWarnings("unused") public void testAnsyncDispatchEvent() { final CountDownLatch l1Condition = new CountDownLatch(1); final CountDownLatch lCondition = new CountDownLatch(2); final CountDownLatch eventCondition = new CountDownLatch(3); final AtomicReference<Thread> executorThread1 = new AtomicReference<Thread>(); final AtomicReference<Thread> executorThread2 = new AtomicReference<Thread>(); final AtomicReference<Thread> executorThread3 = new AtomicReference<Thread>(); final Thread mainThread = Thread.currentThread(); class MockCallSequence { @Mock MockCallSequence mock; public void listener1() { // System.out.println("listener1"); mock.listener1(); } public void listener2() { // System.out.println("listener2"); mock.listener2(); } public void listener3() { // System.out.println("listener3"); mock.listener3(); } } class Listener1 { MockCallSequence callSequence; @OnTransitionBegin public void onTransitionBegin() { callSequence.listener1(); executorThread1.set(Thread.currentThread()); lCondition.countDown(); eventCondition.countDown(); } } class Listener2 { MockCallSequence callSequence; @OnTransitionBegin @AsyncExecute public void onTransitionBegin() { try { l1Condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } callSequence.listener2(); executorThread2.set(Thread.currentThread()); eventCondition.countDown(); } } class Listener3 { MockCallSequence callSequence; @OnTransitionBegin public void onTransitionBegin() { callSequence.listener3(); executorThread3.set(Thread.currentThread()); lCondition.countDown(); eventCondition.countDown(); } } MockCallSequence callSequence = new MockCallSequence(); final Listener1 l1 = new Listener1(); l1.callSequence = callSequence; final Listener2 l2 = new Listener2(); l2.callSequence = callSequence; final Listener3 l3 = new Listener3(); l3.callSequence = callSequence; MockitoAnnotations.initMocks(callSequence); builder.transition().from("A").to("B").on("FIRST"); final UntypedStateMachine fsm = builder.newStateMachine("A"); fsm.addDeclarativeListener(l1); fsm.addDeclarativeListener(l2); fsm.addDeclarativeListener(l3); InOrder inOrder = Mockito.inOrder(callSequence.mock); fsm.fire("FIRST"); try { lCondition.await(); } catch (InterruptedException e) { e.printStackTrace(); } l1Condition.countDown(); try { eventCondition.await(1000, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { fail(); } inOrder.verify(callSequence.mock, Mockito.times(1)).listener1(); inOrder.verify(callSequence.mock, Mockito.times(1)).listener3(); inOrder.verify(callSequence.mock, Mockito.times(1)).listener2(); assertTrue(executorThread1.get()!=null && executorThread1.get()==mainThread); assertTrue(executorThread3.get()!=null && executorThread3.get()==mainThread); assertTrue(executorThread2.get()!=null && executorThread2.get()!=mainThread); } }