// ================================================================================================= // Copyright 2011 Twitter, Inc. // ------------------------------------------------------------------------------------------------- // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this work except in compliance with the License. // You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.util; import com.google.common.collect.ImmutableSet; import com.twitter.common.base.Closure; import com.twitter.common.base.Closures; import com.twitter.common.base.Command; import com.twitter.common.base.Commands; import com.twitter.common.base.ExceptionalSupplier; import com.twitter.common.base.Supplier; import com.twitter.common.testing.EasyMockTest; import com.twitter.common.util.StateMachine.Transition; import com.twitter.common.util.StateMachine.Rule; import org.junit.Test; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** * Tests the functionality of StateMachine. * * @author William Farner */ public class StateMachineTest extends EasyMockTest { private static final String NAME = "State machine."; private static final String A = "A"; private static final String B = "B"; private static final String C = "C"; private static final String D = "D"; @Test public void testEmptySM() { control.replay(); try { StateMachine.builder(NAME).build(); fail(); } catch (IllegalStateException e) { // Expected. } } @Test public void testMachineNoInit() { control.replay(); try { StateMachine.<String>builder(NAME) .addState(Rule.from(A).to(B)) .build(); fail(); } catch (IllegalStateException e) { // Expected. } } @Test public void testBasicFSM() { control.replay(); StateMachine<String> fsm = StateMachine.<String>builder(NAME) .initialState(A) .addState(Rule.from(A).to(B)) .addState(Rule.from(B).to(C)) .addState(Rule.from(C).to(D)) .build(); assertThat(fsm.getState(), is(A)); changeState(fsm, B); changeState(fsm, C); changeState(fsm, D); } @Test public void testLoopingFSM() { control.replay(); StateMachine<String> fsm = StateMachine.<String>builder(NAME) .initialState(A) .addState(Rule.from(A).to(B)) .addState(Rule.from(B).to(C)) .addState(Rule.from(C).to(B, D)) .build(); assertThat(fsm.getState(), is(A)); changeState(fsm, B); changeState(fsm, C); changeState(fsm, B); changeState(fsm, C); changeState(fsm, B); changeState(fsm, C); changeState(fsm, D); } @Test public void testMachineUnknownState() { control.replay(); StateMachine<String> fsm = StateMachine.<String>builder(NAME) .initialState(A) .addState(Rule.from(A).to(B)) .addState(Rule.from(B).to(C)) .build(); assertThat(fsm.getState(), is(A)); changeState(fsm, B); changeState(fsm, C); changeStateFail(fsm, D); } @Test public void testMachineBadTransition() { control.replay(); StateMachine<String> fsm = StateMachine.<String>builder(NAME) .initialState(A) .addState(Rule.from(A).to(B)) .addState(Rule.from(B).to(C)) .build(); assertThat(fsm.getState(), is(A)); changeState(fsm, B); changeState(fsm, C); changeStateFail(fsm, B); } @Test public void testMachineSelfTransitionAllowed() { control.replay(); StateMachine<String> fsm = StateMachine.<String>builder(NAME) .initialState(A) .addState(Rule.from(A).to(A)) .build(); assertThat(fsm.getState(), is(A)); changeState(fsm, A); changeState(fsm, A); } @Test public void testMachineSelfTransitionDisallowed() { control.replay(); StateMachine<String> fsm = StateMachine.<String>builder(NAME) .initialState(A) .addState(Rule.from(A).to(B)) .build(); assertThat(fsm.getState(), is(A)); changeStateFail(fsm, A); changeStateFail(fsm, A); } @Test public void testCheckStateMatches() { control.replay(); StateMachine<String> stateMachine = StateMachine.<String>builder(NAME) .initialState(A) .addState(Rule.from(A).to(B)) .build(); stateMachine.checkState(A); stateMachine.transition(B); stateMachine.checkState(B); } @Test(expected = IllegalStateException.class) public void testCheckStateFails() { control.replay(); StateMachine.<String>builder(NAME) .initialState(A) .addState(Rule.from(A).to(B)) .build() .checkState(B); } @Test public void testDoInStateMatches() { control.replay(); StateMachine<String> stateMachine = StateMachine.<String>builder(NAME) .initialState(A) .addState(Rule.from(A).to(B)) .build(); int amount = stateMachine.doInState(A, new Supplier<Integer>() { @Override public Integer get() { return 42; } }); assertThat(amount, is(42)); stateMachine.transition(B); String name = stateMachine.doInState(B, new Supplier<String>() { @Override public String get() { return "jake"; } }); assertThat(name, is("jake")); } @Test public void testDoInStateConcurrently() throws InterruptedException { control.replay(); final StateMachine<String> stateMachine = StateMachine.<String>builder(NAME) .initialState(A) .addState(A, B) .build(); final BlockingQueue<Integer> results = new LinkedBlockingQueue<Integer>(); final CountDownLatch supplier1Proceed = new CountDownLatch(1); final ExceptionalSupplier<Void, RuntimeException> supplier1 = Commands.asSupplier(new Command() { @Override public void execute() { results.offer(1); try { supplier1Proceed.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); final CountDownLatch supplier2Proceed = new CountDownLatch(1); final ExceptionalSupplier<Void, RuntimeException> supplier2 = Commands.asSupplier(new Command() { @Override public void execute() { results.offer(2); try { supplier2Proceed.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); Thread thread1 = new Thread(new Runnable() { @Override public void run() { stateMachine.doInState(A, supplier1); } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { stateMachine.doInState(A, supplier2); } }); Thread thread3 = new Thread(new Runnable() { @Override public void run() { stateMachine.transition(B); } }); thread1.start(); thread2.start(); Integer result1 = results.take(); Integer result2 = results.take(); // we know 1 and 2 have the read lock held thread3.start(); // should be blocked by read locks in place assertThat(ImmutableSet.of(result1, result2), is(ImmutableSet.of(1, 2))); assertTrue(results.isEmpty()); supplier1Proceed.countDown(); supplier2Proceed.countDown(); thread1.join(); thread2.join(); thread3.join(); assertThat(B, is(stateMachine.getState())); } @Test(expected = IllegalStateException.class) public void testDoInStateFails() { control.replay(); StateMachine.<String>builder(NAME) .initialState(A) .addState(A, B) .build() .doInState(B, Commands.asSupplier(Commands.NOOP)); } @Test public void testNoThrowOnInvalidTransition() { control.replay(); StateMachine<String> machine = StateMachine.<String>builder(NAME) .initialState(A) .addState(A, B) .throwOnBadTransition(false) .build(); machine.transition(C); assertThat(machine.getState(), is(A)); } private static final Clazz<Closure<Transition<String>>> TRANSITION_CLOSURE_CLZ = new Clazz<Closure<Transition<String>>>() {}; @Test public void testTransitionCallbacks() { Closure<Transition<String>> anyTransition = createMock(TRANSITION_CLOSURE_CLZ); Closure<Transition<String>> fromA = createMock(TRANSITION_CLOSURE_CLZ); Closure<Transition<String>> fromB = createMock(TRANSITION_CLOSURE_CLZ); Transition<String> aToB = new Transition<String>(A, B, true); anyTransition.execute(aToB); fromA.execute(aToB); Transition<String> bToB = new Transition<String>(B, B, false); anyTransition.execute(bToB); fromB.execute(bToB); Transition<String> bToC = new Transition<String>(B, C, true); anyTransition.execute(bToC); fromB.execute(bToC); anyTransition.execute(new Transition<String>(C, B, true)); Transition<String> bToD = new Transition<String>(B, D, true); anyTransition.execute(bToD); fromB.execute(bToD); control.replay(); StateMachine<String> machine = StateMachine.<String>builder(NAME) .initialState(A) .addState(Rule.from(A).to(B).withCallback(fromA)) .addState(Rule.from(B).to(C, D).withCallback(fromB)) .addState(Rule.from(C).to(B)) .addState(Rule.from(D).noTransitions()) .onAnyTransition(anyTransition) .throwOnBadTransition(false) .build(); machine.transition(B); machine.transition(B); machine.transition(C); machine.transition(B); machine.transition(D); } @Test public void testFilteredTransitionCallbacks() { Closure<Transition<String>> aToBHandler = createMock(TRANSITION_CLOSURE_CLZ); Closure<Transition<String>> impossibleHandler = createMock(TRANSITION_CLOSURE_CLZ); aToBHandler.execute(new Transition<String>(A, B, true)); control.replay(); StateMachine<String> machine = StateMachine.<String>builder(NAME) .initialState(A) .addState(Rule .from(A).to(B, C) .withCallback(Closures.filter(Transition.to(B), aToBHandler))) .addState(Rule.from(B).to(A) .withCallback(Closures.filter(Transition.to(B), impossibleHandler))) .addState(Rule.from(C).noTransitions()) .build(); machine.transition(B); machine.transition(A); machine.transition(C); } private static void changeState(StateMachine<String> machine, String to, boolean expectAllowed) { boolean allowed = true; try { machine.transition(to); assertThat(machine.getState(), is(to)); } catch (StateMachine.IllegalStateTransitionException e) { allowed = false; } assertThat(allowed, is(expectAllowed)); } private static void changeState(StateMachine<String> machine, String to) { changeState(machine, to, true); } private static void changeStateFail(StateMachine<String> machine, String to) { changeState(machine, to, false); } }