Loading services/core/java/com/android/server/wm/utils/StateMachine.java 0 → 100644 +280 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * 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 com.android.server.wm.utils; import android.annotation.IntRange; import android.annotation.Nullable; import android.util.IntArray; import android.util.Slog; import android.util.SparseArray; import com.android.internal.util.AnnotationValidations; import java.util.ArrayDeque; import java.util.Queue; /** * Simple hierarchical state machine. * * The state is represented by an integer value. The root state has a value {@code 0x0}, and top * level state has a value in range {@code 0x1} to {@code 0xF}. To indicate a state B is a sub state * of a state A, assign an integer state_value(B) = state_value(A) << 4 + (0x0 .. 0xF). */ public class StateMachine { private static final String TAG = "StateMachine"; /** * Interface for implementing state specific actions. */ public interface Handler { /** * Called when state machine changes its state to this state. */ default void enter() {} /** * Called when state machine changes its state from this state to other state. */ default void exit() {} /** * @param event type of this event. * @param param parameter passed to {@link StateMachine#handle(int, Object)} * @return {@code true} if the event was handled in this handler, so we don't need to * check the parent state. Otherwise, handle() of the parent state is triggered. */ default boolean handle(int event, @Nullable Object param) { return false; } } /** * The most recent state requested by transit() call. * * @note When transit() is called recursively, this might not be same value as mState until * transit() finishes. */ private int mLastRequestedState; /** * The current state of this state machine. */ private int mState; private final IntArray mTmp = new IntArray(); private final SparseArray<Handler> mStateHandlers = new SparseArray<>(); /** * Actions which need to execute to finish requested transition. */ private final Queue<Command> mCommands = new ArrayDeque<>(); protected static class Command { static final int COMMIT = 1; static final int ENTER = 2; static final int EXIT = 3; final int mType; final int mState; private Command(int type, @IntRange(from = 0) int state) { mType = type; AnnotationValidations.validate(IntRange.class, null, state, "from", 0); mState = state; } static Command newCommit(int state) { return new Command(COMMIT, state); } static Command newEnter(int state) { return new Command(ENTER, state); } static Command newExit(int state) { return new Command(EXIT, state); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Command{ type: "); switch (mType) { case COMMIT: sb.append("commit"); break; case ENTER: sb.append("enter"); break; case EXIT: sb.append("exit"); break; default: sb.append("UNKNOWN("); sb.append(mType); sb.append(")"); break; } sb.append(" state: "); sb.append(Integer.toHexString(mState)); sb.append(" }"); return sb.toString(); } } public StateMachine() { this(0); } public StateMachine(@IntRange(from = 0) int initialState) { mState = initialState; AnnotationValidations.validate(IntRange.class, null, initialState, "from", 0); mLastRequestedState = initialState; } /** * @see #mLastRequestedState */ public int getState() { return mLastRequestedState; } protected int getCurrentState() { return mState; } protected Command[] getCommands() { final Command[] commands = new Command[mCommands.size()]; mCommands.toArray(commands); return commands; } /** * Add a handler for a specific state. * * @param state State which the given handler processes. * @param handler A handler which runs entry, exit actions and processes events. * @return Previous state handler if it's already registered, or {@code null}. */ @Nullable public Handler addStateHandler(int state, @Nullable Handler handler) { final Handler handlerOld = mStateHandlers.get(state); mStateHandlers.put(state, handler); return handlerOld; } /** * Process an event. Search handler for a given event and {@link Handler#handle(int)}. If the * handler cannot handle the event, delegate it to a handler for a parent of the given state. * * @param event Type of an event. */ public void handle(int event, @Nullable Object param) { int state = mState; while (state != 0) { final Handler h = mStateHandlers.get(state); if (h != null && h.handle(event, param)) { return; } state >>= 4; } } protected void enter(@IntRange(from = 0) int state) { AnnotationValidations.validate(IntRange.class, null, state, "from", 0); final Handler h = mStateHandlers.get(state); if (h != null) { h.enter(); } } protected void exit(@IntRange(from = 0) int state) { AnnotationValidations.validate(IntRange.class, null, state, "from", 0); final Handler h = mStateHandlers.get(state); if (h != null) { h.exit(); } } /** * @return {@code true} if a given sub state is a descendant of a given super state. */ public static boolean isIn(int subState, int superState) { while (subState > superState) { subState >>= 4; } return subState == superState; } /** * Check if the last requested state is a sub state of a given state. * * @return {@code true} if the last requested state (via {@link #transit(int)}) is a sub state * of a given state. */ public boolean isIn(int state) { return isIn(mLastRequestedState, state); } /** * Change state to the requested state. * * @param newState The new state that the state machine should be changed. */ public void transit(@IntRange(from = 0) int newState) { AnnotationValidations.validate(IntRange.class, null, newState, "from", 0); // entry and exit action might start another transition, so this transit() function can be // called recursively. In order to guarantee entry and exit actions in expected order, // we first compute the sequence and push them into a queue, then process them later. mCommands.add(Command.newCommit(newState)); if (mLastRequestedState == newState) { mCommands.add(Command.newExit(newState)); mCommands.add(Command.newEnter(newState)); } else { // mLastRequestedState to least common ancestor for (int s = mLastRequestedState; !isIn(newState, s); s >>= 4) { mCommands.add(Command.newExit(s)); } // least common ancestor to newState mTmp.clear(); for (int s = newState; !isIn(mLastRequestedState, s); s >>= 4) { mTmp.add(s); } for (int i = mTmp.size() - 1; i >= 0; --i) { mCommands.add(Command.newEnter(mTmp.get(i))); } } mLastRequestedState = newState; while (!mCommands.isEmpty()) { final Command cmd = mCommands.remove(); switch (cmd.mType) { case Command.EXIT: exit(cmd.mState); break; case Command.ENTER: enter(cmd.mState); break; case Command.COMMIT: mState = cmd.mState; break; default: Slog.e(TAG, "Unknown command type: " + cmd.mType); break; } } } } services/core/java/com/android/server/wm/utils/TEST_MAPPING 0 → 100644 +18 −0 Original line number Diff line number Diff line { "presubmit": [ { "name": "WmTests", "options": [ { "include-filter": "com.android.server.wm.utils" }, { "include-annotation": "android.platform.test.annotations.Presubmit" }, { "exclude-annotation": "androidx.test.filters.FlakyTest" } ] } ] } services/tests/wmtests/src/com/android/server/wm/utils/StateMachineTest.java 0 → 100644 +237 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * 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 com.android.server.wm.utils; import static com.android.server.wm.utils.StateMachine.isIn; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; import org.junit.Test; /** * Build/Install/Run: * atest WmTests:StateMachineTest */ @SmallTest @Presubmit public class StateMachineTest { static class LoggingHandler implements StateMachine.Handler { final int mState; final StringBuffer mStringBuffer; // True if process #handle final boolean mHandleSelf; LoggingHandler(int state, StringBuffer sb, boolean handleSelf) { mHandleSelf = handleSelf; mState = state; mStringBuffer = sb; } LoggingHandler(int state, StringBuffer sb) { this(state, sb, true /* handleSelf */); } @Override public void enter() { mStringBuffer.append('i'); mStringBuffer.append(Integer.toHexString(mState)); mStringBuffer.append(';'); } @Override public void exit() { mStringBuffer.append('o'); mStringBuffer.append(Integer.toHexString(mState)); mStringBuffer.append(';'); } @Override public boolean handle(int event, Object param) { if (mHandleSelf) { mStringBuffer.append('h'); mStringBuffer.append(Integer.toHexString(mState)); mStringBuffer.append(';'); } return mHandleSelf; } } static class LoggingHandlerTransferInExit extends LoggingHandler { final StateMachine mStateMachine; final int mStateToTransit; LoggingHandlerTransferInExit(int state, StringBuffer sb, StateMachine stateMachine, int stateToTransit) { super(state, sb); mStateMachine = stateMachine; mStateToTransit = stateToTransit; } @Override public void exit() { super.exit(); mStateMachine.transit(mStateToTransit); } } @Test public void testStateMachineIsIn() { assertTrue(isIn(0x112, 0x1)); assertTrue(isIn(0x112, 0x11)); assertTrue(isIn(0x112, 0x112)); assertFalse(isIn(0x1, 0x112)); assertFalse(isIn(0x12, 0x2)); } @Test public void testStateMachineInitialState() { StateMachine stateMachine = new StateMachine(); assertEquals(0, stateMachine.getState()); stateMachine = new StateMachine(0x23); assertEquals(0x23, stateMachine.getState()); } @Test public void testStateMachineTransitToChild() { final StringBuffer log = new StringBuffer(); StateMachine stateMachine = new StateMachine(); stateMachine.addStateHandler(0x1, new LoggingHandler(0x1, log)); stateMachine.addStateHandler(0x12, new LoggingHandler(0x12, log)); stateMachine.addStateHandler(0x123, new LoggingHandler(0x123, log)); stateMachine.addStateHandler(0x1233, new LoggingHandler(0x1233, log)); // 0x0 -> 0x12 stateMachine.transit(0x12); assertEquals("i1;i12;", log.toString()); assertEquals(0x12, stateMachine.getState()); // 0x12 -> 0x1233 log.setLength(0); stateMachine.transit(0x1233); assertEquals(0x1233, stateMachine.getState()); assertEquals("i123;i1233;", log.toString()); } @Test public void testStateMachineTransitToParent() { final StringBuffer log = new StringBuffer(); StateMachine stateMachine = new StateMachine(0x253); stateMachine.addStateHandler(0x2, new LoggingHandler(0x2, log)); stateMachine.addStateHandler(0x25, new LoggingHandler(0x25, log)); stateMachine.addStateHandler(0x253, new LoggingHandler(0x253, log)); // 0x253 -> 0x2 stateMachine.transit(0x2); assertEquals(0x2, stateMachine.getState()); assertEquals("o253;o25;", log.toString()); } @Test public void testStateMachineTransitSelf() { final StringBuffer log = new StringBuffer(); StateMachine stateMachine = new StateMachine(0x253); stateMachine.addStateHandler(0x2, new LoggingHandler(0x2, log)); stateMachine.addStateHandler(0x25, new LoggingHandler(0x25, log)); stateMachine.addStateHandler(0x253, new LoggingHandler(0x253, log)); // 0x253 -> 0x253 stateMachine.transit(0x253); assertEquals(0x253, stateMachine.getState()); assertEquals("o253;i253;", log.toString()); } @Test public void testStateMachineTransitGeneral() { final StringBuffer log = new StringBuffer(); StateMachine stateMachine = new StateMachine(0x1351); stateMachine.addStateHandler(0x1, new LoggingHandler(0x1, log)); stateMachine.addStateHandler(0x13, new LoggingHandler(0x13, log)); stateMachine.addStateHandler(0x132, new LoggingHandler(0x132, log)); stateMachine.addStateHandler(0x1322, new LoggingHandler(0x1322, log)); stateMachine.addStateHandler(0x1322, new LoggingHandler(0x1322, log)); stateMachine.addStateHandler(0x135, new LoggingHandler(0x135, log)); stateMachine.addStateHandler(0x1351, new LoggingHandler(0x1351, log)); // 0x1351 -> 0x1322 // least common ancestor = 0x13 stateMachine.transit(0x1322); assertEquals(0x1322, stateMachine.getState()); assertEquals("o1351;o135;i132;i1322;", log.toString()); } @Test public void testStateMachineTriggerStateAction() { final StringBuffer log = new StringBuffer(); StateMachine stateMachine = new StateMachine(0x253); stateMachine.addStateHandler(0x2, new LoggingHandler(0x2, log)); stateMachine.addStateHandler(0x25, new LoggingHandler(0x25, log)); stateMachine.addStateHandler(0x253, new LoggingHandler(0x253, log)); // state 0x253 handles the message itself stateMachine.handle(0, null); assertEquals("h253;", log.toString()); } @Test public void testStateMachineTriggerStateActionDelegate() { final StringBuffer log = new StringBuffer(); StateMachine stateMachine = new StateMachine(0x253); stateMachine.addStateHandler(0x2, new LoggingHandler(0x2, log)); stateMachine.addStateHandler(0x25, new LoggingHandler(0x25, log)); stateMachine.addStateHandler(0x253, new LoggingHandler(0x253, log, false /* handleSelf */)); // state 0x253 delegate the message handling to its parent state stateMachine.handle(0, null); assertEquals("h25;", log.toString()); } @Test public void testStateMachineNestedTransition() { final StringBuffer log = new StringBuffer(); StateMachine stateMachine = new StateMachine(0x25); stateMachine.addStateHandler(0x1, new LoggingHandler(0x1, log)); // Force transit to state 0x3 in exit() stateMachine.addStateHandler(0x2, new LoggingHandlerTransferInExit(0x2, log, stateMachine, 0x3)); stateMachine.addStateHandler(0x25, new LoggingHandler(0x25, log)); stateMachine.addStateHandler(0x3, new LoggingHandler(0x3, log)); stateMachine.transit(0x1); // Start transit to 0x1 // 0x25 -> 0x2 [transit(0x3) requested] -> 0x1 // 0x1 -> 0x3 // Immediately set the status to 0x1, no enter/exit assertEquals("o25;o2;i1;o1;i3;", log.toString()); } } Loading
services/core/java/com/android/server/wm/utils/StateMachine.java 0 → 100644 +280 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * 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 com.android.server.wm.utils; import android.annotation.IntRange; import android.annotation.Nullable; import android.util.IntArray; import android.util.Slog; import android.util.SparseArray; import com.android.internal.util.AnnotationValidations; import java.util.ArrayDeque; import java.util.Queue; /** * Simple hierarchical state machine. * * The state is represented by an integer value. The root state has a value {@code 0x0}, and top * level state has a value in range {@code 0x1} to {@code 0xF}. To indicate a state B is a sub state * of a state A, assign an integer state_value(B) = state_value(A) << 4 + (0x0 .. 0xF). */ public class StateMachine { private static final String TAG = "StateMachine"; /** * Interface for implementing state specific actions. */ public interface Handler { /** * Called when state machine changes its state to this state. */ default void enter() {} /** * Called when state machine changes its state from this state to other state. */ default void exit() {} /** * @param event type of this event. * @param param parameter passed to {@link StateMachine#handle(int, Object)} * @return {@code true} if the event was handled in this handler, so we don't need to * check the parent state. Otherwise, handle() of the parent state is triggered. */ default boolean handle(int event, @Nullable Object param) { return false; } } /** * The most recent state requested by transit() call. * * @note When transit() is called recursively, this might not be same value as mState until * transit() finishes. */ private int mLastRequestedState; /** * The current state of this state machine. */ private int mState; private final IntArray mTmp = new IntArray(); private final SparseArray<Handler> mStateHandlers = new SparseArray<>(); /** * Actions which need to execute to finish requested transition. */ private final Queue<Command> mCommands = new ArrayDeque<>(); protected static class Command { static final int COMMIT = 1; static final int ENTER = 2; static final int EXIT = 3; final int mType; final int mState; private Command(int type, @IntRange(from = 0) int state) { mType = type; AnnotationValidations.validate(IntRange.class, null, state, "from", 0); mState = state; } static Command newCommit(int state) { return new Command(COMMIT, state); } static Command newEnter(int state) { return new Command(ENTER, state); } static Command newExit(int state) { return new Command(EXIT, state); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Command{ type: "); switch (mType) { case COMMIT: sb.append("commit"); break; case ENTER: sb.append("enter"); break; case EXIT: sb.append("exit"); break; default: sb.append("UNKNOWN("); sb.append(mType); sb.append(")"); break; } sb.append(" state: "); sb.append(Integer.toHexString(mState)); sb.append(" }"); return sb.toString(); } } public StateMachine() { this(0); } public StateMachine(@IntRange(from = 0) int initialState) { mState = initialState; AnnotationValidations.validate(IntRange.class, null, initialState, "from", 0); mLastRequestedState = initialState; } /** * @see #mLastRequestedState */ public int getState() { return mLastRequestedState; } protected int getCurrentState() { return mState; } protected Command[] getCommands() { final Command[] commands = new Command[mCommands.size()]; mCommands.toArray(commands); return commands; } /** * Add a handler for a specific state. * * @param state State which the given handler processes. * @param handler A handler which runs entry, exit actions and processes events. * @return Previous state handler if it's already registered, or {@code null}. */ @Nullable public Handler addStateHandler(int state, @Nullable Handler handler) { final Handler handlerOld = mStateHandlers.get(state); mStateHandlers.put(state, handler); return handlerOld; } /** * Process an event. Search handler for a given event and {@link Handler#handle(int)}. If the * handler cannot handle the event, delegate it to a handler for a parent of the given state. * * @param event Type of an event. */ public void handle(int event, @Nullable Object param) { int state = mState; while (state != 0) { final Handler h = mStateHandlers.get(state); if (h != null && h.handle(event, param)) { return; } state >>= 4; } } protected void enter(@IntRange(from = 0) int state) { AnnotationValidations.validate(IntRange.class, null, state, "from", 0); final Handler h = mStateHandlers.get(state); if (h != null) { h.enter(); } } protected void exit(@IntRange(from = 0) int state) { AnnotationValidations.validate(IntRange.class, null, state, "from", 0); final Handler h = mStateHandlers.get(state); if (h != null) { h.exit(); } } /** * @return {@code true} if a given sub state is a descendant of a given super state. */ public static boolean isIn(int subState, int superState) { while (subState > superState) { subState >>= 4; } return subState == superState; } /** * Check if the last requested state is a sub state of a given state. * * @return {@code true} if the last requested state (via {@link #transit(int)}) is a sub state * of a given state. */ public boolean isIn(int state) { return isIn(mLastRequestedState, state); } /** * Change state to the requested state. * * @param newState The new state that the state machine should be changed. */ public void transit(@IntRange(from = 0) int newState) { AnnotationValidations.validate(IntRange.class, null, newState, "from", 0); // entry and exit action might start another transition, so this transit() function can be // called recursively. In order to guarantee entry and exit actions in expected order, // we first compute the sequence and push them into a queue, then process them later. mCommands.add(Command.newCommit(newState)); if (mLastRequestedState == newState) { mCommands.add(Command.newExit(newState)); mCommands.add(Command.newEnter(newState)); } else { // mLastRequestedState to least common ancestor for (int s = mLastRequestedState; !isIn(newState, s); s >>= 4) { mCommands.add(Command.newExit(s)); } // least common ancestor to newState mTmp.clear(); for (int s = newState; !isIn(mLastRequestedState, s); s >>= 4) { mTmp.add(s); } for (int i = mTmp.size() - 1; i >= 0; --i) { mCommands.add(Command.newEnter(mTmp.get(i))); } } mLastRequestedState = newState; while (!mCommands.isEmpty()) { final Command cmd = mCommands.remove(); switch (cmd.mType) { case Command.EXIT: exit(cmd.mState); break; case Command.ENTER: enter(cmd.mState); break; case Command.COMMIT: mState = cmd.mState; break; default: Slog.e(TAG, "Unknown command type: " + cmd.mType); break; } } } }
services/core/java/com/android/server/wm/utils/TEST_MAPPING 0 → 100644 +18 −0 Original line number Diff line number Diff line { "presubmit": [ { "name": "WmTests", "options": [ { "include-filter": "com.android.server.wm.utils" }, { "include-annotation": "android.platform.test.annotations.Presubmit" }, { "exclude-annotation": "androidx.test.filters.FlakyTest" } ] } ] }
services/tests/wmtests/src/com/android/server/wm/utils/StateMachineTest.java 0 → 100644 +237 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * 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 com.android.server.wm.utils; import static com.android.server.wm.utils.StateMachine.isIn; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; import org.junit.Test; /** * Build/Install/Run: * atest WmTests:StateMachineTest */ @SmallTest @Presubmit public class StateMachineTest { static class LoggingHandler implements StateMachine.Handler { final int mState; final StringBuffer mStringBuffer; // True if process #handle final boolean mHandleSelf; LoggingHandler(int state, StringBuffer sb, boolean handleSelf) { mHandleSelf = handleSelf; mState = state; mStringBuffer = sb; } LoggingHandler(int state, StringBuffer sb) { this(state, sb, true /* handleSelf */); } @Override public void enter() { mStringBuffer.append('i'); mStringBuffer.append(Integer.toHexString(mState)); mStringBuffer.append(';'); } @Override public void exit() { mStringBuffer.append('o'); mStringBuffer.append(Integer.toHexString(mState)); mStringBuffer.append(';'); } @Override public boolean handle(int event, Object param) { if (mHandleSelf) { mStringBuffer.append('h'); mStringBuffer.append(Integer.toHexString(mState)); mStringBuffer.append(';'); } return mHandleSelf; } } static class LoggingHandlerTransferInExit extends LoggingHandler { final StateMachine mStateMachine; final int mStateToTransit; LoggingHandlerTransferInExit(int state, StringBuffer sb, StateMachine stateMachine, int stateToTransit) { super(state, sb); mStateMachine = stateMachine; mStateToTransit = stateToTransit; } @Override public void exit() { super.exit(); mStateMachine.transit(mStateToTransit); } } @Test public void testStateMachineIsIn() { assertTrue(isIn(0x112, 0x1)); assertTrue(isIn(0x112, 0x11)); assertTrue(isIn(0x112, 0x112)); assertFalse(isIn(0x1, 0x112)); assertFalse(isIn(0x12, 0x2)); } @Test public void testStateMachineInitialState() { StateMachine stateMachine = new StateMachine(); assertEquals(0, stateMachine.getState()); stateMachine = new StateMachine(0x23); assertEquals(0x23, stateMachine.getState()); } @Test public void testStateMachineTransitToChild() { final StringBuffer log = new StringBuffer(); StateMachine stateMachine = new StateMachine(); stateMachine.addStateHandler(0x1, new LoggingHandler(0x1, log)); stateMachine.addStateHandler(0x12, new LoggingHandler(0x12, log)); stateMachine.addStateHandler(0x123, new LoggingHandler(0x123, log)); stateMachine.addStateHandler(0x1233, new LoggingHandler(0x1233, log)); // 0x0 -> 0x12 stateMachine.transit(0x12); assertEquals("i1;i12;", log.toString()); assertEquals(0x12, stateMachine.getState()); // 0x12 -> 0x1233 log.setLength(0); stateMachine.transit(0x1233); assertEquals(0x1233, stateMachine.getState()); assertEquals("i123;i1233;", log.toString()); } @Test public void testStateMachineTransitToParent() { final StringBuffer log = new StringBuffer(); StateMachine stateMachine = new StateMachine(0x253); stateMachine.addStateHandler(0x2, new LoggingHandler(0x2, log)); stateMachine.addStateHandler(0x25, new LoggingHandler(0x25, log)); stateMachine.addStateHandler(0x253, new LoggingHandler(0x253, log)); // 0x253 -> 0x2 stateMachine.transit(0x2); assertEquals(0x2, stateMachine.getState()); assertEquals("o253;o25;", log.toString()); } @Test public void testStateMachineTransitSelf() { final StringBuffer log = new StringBuffer(); StateMachine stateMachine = new StateMachine(0x253); stateMachine.addStateHandler(0x2, new LoggingHandler(0x2, log)); stateMachine.addStateHandler(0x25, new LoggingHandler(0x25, log)); stateMachine.addStateHandler(0x253, new LoggingHandler(0x253, log)); // 0x253 -> 0x253 stateMachine.transit(0x253); assertEquals(0x253, stateMachine.getState()); assertEquals("o253;i253;", log.toString()); } @Test public void testStateMachineTransitGeneral() { final StringBuffer log = new StringBuffer(); StateMachine stateMachine = new StateMachine(0x1351); stateMachine.addStateHandler(0x1, new LoggingHandler(0x1, log)); stateMachine.addStateHandler(0x13, new LoggingHandler(0x13, log)); stateMachine.addStateHandler(0x132, new LoggingHandler(0x132, log)); stateMachine.addStateHandler(0x1322, new LoggingHandler(0x1322, log)); stateMachine.addStateHandler(0x1322, new LoggingHandler(0x1322, log)); stateMachine.addStateHandler(0x135, new LoggingHandler(0x135, log)); stateMachine.addStateHandler(0x1351, new LoggingHandler(0x1351, log)); // 0x1351 -> 0x1322 // least common ancestor = 0x13 stateMachine.transit(0x1322); assertEquals(0x1322, stateMachine.getState()); assertEquals("o1351;o135;i132;i1322;", log.toString()); } @Test public void testStateMachineTriggerStateAction() { final StringBuffer log = new StringBuffer(); StateMachine stateMachine = new StateMachine(0x253); stateMachine.addStateHandler(0x2, new LoggingHandler(0x2, log)); stateMachine.addStateHandler(0x25, new LoggingHandler(0x25, log)); stateMachine.addStateHandler(0x253, new LoggingHandler(0x253, log)); // state 0x253 handles the message itself stateMachine.handle(0, null); assertEquals("h253;", log.toString()); } @Test public void testStateMachineTriggerStateActionDelegate() { final StringBuffer log = new StringBuffer(); StateMachine stateMachine = new StateMachine(0x253); stateMachine.addStateHandler(0x2, new LoggingHandler(0x2, log)); stateMachine.addStateHandler(0x25, new LoggingHandler(0x25, log)); stateMachine.addStateHandler(0x253, new LoggingHandler(0x253, log, false /* handleSelf */)); // state 0x253 delegate the message handling to its parent state stateMachine.handle(0, null); assertEquals("h25;", log.toString()); } @Test public void testStateMachineNestedTransition() { final StringBuffer log = new StringBuffer(); StateMachine stateMachine = new StateMachine(0x25); stateMachine.addStateHandler(0x1, new LoggingHandler(0x1, log)); // Force transit to state 0x3 in exit() stateMachine.addStateHandler(0x2, new LoggingHandlerTransferInExit(0x2, log, stateMachine, 0x3)); stateMachine.addStateHandler(0x25, new LoggingHandler(0x25, log)); stateMachine.addStateHandler(0x3, new LoggingHandler(0x3, log)); stateMachine.transit(0x1); // Start transit to 0x1 // 0x25 -> 0x2 [transit(0x3) requested] -> 0x1 // 0x1 -> 0x3 // Immediately set the status to 0x1, no enter/exit assertEquals("o25;o2;i1;o1;i3;", log.toString()); } }