Skip to content

Commit

Permalink
itertest: add internal iterator datadriven helper
Browse files Browse the repository at this point in the history
Move the datadriven internal iterator helper out of the pebble package's
data_test.go file and into a new internal/itertest package so that it may be
used to drive unit tests outside of the primary pebble package.
  • Loading branch information
jbowens committed Nov 14, 2023
1 parent c0b4bd4 commit 6682fd5
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 181 deletions.
7 changes: 4 additions & 3 deletions batch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/cockroachdb/errors"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/batchskl"
"github.com/cockroachdb/pebble/internal/itertest"
"github.com/cockroachdb/pebble/internal/keyspan"
"github.com/cockroachdb/pebble/internal/testkeys"
"github.com/cockroachdb/pebble/vfs"
Expand Down Expand Up @@ -911,7 +912,7 @@ func TestBatchIter(t *testing.T) {
}
iter := b.newInternalIter(&options)
defer iter.Close()
return runInternalIterCmd(t, d, iter)
return itertest.RunInternalIterCmd(t, d, iter)

default:
return fmt.Sprintf("unknown command: %s", d.Cmd)
Expand Down Expand Up @@ -1028,7 +1029,7 @@ func TestFlushableBatchIter(t *testing.T) {
case "iter":
iter := b.newIter(nil)
defer iter.Close()
return runInternalIterCmd(t, d, iter)
return itertest.RunInternalIterCmd(t, d, iter)

default:
return fmt.Sprintf("unknown command: %s", d.Cmd)
Expand Down Expand Up @@ -1084,7 +1085,7 @@ func TestFlushableBatch(t *testing.T) {

iter := b.newIter(&opts)
defer iter.Close()
return runInternalIterCmd(t, d, iter)
return itertest.RunInternalIterCmd(t, d, iter)

case "dump":
if len(d.CmdArgs) != 1 || len(d.CmdArgs[0].Vals) != 1 || d.CmdArgs[0].Key != "seq" {
Expand Down
131 changes: 0 additions & 131 deletions data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,6 @@ import (
"github.com/stretchr/testify/require"
)

type iterCmdOpts struct {
verboseKey bool
stats *base.InternalIteratorStats
}

type iterCmdOpt func(*iterCmdOpts)

func iterCmdVerboseKey(opts *iterCmdOpts) { opts.verboseKey = true }

func iterCmdStats(stats *base.InternalIteratorStats) iterCmdOpt {
return func(opts *iterCmdOpts) {
opts.stats = stats
}
}

func runGetCmd(t testing.TB, td *datadriven.TestData, d *DB) string {
snap := Snapshot{
db: d,
Expand Down Expand Up @@ -394,122 +379,6 @@ func writeRangeKeys(b io.Writer, iter *Iterator) {
}
}

func runInternalIterCmd(
t *testing.T, d *datadriven.TestData, iter internalIterator, opts ...iterCmdOpt,
) string {
var o iterCmdOpts
for _, opt := range opts {
opt(&o)
}

getKV := func(key *InternalKey, val LazyValue) (*InternalKey, []byte) {
v, _, err := val.Value(nil)
require.NoError(t, err)
return key, v
}
var b bytes.Buffer
var prefix []byte
for _, line := range strings.Split(d.Input, "\n") {
parts := strings.Fields(line)
if len(parts) == 0 {
continue
}
var key *InternalKey
var value []byte
switch parts[0] {
case "seek-ge":
if len(parts) < 2 || len(parts) > 3 {
return "seek-ge <key> [<try-seek-using-next>]\n"
}
prefix = nil
var flags base.SeekGEFlags
if len(parts) == 3 {
if trySeekUsingNext, err := strconv.ParseBool(parts[2]); err != nil {
return err.Error()
} else if trySeekUsingNext {
flags = flags.EnableTrySeekUsingNext()
}
}
key, value = getKV(iter.SeekGE([]byte(strings.TrimSpace(parts[1])), flags))
case "seek-prefix-ge":
if len(parts) != 2 && len(parts) != 3 {
return "seek-prefix-ge <key> [<try-seek-using-next>]\n"
}
prefix = []byte(strings.TrimSpace(parts[1]))
var flags base.SeekGEFlags
if len(parts) == 3 {
if trySeekUsingNext, err := strconv.ParseBool(parts[2]); err != nil {
return err.Error()
} else if trySeekUsingNext {
flags = flags.EnableTrySeekUsingNext()
}
}
key, value = getKV(iter.SeekPrefixGE(prefix, prefix /* key */, flags))
case "seek-lt":
if len(parts) != 2 {
return "seek-lt <key>\n"
}
prefix = nil
key, value = getKV(iter.SeekLT([]byte(strings.TrimSpace(parts[1])), base.SeekLTFlagsNone))
case "first":
prefix = nil
key, value = getKV(iter.First())
case "last":
prefix = nil
key, value = getKV(iter.Last())
case "next":
key, value = getKV(iter.Next())
case "prev":
key, value = getKV(iter.Prev())
case "set-bounds":
if len(parts) <= 1 || len(parts) > 3 {
return "set-bounds lower=<lower> upper=<upper>\n"
}
var lower []byte
var upper []byte
for _, part := range parts[1:] {
arg := strings.Split(strings.TrimSpace(part), "=")
switch arg[0] {
case "lower":
lower = []byte(arg[1])
case "upper":
upper = []byte(arg[1])
default:
return fmt.Sprintf("set-bounds: unknown arg: %s", arg)
}
}
iter.SetBounds(lower, upper)
continue
case "stats":
if o.stats != nil {
// The timing is non-deterministic, so set to 0.
o.stats.BlockReadDuration = 0
fmt.Fprintf(&b, "%+v\n", *o.stats)
}
continue
case "reset-stats":
if o.stats != nil {
*o.stats = base.InternalIteratorStats{}
}
continue
default:
return fmt.Sprintf("unknown op: %s", parts[0])
}
if key != nil {
if o.verboseKey {
fmt.Fprintf(&b, "%s:%s\n", key, value)
} else {
fmt.Fprintf(&b, "%s:%s\n", key.UserKey, value)
}
} else if err := iter.Error(); err != nil {
fmt.Fprintf(&b, "err=%v\n", err)
} else {
fmt.Fprintf(&b, ".\n")
}
}
return b.String()
}

func runBatchDefineCmd(d *datadriven.TestData, b *Batch) error {
for _, line := range strings.Split(d.Input, "\n") {
parts := strings.Fields(line)
Expand Down
3 changes: 2 additions & 1 deletion external_iterator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/cockroachdb/errors"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/cache"
"github.com/cockroachdb/pebble/internal/itertest"
"github.com/cockroachdb/pebble/internal/testkeys"
"github.com/cockroachdb/pebble/objstorage/objstorageprovider"
"github.com/cockroachdb/pebble/sstable"
Expand Down Expand Up @@ -123,7 +124,7 @@ func TestSimpleLevelIter(t *testing.T) {
it := &simpleLevelIter{cmp: o.Comparer.Compare, iters: internalIters}
it.init(IterOptions{})

response := runInternalIterCmd(t, td, it)
response := itertest.RunInternalIterCmd(t, td, it)
require.NoError(t, it.Close())
return response
default:
Expand Down
196 changes: 196 additions & 0 deletions internal/itertest/datadriven.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// Copyright 2023 The LevelDB-Go and Pebble Authors. All rights reserved. Use
// of this source code is governed by a BSD-style license that can be found in
// the LICENSE file.

// Package itertest provides facilities for testing internal iterators.
package itertest

import (
"bytes"
"fmt"
"io"
"strconv"
"strings"
"testing"

"github.com/cockroachdb/datadriven"
"github.com/cockroachdb/pebble/internal/base"
"github.com/stretchr/testify/require"
)

type iterCmdOpts struct {
fmtKV func(io.Writer, *base.InternalKey, []byte, base.InternalIterator)
stats *base.InternalIteratorStats
}

// An IterOpt configures the behavior of RunInternalIterCmd.
type IterOpt func(*iterCmdOpts)

// Verbose configures RunInternalIterCmd to output verbose results.
func Verbose(opts *iterCmdOpts) { opts.fmtKV = verboseFmt }

// Condensed configures RunInternalIterCmd to output condensed results without
// values.
func Condensed(opts *iterCmdOpts) { opts.fmtKV = condensedFmt }

// WithStats configures RunInternalIterCmd to collect iterator stats in the
// struct pointed to by stats.
func WithStats(stats *base.InternalIteratorStats) IterOpt {
return func(opts *iterCmdOpts) {
opts.stats = stats
}
}

func defaultFmt(w io.Writer, key *base.InternalKey, v []byte, iter base.InternalIterator) {
if key != nil {
fmt.Fprintf(w, "%s:%s\n", key.UserKey, v)
} else if err := iter.Error(); err != nil {
fmt.Fprintf(w, "err=%v\n", err)
} else {
fmt.Fprintf(w, ".\n")
}
}

func condensedFmt(w io.Writer, key *base.InternalKey, v []byte, iter base.InternalIterator) {
if key != nil {
fmt.Fprintf(w, "<%s:%d>", key.UserKey, key.SeqNum())
} else if err := iter.Error(); err != nil {
fmt.Fprintf(w, "err=%v", err)
} else {
fmt.Fprint(w, ".")
}
}

func verboseFmt(w io.Writer, key *base.InternalKey, v []byte, iter base.InternalIterator) {
if key != nil {
fmt.Fprintf(w, "%s:%s\n", key, v)
return
}
defaultFmt(w, key, v, iter)
}

// RunInternalIterCmd evaluates a datadriven command controlling an internal
// iterator, returning a string with the results of the iterator operations.
func RunInternalIterCmd(
t *testing.T, d *datadriven.TestData, iter base.InternalIterator, opts ...IterOpt,
) string {
var buf bytes.Buffer
RunInternalIterCmdWriter(t, &buf, d, iter, opts...)
return buf.String()
}

// RunInternalIterCmdWriter evaluates a datadriven command controlling an
// internal iterator, writing the results of the iterator operations to the
// provided Writer.
func RunInternalIterCmdWriter(
t *testing.T, w io.Writer, d *datadriven.TestData, iter base.InternalIterator, opts ...IterOpt,
) {
o := iterCmdOpts{fmtKV: defaultFmt}
for _, opt := range opts {
opt(&o)
}

getKV := func(key *base.InternalKey, val base.LazyValue) (*base.InternalKey, []byte) {
v, _, err := val.Value(nil)
require.NoError(t, err)
return key, v
}
var prefix []byte
for _, line := range strings.Split(d.Input, "\n") {
parts := strings.Fields(line)
if len(parts) == 0 {
continue
}
var key *base.InternalKey
var value []byte
switch parts[0] {
case "seek-ge":
if len(parts) < 2 || len(parts) > 3 {
fmt.Fprint(w, "seek-ge <key> [<try-seek-using-next>]\n")
return
}
prefix = nil
var flags base.SeekGEFlags
if len(parts) == 3 {
if trySeekUsingNext, err := strconv.ParseBool(parts[2]); err != nil {
fmt.Fprintf(w, "%s", err.Error())
return
} else if trySeekUsingNext {
flags = flags.EnableTrySeekUsingNext()
}
}
key, value = getKV(iter.SeekGE([]byte(strings.TrimSpace(parts[1])), flags))
case "seek-prefix-ge":
if len(parts) != 2 && len(parts) != 3 {
fmt.Fprint(w, "seek-prefix-ge <key> [<try-seek-using-next>]\n")
return
}
prefix = []byte(strings.TrimSpace(parts[1]))
var flags base.SeekGEFlags
if len(parts) == 3 {
if trySeekUsingNext, err := strconv.ParseBool(parts[2]); err != nil {
fmt.Fprintf(w, "%s", err.Error())
return
} else if trySeekUsingNext {
flags = flags.EnableTrySeekUsingNext()
}
}
key, value = getKV(iter.SeekPrefixGE(prefix, prefix /* key */, flags))
case "seek-lt":
if len(parts) != 2 {
fmt.Fprint(w, "seek-lt <key>\n")
return
}
prefix = nil
key, value = getKV(iter.SeekLT([]byte(strings.TrimSpace(parts[1])), base.SeekLTFlagsNone))
case "first":
prefix = nil
key, value = getKV(iter.First())
case "last":
prefix = nil
key, value = getKV(iter.Last())
case "next":
key, value = getKV(iter.Next())
case "prev":
key, value = getKV(iter.Prev())
case "set-bounds":
if len(parts) <= 1 || len(parts) > 3 {
fmt.Fprint(w, "set-bounds lower=<lower> upper=<upper>\n")
return
}
var lower []byte
var upper []byte
for _, part := range parts[1:] {
arg := strings.Split(strings.TrimSpace(part), "=")
switch arg[0] {
case "lower":
lower = []byte(arg[1])
case "upper":
upper = []byte(arg[1])
default:
fmt.Fprintf(w, "set-bounds: unknown arg: %s", arg)
return
}
}
iter.SetBounds(lower, upper)
continue
case "stats":
if o.stats != nil {
// The timing is non-deterministic, so set to 0.
o.stats.BlockReadDuration = 0
fmt.Fprintf(w, "%+v\n", *o.stats)
}
continue
case "reset-stats":
if o.stats != nil {
*o.stats = base.InternalIteratorStats{}
}
continue
default:
fmt.Fprintf(w, "unknown op: %s", parts[0])
return
}
o.fmtKV(w, key, value, iter)

}
}
Loading

0 comments on commit 6682fd5

Please sign in to comment.