Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Entry/Exit callback not called in Child state #25

Closed
maros136 opened this issue Feb 8, 2022 · 3 comments
Closed

Entry/Exit callback not called in Child state #25

maros136 opened this issue Feb 8, 2022 · 3 comments
Assignees
Labels
bug Something isn't working

Comments

@maros136
Copy link

maros136 commented Feb 8, 2022

I find cases when Entry/Exit callback not called in Child state.

I try it in latest version 0.9.0
Below is unit test how to reproduce it

package ru.nsk.kstatemachine

import io.kotest.core.spec.style.StringSpec

class AdvancedCrossLevelTransitionTest : StringSpec({

    "1. child to neighbors 1. child and then back 1. child" {
        val callbacks = mockkCallbacks()

        lateinit var state1: State
        lateinit var state11: State
        lateinit var state12: State
        lateinit var state2: State
        lateinit var state21: State
        lateinit var state22: State

        val machine = createStateMachine {
            state1 = initialState("1") {
                callbacks.listen(this)

                state11 = initialState("11") {
                    callbacks.listen(this)

                    transitionOn<SwitchEvent> {
                        targetState = { state12 }
                        callbacks.listen(this)
                    }
                }

                state12 = state("12") {
                    callbacks.listen(this)

                    transitionOn<SwitchEvent> {
                        targetState = { state11 }
                        callbacks.listen(this)
                    }
                }

                transitionOn<SwitchEventL1> {
                    targetState = { state2 }
                    callbacks.listen(this)
                }
            }
            state2 = state("2") {
                callbacks.listen(this)

                state21 = initialState("21") {
                    callbacks.listen(this)

                    transitionOn<SwitchEvent> {
                        targetState = { state22 }
                        callbacks.listen(this)
                    }
                }

                state22 = state("22") {
                    callbacks.listen(this)

                    transitionOn<SwitchEvent> {
                        targetState = { state21 }
                        callbacks.listen(this)
                    }
                }

                transitionOn<SwitchEventL1> {
                    targetState = { state1 }
                    callbacks.listen(this)
                }
            }
        }

        //* -> 1 (11)   - ok
        verifySequenceAndClear(callbacks) {
            callbacks.onEntryState(state1)
            callbacks.onEntryState(state11)
        }

        //1 (11) -> 1 (12)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state11)
            callbacks.onEntryState(state12)
        }

        //1 (12) -> 2 (21)  - ok
        machine.processEvent(SwitchEventL1)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEventL1)
            callbacks.onExitState(state12)
            callbacks.onExitState(state1)
            callbacks.onEntryState(state2)
            callbacks.onEntryState(state21)
        }

        //2 (21) -> 2 (22)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state21)
            callbacks.onEntryState(state22)
        }

        //2 (22) -> 1 (11)  - ok
        machine.processEvent(SwitchEventL1)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEventL1)
            callbacks.onExitState(state22)
            callbacks.onExitState(state2)
            callbacks.onEntryState(state1)
            callbacks.onEntryState(state11)
        }

        //1 (11) -> 1 (12)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state11)
            callbacks.onEntryState(state12)
        }

        //1 (12) -> 1 (11)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state12)
            callbacks.onEntryState(state11)
        }

        //1 (11) -> 2 (21)  - ok
        machine.processEvent(SwitchEventL1)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEventL1)
            callbacks.onExitState(state11)
            callbacks.onExitState(state1)
            callbacks.onEntryState(state2)
            callbacks.onEntryState(state21)
        }

        //2 (21) -> 1 (11)  - failed (missing child entry callback)
        machine.processEvent(SwitchEventL1)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEventL1)
            callbacks.onExitState(state21)
            callbacks.onExitState(state2)
            callbacks.onEntryState(state1)
            callbacks.onEntryState(state11)//Missing entry state!
        }

        //1 (11) -> 1 (12)  - failed (missing child exit callback)
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state11)//Missing exit state!
            callbacks.onEntryState(state12)
        }

        //1 (12) -> 1 (11)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state12)
            callbacks.onEntryState(state11)
        }

        //1 (11) -> 2 (21)  - failed (missing child entry callback)
        machine.processEvent(SwitchEventL1)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEventL1)
            callbacks.onExitState(state11)
            callbacks.onExitState(state1)
            callbacks.onEntryState(state2)
            callbacks.onEntryState(state21)//Missing entry state!
        }
    }
})

Solution how to fix it (with very ugly fix in State)

package ru.nsk.kstatemachine

import io.kotest.core.spec.style.StringSpec
import timber.log.Timber
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible

open class FixedState(
        name: String? = null,
        childMode: ChildMode = ChildMode.EXCLUSIVE
) : DefaultState(name, childMode) {

    override fun onDoExit(transitionParams: TransitionParams<*>) {
        super.onDoExit(transitionParams)
        //Need clear CurrentState after exit
        // - not working - reEnter parent state after leaving it with child state (as initial state)
        try {
            DefaultStateWithDetail.propertyCurrentState?.set(this, null)
        } catch (e : Exception) {
            Timber.e(e, "Cannot set value for currentState property!")
        }
    }

    companion object {
        internal var propertyCurrentState : KMutableProperty1<InternalState, Any?>? = null

        init {
            try {
                propertyCurrentState = BaseStateImpl::class.memberProperties
                        .find { it.name == "currentState" }
                        ?.apply { isAccessible = true } as KMutableProperty1<InternalState, Any?>
            } catch (e : Exception){
                Timber.e(e, "Cannot get currentState property!")
            }
        }
    }
}

class FixedAdvancedCrossLevelTransitionTest : StringSpec({
    "1. child to neighbors 1. child and then back 1. child" {
        val callbacks = mockkCallbacks()

        val state1 = FixedState("1")
        val state11 = FixedState("11")
        val state12 = FixedState("12")
        val state2 = FixedState("2")
        val state21 = FixedState("21")
        val state22 = FixedState("22")

        val machine = createStateMachine {
            addInitialState(state1) {
                callbacks.listen(this)

                addInitialState(state11) {
                    callbacks.listen(this)

                    transitionOn<SwitchEvent> {
                        targetState = { state12 }
                        callbacks.listen(this)
                    }
                }

                addState(state12) {
                    callbacks.listen(this)

                    transitionOn<SwitchEvent> {
                        targetState = { state11 }
                        callbacks.listen(this)
                    }
                }

                transitionOn<SwitchEventL1> {
                    targetState = { state2 }
                    callbacks.listen(this)
                }
            }
            addState(state2) {
                callbacks.listen(this)

                addInitialState(state21) {
                    callbacks.listen(this)

                    transitionOn<SwitchEvent> {
                        targetState = { state22 }
                        callbacks.listen(this)
                    }
                }

                addState(state22) {
                    callbacks.listen(this)

                    transitionOn<SwitchEvent> {
                        targetState = { state21 }
                        callbacks.listen(this)
                    }
                }

                transitionOn<SwitchEventL1> {
                    targetState = { state1 }
                    callbacks.listen(this)
                }
            }
        }

        //* -> 1 (11)   - ok
        verifySequenceAndClear(callbacks) {
            callbacks.onEntryState(state1)
            callbacks.onEntryState(state11)
        }

        //1 (11) -> 1 (12)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state11)
            callbacks.onEntryState(state12)
        }

        //1 (12) -> 2 (21)  - ok
        machine.processEvent(SwitchEventL1)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEventL1)
            callbacks.onExitState(state12)
            callbacks.onExitState(state1)
            callbacks.onEntryState(state2)
            callbacks.onEntryState(state21)
        }

        //2 (21) -> 2 (22)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state21)
            callbacks.onEntryState(state22)
        }

        //2 (22) -> 1 (11)  - ok
        machine.processEvent(SwitchEventL1)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEventL1)
            callbacks.onExitState(state22)
            callbacks.onExitState(state2)
            callbacks.onEntryState(state1)
            callbacks.onEntryState(state11)
        }

        //1 (11) -> 1 (12)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state11)
            callbacks.onEntryState(state12)
        }

        //1 (12) -> 1 (11)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state12)
            callbacks.onEntryState(state11)
        }

        //1 (11) -> 2 (21)  - ok
        machine.processEvent(SwitchEventL1)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEventL1)
            callbacks.onExitState(state11)
            callbacks.onExitState(state1)
            callbacks.onEntryState(state2)
            callbacks.onEntryState(state21)
        }

        //2 (21) -> 1 (11)  - ok
        machine.processEvent(SwitchEventL1)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEventL1)
            callbacks.onExitState(state21)
            callbacks.onExitState(state2)
            callbacks.onEntryState(state1)
            callbacks.onEntryState(state11)
        }

        //1 (11) -> 1 (12)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state11)
            callbacks.onEntryState(state12)
        }

        //1 (12) -> 1 (11)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state12)
            callbacks.onEntryState(state11)
        }

        //1 (11) -> 2 (21)  - ok
        machine.processEvent(SwitchEventL1)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEventL1)
            callbacks.onExitState(state11)
            callbacks.onExitState(state1)
            callbacks.onEntryState(state2)
            callbacks.onEntryState(state21)
        }
    }
})
@nsk90
Copy link
Collaborator

nsk90 commented Feb 8, 2022

Thank you, I will check it out after 16'th February.

@nsk90
Copy link
Collaborator

nsk90 commented Feb 18, 2022

I confirm a bug, and start working on a fix. Thank you for a good test sample.

@nsk90 nsk90 self-assigned this Feb 18, 2022
@nsk90 nsk90 added the bug Something isn't working label Feb 18, 2022
@nsk90
Copy link
Collaborator

nsk90 commented Feb 18, 2022

Fixed by clearing currentState field on exiting state.

8f658cf

@nsk90 nsk90 closed this as completed Feb 18, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants