Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions buildSrc/src/main/kotlin/ru/nsk/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ object Versions {
const val libraryVersion = "0.35.0"

// tools
const val kotlin = "2.2.0"
const val kotlinDokka = "2.0.0"
const val kotlin = "2.3.0"
const val kotlinDokka = "2.1.0"
const val kotlinBinaryCompatibilityValidatorPlugin = "0.18.1"
const val kotlinKoverPlugin = "0.9.1"
const val kotlinKoverPlugin = "0.9.4"

// compatibility
const val jdkVersion = 17
const val languageVersion = "1.8"
const val apiVersion = "1.8"
const val languageVersion = "2.0"
const val apiVersion = "2.0"

// dependencies
const val coroutinesCore = "1.10.2"
const val serialization = "1.9.0"
const val serialization = "1.10.0"

// test dependencies
const val mockk = "1.14.7"
const val kotest = "6.0.7"
const val kotest = "6.1.0"
}
2 changes: 1 addition & 1 deletion docs/notes/publishing.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Publishing to maven central
The process of publishing to maven central is absolutely non-intuitive, not visualizable and confusing.
I faced unexpectable behaviour when my publications had a random count of components in the UI
I faced unexpectable behavior when my publications had a random count of components in the UI
https://central.sonatype.com/publishing/deployments
Publishing such a library version causes error on client's side, when he tries to resolve all required dependencies.
There are absolutely no errors nor in publishing logs nor in web UI.
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import ru.nsk.kstatemachine.state.ChildMode
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.coroutines.ContinuationInterceptor
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext

Expand Down Expand Up @@ -42,10 +43,31 @@ suspend fun createStateMachine(
contract {
callsInPlace(init, InvocationKind.EXACTLY_ONCE)
}
checkCoroutineScopeValidity(scope, creationArguments)
return CoroutinesLibCoroutineAbstraction(scope)
.createStateMachine(name, childMode, start, creationArguments, init)
}

private fun checkCoroutineScopeValidity(scope: CoroutineScope, creationArguments: CreationArguments) {
if (creationArguments.skipCoroutineScopeValidityCheck) return

val dispatcher = scope.coroutineContext[ContinuationInterceptor]
val dispatcherName = dispatcher.toString()
if (dispatcher === Dispatchers.Default ||
dispatcherName == "Dispatchers.Default" ||
dispatcherName.startsWith("Dispatchers.Default.limitedParallelism") ||
dispatcherName == "Dispatchers.IO" || // can't get IO dispatcher in commonMain
dispatcherName.startsWith("Dispatchers.IO.limitedParallelism")
) {
error(
"Using Dispatchers.Default or Dispatchers.IO for StateMachine even with limitedParallelism(1) is the most likely an error," +
" as it is multi-threaded, see the docs: \n" +
"https://kstatemachine.github.io/kstatemachine/pages/multithreading.html#use-single-threaded-coroutinescope" +
"You can opt-out this check by CreationArguments::skipCoroutineScopeValidityCheck flag."
)
}
}

/**
* Processes event in async fashion (using launch() to start new coroutine).
*
Expand Down
11 changes: 9 additions & 2 deletions kstatemachine/api/kstatemachine.api
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ public final class ru/nsk/kstatemachine/persistence/StrictValidator : ru/nsk/kst
public final class ru/nsk/kstatemachine/persistence/WarningType : java/lang/Enum {
public static final field ProcessingResultNotMatch Lru/nsk/kstatemachine/persistence/WarningType;
public static final field RecordedAndProcessedEventCountNotMatch Lru/nsk/kstatemachine/persistence/WarningType;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lru/nsk/kstatemachine/persistence/WarningType;
public static fun values ()[Lru/nsk/kstatemachine/persistence/WarningType;
}
Expand Down Expand Up @@ -256,6 +257,7 @@ public class ru/nsk/kstatemachine/state/BaseStateImpl : ru/nsk/kstatemachine/sta
public final class ru/nsk/kstatemachine/state/ChildMode : java/lang/Enum {
public static final field EXCLUSIVE Lru/nsk/kstatemachine/state/ChildMode;
public static final field PARALLEL Lru/nsk/kstatemachine/state/ChildMode;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lru/nsk/kstatemachine/state/ChildMode;
public static fun values ()[Lru/nsk/kstatemachine/state/ChildMode;
}
Expand Down Expand Up @@ -372,6 +374,7 @@ public final class ru/nsk/kstatemachine/state/HistoryState$DefaultImpls {
public final class ru/nsk/kstatemachine/state/HistoryType : java/lang/Enum {
public static final field DEEP Lru/nsk/kstatemachine/state/HistoryType;
public static final field SHALLOW Lru/nsk/kstatemachine/state/HistoryType;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lru/nsk/kstatemachine/state/HistoryType;
public static fun values ()[Lru/nsk/kstatemachine/state/HistoryType;
}
Expand Down Expand Up @@ -616,6 +619,7 @@ public abstract interface class ru/nsk/kstatemachine/statemachine/CreationArgume
public abstract fun getDoNotThrowOnMultipleTransitionsMatch ()Z
public abstract fun getEventRecordingArguments ()Lru/nsk/kstatemachine/statemachine/EventRecordingArguments;
public abstract fun getRequireNonBlankNames ()Z
public abstract fun getSkipCoroutineScopeValidityCheck ()Z
public abstract fun isUndoEnabled ()Z
}

Expand All @@ -624,11 +628,13 @@ public abstract interface class ru/nsk/kstatemachine/statemachine/CreationArgume
public abstract fun getDoNotThrowOnMultipleTransitionsMatch ()Z
public abstract fun getEventRecordingArguments ()Lru/nsk/kstatemachine/statemachine/EventRecordingArguments;
public abstract fun getRequireNonBlankNames ()Z
public abstract fun getSkipCoroutineScopeValidityCheck ()Z
public abstract fun isUndoEnabled ()Z
public abstract fun setAutoDestroyOnStatesReuse (Z)V
public abstract fun setDoNotThrowOnMultipleTransitionsMatch (Z)V
public abstract fun setEventRecordingArguments (Lru/nsk/kstatemachine/statemachine/EventRecordingArguments;)V
public abstract fun setRequireNonBlankNames (Z)V
public abstract fun setSkipCoroutineScopeValidityCheck (Z)V
public abstract fun setUndoEnabled (Z)V
}

Expand Down Expand Up @@ -662,6 +668,7 @@ public final class ru/nsk/kstatemachine/statemachine/ProcessingResult : java/lan
public static final field IGNORED Lru/nsk/kstatemachine/statemachine/ProcessingResult;
public static final field PENDING Lru/nsk/kstatemachine/statemachine/ProcessingResult;
public static final field PROCESSED Lru/nsk/kstatemachine/statemachine/ProcessingResult;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lru/nsk/kstatemachine/statemachine/ProcessingResult;
public static fun values ()[Lru/nsk/kstatemachine/statemachine/ProcessingResult;
}
Expand Down Expand Up @@ -959,8 +966,6 @@ public final class ru/nsk/kstatemachine/transition/TransitionParams {
public final fun component2 ()Lru/nsk/kstatemachine/transition/TransitionDirection;
public final fun component3 ()Lru/nsk/kstatemachine/event/Event;
public final fun component4 ()Ljava/lang/Object;
public final fun copy (Lru/nsk/kstatemachine/transition/Transition;Lru/nsk/kstatemachine/transition/TransitionDirection;Lru/nsk/kstatemachine/event/Event;Ljava/lang/Object;)Lru/nsk/kstatemachine/transition/TransitionParams;
public static synthetic fun copy$default (Lru/nsk/kstatemachine/transition/TransitionParams;Lru/nsk/kstatemachine/transition/Transition;Lru/nsk/kstatemachine/transition/TransitionDirection;Lru/nsk/kstatemachine/event/Event;Ljava/lang/Object;ILjava/lang/Object;)Lru/nsk/kstatemachine/transition/TransitionParams;
public fun equals (Ljava/lang/Object;)Z
public final fun getArgument ()Ljava/lang/Object;
public final fun getDirection ()Lru/nsk/kstatemachine/transition/TransitionDirection;
Expand All @@ -973,11 +978,13 @@ public final class ru/nsk/kstatemachine/transition/TransitionParams {
public final class ru/nsk/kstatemachine/transition/TransitionParamsKt {
public static final fun getUnwrappedArgument (Lru/nsk/kstatemachine/transition/TransitionParams;)Ljava/lang/Object;
public static final fun getUnwrappedEvent (Lru/nsk/kstatemachine/transition/TransitionParams;)Lru/nsk/kstatemachine/event/Event;
public static final fun isStartTransition (Lru/nsk/kstatemachine/transition/TransitionParams;)Z
}

public final class ru/nsk/kstatemachine/transition/TransitionType : java/lang/Enum {
public static final field EXTERNAL Lru/nsk/kstatemachine/transition/TransitionType;
public static final field LOCAL Lru/nsk/kstatemachine/transition/TransitionType;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lru/nsk/kstatemachine/transition/TransitionType;
public static fun values ()[Lru/nsk/kstatemachine/transition/TransitionType;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ interface CreationArguments {
* Default: null
*/
val eventRecordingArguments: EventRecordingArguments?

/**
* The library checks if you are trying to use multithreaded Dispatcher like
* Dispatchers.Default or Dispatcher.IO which is usually an error.
* @see [https://kstatemachine.github.io/kstatemachine/pages/multithreading.html#use-single-threaded-coroutinescope]
* You can skip this validation setting the flag to true.
* Default: false
*/
val skipCoroutineScopeValidityCheck: Boolean
}

interface CreationArgumentsBuilder : CreationArguments {
Expand All @@ -56,14 +65,16 @@ interface CreationArgumentsBuilder : CreationArguments {
override var doNotThrowOnMultipleTransitionsMatch: Boolean
override var requireNonBlankNames: Boolean
override var eventRecordingArguments: EventRecordingArguments?
override var skipCoroutineScopeValidityCheck: Boolean
}

private data class CreationArgumentsBuilderImpl(
override var autoDestroyOnStatesReuse: Boolean = true,
override var isUndoEnabled: Boolean = false,
override var doNotThrowOnMultipleTransitionsMatch: Boolean = false,
override var requireNonBlankNames: Boolean = false,
override var eventRecordingArguments: EventRecordingArguments? = null
override var eventRecordingArguments: EventRecordingArguments? = null,
override var skipCoroutineScopeValidityCheck: Boolean = false,
) : CreationArgumentsBuilder

@OptIn(ExperimentalContracts::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
package ru.nsk.kstatemachine.transition

import ru.nsk.kstatemachine.event.Event
import ru.nsk.kstatemachine.event.StartEvent
import ru.nsk.kstatemachine.event.WrappedEvent
import ru.nsk.kstatemachine.statemachine.StateMachineDslMarker

@ConsistentCopyVisibility
@StateMachineDslMarker
data class TransitionParams<E : Event> internal constructor(
val transition: Transition<E>,
Expand All @@ -34,4 +36,12 @@ val TransitionParams<*>.unwrappedEvent get() = if (event is WrappedEvent) event.
* Convenience property for unwrapping original argument.
* If the event is not [WrappedEvent] this is same as [TransitionParams.argument] property
*/
val TransitionParams<*>.unwrappedArgument get() = if (event is WrappedEvent) event.argument else argument
val TransitionParams<*>.unwrappedArgument get() = if (event is WrappedEvent) event.argument else argument

/**
* Returns true is the transition is triggered by [StartEvent].
* This means that the StateMachine is starting.
* Might be useful to check if you're entering some State just by a machine startup
* or by outside event.
*/
val TransitionParams<*>.isStartTransition get() = event is StartEvent
9 changes: 0 additions & 9 deletions tests/src/commonTest/kotlin/ru/nsk/kstatemachine/TestUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ enum class CoroutineStarterType {
* but it should be ok as it happens sequentially.
*/
COROUTINES_LIB_SINGLE_THREAD_DISPATCHER,
COROUTINES_LIB_DEFAULT_LIMITED_DISPATCHER,
}

@OptIn(ExperimentalCoroutinesApi::class)
Expand Down Expand Up @@ -132,13 +131,5 @@ suspend fun createTestStateMachine(
creationArguments,
init = init
)
CoroutineStarterType.COROUTINES_LIB_DEFAULT_LIMITED_DISPATCHER -> createStateMachine(
CoroutineScope(Dispatchers.Default.limitedParallelism(1)), // does not guarantee same thread for each task
name,
childMode,
start,
creationArguments,
init = init
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,45 @@ package ru.nsk.kstatemachine.state
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe
import io.mockk.coVerify
import io.mockk.spyk
import io.mockk.mockk
import ru.nsk.kstatemachine.CoroutineStarterType
import ru.nsk.kstatemachine.createTestStateMachine
import ru.nsk.kstatemachine.state.StateCleanupTestData.State1
import ru.nsk.kstatemachine.statemachine.destroyBlocking

private object StateCleanupTestData {
class State1 : DefaultState("state1")
class State1(private val onCleanupListener: () -> Unit) : DefaultState("state1") {
override suspend fun onCleanup() {
super.onCleanup()
onCleanupListener()
}
}
}

class StateCleanupTest : FreeSpec({
CoroutineStarterType.entries.forEach { coroutineStarterType ->
"$coroutineStarterType" - {
"cleanup is not called" {
val state = spyk<State1>()
val listener = mockk<() -> Unit>(relaxed = true)
val state = State1(listener)
useInMachine(coroutineStarterType, state)
coVerify(inverse = true) { state.onCleanup() }
coVerify(inverse = true) { listener() }
}

"cleanup is called on machine manual destruction" {
val state = spyk<State1>()
val listener = mockk<() -> Unit>(relaxed = true)
val state = State1(listener)
useInMachine(coroutineStarterType, state).destroyBlocking()
coVerify(exactly = 1) { state.onCleanup() }
coVerify(exactly = 1) { listener() }
}

"cleanup is called on machine auto destruction" {
val state = spyk<State1>()
val listener = mockk<() -> Unit>(relaxed = true)
val state = State1(listener)
val machine1 = useInMachine(coroutineStarterType, state)
val machine2 = useInMachine(coroutineStarterType, state)

coVerify(exactly = 1) { state.onCleanup() }
coVerify(exactly = 1) { listener() }
machine1.isDestroyed shouldBe true
machine2.isDestroyed shouldBe false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@ import io.kotest.assertions.throwables.shouldThrowUnitWithMessage
import io.kotest.assertions.throwables.shouldThrowWithMessage
import io.kotest.core.spec.style.FreeSpec
import io.kotest.datatest.withData
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldEndWith
import io.mockk.called
import io.mockk.verify
import io.mockk.verifySequence
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import ru.nsk.kstatemachine.*
import ru.nsk.kstatemachine.event.Event
import ru.nsk.kstatemachine.event.EventMatcher
Expand All @@ -26,17 +30,44 @@ import ru.nsk.kstatemachine.state.*
import ru.nsk.kstatemachine.statemachine.StateMachineTestData.OffEvent
import ru.nsk.kstatemachine.statemachine.StateMachineTestData.OnEvent
import ru.nsk.kstatemachine.testing.Testing.startFromBlocking
import ru.nsk.kstatemachine.transition.DefaultTransition
import ru.nsk.kstatemachine.transition.Transition
import ru.nsk.kstatemachine.transition.TransitionType
import ru.nsk.kstatemachine.transition.onTriggered
import ru.nsk.kstatemachine.transition.*

private object StateMachineTestData {
object OnEvent : Event
object OffEvent : Event
}

class StateMachineTest : FreeSpec({
withData(
nameFn = { "dispatcher: $it" },
CoroutineScope(Dispatchers.Default),
CoroutineScope(Dispatchers.Default.limitedParallelism(1)),
CoroutineScope(Dispatchers.IO),
CoroutineScope(Dispatchers.IO.limitedParallelism(1)),
) { scope ->
"scope validation" {
try {
createStateMachine(
scope,
creationArguments = buildCreationArguments { skipCoroutineScopeValidityCheck = true }
) {
initialState("initial")
}

shouldThrowWithMessage<IllegalStateException>(
"Using Dispatchers.Default or Dispatchers.IO for StateMachine even with limitedParallelism(1) is the most likely an error, as it is multi-threaded, see the docs: \n" +
"https://kstatemachine.github.io/kstatemachine/pages/multithreading.html#use-single-threaded-coroutinescopeYou can opt-out this check by CreationArguments::skipCoroutineScopeValidityCheck flag."
) {
createStateMachine(scope) {
initialState("initial")
}
}
} finally {
scope.cancel()
}
}
}

CoroutineStarterType.entries.forEach { coroutineStarterType ->
"$coroutineStarterType" - {
"no initial state" {
Expand Down Expand Up @@ -363,6 +394,22 @@ class StateMachineTest : FreeSpec({
}
}

"isStartTransition" {
lateinit var state2: State
val machine = createTestStateMachine(coroutineStarterType) {
state2 = state("state2") {
onEntry { it.isStartTransition shouldBe false }
}
initialState("initial") {
onEntry { it.isStartTransition shouldBe true }
transitionOn<SwitchEvent> { targetState = { state2 } }
}
onStarted { it.isStartTransition shouldBe true }
}
machine.processEvent(SwitchEvent)
machine.activeStates().shouldContain(state2)
}

"destroy from onStart" {
val callbacks = mockkCallbacks()
val machine = createTestStateMachine(coroutineStarterType) {
Expand Down
Loading
Loading