Skip to content

Commit 0fd6bcd

Browse files
committed
Complete recorded events tests
1 parent 40d1d54 commit 0fd6bcd

6 files changed

Lines changed: 176 additions & 152 deletions

File tree

kstatemachine/src/commonMain/kotlin/ru/nsk/kstatemachine/persistence/EventRecorder.kt

Lines changed: 57 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ internal class EventRecorderImpl(
4242
fun onProcessEvent(eventAndArgument: EventAndArgument<*>, processingResult: ProcessingResult) {
4343
val lastEvent = records.lastOrNull()?.eventAndArgument?.event
4444
check(lastEvent !is DestroyEvent) {
45-
"Internal error, ${::onProcessEvent::name} called after " +
45+
"Internal error, ${::onProcessEvent.name} called after " +
4646
"${DestroyEvent::class.simpleName} processing, which is considered as last possible event"
4747
}
4848
if (arguments.skipIgnoredEvents && processingResult == ProcessingResult.IGNORED) return
@@ -89,12 +89,14 @@ object StrictValidator : RestorationResultValidator {
8989
throw RestorationResultValidationException(
9090
result,
9191
"The ${RestorationResult::class.simpleName} contains warnings",
92+
it.warnings.first(),
9293
)
9394
}
9495
if (it.processingResult.isFailure) {
9596
throw RestorationResultValidationException(
9697
result,
9798
"The ${RestorationResult::class.simpleName} contains failed processing result",
99+
it.processingResult.exceptionOrNull(),
98100
)
99101
}
100102
}
@@ -120,9 +122,15 @@ class RestorationWarningException(
120122

121123
/**
122124
* Processes [RecordedEvents] with purpose of restoring a [StateMachine] to a state configuration as it was before.
125+
* Starts the [StateMachine] if necessary and returns [RestorationResult] allowing to inspect
126+
* how the restoration was processed.
127+
*
128+
* There is no way on library side to decide if some exceptions during event processing are errors or not.
129+
* For instance [StateMachine] may be configured with [throwingIgnoredEventHandler] so some exceptions might
130+
* be expected and are not really errors.
123131
*
124132
* @param muteListeners listeners are not triggered by default,
125-
* as I assume that client code reactions were already processed before.
133+
* as we assume that client code reactions were already processed before.
126134
* @param disableStructureHashCodeCheck allows to skip the machine structure check
127135
* to force processing of [RecordedEvents]. Note that running the same event sequence on similar machines but having
128136
* different structureHashCode value, may produce different results more likely.
@@ -132,100 +140,78 @@ suspend fun StateMachine.restoreByRecordedEvents(
132140
muteListeners: Boolean = true,
133141
disableStructureHashCodeCheck: Boolean = false,
134142
validator: RestorationResultValidator = StrictValidator,
135-
): Unit = coroutineAbstraction.withContext {
136-
if (isRunning) {
137-
restoreRunningMachineByRecordedEvents(recordedEvents, muteListeners, disableStructureHashCodeCheck, validator)
138-
} else {
139-
onStarted {
140-
restoreRunningMachineByRecordedEvents(
141-
recordedEvents,
142-
muteListeners,
143-
disableStructureHashCodeCheck,
144-
validator,
145-
)
146-
}
147-
}
148-
}
149-
150-
/**
151-
* May be called on started ([StateMachine.isRunning] == true) [StateMachine] only,
152-
* and returns [RestorationResult] allowing to inspect how the restoration was processed.
153-
*
154-
* There is no way on library side to decide if some exceptions during event processing are errors or not.
155-
* For instance [StateMachine] may be configured with [throwingIgnoredEventHandler] so some exceptions might
156-
* be expected and are not really errors.
157-
*/
158-
suspend fun StateMachine.restoreRunningMachineByRecordedEvents(
159-
recordedEvents: RecordedEvents,
160-
muteListeners: Boolean = true,
161-
disableStructureHashCodeCheck: Boolean = false,
162-
validator: RestorationResultValidator = StrictValidator,
163143
): RestorationResult = coroutineAbstraction.withContext {
164-
check(isRunning) {
165-
"$this is not running, ${::restoreRunningMachineByRecordedEvents.name}() operation only makes sense on " +
166-
"created and started ${StateMachine::class.simpleName}, please call it after the machine is started"
167-
}
168144
checkNotDestroyed()
169-
check(!hasProcessedEvents) {
170-
"$this has already processed events, ${::restoreRunningMachineByRecordedEvents.name}() operation only makes " +
171-
"sense on initially clear ${StateMachine::class.simpleName}, please call it before " +
172-
"processing any other events"
145+
if (isRunning) {
146+
check(!hasProcessedEvents) {
147+
"$this has already processed events, ${::restoreByRecordedEvents.name}() operation only makes " +
148+
"sense on initially clear ${StateMachine::class.simpleName}, please call it before " +
149+
"processing any other events (or even before start - optionally)"
150+
}
173151
}
174152

175153
if (!disableStructureHashCodeCheck)
176154
check(structureHashCode == recordedEvents.structureHashCode) {
177-
"$this structure seems to be different from recorded original one"
155+
"$this structure seems to be different from recorded original one, you can disable this error by the " +
156+
"disableStructureHashCodeCheck argument if you are sure that it is correct"
178157
}
179158

180159
this as InternalStateMachine
181160
val results = mutableListOf<RestoredEventResult>()
182161
val mutationSection = if (muteListeners) openListenersMutationSection() else EmptyListenersMutationSection
183162
mutationSection.use {
184-
for (record in recordedEvents.records) {
163+
recordedEvents.records.forEachIndexed iteration@{ index, record ->
185164
val warnings = mutableListOf<RestorationWarningException>()
186165
val (event, argument) = record.eventAndArgument
187-
if (event is StartEvent)
188-
continue // fixme вызов start мог иметь argument что с ним делать?
189-
val processingResult = runCatching { processEvent(event, argument) }
190-
val actualResult = processingResult.getOrNull()
191-
if (actualResult != null && actualResult != record.processingResult) {
192-
if (actualResult == ProcessingResult.PENDING) {
193-
if (pendingEventHandler !is QueuePendingEventHandler)
166+
if (event is StartEvent) {
167+
if (isRunning) {
168+
if (argument == null) {
169+
return@iteration // continue
170+
} else {
171+
if (index == 0)
172+
error(
173+
"The ${StateMachine::class.simpleName} is already started, but " +
174+
"the ${RecordedEvents::class.simpleName} contains an argument for " +
175+
"${StateMachine::start.name} method. " +
176+
"To restore such machine, " +
177+
"do not start it before calling ${::restoreByRecordedEvents.name}"
178+
)
179+
else
180+
error("The machine should not be running here. Internal error. Never get here")
181+
}
182+
} else {
183+
start(argument)
184+
// fixme add RestoredEventResult?
185+
}
186+
} else {
187+
val processingResult = runCatching { processEvent(event, argument) }
188+
val actualResult = processingResult.getOrNull()
189+
if (actualResult != null && actualResult != record.processingResult) {
190+
if (actualResult == ProcessingResult.PENDING) {
191+
if (pendingEventHandler !is QueuePendingEventHandler)
192+
warnings += RestorationWarningException(
193+
WarningType.PendingEventMightBeIgnored,
194+
"Actual result is ${ProcessingResult.PENDING}, " +
195+
"but the ${StateMachine::class.simpleName} is NOT configured " +
196+
"with ${QueuePendingEventHandler::class::simpleName}, which potentially means that " +
197+
"the event {${record.eventAndArgument.event}} might be silently ignored",
198+
)
199+
} else {
194200
warnings += RestorationWarningException(
195-
WarningType.PendingEventMightBeIgnored,
196-
"Actual result is ${ProcessingResult.PENDING}, " +
197-
"but the ${StateMachine::class.simpleName} is NOT configured " +
198-
"with ${QueuePendingEventHandler::class::simpleName}, which potentially means that " +
199-
"the event {${record.eventAndArgument.event}} might be silently ignored",
201+
WarningType.ProcessingResultNotMatch,
202+
"Recorded (${record.processingResult}) and actual ($actualResult) processing results does not match",
200203
)
201-
} else {
202-
warnings += RestorationWarningException(
203-
WarningType.ProcessingResultNotMatch,
204-
"Recorded (${record.processingResult}) and actual ($actualResult) processing results does not match",
205-
)
204+
}
206205
}
206+
results += RestoredEventResult(record, processingResult, warnings)
207207
}
208-
results += RestoredEventResult(record, processingResult, warnings)
209208
}
210209
}
211210
RestorationResult(results).also {
212211
validator.validate(it)
213212
}
214213
}
215214

216-
/**
217-
* Blocking [restoreRunningMachineByRecordedEvents] alternative
218-
*/
219-
fun StateMachine.restoreRunningMachineByRecordedEventsBlocking(
220-
recordedEvents: RecordedEvents,
221-
muteListeners: Boolean = true,
222-
disableStructureHashCodeCheck: Boolean = false,
223-
): RestorationResult {
224-
return coroutineAbstraction.runBlocking {
225-
restoreRunningMachineByRecordedEvents(recordedEvents, muteListeners, disableStructureHashCodeCheck)
226-
}
227-
}
228-
229215
/**
230216
* Blocking [restoreByRecordedEvents] alternative
231217
*/

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ fun StateMachine.throwingIgnoredEventHandler(): StateMachine.IgnoredEventHandler
88
return StateMachine.IgnoredEventHandler {
99
error(
1010
"${this@throwingIgnoredEventHandler} received ${it.event} that is going to be ignored. " +
11-
"The machine was configured with ${StateMachine::throwingIgnoredEventHandler::name}, " +
11+
"The machine was configured with ${StateMachine::throwingIgnoredEventHandler.name}, " +
1212
"that forbids such behaviour."
1313
)
1414
}

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ package ru.nsk.kstatemachine.statemachine
33
import ru.nsk.kstatemachine.coroutines.CoroutineAbstraction
44
import ru.nsk.kstatemachine.coroutines.StdLibCoroutineAbstraction
55
import ru.nsk.kstatemachine.coroutines.createStateMachine
6+
import ru.nsk.kstatemachine.event.*
67
import ru.nsk.kstatemachine.event.DestroyEvent
7-
import ru.nsk.kstatemachine.event.Event
88
import ru.nsk.kstatemachine.event.StopEvent
9-
import ru.nsk.kstatemachine.event.UndoEvent
109
import ru.nsk.kstatemachine.persistence.EventRecorder
1110
import ru.nsk.kstatemachine.state.ChildMode
1211
import ru.nsk.kstatemachine.state.IState
@@ -47,7 +46,7 @@ interface StateMachine : State {
4746
val isRunning: Boolean
4847

4948
/**
50-
* Indicates that machine is started and has clear initial state (has not processed any events yet)
49+
* Indicates that machine is started and has clear initial state (has not processed any events but [StartEvent] yet)
5150
*/
5251
val hasProcessedEvents: Boolean
5352

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,12 @@ internal class StateMachineImpl(
174174
}
175175

176176
override suspend fun processEvent(event: Event, argument: Any?): ProcessingResult {
177+
check(event !is StartEvent) {
178+
"Incorrect ${StartEvent::class.simpleName} usage. Use ${::start.name}() method instead"
179+
}
177180
return coroutineAbstraction.withContext {
178181
checkNotDestroyed()
179-
check(isRunning || event is DestroyEvent) { "$this is not started, call start() first" }
182+
check(isRunning || event is DestroyEvent) { "$this is not started, call ${::start.name}() first" }
180183

181184
val eventAndArgument = EventAndArgument(event, argument)
182185

tests/src/commonTest/kotlin/ru/nsk/kstatemachine/persistence/EventRecorderTest.kt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package ru.nsk.kstatemachine.persistence
22

3-
import io.kotest.assertions.throwables.shouldNotThrowAny
43
import io.kotest.assertions.throwables.shouldThrowWithMessage
54
import io.kotest.core.spec.style.StringSpec
65
import io.kotest.matchers.collections.shouldContain
@@ -118,7 +117,7 @@ class EventRecorderTest : StringSpec({
118117
recordedEvents.records shouldHaveSize 3 // DestroyEvent
119118
}
120119

121-
"check recorded events on restart without ${EventRecordingArguments::clearRecordsOnMachineRestart::name} flag" {
120+
"check recorded events on restart without ${EventRecordingArguments::clearRecordsOnMachineRestart.name} flag" {
122121
val machine = createTestStateMachine(
123122
coroutineStarterType,
124123
creationArguments = CreationArguments(
@@ -145,7 +144,7 @@ class EventRecorderTest : StringSpec({
145144
recordedEvents.records shouldHaveSize 5
146145
}
147146

148-
"check recorded events on restart with ${EventRecordingArguments::clearRecordsOnMachineRestart::name} flag (default)" {
147+
"check recorded events on restart with ${EventRecordingArguments::clearRecordsOnMachineRestart.name} flag (default)" {
149148
val machine = createTestStateMachine(
150149
coroutineStarterType,
151150
creationArguments = CreationArguments(eventRecordingArguments = EventRecordingArguments())
@@ -167,7 +166,7 @@ class EventRecorderTest : StringSpec({
167166
recordedEvents.records shouldHaveSize 2
168167
}
169168

170-
"check recorded events with ${EventRecordingArguments::skipIgnoredEvents::name} flag (default)" {
169+
"check recorded events with ${EventRecordingArguments::skipIgnoredEvents.name} flag (default)" {
171170
val machine = createTestStateMachine(
172171
coroutineStarterType,
173172
creationArguments = CreationArguments(eventRecordingArguments = EventRecordingArguments())
@@ -182,7 +181,7 @@ class EventRecorderTest : StringSpec({
182181
recordedEvents.records shouldHaveSize 1
183182
}
184183

185-
"check recorded events without ${EventRecordingArguments::skipIgnoredEvents::name} flag" {
184+
"check recorded events without ${EventRecordingArguments::skipIgnoredEvents.name} flag" {
186185
val machine = createTestStateMachine(
187186
coroutineStarterType,
188187
creationArguments = CreationArguments(

0 commit comments

Comments
 (0)