diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index a3d7215..5529880 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -11,9 +11,6 @@
-
-
-
diff --git a/.idea/markdown-navigator-enh.xml b/.idea/markdown-navigator-enh.xml
new file mode 100644
index 0000000..12fb99d
--- /dev/null
+++ b/.idea/markdown-navigator-enh.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/markdown-navigator.xml b/.idea/markdown-navigator.xml
new file mode 100644
index 0000000..4463382
--- /dev/null
+++ b/.idea/markdown-navigator.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 3a728bb..28852da 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ allprojects {
At a module level `build.gradle`, add the following dependency:
```groovy
-implementation 'vit.khudenko.android:fsm:0.3.0'
+implementation 'vit.khudenko.android:fsm:0.4.0'
```
### Usage
@@ -26,7 +26,7 @@ A sample configuration, assuming your app has `Session` class, that defines __sp
to your app__ events and states (`Session.Event` and `Session.State` enums):
```kotlin
-val sessionStateMachine = StateMachine.Builder()
+val sessionStateMachine = StateMachine.Builder()
.setInitialState(Session.State.ACTIVE)
.addTransition(
StateMachine.Transition(
@@ -71,13 +71,32 @@ sessionStateMachine.consumeEvent(Session.Event.LOGOUT)
State changes are propagated via `StateMachine.Listener`.
```kotlin
-sessionStateMachine.addListener(object : StateMachine.Listener {
- override fun onStateChanged(oldState: Session.State, newState: Session.State) {
+sessionStateMachine.addListener(object : StateMachine.Listener {
+ override fun onStateChanged(oldState: Session.State, newState: Session.State, eventData: Unit?) {
// do something
}
})
```
+#### Event payloads
+
+Starting from v0.4.0 `StateMachine` supports event payloads, i.e. in addition to bare `enum` instances serving as events the library supports event extension/payloads:
+
+```kotlin
+data class MyEventPayload(val value: Int)
+
+val stateMachine: StateMachine // instantiation is omitted for brevity
+
+stateMachine.addListener(object : StateMachine.Listener {
+ override fun onStateChanged(oldState: MyState, newState: MyState, eventData: MyEventPayload?) {
+ // do something
+ }
+})
+
+stateMachine.consumeEvent(MyEvent.A) // equivalent to stateMachine.consumeEvent(MyEvent.A, null)
+stateMachine.consumeEvent(MyEvent.B, MyEventPayload(47))
+```
+
### Threading
The `StateMachine` implementation is thread-safe. Public API methods are declared as `synchronized`.
diff --git a/build.gradle b/build.gradle
index 1458b18..22772a2 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext.kotlin_version = '1.3.61'
+ ext.kotlin_version = '1.3.70'
repositories {
google()
jcenter()
diff --git a/state_machine_lib/build.gradle b/state_machine_lib/build.gradle
index 0c867e7..d90dfed 100644
--- a/state_machine_lib/build.gradle
+++ b/state_machine_lib/build.gradle
@@ -9,7 +9,7 @@ afterEvaluate {
apply plugin: 'kotlin-android'
-def libraryVersionName = "0.3.0"
+def libraryVersionName = "0.4.0"
def artifactName = 'StateMachine'
android {
diff --git a/state_machine_lib/src/main/kotlin/vit/khudenko/android/fsm/StateMachine.kt b/state_machine_lib/src/main/kotlin/vit/khudenko/android/fsm/StateMachine.kt
index 8a8c9a8..109a86c 100644
--- a/state_machine_lib/src/main/kotlin/vit/khudenko/android/fsm/StateMachine.kt
+++ b/state_machine_lib/src/main/kotlin/vit/khudenko/android/fsm/StateMachine.kt
@@ -1,9 +1,6 @@
package vit.khudenko.android.fsm
-import java.util.*
-import kotlin.collections.ArrayList
-import kotlin.collections.HashMap
-import kotlin.collections.LinkedHashSet
+import java.util.Collections
import kotlin.collections.set
/**
@@ -40,10 +37,11 @@ import kotlin.collections.set
*
* @param [Event] event parameter of enum type.
* @param [State] state parameter of enum type.
+ * @param [EventPayload] event payload parameter.
*
* @see [StateMachine.Builder]
*/
-class StateMachine, State : Enum> private constructor(
+class StateMachine, State : Enum, EventPayload> private constructor(
private val graph: Map, List>,
initialState: State
) {
@@ -51,14 +49,14 @@ class StateMachine, State : Enum> private constructor
/**
* A callback to communicate state changes of a [`StateMachine`][StateMachine].
*/
- interface Listener {
- fun onStateChanged(oldState: State, newState: State)
+ interface Listener {
+ fun onStateChanged(oldState: State, newState: State, eventPayload: EventPayload?)
}
/**
* Builder is not thread-safe.
*/
- class Builder, State : Enum> {
+ class Builder, State : Enum, EventPayload> {
private val graph = HashMap, List>()
private lateinit var initialState: State
@@ -76,7 +74,7 @@ class StateMachine, State : Enum> private constructor
* @throws [StateMachineBuilderValidationException] if a duplicate transition identified (by a combination
* of event and starting state)
*/
- fun addTransition(transition: Transition): Builder {
+ fun addTransition(transition: Transition): Builder {
val statePathCopy = transition.statePath.toMutableList()
val startState = statePathCopy.removeAt(0)
@@ -96,7 +94,7 @@ class StateMachine, State : Enum> private constructor
*
* @return [`StateMachine.Builder`][StateMachine.Builder]
*/
- fun setInitialState(state: State): Builder {
+ fun setInitialState(state: State): Builder {
this.initialState = state
return this
}
@@ -109,7 +107,7 @@ class StateMachine, State : Enum> private constructor
* @throws [StateMachineBuilderValidationException] if no transition defined with starting state matching
* the initial state
*/
- fun build(): StateMachine {
+ fun build(): StateMachine {
if (this::initialState.isInitialized.not()) {
throw StateMachineBuilderValidationException(
"initial state is not defined, make sure to call ${StateMachine::class.java.simpleName}" +
@@ -132,7 +130,7 @@ class StateMachine, State : Enum> private constructor
}
private var currentState: State = initialState
- private val listeners: LinkedHashSet> = LinkedHashSet()
+ private val listeners: LinkedHashSet> = LinkedHashSet()
private var inTransition: Boolean = false
/**
@@ -141,7 +139,7 @@ class StateMachine, State : Enum> private constructor
* If this [`listener`][listener] has been already added, then this call is no op.
*/
@Synchronized
- fun addListener(listener: Listener) {
+ fun addListener(listener: Listener) {
if (!listeners.contains(listener)) {
listeners.add(listener)
}
@@ -159,7 +157,7 @@ class StateMachine, State : Enum> private constructor
* Removes [`listener`][listener] from this `StateMachine`.
*/
@Synchronized
- fun removeListener(listener: Listener) {
+ fun removeListener(listener: Listener) {
listeners.remove(listener)
}
@@ -174,6 +172,7 @@ class StateMachine, State : Enum> private constructor
* State changes are communicated via the [`StateMachine.Listener`][StateMachine.Listener] listeners.
*
* @param event [`Event`][Event]
+ * @param eventPayload [`EventPayload`][EventPayload] optional payload associated with the [event]
*
* @return flag of whether the event was actually consumed (meaning moving to a new state) or ignored.
*
@@ -181,7 +180,7 @@ class StateMachine, State : Enum> private constructor
* but there is still an unfinished transition in progress.
*/
@Synchronized
- fun consumeEvent(event: Event): Boolean {
+ fun consumeEvent(event: Event, eventPayload: EventPayload? = null): Boolean {
val transitionIdentity = Transition.Identity(event, currentState)
val transition = graph[transitionIdentity] ?: return false
@@ -194,7 +193,7 @@ class StateMachine, State : Enum> private constructor
val newState = transition[i]
currentState = newState
for (listener in ArrayList(listeners)) {
- listener.onStateChanged(oldState, newState)
+ listener.onStateChanged(oldState, newState, eventPayload)
}
}
diff --git a/state_machine_lib/src/test/kotlin/vit/khudenko/android/fsm/StateMachineBuilderTest.kt b/state_machine_lib/src/test/kotlin/vit/khudenko/android/fsm/StateMachineBuilderTest.kt
index aace409..4d6f888 100644
--- a/state_machine_lib/src/test/kotlin/vit/khudenko/android/fsm/StateMachineBuilderTest.kt
+++ b/state_machine_lib/src/test/kotlin/vit/khudenko/android/fsm/StateMachineBuilderTest.kt
@@ -20,7 +20,7 @@ class StateMachineBuilderTest {
StateMachineBuilderValidationException::class.java,
"no transitions defined, make sure to call StateMachine.Builder.addTransition()"
) {
- StateMachine.Builder()
+ StateMachine.Builder()
.setInitialState(STATE_A)
.build()
}
@@ -34,7 +34,7 @@ class StateMachineBuilderTest {
StateMachineBuilderValidationException::class.java,
"initial state is not defined, make sure to call StateMachine.Builder.setInitialState()"
) {
- StateMachine.Builder()
+ StateMachine.Builder()
.addTransition(transition)
.build()
}
@@ -52,7 +52,7 @@ class StateMachineBuilderTest {
StateMachineBuilderValidationException::class.java,
getCauseForDuplicateStartState(event, state)
) {
- StateMachine.Builder()
+ StateMachine.Builder()
.addTransition(transition1)
.addTransition(transition2)
}
@@ -70,7 +70,7 @@ class StateMachineBuilderTest {
StateMachineBuilderValidationException::class.java,
getCauseForDuplicateStartState(event, state)
) {
- StateMachine.Builder()
+ StateMachine.Builder()
.addTransition(transition1)
.addTransition(transition2)
}
@@ -88,7 +88,7 @@ class StateMachineBuilderTest {
StateMachineBuilderValidationException::class.java,
getCauseForDuplicateStartState(event, state)
) {
- StateMachine.Builder()
+ StateMachine.Builder()
.addTransition(transition1)
.addTransition(transition2)
}
@@ -103,7 +103,7 @@ class StateMachineBuilderTest {
StateMachineBuilderValidationException::class.java,
"no transition defined with start state matching the initial state ($initialState)"
) {
- StateMachine.Builder()
+ StateMachine.Builder()
.setInitialState(initialState)
.addTransition(transition)
.build()
@@ -119,7 +119,7 @@ class StateMachineBuilderTest {
val state = STATE_A
- val stateMachine = StateMachine.Builder()
+ val stateMachine = StateMachine.Builder()
.addTransition(transition1)
.addTransition(transition2)
.addTransition(transition3)
diff --git a/state_machine_lib/src/test/kotlin/vit/khudenko/android/fsm/StateMachineTest.kt b/state_machine_lib/src/test/kotlin/vit/khudenko/android/fsm/StateMachineTest.kt
index 34032b6..6964a61 100644
--- a/state_machine_lib/src/test/kotlin/vit/khudenko/android/fsm/StateMachineTest.kt
+++ b/state_machine_lib/src/test/kotlin/vit/khudenko/android/fsm/StateMachineTest.kt
@@ -12,6 +12,7 @@ import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import vit.khudenko.android.fsm.test_utils.Utils
+import vit.khudenko.android.fsm.test_utils.Utils.Companion.eventItem
import vit.khudenko.android.fsm.test_utils.Utils.Event
import vit.khudenko.android.fsm.test_utils.Utils.Event.EVENT_1
import vit.khudenko.android.fsm.test_utils.Utils.Event.EVENT_2
@@ -30,11 +31,11 @@ class StateMachineTest {
@Test
fun `initial state should be set as expected`() {
- val listener: StateMachine.Listener = mock()
+ val listener: StateMachine.Listener = mock()
val state = STATE_A
- val stateMachine = StateMachine.Builder()
+ val stateMachine = StateMachine.Builder()
.addTransition(
StateMachine.Transition(
EVENT_1,
@@ -47,14 +48,14 @@ class StateMachineTest {
stateMachine.addListener(listener)
assertEquals(state, stateMachine.getCurrentState())
- verify(listener, never()).onStateChanged(anyOrNull(), anyOrNull())
+ verify(listener, never()).onStateChanged(anyOrNull(), anyOrNull(), anyOrNull())
}
@Test
fun `out-of-config events should be ignored`() {
- val listener: StateMachine.Listener = mock()
+ val listener: StateMachine.Listener = mock()
- val stateMachine = StateMachine.Builder()
+ val stateMachine = StateMachine.Builder()
.addTransition(
StateMachine.Transition(
EVENT_1,
@@ -81,15 +82,20 @@ class StateMachineTest {
Utils.checkEventsAreIgnored(
stateMachine,
listener,
- listOf(EVENT_4, EVENT_5)
+ listOf(
+ eventItem(EVENT_4),
+ eventItem(EVENT_4, "event payload"),
+ eventItem(EVENT_5),
+ eventItem(EVENT_5, "event payload")
+ )
)
}
@Test
fun `in-config events, that do not match the current state of the state machine, should be ignored`() {
- val listener: StateMachine.Listener = mock()
+ val listener: StateMachine.Listener = mock()
- val stateMachine = StateMachine.Builder()
+ val stateMachine = StateMachine.Builder()
.addTransition(
StateMachine.Transition(
EVENT_1,
@@ -116,15 +122,20 @@ class StateMachineTest {
Utils.checkEventsAreIgnored(
stateMachine,
listener,
- listOf(EVENT_2, EVENT_3)
+ listOf(
+ eventItem(EVENT_2),
+ eventItem(EVENT_2, "event payload"),
+ eventItem(EVENT_3),
+ eventItem(EVENT_3, "event payload")
+ )
)
}
@Test
fun `same transition should not happen, if the same event is fired again`() {
- val listener: StateMachine.Listener = mock()
+ val listener: StateMachine.Listener = mock()
- val stateMachine = StateMachine.Builder()
+ val stateMachine = StateMachine.Builder()
.addTransition(
StateMachine.Transition(
EVENT_1,
@@ -152,22 +163,25 @@ class StateMachineTest {
Utils.checkEventIsConsumed(
stateMachine,
listOf(listener),
- EVENT_1,
+ eventItem(EVENT_1),
listOf(STATE_A, STATE_B)
)
- Utils.checkEventIsIgnored(
+ Utils.checkEventsAreIgnored(
stateMachine,
listener,
- EVENT_1
+ listOf(
+ eventItem(EVENT_1),
+ eventItem(EVENT_1, "event payload")
+ )
)
}
@Test
fun `same transition should happen, if the same event is fired again`() {
- val listener: StateMachine.Listener = mock()
+ val listener: StateMachine.Listener = mock()
- val stateMachine = StateMachine.Builder()
+ val stateMachine = StateMachine.Builder()
.addTransition(
StateMachine.Transition(
EVENT_1,
@@ -201,25 +215,32 @@ class StateMachineTest {
Utils.checkEventIsConsumed(
stateMachine,
listOf(listener),
- EVENT_1,
+ eventItem(EVENT_1),
listOf(STATE_A, STATE_B, STATE_A)
)
- // verify same transition happens again
- reset(listener)
- Utils.checkEventIsConsumed(
- stateMachine,
- listOf(listener),
- EVENT_1,
- listOf(STATE_A, STATE_B, STATE_A)
- )
+ // verify same transition happens again (regardless of event payload)
+ listOf(
+ eventItem(EVENT_1),
+ eventItem(EVENT_1, ""),
+ eventItem(EVENT_1),
+ eventItem(EVENT_1, "event payload")
+ ).forEach { eventItem ->
+ reset(listener)
+ Utils.checkEventIsConsumed(
+ stateMachine,
+ listOf(listener),
+ eventItem,
+ listOf(STATE_A, STATE_B, STATE_A)
+ )
+ }
}
@Test
fun `several transitions and final state`() {
- val listener: StateMachine.Listener = mock()
+ val listener: StateMachine.Listener = mock()
- val stateMachine = StateMachine.Builder()
+ val stateMachine = StateMachine.Builder()
.addTransition(
StateMachine.Transition(
EVENT_1,
@@ -244,13 +265,13 @@ class StateMachineTest {
stateMachine.addListener(listener)
assertEquals(STATE_A, stateMachine.getCurrentState())
- verify(listener, never()).onStateChanged(anyOrNull(), anyOrNull())
+ verify(listener, never()).onStateChanged(anyOrNull(), anyOrNull(), anyOrNull())
// verify transition from STATE_A to STATE_B happens
Utils.checkEventIsConsumed(
stateMachine,
listOf(listener),
- EVENT_1,
+ eventItem(EVENT_1),
listOf(STATE_A, STATE_B)
)
@@ -258,7 +279,7 @@ class StateMachineTest {
Utils.checkEventIsConsumed(
stateMachine,
listOf(listener),
- EVENT_2,
+ eventItem(EVENT_2),
listOf(STATE_B, STATE_C)
)
@@ -266,21 +287,21 @@ class StateMachineTest {
Utils.checkEventIsConsumed(
stateMachine,
listOf(listener),
- EVENT_3,
+ eventItem(EVENT_3),
listOf(STATE_C, STATE_D, STATE_E)
)
- // verify any further events should are ignored as state machine is its final state
+ // verify any further events should are ignored as state machine is at its final state
Utils.checkEventsAreIgnored(
stateMachine,
listener,
- Event.values().asList()
+ Event.values().asList().map { event -> eventItem(event) }
)
}
@Test
fun `starting new transition while ongoing transition is not finished yet should be a consistency violation`() {
- val stateMachine = StateMachine.Builder()
+ val stateMachine = StateMachine.Builder()
.addTransition(
StateMachine.Transition(
EVENT_1,
@@ -296,10 +317,10 @@ class StateMachineTest {
.setInitialState(STATE_A)
.build()
- val listener: StateMachine.Listener = mock {
+ val listener: StateMachine.Listener = mock {
// listener will fire EVENT_2 as soon as the intermediate state STATE_B
// in the first transition (STATE_A - STATE_B - STATE_C) is reached
- on { onStateChanged(STATE_A, STATE_B) } doAnswer {
+ on { onStateChanged(STATE_A, STATE_B, null) } doAnswer {
assertEquals(STATE_B, stateMachine.getCurrentState())
assertTrue(stateMachine.consumeEvent(EVENT_2))
Unit
@@ -318,7 +339,7 @@ class StateMachineTest {
@Test
fun `starting new transition once ongoing transition has finished should not be a consistency violation (case with 2 transitions)`() {
- val stateMachine = StateMachine.Builder()
+ val stateMachine = StateMachine.Builder()
.addTransition(
StateMachine.Transition(
EVENT_1,
@@ -334,10 +355,10 @@ class StateMachineTest {
.setInitialState(STATE_A)
.build()
- val listener: StateMachine.Listener = mock {
+ val listener: StateMachine.Listener = mock {
// listener will fire EVENT_2 as soon as the final state STATE_C in the first
// transition (STATE_A - STATE_B - STATE_C) is reached
- on { onStateChanged(STATE_B, STATE_C) } doAnswer {
+ on { onStateChanged(STATE_B, STATE_C, null) } doAnswer {
assertEquals(STATE_C, stateMachine.getCurrentState())
assertTrue(stateMachine.consumeEvent(EVENT_2))
Unit
@@ -350,14 +371,14 @@ class StateMachineTest {
Utils.checkEventIsConsumed(
stateMachine,
listOf(listener),
- EVENT_1,
+ eventItem(EVENT_1),
listOf(STATE_A, STATE_B, STATE_C, STATE_D, STATE_E)
)
}
@Test
fun `starting new transition once ongoing transition has finished should not be a consistency violation (case with 3 transitions)`() {
- val stateMachine = StateMachine.Builder()
+ val stateMachine = StateMachine.Builder()
.addTransition(
StateMachine.Transition(
EVENT_1,
@@ -379,17 +400,17 @@ class StateMachineTest {
.setInitialState(STATE_A)
.build()
- val listener: StateMachine.Listener = mock {
+ val listener: StateMachine.Listener = mock {
// listener will fire EVENT_2 as soon as the final state STATE_B in the first
// transition (STATE_A - STATE_B) is reached
- on { onStateChanged(STATE_A, STATE_B) } doAnswer {
+ on { onStateChanged(STATE_A, STATE_B, null) } doAnswer {
assertEquals(STATE_B, stateMachine.getCurrentState())
assertTrue(stateMachine.consumeEvent(EVENT_2))
Unit
}
// listener will fire EVENT_3 as soon as the final state STATE_C in the second
// transition (STATE_B - STATE_C) is reached
- on { onStateChanged(STATE_B, STATE_C) } doAnswer {
+ on { onStateChanged(STATE_B, STATE_C, null) } doAnswer {
assertEquals(STATE_C, stateMachine.getCurrentState())
assertTrue(stateMachine.consumeEvent(EVENT_3))
Unit
@@ -405,17 +426,17 @@ class StateMachineTest {
Utils.checkEventIsConsumed(
stateMachine,
listOf(listener),
- EVENT_1,
+ eventItem(EVENT_1),
listOf(STATE_A, STATE_B, STATE_C, STATE_D)
)
}
@Test
fun `both listeners should be notified as expected`() {
- val listener1: StateMachine.Listener = mock()
- val listener2: StateMachine.Listener = mock()
+ val listener1: StateMachine.Listener = mock()
+ val listener2: StateMachine.Listener = mock()
- val stateMachine = StateMachine.Builder()
+ val stateMachine = StateMachine.Builder()
.addTransition(
StateMachine.Transition(
EVENT_1,
@@ -433,16 +454,16 @@ class StateMachineTest {
Utils.checkEventIsConsumed(
stateMachine,
listOf(listener1, listener2),
- EVENT_1,
+ eventItem(EVENT_1),
listOf(STATE_A, STATE_B)
)
}
@Test
fun `adding same listener twice should be a no op`() {
- val listener: StateMachine.Listener = mock()
+ val listener: StateMachine.Listener = mock()
- val stateMachine = StateMachine.Builder()
+ val stateMachine = StateMachine.Builder()
.addTransition(
StateMachine.Transition(
EVENT_1,
@@ -460,17 +481,17 @@ class StateMachineTest {
Utils.checkEventIsConsumed(
stateMachine,
listOf(listener),
- EVENT_1,
+ eventItem(EVENT_1),
listOf(STATE_A, STATE_B)
)
}
@Test
fun `explicit call to remove one of the two listeners`() {
- val listener1: StateMachine.Listener = mock()
- val listener2: StateMachine.Listener = mock()
+ val listener1: StateMachine.Listener = mock()
+ val listener2: StateMachine.Listener = mock()
- val stateMachine = StateMachine.Builder()
+ val stateMachine = StateMachine.Builder()
.addTransition(
StateMachine.Transition(
EVENT_1,
@@ -501,7 +522,7 @@ class StateMachineTest {
Utils.checkEventIsConsumed(
stateMachine,
listOf(listener1, listener2),
- EVENT_1,
+ eventItem(EVENT_1),
listOf(STATE_A, STATE_B)
)
@@ -509,7 +530,7 @@ class StateMachineTest {
stateMachine.removeListener(listener1)
assertTrue(stateMachine.consumeEvent(EVENT_2))
- verify(listener2).onStateChanged(STATE_B, STATE_C)
+ verify(listener2).onStateChanged(STATE_B, STATE_C, null)
verifyNoMoreInteractions(listener1, listener2)
// leave no listeners attached to state machine
@@ -523,10 +544,10 @@ class StateMachineTest {
@Test
fun `first listener removes itself during notification`() {
- val listener1: StateMachine.Listener = mock()
- val listener2: StateMachine.Listener = mock()
+ val listener1: StateMachine.Listener = mock()
+ val listener2: StateMachine.Listener = mock()
- val stateMachine = StateMachine.Builder()
+ val stateMachine = StateMachine.Builder()
.addTransition(
StateMachine.Transition(
EVENT_1,
@@ -542,11 +563,10 @@ class StateMachineTest {
.setInitialState(STATE_A)
.build()
-
doAnswer {
// listener1 will remove itself as soon as notified
stateMachine.removeListener(listener1)
- }.`when`(listener1).onStateChanged(STATE_A, STATE_B)
+ }.`when`(listener1).onStateChanged(STATE_A, STATE_B, null)
with(stateMachine) {
addListener(listener1)
@@ -557,22 +577,22 @@ class StateMachineTest {
Utils.checkEventIsConsumed(
stateMachine,
listOf(listener1, listener2),
- EVENT_1,
+ eventItem(EVENT_1),
listOf(STATE_A, STATE_B)
)
// verify state changed and only remaining listener2 is notified
assertTrue(stateMachine.consumeEvent(EVENT_2))
- verify(listener2).onStateChanged(STATE_B, STATE_C)
+ verify(listener2).onStateChanged(STATE_B, STATE_C, null)
verifyNoMoreInteractions(listener1, listener2)
}
@Test
fun `second listener removes itself during notification`() {
- val listener1: StateMachine.Listener = mock()
- val listener2: StateMachine.Listener = mock()
+ val listener1: StateMachine.Listener = mock()
+ val listener2: StateMachine.Listener = mock()
- val stateMachine = StateMachine.Builder()
+ val stateMachine = StateMachine.Builder()
.addTransition(
StateMachine.Transition(
EVENT_1,
@@ -591,7 +611,7 @@ class StateMachineTest {
doAnswer {
// listener2 will remove itself as soon as notified
stateMachine.removeListener(listener2)
- }.`when`(listener1).onStateChanged(STATE_A, STATE_B)
+ }.`when`(listener1).onStateChanged(STATE_A, STATE_B, null)
with(stateMachine) {
addListener(listener1)
@@ -602,19 +622,19 @@ class StateMachineTest {
Utils.checkEventIsConsumed(
stateMachine,
listOf(listener1, listener2),
- EVENT_1,
+ eventItem(EVENT_1),
listOf(STATE_A, STATE_B)
)
// verify state changed and only remaining listener1 is notified
assertTrue(stateMachine.consumeEvent(EVENT_2))
- verify(listener1).onStateChanged(STATE_B, STATE_C)
+ verify(listener1).onStateChanged(STATE_B, STATE_C, null)
verifyNoMoreInteractions(listener1, listener2)
}
@Test
fun `first listener removes second listener during notification`() {
- val stateMachine = StateMachine.Builder()
+ val stateMachine = StateMachine.Builder()
.addTransition(
StateMachine.Transition(
EVENT_1,
@@ -630,11 +650,11 @@ class StateMachineTest {
.setInitialState(STATE_A)
.build()
- val listener2: StateMachine.Listener = mock()
- val listener1: StateMachine.Listener = mock {
+ val listener2: StateMachine.Listener = mock()
+ val listener1: StateMachine.Listener = mock {
on {
// listener1 will remove listener2 as soon as notified
- onStateChanged(STATE_A, STATE_B)
+ onStateChanged(STATE_A, STATE_B, null)
} doAnswer { stateMachine.removeListener(listener2) }
}
@@ -647,19 +667,19 @@ class StateMachineTest {
Utils.checkEventIsConsumed(
stateMachine,
listOf(listener1, listener2),
- EVENT_1,
+ eventItem(EVENT_1),
listOf(STATE_A, STATE_B)
)
// verify state changed and only remaining listener1 is notified
assertTrue(stateMachine.consumeEvent(EVENT_2))
- verify(listener1).onStateChanged(STATE_B, STATE_C)
+ verify(listener1).onStateChanged(STATE_B, STATE_C, null)
verifyNoMoreInteractions(listener1, listener2)
}
@Test
fun `second listener removes first listener during notification`() {
- val stateMachine = StateMachine.Builder()
+ val stateMachine = StateMachine.Builder()
.addTransition(
StateMachine.Transition(
EVENT_1,
@@ -675,11 +695,11 @@ class StateMachineTest {
.setInitialState(STATE_A)
.build()
- val listener1: StateMachine.Listener = mock()
- val listener2: StateMachine.Listener = mock {
+ val listener1: StateMachine.Listener = mock()
+ val listener2: StateMachine.Listener = mock {
on {
// listener2 will remove listener1 as soon as notified
- onStateChanged(STATE_A, STATE_B)
+ onStateChanged(STATE_A, STATE_B, null)
} doAnswer { stateMachine.removeListener(listener1) }
}
@@ -692,22 +712,22 @@ class StateMachineTest {
Utils.checkEventIsConsumed(
stateMachine,
listOf(listener1, listener2),
- EVENT_1,
+ eventItem(EVENT_1),
listOf(STATE_A, STATE_B)
)
// verify state changed and only remaining listener2 is notified as expected
assertTrue(stateMachine.consumeEvent(EVENT_2))
- verify(listener2).onStateChanged(STATE_B, STATE_C)
+ verify(listener2).onStateChanged(STATE_B, STATE_C, null)
verifyNoMoreInteractions(listener1, listener2)
}
@Test
fun `remove all listeners via removeAllListeners()`() {
- val listener1: StateMachine.Listener = mock()
- val listener2: StateMachine.Listener = mock()
+ val listener1: StateMachine.Listener = mock()
+ val listener2: StateMachine.Listener = mock()
- val stateMachine = StateMachine.Builder()
+ val stateMachine = StateMachine.Builder()
.addTransition(
StateMachine.Transition(
EVENT_1,
diff --git a/state_machine_lib/src/test/kotlin/vit/khudenko/android/fsm/test_utils/Utils.kt b/state_machine_lib/src/test/kotlin/vit/khudenko/android/fsm/test_utils/Utils.kt
index 8b74393..1e72dea 100644
--- a/state_machine_lib/src/test/kotlin/vit/khudenko/android/fsm/test_utils/Utils.kt
+++ b/state_machine_lib/src/test/kotlin/vit/khudenko/android/fsm/test_utils/Utils.kt
@@ -6,43 +6,46 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import vit.khudenko.android.fsm.StateMachine
+import java.util.Optional
class Utils {
companion object {
fun checkEventIsIgnored(
- stateMachine: StateMachine,
- listener: StateMachine.Listener,
- event: Event
+ stateMachine: StateMachine,
+ listener: StateMachine.Listener,
+ eventItem: Pair>
) {
checkEventsAreIgnored(
stateMachine,
listOf(listener),
- listOf(event)
+ listOf(eventItem)
)
}
fun checkEventsAreIgnored(
- stateMachine: StateMachine,
- listener: StateMachine.Listener,
- events: List
+ stateMachine: StateMachine,
+ listener: StateMachine.Listener,
+ eventItems: List>>
) {
checkEventsAreIgnored(
stateMachine,
listOf(listener),
- events
+ eventItems
)
}
fun checkEventIsConsumed(
- stateMachine: StateMachine,
- listeners: List>,
- event: Event,
+ stateMachine: StateMachine,
+ listeners: List>,
+ eventItem: Pair>,
transition: List
) {
+ val (event, eventPayloadOptional) = eventItem
+
assertEquals(transition.first(), stateMachine.getCurrentState())
- assertTrue(stateMachine.consumeEvent(event))
+ assertTrue(stateMachine.consumeEvent(event, eventPayloadOptional.getOrNull()))
assertEquals(transition.last(), stateMachine.getCurrentState())
for (listener in listeners) {
@@ -52,22 +55,24 @@ class Utils {
window.first() to window.last()
}
.forEach { (stateFrom, stateTo) ->
- verify(listener).onStateChanged(stateFrom, stateTo)
+ verify(listener).onStateChanged(stateFrom, stateTo, eventPayloadOptional.getOrNull())
}
}
verifyNoMoreInteractions(listener)
}
}
+ fun eventItem(event: Event, eventPayload: String? = null) = Pair(event, Optional.ofNullable(eventPayload))
+
private fun checkEventsAreIgnored(
- stateMachine: StateMachine,
- listeners: List>,
- events: List
+ stateMachine: StateMachine,
+ listeners: List>,
+ eventItems: List>>
) {
val state = stateMachine.getCurrentState()
val listenersCopy = ArrayList(listeners)
- for (event in events) {
- assertFalse(stateMachine.consumeEvent(event))
+ for ((event, eventPayloadOptional) in eventItems) {
+ assertFalse(stateMachine.consumeEvent(event, eventPayloadOptional.getOrNull()))
assertEquals(state, stateMachine.getCurrentState())
for (listener in listenersCopy) {
verifyNoMoreInteractions(listener)
@@ -91,4 +96,10 @@ class Utils {
EVENT_4,
EVENT_5
}
-}
\ No newline at end of file
+}
+
+fun Optional.getOrNull(): T? = if (this.isPresent) {
+ get()
+} else {
+ null
+}