From b520c54bc0305d3133b04bceda2417c9f85cb175 Mon Sep 17 00:00:00 2001 From: ikurenkov Date: Sat, 16 May 2020 00:22:03 +0300 Subject: [PATCH 1/2] Initial support for Kotlin Native & Multiplatform: - Multiplatform atomic reference - Multiplatform synchronization - Switch from java class to kotlin class --- build.gradle | 11 ++++--- src/main/kotlin/com/tinder/StateMachine.kt | 34 +++++++++++++--------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/build.gradle b/build.gradle index bbc6c51..0851627 100644 --- a/build.gradle +++ b/build.gradle @@ -1,11 +1,13 @@ buildscript { - ext.kotlin_version = '1.3.21' + ext.kotlin_version = '1.3.72' + ext.atomicfu_version = '0.14.2' repositories { mavenCentral() jcenter() } dependencies { + classpath "org.jetbrains.kotlinx:atomicfu-gradle-plugin:$atomicfu_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.vanniktech:gradle-maven-publish-plugin:0.8.0' } @@ -16,23 +18,24 @@ version VERSION_NAME apply plugin: 'com.vanniktech.maven.publish' apply plugin: 'kotlin' +apply plugin: 'kotlinx-atomicfu' repositories { mavenCentral() } -dependencies { + dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" testImplementation 'junit:junit:4.12' testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0' testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" testImplementation 'org.assertj:assertj-core:3.11.1' -} + } compileKotlin { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8 -} + } compileTestKotlin { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8 } diff --git a/src/main/kotlin/com/tinder/StateMachine.kt b/src/main/kotlin/com/tinder/StateMachine.kt index 4b785c7..9218557 100644 --- a/src/main/kotlin/com/tinder/StateMachine.kt +++ b/src/main/kotlin/com/tinder/StateMachine.kt @@ -1,22 +1,28 @@ package com.tinder -import java.util.concurrent.atomic.AtomicReference +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.locks.SynchronizedObject +import kotlinx.atomicfu.locks.synchronized +import kotlin.reflect.KClass class StateMachine private constructor( private val graph: Graph ) { + private val lock = SynchronizedObject() + private val stateRef = atomic(graph.initialState) - private val stateRef = AtomicReference(graph.initialState) - - val state: STATE - get() = stateRef.get() + var state: STATE + get() = stateRef.value + private set(value) { + stateRef.value = value + } fun transition(event: EVENT): Transition { - val transition = synchronized(this) { - val fromState = stateRef.get() + val transition = synchronized(lock) { + val fromState = state val transition = fromState.getTransition(event) if (transition is Transition.Valid) { - stateRef.set(transition.toState) + state = transition.toState } transition } @@ -51,7 +57,7 @@ class StateMachine private construc private fun STATE.getDefinition() = graph.stateDefinitions .filter { it.key.matches(this) } .map { it.value } - .firstOrNull() ?: error("Missing definition for state ${this.javaClass.simpleName}!") + .firstOrNull() ?: error("Missing definition for state ${this::class.simpleName}!") private fun STATE.notifyOnEnter(cause: EVENT) { getDefinition().onEnterListeners.forEach { it(this, cause) } @@ -101,7 +107,7 @@ class StateMachine private construc } } - class Matcher private constructor(private val clazz: Class) { + class Matcher private constructor(private val clazz: KClass) { private val predicates = mutableListOf<(T) -> Boolean>({ clazz.isInstance(it) }) @@ -115,9 +121,9 @@ class StateMachine private construc fun matches(value: T) = predicates.all { it(value) } companion object { - fun any(clazz: Class): Matcher = Matcher(clazz) + fun any(clazz: KClass): Matcher = Matcher(clazz) - inline fun any(): Matcher = any(R::class.java) + inline fun any(): Matcher = any(R::class) inline fun eq(value: R): Matcher = any().where { this == value } } @@ -135,8 +141,8 @@ class StateMachine private construc } fun state( - stateMatcher: Matcher, - init: StateDefinitionBuilder.() -> Unit + stateMatcher: Matcher, + init: StateDefinitionBuilder.() -> Unit ) { stateDefinitions[stateMatcher] = StateDefinitionBuilder().apply(init).build() } From 6b7564cafddf9576522178c7e505a3a83c17540e Mon Sep 17 00:00:00 2001 From: ikurenkov Date: Sat, 16 May 2020 00:29:25 +0300 Subject: [PATCH 2/2] Support for Kotlin Native & Multiplatform: - Kotlin gradle project -> Kotlin multiplatform gradle project - Support for js, jvm, mac & windows - Tests are reimplemented due to platforms limitations --- build.gradle | 91 +++++- .../kotlin/com/tinder/StateMachineTest.kt | 305 ++++++++---------- 2 files changed, 215 insertions(+), 181 deletions(-) diff --git a/build.gradle b/build.gradle index 0851627..c8cf27f 100644 --- a/build.gradle +++ b/build.gradle @@ -9,33 +9,98 @@ buildscript { dependencies { classpath "org.jetbrains.kotlinx:atomicfu-gradle-plugin:$atomicfu_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.vanniktech:gradle-maven-publish-plugin:0.8.0' } } group GROUP version VERSION_NAME -apply plugin: 'com.vanniktech.maven.publish' -apply plugin: 'kotlin' -apply plugin: 'kotlinx-atomicfu' +apply plugin: "org.jetbrains.kotlin.multiplatform" +apply plugin: "kotlinx-atomicfu" repositories { mavenCentral() } + +kotlin { + jvm("jvm6") + jvm("jvm8") + + js { + browser { + } + nodejs { + } + } + // For ARM, should be changed to iosArm32 or iosArm64 + // For Linux, should be changed to e.g. linuxX64 + // For MacOS, should be changed to e.g. macosX64 + // For Windows, should be changed to e.g. mingwX64 + macosX64("macos") + linuxX64() + mingwX64() + + sourceSets { + commonMain { + kotlin.srcDir('src/main/') + dependencies { + implementation kotlin('stdlib-common') + implementation "org.jetbrains.kotlinx:atomicfu-common:$atomicfu_version" + } + } + commonTest { + kotlin.srcDir('src/test/') + dependencies { + implementation kotlin('test-common') + implementation kotlin('test-annotations-common') + } + } + + jvm6Main { + dependencies { + implementation kotlin('stdlib') + implementation kotlin('test-annotations-common') + } + + } + jvm6Test { dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation kotlin('test') + implementation kotlin('test-junit') + } + } + + jvm8Main { + task { + kotlinOption.jvmTarget = JavaVersion.VERSION_1_8 + } - testImplementation 'junit:junit:4.12' - testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0' - testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - testImplementation 'org.assertj:assertj-core:3.11.1' + dependencies { + implementation kotlin('stdlib-jdk8') + implementation kotlin('test-annotations-common') } + } -compileKotlin { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8 + jvm8Test { + dependencies { + implementation kotlin('test') + implementation kotlin('test-junit') } -compileTestKotlin { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8 + } + jsMain { + dependencies { + implementation kotlin('stdlib-js') } + } + jsTest { + dependencies { + implementation kotlin('test-js') + } + } + macosMain { + } + macosTest { + } + + } } diff --git a/src/test/kotlin/com/tinder/StateMachineTest.kt b/src/test/kotlin/com/tinder/StateMachineTest.kt index 5d6e579..c508fe1 100644 --- a/src/test/kotlin/com/tinder/StateMachineTest.kt +++ b/src/test/kotlin/com/tinder/StateMachineTest.kt @@ -1,20 +1,15 @@ package com.tinder -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.then -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.assertThatIllegalArgumentException -import org.assertj.core.api.Assertions.assertThatIllegalStateException -import org.junit.Test -import org.junit.experimental.runners.Enclosed -import org.junit.runner.RunWith - -@RunWith(Enclosed::class) +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + internal class StateMachineTest { class MatterStateMachine { - private val logger = mock() + private val logger = LoggerMock() + private val stateMachine = StateMachine.create { initialState(State.Solid) state { @@ -49,7 +44,7 @@ internal class StateMachineTest { @Test fun initialState_shouldBeSolid() { // Then - assertThat(stateMachine.state).isEqualTo(State.Solid) + assertEquals(State.Solid, stateMachine.state) } @Test @@ -61,11 +56,10 @@ internal class StateMachineTest { val transition = stateMachine.transition(Event.OnMelted) // Then - assertThat(stateMachine.state).isEqualTo(State.Liquid) - assertThat(transition).isEqualTo( - StateMachine.Transition.Valid(State.Solid, Event.OnMelted, State.Liquid, SideEffect.LogMelted) - ) - then(logger).should().log(ON_MELTED_MESSAGE) + assertEquals(State.Liquid, stateMachine.state) + assertEquals(StateMachine.Transition.Valid(State.Solid, Event.OnMelted, State.Liquid, SideEffect.LogMelted), + transition) + assertEquals(ON_MELTED_MESSAGE, logger.lastMessage) } @Test @@ -77,11 +71,10 @@ internal class StateMachineTest { val transition = stateMachine.transition(Event.OnFrozen) // Then - assertThat(stateMachine.state).isEqualTo(State.Solid) - assertThat(transition).isEqualTo( - StateMachine.Transition.Valid(State.Liquid, Event.OnFrozen, State.Solid, SideEffect.LogFrozen) - ) - then(logger).should().log(ON_FROZEN_MESSAGE) + assertEquals(State.Solid, stateMachine.state) + assertEquals(StateMachine.Transition.Valid(State.Liquid, Event.OnFrozen, State.Solid, SideEffect.LogFrozen), + transition) + assertEquals(ON_FROZEN_MESSAGE, logger.lastMessage) } @Test @@ -93,11 +86,10 @@ internal class StateMachineTest { val transition = stateMachine.transition(Event.OnVaporized) // Then - assertThat(stateMachine.state).isEqualTo(State.Gas) - assertThat(transition).isEqualTo( - StateMachine.Transition.Valid(State.Liquid, Event.OnVaporized, State.Gas, SideEffect.LogVaporized) - ) - then(logger).should().log(ON_VAPORIZED_MESSAGE) + assertEquals(State.Gas, stateMachine.state) + assertEquals(StateMachine.Transition.Valid(State.Liquid, Event.OnVaporized, State.Gas, SideEffect.LogVaporized), + transition) + assertEquals(ON_VAPORIZED_MESSAGE, logger.lastMessage) } @Test @@ -109,11 +101,10 @@ internal class StateMachineTest { val transition = stateMachine.transition(Event.OnCondensed) // Then - assertThat(stateMachine.state).isEqualTo(State.Liquid) - assertThat(transition).isEqualTo( - StateMachine.Transition.Valid(State.Gas, Event.OnCondensed, State.Liquid, SideEffect.LogCondensed) - ) - then(logger).should().log(ON_CONDENSED_MESSAGE) + assertEquals(State.Liquid, stateMachine.state) + assertEquals(StateMachine.Transition.Valid(State.Gas, Event.OnCondensed, State.Liquid, SideEffect.LogCondensed), + transition) + assertEquals(ON_CONDENSED_MESSAGE, logger.lastMessage) } private fun givenStateIs(state: State): StateMachine { @@ -145,9 +136,12 @@ internal class StateMachineTest { object LogVaporized : SideEffect() object LogCondensed : SideEffect() } - - interface Logger { - fun log(message: String) + //Utility class to mock logger due to Kotlin Multiplatform limitations + class LoggerMock { + lateinit var lastMessage: String; + fun log(message: String) { + lastMessage = message + } } } } @@ -187,7 +181,7 @@ internal class StateMachineTest { @Test fun initialState_shouldBeLocked() { // Then - assertThat(stateMachine.state).isEqualTo(State.Locked(credit = 0)) + assertEquals(State.Locked(credit = 0), stateMachine.state) } @Test @@ -196,15 +190,13 @@ internal class StateMachineTest { val transition = stateMachine.transition(Event.InsertCoin(10)) // Then - assertThat(stateMachine.state).isEqualTo(State.Locked(credit = 10)) - assertThat(transition).isEqualTo( - StateMachine.Transition.Valid( + assertEquals(State.Locked(credit = 10), stateMachine.state) + assertEquals(StateMachine.Transition.Valid( State.Locked(credit = 0), Event.InsertCoin(10), State.Locked(credit = 10), - null - ) - ) + null), + transition) } @Test @@ -216,15 +208,13 @@ internal class StateMachineTest { val transition = stateMachine.transition(Event.InsertCoin(15)) // Then - assertThat(stateMachine.state).isEqualTo(State.Unlocked) - assertThat(transition).isEqualTo( - StateMachine.Transition.Valid( + assertEquals(State.Unlocked, stateMachine.state) + assertEquals(StateMachine.Transition.Valid( State.Locked(credit = 35), Event.InsertCoin(15), State.Unlocked, - Command.OpenDoors - ) - ) + Command.OpenDoors), + transition) } @Test @@ -236,15 +226,13 @@ internal class StateMachineTest { val transition = stateMachine.transition(Event.InsertCoin(20)) // Then - assertThat(stateMachine.state).isEqualTo(State.Unlocked) - assertThat(transition).isEqualTo( - StateMachine.Transition.Valid( + assertEquals(State.Unlocked, stateMachine.state) + assertEquals(StateMachine.Transition.Valid( State.Locked(credit = 35), Event.InsertCoin(20), State.Unlocked, - Command.OpenDoors - ) - ) + Command.OpenDoors), + transition) } @Test @@ -256,15 +244,13 @@ internal class StateMachineTest { val transition = stateMachine.transition(Event.AdmitPerson) // Then - assertThat(stateMachine.state).isEqualTo(State.Locked(credit = 35)) - assertThat(transition).isEqualTo( - StateMachine.Transition.Valid( + assertEquals(State.Locked(credit = 35), stateMachine.state) + assertEquals(StateMachine.Transition.Valid( State.Locked(credit = 35), Event.AdmitPerson, State.Locked(credit = 35), - Command.SoundAlarm - ) - ) + Command.SoundAlarm), + transition) } @Test @@ -276,15 +262,13 @@ internal class StateMachineTest { val transitionToBroken = stateMachine.transition(Event.MachineDidFail) // Then - assertThat(stateMachine.state).isEqualTo(State.Broken(oldState = State.Locked(credit = 15))) - assertThat(transitionToBroken).isEqualTo( - StateMachine.Transition.Valid( + assertEquals(State.Broken(oldState = State.Locked(credit = 15)), stateMachine.state) + assertEquals(StateMachine.Transition.Valid( State.Locked(credit = 15), Event.MachineDidFail, State.Broken(oldState = State.Locked(credit = 15)), - Command.OrderRepair - ) - ) + Command.OrderRepair), + transitionToBroken) } @Test @@ -296,15 +280,13 @@ internal class StateMachineTest { val transition = stateMachine.transition(Event.AdmitPerson) // Then - assertThat(stateMachine.state).isEqualTo(State.Locked(credit = 0)) - assertThat(transition).isEqualTo( - StateMachine.Transition.Valid( + assertEquals(State.Locked(credit = 0), stateMachine.state) + assertEquals(StateMachine.Transition.Valid( State.Unlocked, Event.AdmitPerson, State.Locked(credit = 0), - Command.CloseDoors - ) - ) + Command.CloseDoors), + transition) } @Test @@ -316,15 +298,13 @@ internal class StateMachineTest { val transition = stateMachine.transition(Event.MachineRepairDidComplete) // Then - assertThat(stateMachine.state).isEqualTo(State.Locked(credit = 15)) - assertThat(transition).isEqualTo( - StateMachine.Transition.Valid( + assertEquals(State.Locked(credit = 15), stateMachine.state) + assertEquals(StateMachine.Transition.Valid( State.Broken(oldState = State.Locked(credit = 15)), Event.MachineRepairDidComplete, State.Locked(credit = 15), - null - ) - ) + null), + transition) } private fun givenStateIs(state: State): StateMachine { @@ -356,22 +336,21 @@ internal class StateMachineTest { } } - @RunWith(Enclosed::class) class ObjectStateMachine { class WithInitialState { - private val onTransitionListener1 = mock<(StateMachine.Transition) -> Unit>() - private val onTransitionListener2 = mock<(StateMachine.Transition) -> Unit>() - private val onStateAExitListener1 = mock Unit>() - private val onStateAExitListener2 = mock Unit>() - private val onStateCEnterListener1 = mock Unit>() - private val onStateCEnterListener2 = mock Unit>() + private val onTransitionListener1 = TransitionListenerMock<(StateMachine.Transition)>() + private val onTransitionListener2 = TransitionListenerMock<(StateMachine.Transition)>() + private val onStateAExitListener1 = StateListenerMock() + private val onStateAExitListener2 = StateListenerMock() + private val onStateCEnterListener1 = StateListenerMock() + private val onStateCEnterListener2 = StateListenerMock() private val stateMachine = StateMachine.create { initialState(State.A) state { - onExit(onStateAExitListener1) - onExit(onStateAExitListener2) + onExit(onStateAExitListener1::invoke) + onExit(onStateAExitListener2::invoke) on { transitionTo(State.B) } @@ -391,11 +370,11 @@ internal class StateMachineTest { on { dontTransition() } - onEnter(onStateCEnterListener1) - onEnter(onStateCEnterListener2) + onEnter(onStateCEnterListener1::invoke) + onEnter(onStateCEnterListener2::invoke) } - onTransition(onTransitionListener1) - onTransition(onTransitionListener2) + onTransition(onTransitionListener1::invoke) + onTransition(onTransitionListener2::invoke) } @Test @@ -404,7 +383,7 @@ internal class StateMachineTest { val state = stateMachine.state // Then - assertThat(state).isEqualTo(State.A) + assertEquals(State.A, state) } @Test @@ -413,17 +392,15 @@ internal class StateMachineTest { val transitionFromStateAToStateB = stateMachine.transition(Event.E1) // Then - assertThat(transitionFromStateAToStateB).isEqualTo( - StateMachine.Transition.Valid(State.A, Event.E1, State.B, null) - ) + assertEquals(StateMachine.Transition.Valid(State.A, Event.E1, State.B, null), + transitionFromStateAToStateB) // When val transitionFromStateBToStateC = stateMachine.transition(Event.E3) // Then - assertThat(transitionFromStateBToStateC).isEqualTo( - StateMachine.Transition.Valid(State.B, Event.E3, State.C, SideEffect.SE1) - ) + assertEquals(StateMachine.Transition.Valid(State.B, Event.E3, State.C, SideEffect.SE1), + transitionFromStateBToStateC) } @Test @@ -432,13 +409,13 @@ internal class StateMachineTest { stateMachine.transition(Event.E1) // Then - assertThat(stateMachine.state).isEqualTo(State.B) + assertEquals(State.B, stateMachine.state) // When stateMachine.transition(Event.E3) // Then - assertThat(stateMachine.state).isEqualTo(State.C) + assertEquals(State.C, stateMachine.state) } @Test @@ -447,23 +424,22 @@ internal class StateMachineTest { stateMachine.transition(Event.E1) // Then - then(onTransitionListener1).should().invoke( - StateMachine.Transition.Valid(State.A, Event.E1, State.B, null) - ) + assertEquals(StateMachine.Transition.Valid(State.A, Event.E1, State.B, null), + onTransitionListener1.callers.last()) // When stateMachine.transition(Event.E3) // Then - then(onTransitionListener2).should() - .invoke(StateMachine.Transition.Valid(State.B, Event.E3, State.C, SideEffect.SE1)) + assertEquals(StateMachine.Transition.Valid(State.B, Event.E3, State.C, SideEffect.SE1), + onTransitionListener2.callers.last()) // When stateMachine.transition(Event.E4) // Then - then(onTransitionListener2).should() - .invoke(StateMachine.Transition.Valid(State.C, Event.E4, State.C, null)) + assertEquals(StateMachine.Transition.Valid(State.C, Event.E4, State.C, null), + onTransitionListener2.callers.last()) } @Test @@ -472,8 +448,8 @@ internal class StateMachineTest { stateMachine.transition(Event.E2) // Then - then(onStateCEnterListener1).should().invoke(State.C, Event.E2) - then(onStateCEnterListener2).should().invoke(State.C, Event.E2) + assertEquals(listOf(State.C, Event.E2), onStateCEnterListener1.callers.last()) + assertEquals(listOf(State.C, Event.E2), onStateCEnterListener2.callers.last()) } @Test @@ -482,8 +458,8 @@ internal class StateMachineTest { stateMachine.transition(Event.E2) // Then - then(onStateAExitListener1).should().invoke(State.A, Event.E2) - then(onStateAExitListener2).should().invoke(State.A, Event.E2) + assertEquals(listOf(State.A, Event.E2), onStateAExitListener1.callers.last()) + assertEquals(listOf(State.A, Event.E2), onStateAExitListener2.callers.last()) } @Test @@ -493,19 +469,15 @@ internal class StateMachineTest { val transition = stateMachine.transition(Event.E3) // Then - assertThat(transition).isEqualTo( - StateMachine.Transition.Invalid(State.A, Event.E3) - ) - assertThat(stateMachine.state).isEqualTo(fromState) + assertEquals(StateMachine.Transition.Invalid(State.A, Event.E3), transition) + + assertEquals(fromState, stateMachine.state) } @Test fun transition_givenUndeclaredState_shouldThrowIllegalStateException() { // Then - assertThatIllegalStateException() - .isThrownBy { - stateMachine.transition(Event.E4) - } + assertFailsWith { stateMachine.transition(Event.E4) } } } @@ -514,9 +486,7 @@ internal class StateMachineTest { @Test fun create_givenNoInitialState_shouldThrowIllegalArgumentException() { // Then - assertThatIllegalArgumentException().isThrownBy { - StateMachine.create {} - } + assertFailsWith { StateMachine.create {} } } } @@ -543,22 +513,21 @@ internal class StateMachineTest { } } - @RunWith(Enclosed::class) class ConstantStateMachine { class WithInitialState { - private val onTransitionListener1 = mock<(StateMachine.Transition) -> Unit>() - private val onTransitionListener2 = mock<(StateMachine.Transition) -> Unit>() - private val onStateCEnterListener1 = mock Unit>() - private val onStateCEnterListener2 = mock Unit>() - private val onStateAExitListener1 = mock Unit>() - private val onStateAExitListener2 = mock Unit>() + private val onTransitionListener1 = TransitionListenerMock<(StateMachine.Transition)>(); + private val onTransitionListener2 = TransitionListenerMock<(StateMachine.Transition)>() + private val onStateCEnterListener1 = StateListenerMock() + private val onStateCEnterListener2 = StateListenerMock() + private val onStateAExitListener1 = StateListenerMock() + private val onStateAExitListener2 = StateListenerMock() private val stateMachine = StateMachine.create { initialState(STATE_A) state(STATE_A) { - onExit(onStateAExitListener1) - onExit(onStateAExitListener2) + onExit(onStateAExitListener1::invoke) + onExit(onStateAExitListener2::invoke) on(EVENT_1) { transitionTo(STATE_B) } @@ -575,11 +544,11 @@ internal class StateMachineTest { } } state(STATE_C) { - onEnter(onStateCEnterListener1) - onEnter(onStateCEnterListener2) + onEnter(onStateCEnterListener1::invoke) + onEnter(onStateCEnterListener2::invoke) } - onTransition(onTransitionListener1) - onTransition(onTransitionListener2) + onTransition(onTransitionListener1::invoke) + onTransition(onTransitionListener2::invoke) } @Test @@ -588,7 +557,7 @@ internal class StateMachineTest { val state = stateMachine.state // Then - assertThat(state).isEqualTo(STATE_A) + assertEquals(STATE_A, state) } @Test @@ -597,17 +566,15 @@ internal class StateMachineTest { val transitionFromStateAToStateB = stateMachine.transition(EVENT_1) // Then - assertThat(transitionFromStateAToStateB).isEqualTo( - StateMachine.Transition.Valid(STATE_A, EVENT_1, STATE_B, null) - ) + assertEquals(StateMachine.Transition.Valid(STATE_A, EVENT_1, STATE_B, null), + transitionFromStateAToStateB) // When val transitionFromStateBToStateC = stateMachine.transition(EVENT_3) // Then - assertThat(transitionFromStateBToStateC).isEqualTo( - StateMachine.Transition.Valid(STATE_B, EVENT_3, STATE_C, SIDE_EFFECT_1) - ) + assertEquals(StateMachine.Transition.Valid(STATE_B, EVENT_3, STATE_C, SIDE_EFFECT_1), + transitionFromStateBToStateC) } @Test @@ -616,13 +583,13 @@ internal class StateMachineTest { stateMachine.transition(EVENT_1) // Then - assertThat(stateMachine.state).isEqualTo(STATE_B) + assertEquals(STATE_B, stateMachine.state) // When stateMachine.transition(EVENT_3) // Then - assertThat(stateMachine.state).isEqualTo(STATE_C) + assertEquals(STATE_C, stateMachine.state) } @Test @@ -631,17 +598,13 @@ internal class StateMachineTest { stateMachine.transition(EVENT_1) // Then - then(onTransitionListener1).should().invoke( - StateMachine.Transition.Valid(STATE_A, EVENT_1, STATE_B, null) - ) + assertEquals(StateMachine.Transition.Valid(STATE_A, EVENT_1, STATE_B, null), onTransitionListener1.callers.last()) // When stateMachine.transition(EVENT_3) // Then - then(onTransitionListener2).should().invoke( - StateMachine.Transition.Valid(STATE_B, EVENT_3, STATE_C, SIDE_EFFECT_1) - ) + assertEquals(StateMachine.Transition.Valid(STATE_B, EVENT_3, STATE_C, SIDE_EFFECT_1), onTransitionListener2.callers.last()) } @Test @@ -650,8 +613,8 @@ internal class StateMachineTest { stateMachine.transition(EVENT_2) // Then - then(onStateCEnterListener1).should().invoke(STATE_C, EVENT_2) - then(onStateCEnterListener2).should().invoke(STATE_C, EVENT_2) + assertEquals(listOf(STATE_C, EVENT_2), onStateCEnterListener1.callers.last()) + assertEquals(listOf(STATE_C, EVENT_2), onStateCEnterListener2.callers.last()) } @Test @@ -660,8 +623,8 @@ internal class StateMachineTest { stateMachine.transition(EVENT_2) // Then - then(onStateAExitListener1).should().invoke(STATE_A, EVENT_2) - then(onStateAExitListener2).should().invoke(STATE_A, EVENT_2) + assertEquals(listOf(STATE_A, EVENT_2), onStateAExitListener1.callers.last()) + assertEquals(listOf(STATE_A, EVENT_2), onStateAExitListener2.callers.last()) } @Test @@ -671,19 +634,15 @@ internal class StateMachineTest { val transition = stateMachine.transition(EVENT_3) // Then - assertThat(transition).isEqualTo( - StateMachine.Transition.Invalid(STATE_A, EVENT_3) - ) - assertThat(stateMachine.state).isEqualTo(fromState) + assertEquals(StateMachine.Transition.Invalid(STATE_A, EVENT_3), + transition) + assertEquals(fromState, stateMachine.state) } @Test fun transition_givenUndeclaredState_shouldThrowIllegalStateException() { // Then - assertThatIllegalStateException() - .isThrownBy { - stateMachine.transition(EVENT_4) - } + assertFailsWith { stateMachine.transition(EVENT_4) } } } @@ -692,9 +651,7 @@ internal class StateMachineTest { @Test fun create_givenNoInitialState_shouldThrowIllegalArgumentException() { // Then - assertThatIllegalArgumentException().isThrownBy { - StateMachine.create {} - } + assertFailsWith { StateMachine.create {} } } } @@ -713,9 +670,8 @@ internal class StateMachineTest { @Test fun transition_givenMissingDestinationStateDefinition_shouldThrowIllegalStateExceptionWithStateName() { // Then - assertThatIllegalStateException() - .isThrownBy { stateMachine.transition(EVENT_1) } - .withMessage("Missing definition for state ${STATE_B.javaClass.simpleName}!") + val exception = assertFailsWith { stateMachine.transition(EVENT_1) } + assertEquals("Missing definition for state ${STATE_B::class.simpleName}!", exception.message) } } @@ -733,5 +689,18 @@ internal class StateMachineTest { private const val SIDE_EFFECT_1 = "alpha" } } + //Utility classes to mock listeners due to Kotlin Multiplatform limitations + private class TransitionListenerMock { + val callers = mutableListOf() + fun invoke(p1: T) { + callers.add(p1); + } + } + private class StateListenerMock { + val callers = mutableListOf>() + fun invoke(p1: T, p2: K) { + callers.add(listOf(p1, p2)); + } + } }