Skip to content

Commit

Permalink
metamorphic: test Clone with Options
Browse files Browse the repository at this point in the history
Add coverage for Clone calls with non-nil Options reconfiguring the cloned
iterator.

Close cockroachdb#1904.
  • Loading branch information
jbowens committed Aug 24, 2022
1 parent 4cc0974 commit e729e74
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 97 deletions.
111 changes: 63 additions & 48 deletions internal/metamorphic/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ type iterOpts struct {
// ensure determinism.
filterMin uint64
filterMax uint64

// NB: If adding or removing fields, ensure IsZero is in sync.
}

func (o iterOpts) IsZero() bool {
return o.lower == nil && o.upper == nil && o.keyTypes == 0 &&
o.maskSuffix == nil && o.filterMin == 0 && o.filterMax == 0
}

type generator struct {
Expand Down Expand Up @@ -570,13 +577,19 @@ func (g *generator) newIterUsingClone() {
// closes.
}

// TODO(jackson): Exercise changing iterator options as a part of Clone.
var opts iterOpts
if g.rng.Intn(2) == 1 {
g.maybeMutateOptions(&opts)
g.itersLastOpts[iterID] = opts
} else {
g.itersLastOpts[iterID] = g.itersLastOpts[existingIterID]
}

g.itersLastOpts[iterID] = g.itersLastOpts[existingIterID]
g.add(&newIterUsingCloneOp{
existingIterID: existingIterID,
iterID: iterID,
refreshBatch: g.rng.Intn(2) == 1,
iterOpts: opts,
})
}

Expand Down Expand Up @@ -699,52 +712,7 @@ func (g *generator) iterSetBounds(iterID objID) {

func (g *generator) iterSetOptions(iterID objID) {
opts := g.itersLastOpts[iterID]

// With 95% probability, allow changes to any options at all. This ensures
// that in 5% of cases there are no changes, and SetOptions hits its fast
// path.
if g.rng.Intn(100) >= 5 {
// With 1/3 probability, clear existing bounds.
if opts.lower != nil && g.rng.Intn(3) == 0 {
opts.lower = nil
}
if opts.upper != nil && g.rng.Intn(3) == 0 {
opts.upper = nil
}
// With 1/3 probability, update the bounds.
if g.rng.Intn(3) == 0 {
// Generate a new key with a .1% probability.
opts.lower = g.randKeyToRead(0.001)
}
if g.rng.Intn(3) == 0 {
// Generate a new key with a .1% probability.
opts.upper = g.randKeyToRead(0.001)
}
if g.cmp(opts.lower, opts.upper) > 0 {
opts.lower, opts.upper = opts.upper, opts.lower
}

// With 1/3 probability, update the key-types/mask.
if g.rng.Intn(3) == 0 {
opts.keyTypes, opts.maskSuffix = g.randKeyTypesAndMask()
}

// With 1/3 probability, clear existing filter.
if opts.filterMax > 0 && g.rng.Intn(3) == 0 {
opts.filterMax, opts.filterMin = 0, 0
}
// With 10% probability, set a filter range.
if g.rng.Intn(10) == 1 {
max := g.cfg.writeSuffixDist.Max()
opts.filterMin, opts.filterMax = g.rng.Uint64n(max)+1, g.rng.Uint64n(max)+1
if opts.filterMin > opts.filterMax {
opts.filterMin, opts.filterMax = opts.filterMax, opts.filterMin
} else if opts.filterMin == opts.filterMax {
opts.filterMax = opts.filterMin + 1
}
}
}

g.maybeMutateOptions(&opts)
g.itersLastOpts[iterID] = opts
g.add(&iterSetOptionsOp{
iterID: iterID,
Expand Down Expand Up @@ -1098,6 +1066,53 @@ func (g *generator) writerSingleDelete() {
})
}

func (g *generator) maybeMutateOptions(opts *iterOpts) {
// With 95% probability, allow changes to any options at all. This ensures
// that in 5% of cases there are no changes, and SetOptions hits its fast
// path.
if g.rng.Intn(100) >= 5 {
// With 1/3 probability, clear existing bounds.
if opts.lower != nil && g.rng.Intn(3) == 0 {
opts.lower = nil
}
if opts.upper != nil && g.rng.Intn(3) == 0 {
opts.upper = nil
}
// With 1/3 probability, update the bounds.
if g.rng.Intn(3) == 0 {
// Generate a new key with a .1% probability.
opts.lower = g.randKeyToRead(0.001)
}
if g.rng.Intn(3) == 0 {
// Generate a new key with a .1% probability.
opts.upper = g.randKeyToRead(0.001)
}
if g.cmp(opts.lower, opts.upper) > 0 {
opts.lower, opts.upper = opts.upper, opts.lower
}

// With 1/3 probability, update the key-types/mask.
if g.rng.Intn(3) == 0 {
opts.keyTypes, opts.maskSuffix = g.randKeyTypesAndMask()
}

// With 1/3 probability, clear existing filter.
if opts.filterMax > 0 && g.rng.Intn(3) == 0 {
opts.filterMax, opts.filterMin = 0, 0
}
// With 10% probability, set a filter range.
if g.rng.Intn(10) == 1 {
max := g.cfg.writeSuffixDist.Max()
opts.filterMin, opts.filterMax = g.rng.Uint64n(max)+1, g.rng.Uint64n(max)+1
if opts.filterMin > opts.filterMax {
opts.filterMin, opts.filterMax = opts.filterMax, opts.filterMin
} else if opts.filterMin == opts.filterMax {
opts.filterMax = opts.filterMin + 1
}
}
}
}

func (g *generator) pickOneUniform(options ...func(objID)) func(objID) {
i := g.rng.Intn(len(options))
return options[i]
Expand Down
96 changes: 48 additions & 48 deletions internal/metamorphic/ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -561,31 +561,7 @@ type newIterOp struct {

func (o *newIterOp) run(t *test, h *history) {
r := t.getReader(o.readerID)
var lower, upper []byte
if o.lower != nil {
lower = append(lower, o.lower...)
}
if o.upper != nil {
upper = append(upper, o.upper...)
}
opts := &pebble.IterOptions{
LowerBound: lower,
UpperBound: upper,
KeyTypes: pebble.IterKeyType(o.keyTypes),
RangeKeyMasking: pebble.RangeKeyMasking{
Suffix: o.maskSuffix,
},
}
if opts.RangeKeyMasking.Suffix != nil {
opts.RangeKeyMasking.Filter = func() pebble.BlockPropertyFilterMask {
return blockprop.NewMaskingFilter()
}
}
if o.filterMax > 0 {
opts.PointKeyFilters = []pebble.BlockPropertyFilter{
blockprop.NewBlockPropertyFilter(o.filterMin, o.filterMax),
}
}
opts := iterOptions(o.iterOpts)

var i *pebble.Iterator
for {
Expand All @@ -600,9 +576,10 @@ func (o *newIterOp) run(t *test, h *history) {

// Trash the bounds to ensure that Pebble doesn't rely on the stability of
// the user-provided bounds.
rand.Read(lower[:])
rand.Read(upper[:])

if opts != nil {
rand.Read(opts.LowerBound[:])
rand.Read(opts.UpperBound[:])
}
h.Recordf("%s // %v", o, i.Error())
}

Expand All @@ -616,20 +593,33 @@ type newIterUsingCloneOp struct {
existingIterID objID
iterID objID
refreshBatch bool
iterOpts
}

func (o *newIterUsingCloneOp) run(t *test, h *history) {
iter := t.getIter(o.existingIterID)
i, err := iter.iter.Clone(pebble.CloneOptions{})
cloneOpts := pebble.CloneOptions{
IterOptions: iterOptions(o.iterOpts),
RefreshBatchView: o.refreshBatch,
}
i, err := iter.iter.Clone(cloneOpts)
if err != nil {
panic(err)
}
t.setIter(o.iterID, i, iter.filterMin, iter.filterMax)
filterMin, filterMax := o.filterMin, o.filterMax
if cloneOpts.IterOptions == nil {
// We're adopting the same block property filters as iter, so we need to
// adopt the same run-time filters to ensure determinism.
filterMin, filterMax = iter.filterMin, iter.filterMax
}
t.setIter(o.iterID, i, filterMin, filterMax)
h.Recordf("%s // %v", o, i.Error())
}

func (o *newIterUsingCloneOp) String() string {
return fmt.Sprintf("%s = %s.Clone(%t)", o.iterID, o.existingIterID, o.refreshBatch)
return fmt.Sprintf("%s = %s.Clone(%t, %q, %q, %d /* key types */, %d, %d, %q /* masking suffix */)",
o.iterID, o.existingIterID, o.refreshBatch, o.lower, o.upper,
o.keyTypes, o.filterMin, o.filterMax, o.maskSuffix)
}

// iterSetBoundsOp models an Iterator.SetBounds operation.
Expand Down Expand Up @@ -671,6 +661,32 @@ type iterSetOptionsOp struct {
func (o *iterSetOptionsOp) run(t *test, h *history) {
i := t.getIter(o.iterID)

opts := iterOptions(o.iterOpts)
if opts == nil {
opts = &pebble.IterOptions{}
}
i.SetOptions(opts)

// Trash the bounds to ensure that Pebble doesn't rely on the stability of
// the user-provided bounds.
rand.Read(opts.LowerBound[:])
rand.Read(opts.UpperBound[:])

// Adjust the iterator's filters.
i.filterMin, i.filterMax = o.filterMin, o.filterMax

h.Recordf("%s // %v", o, i.Error())
}

func (o *iterSetOptionsOp) String() string {
return fmt.Sprintf("%s.SetOptions(%q, %q, %d /* key types */, %d, %d, %q /* masking suffix */)",
o.iterID, o.lower, o.upper, o.keyTypes, o.filterMin, o.filterMax, o.maskSuffix)
}

func iterOptions(o iterOpts) *pebble.IterOptions {
if o.IsZero() {
return nil
}
var lower, upper []byte
if o.lower != nil {
lower = append(lower, o.lower...)
Expand All @@ -696,23 +712,7 @@ func (o *iterSetOptionsOp) run(t *test, h *history) {
blockprop.NewBlockPropertyFilter(o.filterMin, o.filterMax),
}
}

i.SetOptions(opts)

// Trash the bounds to ensure that Pebble doesn't rely on the stability of
// the user-provided bounds.
rand.Read(lower[:])
rand.Read(upper[:])

// Adjust the iterator's filters.
i.filterMin, i.filterMax = o.filterMin, o.filterMax

h.Recordf("%s // %v", o, i.Error())
}

func (o *iterSetOptionsOp) String() string {
return fmt.Sprintf("%s.SetOptions(%q, %q, %d /* key types */, %d, %d, %q /* masking suffix */)",
o.iterID, o.lower, o.upper, o.keyTypes, o.filterMin, o.filterMax, o.maskSuffix)
return opts
}

// iterSeekGEOp models an Iterator.SeekGE[WithLimit] operation.
Expand Down
2 changes: 1 addition & 1 deletion internal/metamorphic/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func opArgs(op op) (receiverID *objID, targetID *objID, args []interface{}) {
case *newIterOp:
return &t.readerID, &t.iterID, []interface{}{&t.lower, &t.upper, &t.keyTypes, &t.filterMin, &t.filterMax, &t.maskSuffix}
case *newIterUsingCloneOp:
return &t.existingIterID, &t.iterID, []interface{}{&t.refreshBatch}
return &t.existingIterID, &t.iterID, []interface{}{&t.refreshBatch, &t.lower, &t.upper, &t.keyTypes, &t.filterMin, &t.filterMax, &t.maskSuffix}
case *newSnapshotOp:
return nil, &t.snapID, nil
case *iterNextOp:
Expand Down

0 comments on commit e729e74

Please sign in to comment.