qmuntal / stateless

Go library for creating finite state machines
BSD 2-Clause "Simplified" License
942 stars 49 forks source link

possible improvement auto configure final state #1

Closed pmalhaire closed 4 years ago

pmalhaire commented 4 years ago

hello,

thanks for this lib it's really interresting.

This is not a bug but a possible usage improvement.

In the following code the final state is "B". Since it's the final state I omitted to configure it. I have no problem to run the code but the graph function panics.

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x38 pc=0x4969f1]

goroutine 1 [running]:
github.com/qmuntal/stateless.(*graph).formatAllStateTransitions(0xc000090f07, 0xc00005e2a0, 0xc000094c00, 0xc00001a150, 0x2e)
    /home/pierrot/go/src/github.com/qmuntal/stateless/graph.go:101 +0x611
github.com/qmuntal/stateless.(*graph).FormatStateMachine(0xc000090f07, 0xc00005e2a0, 0xc00005e240, 0x4cfb40)
    /home/pierrot/go/src/github.com/qmuntal/stateless/graph.go:22 +0x49f
github.com/qmuntal/stateless.(*StateMachine).ToGraph(...)
    /home/pierrot/go/src/github.com/qmuntal/stateless/statemachine.go:109
main.showGraph()
    /home/pierrot/dev/test/main.go:106 +0x3e
main.main()
    /home/pierrot/dev/test/main.go:117 +0xe9
exit status 2

If I un-comment this line graph is fine.

//machine.Configure(majorB)

Therefore I guess it's possible to either :

package main

import (
    "context"
    "fmt"

    "github.com/qmuntal/stateless"
)

const (
    majorA  = "A"
    subOn   = "On"
    subOn1  = "On1"
    subOff  = "Off"
    subOff1 = "Off1"
    majorB  = "B"

    trigger     = "trigger"
    triggerOn   = "triggerOn"
    triggerOn1  = "triggerOn1"
    triggerOff  = "triggerOff"
    triggerOff1 = "triggerOff1"
)

var button = false

func trButton(_ context.Context, _ ...interface{}) error {
    fmt.Print("trButton :")
    if button {
        fmt.Println("On")
    } else {
        fmt.Println("Off")
    }
    return nil
}
func trOn(_ context.Context, _ ...interface{}) error {
    fmt.Println("trOn")
    return nil
}
func trOn1(_ context.Context, _ ...interface{}) error {
    fmt.Println("trOn1")
    return nil
}
func trOff(_ context.Context, _ ...interface{}) error {
    fmt.Println("trOff")
    return nil
}
func trOff1(_ context.Context, _ ...interface{}) error {
    fmt.Println("trOff1")
    return nil
}

func guardOff(_ context.Context, _ ...interface{}) bool {
    fmt.Println("   << guardOff:", !button)
    return !button
}

func guardOn(_ context.Context, _ ...interface{}) bool {
    fmt.Println("   << guardOn:", button)
    return button
}

func newMachine() *stateless.StateMachine {
    machine := stateless.NewStateMachine(majorA)

    machine.Configure(majorA).
        Permit(trigger, subOn, guardOn).
        Permit(trigger, subOff, guardOff)

    machine.Configure(subOn).OnEntry(trOn).
        Permit(triggerOn, subOn1)
    machine.Configure(subOn1).OnEntry(trOn1).
        Permit(triggerOn1, majorB)

    machine.Configure(subOff).OnEntry(trOff).
        Permit(triggerOff, subOff1)
    machine.Configure(subOff1).OnEntry(trOff1).
        Permit(triggerOff1, majorB)

    //machine.Configure(majorB)

    return machine
}

func runMachine() {
    fmt.Println("Create Machine")
    machine := newMachine()
    fmt.Println("Execute Machine")
    fmt.Println(machine)
    list, err := machine.PermittedTriggers()
    if err != nil {
        panic("invalid case:" + err.Error())
    }
    for len(list) > 0 {
        machine.Fire(list[0])
        fmt.Println(machine)
        list, err = machine.PermittedTriggers()
        if err != nil {
            panic("invalid case:" + err.Error())
        }
    }
}

func showGraph() {
    machine := newMachine()
    fmt.Println(machine.ToGraph())
}

func main() {
    fmt.Println("\n#### Off ")
    runMachine()

    button = true
    fmt.Println("\n#### On ")
    runMachine()

    showGraph()
}

Note that I tried to reproduce with a simpler example but no panic here

package main

import (
    "fmt"

    "github.com/qmuntal/stateless"
)

const (
    stateA = "A"
    stateB = "B"

    triggerB = "triggerB"
)

func main() {
    machine := stateless.NewStateMachine(stateA)

    machine.Configure(stateA).
        Permit(triggerB, stateB)

    fmt.Println(machine)
    machine.Fire(triggerB)
    fmt.Println(machine)

    // no panic if I comment this line
    // machine.Configure(stateB)

    fmt.Println(machine.ToGraph())
}
qmuntal commented 4 years ago

Thanks for the feedback!

I have implemented your sugestion in 0f3d5afd7079f62e5489a7a11234ea2717d10f96 and pushed the new version v1.0.2. In fact I think this was more a bug than an enhancements, as this was supposed to work.

pmalhaire commented 4 years ago

Welcome ! closing this issue then