Skip to content

Commit

Permalink
meta: add state machine for generating sequences of operations
Browse files Browse the repository at this point in the history
Currently, sequences of operations for metamorphic tests are generated
for certain specific cases (e.g. a SINGLEDEL follows a key that has been
SET once). Additional test sequences require maintaining the "state" of
the sequence in the `generator`, which clutters the struct with various
fields required for the state management.

Add a `sequenceGenerator` struct, which uses a "transition map" (a
mapping from a current state to next state, along with a corresponding
output for the transition) to model a state machine. The state machine
can be used to generate random sequences of operations that adhere to
certain rules (e.g. output a GET following a SET for a given key).

A `generator` can be constructed containing one or more
`sequenceGenerator`s that, when selected by the random number generator
(i.e. the "deck"), generate the next operation in the sequence governed
by the state machine and place the operation into the operation log.

Related to cockroachdb/cockroach#69414.
  • Loading branch information
nicktrav committed Sep 7, 2021
1 parent 47a0129 commit 565ec92
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 1 deletion.
2 changes: 2 additions & 0 deletions internal/metamorphic/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const (
writerMerge
writerSet
writerSingleDelete
pickScenario
)

type config struct {
Expand Down Expand Up @@ -93,5 +94,6 @@ var defaultConfig = config{
writerMerge: 100,
writerSet: 100,
writerSingleDelete: 25,
pickScenario: 100,
},
}
28 changes: 27 additions & 1 deletion internal/metamorphic/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ type generator struct {
// snapshotID -> snapshot iters: used to keep track of the open iterators on
// a snapshot. The iter set value will also be indexed by the readers map.
snapshots map[objID]objIDSet
// Set of custom scenarios for which to generate ops for.
scenarios []*sequenceGenerator
}

func newGenerator(rng *rand.Rand) *generator {
Expand All @@ -86,6 +88,11 @@ func newGenerator(rng *rand.Rand) *generator {
g.writerToSingleSetKeys[makeObjID(dbTag, 0)] = g.singleSetKeysInDB
// Note that the initOp fields are populated during generation.
g.ops = append(g.ops, g.init)

// Specific scenarios for which to generate operations for.
// TODO(travers): Add additional sequences.
g.scenarios = []*sequenceGenerator{}

return g
}

Expand Down Expand Up @@ -126,12 +133,13 @@ func generate(rng *rand.Rand, count uint64, cfg config) []op {
writerMerge: g.writerMerge,
writerSet: g.writerSet,
writerSingleDelete: g.writerSingleDelete,
pickScenario: g.pickScenario,
}

// TPCC-style deck of cards randomization. Every time the end of the deck is
// reached, we shuffle the deck.
deck := randvar.NewDeck(g.rng, cfg.ops...)
for i := uint64(0); i < count; i++ {
for uint64(len(g.ops)) < count {
generators[deck.Int()]()
}

Expand Down Expand Up @@ -862,6 +870,24 @@ func (g *generator) tryRepositionBatchIters(writerID objID) {
}
}

func (g *generator) pickScenario() {
// All scenarios have been exhausted.
if len(g.scenarios) == 0 {
return
}
// Pick a random scenario to generate the next operation.
idx := g.rng.Intn(len(g.scenarios))
s := g.scenarios[idx]
op := s.next(g.rng)
if op == nil {
// The scenario ran to completion. Remove the scenario from the slice.
g.scenarios[idx] = g.scenarios[len(g.scenarios)-1]
g.scenarios = g.scenarios[:len(g.scenarios)-1]
return
}
g.add(op)
}

func (g *generator) String() string {
var buf bytes.Buffer
for _, op := range g.ops {
Expand Down
49 changes: 49 additions & 0 deletions internal/metamorphic/sequence.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package metamorphic

import "golang.org/x/exp/rand"

// transitionFn is a node in the state machine graph. The node is a function
// that generates the op resulting from moving to this node and the next opType
// representing the next state in the state machine.
type transitionFn func(rng *rand.Rand) (current op, next opType)

// transitionMap is a blueprint for generating sequences of ops. It functions as
// a graph representing a state machine. Each node in the graph is a function
// returning the current op and the next opType to transition to.
type transitionMap map[opType]transitionFn

// stateDone is the terminal state for the state machine.
var stateDone opType = -1

// sequenceGenerator generates a sequence of ops from an transitionMap
// corresponding to a specific sequenceGenerator.
type sequenceGenerator struct {
tMap transitionMap
nextState opType
steps int
}

// newSequenceGenerator constructs a new sequenceGenerator with the given
// transition map, post-ops, and the initial state of the sequence.
func newSequenceGenerator(m transitionMap, initial opType) *sequenceGenerator {
return &sequenceGenerator{
tMap: m,
nextState: initial,
}
}

// next generates the next ops from the transitionMap state machine, and
// transitions the transitionMap to the next state.
func (g *sequenceGenerator) next(rng *rand.Rand) op {
if g.nextState == stateDone {
return nil
}

// Randomly pick the next state to transition into. Generate the op from
// this transition and move to the next state.
var o op
o, g.nextState = g.tMap[g.nextState](rng)
g.steps++

return o
}
68 changes: 68 additions & 0 deletions internal/metamorphic/sequence_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package metamorphic

import (
"testing"

"github.com/stretchr/testify/require"
"golang.org/x/exp/rand"
)

func TestSequenceGenerator(t *testing.T) {
n := 10
key, value := []byte("foo"), []byte("bar")

// A simple state machine that alternates states between GET and SET n times
// before transitioning into a terminal state.
tMap := transitionMap{
readerGet: func(rng *rand.Rand) (current op, next opType) {
current = &getOp{key: key, readerID: makeObjID(dbTag, 0)}
n--
if n > 0 {
next = writerSet
} else {
next = stateDone
}
return
},
writerSet: func(rng *rand.Rand) (current op, next opType) {
current = &setOp{key: key, value: value, writerID: makeObjID(dbTag, 0)}
n--
if n > 0 {
next = readerGet
} else {
next = stateDone
}
return
},
}

rng := rand.New(rand.NewSource(0))
g := newSequenceGenerator(tMap, readerGet)

// Generate the sequence from the state machine, counting GETs and SETs.
var nGet, nSet int
var last opType
for {
op := g.next(rng)
if op == nil {
break // Terminal state.
}
switch op.(type) {
case *getOp:
nGet++
if last > 0 {
require.Equal(t, writerSet, last)
}
last = readerGet
case *setOp:
nSet++
require.Equal(t, readerGet, last)
last = writerSet
default:
t.Fatalf("unknown op type: %s", op)
}
}

require.Equal(t, nGet, 5)
require.Equal(t, nSet, 5)
}

0 comments on commit 565ec92

Please sign in to comment.