Skip to content

Commit

Permalink
Add GetStructureHashCodeVisitorTest
Browse files Browse the repository at this point in the history
  • Loading branch information
nsk90 committed Mar 6, 2024
1 parent cc6106d commit 4010b4c
Show file tree
Hide file tree
Showing 11 changed files with 330 additions and 14 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
[![Share on Reddit](https://img.shields.io/badge/reddit-share-red?logo=reddit&style=flat)](https://www.reddit.com/submit?url=https%3A%2F%2Fgithub.com%2Fkstatemachine%2Fkstatemachine&title=I%20like%20KStateMachine%20library)


[Documentation](https://kstatemachine.github.io/kstatemachine) | [Sponsors](#sponsors-) | [Quick start](#quick-start-sample) | [Samples](#samples) | [Install](#install) | [Contribution](#contribution) | [License](#license) | [Discussions](https://github.com/kstatemachine/kstatemachine/discussions)
**[Documentation](https://kstatemachine.github.io/kstatemachine) | [Sponsors](#sponsors-) | [Quick start](#quick-start-sample) | [Samples](#samples) | [Install](#install) | [Contribution](#contribution) | [License](#license) | [Discussions](https://github.com/kstatemachine/kstatemachine/discussions)**

KStateMachine is a Kotlin DSL library for creating [state machines](https://en.wikipedia.org/wiki/Finite-state_machine)
and [statecharts](https://www.sciencedirect.com/science/article/pii/0167642387900359/pdf).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ru.nsk.kstatemachine.event

import ru.nsk.kstatemachine.state.DataState
import ru.nsk.kstatemachine.transition.TransitionParams
import kotlin.reflect.KClass

/**
* Allows to extract data for [DataState] from any [Event]
Expand All @@ -10,11 +11,14 @@ import ru.nsk.kstatemachine.transition.TransitionParams
* when implementing custom [DataExtractor].
*/
interface DataExtractor<D : Any> {
val dataClass: KClass<D>
suspend fun extractFinishedEvent(transitionParams: TransitionParams<*>, event: FinishedEvent): D?
suspend fun extract(transitionParams: TransitionParams<*>): D?
}

inline fun <reified D : Any> defaultDataExtractor() = object : DataExtractor<D> {
override val dataClass = D::class

override suspend fun extractFinishedEvent(transitionParams: TransitionParams<*>, event: FinishedEvent) =
event.data as? D

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import kotlin.reflect.KClass
/**
* Adds an ability to select who [Transition] matches [Event] subclass
*/
abstract class EventMatcher<E : Event>(val eventClass: KClass<E>) {
abstract suspend fun match(value: Event): Boolean
interface EventMatcher<E : Event> {
val eventClass: KClass<E>
suspend fun match(value: Event): Boolean

companion object {
/** This matcher is used by default, allowing [Event] subclasses */
inline fun <reified E : Event> isInstanceOf() = object : EventMatcher<E>(E::class) {
inline fun <reified E : Event> isInstanceOf() = object : EventMatcher<E> {
override val eventClass = E::class
override suspend fun match(value: Event) = value is E
}
}
Expand All @@ -23,10 +25,12 @@ abstract class EventMatcher<E : Event>(val eventClass: KClass<E>) {
inline fun <reified E : Event> TransitionBuilder<E>.isInstanceOf() = EventMatcher.isInstanceOf<E>()

@Suppress("UNUSED") // The unused warning is probably a bug
inline fun <reified E : Event> TransitionBuilder<E>.isEqual() = object : EventMatcher<E>(E::class) {
inline fun <reified E : Event> TransitionBuilder<E>.isEqual() = object : EventMatcher<E> {
override val eventClass = E::class
override suspend fun match(value: Event) = value::class == E::class
}

fun finishedEventMatcher(state: IState) = object : EventMatcher<FinishedEvent>(FinishedEvent::class) {
fun finishedEventMatcher(state: IState) = object : EventMatcher<FinishedEvent> {
override val eventClass = FinishedEvent::class
override suspend fun match(value: Event) = value is FinishedEvent && value.state === state
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ru.nsk.kstatemachine.state
import ru.nsk.kstatemachine.event.*
import ru.nsk.kstatemachine.state.ChildMode.EXCLUSIVE
import ru.nsk.kstatemachine.transition.TransitionParams
import kotlin.reflect.KClass

/** inline constructor function */
inline fun <reified D : Any> defaultDataState(
Expand All @@ -26,6 +27,7 @@ open class DefaultDataState<D : Any>(
get() = checkNotNull(_lastData ?: defaultData) {
"Last data is not available yet in $this, and default data not provided"
}
override val dataClass: KClass<D> get() = dataExtractor.dataClass

override suspend fun onDoEnter(transitionParams: TransitionParams<*>) {
fun assign(data: D?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ interface DataState<D : Any> : IState, DataTransitionStateApi<D> {
* If state was not entered this property falls back to [defaultData] if it was specified
*/
val lastData: D

val dataClass: KClass<D>
}

/**
Expand Down Expand Up @@ -305,15 +307,15 @@ fun IState.initialChoiceState(
choiceAction: suspend EventAndArgument<*>.() -> State
) = addInitialState(DefaultChoiceState(name, choiceAction = choiceAction))

fun <D : Any> IState.choiceDataState(
inline fun <reified D : Any> IState.choiceDataState(
name: String? = null,
choiceAction: suspend EventAndArgument<*>.() -> DataState<D>
) = addState(DefaultChoiceDataState(name, choiceAction = choiceAction))
noinline choiceAction: suspend EventAndArgument<*>.() -> DataState<D>
) = addState(DefaultChoiceDataState(name, D::class, choiceAction = choiceAction))

fun <D : Any> IState.initialChoiceDataState(
inline fun <reified D : Any> IState.initialChoiceDataState(
name: String? = null,
choiceAction: suspend EventAndArgument<*>.() -> DataState<D>
) = addInitialState(DefaultChoiceDataState(name, choiceAction = choiceAction))
noinline choiceAction: suspend EventAndArgument<*>.() -> DataState<D>
) = addInitialState(DefaultChoiceDataState(name, D::class, choiceAction = choiceAction))

fun IState.historyState(
name: String? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ru.nsk.kstatemachine.transition.EventAndArgument
import ru.nsk.kstatemachine.transition.TransitionDirection
import ru.nsk.kstatemachine.transition.TransitionDirectionProducerPolicy
import ru.nsk.kstatemachine.transition.noTransition
import kotlin.reflect.KClass

open class DefaultChoiceState(
name: String? = null,
Expand All @@ -18,6 +19,7 @@ open class DefaultChoiceState(

open class DefaultChoiceDataState<D : Any>(
name: String? = null,
override val dataClass: KClass<D>,
private val choiceAction: suspend EventAndArgument<*>.() -> DataState<D>,
) : DataState<D>, BasePseudoState(name), RedirectPseudoState {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package ru.nsk.kstatemachine.visitors

import ru.nsk.kstatemachine.event.Event
import ru.nsk.kstatemachine.state.DataState
import ru.nsk.kstatemachine.state.DefaultDataState
import ru.nsk.kstatemachine.state.HistoryState
import ru.nsk.kstatemachine.state.IState
import ru.nsk.kstatemachine.statemachine.StateMachine
import ru.nsk.kstatemachine.transition.Transition

/**
* This visitor collects structural information about [StateMachine] in order to compare two [StateMachine]
* instances for structural equality
*/
internal class GetStructureHashCodeVisitor : RecursiveVisitor {
private val nodes = mutableListOf<String>()
val structureHashCode get() = nodes.hashCode()

override fun visit(machine: StateMachine) {
nodes += machine.stateInfo()
machine.visitChildren()
}

override fun visit(state: IState) {
if (state !is StateMachine) { // do not check nested machines
nodes += state.stateInfo()
state.visitChildren()
} else {
nodes += "class:${state::class.simpleName}, name:${state.name}"
}
}

private fun IState.stateInfo() = "class:${this::class.simpleName}, " +
"name:$name, " +
"statesCount:${states.size}, " +
"transitionsCount:${transitions.size}, " +
"childMode:$childMode" +
if (this is HistoryState) ", historyType:$historyType" else "" +
if (this is DataState<*>) ", dataClass:$dataClass, defaultData:$defaultData" else ""

override fun <E : Event> visit(transition: Transition<E>) {
nodes += "class:${transition::class.simpleName}, " +
"name:${transition.name}, " +
"type:${transition.type}, " +
"event:${transition.eventMatcher.eventClass}"
}
}

fun StateMachine.getStructureHashCode() = with(GetStructureHashCodeVisitor()) {
visit(this@getStructureHashCode)
structureHashCode
}
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ class CoroutinesTest : StringSpec({
statesFlow.first().shouldContainExactlyInAnyOrder(state2)
}

"f:context switching" {
"context switching" {
println(""+ Thread.currentThread() + Thread.currentThread().hashCode() )
val scope = CoroutineScope(EmptyCoroutineContext)
val scope1 = CoroutineScope(Dispatchers.Default)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ru.nsk.kstatemachine.state

import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.shouldBe
import io.mockk.verifySequence
import ru.nsk.kstatemachine.*
import ru.nsk.kstatemachine.event.DataEvent
Expand Down Expand Up @@ -61,7 +62,7 @@ class ChoiceStateTest : StringSpec({
verifySequence { callbacks.onStateEntry(State2) }
}

"initial choice state" {
"initial choiceState" {
val callbacks = mockkCallbacks()

createTestStateMachine(coroutineStarterType) {
Expand All @@ -71,6 +72,17 @@ class ChoiceStateTest : StringSpec({
verifySequence { callbacks.onStateEntry(State2) }
}

"initial choiceDataState" {
val callbacks = mockkCallbacks()
lateinit var state2: DataState<Int>
createTestStateMachine(coroutineStarterType) {
initialChoiceDataState("choice") { state2 }
state2 = dataState(defaultData = 42) { callbacks.listen(this) }
}
verifySequence { callbacks.onStateEntry(state2) }
state2.data shouldBe 42
}

"initial choice state on entry parent" {
lateinit var state1: State
lateinit var state2: State
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import ru.nsk.kstatemachine.statemachine.processEventBlocking
import ru.nsk.kstatemachine.transition.TypesafeTransitionTestData.CustomDataEvent
import ru.nsk.kstatemachine.transition.TypesafeTransitionTestData.IdEvent
import ru.nsk.kstatemachine.transition.TypesafeTransitionTestData.NameEvent
import kotlin.reflect.KClass

private object TypesafeTransitionTestData {
class CustomDataEvent(val value: Int) : Event
Expand Down Expand Up @@ -342,6 +343,8 @@ class TypesafeTransitionTest : StringSpec({
dataState = dataState(
"data state",
dataExtractor = object : DataExtractor<Int> {
override val dataClass = Int::class

override suspend fun extractFinishedEvent(transitionParams: TransitionParams<*>, event: FinishedEvent) =
event.data as? Int

Expand Down
Loading

0 comments on commit 4010b4c

Please sign in to comment.