mtest: implement Checker, a very basic property checker
This commit is contained in:
parent
1be0072701
commit
402aa13495
142
mtest/checker.go
Normal file
142
mtest/checker.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package mtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mediocregopher/mediocre-go-lib/mrand"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckerErr represents an test case error which was returned by a Checker Run.
|
||||||
|
//
|
||||||
|
// The string form of CheckerErr includes the sequence of Applyers which can be
|
||||||
|
// copy-pasted directly into Checker's RunCase method's arguments.
|
||||||
|
type CheckerErr struct {
|
||||||
|
// The sequence of applied actions which generated the error
|
||||||
|
Applied []Applyer
|
||||||
|
|
||||||
|
// The error returned by the final Action
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ce CheckerErr) Error() string {
|
||||||
|
typeName := func(a Applyer) string {
|
||||||
|
t := fmt.Sprintf("%T", a)
|
||||||
|
return strings.SplitN(t, ".", 2)[1] // remove the package name
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
fmt.Fprintf(buf, "Test case: []mtest.Applyer{\n")
|
||||||
|
for _, a := range ce.Applied {
|
||||||
|
fmt.Fprintf(buf, "\t%s(%#v),\n", typeName(a), a)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(buf, "}\n")
|
||||||
|
fmt.Fprintf(buf, "Generated error: %s\n", ce.Err)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// State represents the current state of a Checker run. It can be represented by
|
||||||
|
// any value convenient and useful to the test.
|
||||||
|
type State interface{}
|
||||||
|
|
||||||
|
// Applyer performs the applies an Action's changes to a State, returning the
|
||||||
|
// new State. After modifying the State the Applyer should also assert that the
|
||||||
|
// new State is what it's expected to be, returning an error if it's not.
|
||||||
|
type Applyer interface {
|
||||||
|
Apply(State) (State, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action describes a change which can take place on a state. It must contain an
|
||||||
|
// Applyer which will peform the actual change.
|
||||||
|
type Action struct {
|
||||||
|
Applyer
|
||||||
|
|
||||||
|
// Weight is used when the Checker is choosing which Action to apply on
|
||||||
|
// every loop. If not set it is assumed to be 1, and can be increased
|
||||||
|
// further to increase its chances of being picked.
|
||||||
|
Weight uint64
|
||||||
|
|
||||||
|
// Incomplete can be set to true to indicate that this Action should never
|
||||||
|
// be the last Action applied, even if that means the maxDepth of the Run is
|
||||||
|
// gone over.
|
||||||
|
Incomplete bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checker implements a very basic property checker. It generates random test
|
||||||
|
// cases, attempting to find and print out failing ones.
|
||||||
|
type Checker struct {
|
||||||
|
// Init returns the initial state of the test. It should always return the
|
||||||
|
// exact same value.
|
||||||
|
Init func() State
|
||||||
|
|
||||||
|
// Actions returns possible Actions which could be applied to the given
|
||||||
|
// State. This is called after Init and after every subsequent Action is
|
||||||
|
// applied.
|
||||||
|
Actions func(State) []Action
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run performs RunOnce in a loop until maxDuration has elapsed.
|
||||||
|
func (c Checker) Run(maxDepth int, maxDuration time.Duration) error {
|
||||||
|
doneTimer := time.After(maxDuration)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-doneTimer:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.RunOnce(maxDepth); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunOnce generates a single sequence of Actions and applies them in order,
|
||||||
|
// returning nil once the number of Actions performed has reached maxDepth or a
|
||||||
|
// CheckErr if an error is returned.
|
||||||
|
func (c Checker) RunOnce(maxDepth int) error {
|
||||||
|
s := c.Init()
|
||||||
|
applied := make([]Applyer, 0, maxDepth)
|
||||||
|
for {
|
||||||
|
actions := c.Actions(s)
|
||||||
|
action := mrand.Element(actions, func(i int) uint64 {
|
||||||
|
if actions[i].Weight == 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return actions[i].Weight
|
||||||
|
}).(Action)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
s, err = action.Apply(s)
|
||||||
|
applied = append(applied, action.Applyer)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return CheckerErr{
|
||||||
|
Applied: applied,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
} else if action.Incomplete {
|
||||||
|
continue
|
||||||
|
} else if len(applied) >= maxDepth {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunCase performs a single sequence of Applyers in order, returning a CheckErr
|
||||||
|
// if one is returned by one of the Applyers.
|
||||||
|
func (c Checker) RunCase(aa ...Applyer) error {
|
||||||
|
s := c.Init()
|
||||||
|
for i := range aa {
|
||||||
|
var err error
|
||||||
|
if s, err = aa[i].Apply(s); err != nil {
|
||||||
|
return CheckerErr{
|
||||||
|
Applied: aa[:i+1],
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
53
mtest/checker_test.go
Normal file
53
mtest/checker_test.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package mtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
. "testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testCheckerRunIncr int
|
||||||
|
|
||||||
|
func (i testCheckerRunIncr) Apply(s State) (State, error) {
|
||||||
|
si := s.(int) + int(i)
|
||||||
|
if si > 5 {
|
||||||
|
return nil, errors.New("went over 5")
|
||||||
|
}
|
||||||
|
return si, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckerRun(t *T) {
|
||||||
|
c := Checker{
|
||||||
|
Init: func() State { return 0 },
|
||||||
|
Actions: func(State) []Action {
|
||||||
|
return []Action{
|
||||||
|
{
|
||||||
|
Applyer: testCheckerRunIncr(1),
|
||||||
|
Weight: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Applyer: testCheckerRunIncr(-1),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4 Actions should never be able to go over 5
|
||||||
|
if err := c.Run(4, time.Second); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 20 should always go over 5 eventually
|
||||||
|
err := c.Run(20, time.Second)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error when maxDepth is 20")
|
||||||
|
} else if len(err.(CheckerErr).Applied) < 6 {
|
||||||
|
t.Fatalf("strange CheckerErr when maxDepth is 20: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("got expected error with large maxDepth:\n%s", err)
|
||||||
|
caseErr := c.RunCase(err.(CheckerErr).Applied...)
|
||||||
|
if caseErr == nil || err.Error() != caseErr.Error() {
|
||||||
|
t.Fatalf("unexpected caseErr: %v", caseErr)
|
||||||
|
}
|
||||||
|
}
|
2
mtest/mtest.go
Normal file
2
mtest/mtest.go
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Package mtest implements functionality useful for testing.
|
||||||
|
package mtest
|
Loading…
Reference in New Issue
Block a user