Skip to content

Commit

Permalink
Add StateMachineActions() helper
Browse files Browse the repository at this point in the history
  • Loading branch information
flyingmutant committed May 11, 2023
1 parent 69db632 commit 95d7a39
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 6 deletions.
5 changes: 2 additions & 3 deletions engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ func brokenGen(*T) int { panic("this generator is not working") }
type brokenMachine struct{}

func (m *brokenMachine) DoNothing(_ *T) { panic("this state machine is not working") }
func (m *brokenMachine) Check(_ *T) {}

func TestPanicTraceback(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -49,9 +50,7 @@ func TestPanicTraceback(t *testing.T) {
func(t *T) *testError {
return checkOnce(t, func(t *T) {
var sm brokenMachine
t.Run(map[string]func(*T){
"DoNothing": sm.DoNothing,
})
t.Run(StateMachineActions(&sm))
})
},
},
Expand Down
43 changes: 40 additions & 3 deletions statemachine.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,25 @@ package rapid

import (
"math"
"reflect"
"sort"
"testing"
)

const (
actionLabel = "action"
validActionTries = 100 // hack, but probably good enough for now

actionLabel = "action"
validActionTries = 100 // hack, but probably good enough for now
checkMethodName = "Check"
noValidActionsMsg = "can't find a valid action"
)

// Run executes a random sequence of actions (often called a "state machine" test).
// actions[""], if set, is executed before/after every other action invocation
// and should only contain invariant checking code.
//
// For complex state machines, it can be more convenient to specify actions as
// methods of a special state machine type. In this case, [StateMachineActions]
// can be used to create an actions map from state machine methods using reflection.
func (t *T) Run(actions map[string]func(*T)) {
t.Helper()
if len(actions) == 0 {
Expand Down Expand Up @@ -64,6 +69,38 @@ func (t *T) Run(actions map[string]func(*T)) {
}
}

type StateMachine interface {
// Check is ran after every action and should contain invariant checks.
//
// All other public methods should have a form ActionName(t *rapid.T)
// and are used as possible actions. At least one action has to be specified.
Check(*T)
}

// StateMachineActions creates an actions map for [*T.Run]
// from methods of a [StateMachine] type instance using reflection.
func StateMachineActions(sm StateMachine) map[string]func(*T) {
var (
v = reflect.ValueOf(sm)
t = v.Type()
n = t.NumMethod()
)

actions := make(map[string]func(*T), n)
for i := 0; i < n; i++ {
name := t.Method(i).Name
m, ok := v.Method(i).Interface().(func(*T))
if ok && name != checkMethodName {
actions[name] = m
}
}

assertf(len(actions) > 0, "state machine of type %v has no actions specified", t)
actions[""] = sm.Check

return actions
}

type stateMachine struct {
check func(*T)
actionKeys *Generator[string]
Expand Down

0 comments on commit 95d7a39

Please sign in to comment.