Skip to content

Commit 8413ffa

Browse files
authored
Merge pull request #122 from KStateMachine/deps
Update dependencies
2 parents f289196 + 70646b8 commit 8413ffa

File tree

11 files changed

+131
-36
lines changed

11 files changed

+131
-36
lines changed

buildSrc/src/main/kotlin/ru/nsk/Versions.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,21 @@ object Versions {
55
const val libraryVersion = "0.35.0"
66

77
// tools
8-
const val kotlin = "2.2.0"
9-
const val kotlinDokka = "2.0.0"
8+
const val kotlin = "2.3.0"
9+
const val kotlinDokka = "2.1.0"
1010
const val kotlinBinaryCompatibilityValidatorPlugin = "0.18.1"
11-
const val kotlinKoverPlugin = "0.9.1"
11+
const val kotlinKoverPlugin = "0.9.4"
1212

1313
// compatibility
1414
const val jdkVersion = 17
15-
const val languageVersion = "1.8"
16-
const val apiVersion = "1.8"
15+
const val languageVersion = "2.0"
16+
const val apiVersion = "2.0"
1717

1818
// dependencies
1919
const val coroutinesCore = "1.10.2"
20-
const val serialization = "1.9.0"
20+
const val serialization = "1.10.0"
2121

2222
// test dependencies
2323
const val mockk = "1.14.7"
24-
const val kotest = "6.0.7"
24+
const val kotest = "6.1.0"
2525
}

docs/notes/publishing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Publishing to maven central
22
The process of publishing to maven central is absolutely non-intuitive, not visualizable and confusing.
3-
I faced unexpectable behaviour when my publications had a random count of components in the UI
3+
I faced unexpectable behavior when my publications had a random count of components in the UI
44
https://central.sonatype.com/publishing/deployments
55
Publishing such a library version causes error on client's side, when he tries to resolve all required dependencies.
66
There are absolutely no errors nor in publishing logs nor in web UI.

gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
44
networkTimeout=10000
55
validateDistributionUrl=true
66
zipStoreBase=GRADLE_USER_HOME

kstatemachine-coroutines/src/commonMain/kotlin/ru/nsk/kstatemachine/statemachine/CoroutinesStateMachine.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import ru.nsk.kstatemachine.state.ChildMode
1515
import kotlin.contracts.ExperimentalContracts
1616
import kotlin.contracts.InvocationKind
1717
import kotlin.contracts.contract
18+
import kotlin.coroutines.ContinuationInterceptor
1819
import kotlin.coroutines.CoroutineContext
1920
import kotlin.coroutines.EmptyCoroutineContext
2021

@@ -42,10 +43,31 @@ suspend fun createStateMachine(
4243
contract {
4344
callsInPlace(init, InvocationKind.EXACTLY_ONCE)
4445
}
46+
checkCoroutineScopeValidity(scope, creationArguments)
4547
return CoroutinesLibCoroutineAbstraction(scope)
4648
.createStateMachine(name, childMode, start, creationArguments, init)
4749
}
4850

51+
private fun checkCoroutineScopeValidity(scope: CoroutineScope, creationArguments: CreationArguments) {
52+
if (creationArguments.skipCoroutineScopeValidityCheck) return
53+
54+
val dispatcher = scope.coroutineContext[ContinuationInterceptor]
55+
val dispatcherName = dispatcher.toString()
56+
if (dispatcher === Dispatchers.Default ||
57+
dispatcherName == "Dispatchers.Default" ||
58+
dispatcherName.startsWith("Dispatchers.Default.limitedParallelism") ||
59+
dispatcherName == "Dispatchers.IO" || // can't get IO dispatcher in commonMain
60+
dispatcherName.startsWith("Dispatchers.IO.limitedParallelism")
61+
) {
62+
error(
63+
"Using Dispatchers.Default or Dispatchers.IO for StateMachine even with limitedParallelism(1) is the most likely an error," +
64+
" as it is multi-threaded, see the docs: \n" +
65+
"https://kstatemachine.github.io/kstatemachine/pages/multithreading.html#use-single-threaded-coroutinescope" +
66+
"You can opt-out this check by CreationArguments::skipCoroutineScopeValidityCheck flag."
67+
)
68+
}
69+
}
70+
4971
/**
5072
* Processes event in async fashion (using launch() to start new coroutine).
5173
*

kstatemachine/api/kstatemachine.api

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ public final class ru/nsk/kstatemachine/persistence/StrictValidator : ru/nsk/kst
219219
public final class ru/nsk/kstatemachine/persistence/WarningType : java/lang/Enum {
220220
public static final field ProcessingResultNotMatch Lru/nsk/kstatemachine/persistence/WarningType;
221221
public static final field RecordedAndProcessedEventCountNotMatch Lru/nsk/kstatemachine/persistence/WarningType;
222+
public static fun getEntries ()Lkotlin/enums/EnumEntries;
222223
public static fun valueOf (Ljava/lang/String;)Lru/nsk/kstatemachine/persistence/WarningType;
223224
public static fun values ()[Lru/nsk/kstatemachine/persistence/WarningType;
224225
}
@@ -256,6 +257,7 @@ public class ru/nsk/kstatemachine/state/BaseStateImpl : ru/nsk/kstatemachine/sta
256257
public final class ru/nsk/kstatemachine/state/ChildMode : java/lang/Enum {
257258
public static final field EXCLUSIVE Lru/nsk/kstatemachine/state/ChildMode;
258259
public static final field PARALLEL Lru/nsk/kstatemachine/state/ChildMode;
260+
public static fun getEntries ()Lkotlin/enums/EnumEntries;
259261
public static fun valueOf (Ljava/lang/String;)Lru/nsk/kstatemachine/state/ChildMode;
260262
public static fun values ()[Lru/nsk/kstatemachine/state/ChildMode;
261263
}
@@ -372,6 +374,7 @@ public final class ru/nsk/kstatemachine/state/HistoryState$DefaultImpls {
372374
public final class ru/nsk/kstatemachine/state/HistoryType : java/lang/Enum {
373375
public static final field DEEP Lru/nsk/kstatemachine/state/HistoryType;
374376
public static final field SHALLOW Lru/nsk/kstatemachine/state/HistoryType;
377+
public static fun getEntries ()Lkotlin/enums/EnumEntries;
375378
public static fun valueOf (Ljava/lang/String;)Lru/nsk/kstatemachine/state/HistoryType;
376379
public static fun values ()[Lru/nsk/kstatemachine/state/HistoryType;
377380
}
@@ -616,6 +619,7 @@ public abstract interface class ru/nsk/kstatemachine/statemachine/CreationArgume
616619
public abstract fun getDoNotThrowOnMultipleTransitionsMatch ()Z
617620
public abstract fun getEventRecordingArguments ()Lru/nsk/kstatemachine/statemachine/EventRecordingArguments;
618621
public abstract fun getRequireNonBlankNames ()Z
622+
public abstract fun getSkipCoroutineScopeValidityCheck ()Z
619623
public abstract fun isUndoEnabled ()Z
620624
}
621625

@@ -624,11 +628,13 @@ public abstract interface class ru/nsk/kstatemachine/statemachine/CreationArgume
624628
public abstract fun getDoNotThrowOnMultipleTransitionsMatch ()Z
625629
public abstract fun getEventRecordingArguments ()Lru/nsk/kstatemachine/statemachine/EventRecordingArguments;
626630
public abstract fun getRequireNonBlankNames ()Z
631+
public abstract fun getSkipCoroutineScopeValidityCheck ()Z
627632
public abstract fun isUndoEnabled ()Z
628633
public abstract fun setAutoDestroyOnStatesReuse (Z)V
629634
public abstract fun setDoNotThrowOnMultipleTransitionsMatch (Z)V
630635
public abstract fun setEventRecordingArguments (Lru/nsk/kstatemachine/statemachine/EventRecordingArguments;)V
631636
public abstract fun setRequireNonBlankNames (Z)V
637+
public abstract fun setSkipCoroutineScopeValidityCheck (Z)V
632638
public abstract fun setUndoEnabled (Z)V
633639
}
634640

@@ -662,6 +668,7 @@ public final class ru/nsk/kstatemachine/statemachine/ProcessingResult : java/lan
662668
public static final field IGNORED Lru/nsk/kstatemachine/statemachine/ProcessingResult;
663669
public static final field PENDING Lru/nsk/kstatemachine/statemachine/ProcessingResult;
664670
public static final field PROCESSED Lru/nsk/kstatemachine/statemachine/ProcessingResult;
671+
public static fun getEntries ()Lkotlin/enums/EnumEntries;
665672
public static fun valueOf (Ljava/lang/String;)Lru/nsk/kstatemachine/statemachine/ProcessingResult;
666673
public static fun values ()[Lru/nsk/kstatemachine/statemachine/ProcessingResult;
667674
}
@@ -959,8 +966,6 @@ public final class ru/nsk/kstatemachine/transition/TransitionParams {
959966
public final fun component2 ()Lru/nsk/kstatemachine/transition/TransitionDirection;
960967
public final fun component3 ()Lru/nsk/kstatemachine/event/Event;
961968
public final fun component4 ()Ljava/lang/Object;
962-
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;
963-
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;
964969
public fun equals (Ljava/lang/Object;)Z
965970
public final fun getArgument ()Ljava/lang/Object;
966971
public final fun getDirection ()Lru/nsk/kstatemachine/transition/TransitionDirection;
@@ -973,11 +978,13 @@ public final class ru/nsk/kstatemachine/transition/TransitionParams {
973978
public final class ru/nsk/kstatemachine/transition/TransitionParamsKt {
974979
public static final fun getUnwrappedArgument (Lru/nsk/kstatemachine/transition/TransitionParams;)Ljava/lang/Object;
975980
public static final fun getUnwrappedEvent (Lru/nsk/kstatemachine/transition/TransitionParams;)Lru/nsk/kstatemachine/event/Event;
981+
public static final fun isStartTransition (Lru/nsk/kstatemachine/transition/TransitionParams;)Z
976982
}
977983

978984
public final class ru/nsk/kstatemachine/transition/TransitionType : java/lang/Enum {
979985
public static final field EXTERNAL Lru/nsk/kstatemachine/transition/TransitionType;
980986
public static final field LOCAL Lru/nsk/kstatemachine/transition/TransitionType;
987+
public static fun getEntries ()Lkotlin/enums/EnumEntries;
981988
public static fun valueOf (Ljava/lang/String;)Lru/nsk/kstatemachine/transition/TransitionType;
982989
public static fun values ()[Lru/nsk/kstatemachine/transition/TransitionType;
983990
}

kstatemachine/src/commonMain/kotlin/ru/nsk/kstatemachine/statemachine/CreationArguments.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ interface CreationArguments {
4848
* Default: null
4949
*/
5050
val eventRecordingArguments: EventRecordingArguments?
51+
52+
/**
53+
* The library checks if you are trying to use multithreaded Dispatcher like
54+
* Dispatchers.Default or Dispatcher.IO which is usually an error.
55+
* @see [https://kstatemachine.github.io/kstatemachine/pages/multithreading.html#use-single-threaded-coroutinescope]
56+
* You can skip this validation setting the flag to true.
57+
* Default: false
58+
*/
59+
val skipCoroutineScopeValidityCheck: Boolean
5160
}
5261

5362
interface CreationArgumentsBuilder : CreationArguments {
@@ -56,14 +65,16 @@ interface CreationArgumentsBuilder : CreationArguments {
5665
override var doNotThrowOnMultipleTransitionsMatch: Boolean
5766
override var requireNonBlankNames: Boolean
5867
override var eventRecordingArguments: EventRecordingArguments?
68+
override var skipCoroutineScopeValidityCheck: Boolean
5969
}
6070

6171
private data class CreationArgumentsBuilderImpl(
6272
override var autoDestroyOnStatesReuse: Boolean = true,
6373
override var isUndoEnabled: Boolean = false,
6474
override var doNotThrowOnMultipleTransitionsMatch: Boolean = false,
6575
override var requireNonBlankNames: Boolean = false,
66-
override var eventRecordingArguments: EventRecordingArguments? = null
76+
override var eventRecordingArguments: EventRecordingArguments? = null,
77+
override var skipCoroutineScopeValidityCheck: Boolean = false,
6778
) : CreationArgumentsBuilder
6879

6980
@OptIn(ExperimentalContracts::class)

kstatemachine/src/commonMain/kotlin/ru/nsk/kstatemachine/transition/TransitionParams.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
package ru.nsk.kstatemachine.transition
99

1010
import ru.nsk.kstatemachine.event.Event
11+
import ru.nsk.kstatemachine.event.StartEvent
1112
import ru.nsk.kstatemachine.event.WrappedEvent
1213
import ru.nsk.kstatemachine.statemachine.StateMachineDslMarker
1314

15+
@ConsistentCopyVisibility
1416
@StateMachineDslMarker
1517
data class TransitionParams<E : Event> internal constructor(
1618
val transition: Transition<E>,
@@ -34,4 +36,12 @@ val TransitionParams<*>.unwrappedEvent get() = if (event is WrappedEvent) event.
3436
* Convenience property for unwrapping original argument.
3537
* If the event is not [WrappedEvent] this is same as [TransitionParams.argument] property
3638
*/
37-
val TransitionParams<*>.unwrappedArgument get() = if (event is WrappedEvent) event.argument else argument
39+
val TransitionParams<*>.unwrappedArgument get() = if (event is WrappedEvent) event.argument else argument
40+
41+
/**
42+
* Returns true is the transition is triggered by [StartEvent].
43+
* This means that the StateMachine is starting.
44+
* Might be useful to check if you're entering some State just by a machine startup
45+
* or by outside event.
46+
*/
47+
val TransitionParams<*>.isStartTransition get() = event is StartEvent

tests/src/commonTest/kotlin/ru/nsk/kstatemachine/TestUtils.kt

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ enum class CoroutineStarterType {
7979
* but it should be ok as it happens sequentially.
8080
*/
8181
COROUTINES_LIB_SINGLE_THREAD_DISPATCHER,
82-
COROUTINES_LIB_DEFAULT_LIMITED_DISPATCHER,
8382
}
8483

8584
@OptIn(ExperimentalCoroutinesApi::class)
@@ -132,13 +131,5 @@ suspend fun createTestStateMachine(
132131
creationArguments,
133132
init = init
134133
)
135-
CoroutineStarterType.COROUTINES_LIB_DEFAULT_LIMITED_DISPATCHER -> createStateMachine(
136-
CoroutineScope(Dispatchers.Default.limitedParallelism(1)), // does not guarantee same thread for each task
137-
name,
138-
childMode,
139-
start,
140-
creationArguments,
141-
init = init
142-
)
143134
}
144135
}

tests/src/commonTest/kotlin/ru/nsk/kstatemachine/state/StateCleanupTest.kt

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,37 +10,45 @@ package ru.nsk.kstatemachine.state
1010
import io.kotest.core.spec.style.FreeSpec
1111
import io.kotest.matchers.shouldBe
1212
import io.mockk.coVerify
13-
import io.mockk.spyk
13+
import io.mockk.mockk
1414
import ru.nsk.kstatemachine.CoroutineStarterType
1515
import ru.nsk.kstatemachine.createTestStateMachine
1616
import ru.nsk.kstatemachine.state.StateCleanupTestData.State1
1717
import ru.nsk.kstatemachine.statemachine.destroyBlocking
1818

1919
private object StateCleanupTestData {
20-
class State1 : DefaultState("state1")
20+
class State1(private val onCleanupListener: () -> Unit) : DefaultState("state1") {
21+
override suspend fun onCleanup() {
22+
super.onCleanup()
23+
onCleanupListener()
24+
}
25+
}
2126
}
2227

2328
class StateCleanupTest : FreeSpec({
2429
CoroutineStarterType.entries.forEach { coroutineStarterType ->
2530
"$coroutineStarterType" - {
2631
"cleanup is not called" {
27-
val state = spyk<State1>()
32+
val listener = mockk<() -> Unit>(relaxed = true)
33+
val state = State1(listener)
2834
useInMachine(coroutineStarterType, state)
29-
coVerify(inverse = true) { state.onCleanup() }
35+
coVerify(inverse = true) { listener() }
3036
}
3137

3238
"cleanup is called on machine manual destruction" {
33-
val state = spyk<State1>()
39+
val listener = mockk<() -> Unit>(relaxed = true)
40+
val state = State1(listener)
3441
useInMachine(coroutineStarterType, state).destroyBlocking()
35-
coVerify(exactly = 1) { state.onCleanup() }
42+
coVerify(exactly = 1) { listener() }
3643
}
3744

3845
"cleanup is called on machine auto destruction" {
39-
val state = spyk<State1>()
46+
val listener = mockk<() -> Unit>(relaxed = true)
47+
val state = State1(listener)
4048
val machine1 = useInMachine(coroutineStarterType, state)
4149
val machine2 = useInMachine(coroutineStarterType, state)
4250

43-
coVerify(exactly = 1) { state.onCleanup() }
51+
coVerify(exactly = 1) { listener() }
4452
machine1.isDestroyed shouldBe true
4553
machine2.isDestroyed shouldBe false
4654
}

tests/src/commonTest/kotlin/ru/nsk/kstatemachine/statemachine/StateMachineTest.kt

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@ import io.kotest.assertions.throwables.shouldThrowUnitWithMessage
1212
import io.kotest.assertions.throwables.shouldThrowWithMessage
1313
import io.kotest.core.spec.style.FreeSpec
1414
import io.kotest.datatest.withData
15+
import io.kotest.matchers.collections.shouldContain
1516
import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
1617
import io.kotest.matchers.shouldBe
1718
import io.kotest.matchers.string.shouldEndWith
1819
import io.mockk.called
1920
import io.mockk.verify
2021
import io.mockk.verifySequence
22+
import kotlinx.coroutines.CoroutineScope
23+
import kotlinx.coroutines.Dispatchers
24+
import kotlinx.coroutines.cancel
2125
import ru.nsk.kstatemachine.*
2226
import ru.nsk.kstatemachine.event.Event
2327
import ru.nsk.kstatemachine.event.EventMatcher
@@ -26,17 +30,44 @@ import ru.nsk.kstatemachine.state.*
2630
import ru.nsk.kstatemachine.statemachine.StateMachineTestData.OffEvent
2731
import ru.nsk.kstatemachine.statemachine.StateMachineTestData.OnEvent
2832
import ru.nsk.kstatemachine.testing.Testing.startFromBlocking
29-
import ru.nsk.kstatemachine.transition.DefaultTransition
30-
import ru.nsk.kstatemachine.transition.Transition
31-
import ru.nsk.kstatemachine.transition.TransitionType
32-
import ru.nsk.kstatemachine.transition.onTriggered
33+
import ru.nsk.kstatemachine.transition.*
3334

3435
private object StateMachineTestData {
3536
object OnEvent : Event
3637
object OffEvent : Event
3738
}
3839

3940
class StateMachineTest : FreeSpec({
41+
withData(
42+
nameFn = { "dispatcher: $it" },
43+
CoroutineScope(Dispatchers.Default),
44+
CoroutineScope(Dispatchers.Default.limitedParallelism(1)),
45+
CoroutineScope(Dispatchers.IO),
46+
CoroutineScope(Dispatchers.IO.limitedParallelism(1)),
47+
) { scope ->
48+
"scope validation" {
49+
try {
50+
createStateMachine(
51+
scope,
52+
creationArguments = buildCreationArguments { skipCoroutineScopeValidityCheck = true }
53+
) {
54+
initialState("initial")
55+
}
56+
57+
shouldThrowWithMessage<IllegalStateException>(
58+
"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" +
59+
"https://kstatemachine.github.io/kstatemachine/pages/multithreading.html#use-single-threaded-coroutinescopeYou can opt-out this check by CreationArguments::skipCoroutineScopeValidityCheck flag."
60+
) {
61+
createStateMachine(scope) {
62+
initialState("initial")
63+
}
64+
}
65+
} finally {
66+
scope.cancel()
67+
}
68+
}
69+
}
70+
4071
CoroutineStarterType.entries.forEach { coroutineStarterType ->
4172
"$coroutineStarterType" - {
4273
"no initial state" {
@@ -363,6 +394,22 @@ class StateMachineTest : FreeSpec({
363394
}
364395
}
365396

397+
"isStartTransition" {
398+
lateinit var state2: State
399+
val machine = createTestStateMachine(coroutineStarterType) {
400+
state2 = state("state2") {
401+
onEntry { it.isStartTransition shouldBe false }
402+
}
403+
initialState("initial") {
404+
onEntry { it.isStartTransition shouldBe true }
405+
transitionOn<SwitchEvent> { targetState = { state2 } }
406+
}
407+
onStarted { it.isStartTransition shouldBe true }
408+
}
409+
machine.processEvent(SwitchEvent)
410+
machine.activeStates().shouldContain(state2)
411+
}
412+
366413
"destroy from onStart" {
367414
val callbacks = mockkCallbacks()
368415
val machine = createTestStateMachine(coroutineStarterType) {

0 commit comments

Comments
 (0)