From 6a7e78fa87aafe01fe5fefe1f7baa2094ff631fd Mon Sep 17 00:00:00 2001 From: Kevin Herron Date: Sun, 11 Aug 2024 10:43:28 -0700 Subject: [PATCH] Prepare for 1.0 release (#3) - Migrate tests to Java, apply code formatting - Logging without slf4j - Convert to Maven, apply checkstyle - Configurable context object - Create maven.yml - Create maven-publish.yml --- .github/workflows/maven-publish.yml | 22 + .github/workflows/maven.yml | 26 + README.md | 4 +- build.gradle.kts | 129 ---- config/checkstyle/checkstyle.xml | 385 ++++++++++++ gradle.properties | 3 - gradle/wrapper/gradle-wrapper.properties | 5 - gradlew | 185 ------ gradlew.bat | 89 --- pom.xml | 228 +++++++ settings.gradle | 2 - .../com/digitalpetri/strictmachine/Fsm.java | 98 +-- .../strictmachine/FsmContext.java | 237 +++---- .../strictmachine/FsmLogging.java | 54 ++ .../com/digitalpetri/strictmachine/Log.java | 94 +++ .../strictmachine/StrictMachine.java | 590 ++++++++---------- .../strictmachine/dsl/Action.java | 12 +- .../strictmachine/dsl/ActionBuilder.java | 174 +++--- .../strictmachine/dsl/ActionContext.java | 41 +- .../strictmachine/dsl/ActionFromBuilder.java | 46 +- .../strictmachine/dsl/ActionProxy.java | 14 +- .../strictmachine/dsl/ActionToBuilder.java | 46 +- .../strictmachine/dsl/FsmBuilder.java | 269 ++++---- .../strictmachine/dsl/GuardBuilder.java | 37 +- .../dsl/PredicatedTransition.java | 67 +- .../strictmachine/dsl/Transition.java | 30 +- .../strictmachine/dsl/TransitionAction.java | 30 +- .../strictmachine/dsl/TransitionBuilder.java | 162 ++--- .../strictmachine/dsl/Transitions.java | 65 +- .../strictmachine/dsl/ViaBuilder.java | 88 +-- .../strictmachine/dsl/ActionBuilderTest.java | 30 + .../strictmachine/dsl/ActionBuilderTest.kt | 28 - .../dsl/ActionFromBuilderTest.java | 48 ++ .../dsl/ActionFromBuilderTest.kt | 68 -- .../strictmachine/dsl/ActionProxyTest.java | 51 ++ .../strictmachine/dsl/ActionProxyTest.kt | 66 -- .../dsl/ActionToBuilderTest.java | 49 ++ .../strictmachine/dsl/ActionToBuilderTest.kt | 69 -- .../digitalpetri/strictmachine/dsl/Event.java | 45 ++ .../digitalpetri/strictmachine/dsl/FsmTest.kt | 38 -- .../strictmachine/dsl/GuardBuilderTest.java | 48 ++ .../strictmachine/dsl/GuardBuilderTest.kt | 67 -- .../digitalpetri/strictmachine/dsl/State.java | 5 + .../strictmachine/dsl/StrictMachineTest.java | 81 +++ .../strictmachine/dsl/StrictMachineTest.kt | 100 --- .../dsl/TransitionBuilderTest.java | 84 +++ .../dsl/TransitionBuilderTest.kt | 147 ----- .../strictmachine/dsl/ViaBuilderTest.java | 94 +++ .../strictmachine/dsl/ViaBuilderTest.kt | 122 ---- .../strictmachine/dsl/atm/AtmFsm.java | 205 +++--- .../strictmachine/dsl/atm/AtmFsmTest.kt | 83 --- .../strictmachine/dsl/atm/AtmTest.java | 75 +++ 52 files changed, 2510 insertions(+), 2325 deletions(-) create mode 100644 .github/workflows/maven-publish.yml create mode 100644 .github/workflows/maven.yml delete mode 100644 build.gradle.kts create mode 100644 config/checkstyle/checkstyle.xml delete mode 100644 gradle.properties delete mode 100644 gradle/wrapper/gradle-wrapper.properties delete mode 100755 gradlew delete mode 100644 gradlew.bat create mode 100644 pom.xml delete mode 100644 settings.gradle create mode 100644 src/main/java/com/digitalpetri/strictmachine/FsmLogging.java create mode 100644 src/main/java/com/digitalpetri/strictmachine/Log.java create mode 100644 src/test/java/com/digitalpetri/strictmachine/dsl/ActionBuilderTest.java delete mode 100644 src/test/java/com/digitalpetri/strictmachine/dsl/ActionBuilderTest.kt create mode 100644 src/test/java/com/digitalpetri/strictmachine/dsl/ActionFromBuilderTest.java delete mode 100644 src/test/java/com/digitalpetri/strictmachine/dsl/ActionFromBuilderTest.kt create mode 100644 src/test/java/com/digitalpetri/strictmachine/dsl/ActionProxyTest.java delete mode 100644 src/test/java/com/digitalpetri/strictmachine/dsl/ActionProxyTest.kt create mode 100644 src/test/java/com/digitalpetri/strictmachine/dsl/ActionToBuilderTest.java delete mode 100644 src/test/java/com/digitalpetri/strictmachine/dsl/ActionToBuilderTest.kt create mode 100644 src/test/java/com/digitalpetri/strictmachine/dsl/Event.java delete mode 100644 src/test/java/com/digitalpetri/strictmachine/dsl/FsmTest.kt create mode 100644 src/test/java/com/digitalpetri/strictmachine/dsl/GuardBuilderTest.java delete mode 100644 src/test/java/com/digitalpetri/strictmachine/dsl/GuardBuilderTest.kt create mode 100644 src/test/java/com/digitalpetri/strictmachine/dsl/State.java create mode 100644 src/test/java/com/digitalpetri/strictmachine/dsl/StrictMachineTest.java delete mode 100644 src/test/java/com/digitalpetri/strictmachine/dsl/StrictMachineTest.kt create mode 100644 src/test/java/com/digitalpetri/strictmachine/dsl/TransitionBuilderTest.java delete mode 100644 src/test/java/com/digitalpetri/strictmachine/dsl/TransitionBuilderTest.kt create mode 100644 src/test/java/com/digitalpetri/strictmachine/dsl/ViaBuilderTest.java delete mode 100644 src/test/java/com/digitalpetri/strictmachine/dsl/ViaBuilderTest.kt delete mode 100644 src/test/java/com/digitalpetri/strictmachine/dsl/atm/AtmFsmTest.kt create mode 100644 src/test/java/com/digitalpetri/strictmachine/dsl/atm/AtmTest.java diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml new file mode 100644 index 0000000..d58053c --- /dev/null +++ b/.github/workflows/maven-publish.yml @@ -0,0 +1,22 @@ +name: Maven Publish +on: + release: + types: [created] +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Maven Central Repository + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'temurin' + server-id: ossrh + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + - name: Publish package + run: mvn --batch-mode deploy + env: + MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 0000000..5829512 --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,26 @@ +name: Java CI with Maven + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'temurin' + cache: maven + - name: Build with Maven + run: mvn -B package --file pom.xml + + # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive + - name: Update dependency graph + uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 diff --git a/README.md b/README.md index cd9768c..23ef36f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Release builds are available from [Maven Central](https://repo.maven.apache.org/ ## Gradle ``` dependencies { - compile("com.digitalpetri.fsm:strict-machine:0.7") + compile("com.digitalpetri.fsm:strict-machine:1.0.0-SNAPSHOT") } ``` @@ -18,7 +18,7 @@ dependencies { com.digitalpetri.fsm strict-machine - 0.7 + 1.0.0-SNAPSHOT ``` diff --git a/build.gradle.kts b/build.gradle.kts deleted file mode 100644 index e1f2767..0000000 --- a/build.gradle.kts +++ /dev/null @@ -1,129 +0,0 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -plugins { - `java-library` - kotlin("jvm") version "1.4.30" - `maven-publish` - signing -} - -group = "com.digitalpetri.fsm" -version = "0.7-SNAPSHOT" - -repositories { - mavenCentral() -} - -dependencies { - // BYO SLF4J - compileOnly("org.slf4j:slf4j-api:1.7.+") - - testImplementation(kotlin("stdlib", "1.4.30")) - testImplementation("org.junit.jupiter:junit-jupiter-api:5.3.1") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.3.1") - testRuntimeOnly("org.slf4j:slf4j-simple:1.7.25") -} - -tasks { - withType { - sourceCompatibility = "1.8" - targetCompatibility = "1.8" - } - - withType { - kotlinOptions.jvmTarget = "1.8" - } - - withType { - useJUnitPlatform() - - testLogging { - events("PASSED", "FAILED", "SKIPPED") - } - } -} - -task("sourcesJar") { - from(sourceSets.main.get().allJava) - classifier = "sources" -} - -task("javadocJar") { - from(tasks.javadoc) - classifier = "javadoc" -} - -tasks.withType { - manifest { - attributes("Automatic-Module-Name" to "com.digitalpetri.strictmachine") - } -} - -publishing { - publications { - create("mavenJava") { - artifactId = "strict-machine" - - from(components["java"]) - - artifact(tasks["sourcesJar"]) - artifact(tasks["javadocJar"]) - - pom { - name.set("Strict Machine") - description.set("A declarative DSL for building asynchronously evaluated Finite State Machines on the JVM") - url.set("https://github.com/digitalpetri/strict-machine") - - licenses { - license { - name.set("The Apache License, Version 2.0") - url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") - } - } - - developers { - developer { - id.set("kevinherron") - name.set("Kevin Herron") - email.set("kevinherron@gmail.com") - } - } - - scm { - connection.set("scm:git:git://github.com.com/digitalpetri/strict-machine.git") - developerConnection.set("scm:git:ssh://github.com.com/digitalpetri/strict-machine.git") - url.set("https://github.com/digitalpetri/strict-machine") - } - } - } - } - - repositories { - maven { - credentials { - username = project.findProperty("ossrhUsername") as String? - password = project.findProperty("ossrhPassword") as String? - } - - // change URLs to point to your repos, e.g. http://my.org/repo - val releasesRepoUrl = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/") - val snapshotsRepoUrl = uri("https://oss.sonatype.org/content/repositories/snapshots/") - url = if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl - } - - mavenLocal() - } -} - -signing { - if (!version.toString().endsWith("SNAPSHOT")) { - useGpgCmd() - sign(publishing.publications["mavenJava"]) - } -} - -tasks.javadoc { - if (JavaVersion.current().isJava9Compatible) { - (options as StandardJavadocDocletOptions).addBooleanOption("html5", true) - } -} diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..7700b12 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,385 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle.properties b/gradle.properties deleted file mode 100644 index 0992275..0000000 --- a/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -# Prevent the kotlin-jvm plugin from automatically adding a dependency on -# Kotlin stdlib. We only use Kotlin for tests, we don't need it. -kotlin.stdlib.default.dependency=false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 442d913..0000000 --- a/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew deleted file mode 100755 index 4f906e0..0000000 --- a/gradlew +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://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. -# - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat deleted file mode 100644 index 107acd3..0000000 --- a/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..a9bb1b3 --- /dev/null +++ b/pom.xml @@ -0,0 +1,228 @@ + + + 4.0.0 + + com.digitalpetri.fsm + strict-machine + 1.0.0-SNAPSHOT + + Strict Machine + + A declarative DSL for building asynchronously evaluated Finite State Machines on the JVM. + + https://github.com/digitalpetri/strict-machine + + + + kevinherron + Kevin Herron + kevinherron@gmail.com + + + + + + Eclipse Public License - v 2.0 + https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.html + repo + + + + + https://github.com/digitalpetri/strict-machine + scm:git:git://github.com/digitalpetri/strict-machine.git + scm:git:git@github.com:digitalpetri/strict-machine.git + + HEAD + + + + 11 + 11 + 11 + UTF-8 + + + 5.10.2 + + + 10.17.0 + 3.4.0 + 3.5.0 + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + + + + release + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.8.0 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.4 + + + sign-artifacts + verify + + sign + + + + --pinentry-mode + loopback + + + + + + + org.apache.maven.plugins + maven-release-plugin + 3.1.1 + + v@{project.version} + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.7.0 + true + + ossrh + https://oss.sonatype.org/ + true + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + true + + + -Xlint:all + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + + org.apache.maven.plugins + maven-enforcer-plugin + ${maven-enforcer-plugin.version} + + + enforce-maven + + enforce + + + + + + 3.6.3 + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven-checkstyle-plugin.version} + + config/checkstyle/checkstyle.xml + true + true + true + false + + + + validate + validate + + check + + + + + + com.puppycrawl.tools + checkstyle + ${checkstyle.version} + + + + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 968567b..0000000 --- a/settings.gradle +++ /dev/null @@ -1,2 +0,0 @@ -rootProject.name = 'strict-machine' - diff --git a/src/main/java/com/digitalpetri/strictmachine/Fsm.java b/src/main/java/com/digitalpetri/strictmachine/Fsm.java index 1793803..1103c7e 100644 --- a/src/main/java/com/digitalpetri/strictmachine/Fsm.java +++ b/src/main/java/com/digitalpetri/strictmachine/Fsm.java @@ -21,57 +21,61 @@ public interface Fsm { - /** - * Get the current state of the FSM. - * - * @return the current state of the FSM. - */ - S getState(); + /** + * Get the current state of the FSM. + * + * @return the current state of the FSM. + */ + S getState(); - /** - * Fire an event for the FSM to evaluate. - *

- * The subsequent state transition may occur asynchronously. There is no guarantee that a subsequent call to - * {@link #getState()} reflects a state arrived at via evaluation of this event. - * - * @param event the event to evaluate. - * @see #fireEvent(Object, Consumer) - */ - void fireEvent(E event); + /** + * Fire an event for the FSM to evaluate. + * + *

The subsequent state transition may occur asynchronously. There is no guarantee that a + * subsequent call to {@link #getState()} reflects a state arrived at via evaluation of this + * event. + * + * @param event the event to evaluate. + * @see #fireEvent(Object, Consumer) + */ + void fireEvent(E event); - /** - * Fire an event for the FSM to evaluate, providing a callback that will be invoked when the event is evaluated. - * This callback may occur asynchronously. - * - * @param event the event to evaluate. - * @param stateConsumer the callback that will receive the state transitioned to via evaluation of {@code event}. - */ - void fireEvent(E event, Consumer stateConsumer); + /** + * Fire an event for the FSM to evaluate, providing a callback that will be invoked when the event + * is evaluated. + * + *

This callback may occur asynchronously. + * + * @param event the event to evaluate. + * @param stateConsumer the callback that will receive the state transitioned to via + * evaluation of {@code event}. + */ + void fireEvent(E event, Consumer stateConsumer); - /** - * Fire an event for the FSM to evaluate and block waiting until the state transitioned to as a result of - * evaluating {@code event} is available. - * - * @param event the event to evaluate. - * @return the state transitioned to as a result of evaluating {@code event}. - * @throws InterruptedException if interrupted while blocking. - */ - S fireEventBlocking(E event) throws InterruptedException; + /** + * Fire an event for the FSM to evaluate and block waiting until the state transitioned to as a + * result of evaluating {@code event} is available. + * + * @param event the event to evaluate. + * @return the state transitioned to as a result of evaluating {@code event}. + * @throws InterruptedException if interrupted while blocking. + */ + S fireEventBlocking(E event) throws InterruptedException; - /** - * Provides safe access to the {@link FsmContext} in order to retrieve a value from it. - * - * @param get the Function provided access to the context. - * @param the type of the value being retrieved. - * @return a value from {@code context}. - */ - T getFromContext(Function, T> get); + /** + * Provides safe access to the {@link FsmContext} in order to retrieve a value from it. + * + * @param get the Function provided access to the context. + * @param the type of the value being retrieved. + * @return a value from {@code context}. + */ + T getFromContext(Function, T> get); - /** - * Provides safe access to the {@link FsmContext}. - * - * @param contextConsumer the callback that will receive the {@link FsmContext}. - */ - void withContext(Consumer> contextConsumer); + /** + * Provides safe access to the {@link FsmContext}. + * + * @param contextConsumer the callback that will receive the {@link FsmContext}. + */ + void withContext(Consumer> contextConsumer); } diff --git a/src/main/java/com/digitalpetri/strictmachine/FsmContext.java b/src/main/java/com/digitalpetri/strictmachine/FsmContext.java index 6b89bc1..8c7a24c 100644 --- a/src/main/java/com/digitalpetri/strictmachine/FsmContext.java +++ b/src/main/java/com/digitalpetri/strictmachine/FsmContext.java @@ -21,122 +21,129 @@ public interface FsmContext { - /** - * Get the current state of the FSM. - * - * @return the current state of the FSM. - */ - S currentState(); - - /** - * Fire an event to be evaluated against the current state of the {@link Fsm}. - * - * @param event the event to be evaluated. - */ - void fireEvent(E event); - - /** - * Shelve an event to to be evaluated at some later time. - *

- * This is useful e.g. when an event can't be handled in the current state but shouldn't be discarded or ignored - * with an action-less internal transition. - * - * @param event the event to be queued. - * @see #processShelvedEvents() - */ - void shelveEvent(E event); - - /** - * Drain the event shelf of any queued events and fire them for evaluation. - */ - void processShelvedEvents(); - - /** - * Get the value identified by {@code key} from the context, or {@code null} if it does not exist. - * - * @param key the {@link Key}. - * @return the value identified by {@code key}, or {@code null} if it does not exist. - */ - Object get(FsmContext.Key key); - - /** - * Get and remove the value identified by {@code key} from the context, or {@code null} if it does not exist. - * - * @param key the {@link Key}. - * @return the value identified by {@code key}, or {@code null} if it did not exist. - */ - Object remove(FsmContext.Key key); - - /** - * Set a value identified by {@code key} on the context. - * - * @param key the {@link Key}. - * @param value the value. - */ - void set(FsmContext.Key key, Object value); - - /** - * Get the id assigned to this FSM instance. - *

- * The id is a monotonically increasing value assigned to each new instance to aid in determining which log - * messages belong to which instance. - * - * @return the id assigned to this FSM instance. - */ - long getInstanceId(); - - final class Key { - - private final String name; - private final Class type; - - public Key(String name, Class type) { - this.name = name; - this.type = type; - } - - public String name() { return name; } - - public Class type() { return type; } - - public T get(FsmContext context) { - Object value = context.get(this); - - return value != null ? type.cast(value) : null; - } - - public T remove(FsmContext context) { - Object value = context.remove(this); - - return value != null ? type.cast(value) : null; - } - - public void set(FsmContext context, T value) { - context.set(this, value); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Key key = (Key) o; - return Objects.equals(name, key.name) && - Objects.equals(type, key.type); - } - - @Override - public int hashCode() { - return Objects.hash(name, type); - } - - @Override - public String toString() { - return new StringJoiner(", ", Key.class.getSimpleName() + "[", "]") - .add("name='" + name + "'") - .add("type=" + type) - .toString(); - } + /** + * Get the current state of the FSM. + * + * @return the current state of the FSM. + */ + S currentState(); + + /** + * Fire an event to be evaluated against the current state of the {@link Fsm}. + * + * @param event the event to be evaluated. + */ + void fireEvent(E event); + + /** + * Shelve an event to be evaluated at some later time. + * + *

This is useful e.g. when an event can't be handled in the current state but shouldn't be + * discarded or ignored with an action-less internal transition. + * + * @param event the event to be queued. + * @see #processShelvedEvents() + */ + void shelveEvent(E event); + + /** + * Drain the event shelf of any queued events and fire them for evaluation. + */ + void processShelvedEvents(); + + /** + * Get the value identified by {@code key} from the context, or {@code null} if it does not + * exist. + * + * @param key the {@link Key}. + * @return the value identified by {@code key}, or {@code null} if it does not exist. + */ + Object get(FsmContext.Key key); + + /** + * Get and remove the value identified by {@code key} from the context, or {@code null} if it does + * not exist. + * + * @param key the {@link Key}. + * @return the value identified by {@code key}, or {@code null} if it did not exist. + */ + Object remove(FsmContext.Key key); + + /** + * Set a value identified by {@code key} on the context. + * + * @param key the {@link Key}. + * @param value the value. + */ + void set(FsmContext.Key key, Object value); + + /** + * Get the user-configurable context associated with this FSM instance. + * + * @return the user-configurable context associated with this FSM instance. + */ + Object getContext(); + + final class Key { + + private final String name; + private final Class type; + + public Key(String name, Class type) { + this.name = name; + this.type = type; + } + + public String name() { + return name; + } + + public Class type() { + return type; + } + + public T get(FsmContext context) { + Object value = context.get(this); + + return value != null ? type.cast(value) : null; + } + + public T remove(FsmContext context) { + Object value = context.remove(this); + return value != null ? type.cast(value) : null; } + public void set(FsmContext context, T value) { + context.set(this, value); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Key key = (Key) o; + return Objects.equals(name, key.name) + && Objects.equals(type, key.type); + } + + @Override + public int hashCode() { + return Objects.hash(name, type); + } + + @Override + public String toString() { + return new StringJoiner(", ", Key.class.getSimpleName() + "[", "]") + .add("name='" + name + "'") + .add("type=" + type) + .toString(); + } + + } + } diff --git a/src/main/java/com/digitalpetri/strictmachine/FsmLogging.java b/src/main/java/com/digitalpetri/strictmachine/FsmLogging.java new file mode 100644 index 0000000..caab6c7 --- /dev/null +++ b/src/main/java/com/digitalpetri/strictmachine/FsmLogging.java @@ -0,0 +1,54 @@ +package com.digitalpetri.strictmachine; + +public final class FsmLogging { + + private FsmLogging() {} + + /** + * Configure the {@link Callback} to use for logging. + * + * @param callback the {@link Callback} to use for logging. + */ + public static void configure(Callback callback) { + Log.CALLBACK.set(callback); + } + + public interface Callback { + + /** + * Log a message at the given {@link Level}. + * + * @param context the user-configurable context. May be {@code null} even when configured if + * the message originates from a global context. + * @param level the {@link Level} to log at. + * @param message the message. + */ + void log(Object context, Level level, String message); + + /** + * Check if logging is enabled for the given {@link Level}. + * + * @param level the {@link Level} to check. + * @return {@code true} if logging is enabled for the given {@link Level}. + */ + default boolean isEnabled(Level level) { + return true; + } + + } + + public enum Level { + TRACE, + DEBUG, + INFO, + WARN, + ERROR + } + + /** + * A logging {@link Callback} that logs to {@link System#out}. + */ + public static final Callback SYSTEM_OUT_CALLBACK = + (context, level, message) -> System.out.printf("%s: %s%n", level, message); + +} diff --git a/src/main/java/com/digitalpetri/strictmachine/Log.java b/src/main/java/com/digitalpetri/strictmachine/Log.java new file mode 100644 index 0000000..edfe7c8 --- /dev/null +++ b/src/main/java/com/digitalpetri/strictmachine/Log.java @@ -0,0 +1,94 @@ +package com.digitalpetri.strictmachine; + +import com.digitalpetri.strictmachine.FsmLogging.Level; +import java.util.concurrent.atomic.AtomicReference; + +public class Log { + + public static final AtomicReference CALLBACK = new AtomicReference<>(); + + private Log() {} + + /** + * Log a message at {@link Level#TRACE}. + * + * @param context the user-configurable context. May be {@code null} even when configured. + * @param format the message format. + * @param args the message arguments. + */ + public static void trace(Object context, String format, Object... args) { + log(context, Level.TRACE, format, args); + } + + /** + * Log a message at {@link Level#DEBUG}. + * + * @param context the user-configurable context. May be {@code null} even when configured. + * @param format the message format. + * @param args the message arguments. + */ + public static void debug(Object context, String format, Object... args) { + log(context, Level.DEBUG, format, args); + } + + /** + * Log a message at {@link Level#INFO}. + * + * @param context the user-configurable context. May be {@code null} even when configured. + * @param format the message format. + * @param args the message arguments. + */ + public static void info(Object context, String format, Object... args) { + log(context, Level.INFO, format, args); + } + + /** + * Log a message at {@link Level#WARN}. + * + * @param context the user-configurable context. May be {@code null} even when configured. + * @param format the message format. + * @param args the message arguments. + */ + public static void warn(Object context, String format, Object... args) { + log(context, Level.WARN, format, args); + } + + /** + * Log a message at {@link Level#ERROR}. + * + * @param context the user-configurable context. May be {@code null} even when configured. + * @param format the message format. + * @param args the message arguments. + */ + public static void error(Object context, String format, Object... args) { + log(context, Level.ERROR, format, args); + } + + /** + * Check if logging is enabled for the given {@link Level}. + * + * @param level the {@link Level} to check. + * @return {@code true} if logging is enabled for the given {@link Level}. + */ + public static boolean isLevelEnabled(FsmLogging.Level level) { + FsmLogging.Callback callback = CALLBACK.get(); + + return callback == null || callback.isEnabled(level); + } + + /** + * Log a message at the given {@link Level}. + * + * @param context the user-configurable context. May be {@code null} even when configured. + * @param level the {@link Level} to log at. + * @param format the message format. + * @param args the message arguments. + */ + public static void log(Object context, Level level, String format, Object... args) { + FsmLogging.Callback callback = CALLBACK.get(); + if (callback != null && callback.isEnabled(level)) { + callback.log(context, level, String.format(format, args)); + } + } + +} diff --git a/src/main/java/com/digitalpetri/strictmachine/StrictMachine.java b/src/main/java/com/digitalpetri/strictmachine/StrictMachine.java index e431dab..687cd63 100644 --- a/src/main/java/com/digitalpetri/strictmachine/StrictMachine.java +++ b/src/main/java/com/digitalpetri/strictmachine/StrictMachine.java @@ -16,401 +16,347 @@ package com.digitalpetri.strictmachine; +import com.digitalpetri.strictmachine.FsmLogging.Level; +import com.digitalpetri.strictmachine.dsl.ActionContext; +import com.digitalpetri.strictmachine.dsl.ActionProxy; +import com.digitalpetri.strictmachine.dsl.Transition; +import com.digitalpetri.strictmachine.dsl.TransitionAction; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.LinkedTransferQueue; -import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Consumer; import java.util.function.Function; -import com.digitalpetri.strictmachine.dsl.ActionContext; -import com.digitalpetri.strictmachine.dsl.ActionProxy; -import com.digitalpetri.strictmachine.dsl.Transition; -import com.digitalpetri.strictmachine.dsl.TransitionAction; -import org.slf4j.Logger; -import org.slf4j.MDC; - public class StrictMachine implements Fsm { - private static final AtomicLong INSTANCE_ID = new AtomicLong(0); - - private final long instanceId = INSTANCE_ID.getAndIncrement(); - - private volatile boolean pollExecuted = false; - private final Object queueLock = new Object(); - private final ArrayDeque eventQueue = new ArrayDeque<>(); - private final ArrayDeque eventShelf = new ArrayDeque<>(); - - private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(true); - private final Map, Object> contextValues = new ConcurrentHashMap<>(); - private final AtomicReference state = new AtomicReference<>(); + private volatile boolean pollExecuted = false; + private final Object queueLock = new Object(); + private final ArrayDeque eventQueue = new ArrayDeque<>(); + private final ArrayDeque eventShelf = new ArrayDeque<>(); + + private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(true); + private final Map, Object> contextValues = new ConcurrentHashMap<>(); + private final AtomicReference state = new AtomicReference<>(); + + private final Object context; + private final Executor executor; + private final ActionProxy actionProxy; + private final List> transitions; + private final List> transitionActions; + + public StrictMachine( + Object context, + Executor executor, + ActionProxy actionProxy, + S initialState, + List> transitions, + List> transitionActions + ) { + + this.context = context; + this.executor = executor; + this.actionProxy = actionProxy; + this.transitions = transitions; + this.transitionActions = transitionActions; + + state.set(initialState); + } + + @Override + public S getState() { + try { + readWriteLock.readLock().lock(); + + return state.get(); + } finally { + readWriteLock.readLock().unlock(); + } + } - private final Logger logger; - private final Map mdc; - private final Executor executor; - private final ActionProxy actionProxy; - private final List> transitions; - private final List> transitionActions; + @Override + public void fireEvent(E event) { + fireEvent(event, null); + } - public StrictMachine( - Logger logger, - Executor executor, - ActionProxy actionProxy, - S initialState, - List> transitions, - List> transitionActions) { + @Override + public void fireEvent(E event, Consumer stateConsumer) { + synchronized (queueLock) { + eventQueue.add(new PendingEvent(event, stateConsumer)); - this(logger, Collections.emptyMap(), executor, actionProxy, initialState, transitions, transitionActions); + maybeExecutePoll(); } + } - public StrictMachine( - Logger logger, - Map mdc, - Executor executor, - ActionProxy actionProxy, - S initialState, - List> transitions, - List> transitionActions) { - - this.logger = logger; - this.mdc = mdc; - this.executor = executor; - this.actionProxy = actionProxy; - this.transitions = transitions; - this.transitionActions = transitionActions; - - state.set(initialState); - } + @Override + public S fireEventBlocking(E event) throws InterruptedException { + var transferQueue = new LinkedTransferQueue(); - @Override - public S getState() { - try { - readWriteLock.readLock().lock(); + fireEvent(event, transferQueue::put); - return state.get(); - } finally { - readWriteLock.readLock().unlock(); - } - } + return transferQueue.take(); + } - @Override - public void fireEvent(E event) { - fireEvent(event, null); + @Override + public T getFromContext(Function, T> get) { + try { + readWriteLock.writeLock().lock(); + + return get.apply(new FsmContextImpl()); + } finally { + readWriteLock.writeLock().unlock(); } + } - @Override - public void fireEvent(E event, Consumer stateConsumer) { - synchronized (queueLock) { - eventQueue.add(new PendingEvent(event, stateConsumer)); + @Override + public void withContext(Consumer> contextConsumer) { + try { + readWriteLock.writeLock().lock(); - maybeExecutePoll(); - } + contextConsumer.accept(new FsmContextImpl()); + } finally { + readWriteLock.writeLock().unlock(); + } + } + + private void maybeExecutePoll() { + synchronized (queueLock) { + if (!pollExecuted && !eventQueue.isEmpty()) { + executor.execute(new PollAndEvaluate()); + pollExecuted = true; + } } + } - @Override - public S fireEventBlocking(E event) throws InterruptedException { - LinkedTransferQueue transferQueue = new LinkedTransferQueue<>(); + private class PendingEvent { - fireEvent(event, transferQueue::put); + final E event; + final Consumer stateConsumer; - return transferQueue.take(); + PendingEvent(E event, Consumer stateConsumer) { + this.event = event; + this.stateConsumer = stateConsumer; } + } - @Override - public T getFromContext(Function, T> get) { - try { - readWriteLock.writeLock().lock(); - - return get.apply(new FsmContextImpl()); - } finally { - readWriteLock.writeLock().unlock(); - } - } + private class PollAndEvaluate implements Runnable { @Override - public void withContext(Consumer> contextConsumer) { - try { - readWriteLock.writeLock().lock(); + public void run() { + PendingEvent pending; - contextConsumer.accept(new FsmContextImpl()); - } finally { - readWriteLock.writeLock().unlock(); - } - } + synchronized (queueLock) { + pending = eventQueue.poll(); - private void maybeExecutePoll() { - synchronized (queueLock) { - if (!pollExecuted && !eventQueue.isEmpty()) { - executor.execute(new PollAndEvaluate()); - pollExecuted = true; - } + if (pending == null) { + return; } - } + } - private class PendingEvent { - final E event; - final Consumer stateConsumer; + E event = pending.event; - PendingEvent(E event, Consumer stateConsumer) { - this.event = event; - this.stateConsumer = stateConsumer; - } - } + try { + readWriteLock.writeLock().lock(); - private class PollAndEvaluate implements Runnable { - @Override - public void run() { - PendingEvent pending; + S currState = state.get(); + S nextState = currState; - synchronized (queueLock) { - pending = eventQueue.poll(); - - if (pending == null) return; - } + var ctx = new FsmContextImpl(); - E event = pending.event; - - try { - readWriteLock.writeLock().lock(); - - S currState = state.get(); - S nextState = currState; - - FsmContextImpl ctx = new FsmContextImpl(); - - for (Transition transition : transitions) { - if (transition.matches(ctx, currState, event)) { - nextState = transition.target(); - break; - } - } - - state.set(nextState); - - if (logger.isDebugEnabled()) { - mdc.forEach(MDC::put); - try { - logger.debug( - "[{}] {} x {} = {}", - instanceId, - padRight(String.format("S(%s)", currState)), - padRight(String.format("E(%s)", event)), - padRight(String.format("S'(%s)", nextState)) - ); - } finally { - mdc.keySet().forEach(MDC::remove); - } - } - - ActionContextImpl actionContext = new ActionContextImpl( - currState, - nextState, - event - ); - - List> matchingActions = new ArrayList<>(); - - for (TransitionAction transitionAction : transitionActions) { - if (transitionAction.matches(currState, nextState, event)) { - matchingActions.add(transitionAction); - } - } - - if (logger.isTraceEnabled()) { - mdc.forEach(MDC::put); - try { - logger.trace( - "[{}] found {} matching TransitionActions", - instanceId, matchingActions.size() - ); - } finally { - mdc.keySet().forEach(MDC::remove); - } - } - - matchingActions.forEach(transitionAction -> { - try { - if (actionProxy == null) { - if (logger.isTraceEnabled()) { - mdc.forEach(MDC::put); - try { - logger.trace( - "[{}] executing TransitionAction: {}", - instanceId, transitionAction - ); - } finally { - mdc.keySet().forEach(MDC::remove); - } - } - - transitionAction.execute(actionContext); - } else { - if (logger.isTraceEnabled()) { - mdc.forEach(MDC::put); - try { - logger.trace( - "[{}] executing (via proxy) TransitionAction: {}", - instanceId, transitionAction - ); - } finally { - mdc.keySet().forEach(MDC::remove); - } - } - - actionProxy.execute(actionContext, transitionAction::execute); - } - } catch (Throwable ex) { - mdc.forEach(MDC::put); - try { - logger.warn( - "[{}] Uncaught Throwable executing TransitionAction: {}", - instanceId, transitionAction, ex - ); - } finally { - mdc.keySet().forEach(MDC::remove); - } - } - }); - - } finally { - readWriteLock.writeLock().unlock(); - } + for (Transition transition : transitions) { + if (transition.matches(ctx, currState, event)) { + nextState = transition.target(); + break; + } + } - if (pending.stateConsumer != null) { - pending.stateConsumer.accept(state.get()); - } + state.set(nextState); - synchronized (queueLock) { - if (eventQueue.isEmpty()) { - pollExecuted = false; - } else { - // pollExecuted remains true - executor.execute(new PollAndEvaluate()); - } - } + if (Log.isLevelEnabled(Level.DEBUG)) { + Log.debug( + context, + "%s x %s = %s", + padRight(String.format("S(%s)", currState)), + padRight(String.format("E(%s)", event)), + padRight(String.format("S'(%s)", nextState)) + ); } - } - private static final int PADDING = 24; + var actionContext = new ActionContextImpl( + currState, + nextState, + event + ); - private static String padRight(String s) { - return String.format("%1$-" + PADDING + "s", s); - } - - private class FsmContextImpl implements FsmContext { + var matchingActions = new ArrayList>(); - @Override - public S currentState() { - return getState(); + for (TransitionAction transitionAction : transitionActions) { + if (transitionAction.matches(currState, nextState, event)) { + matchingActions.add(transitionAction); + } } - @Override - public void fireEvent(E event) { - StrictMachine.this.fireEvent(event); - } + Log.trace(context, "found %d matching TransitionActions", matchingActions.size()); - @Override - public void shelveEvent(E event) { - try { - readWriteLock.writeLock().lock(); + matchingActions.forEach(transitionAction -> { + try { + if (actionProxy == null) { + Log.trace(context, "executing TransitionAction: %s", transitionAction); - eventShelf.add(new PendingEvent(event, s -> {})); - } finally { - readWriteLock.writeLock().unlock(); + transitionAction.execute(actionContext); + } else { + Log.trace( + context, + "executing (via proxy) TransitionAction: %s", transitionAction + ); + + actionProxy.execute(actionContext, transitionAction::execute); } + } catch (Throwable ex) { + Log.warn( + context, + "Uncaught Throwable executing TransitionAction: %s\n%s", transitionAction, ex + ); + } + }); + + } finally { + readWriteLock.writeLock().unlock(); + } + + if (pending.stateConsumer != null) { + pending.stateConsumer.accept(state.get()); + } + + synchronized (queueLock) { + if (eventQueue.isEmpty()) { + pollExecuted = false; + } else { + // pollExecuted remains true + executor.execute(new PollAndEvaluate()); } + } + } + } - @Override - public void processShelvedEvents() { - try { - readWriteLock.writeLock().lock(); + private static final int PADDING = 24; - synchronized (queueLock) { - while (!eventShelf.isEmpty()) { - eventQueue.addFirst(eventShelf.removeLast()); - } + private static String padRight(String s) { + return String.format("%1$-" + PADDING + "s", s); + } - maybeExecutePoll(); - } - } finally { - readWriteLock.writeLock().unlock(); - } - } + private class FsmContextImpl implements FsmContext { - @Override - public Object get(Key key) { - try { - readWriteLock.readLock().lock(); + @Override + public S currentState() { + return getState(); + } - return contextValues.get(key); - } finally { - readWriteLock.readLock().unlock(); - } - } + @Override + public void fireEvent(E event) { + StrictMachine.this.fireEvent(event); + } - @Override - public Object remove(Key key) { - try { - readWriteLock.writeLock().lock(); + @Override + public void shelveEvent(E event) { + try { + readWriteLock.writeLock().lock(); + + eventShelf.add(new PendingEvent(event, s -> {})); + } finally { + readWriteLock.writeLock().unlock(); + } + } - return contextValues.remove(key); - } finally { - readWriteLock.writeLock().unlock(); - } - } + @Override + public void processShelvedEvents() { + try { + readWriteLock.writeLock().lock(); - @Override - public void set(Key key, Object value) { - try { - readWriteLock.writeLock().lock(); + synchronized (queueLock) { + while (!eventShelf.isEmpty()) { + eventQueue.addFirst(eventShelf.removeLast()); + } - contextValues.put(key, value); - } finally { - readWriteLock.writeLock().unlock(); - } + maybeExecutePoll(); } + } finally { + readWriteLock.writeLock().unlock(); + } + } - @Override - public long getInstanceId() { - return instanceId; - } + @Override + public Object get(Key key) { + try { + readWriteLock.readLock().lock(); + + return contextValues.get(key); + } finally { + readWriteLock.readLock().unlock(); + } + } + @Override + public Object remove(Key key) { + try { + readWriteLock.writeLock().lock(); + + return contextValues.remove(key); + } finally { + readWriteLock.writeLock().unlock(); + } } - private class ActionContextImpl extends FsmContextImpl implements ActionContext { + @Override + public void set(Key key, Object value) { + try { + readWriteLock.writeLock().lock(); + + contextValues.put(key, value); + } finally { + readWriteLock.writeLock().unlock(); + } + } - private final S from; - private final S to; - private final E event; + @Override + public Object getContext() { + return context; + } - ActionContextImpl(S from, S to, E event) { - this.from = from; - this.to = to; - this.event = event; - } + } - @Override - public S from() { - return from; - } + private class ActionContextImpl extends FsmContextImpl implements ActionContext { - @Override - public S to() { - return to; - } + private final S from; + private final S to; + private final E event; - @Override - public E event() { - return event; - } + ActionContextImpl(S from, S to, E event) { + this.from = from; + this.to = to; + this.event = event; + } + + @Override + public S from() { + return from; + } + @Override + public S to() { + return to; } + @Override + public E event() { + return event; + } + + } + } diff --git a/src/main/java/com/digitalpetri/strictmachine/dsl/Action.java b/src/main/java/com/digitalpetri/strictmachine/dsl/Action.java index 2f8d10a..3ca1346 100644 --- a/src/main/java/com/digitalpetri/strictmachine/dsl/Action.java +++ b/src/main/java/com/digitalpetri/strictmachine/dsl/Action.java @@ -19,11 +19,11 @@ @FunctionalInterface public interface Action { - /** - * Execute this action. - * - * @param context the {@link ActionContext}. - */ - void execute(ActionContext context); + /** + * Execute this action. + * + * @param context the {@link ActionContext}. + */ + void execute(ActionContext context); } diff --git a/src/main/java/com/digitalpetri/strictmachine/dsl/ActionBuilder.java b/src/main/java/com/digitalpetri/strictmachine/dsl/ActionBuilder.java index 9166196..052ccaf 100644 --- a/src/main/java/com/digitalpetri/strictmachine/dsl/ActionBuilder.java +++ b/src/main/java/com/digitalpetri/strictmachine/dsl/ActionBuilder.java @@ -22,108 +22,108 @@ public class ActionBuilder { - private Predicate from; - private Predicate to; - private Predicate via; - private final LinkedList> transitionActions; - - ActionBuilder( - Predicate from, - Predicate to, - Predicate via, - LinkedList> transitionActions - ) { - - this.from = from; - this.to = to; - this.via = via; - this.transitionActions = transitionActions; - } - - /** - * Add {@code action} to the list of {@link TransitionAction}s to be executed. - *

- * Actions are executed in the order they appear in the list. - * - * @param action the action to execute. - * @return this {@link ActionBuilder}. - */ - public ActionBuilder execute(Action action) { - return executeLast(action); - } - - /** - * Add {@code action} to the end of the list of {@link TransitionAction}s to be executed. - *

- * Actions are executed in the order they appear in the list. - * - * @param action the action to execute. - * @return this {@link ActionBuilder}. - */ - public ActionBuilder executeLast(Action action) { - TransitionAction transitionAction = new PredicatedTransitionAction<>( + private Predicate from; + private Predicate to; + private Predicate via; + private final LinkedList> transitionActions; + + ActionBuilder( + Predicate from, + Predicate to, + Predicate via, + LinkedList> transitionActions + ) { + + this.from = from; + this.to = to; + this.via = via; + this.transitionActions = transitionActions; + } + + /** + * Add {@code action} to the list of {@link TransitionAction}s to be executed. + * + *

Actions are executed in the order they appear in the list. + * + * @param action the action to execute. + * @return this {@link ActionBuilder}. + */ + public ActionBuilder execute(Action action) { + return executeLast(action); + } + + /** + * Add {@code action} to the end of the list of {@link TransitionAction}s to be executed. + * + *

Actions are executed in the order they appear in the list. + * + * @param action the action to execute. + * @return this {@link ActionBuilder}. + */ + public ActionBuilder executeLast(Action action) { + transitionActions.addLast( + new PredicatedTransitionAction<>( from, to, via, action::execute - ); - - transitionActions.addLast(transitionAction); - - return this; - } - - /** - * Add {@code action} to the beginning of the list of {@link TransitionAction}s to be executed. - *

- * Actions are executed in the order they appear in the list. - * - * @param action the action to execute. - * @return this {@link ActionBuilder}. - */ - public ActionBuilder executeFirst(Action action) { - TransitionAction transitionAction = new PredicatedTransitionAction<>( + ) + ); + + return this; + } + + /** + * Add {@code action} to the beginning of the list of {@link TransitionAction}s to be executed. + * + *

Actions are executed in the order they appear in the list. + * + * @param action the action to execute. + * @return this {@link ActionBuilder}. + */ + public ActionBuilder executeFirst(Action action) { + transitionActions.addFirst( + new PredicatedTransitionAction<>( from, to, via, action::execute - ); - - transitionActions.addFirst(transitionAction); + ) + ); - return this; - } - - private static class PredicatedTransitionAction implements TransitionAction { + return this; + } - private final Predicate from; - private final Predicate to; - private final Predicate via; - private final Consumer> action; + private static class PredicatedTransitionAction implements TransitionAction { - PredicatedTransitionAction( - Predicate from, - Predicate to, - Predicate via, - Consumer> action - ) { + private final Predicate from; + private final Predicate to; + private final Predicate via; + private final Consumer> action; - this.from = from; - this.to = to; - this.via = via; - this.action = action; - } + PredicatedTransitionAction( + Predicate from, + Predicate to, + Predicate via, + Consumer> action + ) { - @Override - public void execute(ActionContext context) { - action.accept(context); - } + this.from = from; + this.to = to; + this.via = via; + this.action = action; + } - @Override - public boolean matches(S from, S to, E event) { - return this.from.test(from) && this.to.test(to) && this.via.test(event); - } + @Override + public void execute(ActionContext context) { + action.accept(context); + } + @Override + public boolean matches(S from, S to, E event) { + return this.from.test(from) && this.to.test(to) && this.via.test(event); } + } + } diff --git a/src/main/java/com/digitalpetri/strictmachine/dsl/ActionContext.java b/src/main/java/com/digitalpetri/strictmachine/dsl/ActionContext.java index 5a87cfe..3d8e6fc 100644 --- a/src/main/java/com/digitalpetri/strictmachine/dsl/ActionContext.java +++ b/src/main/java/com/digitalpetri/strictmachine/dsl/ActionContext.java @@ -20,33 +20,34 @@ /** * The context in which a {@link Action} is being executed. - *

- * Provides access to the transition criteria: the from state, to state, and event that triggered the transition. + * + *

Provides access to the transition criteria: the from state, to state, and event that triggered + * the transition. * * @param state type * @param event type */ public interface ActionContext extends FsmContext { - /** - * Get the state being transitioned from. - * - * @return the state being transitioned from. - */ - S from(); + /** + * Get the state being transitioned from. + * + * @return the state being transitioned from. + */ + S from(); - /** - * Get the state transitioned to. - * - * @return the state transitioned to. - */ - S to(); + /** + * Get the state transitioned to. + * + * @return the state transitioned to. + */ + S to(); - /** - * Get the event that caused the transition. - * - * @return the event that caused the transition. - */ - E event(); + /** + * Get the event that caused the transition. + * + * @return the event that caused the transition. + */ + E event(); } diff --git a/src/main/java/com/digitalpetri/strictmachine/dsl/ActionFromBuilder.java b/src/main/java/com/digitalpetri/strictmachine/dsl/ActionFromBuilder.java index 4f0dab0..f332adf 100644 --- a/src/main/java/com/digitalpetri/strictmachine/dsl/ActionFromBuilder.java +++ b/src/main/java/com/digitalpetri/strictmachine/dsl/ActionFromBuilder.java @@ -22,28 +22,28 @@ public class ActionFromBuilder, E> { - private final Predicate fromFilter; - private final LinkedList> transitionActions; - - ActionFromBuilder(Predicate fromFilter, LinkedList> transitionActions) { - this.fromFilter = fromFilter; - this.transitionActions = transitionActions; - } - - public ViaBuilder to(S state) { - return to(s -> Objects.equals(s, state)); - } - - public ViaBuilder to(Predicate toFilter) { - return new ViaBuilder<>( - fromFilter, - toFilter, - transitionActions - ); - } - - public ViaBuilder toAny() { - return to(s -> true); - } + private final Predicate fromFilter; + private final LinkedList> transitionActions; + + ActionFromBuilder(Predicate fromFilter, LinkedList> transitionActions) { + this.fromFilter = fromFilter; + this.transitionActions = transitionActions; + } + + public ViaBuilder to(S state) { + return to(s -> Objects.equals(s, state)); + } + + public ViaBuilder to(Predicate toFilter) { + return new ViaBuilder<>( + fromFilter, + toFilter, + transitionActions + ); + } + + public ViaBuilder toAny() { + return to(s -> true); + } } diff --git a/src/main/java/com/digitalpetri/strictmachine/dsl/ActionProxy.java b/src/main/java/com/digitalpetri/strictmachine/dsl/ActionProxy.java index 2561925..8397715 100644 --- a/src/main/java/com/digitalpetri/strictmachine/dsl/ActionProxy.java +++ b/src/main/java/com/digitalpetri/strictmachine/dsl/ActionProxy.java @@ -19,12 +19,12 @@ @FunctionalInterface public interface ActionProxy { - /** - * Execute this action. - * - * @param context the {@link ActionContext}. - * @param action the {@link Action} to execute. - */ - void execute(ActionContext context, Action action); + /** + * Execute this action. + * + * @param context the {@link ActionContext}. + * @param action the {@link Action} to execute. + */ + void execute(ActionContext context, Action action); } diff --git a/src/main/java/com/digitalpetri/strictmachine/dsl/ActionToBuilder.java b/src/main/java/com/digitalpetri/strictmachine/dsl/ActionToBuilder.java index feae79c..43aefb0 100644 --- a/src/main/java/com/digitalpetri/strictmachine/dsl/ActionToBuilder.java +++ b/src/main/java/com/digitalpetri/strictmachine/dsl/ActionToBuilder.java @@ -22,28 +22,28 @@ public class ActionToBuilder, E> { - private final Predicate toFilter; - private final LinkedList> transitionActions; - - ActionToBuilder(Predicate toFilter, LinkedList> transitionActions) { - this.toFilter = toFilter; - this.transitionActions = transitionActions; - } - - public ViaBuilder from(S from) { - return from(s -> Objects.equals(s, from)); - } - - public ViaBuilder from(Predicate fromFilter) { - return new ViaBuilder<>( - fromFilter, - toFilter, - transitionActions - ); - } - - public ViaBuilder fromAny() { - return from(s -> true); - } + private final Predicate toFilter; + private final LinkedList> transitionActions; + + ActionToBuilder(Predicate toFilter, LinkedList> transitionActions) { + this.toFilter = toFilter; + this.transitionActions = transitionActions; + } + + public ViaBuilder from(S from) { + return from(s -> Objects.equals(s, from)); + } + + public ViaBuilder from(Predicate fromFilter) { + return new ViaBuilder<>( + fromFilter, + toFilter, + transitionActions + ); + } + + public ViaBuilder fromAny() { + return from(s -> true); + } } diff --git a/src/main/java/com/digitalpetri/strictmachine/dsl/FsmBuilder.java b/src/main/java/com/digitalpetri/strictmachine/dsl/FsmBuilder.java index 82511c3..5553e18 100644 --- a/src/main/java/com/digitalpetri/strictmachine/dsl/FsmBuilder.java +++ b/src/main/java/com/digitalpetri/strictmachine/dsl/FsmBuilder.java @@ -16,153 +16,148 @@ package com.digitalpetri.strictmachine.dsl; +import com.digitalpetri.strictmachine.Fsm; +import com.digitalpetri.strictmachine.StrictMachine; import java.util.ArrayList; -import java.util.Collections; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Predicate; -import com.digitalpetri.strictmachine.Fsm; -import com.digitalpetri.strictmachine.StrictMachine; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - public class FsmBuilder, E> { - private static final ExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadExecutor(); - - private final List> transitions = new ArrayList<>(); - - private final LinkedList> transitionActions = new LinkedList<>(); - - private ActionProxy actionProxy = null; - - private final Logger logger; - private final Map mdc; - private final Executor executor; - - public FsmBuilder() { - this(EXECUTOR_SERVICE, StrictMachine.class.getName()); - } - - public FsmBuilder(Executor executor, String loggerName) { - this(executor, loggerName, Collections.emptyMap()); - } - - public FsmBuilder(Executor executor, String loggerName, Map mdc) { - this.executor = executor; - this.logger = LoggerFactory.getLogger(loggerName); - this.mdc = mdc; - } - - /** - * Start defining a {@link Transition} from state {@code state}. - * - * @param state the state the transition begins in. - * @return a {@link TransitionBuilder}. - */ - public TransitionBuilder when(S state) { - return new TransitionBuilder<>(state, transitions, transitionActions); - } - - /** - * Start defining an {@link Action} that will be executed after an internal transition from/to {@code state}. - *

- * The criteria for the event that causes this transition is defined on the returned {@link ViaBuilder}. - * - * @param state the state experiencing an internal transition. - * @return a {@link ViaBuilder}. - */ - public ViaBuilder onInternalTransition(S state) { - return onTransitionFrom(state).to(state); - } - - /** - * Start defining an {@link Action} that will executed after a transition to {@code state}. - *

- * Further criteria for execution will be defined on the returned {@link ActionToBuilder}. - * - * @param state the state being transitioned to. - * @return an {@link ActionToBuilder}. - */ - public ActionToBuilder onTransitionTo(S state) { - return onTransitionTo(s -> Objects.equals(s, state)); - } - - /** - * Start defining an {@link Action} that will execute after a transition to any state that passes {@code filter}. - *

- * Further criteria for execution will be defined on the returned {@link ActionToBuilder}. - * - * @param filter the filter for states being transitioned to. - * @return an {@link ActionToBuilder}. - */ - public ActionToBuilder onTransitionTo(Predicate filter) { - return new ActionToBuilder<>(filter, transitionActions); - } - - /** - * Start defining an {@link Action} that will execute after a transition from {@code state}. - * - * @param state the state being transitioned from. - * @return an {@link ActionFromBuilder}. - */ - public ActionFromBuilder onTransitionFrom(S state) { - return onTransitionFrom(s -> Objects.equals(s, state)); - } - - /** - * Start defining an {@link Action} that will execute after a transition from any state that passes {@code filter}. - * - * @param filter the filter for states being transitioned from. - * @return an {@link ActionFromBuilder}. - */ - public ActionFromBuilder onTransitionFrom(Predicate filter) { - return new ActionFromBuilder<>(filter, transitionActions); - } - - /** - * Add a manually defined {@link Transition}. - * - * @param transition the {@link Transition} to add. - */ - public void addTransition(Transition transition) { - transitions.add(transition); - } - - /** - * Add a manually defined {@link TransitionAction}. - * - * @param transitionAction the {@link TransitionAction} to add. - */ - public void addTransitionAction(TransitionAction transitionAction) { - transitionActions.add(transitionAction); - } - - /** - * Configure an {@link ActionProxy} for the {@link Fsm} instance being built. - * - * @param actionProxy an {@link ActionProxy} for the {@link Fsm} instance being built. - */ - public void setActionProxy(ActionProxy actionProxy) { - this.actionProxy = actionProxy; - } - - public Fsm build(S initialState) { - return new StrictMachine<>( - logger, - mdc, - executor, - actionProxy, - initialState, - new ArrayList<>(transitions), - new ArrayList<>(transitionActions) - ); - } + private static final AtomicLong INSTANCE_ID = new AtomicLong(0); + + private static final ExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadExecutor(); + + private final List> transitions = new ArrayList<>(); + + private final LinkedList> transitionActions = new LinkedList<>(); + + private ActionProxy actionProxy = null; + + private final Object context; + private final Executor executor; + + public FsmBuilder() { + this(INSTANCE_ID.getAndIncrement(), EXECUTOR_SERVICE); + } + + public FsmBuilder(Object context, Executor executor) { + this.context = context != null ? context : INSTANCE_ID.getAndIncrement(); + this.executor = executor; + } + + /** + * Start defining a {@link Transition} from state {@code state}. + * + * @param state the state the transition begins in. + * @return a {@link TransitionBuilder}. + */ + public TransitionBuilder when(S state) { + return new TransitionBuilder<>(state, transitions, transitionActions); + } + + /** + * Start defining an {@link Action} that will be executed after an internal transition from/to + * {@code state}. + * + *

The criteria for the event that causes this transition is defined on the returned + * {@link ViaBuilder}. + * + * @param state the state experiencing an internal transition. + * @return a {@link ViaBuilder}. + */ + public ViaBuilder onInternalTransition(S state) { + return onTransitionFrom(state).to(state); + } + + /** + * Start defining an {@link Action} that will be executed after a transition to {@code state}. + * + *

Further criteria for execution will be defined on the returned {@link ActionToBuilder}. + * + * @param state the state being transitioned to. + * @return an {@link ActionToBuilder}. + */ + public ActionToBuilder onTransitionTo(S state) { + return onTransitionTo(s -> Objects.equals(s, state)); + } + + /** + * Start defining an {@link Action} that will execute after a transition to any state that passes + * {@code filter}. + * + *

Further criteria for execution will be defined on the returned {@link ActionToBuilder}. + * + * @param filter the filter for states being transitioned to. + * @return an {@link ActionToBuilder}. + */ + public ActionToBuilder onTransitionTo(Predicate filter) { + return new ActionToBuilder<>(filter, transitionActions); + } + + /** + * Start defining an {@link Action} that will execute after a transition from {@code state}. + * + * @param state the state being transitioned from. + * @return an {@link ActionFromBuilder}. + */ + public ActionFromBuilder onTransitionFrom(S state) { + return onTransitionFrom(s -> Objects.equals(s, state)); + } + + /** + * Start defining an {@link Action} that will execute after a transition from any state that + * passes {@code filter}. + * + * @param filter the filter for states being transitioned from. + * @return an {@link ActionFromBuilder}. + */ + public ActionFromBuilder onTransitionFrom(Predicate filter) { + return new ActionFromBuilder<>(filter, transitionActions); + } + + /** + * Add a manually defined {@link Transition}. + * + * @param transition the {@link Transition} to add. + */ + public void addTransition(Transition transition) { + transitions.add(transition); + } + + /** + * Add a manually defined {@link TransitionAction}. + * + * @param transitionAction the {@link TransitionAction} to add. + */ + public void addTransitionAction(TransitionAction transitionAction) { + transitionActions.add(transitionAction); + } + + /** + * Configure an {@link ActionProxy} for the {@link Fsm} instance being built. + * + * @param actionProxy an {@link ActionProxy} for the {@link Fsm} instance being built. + */ + public void setActionProxy(ActionProxy actionProxy) { + this.actionProxy = actionProxy; + } + + public Fsm build(S initialState) { + return new StrictMachine<>( + context, + executor, + actionProxy, + initialState, + new ArrayList<>(transitions), + new ArrayList<>(transitionActions) + ); + } } diff --git a/src/main/java/com/digitalpetri/strictmachine/dsl/GuardBuilder.java b/src/main/java/com/digitalpetri/strictmachine/dsl/GuardBuilder.java index 8ac1678..ff8e3e8 100644 --- a/src/main/java/com/digitalpetri/strictmachine/dsl/GuardBuilder.java +++ b/src/main/java/com/digitalpetri/strictmachine/dsl/GuardBuilder.java @@ -16,35 +16,34 @@ package com.digitalpetri.strictmachine.dsl; +import com.digitalpetri.strictmachine.FsmContext; import java.util.LinkedList; import java.util.Objects; import java.util.function.Predicate; -import com.digitalpetri.strictmachine.FsmContext; - public class GuardBuilder extends ActionBuilder { - private final PredicatedTransition transition; + private final PredicatedTransition transition; - GuardBuilder( - PredicatedTransition transition, - LinkedList> transitionActions - ) { + GuardBuilder( + PredicatedTransition transition, + LinkedList> transitionActions + ) { - super( - transition.getFrom(), - s -> Objects.equals(s, transition.getTarget()), - transition.getVia(), - transitionActions - ); + super( + transition.getFrom(), + s -> Objects.equals(s, transition.getTarget()), + transition.getVia(), + transitionActions + ); - this.transition = transition; - } + this.transition = transition; + } - public ActionBuilder guardedBy(Predicate> guard) { - transition.setGuard(guard); + public ActionBuilder guardedBy(Predicate> guard) { + transition.setGuard(guard); - return this; - } + return this; + } } diff --git a/src/main/java/com/digitalpetri/strictmachine/dsl/PredicatedTransition.java b/src/main/java/com/digitalpetri/strictmachine/dsl/PredicatedTransition.java index 5ee9f0f..d67bed5 100644 --- a/src/main/java/com/digitalpetri/strictmachine/dsl/PredicatedTransition.java +++ b/src/main/java/com/digitalpetri/strictmachine/dsl/PredicatedTransition.java @@ -16,52 +16,51 @@ package com.digitalpetri.strictmachine.dsl; -import java.util.function.Predicate; - import com.digitalpetri.strictmachine.FsmContext; +import java.util.function.Predicate; class PredicatedTransition implements Transition { - private volatile Predicate> guard = ctx -> true; + private volatile Predicate> guard = ctx -> true; - private final Predicate from; - private final Predicate via; - private final S target; + private final Predicate from; + private final Predicate via; + private final S target; - PredicatedTransition(Predicate from, Predicate via, S target) { - this.from = from; - this.via = via; - this.target = target; - } + PredicatedTransition(Predicate from, Predicate via, S target) { + this.from = from; + this.via = via; + this.target = target; + } - @Override - public S target() { - return target; - } + @Override + public S target() { + return target; + } - @Override - public boolean matches(FsmContext ctx, S state, E event) { - return from.test(state) && via.test(event) && guard.test(ctx); - } + @Override + public boolean matches(FsmContext ctx, S state, E event) { + return from.test(state) && via.test(event) && guard.test(ctx); + } - Predicate> getGuard() { - return guard; - } + Predicate> getGuard() { + return guard; + } - Predicate getFrom() { - return from; - } + Predicate getFrom() { + return from; + } - Predicate getVia() { - return via; - } + Predicate getVia() { + return via; + } - S getTarget() { - return target; - } + S getTarget() { + return target; + } - void setGuard(Predicate> guard) { - this.guard = guard; - } + void setGuard(Predicate> guard) { + this.guard = guard; + } } diff --git a/src/main/java/com/digitalpetri/strictmachine/dsl/Transition.java b/src/main/java/com/digitalpetri/strictmachine/dsl/Transition.java index 5a060a9..8fd09ac 100644 --- a/src/main/java/com/digitalpetri/strictmachine/dsl/Transition.java +++ b/src/main/java/com/digitalpetri/strictmachine/dsl/Transition.java @@ -20,22 +20,22 @@ public interface Transition { - /** - * Get the target state of this transition. - * - * @return the target state of this transition. - */ - S target(); + /** + * Get the target state of this transition. + * + * @return the target state of this transition. + */ + S target(); - /** - * Test whether this Transition is applicable for the current {@code state} and {@code event}. - * - * @param ctx the {@link FsmContext}. - * @param state the current FSM state. - * @param event the event being evaluated. - * @return {@code true} if this transition is applicable for {@code state} and {@code event}. - */ - boolean matches(FsmContext ctx, S state, E event); + /** + * Test whether this Transition is applicable for the current {@code state} and {@code event}. + * + * @param ctx the {@link FsmContext}. + * @param state the current FSM state. + * @param event the event being evaluated. + * @return {@code true} if this transition is applicable for {@code state} and {@code event}. + */ + boolean matches(FsmContext ctx, S state, E event); } diff --git a/src/main/java/com/digitalpetri/strictmachine/dsl/TransitionAction.java b/src/main/java/com/digitalpetri/strictmachine/dsl/TransitionAction.java index 5e74c76..5603e93 100644 --- a/src/main/java/com/digitalpetri/strictmachine/dsl/TransitionAction.java +++ b/src/main/java/com/digitalpetri/strictmachine/dsl/TransitionAction.java @@ -18,21 +18,21 @@ public interface TransitionAction { - /** - * Execute the {@link Action}s backing this TransitionAction. - * - * @param context the {@link ActionContext}. - */ - void execute(ActionContext context); + /** + * Execute the {@link Action}s backing this TransitionAction. + * + * @param context the {@link ActionContext}. + */ + void execute(ActionContext context); - /** - * Test whether this TransitionAction is applicable to the transition criteria. - * - * @param from the state transitioned from. - * @param to the state transitioned to. - * @param event the event that caused the transition. - * @return {@code true} if this transition action is applicable to the transition criteria. - */ - boolean matches(S from, S to, E event); + /** + * Test whether this TransitionAction is applicable to the transition criteria. + * + * @param from the state transitioned from. + * @param to the state transitioned to. + * @param event the event that caused the transition. + * @return {@code true} if this transition action is applicable to the transition criteria. + */ + boolean matches(S from, S to, E event); } diff --git a/src/main/java/com/digitalpetri/strictmachine/dsl/TransitionBuilder.java b/src/main/java/com/digitalpetri/strictmachine/dsl/TransitionBuilder.java index 1237408..ecc8ebc 100644 --- a/src/main/java/com/digitalpetri/strictmachine/dsl/TransitionBuilder.java +++ b/src/main/java/com/digitalpetri/strictmachine/dsl/TransitionBuilder.java @@ -22,85 +22,87 @@ public class TransitionBuilder, E> { - private final S from; - private final List> transitions; - private final LinkedList> transitionActions; - - TransitionBuilder( - S from, - List> transitions, - LinkedList> transitionActions - ) { - - this.from = from; - this.transitions = transitions; - this.transitionActions = transitionActions; - } - - /** - * Continue defining a {@link Transition} that is triggered by {@code event}. - * - * @param event the event that triggers this transition. - * @return a {@link TransitionTo}. - */ - public TransitionTo on(E event) { - return to -> { - PredicatedTransition transition = - Transitions.fromInstanceViaInstance(from, event, to); - - transitions.add(transition); - - return new GuardBuilder<>(transition, transitionActions); - }; - } - - /** - * Continue defining a {@link Transition} that is triggered by an event of type {@code eventClass}. - * - * @param eventClass the †ype of event that triggers this transition. - * @return a {@link TransitionTo}. - */ - public TransitionTo on(Class eventClass) { - return to -> { - PredicatedTransition transition = - Transitions.fromInstanceViaClass(from, eventClass, to); - - transitions.add(transition); - - return new GuardBuilder<>(transition, transitionActions); - }; - } - - /** - * Continue defining a {@link Transition} that is triggered by any event that passes {@code eventFilter}. - * - * @param eventFilter the filter for events that trigger this transition. - * @return a {@link TransitionTo}. - */ - public TransitionTo on(Predicate eventFilter) { - return to -> { - PredicatedTransition transition = - Transitions.fromInstanceViaDynamic(from, eventFilter, to); - - transitions.add(transition); - - return new GuardBuilder<>(transition, transitionActions); - }; - } - - /** - * Continue defining a {@link Transition} that is triggered by any event. - * - * @return a {@link TransitionTo}. - */ - public TransitionTo onAny() { - return on(e -> true); - } - - public interface TransitionTo { - - GuardBuilder transitionTo(S state); - - } + private final S from; + private final List> transitions; + private final LinkedList> transitionActions; + + TransitionBuilder( + S from, + List> transitions, + LinkedList> transitionActions + ) { + + this.from = from; + this.transitions = transitions; + this.transitionActions = transitionActions; + } + + /** + * Continue defining a {@link Transition} that is triggered by {@code event}. + * + * @param event the event that triggers this transition. + * @return a {@link TransitionTo}. + */ + public TransitionTo on(E event) { + return to -> { + PredicatedTransition transition = + Transitions.fromInstanceViaInstance(from, event, to); + + transitions.add(transition); + + return new GuardBuilder<>(transition, transitionActions); + }; + } + + /** + * Continue defining a {@link Transition} that is triggered by an event of type + * {@code eventClass}. + * + * @param eventClass the †ype of event that triggers this transition. + * @return a {@link TransitionTo}. + */ + public TransitionTo on(Class eventClass) { + return to -> { + PredicatedTransition transition = + Transitions.fromInstanceViaClass(from, eventClass, to); + + transitions.add(transition); + + return new GuardBuilder<>(transition, transitionActions); + }; + } + + /** + * Continue defining a {@link Transition} that is triggered by any event that passes + * {@code eventFilter}. + * + * @param eventFilter the filter for events that trigger this transition. + * @return a {@link TransitionTo}. + */ + public TransitionTo on(Predicate eventFilter) { + return to -> { + PredicatedTransition transition = + Transitions.fromInstanceViaDynamic(from, eventFilter, to); + + transitions.add(transition); + + return new GuardBuilder<>(transition, transitionActions); + }; + } + + /** + * Continue defining a {@link Transition} that is triggered by any event. + * + * @return a {@link TransitionTo}. + */ + public TransitionTo onAny() { + return on(e -> true); + } + + public interface TransitionTo { + + GuardBuilder transitionTo(S state); + + } } diff --git a/src/main/java/com/digitalpetri/strictmachine/dsl/Transitions.java b/src/main/java/com/digitalpetri/strictmachine/dsl/Transitions.java index b1cf076..fdd7d63 100644 --- a/src/main/java/com/digitalpetri/strictmachine/dsl/Transitions.java +++ b/src/main/java/com/digitalpetri/strictmachine/dsl/Transitions.java @@ -21,30 +21,45 @@ class Transitions { - private Transitions() {} - - static PredicatedTransition fromInstanceViaClass(S state, Class event, S target) { - return new PredicatedTransition<>( - s -> Objects.equals(s, state), - e -> Objects.equals(e.getClass(), event), - target - ); - } - - static PredicatedTransition fromInstanceViaDynamic(S state, Predicate via, S target) { - return new PredicatedTransition<>( - s -> Objects.equals(s, state), - via, - target - ); - } - - static PredicatedTransition fromInstanceViaInstance(S state, E event, S target) { - return new PredicatedTransition<>( - s -> Objects.equals(s, state), - e -> Objects.equals(e, event), - target - ); - } + private Transitions() {} + + static PredicatedTransition fromInstanceViaClass( + S state, + Class event, + S target + ) { + + return new PredicatedTransition<>( + s -> Objects.equals(s, state), + e -> Objects.equals(e.getClass(), event), + target + ); + } + + static PredicatedTransition fromInstanceViaDynamic( + S state, + Predicate via, + S target + ) { + + return new PredicatedTransition<>( + s -> Objects.equals(s, state), + via, + target + ); + } + + static PredicatedTransition fromInstanceViaInstance( + S state, + E event, + S target + ) { + + return new PredicatedTransition<>( + s -> Objects.equals(s, state), + e -> Objects.equals(e, event), + target + ); + } } diff --git a/src/main/java/com/digitalpetri/strictmachine/dsl/ViaBuilder.java b/src/main/java/com/digitalpetri/strictmachine/dsl/ViaBuilder.java index aa20cbb..186912f 100644 --- a/src/main/java/com/digitalpetri/strictmachine/dsl/ViaBuilder.java +++ b/src/main/java/com/digitalpetri/strictmachine/dsl/ViaBuilder.java @@ -22,55 +22,55 @@ public class ViaBuilder { - private final Predicate fromFilter; - private final Predicate toFilter; - private final LinkedList> transitionActions; + private final Predicate fromFilter; + private final Predicate toFilter; + private final LinkedList> transitionActions; - ViaBuilder( - Predicate fromFilter, - Predicate toFilter, - LinkedList> transitionActions - ) { + ViaBuilder( + Predicate fromFilter, + Predicate toFilter, + LinkedList> transitionActions + ) { - this.fromFilter = fromFilter; - this.toFilter = toFilter; - this.transitionActions = transitionActions; - } + this.fromFilter = fromFilter; + this.toFilter = toFilter; + this.transitionActions = transitionActions; + } - public ActionBuilder via(E event) { - return new ActionBuilder<>( - fromFilter, - toFilter, - e -> Objects.equals(e, event), - transitionActions - ); - } + public ActionBuilder via(E event) { + return new ActionBuilder<>( + fromFilter, + toFilter, + e -> Objects.equals(e, event), + transitionActions + ); + } - public ActionBuilder via(Class eventClass) { - return new ActionBuilder<>( - fromFilter, - toFilter, - e -> Objects.equals(e.getClass(), eventClass), - transitionActions - ); - } + public ActionBuilder via(Class eventClass) { + return new ActionBuilder<>( + fromFilter, + toFilter, + e -> Objects.equals(e.getClass(), eventClass), + transitionActions + ); + } - public ActionBuilder via(Predicate eventFilter) { - return new ActionBuilder<>( - fromFilter, - toFilter, - eventFilter, - transitionActions - ); - } + public ActionBuilder via(Predicate eventFilter) { + return new ActionBuilder<>( + fromFilter, + toFilter, + eventFilter, + transitionActions + ); + } - public ActionBuilder viaAny() { - return new ActionBuilder<>( - fromFilter, - toFilter, - e -> true, - transitionActions - ); - } + public ActionBuilder viaAny() { + return new ActionBuilder<>( + fromFilter, + toFilter, + e -> true, + transitionActions + ); + } } diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/ActionBuilderTest.java b/src/test/java/com/digitalpetri/strictmachine/dsl/ActionBuilderTest.java new file mode 100644 index 0000000..995c9b9 --- /dev/null +++ b/src/test/java/com/digitalpetri/strictmachine/dsl/ActionBuilderTest.java @@ -0,0 +1,30 @@ +package com.digitalpetri.strictmachine.dsl; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import org.junit.jupiter.api.Test; + +class ActionBuilderTest { + + @Test + void actionsAreExecutedInOrder() throws InterruptedException { + var fb = new FsmBuilder(); + + var executed = new ArrayList(); + + fb.when(State.S1) + .on(Event.E1.class) + .transitionTo(State.S2) + .execute(ctx -> executed.add(0)) + .executeLast(ctx -> executed.add(1)) + .executeFirst(ctx -> executed.add(2)); + + fb.build(State.S1).fireEventBlocking(new Event.E1()); + + assertEquals(2, (int) executed.get(0)); + assertEquals(0, (int) executed.get(1)); + assertEquals(1, (int) executed.get(2)); + } + +} diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/ActionBuilderTest.kt b/src/test/java/com/digitalpetri/strictmachine/dsl/ActionBuilderTest.kt deleted file mode 100644 index 19cd4f2..0000000 --- a/src/test/java/com/digitalpetri/strictmachine/dsl/ActionBuilderTest.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.digitalpetri.strictmachine.dsl - -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test - -class ActionBuilderTest { - - @Test - fun `Actions are executed in order`() { - val fb = FsmBuilder() - - val executed = mutableListOf() - - fb.`when`(State.S1) - .on(Event.E1) - .transitionTo(State.S2) - .execute { executed.add(0) } - .executeLast { executed.add(1) } - .executeFirst { executed.add(2) } - - fb.build(State.S1).fireEventBlocking(Event.E1) - - Assertions.assertTrue(executed[0] == 2) - Assertions.assertTrue(executed[1] == 0) - Assertions.assertTrue(executed[2] == 1) - } - -} diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/ActionFromBuilderTest.java b/src/test/java/com/digitalpetri/strictmachine/dsl/ActionFromBuilderTest.java new file mode 100644 index 0000000..750eb8b --- /dev/null +++ b/src/test/java/com/digitalpetri/strictmachine/dsl/ActionFromBuilderTest.java @@ -0,0 +1,48 @@ +package com.digitalpetri.strictmachine.dsl; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import org.junit.jupiter.api.Test; + +class ActionFromBuilderTest { + + @Test + void actionFromBuilder_toInstance() throws InterruptedException { + assertActionExecuted(afb -> afb.to(State.S2)); + } + + @Test + void actionFromBuilder_toPredicate() throws InterruptedException { + assertActionExecuted(afb -> afb.to(s -> s == State.S2)); + } + + @Test + void actionFromBuilder_toAny() throws InterruptedException { + assertActionExecuted(ActionFromBuilder::toAny); + } + + private void assertActionExecuted( + Function, ViaBuilder> f + ) throws InterruptedException { + var fb = new FsmBuilder(); + + fb.when(State.S1) + .on(Event.E1.class) + .transitionTo(State.S2); + + var executed = new AtomicBoolean(false); + + ActionFromBuilder afb = fb.onTransitionFrom(State.S1); + ViaBuilder viaBuilder = f.apply(afb); + + viaBuilder.via(Event.E1.class) + .execute(ctx -> executed.set(true)); + + fb.build(State.S1).fireEventBlocking(new Event.E1()); + + assertTrue(executed.get()); + } + +} diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/ActionFromBuilderTest.kt b/src/test/java/com/digitalpetri/strictmachine/dsl/ActionFromBuilderTest.kt deleted file mode 100644 index ac39bd7..0000000 --- a/src/test/java/com/digitalpetri/strictmachine/dsl/ActionFromBuilderTest.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2018 Kevin Herron - * - * 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.digitalpetri.strictmachine.dsl - -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test -import java.util.concurrent.atomic.AtomicBoolean - - -class ActionFromBuilderTest { - - @Test - fun `Action built with onTransitionFrom using to Instance`() { - testOnTransitionFrom { - to(State.S2) - } - } - - @Test - fun `Action built with onTransitionFrom using to Predicate`() { - testOnTransitionFrom { - to { s -> s == State.S2 } - } - } - - @Test - fun `Action built with onTransitionFrom using to Any`() { - testOnTransitionFrom { - toAny() - } - } - - private fun testOnTransitionFrom(configureTo: ActionFromBuilder.() -> ViaBuilder) { - val fb = FsmBuilder() - - fb.`when`(State.S1) - .on(Event.E1) - .transitionTo(State.S2) - - val executed = AtomicBoolean(false) - - val actionFromBuilder = fb.onTransitionFrom(State.S1) - val viaBuilder = actionFromBuilder.configureTo() - - viaBuilder.via(Event.E1).execute { executed.set(true) } - - fb.build(State.S1).fireEventBlocking(Event.E1) - - assertTrue(executed.get()) { - "expected execute callback to set executed=true" - } - } - -} diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/ActionProxyTest.java b/src/test/java/com/digitalpetri/strictmachine/dsl/ActionProxyTest.java new file mode 100644 index 0000000..fb0e114 --- /dev/null +++ b/src/test/java/com/digitalpetri/strictmachine/dsl/ActionProxyTest.java @@ -0,0 +1,51 @@ +package com.digitalpetri.strictmachine.dsl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class ActionProxyTest { + + @Test + void actionProxyGetsCalled() throws InterruptedException { + var fb = new FsmBuilder(); + + var state3Latch = new CountDownLatch(1); + + fb.when(State.S1) + .on(Event.E1.class) + .transitionTo(State.S2) + .execute(ctx -> { + ctx.fireEvent(new Event.E2()); + ctx.fireEvent(new Event.E3()); + }); + + fb.when(State.S2) + .on(Event.E2.class) + .transitionTo(State.S3); + + fb.when(State.S3) + .on(Event.E3.class) + .transitionTo(State.S1) + .execute(ctx -> state3Latch.countDown()); + + var actionProxyLatch = new CountDownLatch(2); + + fb.setActionProxy((ctx, action) -> { + actionProxyLatch.countDown(); + action.execute(ctx); + }); + + var fsm = fb.build(State.S1); + + fsm.fireEvent(new Event.E1()); + + assertTrue(state3Latch.await(5, TimeUnit.SECONDS)); + assertTrue(actionProxyLatch.await(5, TimeUnit.SECONDS)); + assertEquals(State.S1, fsm.getState()); + } + +} diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/ActionProxyTest.kt b/src/test/java/com/digitalpetri/strictmachine/dsl/ActionProxyTest.kt deleted file mode 100644 index b970b6d..0000000 --- a/src/test/java/com/digitalpetri/strictmachine/dsl/ActionProxyTest.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2019 Kevin Herron - * - * 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.digitalpetri.strictmachine.dsl - -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit - -class ActionProxyTest { - - @Test - fun `configured ActionProxy gets called`() { - val fb = FsmBuilder() - - val actionProxyLatch = CountDownLatch(2) - val s3Latch = CountDownLatch(1) - - fb.`when`(State.S1) - .on(Event.E1) - .transitionTo(State.S2) - .execute { ctx -> - ctx.fireEvent(Event.E2) - ctx.fireEvent(Event.E3) - } - - fb.`when`(State.S2) - .on(Event.E2) - .transitionTo(State.S3) - - fb.`when`(State.S3) - .on(Event.E3) - .transitionTo(State.S1) - .execute { - s3Latch.countDown() - } - - fb.setActionProxy { ctx, action -> - actionProxyLatch.countDown() - - action.execute(ctx) - } - - val fsm = fb.build(State.S1) - - fsm.fireEvent(Event.E1) - - s3Latch.await(5, TimeUnit.SECONDS) - actionProxyLatch.await(5, TimeUnit.SECONDS) - Assertions.assertEquals(fsm.state, State.S1) - } -} diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/ActionToBuilderTest.java b/src/test/java/com/digitalpetri/strictmachine/dsl/ActionToBuilderTest.java new file mode 100644 index 0000000..f7f0904 --- /dev/null +++ b/src/test/java/com/digitalpetri/strictmachine/dsl/ActionToBuilderTest.java @@ -0,0 +1,49 @@ +package com.digitalpetri.strictmachine.dsl; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("MethodName") +class ActionToBuilderTest { + + @Test + void actionToBuilder_fromInstance() throws InterruptedException { + assertActionExecuted(atb -> atb.from(State.S1)); + } + + @Test + void actionToBuilder_fromPredicate() throws InterruptedException { + assertActionExecuted(atb -> atb.from(s -> s == State.S1)); + } + + @Test + void actionToBuilder_fromAny() throws InterruptedException { + assertActionExecuted(ActionToBuilder::fromAny); + } + + private void assertActionExecuted( + Function, ViaBuilder> f + ) throws InterruptedException { + var fb = new FsmBuilder(); + + fb.when(State.S1) + .on(Event.E1.class) + .transitionTo(State.S2); + + var executed = new AtomicBoolean(false); + + ActionToBuilder atb = fb.onTransitionTo(State.S2); + ViaBuilder viaBuilder = f.apply(atb); + + viaBuilder.via(Event.E1.class) + .execute(ctx -> executed.set(true)); + + fb.build(State.S1).fireEventBlocking(new Event.E1()); + + assertTrue(executed.get()); + } + +} diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/ActionToBuilderTest.kt b/src/test/java/com/digitalpetri/strictmachine/dsl/ActionToBuilderTest.kt deleted file mode 100644 index a32569c..0000000 --- a/src/test/java/com/digitalpetri/strictmachine/dsl/ActionToBuilderTest.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2018 Kevin Herron - * - * 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.digitalpetri.strictmachine.dsl - -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test -import java.util.concurrent.atomic.AtomicBoolean - - -class ActionToBuilderTest { - - @Test - fun `Action built with onTransitionTo using from Instance`() { - testOnTransitionTo { - from(State.S1) - } - } - - @Test - fun `Action built with onTransitionTo using from Predicate`() { - testOnTransitionTo { - from { s -> s == State.S1 } - } - } - - @Test - fun `Action built with onTransitionTo using from Any`() { - testOnTransitionTo { - fromAny() - } - } - - private fun testOnTransitionTo(configureFrom: ActionToBuilder.() -> ViaBuilder) { - val fb = FsmBuilder() - - fb.`when`(State.S1) - .on(Event.E1) - .transitionTo(State.S2) - - val executed = AtomicBoolean(false) - - val actionToBuilder = fb.onTransitionTo(State.S2) - val viaBuilder = actionToBuilder.configureFrom() - - viaBuilder.via(Event.E1).execute { executed.set(true) } - - fb.build(State.S1).fireEventBlocking(Event.E1) - - Assertions.assertTrue(executed.get()) { - "expected execute callback to set executed=true" - } - } - - -} diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/Event.java b/src/test/java/com/digitalpetri/strictmachine/dsl/Event.java new file mode 100644 index 0000000..c8c10ec --- /dev/null +++ b/src/test/java/com/digitalpetri/strictmachine/dsl/Event.java @@ -0,0 +1,45 @@ +package com.digitalpetri.strictmachine.dsl; + +abstract class Event { + + static class E1 extends Event { + + static E1 INSTANCE = new E1(); + + @Override + public String toString() { + return "E1"; + } + } + + static class E2 extends Event { + + static E2 INSTANCE = new E2(); + + @Override + public String toString() { + return "E2"; + } + } + + static class E3 extends Event { + + static E3 INSTANCE = new E3(); + + @Override + public String toString() { + return "E3"; + } + } + + static class E4 extends Event { + + static E4 INSTANCE = new E4(); + + @Override + public String toString() { + return "E4"; + } + } + +} diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/FsmTest.kt b/src/test/java/com/digitalpetri/strictmachine/dsl/FsmTest.kt deleted file mode 100644 index 74f44cf..0000000 --- a/src/test/java/com/digitalpetri/strictmachine/dsl/FsmTest.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2018 Kevin Herron - * - * 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.digitalpetri.strictmachine.dsl - - -enum class State { S1, S2, S3, S4 } - -sealed class Event { - object E1 : Event() { - override fun toString(): String = "E1" - } - - object E2 : Event() { - override fun toString(): String = "E2" - } - - object E3 : Event() { - override fun toString(): String = "E3" - } - - object E4 : Event() { - override fun toString(): String = "E4" - } -} diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/GuardBuilderTest.java b/src/test/java/com/digitalpetri/strictmachine/dsl/GuardBuilderTest.java new file mode 100644 index 0000000..be8b0b9 --- /dev/null +++ b/src/test/java/com/digitalpetri/strictmachine/dsl/GuardBuilderTest.java @@ -0,0 +1,48 @@ +package com.digitalpetri.strictmachine.dsl; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.digitalpetri.strictmachine.FsmContext; +import java.util.LinkedList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; +import org.junit.jupiter.api.Test; + +class GuardBuilderTest { + + @Test + void guardBuilder() { + var transition = new PredicatedTransition( + s -> s == State.S1, + e -> e instanceof Event.E1, + State.S2 + ); + + var actions = new LinkedList>(); + var guardBuilder = new GuardBuilder<>(transition, actions); + Predicate> guard = ctx -> true; + + guardBuilder.guardedBy(guard); + + assertEquals(guard, transition.getGuard()); + } + + @Test + void guardedTransition() throws InterruptedException { + var fb = new FsmBuilder(); + + var guardCondition = new AtomicBoolean(false); + + fb.when(State.S1) + .on(Event.E1.class) + .transitionTo(State.S2) + .guardedBy(ctx -> guardCondition.get()); + + var fsm = fb.build(State.S1); + + assertEquals(State.S1, fsm.fireEventBlocking(new Event.E1())); + guardCondition.set(true); + assertEquals(State.S2, fsm.fireEventBlocking(new Event.E1())); + } + +} diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/GuardBuilderTest.kt b/src/test/java/com/digitalpetri/strictmachine/dsl/GuardBuilderTest.kt deleted file mode 100644 index cd2297f..0000000 --- a/src/test/java/com/digitalpetri/strictmachine/dsl/GuardBuilderTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2018 Kevin Herron - * - * 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.digitalpetri.strictmachine.dsl - -import com.digitalpetri.strictmachine.FsmContext -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import java.util.* -import java.util.concurrent.atomic.AtomicBoolean -import java.util.function.Predicate - - -class GuardBuilderTest { - - @Test - fun `GuardBuilder#guardedBy() sets the guard condition on the transition`() { - val transition = PredicatedTransition( - { s -> s == State.S1 }, - { e -> e == Event.E1 }, - State.S2 - ) - val transitionActions = LinkedList>() - - val gb = GuardBuilder(transition, transitionActions) - - val guard: Predicate> = Predicate { true } - - gb.guardedBy(guard) - - assertEquals(guard, transition.guard) - } - - @Test - fun `Transition only occurs when the guard condition is met`() { - val fb = FsmBuilder() - - val guard = AtomicBoolean(false) - - fb.`when`(State.S1) - .on(Event.E1) - .transitionTo(State.S2) - .guardedBy { guard.get() } - - val fsm = fb.build(State.S1) - - assertEquals(State.S1, fsm.fireEventBlocking(Event.E1)) - - guard.set(true) - - assertEquals(State.S2, fsm.fireEventBlocking(Event.E1)) - } - -} diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/State.java b/src/test/java/com/digitalpetri/strictmachine/dsl/State.java new file mode 100644 index 0000000..e03c8ab --- /dev/null +++ b/src/test/java/com/digitalpetri/strictmachine/dsl/State.java @@ -0,0 +1,5 @@ +package com.digitalpetri.strictmachine.dsl; + +enum State { + S1, S2, S3, S4 +} diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/StrictMachineTest.java b/src/test/java/com/digitalpetri/strictmachine/dsl/StrictMachineTest.java new file mode 100644 index 0000000..fa0a8c6 --- /dev/null +++ b/src/test/java/com/digitalpetri/strictmachine/dsl/StrictMachineTest.java @@ -0,0 +1,81 @@ +package com.digitalpetri.strictmachine.dsl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.digitalpetri.strictmachine.FsmContext; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class StrictMachineTest { + + @Test + void eventsFiredInExecuteCallbacks() throws InterruptedException { + var fb = new FsmBuilder(); + + var latch = new CountDownLatch(1); + + fb.when(State.S1) + .on(Event.E1.class) + .transitionTo(State.S2) + .execute(ctx -> { + ctx.fireEvent(new Event.E2()); + ctx.fireEvent(new Event.E3()); + }); + + fb.when(State.S2) + .on(Event.E2.class) + .transitionTo(State.S3); + + fb.when(State.S3) + .on(Event.E3.class) + .transitionTo(State.S1) + .execute(ctx -> latch.countDown()); + + var fsm = fb.build(State.S1); + fsm.fireEvent(new Event.E1()); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertEquals(State.S1, fsm.getState()); + } + + @Test + void eventShelving() throws InterruptedException { + var fb = new FsmBuilder(); + + fb.when(State.S1) + .on(Event.E1.class) + .transitionTo(State.S2); + + fb.onInternalTransition(State.S1) + .via(Event.E2.class) + .execute(ctx -> ctx.shelveEvent(ctx.event())); + + fb.onTransitionFrom(State.S1) + .to(s -> s != State.S1) + .viaAny() + .execute(FsmContext::processShelvedEvents); + + fb.when(State.S2) + .on(Event.E2.class) + .transitionTo(State.S3); + + fb.when(State.S3) + .on(Event.E3.class) + .transitionTo(State.S4); + + var fsm = fb.build(State.S1); + + // fire an E2 that gets shelved + fsm.fireEventBlocking(new Event.E2()); + + // fire E1 to trigger S1 -> S2 + fsm.fireEventBlocking(new Event.E1()); + + // fsm should have processed event shelf and landed in S3. + // now move to S4 via E3 and check the result. + assertEquals(State.S4, fsm.fireEventBlocking(new Event.E3())); + } + +} diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/StrictMachineTest.kt b/src/test/java/com/digitalpetri/strictmachine/dsl/StrictMachineTest.kt deleted file mode 100644 index 059673d..0000000 --- a/src/test/java/com/digitalpetri/strictmachine/dsl/StrictMachineTest.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2018 Kevin Herron - * - * 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.digitalpetri.strictmachine.dsl - -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import java.util.concurrent.CountDownLatch - - -class StrictMachineTest { - - @Test - fun `Events fired in execute callbacks evaluate correctly`() { - val fb = FsmBuilder() - - val latch = CountDownLatch(1) - - fb.`when`(State.S1) - .on(Event.E1) - .transitionTo(State.S2) - .execute { ctx -> - ctx.fireEvent(Event.E2) - ctx.fireEvent(Event.E3) - } - - fb.`when`(State.S2) - .on(Event.E2) - .transitionTo(State.S3) - - fb.`when`(State.S3) - .on(Event.E3) - .transitionTo(State.S1) - .execute { - latch.countDown() - } - - val fsm = fb.build(State.S1) - - fsm.fireEvent(Event.E1) - - latch.await() - assertEquals(fsm.state, State.S1) - } - - @Test - fun `Events can be shelved and un-shelved`() { - val fb = FsmBuilder() - - fb.`when`(State.S1) - .on(Event.E1) - .transitionTo(State.S2) - - // on an internal transition via E2 shelve that event - fb.onInternalTransition(State.S1) - .via(Event.E2) - .execute { ctx -> ctx.shelveEvent(ctx.event()) } - - // on an external transition from S1, process any events shelved while in S1 - fb.onTransitionFrom(State.S1) - .to { s -> s != State.S1 } - .viaAny() - .execute { ctx -> ctx.processShelvedEvents() } - - fb.`when`(State.S2) - .on(Event.E2) - .transitionTo(State.S3) - - fb.`when`(State.S3) - .on(Event.E3) - .transitionTo(State.S4) - - - val fsm = fb.build(State.S1) - - // fire an E2 that gets shelved - fsm.fireEventBlocking(Event.E2) - - // fire E1 to trigger S1 -> S2 - fsm.fireEventBlocking(Event.E1) - - // fsm should have processed event shelf and landed in S3. - // now move it to S4 via E3 and check the result. - assertEquals(State.S4, fsm.fireEventBlocking(Event.E3)) - } - -} diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/TransitionBuilderTest.java b/src/test/java/com/digitalpetri/strictmachine/dsl/TransitionBuilderTest.java new file mode 100644 index 0000000..e74ba33 --- /dev/null +++ b/src/test/java/com/digitalpetri/strictmachine/dsl/TransitionBuilderTest.java @@ -0,0 +1,84 @@ +package com.digitalpetri.strictmachine.dsl; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class TransitionBuilderTest { + + @Test + void transitionFromEventInstance() throws InterruptedException { + var fb = new FsmBuilder(); + + fb.when(State.S1) + .on(Event.E1.INSTANCE) + .transitionTo(State.S2); + + assertEquals(State.S2, fb.build(State.S1).fireEventBlocking(Event.E1.INSTANCE)); + + // internal transitions + assertEquals(State.S1, fb.build(State.S1).fireEventBlocking(Event.E2.INSTANCE)); + assertEquals(State.S1, fb.build(State.S1).fireEventBlocking(Event.E3.INSTANCE)); + } + + @Test + void transitionFromEventClass() throws InterruptedException { + var fb = new FsmBuilder(); + + fb.when(State.S1) + .on(Event.E1.class) + .transitionTo(State.S2); + + assertEquals(State.S2, fb.build(State.S1).fireEventBlocking(new Event.E1())); + + // internal transitions + assertEquals(State.S1, fb.build(State.S1).fireEventBlocking(new Event.E2())); + assertEquals(State.S1, fb.build(State.S1).fireEventBlocking(new Event.E3())); + } + + @Test + void transitionFromPredicate() throws InterruptedException { + var fb = new FsmBuilder(); + + fb.when(State.S1) + .on(e -> e instanceof Event.E1 || e instanceof Event.E2) + .transitionTo(State.S2); + + assertEquals(State.S2, fb.build(State.S1).fireEventBlocking(new Event.E1())); + assertEquals(State.S2, fb.build(State.S1).fireEventBlocking(new Event.E2())); + + // internal transitions + assertEquals(State.S1, fb.build(State.S1).fireEventBlocking(new Event.E3())); + } + + @Test + void transitionFromOnAny() throws InterruptedException { + var fb = new FsmBuilder(); + + fb.when(State.S1) + .onAny() + .transitionTo(State.S2); + + assertEquals(State.S2, fb.build(State.S1).fireEventBlocking(new Event.E1())); + assertEquals(State.S2, fb.build(State.S1).fireEventBlocking(new Event.E2())); + assertEquals(State.S2, fb.build(State.S1).fireEventBlocking(new Event.E3())); + assertEquals(State.S2, fb.build(State.S1).fireEventBlocking(new Event.E4())); + } + + @Test + void firstOfMultipleWins() throws InterruptedException { + var fb = new FsmBuilder(); + + fb.when(State.S1) + .on(Event.E1.class) + .transitionTo(State.S3); + + // first definition "wins", this should not result in S2 + fb.when(State.S1) + .on(Event.E1.class) + .transitionTo(State.S2); + + assertEquals(State.S3, fb.build(State.S1).fireEventBlocking(new Event.E1())); + } + +} diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/TransitionBuilderTest.kt b/src/test/java/com/digitalpetri/strictmachine/dsl/TransitionBuilderTest.kt deleted file mode 100644 index 8147db4..0000000 --- a/src/test/java/com/digitalpetri/strictmachine/dsl/TransitionBuilderTest.kt +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright 2018 Kevin Herron - * - * 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.digitalpetri.strictmachine.dsl - -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - - -class TransitionBuilderTest { - - @Test - fun `Transition built from Event instance`() { - val fb = FsmBuilder() - - fb.`when`(State.S1) - .on(Event.E1) - .transitionTo(State.S2) - - assertEquals( - State.S2, fb.build(State.S1).fireEventBlocking( - Event.E1 - ) - ) - - // internal transitions - assertEquals( - State.S1, fb.build(State.S1).fireEventBlocking( - Event.E2 - ) - ) - assertEquals( - State.S1, fb.build(State.S1).fireEventBlocking( - Event.E3 - ) - ) - } - - @Test - fun `Transition built from Event class`() { - val fb = FsmBuilder() - - fb.`when`(State.S1) - .on(Event.E1::class.java) - .transitionTo(State.S2) - - assertEquals( - State.S2, fb.build(State.S1).fireEventBlocking( - Event.E1 - ) - ) - - // internal transitions - assertEquals( - State.S1, fb.build(State.S1).fireEventBlocking( - Event.E2 - ) - ) - assertEquals( - State.S1, fb.build(State.S1).fireEventBlocking( - Event.E3 - ) - ) - } - - @Test - fun `Transition built from Predicate`() { - val fb = FsmBuilder() - - fb.`when`(State.S1) - .on { e -> e == Event.E1 || e == Event.E2 } - .transitionTo(State.S2) - - assertEquals( - State.S2, fb.build(State.S1).fireEventBlocking( - Event.E1 - ) - ) - assertEquals( - State.S2, fb.build(State.S1).fireEventBlocking( - Event.E2 - ) - ) - - // internal transition - assertEquals( - State.S1, fb.build(State.S1).fireEventBlocking( - Event.E3 - ) - ) - } - - @Test - fun `Transition built from onAny()`() { - val fb = FsmBuilder() - - fb.`when`(State.S1) - .onAny() - .transitionTo(State.S2) - - fb.build(State.S1).apply { - assertEquals(State.S2, fireEventBlocking(Event.E1)) - assertEquals(State.S2, state) - } - fb.build(State.S1).apply { - assertEquals(State.S2, fireEventBlocking(Event.E2)) - assertEquals(State.S2, state) - } - fb.build(State.S1).apply { - assertEquals(State.S2, fireEventBlocking(Event.E3)) - assertEquals(State.S2, state) - } - } - - @Test - fun `First defined Transition of multiple wins`() { - val fb = FsmBuilder() - - fb.`when`(State.S1) - .on(Event.E1) - .transitionTo(State.S3) - - fb.`when`(State.S1) - .on(Event.E1) - .transitionTo(State.S2) - - assertEquals( - State.S3, fb.build(State.S1).fireEventBlocking( - Event.E1 - ) - ) - } - -} diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/ViaBuilderTest.java b/src/test/java/com/digitalpetri/strictmachine/dsl/ViaBuilderTest.java new file mode 100644 index 0000000..734d3f2 --- /dev/null +++ b/src/test/java/com/digitalpetri/strictmachine/dsl/ViaBuilderTest.java @@ -0,0 +1,94 @@ +package com.digitalpetri.strictmachine.dsl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; + +public class ViaBuilderTest { + + @Test + void actionBuiltWithViaInstance() throws InterruptedException { + var fb = new FsmBuilder(); + + fb.when(State.S1) + .on(Event.E1.INSTANCE) + .transitionTo(State.S2); + + var executed = new AtomicBoolean(false); + + fb.onTransitionFrom(State.S1) + .to(State.S2) + .via(Event.E1.INSTANCE) + .execute(ctx -> executed.set(true)); + + assertEquals(State.S2, fb.build(State.S1).fireEventBlocking(Event.E1.INSTANCE)); + + assertTrue(executed.get()); + } + + @Test + void actionBuiltWithViaClass() throws InterruptedException { + var fb = new FsmBuilder(); + + fb.when(State.S1) + .on(Event.E1.INSTANCE) + .transitionTo(State.S2); + + var executed = new AtomicBoolean(false); + + fb.onTransitionFrom(State.S1) + .to(State.S2) + .via(Event.E1.class) + .execute(ctx -> executed.set(true)); + + assertEquals(State.S2, fb.build(State.S1).fireEventBlocking(Event.E1.INSTANCE)); + + assertTrue(executed.get()); + } + + @Test + void actionBuiltWithViaPredicate() throws InterruptedException { + var fb = new FsmBuilder(); + + fb.when(State.S1) + .on(Event.E1.INSTANCE) + .transitionTo(State.S2); + + var executed = new AtomicBoolean(false); + + fb.onTransitionFrom(State.S1) + .to(State.S2) + .via(e -> e instanceof Event.E1) + .execute(ctx -> executed.set(true)); + + assertEquals(State.S2, fb.build(State.S1).fireEventBlocking(Event.E1.INSTANCE)); + + assertTrue(executed.get()); + } + + @Test + void actionBuiltWithViaAny() throws InterruptedException { + var fb = new FsmBuilder(); + + fb.when(State.S1) + .onAny() + .transitionTo(State.S2); + + var executed = new AtomicBoolean(false); + + fb.onTransitionFrom(State.S1) + .to(State.S2) + .viaAny() + .execute(ctx -> executed.set(true)); + + for (Event event : List.of(new Event.E1(), new Event.E2(), new Event.E3())) { + executed.set(false); + assertEquals(State.S2, fb.build(State.S1).fireEventBlocking(event)); + assertTrue(executed.get()); + } + } + +} diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/ViaBuilderTest.kt b/src/test/java/com/digitalpetri/strictmachine/dsl/ViaBuilderTest.kt deleted file mode 100644 index 2a87dac..0000000 --- a/src/test/java/com/digitalpetri/strictmachine/dsl/ViaBuilderTest.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2018 Kevin Herron - * - * 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.digitalpetri.strictmachine.dsl - -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test -import java.util.concurrent.atomic.AtomicBoolean - -class ViaBuilderTest { - - @Test - fun `Action built with via Instance`() { - val fb = FsmBuilder() - - fb.`when`(State.S1) - .on(Event.E1) - .transitionTo(State.S2) - - val executed = AtomicBoolean(false) - - fb.onTransitionFrom(State.S1) - .to(State.S2) - .via(Event.E1) - .execute { executed.set(true) } - - fb.build(State.S1).fireEventBlocking(Event.E1) - - assertTrue(executed.get()) { - "expected execute callback to set executed=true" - } - } - - @Test - fun `Action built with via Class`() { - val fb = FsmBuilder() - - fb.`when`(State.S1) - .on(Event.E1) - .transitionTo(State.S2) - - val executed = AtomicBoolean(false) - - fb.onTransitionFrom(State.S1) - .to(State.S2) - .via(Event.E1::class.java) - .execute { executed.set(true) } - - fb.build(State.S1).fireEventBlocking(Event.E1) - - assertTrue(executed.get()) { - "expected execute callback to set executed=true" - } - } - - @Test - fun `Action built with via Predicate`() { - val fb = FsmBuilder() - - fb.`when`(State.S1) - .on(Event.E1) - .transitionTo(State.S2) - - val executed = AtomicBoolean(false) - - fb.onTransitionFrom(State.S1) - .to(State.S2) - .via { e -> e == Event.E1 } - .execute { executed.set(true) } - - fb.build(State.S1).fireEventBlocking(Event.E1) - - assertTrue(executed.get()) { - "expected execute callback to set executed=true" - } - } - - @Test - fun `Action built with via Any`() { - val fb = FsmBuilder() - - fb.`when`(State.S1) - .onAny() - .transitionTo(State.S2) - - val executed = AtomicBoolean(false) - - fb.onTransitionFrom(State.S1) - .to(State.S2) - .viaAny() - .execute { executed.set(true) } - - for (event in listOf( - Event.E1, - Event.E2, - Event.E3, - Event.E4 - )) { - executed.set(false) - - fb.build(State.S1).fireEventBlocking(event) - - assertTrue(executed.get()) { - "expected execute callback to set executed=true" - } - } - } - -} diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/atm/AtmFsm.java b/src/test/java/com/digitalpetri/strictmachine/dsl/atm/AtmFsm.java index b2a5cdb..9955ea3 100644 --- a/src/test/java/com/digitalpetri/strictmachine/dsl/atm/AtmFsm.java +++ b/src/test/java/com/digitalpetri/strictmachine/dsl/atm/AtmFsm.java @@ -16,113 +16,112 @@ package com.digitalpetri.strictmachine.dsl.atm; -import java.util.function.Consumer; -import java.util.function.Function; - import com.digitalpetri.strictmachine.Fsm; import com.digitalpetri.strictmachine.dsl.FsmBuilder; +import java.util.function.Consumer; +import java.util.function.Function; public class AtmFsm { - private final Fsm fsm; - - private AtmFsm(Fsm fsm) { - this.fsm = fsm; - } - - void fireEvent(Event event, Consumer stateConsumer) { - fsm.fireEvent(event, stateConsumer); - } - - State fireEventBlocking(Event event) throws InterruptedException { - return fsm.fireEventBlocking(event); - } - - enum State { - Idle, - Loading, - OutOfService, - InService, - Disconnected - } - - enum Event { - Connected, - ConnectionClosed, - ConnectionLost, - ConnectionRestored, - LoadFail, - LoadSuccess, - Shutdown, - Startup - } - - /** - * Create a new {@link AtmFsm} in {@link State#Idle}. - * - * @return a new {@link AtmFsm} in {@link State#Idle}. - */ - public static AtmFsm newAtmFsm() { - return buildFsm(fb -> State.Idle); - } - - /** - * Build an {@link AtmFsm}. - *

- * {@code builderStateFunction} may make modifications to the FSM before it's built via the builder and returns the - * desired initial state. - * - * @param builderStateFunction invoked after the builder has set up all state transitions. Returns the desired - * initial state of the FSM. - * @return an {@link AtmFsm}. - */ - static AtmFsm buildFsm(Function, State> builderStateFunction) { - FsmBuilder fb = new FsmBuilder<>(); - - /* Idle */ - fb.when(State.Idle) - .on(Event.Connected) - .transitionTo(State.Loading); - - /* Loading */ - fb.when(State.Loading) - .on(Event.LoadSuccess) - .transitionTo(State.InService); - - fb.when(State.Loading) - .on(Event.LoadFail) - .transitionTo(State.OutOfService); - - fb.when(State.Loading) - .on(Event.ConnectionClosed) - .transitionTo(State.Disconnected); - - /* OutOfService */ - fb.when(State.OutOfService) - .on(Event.Startup) - .transitionTo(State.InService); - - fb.when(State.OutOfService) - .on(Event.ConnectionLost) - .transitionTo(State.Disconnected); - - /* InService */ - fb.when(State.InService) - .on(Event.ConnectionLost) - .transitionTo(State.Disconnected); - - fb.when(State.InService) - .on(Event.Shutdown) - .transitionTo(State.OutOfService); - - /* Disconnected */ - fb.when(State.Disconnected) - .on(Event.ConnectionRestored) - .transitionTo(State.InService); - - State initialState = builderStateFunction.apply(fb); - - return new AtmFsm(fb.build(initialState)); - } + private final Fsm fsm; + + private AtmFsm(Fsm fsm) { + this.fsm = fsm; + } + + void fireEvent(Event event, Consumer stateConsumer) { + fsm.fireEvent(event, stateConsumer); + } + + State fireEventBlocking(Event event) throws InterruptedException { + return fsm.fireEventBlocking(event); + } + + enum State { + Idle, + Loading, + OutOfService, + InService, + Disconnected + } + + enum Event { + Connected, + ConnectionClosed, + ConnectionLost, + ConnectionRestored, + LoadFail, + LoadSuccess, + Shutdown, + Startup + } + + /** + * Create a new {@link AtmFsm} in {@link State#Idle}. + * + * @return a new {@link AtmFsm} in {@link State#Idle}. + */ + public static AtmFsm newAtmFsm() { + return buildFsm(fb -> State.Idle); + } + + /** + * Build an {@link AtmFsm}. + * + *

{@code builderStateFunction} may make modifications to the FSM before it's built via the + * builder and returns the desired initial state. + * + * @param builderStateFunction invoked after the builder has set up all state transitions. + * Returns the desired initial state of the FSM. + * @return an {@link AtmFsm}. + */ + static AtmFsm buildFsm(Function, State> builderStateFunction) { + FsmBuilder fb = new FsmBuilder<>(); + + /* Idle */ + fb.when(State.Idle) + .on(Event.Connected) + .transitionTo(State.Loading); + + /* Loading */ + fb.when(State.Loading) + .on(Event.LoadSuccess) + .transitionTo(State.InService); + + fb.when(State.Loading) + .on(Event.LoadFail) + .transitionTo(State.OutOfService); + + fb.when(State.Loading) + .on(Event.ConnectionClosed) + .transitionTo(State.Disconnected); + + /* OutOfService */ + fb.when(State.OutOfService) + .on(Event.Startup) + .transitionTo(State.InService); + + fb.when(State.OutOfService) + .on(Event.ConnectionLost) + .transitionTo(State.Disconnected); + + /* InService */ + fb.when(State.InService) + .on(Event.ConnectionLost) + .transitionTo(State.Disconnected); + + fb.when(State.InService) + .on(Event.Shutdown) + .transitionTo(State.OutOfService); + + /* Disconnected */ + fb.when(State.Disconnected) + .on(Event.ConnectionRestored) + .transitionTo(State.InService); + + State initialState = builderStateFunction.apply(fb); + + return new AtmFsm(fb.build(initialState)); + } } diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/atm/AtmFsmTest.kt b/src/test/java/com/digitalpetri/strictmachine/dsl/atm/AtmFsmTest.kt deleted file mode 100644 index 4008635..0000000 --- a/src/test/java/com/digitalpetri/strictmachine/dsl/atm/AtmFsmTest.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2018 Kevin Herron - * - * 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.digitalpetri.strictmachine.dsl.atm - -import com.digitalpetri.strictmachine.dsl.atm.AtmFsm.* -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class AtmFsmTest { - - @Test - fun `S(Idle) x E(Connected) = S'(Loading)`() { - assertEquals(State.Loading, State.Idle x Event.Connected) - } - - @Test - fun `S(Loading) x E(LoadSuccess) = S'(InService)`() { - assertEquals(State.InService, State.Loading x Event.LoadSuccess) - } - - @Test - fun `S(Loading) x E(LoadFail) = S'(OutOfService)`() { - assertEquals(State.OutOfService, State.Loading x Event.LoadFail) - } - - @Test - fun `S(Loading) x E(ConnectionClosed) = S'(Disconnected)`() { - assertEquals(State.Disconnected, State.Loading x Event.ConnectionClosed) - } - - @Test - fun `S(OutOfService) x E(Startup) = S'(InService)`() { - assertEquals(State.InService, State.OutOfService x Event.Startup) - } - - @Test - fun `S(OutOfService) x E(ConnectionLost) = S'(Disconnected)`() { - assertEquals(State.Disconnected, State.OutOfService x Event.ConnectionLost) - } - - @Test - fun `S(InService) x E(ConnectionLost) = S'(Disconnected)`() { - assertEquals(State.Disconnected, State.InService x Event.ConnectionLost) - } - - @Test - fun `S(InService) x E(Shutdown) = S'(OutOfService)`() { - assertEquals(State.OutOfService, State.InService x Event.Shutdown) - } - - @Test - fun `S(Disconnected) x E(ConnectionRestored) = S'(InService)`() { - assertEquals(State.InService, State.Disconnected x Event.ConnectionRestored) - } - - private infix fun State.x(event: Event): State { - val fsm = newAtmFsmInState(this) - - return fsm.fireEventBlocking(event).also { nextState -> - println("S($this) x E($event) = S'($nextState)") - } - } - - private fun newAtmFsmInState(state: AtmFsm.State): AtmFsm { - return buildFsm { state } - } - -} - diff --git a/src/test/java/com/digitalpetri/strictmachine/dsl/atm/AtmTest.java b/src/test/java/com/digitalpetri/strictmachine/dsl/atm/AtmTest.java new file mode 100644 index 0000000..723192e --- /dev/null +++ b/src/test/java/com/digitalpetri/strictmachine/dsl/atm/AtmTest.java @@ -0,0 +1,75 @@ +package com.digitalpetri.strictmachine.dsl.atm; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.digitalpetri.strictmachine.dsl.atm.AtmFsm.Event; +import com.digitalpetri.strictmachine.dsl.atm.AtmFsm.State; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("MethodName") +class AtmTest { + + @Test + void Idle_Connected_Loading() throws InterruptedException { + var fsm = AtmFsm.buildFsm(fb -> State.Idle); + + assertEquals(State.Loading, fsm.fireEventBlocking(Event.Connected)); + } + + @Test + void Loading_LoadSuccess_InService() throws InterruptedException { + var fsm = AtmFsm.buildFsm(fb -> State.Loading); + + assertEquals(State.InService, fsm.fireEventBlocking(Event.LoadSuccess)); + } + + @Test + void Loading_LoadFail_OutOfService() throws InterruptedException { + var fsm = AtmFsm.buildFsm(fb -> State.Loading); + + assertEquals(State.OutOfService, fsm.fireEventBlocking(Event.LoadFail)); + } + + @Test + void Loading_ConnectionClosed_Disconnected() throws InterruptedException { + var fsm = AtmFsm.buildFsm(fb -> State.Loading); + + assertEquals(State.Disconnected, fsm.fireEventBlocking(Event.ConnectionClosed)); + } + + @Test + void OutOfService_Startup_InService() throws InterruptedException { + var fsm = AtmFsm.buildFsm(fb -> State.OutOfService); + + assertEquals(State.InService, fsm.fireEventBlocking(Event.Startup)); + } + + @Test + void OutOfService_ConnectionLost_Disconnected() throws InterruptedException { + var fsm = AtmFsm.buildFsm(fb -> State.OutOfService); + + assertEquals(State.Disconnected, fsm.fireEventBlocking(Event.ConnectionLost)); + } + + @Test + void InService_ConnectionLost_Disconnected() throws InterruptedException { + var fsm = AtmFsm.buildFsm(fb -> State.InService); + + assertEquals(State.Disconnected, fsm.fireEventBlocking(Event.ConnectionLost)); + } + + @Test + void InService_Shutdown_OutOfService() throws InterruptedException { + var fsm = AtmFsm.buildFsm(fb -> State.InService); + + assertEquals(State.OutOfService, fsm.fireEventBlocking(Event.Shutdown)); + } + + @Test + void Disconnected_ConnectionRestored_InService() throws InterruptedException { + var fsm = AtmFsm.buildFsm(fb -> State.Disconnected); + + assertEquals(State.InService, fsm.fireEventBlocking(Event.ConnectionRestored)); + } + +}