├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── gradle.xml ├── jarRepositories.xml ├── misc.xml └── vcs.xml ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── hsm ├── build.gradle └── src │ └── main │ └── java │ └── de │ └── artcom │ └── hsm │ ├── Action.java │ ├── Event.java │ ├── EventHandler.java │ ├── Guard.java │ ├── Handler.java │ ├── Parallel.java │ ├── State.java │ ├── StateMachine.java │ ├── Sub.java │ └── TransitionKind.java ├── pmd-rules.xml ├── settings.gradle └── tests ├── build.gradle └── src └── test └── java └── de └── artcom └── hsm └── test ├── BasicStateMachineTest.java ├── ComplexTests.java ├── EventHandlingTest.java ├── GuardTest.java ├── LcaTest.java ├── LocalTransitionTest.java ├── ParallelStateMachineTest.java └── SubStateMachineTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .gradle/ 3 | /local.properties 4 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | script: 4 | - ./gradlew clean test check -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Art+Com AG 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so, subject to the following 8 | conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies 11 | or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 14 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 15 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 16 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 17 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 18 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hierarchical Statemachine for Java 2 | 3 | [![Build Status](https://img.shields.io/travis/artcom/hsm-java/master.svg?style=flat)](https://travis-ci.org/artcom/hsm-java) 4 | 5 | This Project is a hierarchical state machine framework for Java. 6 | The state machine specification is based on the [UML statemachine](http://en.wikipedia.org/wiki/UML_state_machine). 7 | The implementation is based on this project: . 8 | 9 | ### Published artifacts 10 | 11 | The artifacts of this project are intended to be received via [Jitpack.io](https://jitpack.io/) 12 | 13 | Gradle dependency example: 14 |
15 | compile 'com.github.artcom:hsm-java:0.0.3'
16 | 
17 | 18 | ### Publish to Maven Local 19 | 20 | In order to publish to the local Maven repository please use this command: 21 |
22 | ./gradlew -Pgroup=com.github.artcom publishToMavenLocal
23 | 
24 | It is necessary to provide the same groupId as used by jitpack in order to be able 25 | to force the usage of the version in the local maven repository. 26 | 27 | ### License 28 | 29 | Copyright © 2015 [Art+Com AG](http://www.artcom.de/). 30 | Distributed under the MIT License. 31 | Please see [License File](LICENSE) for more information. 32 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | wrapper { 2 | gradleVersion = '6.5.1' 3 | } 4 | 5 | allprojects { 6 | repositories { 7 | mavenCentral() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | projectVersion=0.2.3 2 | projectArtifactId=hsm-java 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artcom/hsm-java/5aced1c85c749e691bfff1ce5e7bc56f9fd1fa96/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Jul 09 20:07:52 CEST 2020 2 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-all.zip 3 | distributionBase=GRADLE_USER_HOME 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | 88 | @rem Execute Gradle 89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 90 | 91 | :end 92 | @rem End local scope for the variables with windows NT shell 93 | if "%ERRORLEVEL%"=="0" goto mainEnd 94 | 95 | :fail 96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 97 | rem the _cmd.exe /c_ return code! 98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 99 | exit /b 1 100 | 101 | :mainEnd 102 | if "%OS%"=="Windows_NT" endlocal 103 | 104 | :omega 105 | -------------------------------------------------------------------------------- /hsm/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'pmd' 3 | apply plugin: 'maven-publish' 4 | 5 | pmd { 6 | toolVersion = '6.0.0' 7 | ignoreFailures = true 8 | ruleSetFiles = files("${rootDir}/pmd-rules.xml") 9 | } 10 | 11 | sourceCompatibility = 1.7 12 | targetCompatibility = 1.7 13 | 14 | if (project.version == 'unspecified' && project.hasProperty('projectVersion')) { 15 | project.version = project.projectVersion 16 | } 17 | 18 | def publishArtifactId = project.name 19 | if (project.hasProperty("projectArtifactId")) { 20 | publishArtifactId = projectArtifactId 21 | } 22 | 23 | publishing { 24 | publications { 25 | maven(MavenPublication) { 26 | artifactId publishArtifactId 27 | from components.java 28 | } 29 | } 30 | } 31 | 32 | dependencies { 33 | compile 'org.slf4j:slf4j-api:1.7.3' 34 | /* compile 'com.google.guava:guava:17.0' */ 35 | compile 'com.google.guava:guava-jdk5:13.0' 36 | } 37 | 38 | task reportDate doLast { 39 | println "execution at: " + new Date() 40 | println "group: " + project.group 41 | println "name: " + project.name 42 | println "version: " + project.version 43 | } 44 | 45 | allprojects { 46 | afterEvaluate { project -> 47 | project.tasks.publishToMavenLocal.dependsOn project.tasks.reportDate 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /hsm/src/main/java/de/artcom/hsm/Action.java: -------------------------------------------------------------------------------- 1 | package de.artcom.hsm; 2 | 3 | import java.util.Map; 4 | 5 | public abstract class Action { 6 | 7 | protected State mPreviousState; 8 | protected State mNextState; 9 | protected Map mPayload; 10 | 11 | public abstract void run(); 12 | 13 | void setPreviousState(State state) { 14 | mPreviousState = state; 15 | } 16 | 17 | void setNextState(State state) { 18 | mNextState = state; 19 | } 20 | 21 | void setPayload(Map payload) { 22 | mPayload = payload; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /hsm/src/main/java/de/artcom/hsm/Event.java: -------------------------------------------------------------------------------- 1 | package de.artcom.hsm; 2 | 3 | import java.util.Map; 4 | 5 | class Event { 6 | 7 | private final Map mPayload; 8 | 9 | private final String mName; 10 | 11 | public Event(String name, Map payload) { 12 | mName = name; 13 | mPayload = payload; 14 | } 15 | 16 | public Map getPayload() { 17 | return mPayload; 18 | } 19 | 20 | public String getName() { 21 | return mName; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /hsm/src/main/java/de/artcom/hsm/EventHandler.java: -------------------------------------------------------------------------------- 1 | package de.artcom.hsm; 2 | 3 | import java.util.Map; 4 | 5 | public interface EventHandler { 6 | 7 | public void handleEvent(String event); 8 | public void handleEvent(String event, Map payload); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /hsm/src/main/java/de/artcom/hsm/Guard.java: -------------------------------------------------------------------------------- 1 | package de.artcom.hsm; 2 | 3 | import java.util.Map; 4 | 5 | public interface Guard { 6 | 7 | public boolean evaluate(Map payload); 8 | } 9 | -------------------------------------------------------------------------------- /hsm/src/main/java/de/artcom/hsm/Handler.java: -------------------------------------------------------------------------------- 1 | package de.artcom.hsm; 2 | 3 | class Handler { 4 | 5 | private final State mTargetState; 6 | private final TransitionKind mKind; 7 | private Guard mGuard; 8 | private Action mAction; 9 | 10 | public Handler(State targetState, TransitionKind kind, Action action, Guard guard) { 11 | mTargetState = targetState; 12 | mKind = kind; 13 | mAction = action; 14 | mGuard = guard; 15 | } 16 | 17 | public Handler(State targetState, TransitionKind kind, Guard guard) { 18 | mTargetState = targetState; 19 | mKind = kind; 20 | mGuard = guard; 21 | } 22 | 23 | public Handler(State targetState, TransitionKind kind, Action action) { 24 | mTargetState = targetState; 25 | mKind = kind; 26 | mAction = action; 27 | } 28 | 29 | public Handler(State targetState, TransitionKind kind) { 30 | mTargetState = targetState; 31 | mKind = kind; 32 | } 33 | 34 | public boolean evaluate(Event event) { 35 | if (mGuard != null) { 36 | return mGuard.evaluate(event.getPayload()); 37 | } 38 | return true; 39 | } 40 | 41 | public State getTargetState() { 42 | return mTargetState; 43 | } 44 | 45 | public Action getAction() { 46 | return mAction; 47 | } 48 | 49 | public TransitionKind getKind() { 50 | return mKind; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /hsm/src/main/java/de/artcom/hsm/Parallel.java: -------------------------------------------------------------------------------- 1 | package de.artcom.hsm; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.Collection; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | public class Parallel extends State { 10 | 11 | private List mStateMachineList; 12 | 13 | public Parallel(String id, StateMachine... stateMachines) { 14 | super(id); 15 | setStateMachineList(Arrays.asList(stateMachines)); 16 | } 17 | 18 | public void setStateMachineList(List stateMachineList) { 19 | mStateMachineList = stateMachineList; 20 | for (StateMachine stateMachine : mStateMachineList) { 21 | stateMachine.setContainer(this); 22 | } 23 | } 24 | 25 | @Override 26 | protected Parallel getThis() { 27 | return this; 28 | } 29 | 30 | @Override 31 | void enter(State prev, State next, Map payload) { 32 | super.enter(prev, next, payload); 33 | for (StateMachine stateMachine : mStateMachineList) { 34 | stateMachine.enterState(prev, next, payload); 35 | } 36 | } 37 | 38 | @Override 39 | void exit(State prev, State next, Map payload) { 40 | super.exit(prev, next, payload); 41 | for (StateMachine stateMachine : mStateMachineList) { 42 | stateMachine.teardown(payload); 43 | } 44 | } 45 | 46 | @Override 47 | boolean handleWithOverride(Event event) { 48 | boolean isHandled = false; 49 | for (StateMachine stateMachine : mStateMachineList) { 50 | if (stateMachine.handleWithOverride(event)) { 51 | isHandled = true; 52 | } 53 | } 54 | if (!isHandled) { 55 | return super.handleWithOverride(event); 56 | } 57 | return true; 58 | } 59 | 60 | @Override 61 | public String toString() { 62 | StringBuilder sb = new StringBuilder(); 63 | sb.append(getId()); 64 | sb.append("/("); 65 | for (StateMachine stateMachine : mStateMachineList) { 66 | sb.append(stateMachine.toString()); 67 | sb.append('|'); 68 | } 69 | sb.deleteCharAt(sb.length() - 1); 70 | sb.append(')'); 71 | return sb.toString(); 72 | } 73 | 74 | @Override 75 | void addParent(StateMachine stateMachine) { 76 | for (StateMachine machine : mStateMachineList) { 77 | machine.addParent(stateMachine); 78 | } 79 | } 80 | 81 | @Override 82 | Collection getDescendantStates() { 83 | List descendantStates = new ArrayList(); 84 | for (StateMachine stateMachine : mStateMachineList) { 85 | descendantStates.addAll(stateMachine.getDescendantStates()); 86 | } 87 | return descendantStates; 88 | } 89 | 90 | public List getAllActiveStates() { 91 | List stateList = new ArrayList(); 92 | for(StateMachine stateMachine : mStateMachineList) { 93 | stateList.addAll(stateMachine.getAllActiveStates()); 94 | } 95 | return stateList; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /hsm/src/main/java/de/artcom/hsm/State.java: -------------------------------------------------------------------------------- 1 | package de.artcom.hsm; 2 | 3 | import com.google.common.collect.LinkedListMultimap; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collection; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | public class State> { 15 | 16 | final static Logger LOGGER = LoggerFactory.getLogger(State.class); 17 | 18 | private final String mId; 19 | private Action mOnEnterAction; 20 | private Action mOnExitAction; 21 | private final LinkedListMultimap mHandlers; 22 | protected StateMachine mOwner; 23 | 24 | protected T getThis() { 25 | return (T) this; 26 | } 27 | 28 | public State(String id) { 29 | mHandlers = LinkedListMultimap.create(); 30 | mId = id; 31 | } 32 | 33 | public T onEnter(Action onEnterAction) { 34 | mOnEnterAction = onEnterAction; 35 | return getThis(); 36 | } 37 | 38 | public T onExit(Action onExitAction) { 39 | mOnExitAction = onExitAction; 40 | return getThis(); 41 | } 42 | 43 | public T addHandler(String eventName, State target, TransitionKind kind, Guard guard) { 44 | mHandlers.put(eventName, new Handler(target, kind, guard)); 45 | return getThis(); 46 | } 47 | 48 | public T addHandler(String eventName, State target, TransitionKind kind, Action action) { 49 | mHandlers.put(eventName, new Handler(target, kind, action)); 50 | return getThis(); 51 | } 52 | 53 | public T addHandler(String eventName, State target, TransitionKind kind, Action action, Guard guard) { 54 | mHandlers.put(eventName, new Handler(target, kind, action, guard)); 55 | return getThis(); 56 | } 57 | 58 | public T addHandler(String eventName, State target, TransitionKind kind) { 59 | mHandlers.put(eventName, new Handler(target, kind)); 60 | return getThis(); 61 | } 62 | 63 | void setOwner(StateMachine ownerMachine) { 64 | mOwner = ownerMachine; 65 | } 66 | 67 | StateMachine getOwner() { 68 | return mOwner; 69 | } 70 | 71 | public String getId() { 72 | return mId; 73 | } 74 | 75 | void enter(State prev, State next, Map payload) { 76 | LOGGER.debug("[" + mOwner.getName() + "] " + getId() + " - enter"); 77 | if (mOnEnterAction != null) { 78 | mOnEnterAction.setPreviousState(prev); 79 | mOnEnterAction.setNextState(next); 80 | mOnEnterAction.setPayload(payload); 81 | mOnEnterAction.run(); 82 | } 83 | } 84 | 85 | void exit(State prev, State next, Map payload) { 86 | LOGGER.debug("[" + mOwner.getName() + "] " + getId() + " - exit"); 87 | if (mOnExitAction != null) { 88 | mOnExitAction.setPreviousState(prev); 89 | mOnExitAction.setNextState(next); 90 | mOnExitAction.setPayload(payload); 91 | mOnExitAction.run(); 92 | } 93 | } 94 | 95 | Handler findHandler(Event event) { 96 | for (Handler handler : mHandlers.get(event.getName())) { 97 | if (handler.evaluate(event)) { 98 | return handler; 99 | } 100 | } 101 | return null; 102 | } 103 | 104 | boolean handleWithOverride(Event event) { 105 | Handler handler = findHandler(event); 106 | if (handler != null) { 107 | LOGGER.debug("[" + mOwner.getName() + "] " + mId + " - handle Event: " + event.getName()); 108 | mOwner.executeHandler(handler, event); 109 | return true; 110 | } 111 | return false; 112 | } 113 | 114 | @Override 115 | public String toString() { 116 | return mId; 117 | } 118 | 119 | void addParent(StateMachine stateMachine) { 120 | // do nothing 121 | } 122 | 123 | Collection getDescendantStates() { 124 | return new ArrayList(); 125 | } 126 | 127 | public EventHandler getEventHandler() { 128 | return mOwner.getPath().get(0); 129 | } 130 | 131 | public List getAllActiveStates() { 132 | return new ArrayList(); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /hsm/src/main/java/de/artcom/hsm/StateMachine.java: -------------------------------------------------------------------------------- 1 | package de.artcom.hsm; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.HashMap; 9 | import java.util.HashSet; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.Queue; 13 | import java.util.Set; 14 | import java.util.concurrent.ConcurrentLinkedQueue; 15 | 16 | public class StateMachine implements EventHandler { 17 | 18 | final static Logger LOGGER = LoggerFactory.getLogger(StateMachine.class); 19 | 20 | private final List mStateList = new ArrayList(); 21 | private final List mDescendantStateList = new ArrayList(); 22 | private String mName; 23 | private State mInitialState; 24 | private State mCurrentState; 25 | private final Queue mEventQueue = new ConcurrentLinkedQueue(); 26 | private boolean mEventQueueInProgress = false; 27 | private final List mPath = new ArrayList(); 28 | private State mContainer; 29 | 30 | public StateMachine(String name, State initialState, State... states) { 31 | this(initialState, states); 32 | mName = name; 33 | } 34 | 35 | public StateMachine(State initialState, State... states) { 36 | mStateList.addAll(Arrays.asList(states)); 37 | mStateList.add(initialState); 38 | mInitialState = initialState; 39 | setOwner(); 40 | generatePath(); 41 | generateDescendantStateList(); 42 | mName = ""; 43 | } 44 | 45 | void setContainer(State container) { 46 | mContainer = container; 47 | } 48 | 49 | State getContainer() { 50 | return mContainer; 51 | } 52 | 53 | private void generateDescendantStateList() { 54 | mDescendantStateList.addAll(mStateList); 55 | for (State state : mStateList) { 56 | mDescendantStateList.addAll(state.getDescendantStates()); 57 | } 58 | } 59 | 60 | private void generatePath() { 61 | mPath.add(0, this); 62 | for (State state : mStateList) { 63 | state.addParent(this); 64 | } 65 | } 66 | 67 | public void init() { 68 | init(new HashMap()); 69 | } 70 | 71 | public void init(Map payload) { 72 | LOGGER.debug(mName + " init"); 73 | if (mInitialState == null) { 74 | throw new IllegalStateException(mName + " Can't init without states defined."); 75 | } else { 76 | mEventQueueInProgress = true; 77 | if(payload == null) { 78 | payload = new HashMap(); 79 | } 80 | enterState(null, mInitialState, payload); 81 | mEventQueueInProgress = false; 82 | processEventQueue(); 83 | } 84 | } 85 | 86 | void teardown(Map payload) { 87 | LOGGER.debug(mName + " teardown"); 88 | if(payload == null) { 89 | payload = new HashMap(); 90 | } 91 | exitState(mCurrentState, null, payload); 92 | mCurrentState = null; 93 | } 94 | 95 | public void teardown() { 96 | teardown(new HashMap()); 97 | } 98 | 99 | @Override 100 | public void handleEvent(String event) { 101 | handleEvent(event, new HashMap()); 102 | } 103 | 104 | @Override 105 | public void handleEvent(String eventName, Map payload) { 106 | if(mCurrentState == null) { 107 | return; // TODO: throw an exception here 108 | } 109 | // TODO: make a deep copy of the payload (also do this in Parallel) 110 | mEventQueue.add(new Event(eventName, payload)); 111 | processEventQueue(); 112 | } 113 | 114 | private void processEventQueue() { 115 | if (mEventQueueInProgress) { 116 | return; 117 | } 118 | mEventQueueInProgress = true; 119 | while (mEventQueue.peek() != null) { 120 | Event event = mEventQueue.poll(); 121 | if (!mCurrentState.handleWithOverride(event)) { 122 | LOGGER.debug(mName + " nobody handled event: " + event.getName()); 123 | } 124 | } 125 | mEventQueueInProgress = false; 126 | } 127 | 128 | boolean handleWithOverride(Event event) { 129 | if (mCurrentState != null ) { 130 | return mCurrentState.handleWithOverride(event); 131 | } else { 132 | return false; 133 | } 134 | } 135 | 136 | void executeHandler(Handler handler, Event event) { 137 | LOGGER.debug(mName + " execute handler for event: " + event.getName()); 138 | 139 | Action action = handler.getAction(); 140 | State targetState = handler.getTargetState(); 141 | if (targetState == null) { 142 | throw new IllegalStateException(mName + " cant find target state for transition " + event.getName()); 143 | } 144 | switch (handler.getKind()) { 145 | case External: 146 | doExternalTransition(mCurrentState, targetState, action, event); 147 | break; 148 | case Local: 149 | doLocalTransition(mCurrentState, targetState, action, event); 150 | break; 151 | case Internal: 152 | executeAction(action, mCurrentState, targetState, event.getPayload()); 153 | break; 154 | } 155 | } 156 | 157 | private void executeAction(Action action, State previousState, State targetState, Map payload) { 158 | if (action != null) { 159 | action.setPreviousState(previousState); 160 | action.setNextState(targetState); 161 | action.setPayload(payload); 162 | action.run(); 163 | } 164 | } 165 | 166 | private void doExternalTransition(State previousState, State targetState, Action action, Event event) { 167 | StateMachine lca = findLowestCommonAncestor(targetState); 168 | lca.switchState(previousState, targetState, action, event.getPayload()); 169 | } 170 | 171 | private void doLocalTransition(State previousState, State targetState, Action action, Event event) { 172 | if(previousState.getDescendantStates().contains(targetState)) { 173 | StateMachine stateMachine = findNextStateMachineOnPathTo(targetState); 174 | stateMachine.switchState(previousState, targetState, action, event.getPayload()); 175 | } else if(targetState.getDescendantStates().contains(previousState)) { 176 | int targetLevel = targetState.getOwner().getPath().size(); 177 | StateMachine stateMachine = mPath.get(targetLevel); 178 | stateMachine.switchState(previousState, targetState, action, event.getPayload()); 179 | } else if(previousState.equals(targetState)) { 180 | //TODO: clarify desired behavior for local transition on self 181 | // currently behaves like an internal transition 182 | } else { 183 | doExternalTransition(previousState, targetState, action, event); 184 | } 185 | } 186 | 187 | private void switchState(State previousState, State nextState, Action action, Map payload) { 188 | exitState(previousState, nextState, payload); 189 | executeAction(action, previousState, nextState, payload); 190 | enterState(previousState, nextState, payload); 191 | } 192 | 193 | void enterState(State previousState, State targetState, Map payload) { 194 | int targetLevel = targetState.getOwner().getPath().size(); 195 | int localLevel = mPath.size(); 196 | State nextState; 197 | if (targetLevel < localLevel) { 198 | nextState = mInitialState; 199 | } else if (targetLevel == localLevel) { 200 | nextState = targetState; 201 | } else { // if targetLevel > localLevel 202 | nextState = findNextStateOnPathTo(targetState); 203 | } 204 | if (mStateList.contains(nextState)) { 205 | mCurrentState = nextState; 206 | } else { 207 | mCurrentState = mInitialState; 208 | } 209 | mCurrentState.enter(previousState, targetState, payload); 210 | } 211 | 212 | private State findNextStateOnPathTo(State targetState) { 213 | return findNextStateMachineOnPathTo(targetState).getContainer(); 214 | } 215 | 216 | private StateMachine findNextStateMachineOnPathTo(State targetState) { 217 | int localLevel = mPath.size(); 218 | StateMachine targetOwner = targetState.getOwner(); 219 | return targetOwner.getPath().get(localLevel); 220 | } 221 | 222 | private void exitState(State previousState, State nextState, Map payload) { 223 | mCurrentState.exit(previousState, nextState, payload); 224 | } 225 | 226 | private void setOwner() { 227 | for (State state : mStateList) { 228 | state.setOwner(this); 229 | } 230 | } 231 | 232 | @Override 233 | public String toString() { 234 | if (mCurrentState == null) { 235 | return mInitialState.toString(); 236 | } 237 | return mCurrentState.toString(); 238 | } 239 | 240 | List getPath() { 241 | return mPath; 242 | } 243 | 244 | public String getPathString() { 245 | StringBuilder sb = new StringBuilder(); 246 | int count = 0; 247 | sb.append("\r\n"); 248 | for (StateMachine stateMachine : mPath) { 249 | sb.append(Integer.toString(++count)); 250 | sb.append(' '); 251 | sb.append(stateMachine.toString()); 252 | sb.append("\r\n"); 253 | } 254 | return sb.toString(); 255 | } 256 | 257 | void addParent(StateMachine stateMachine) { 258 | mPath.add(0, stateMachine); 259 | for (State state : mStateList) { 260 | state.addParent(stateMachine); 261 | } 262 | } 263 | 264 | private StateMachine findLowestCommonAncestor(State targetState) { 265 | if (targetState.getOwner() == null) { 266 | throw new IllegalStateException(mName + " Target state '" + targetState.getId() + "' is not contained in state machine model."); 267 | } 268 | List targetPath = targetState.getOwner().getPath(); 269 | int size = mPath.size(); 270 | for (int i = 1; i < size; i++) { 271 | try { 272 | StateMachine targetAncestor = targetPath.get(i); 273 | StateMachine localAncestor = mPath.get(i); 274 | if (!targetAncestor.equals(localAncestor)) { 275 | return mPath.get(i - 1); 276 | } 277 | } catch (IndexOutOfBoundsException e) { 278 | return mPath.get(i - 1); 279 | } 280 | } 281 | return this; 282 | } 283 | 284 | List getDescendantStates() { 285 | return mDescendantStateList; 286 | } 287 | 288 | public List getAllActiveStates() { 289 | ArrayList stateList = new ArrayList(); 290 | stateList.add(mCurrentState); 291 | stateList.addAll(mCurrentState.getAllActiveStates()); 292 | return stateList; 293 | } 294 | 295 | 296 | String getName() { 297 | return mName; 298 | } 299 | 300 | void setName(String name) { 301 | this.mName = name; 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /hsm/src/main/java/de/artcom/hsm/Sub.java: -------------------------------------------------------------------------------- 1 | package de.artcom.hsm; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | public class Sub extends State { 9 | 10 | private final StateMachine mSubMachine; 11 | 12 | @Override 13 | protected Sub getThis() { 14 | return this; 15 | } 16 | 17 | public Sub(String id, StateMachine subMachine) { 18 | super(id); 19 | mSubMachine = subMachine; 20 | mSubMachine.setContainer(this); 21 | } 22 | 23 | public Sub(String id, State initialState, State... states) { 24 | super(id); 25 | mSubMachine = new StateMachine(initialState, states); 26 | mSubMachine.setContainer(this); 27 | } 28 | 29 | @Override 30 | void enter(State prev, State next, Map payload) { 31 | super.enter(prev, next, payload); 32 | mSubMachine.enterState(prev, next, payload); 33 | } 34 | 35 | @Override 36 | void exit(State prev, State next, Map payload) { 37 | mSubMachine.teardown(payload); 38 | super.exit(prev, next, payload); 39 | } 40 | 41 | @Override 42 | boolean handleWithOverride(Event event) { 43 | if (mSubMachine.handleWithOverride(event)) { 44 | return true; 45 | } else { 46 | return super.handleWithOverride(event); 47 | } 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | return getId() + "/(" + mSubMachine.toString() + ")"; 53 | } 54 | 55 | @Override 56 | void addParent(StateMachine stateMachine) { 57 | mSubMachine.addParent(stateMachine); 58 | } 59 | 60 | @Override 61 | Collection getDescendantStates() { 62 | return mSubMachine.getDescendantStates(); 63 | } 64 | 65 | public List getAllActiveStates() { 66 | return mSubMachine.getAllActiveStates(); 67 | } 68 | 69 | @Override 70 | void setOwner(StateMachine ownerMachine) { 71 | super.setOwner(ownerMachine); 72 | mSubMachine.setName(mOwner.getName()); 73 | } 74 | } -------------------------------------------------------------------------------- /hsm/src/main/java/de/artcom/hsm/TransitionKind.java: -------------------------------------------------------------------------------- 1 | package de.artcom.hsm; 2 | 3 | public enum TransitionKind { 4 | External, 5 | Local, 6 | Internal 7 | } 8 | -------------------------------------------------------------------------------- /pmd-rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'hsm' 2 | include 'tests' 3 | -------------------------------------------------------------------------------- /tests/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'groovy' 3 | apply plugin: 'jacoco' 4 | apply plugin: 'pmd' 5 | 6 | task jacocoReport(type: JacocoReport) { 7 | getClassDirectories().setFrom(files(project(':hsm').sourceSets.main.output)) 8 | getSourceDirectories().setFrom(files(project(':hsm').sourceSets.main.java.getSrcDirs())) 9 | getExecutionData().setFrom(test) 10 | } 11 | 12 | pmd { 13 | toolVersion = '6.0.0' 14 | ignoreFailures = true 15 | ruleSetFiles = files("${rootDir}/pmd-rules.xml") 16 | } 17 | 18 | dependencies { 19 | compile project(':hsm') 20 | testCompile 'ch.qos.logback:logback-classic:1.1.2' 21 | testCompile 'ch.qos.logback:logback-core:1.1.2' 22 | testCompile 'org.hamcrest:hamcrest-integration:1.3' 23 | testCompile 'org.hamcrest:hamcrest-core:1.3' 24 | testCompile 'org.hamcrest:hamcrest-library:1.3' 25 | 26 | testCompile 'org.codehaus.groovy:groovy-all:2.4.0' 27 | testCompile 'org.spockframework:spock-core:0.7-groovy-2.0' 28 | 29 | testCompile 'junit:junit:4.12' 30 | 31 | testCompile 'org.mockito:mockito-core:1.10.8' 32 | } 33 | -------------------------------------------------------------------------------- /tests/src/test/java/de/artcom/hsm/test/BasicStateMachineTest.java: -------------------------------------------------------------------------------- 1 | package de.artcom.hsm.test; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | import org.mockito.InOrder; 6 | 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | import de.artcom.hsm.Action; 12 | import de.artcom.hsm.Parallel; 13 | import de.artcom.hsm.State; 14 | import de.artcom.hsm.StateMachine; 15 | import de.artcom.hsm.Sub; 16 | import de.artcom.hsm.TransitionKind; 17 | 18 | import static org.hamcrest.MatcherAssert.assertThat; 19 | import static org.hamcrest.Matchers.hasItems; 20 | import static org.hamcrest.Matchers.not; 21 | import static org.hamcrest.Matchers.notNullValue; 22 | import static org.hamcrest.Matchers.equalTo; 23 | import static org.mockito.Mockito.inOrder; 24 | import static org.mockito.Mockito.mock; 25 | import static org.mockito.Mockito.verify; 26 | import static org.mockito.Mockito.verifyZeroInteractions; 27 | 28 | public class BasicStateMachineTest { 29 | 30 | @Test 31 | public void cannotCreateEmptyStateMachine() { 32 | 33 | try { 34 | StateMachine sm = new StateMachine(null); 35 | Assert.fail("NullPointerException should raise when creating a empty StateMachine"); 36 | } catch (NullPointerException npe) { 37 | } 38 | } 39 | 40 | @Test 41 | public void canChainMethods() { 42 | State state = new State("foo") 43 | .onEnter(mock(Action.class)) 44 | .onExit(mock(Action.class)); 45 | } 46 | 47 | @Test 48 | public void initialStateIsEntered() { 49 | //given: 50 | Action enterAction = mock(Action.class); 51 | State on = new State("on") 52 | .onEnter(enterAction); 53 | StateMachine sm = new StateMachine(on); 54 | //when: 55 | sm.init(); 56 | //then: 57 | verify(enterAction).run(); 58 | } 59 | 60 | @Test 61 | public void currentStateIsExited() { 62 | //given: 63 | Action exitAction = mock(Action.class); 64 | State on = new State("on") 65 | .onExit(exitAction); 66 | StateMachine sm = new StateMachine(on); 67 | sm.init(); 68 | //when: 69 | sm.teardown(); 70 | //then: 71 | verify(exitAction).run(); 72 | } 73 | 74 | @Test 75 | public void eventsWithoutPayloadCauseStateTransition() { 76 | //given: 77 | Action onExitAction = mock(Action.class); 78 | Action offEnterAction = mock(Action.class); 79 | State off = new State("off") 80 | .onEnter(offEnterAction); 81 | State on = new State("on") 82 | .addHandler("toggle", off, TransitionKind.External) 83 | .onExit(onExitAction); 84 | StateMachine sm = new StateMachine(on, off); 85 | sm.init(); 86 | 87 | //when: 88 | sm.handleEvent("toggle"); 89 | 90 | //then: 91 | verify(onExitAction).run(); 92 | verify(offEnterAction).run(); 93 | } 94 | 95 | @Test 96 | public void impossibleTransitionTest() { 97 | // given: 98 | Action onExitAction = mock(Action.class); 99 | State off = new State("off"); 100 | State on = new State("on") 101 | .addHandler("toggle", off, TransitionKind.External) 102 | .onExit(onExitAction); 103 | StateMachine sm = new StateMachine(on); 104 | sm.init(); 105 | 106 | // when: 107 | try { 108 | sm.handleEvent("toggle"); 109 | Assert.fail("expected NullpointerException but nothing happnend"); 110 | } catch (IllegalStateException npe) { 111 | } 112 | } 113 | 114 | @Test 115 | public void eventsWithPayloadCauseStateTransition() { 116 | //given: 117 | 118 | Action offEnterAction = mock(Action.class); 119 | State off = new State("off") 120 | .onEnter(offEnterAction); 121 | 122 | Action onExitAction = mock(Action.class); 123 | State on = new State("on") 124 | .addHandler("toggle", off, TransitionKind.External) 125 | .onExit(onExitAction); 126 | 127 | 128 | StateMachine sm = new StateMachine(on, off); 129 | sm.init(); 130 | 131 | //when: 132 | sm.handleEvent("toggle", new HashMap()); 133 | 134 | //then: 135 | verify(onExitAction).run(); 136 | verify(offEnterAction).run(); 137 | } 138 | 139 | @Test 140 | public void actionsAreCalledBetweenExitAndEnter() { 141 | Action aExit = mock(Action.class); 142 | Action aaExit = mock(Action.class); 143 | Action bEnter = mock(Action.class); 144 | Action bbEnter = mock(Action.class); 145 | Action action = mock(Action.class); 146 | 147 | State aa = new State("aa").onExit(aaExit); 148 | Sub a = new Sub("a", aa).onExit(aExit); 149 | State bb = new State("bb").onEnter(bbEnter); 150 | Sub b = new Sub("b", bb).onEnter(bEnter); 151 | 152 | aa.addHandler("T", bb, TransitionKind.External, action); 153 | 154 | StateMachine sm = new StateMachine(a, b); 155 | sm.init(); 156 | 157 | sm.handleEvent("T"); 158 | 159 | InOrder inOrder = inOrder(aaExit, aExit, action, bEnter, bbEnter); 160 | inOrder.verify(aaExit).run(); 161 | inOrder.verify(aExit).run(); 162 | inOrder.verify(action).run(); 163 | inOrder.verify(bEnter).run(); 164 | inOrder.verify(bbEnter).run(); 165 | } 166 | 167 | @Test 168 | public void actionsAreCalledOnTransitionsWithPayload() { 169 | //given: 170 | final boolean[] actionGotCalled = {false}; 171 | Action toggleAction = new Action() { 172 | @Override 173 | public void run() { 174 | actionGotCalled[0] = true; 175 | assertThat(mPayload, notNullValue()); 176 | Assert.assertTrue(mPayload.containsKey("foo")); 177 | Assert.assertTrue(mPayload.get("foo") instanceof String); 178 | Assert.assertTrue(((String) mPayload.get("foo")).equals("bar")); 179 | } 180 | }; 181 | State off = new State("off"); 182 | State on = new State("on") 183 | .addHandler("toggle", off, TransitionKind.External, toggleAction); 184 | 185 | StateMachine sm = new StateMachine(on, off); 186 | sm.init(); 187 | Map payload = new HashMap(); 188 | payload.put("foo", "bar"); 189 | 190 | //when: 191 | sm.handleEvent("toggle", payload); 192 | 193 | //then: 194 | if (!actionGotCalled[0]) { 195 | Assert.fail("action was not called"); 196 | } 197 | } 198 | 199 | @Test 200 | public void actionsAreCalledAlwaysWithValidPayload() { 201 | //given: 202 | final boolean[] actionGotCalled = {false}; 203 | Action toggleAction = new Action() { 204 | @Override 205 | public void run() { 206 | actionGotCalled[0] = true; 207 | assertThat(mPayload, notNullValue()); 208 | Assert.assertTrue(mPayload.isEmpty()); 209 | } 210 | }; 211 | State off = new State("off"); 212 | State on = new State("on") 213 | .addHandler("toggle", off, TransitionKind.External, toggleAction); 214 | 215 | StateMachine sm = new StateMachine(on, off); 216 | sm.init(); 217 | 218 | //when: 219 | sm.handleEvent("toggle"); 220 | 221 | //then: 222 | if (!actionGotCalled[0]) { 223 | Assert.fail("action was not called"); 224 | } 225 | } 226 | 227 | @Test 228 | public void actionsCanBeInternal() { 229 | //given: 230 | Action onExitAction = mock(Action.class); 231 | Action toggleAction = mock(Action.class); 232 | State on = new State("on"); 233 | on.addHandler("toggle", on, TransitionKind.Internal, toggleAction) 234 | .onExit(onExitAction); 235 | StateMachine sm = new StateMachine(on); 236 | sm.init(); 237 | 238 | //when: 239 | sm.handleEvent("toggle"); 240 | 241 | //then: 242 | verify(toggleAction).run(); 243 | verifyZeroInteractions(onExitAction); 244 | } 245 | 246 | @Test 247 | public void noMatchingStateAvailable() { 248 | // given: 249 | State off = new State("off"); 250 | State on = new State("on").addHandler("toggle", off, TransitionKind.External); 251 | StateMachine sm = new StateMachine(on); 252 | sm.init(); 253 | 254 | // when: 255 | try { 256 | sm.handleEvent("toggle"); 257 | Assert.fail("expected IllegalStateException since target State was not part of StateMachine"); 258 | } catch (IllegalStateException e) { 259 | } 260 | } 261 | 262 | @Test 263 | public void canGetPathString() { 264 | // given: 265 | State b201 = new State("b201"); 266 | Sub b21 = new Sub("b21", b201); 267 | Sub b1 = new Sub("b1", b21); 268 | Sub b = new Sub("b", b1); 269 | Sub bar = new Sub("bar", b); 270 | 271 | State a1 = new State("a1").addHandler("T1", b201, TransitionKind.External); 272 | Sub a = new Sub("a", a1); 273 | Sub foo = new Sub("foo", a); 274 | 275 | StateMachine sm = new StateMachine(foo, bar); 276 | 277 | // when: 278 | String pathString = sm.getPathString(); 279 | 280 | // then: 281 | assertThat(pathString, notNullValue()); 282 | } 283 | 284 | @Test 285 | public void enumTest() { 286 | // just for the code coverage (^__^) 287 | TransitionKind local = TransitionKind.valueOf("Local"); 288 | assertThat(local, equalTo(TransitionKind.Local)); 289 | } 290 | 291 | @Test 292 | public void emittedEventTestHandledFromTopStateMachine() { 293 | class SampleState extends State { 294 | 295 | public SampleState(String id) { 296 | super(id); 297 | onEnter(new Action() { 298 | @Override 299 | public void run() { 300 | SampleState.this.getEventHandler().handleEvent("T1"); 301 | } 302 | }); 303 | } 304 | } 305 | 306 | // given: 307 | Action bAction = mock(Action.class); 308 | SampleState a1 = new SampleState("a1"); 309 | 310 | Sub a = new Sub("a", a1); 311 | Sub b = new Sub("b", a); 312 | b.addHandler("T1", b, TransitionKind.Internal, bAction); 313 | Sub c = new Sub("c", b); 314 | StateMachine stateMachine = new StateMachine(c); 315 | 316 | // when: 317 | stateMachine.init(); 318 | 319 | // then: 320 | verify(bAction).run(); 321 | } 322 | 323 | @Test 324 | public void getAllActiveStates() { 325 | // given: 326 | State a11 = new State("a11"); 327 | State a22 = new State("a22"); 328 | State a33 = new State("a33"); 329 | Parallel a1 = new Parallel("a1", new StateMachine(a11), new StateMachine(a22, a33)); 330 | Sub a = new Sub("a", a1); 331 | StateMachine sm = new StateMachine(a); 332 | sm.init(); 333 | 334 | // when: 335 | List allActiveStates = sm.getAllActiveStates(); 336 | 337 | // then: 338 | assertThat(allActiveStates, hasItems(a, a1, a11, a22)); 339 | assertThat(allActiveStates, not(hasItems(a33))); 340 | } 341 | 342 | } 343 | -------------------------------------------------------------------------------- /tests/src/test/java/de/artcom/hsm/test/ComplexTests.java: -------------------------------------------------------------------------------- 1 | package de.artcom.hsm.test; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import de.artcom.hsm.Parallel; 7 | import de.artcom.hsm.State; 8 | import de.artcom.hsm.StateMachine; 9 | import de.artcom.hsm.Sub; 10 | 11 | public class ComplexTests { 12 | 13 | private State a1; 14 | private State a2; 15 | private State a3; 16 | private Sub a; 17 | 18 | private State b1; 19 | private Sub b2; 20 | private State b21; 21 | private State b22; 22 | private Sub b; 23 | 24 | private State c11; 25 | private State c12; 26 | private State c21; 27 | private State c22; 28 | private StateMachine c1Machine; 29 | private StateMachine c2Machine; 30 | private Parallel c; 31 | 32 | private StateMachine stateMachine; 33 | 34 | @Before 35 | public void setUpTest() { 36 | a1 = new State("a1"); 37 | a2 = new State("a2"); 38 | a3 = new State("a3"); 39 | a = new Sub("a", a1, a2 , a3); 40 | 41 | b1 = new State("b1"); 42 | b21 = new State("b21"); 43 | b22 = new State("b22"); 44 | b2 = new Sub("b2", b21, b22); 45 | b = new Sub("b", b1, b2); 46 | 47 | c11 = new State("c11"); 48 | c12 = new State("c12"); 49 | c21 = new State("c21"); 50 | c22 = new State("c22"); 51 | c1Machine = new StateMachine(c11, c12); 52 | c2Machine = new StateMachine(c21, c22); 53 | c = new Parallel("c", c1Machine, c2Machine); 54 | 55 | stateMachine = new StateMachine(a, b, c); 56 | } 57 | 58 | @Test 59 | public void complexStateMachineTest1() { 60 | //when: 61 | stateMachine.init(); 62 | stateMachine.teardown(); 63 | //then: no exceptions 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/src/test/java/de/artcom/hsm/test/EventHandlingTest.java: -------------------------------------------------------------------------------- 1 | package de.artcom.hsm.test; 2 | 3 | import org.hamcrest.Matchers; 4 | import org.hamcrest.collection.IsMapContaining; 5 | import org.hamcrest.core.IsEqual; 6 | import org.junit.Test; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | import de.artcom.hsm.Action; 12 | import de.artcom.hsm.Guard; 13 | import de.artcom.hsm.State; 14 | import de.artcom.hsm.StateMachine; 15 | import de.artcom.hsm.Sub; 16 | import de.artcom.hsm.TransitionKind; 17 | 18 | import static org.hamcrest.Matchers.equalTo; 19 | import static org.junit.Assert.assertThat; 20 | import static org.junit.Assert.fail; 21 | import static org.mockito.Mockito.mock; 22 | import static org.mockito.Mockito.verify; 23 | import static org.mockito.Mockito.verifyZeroInteractions; 24 | 25 | public class EventHandlingTest { 26 | 27 | @Test 28 | public void runToCompletionTest() { 29 | // given: 30 | State rawEgg = new State("rawEgg"); 31 | State softEgg = new State("softEgg"); 32 | State hardEgg = new State("hardEgg"); 33 | final StateMachine sm = new StateMachine(rawEgg, softEgg, hardEgg); 34 | 35 | Action onEnterHardEgg = mock(Action.class); 36 | Action onEnterSoftEgg = mock(Action.class); 37 | 38 | softEgg.onEnter(onEnterSoftEgg); 39 | hardEgg.onEnter(onEnterHardEgg); 40 | 41 | Action boilAction = new Action() { 42 | @Override 43 | public void run() { 44 | sm.handleEvent("boil_too_long"); 45 | } 46 | }; 47 | rawEgg.addHandler("boil", softEgg, TransitionKind.External, boilAction); 48 | 49 | Action boilTooLongAction = mock(Action.class); 50 | rawEgg.addHandler("boil_too_long", hardEgg, TransitionKind.External, boilTooLongAction); 51 | 52 | Action boilTooLongAction2 = mock(Action.class); 53 | softEgg.addHandler("boil_too_long", softEgg, TransitionKind.Internal, boilTooLongAction2); 54 | 55 | sm.init(); 56 | 57 | // when: 58 | sm.handleEvent("boil"); 59 | 60 | // then: 61 | verifyZeroInteractions(onEnterHardEgg); 62 | verifyZeroInteractions(boilTooLongAction); 63 | verify(onEnterSoftEgg).run(); 64 | verify(boilTooLongAction2).run(); 65 | } 66 | 67 | @Test 68 | public void dontBubbleUpTest() { 69 | // given: 70 | Action enterA2 = mock(Action.class); 71 | Action enterB = mock(Action.class); 72 | 73 | State a1 = new State("a1"); 74 | State a2 = new State("a2").onEnter(enterA2); 75 | Sub a = new Sub("a", a1, a2); 76 | State b = new State("b").onEnter(enterB); 77 | 78 | a1.addHandler("T1", a2, TransitionKind.External, new Guard() { 79 | @Override 80 | public boolean evaluate(Map payload) { 81 | return payload.containsKey("foo"); 82 | } 83 | }); 84 | a.addHandler("T1", b, TransitionKind.External); 85 | 86 | StateMachine sm = new StateMachine(a, b); 87 | sm.init(); 88 | 89 | // when: 90 | Map payload = new HashMap(); 91 | payload.put("foo", "bar"); 92 | sm.handleEvent("T1", payload); 93 | 94 | // then: 95 | verify(enterA2).run(); 96 | verifyZeroInteractions(enterB); 97 | } 98 | 99 | @Test 100 | public void bubbleUpTest() { 101 | // given: 102 | Action enterA2 = mock(Action.class); 103 | Action enterB = mock(Action.class); 104 | 105 | State a1 = new State("a1"); 106 | State a2 = new State("a2").onEnter(enterA2); 107 | Sub a = new Sub("a", a1, a2); 108 | State b = new State("b").onEnter(enterB); 109 | 110 | a1.addHandler("T1", a2, TransitionKind.External, new Guard() { 111 | @Override 112 | public boolean evaluate(Map payload) { 113 | return payload.containsKey("foo"); 114 | } 115 | }); 116 | a.addHandler("T1", b, TransitionKind.External); 117 | 118 | StateMachine sm = new StateMachine(a, b); 119 | sm.init(); 120 | 121 | // when: 122 | sm.handleEvent("T1"); 123 | 124 | // then: 125 | verify(enterB).run(); 126 | verifyZeroInteractions(enterA2); 127 | } 128 | 129 | @Test 130 | public void handleTransitionWithPayload() { 131 | // given 132 | State a1 = new State("a1"); 133 | State a2 = new State("a2") 134 | .onEnter(new Action() { 135 | @Override 136 | public void run() { 137 | assertThat(mPayload, Matchers.hasKey("foo")); 138 | } 139 | }); 140 | 141 | a1.addHandler("T1", a2, TransitionKind.External) 142 | .onExit(new Action() { 143 | @Override 144 | public void run() { 145 | assertThat(mPayload, Matchers.hasKey("foo")); 146 | } 147 | }); 148 | 149 | StateMachine sm = new StateMachine(a1, a2); 150 | sm.init(); 151 | 152 | Map payload = new HashMap(); 153 | payload.put("foo", "bar"); 154 | 155 | // when 156 | sm.handleEvent("T1", payload); 157 | } 158 | 159 | @Test 160 | public void initWithPayload() { 161 | // given 162 | Action a1Enter = new Action() { 163 | @Override 164 | public void run() { 165 | // then 166 | assertThat(mPayload, IsMapContaining.hasKey("foo")); 167 | } 168 | }; 169 | State a1 = new State("a1").onEnter(a1Enter); 170 | StateMachine sm = new StateMachine(a1); 171 | Map payload = new HashMap(); 172 | payload.put("foo", "bar"); 173 | 174 | // when 175 | sm.init(payload); 176 | } 177 | 178 | @Test 179 | public void initWithNullPayload() { 180 | // given 181 | Action a1Enter = mock(Action.class); 182 | State a1 = new State("a1").onEnter(a1Enter); 183 | StateMachine sm = new StateMachine(a1); 184 | 185 | // when 186 | try { 187 | sm.init(null); 188 | } catch (NullPointerException npe) { 189 | fail("StateMachine.init() should instantiate a new payload instead of throwing npe"); 190 | } 191 | 192 | // then 193 | } 194 | 195 | @Test 196 | public void teardownWithPayload() { 197 | // when 198 | State a1 = new State("a1"); 199 | State a = new Sub("a", a1); 200 | State b1 = new State("b1").onEnter(new Action() { 201 | @Override 202 | public void run() { 203 | assertThat(mPayload, IsMapContaining.hasKey("foo")); 204 | } 205 | }); 206 | 207 | a1.addHandler("T1", b1, TransitionKind.External).onExit(new Action() { 208 | @Override 209 | public void run() { 210 | mPayload.put("foo", "bar"); 211 | } 212 | }); 213 | 214 | State b = new Sub("b", a, b1); 215 | StateMachine sm = new StateMachine(b); 216 | sm.init(); 217 | sm.handleEvent("T1"); 218 | } 219 | } 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | -------------------------------------------------------------------------------- /tests/src/test/java/de/artcom/hsm/test/GuardTest.java: -------------------------------------------------------------------------------- 1 | package de.artcom.hsm.test; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | import de.artcom.hsm.Action; 9 | import de.artcom.hsm.Guard; 10 | import de.artcom.hsm.State; 11 | import de.artcom.hsm.StateMachine; 12 | import de.artcom.hsm.TransitionKind; 13 | 14 | import static org.mockito.Mockito.mock; 15 | import static org.mockito.Mockito.verify; 16 | import static org.mockito.Mockito.verifyZeroInteractions; 17 | 18 | public class GuardTest { 19 | 20 | @Test 21 | public void testFirstGuard() { 22 | // given: 23 | Action enterA2 = mock(Action.class); 24 | Action enterA3 = mock(Action.class); 25 | 26 | State a1 = new State("a1"); 27 | State a2 = new State("a2").onEnter(enterA2); 28 | State a3 = new State("a3").onEnter(enterA3); 29 | 30 | a1.addHandler("T1", a2, TransitionKind.External, new Guard() { 31 | @Override 32 | public boolean evaluate(Map payload) { 33 | Boolean foo = (Boolean) payload.get("foo"); 34 | return foo; 35 | } 36 | }).addHandler("T1", a3, TransitionKind.External, new Guard() { 37 | @Override 38 | public boolean evaluate(Map payload) { 39 | Boolean foo = (Boolean) payload.get("foo"); 40 | return !foo; 41 | } 42 | }); 43 | 44 | StateMachine sm = new StateMachine(a1, a2, a3); 45 | sm.init(); 46 | 47 | //when: 48 | Map payload = new HashMap(); 49 | payload.put("foo", true); 50 | sm.handleEvent("T1", payload); 51 | 52 | //then: 53 | verify(enterA2).run(); 54 | verifyZeroInteractions(enterA3); 55 | } 56 | 57 | @Test 58 | public void testSecondGuard() { 59 | // given: 60 | Action enterA2 = mock(Action.class); 61 | Action enterA3 = mock(Action.class); 62 | 63 | State a1 = new State("a1"); 64 | State a2 = new State("a2").onEnter(enterA2); 65 | State a3 = new State("a3").onEnter(enterA3); 66 | 67 | a1.addHandler("T1", a2, TransitionKind.External, new Guard() { 68 | @Override 69 | public boolean evaluate(Map payload) { 70 | Boolean foo = (Boolean) payload.get("foo"); 71 | return foo; 72 | } 73 | }).addHandler("T1", a3, TransitionKind.External, new Guard() { 74 | @Override 75 | public boolean evaluate(Map payload) { 76 | Boolean foo = (Boolean) payload.get("foo"); 77 | return !foo; 78 | } 79 | }); 80 | 81 | StateMachine sm = new StateMachine(a1, a2, a3); 82 | sm.init(); 83 | 84 | //when: 85 | Map payload = new HashMap(); 86 | payload.put("foo", false); 87 | sm.handleEvent("T1", payload); 88 | 89 | //then: 90 | verify(enterA3).run(); 91 | verifyZeroInteractions(enterA2); 92 | } 93 | 94 | @Test 95 | public void createHandlerWithActionAndGuard() { 96 | // given: 97 | Action a1Action = mock(Action.class); 98 | State a = new State("a"); 99 | a.addHandler("T1", a, TransitionKind.Internal, a1Action, new Guard() { 100 | @Override 101 | public boolean evaluate(Map payload) { 102 | return payload.containsKey("foo"); 103 | } 104 | }); 105 | StateMachine sm = new StateMachine(a); 106 | sm.init(); 107 | 108 | // when: 109 | Map payload = new HashMap(); 110 | payload.put("foo", "bar"); 111 | sm.handleEvent("T1", payload); 112 | 113 | // then: 114 | verify(a1Action).run(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /tests/src/test/java/de/artcom/hsm/test/LcaTest.java: -------------------------------------------------------------------------------- 1 | package de.artcom.hsm.test; 2 | 3 | import org.junit.Test; 4 | import org.mockito.InOrder; 5 | 6 | import de.artcom.hsm.Action; 7 | import de.artcom.hsm.State; 8 | import de.artcom.hsm.StateMachine; 9 | import de.artcom.hsm.Sub; 10 | import de.artcom.hsm.TransitionKind; 11 | 12 | import static org.mockito.Mockito.inOrder; 13 | import static org.mockito.Mockito.mock; 14 | import static org.mockito.Mockito.verify; 15 | import static org.mockito.Mockito.verifyZeroInteractions; 16 | 17 | public class LcaTest { 18 | 19 | @Test 20 | public void testLowestCommonAncestor1() { 21 | // given: 22 | Action exitA1 = mock(Action.class); 23 | Action exitA = mock(Action.class); 24 | Action exitFoo = mock(Action.class); 25 | State a1 = new State("a1").onExit(exitA1); 26 | Sub a = new Sub("a", a1).onExit(exitA); 27 | Sub foo = new Sub("foo", a).onExit(exitFoo); 28 | 29 | Action enterB21 = mock(Action.class); 30 | Action enterB201 = mock(Action.class); 31 | Action enterB1 = mock(Action.class); 32 | Action enterB = mock(Action.class); 33 | Action enterBar = mock(Action.class); 34 | State b201 = new State("b201").onEnter(enterB201); 35 | Sub b21 = new Sub("b21", b201).onEnter(enterB21); 36 | Sub b1 = new Sub("b1", b21).onEnter(enterB1); 37 | Sub b = new Sub("b", b1).onEnter(enterB); 38 | Sub bar = new Sub("bar", b).onEnter(enterBar); 39 | StateMachine sm = new StateMachine(foo, bar); 40 | 41 | a1.addHandler("T1", bar, TransitionKind.External); 42 | 43 | sm.init(); 44 | 45 | // when: 46 | sm.handleEvent("T1"); 47 | 48 | // then: 49 | InOrder inOrder = inOrder(exitA1, exitA, exitFoo, enterBar, enterB, enterB1, enterB21, enterB201); 50 | inOrder.verify(exitA1).run(); 51 | inOrder.verify(exitA).run(); 52 | inOrder.verify(exitFoo).run(); 53 | inOrder.verify(enterBar).run(); 54 | inOrder.verify(enterB).run(); 55 | inOrder.verify(enterB1).run(); 56 | inOrder.verify(enterB21).run(); 57 | inOrder.verify(enterB201).run(); 58 | } 59 | 60 | @Test 61 | public void testLowestCommonAncestor2() { 62 | // given: 63 | Action exitA1 = mock(Action.class); 64 | Action exitA = mock(Action.class); 65 | Action exitFoo = mock(Action.class); 66 | State a1 = new State("a1").onExit(exitA1); 67 | Sub a = new Sub("a", a1).onExit(exitA); 68 | Sub foo = new Sub("foo", a).onExit(exitFoo); 69 | 70 | Action enterB21 = mock(Action.class); 71 | Action enterB201 = mock(Action.class); 72 | Action enterB1 = mock(Action.class); 73 | Action enterB = mock(Action.class); 74 | Action enterBar = mock(Action.class); 75 | State b201 = new State("b201").onEnter(enterB201); 76 | Sub b21 = new Sub("b21", b201).onEnter(enterB21); 77 | Sub b1 = new Sub("b1", b21).onEnter(enterB1); 78 | Sub b = new Sub("b", b1).onEnter(enterB); 79 | Sub bar = new Sub("bar", b).onEnter(enterBar); 80 | StateMachine sm = new StateMachine(foo, bar); 81 | 82 | a1.addHandler("T1", b201, TransitionKind.External); 83 | 84 | sm.init(); 85 | 86 | // when: 87 | sm.handleEvent("T1"); 88 | 89 | // then: 90 | InOrder inOrder = inOrder(exitA1, exitA, exitFoo, enterBar, enterB, enterB1, enterB21, enterB201); 91 | inOrder.verify(exitA1).run(); 92 | inOrder.verify(exitA).run(); 93 | inOrder.verify(exitFoo).run(); 94 | inOrder.verify(enterBar).run(); 95 | inOrder.verify(enterB).run(); 96 | inOrder.verify(enterB1).run(); 97 | inOrder.verify(enterB21).run(); 98 | inOrder.verify(enterB201).run(); 99 | } 100 | 101 | @Test 102 | public void testLowestCommonAncestor3() { 103 | // given: 104 | Action enterA2 = mock(Action.class); 105 | Action enterB2 = mock(Action.class); 106 | Action exitM = mock(Action.class); 107 | 108 | State a1 = new State("a1"); 109 | State a2 = new State("a2") 110 | .onEnter(enterA2); 111 | 112 | Sub a = new Sub("a", a1, a2); 113 | Sub foo = new Sub("foo", a); 114 | 115 | State b1 = new State("b1"); 116 | State b2 = new State("b2") 117 | .onEnter(enterB2); 118 | 119 | Sub b = new Sub("b", b1, b2); 120 | Sub bar = new Sub("bar", b); 121 | 122 | Sub main = new Sub("main", foo, bar) 123 | .onExit(exitM); 124 | 125 | StateMachine sm = new StateMachine(main); 126 | 127 | a1.addHandler("B1", b1, TransitionKind.External) 128 | .addHandler("T1", a2, TransitionKind.External); 129 | b1.addHandler("T1", a2, TransitionKind.External); 130 | 131 | sm.init(); 132 | 133 | // when: 134 | sm.handleEvent("B1"); 135 | sm.handleEvent("T1"); 136 | 137 | // then: 138 | verify(enterA2).run(); 139 | verifyZeroInteractions(enterB2); 140 | verifyZeroInteractions(exitM); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /tests/src/test/java/de/artcom/hsm/test/LocalTransitionTest.java: -------------------------------------------------------------------------------- 1 | package de.artcom.hsm.test; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | import org.junit.Ignore; 6 | 7 | import de.artcom.hsm.Action; 8 | import de.artcom.hsm.State; 9 | import de.artcom.hsm.StateMachine; 10 | import de.artcom.hsm.Sub; 11 | import de.artcom.hsm.TransitionKind; 12 | 13 | import static org.mockito.Mockito.mock; 14 | import static org.mockito.Mockito.times; 15 | import static org.mockito.Mockito.verify; 16 | import static org.mockito.Mockito.inOrder; 17 | import org.mockito.InOrder; 18 | 19 | public class LocalTransitionTest { 20 | 21 | @Test 22 | public void canExecuteLocalTransition() { 23 | // given: 24 | Action sEnter = mock(Action.class); 25 | Action s1Enter = mock(Action.class); 26 | State s1 = new State("s1").onEnter(s1Enter); 27 | Sub s = new Sub("s", s1).onEnter(sEnter).addHandler("T1", s1, TransitionKind.Local); 28 | StateMachine sm = new StateMachine(s); 29 | sm.init(); 30 | 31 | // when: 32 | sm.handleEvent("T1"); 33 | 34 | // then: 35 | verify(sEnter, times(1)).run(); 36 | verify(s1Enter, times(2)).run(); 37 | } 38 | 39 | @Test 40 | public void canExecuteLocalTransitionToAncestorState() { 41 | // given: 42 | Action sEnter = mock(Action.class); 43 | Action s1Enter = mock(Action.class); 44 | State s1 = new State("s1").onEnter(s1Enter); 45 | State s2 = new State("s2"); 46 | Sub s = new Sub("s", s1, s2).onEnter(sEnter); 47 | 48 | s1.addHandler("T1", s2, TransitionKind.External); 49 | s2.addHandler("T2", s, TransitionKind.Local); 50 | 51 | StateMachine sm = new StateMachine(s); 52 | sm.init(); 53 | 54 | // when: 55 | sm.handleEvent("T1"); 56 | sm.handleEvent("T2"); 57 | 58 | // then: 59 | verify(sEnter, times(1)).run(); 60 | verify(s1Enter, times(2)).run(); 61 | } 62 | 63 | @Test 64 | public void canExecuteLocalTransitionWhichResultsInExternal() { 65 | // given: 66 | Action sExit = mock(Action.class); 67 | Action s1Exit = mock(Action.class); 68 | Action b1Enter = mock(Action.class); 69 | 70 | State s1 = new State("s1").onExit(s1Exit); 71 | Sub s = new Sub("s", s1).onExit(sExit); 72 | 73 | State b1 = new State("b1").onEnter(b1Enter); 74 | Sub a = new Sub("a", s, b1); 75 | StateMachine sm = new StateMachine(a); 76 | 77 | s.addHandler("T1", b1, TransitionKind.Local); 78 | 79 | sm.init(); 80 | 81 | // when: 82 | sm.handleEvent("T1"); 83 | 84 | // then: 85 | InOrder inOrder = inOrder(s1Exit, sExit, b1Enter); 86 | inOrder.verify(s1Exit).run(); 87 | inOrder.verify(sExit).run(); 88 | inOrder.verify(b1Enter).run(); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /tests/src/test/java/de/artcom/hsm/test/ParallelStateMachineTest.java: -------------------------------------------------------------------------------- 1 | package de.artcom.hsm.test; 2 | 3 | import org.junit.Test; 4 | import org.mockito.InOrder; 5 | 6 | import de.artcom.hsm.Action; 7 | import de.artcom.hsm.Parallel; 8 | import de.artcom.hsm.State; 9 | import de.artcom.hsm.StateMachine; 10 | import de.artcom.hsm.Sub; 11 | import de.artcom.hsm.TransitionKind; 12 | 13 | import static org.mockito.Mockito.inOrder; 14 | import static org.mockito.Mockito.mock; 15 | import static org.mockito.Mockito.verify; 16 | import static org.mockito.Mockito.never; 17 | 18 | public class ParallelStateMachineTest { 19 | 20 | @Test 21 | public void canCreateParallelState() { 22 | // given: 23 | State capsOn = new State("caps_on"); 24 | State capsOff = new State("caps_off"); 25 | StateMachine capsStateMachine = new StateMachine(capsOff, capsOn); 26 | 27 | State numOn = new State("num_on"); 28 | State numOff = new State("num_off"); 29 | StateMachine numStateMachine = new StateMachine(numOff, numOn); 30 | 31 | Parallel keyboardOn = new Parallel("keyboard_on", capsStateMachine, numStateMachine); 32 | StateMachine sm = new StateMachine(keyboardOn); 33 | 34 | // when: 35 | sm.init(); 36 | System.out.println(sm.toString()); 37 | 38 | // then: 39 | // no exception 40 | } 41 | 42 | 43 | @Test 44 | public void canSwitchStatesInParallel() { 45 | // given: 46 | Action onEnterCapsOn = mock(Action.class); 47 | Action onEnterCapsOff = mock(Action.class); 48 | Action onEnterNumOn = mock(Action.class); 49 | Action onEnterNumOff = mock(Action.class); 50 | Action onEnterKeyboardOn = mock(Action.class); 51 | Action onEnterKeyboardOff = mock(Action.class); 52 | 53 | State capsOn = new State("caps_on") 54 | .onEnter(onEnterCapsOn); 55 | 56 | State capsOff = new State("caps_off") 57 | .onEnter(onEnterCapsOff); 58 | 59 | StateMachine capsStateMachine = new StateMachine(capsOff, capsOn); 60 | 61 | capsOn.addHandler("capslock", capsOff, TransitionKind.External); 62 | capsOff.addHandler("capslock", capsOn, TransitionKind.External); 63 | 64 | State numOn = new State("num_on") 65 | .onEnter(onEnterNumOn); 66 | 67 | State numOff = new State("num_off") 68 | .onEnter(onEnterNumOff); 69 | 70 | StateMachine numStateMachine = new StateMachine(numOff, numOn); 71 | 72 | numOn.addHandler("numlock", numOff, TransitionKind.External); 73 | numOff.addHandler("numlock", numOn, TransitionKind.External); 74 | 75 | Parallel keyboardOn = new Parallel("keyboard_on", capsStateMachine, numStateMachine) 76 | .onEnter(onEnterKeyboardOn); 77 | State keyboardOff = new State("keyboard_off") 78 | .onEnter(onEnterKeyboardOff); 79 | StateMachine sm = new StateMachine(keyboardOff, keyboardOn); 80 | 81 | keyboardOn.addHandler("unplug", keyboardOff, TransitionKind.External); 82 | keyboardOff.addHandler("plug", keyboardOn, TransitionKind.External); 83 | 84 | sm.init(); 85 | 86 | // when: 87 | sm.handleEvent("plug"); 88 | sm.handleEvent("capslock"); 89 | sm.handleEvent("capslock"); 90 | sm.handleEvent("numlock"); 91 | sm.handleEvent("unplug"); 92 | sm.handleEvent("capslock"); 93 | sm.handleEvent("plug"); 94 | 95 | // then: 96 | InOrder inOrder = inOrder(onEnterCapsOff, onEnterCapsOn, onEnterKeyboardOff, 97 | onEnterKeyboardOn, onEnterNumOff, onEnterNumOn); 98 | 99 | inOrder.verify(onEnterKeyboardOff).run(); 100 | inOrder.verify(onEnterKeyboardOn).run(); 101 | inOrder.verify(onEnterCapsOff).run(); 102 | inOrder.verify(onEnterNumOff).run(); 103 | inOrder.verify(onEnterCapsOn).run(); 104 | inOrder.verify(onEnterCapsOff).run(); 105 | inOrder.verify(onEnterNumOn).run(); 106 | inOrder.verify(onEnterKeyboardOff).run(); 107 | inOrder.verify(onEnterKeyboardOn).run(); 108 | inOrder.verify(onEnterCapsOff).run(); 109 | inOrder.verify(onEnterNumOff).run(); 110 | } 111 | 112 | @Test 113 | public void anotherParallelStateTest() { 114 | // given: 115 | Action p11Enter = mock(Action.class); 116 | Action p21Enter = mock(Action.class); 117 | State p11 = new State("p11").onEnter(p11Enter); 118 | State p21 = new State("p21").onEnter(p21Enter); 119 | State s1 = new State("s1"); 120 | StateMachine p1 = new StateMachine(p11); 121 | StateMachine p2 = new StateMachine(p21); 122 | Parallel s2 = new Parallel("s2", p1, p2); 123 | Sub s = new Sub("s", s1, s2).addHandler("T1", p21, TransitionKind.External); 124 | 125 | StateMachine sm = new StateMachine(s); 126 | sm.init(); 127 | 128 | // when: 129 | sm.handleEvent("T1"); 130 | 131 | // then: 132 | verify(p11Enter).run(); 133 | verify(p21Enter).run(); 134 | } 135 | 136 | @Test 137 | public void parallelStatesCanEmitEventInEnter() { 138 | // given: 139 | final State p1 = new State("p1"); 140 | final State p2 = new State("p2"); 141 | Action p1Enter = new Action() { 142 | @Override 143 | public void run(){ 144 | p1.getEventHandler().handleEvent("foo"); 145 | } 146 | }; 147 | Action p2Action = mock(Action.class); 148 | p1.onEnter(p1Enter); 149 | p2.addHandler("foo", p2, TransitionKind.Internal, p2Action); 150 | Parallel p = new Parallel("p", 151 | new StateMachine(p1), 152 | new StateMachine(p2) 153 | ); 154 | StateMachine sm = new StateMachine(p); 155 | 156 | // when: 157 | sm.init(); 158 | 159 | // then: 160 | verify(p2Action).run(); 161 | } 162 | 163 | @Test 164 | public void parallelStatesCanEmitEventInExitAndHandleAction() { 165 | // given: 166 | final State p1 = new State("p1"); 167 | final State p2 = new State("p2"); 168 | Action p1Action = new Action() { 169 | @Override 170 | public void run(){ 171 | p1.getEventHandler().handleEvent("foo"); 172 | } 173 | }; 174 | Action p2Action = mock(Action.class); 175 | p1.onExit(p1Action); 176 | p2.addHandler("foo", p2, TransitionKind.Internal, p2Action); 177 | Parallel p = new Parallel("p", 178 | new StateMachine(p1), 179 | new StateMachine(p2) 180 | ); 181 | StateMachine sm = new StateMachine(p); 182 | 183 | // when: 184 | sm.init(); 185 | sm.teardown(); 186 | 187 | // then: 188 | verify(p2Action).run(); 189 | } 190 | 191 | @Test 192 | public void parallelStatesCanEmitEventInExitButIsNotHandledInFinishedStates() { 193 | // given: 194 | final State p1 = new State("p1"); 195 | final State p2 = new State("p2"); 196 | Action p1Action = new Action() { 197 | @Override 198 | public void run(){ 199 | p1.getEventHandler().handleEvent("foo"); 200 | } 201 | }; 202 | Action p2Action = mock(Action.class); 203 | p1.onExit(p1Action); 204 | p2.addHandler("foo", p2, TransitionKind.Internal, p2Action); 205 | Parallel p = new Parallel("p", 206 | new StateMachine(p2), 207 | new StateMachine(p1) 208 | ); 209 | StateMachine sm = new StateMachine(p); 210 | 211 | // when: 212 | sm.init(); 213 | sm.teardown(); 214 | 215 | // then: 216 | verify(p2Action, never()).run(); 217 | } 218 | 219 | 220 | } 221 | -------------------------------------------------------------------------------- /tests/src/test/java/de/artcom/hsm/test/SubStateMachineTest.java: -------------------------------------------------------------------------------- 1 | package de.artcom.hsm.test; 2 | 3 | import org.junit.Test; 4 | import org.mockito.InOrder; 5 | 6 | import de.artcom.hsm.Action; 7 | import de.artcom.hsm.State; 8 | import de.artcom.hsm.StateMachine; 9 | import de.artcom.hsm.Sub; 10 | import de.artcom.hsm.TransitionKind; 11 | 12 | import static org.mockito.Mockito.inOrder; 13 | import static org.mockito.Mockito.mock; 14 | 15 | public class SubStateMachineTest { 16 | 17 | @Test 18 | public void canCreateSubStateMachine() { 19 | //given: 20 | State loud = new State("loud"); 21 | State quiet = new State("quiet"); 22 | Sub on = new Sub("on", new StateMachine(quiet, loud)); 23 | 24 | //when: 25 | StateMachine sm = new StateMachine(on); 26 | sm.init(); 27 | 28 | //then: no exception 29 | } 30 | 31 | @Test 32 | public void canTransitionSubStates() { 33 | //given: 34 | Action onEnterLoud = mock(Action.class); 35 | Action onEnterQuiet = mock(Action.class); 36 | Action onEnterOn = mock(Action.class); 37 | Action onEnterOff = mock(Action.class); 38 | State loud = new State("loud") 39 | .onEnter(onEnterLoud); 40 | State quiet = new State("quiet") 41 | .onEnter(onEnterQuiet); 42 | 43 | quiet.addHandler("volume_up", loud, TransitionKind.External); 44 | loud.addHandler("volume_down", quiet, TransitionKind.External); 45 | 46 | Sub on = new Sub("on", new StateMachine(quiet, loud)) 47 | .onEnter(onEnterOn); 48 | 49 | State off = new State("off") 50 | .onEnter(onEnterOff); 51 | 52 | on.addHandler("switched_off", off, TransitionKind.External); 53 | off.addHandler("switched_on", on, TransitionKind.External); 54 | 55 | StateMachine sm = new StateMachine(off, on); 56 | sm.init(); 57 | 58 | //when: 59 | sm.handleEvent("switched_on"); 60 | sm.handleEvent("volume_up"); 61 | sm.handleEvent("switched_off"); 62 | sm.handleEvent("switched_on"); 63 | 64 | //then: 65 | InOrder inOrder = inOrder(onEnterOff, onEnterOn, onEnterQuiet, onEnterLoud); 66 | inOrder.verify(onEnterOff).run(); 67 | inOrder.verify(onEnterOn).run(); 68 | inOrder.verify(onEnterQuiet).run(); 69 | inOrder.verify(onEnterLoud).run(); 70 | inOrder.verify(onEnterOff).run(); 71 | inOrder.verify(onEnterOn).run(); 72 | inOrder.verify(onEnterQuiet).run(); 73 | } 74 | 75 | } 76 | --------------------------------------------------------------------------------