package ffi
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
)
type kvIter interface {
SetBatchSize(int)
Next() bool
Key() []byte
Value() []byte
Err() error
Drop() error
}
type borrowIter struct{ it *Iterator }
func (b *borrowIter) SetBatchSize(batchSize int) { b.it.SetBatchSize(batchSize) }
func (b *borrowIter) Next() bool { return b.it.NextBorrowed() }
func (b *borrowIter) Key() []byte { return b.it.Key() }
func (b *borrowIter) Value() []byte { return b.it.Value() }
func (b *borrowIter) Err() error { return b.it.Err() }
func (b *borrowIter) Drop() error { return b.it.Drop() }
func assertIteratorYields(r *require.Assertions, it kvIter, keys [][]byte, vals [][]byte) {
i := 0
for ; it.Next(); i += 1 {
r.Equal(keys[i], it.Key())
r.Equal(vals[i], it.Value())
}
r.NoError(it.Err())
r.Equal(len(keys), i)
}
type iteratorConfigFn = func(it kvIter) kvIter
var iterConfigs = map[string]iteratorConfigFn{
"Owned": func(it kvIter) kvIter { return it },
"Borrowed": func(it kvIter) kvIter { return &borrowIter{it: it.(*Iterator)} },
"Single": func(it kvIter) kvIter {
it.SetBatchSize(1)
return it
},
"Batched": func(it kvIter) kvIter {
it.SetBatchSize(100)
return it
},
}
func runIteratorTestForModes(t *testing.T, fn func(*testing.T, iteratorConfigFn), modes ...string) {
testName := strings.Join(modes, "/")
t.Run(testName, func(t *testing.T) {
r := require.New(t)
fn(t, func(it kvIter) kvIter {
for _, m := range modes {
config, ok := iterConfigs[m]
r.Truef(ok, "specified config mode %s does not exist", m)
it = config(it)
}
return it
})
})
}
func runIteratorTestForAllModes(parentT *testing.T, fn func(*testing.T, iteratorConfigFn)) {
for _, dataMode := range []string{"Owned", "Borrowed"} {
for _, batchMode := range []string{"Single", "Batched"} {
runIteratorTestForModes(parentT, fn, batchMode, dataMode)
}
}
}
func TestIter(t *testing.T) {
r := require.New(t)
db := newTestDatabase(t)
keys, vals, batch := kvForTest(100)
_, err := db.Update(batch)
r.NoError(err)
runIteratorTestForAllModes(t, func(t *testing.T, cfn iteratorConfigFn) {
r := require.New(t)
rev, err := db.LatestRevision()
r.NoError(err)
it, err := rev.Iter(nil)
r.NoError(err)
t.Cleanup(func() {
r.NoError(it.Drop())
r.NoError(rev.Drop())
})
assertIteratorYields(r, cfn(it), keys, vals)
})
}
func TestIterOnRoot(t *testing.T) {
r := require.New(t)
db := newTestDatabase(t)
keys, vals, batch := kvForTest(240)
firstRoot, err := db.Update(batch[:80])
r.NoError(err)
secondRoot, err := db.Update(batch[80:160])
r.NoError(err)
thirdRoot, err := db.Update(batch[160:])
r.NoError(err)
runIteratorTestForAllModes(t, func(t *testing.T, cfn iteratorConfigFn) {
r := require.New(t)
r1, err := db.Revision(firstRoot)
r.NoError(err)
h1, err := r1.Iter(nil)
r.NoError(err)
t.Cleanup(func() {
r.NoError(h1.Drop())
r.NoError(r1.Drop())
})
r2, err := db.Revision(secondRoot)
r.NoError(err)
h2, err := r2.Iter(nil)
r.NoError(err)
t.Cleanup(func() {
r.NoError(h2.Drop())
r.NoError(r2.Drop())
})
r3, err := db.Revision(thirdRoot)
r.NoError(err)
h3, err := r3.Iter(nil)
r.NoError(err)
t.Cleanup(func() {
r.NoError(h3.Drop())
r.NoError(r3.Drop())
})
assertIteratorYields(r, cfn(h1), keys[:80], vals[:80])
assertIteratorYields(r, cfn(h2), keys[:160], vals[:160])
assertIteratorYields(r, cfn(h3), keys, vals)
})
}
func TestIterOnProposal(t *testing.T) {
r := require.New(t)
db := newTestDatabase(t)
keys, vals, batch := kvForTest(240)
_, err := db.Update(batch)
r.NoError(err)
runIteratorTestForAllModes(t, func(t *testing.T, cfn iteratorConfigFn) {
r := require.New(t)
updatedValues := make([][]byte, len(vals))
copy(updatedValues, vals)
changedKeys := make([][]byte, 0)
changedVals := make([][]byte, 0)
for i := 0; i < len(vals); i += 4 {
changedKeys = append(changedKeys, keys[i])
newVal := []byte{byte(i)}
changedVals = append(changedVals, newVal)
updatedValues[i] = newVal
}
p, err := db.Propose(makeBatch(changedKeys, changedVals))
r.NoError(err)
it, err := p.Iter(nil)
r.NoError(err)
t.Cleanup(func() {
r.NoError(it.Drop())
})
assertIteratorYields(r, cfn(it), keys, updatedValues)
})
}
func TestIterAfterProposalCommit(t *testing.T) {
r := require.New(t)
db := newTestDatabase(t)
keys, vals, batch := kvForTest(10)
p, err := db.Propose(batch)
r.NoError(err)
it, err := p.Iter(nil)
r.NoError(err)
t.Cleanup(func() {
r.NoError(it.Drop())
})
err = p.Commit()
r.NoError(err)
assertIteratorYields(r, it, keys, vals)
}
func TestIterUpdate(t *testing.T) {
r := require.New(t)
db := newTestDatabase(t)
keys, vals, batch := kvForTest(10)
_, err := db.Update(batch)
r.NoError(err)
rev, err := db.LatestRevision()
r.NoError(err)
it, err := rev.Iter(nil)
r.NoError(err)
t.Cleanup(func() {
r.NoError(it.Drop())
r.NoError(rev.Drop())
})
_, _, batch2 := kvForTest(10)
_, err = db.Update(batch2)
r.NoError(err)
assertIteratorYields(r, it, keys, vals)
}
func TestIterDone(t *testing.T) {
r := require.New(t)
db := newTestDatabase(t)
keys, vals, batch := kvForTest(18)
_, err := db.Update(batch)
r.NoError(err)
rev, err := db.LatestRevision()
r.NoError(err)
it, err := rev.Iter(nil)
r.NoError(err)
t.Cleanup(func() {
r.NoError(it.Drop())
r.NoError(rev.Drop())
})
assertIteratorYields(r, it, keys, vals)
r.False(it.Next())
r.NoError(it.Err())
it2, err := rev.Iter(nil)
t.Cleanup(func() {
r.NoError(it2.Drop())
})
r.NoError(err)
it2.SetBatchSize(5)
assertIteratorYields(r, it2, keys, vals)
r.False(it.Next())
r.NoError(it.Err())
r.NoError(it2.Drop())
}
func TestIterOutlivesRevision(t *testing.T) {
r := require.New(t)
db := newTestDatabase(t, func(config *config) {
config.revisions = 2
})
keys, vals, batch := kvForTest(30)
_, err := db.Update(batch[:10])
r.NoErrorf(err, "%T.Update(...)", db)
rev, err := db.LatestRevision()
r.NoErrorf(err, "%T.LatestRevision()", db)
it, err := rev.Iter(nil)
r.NoErrorf(err, "%T.Iter()", rev)
t.Cleanup(func() {
r.NoErrorf(it.Drop(), "%T.Drop()", it)
})
r.NoErrorf(rev.Drop(), "%T.Drop()", rev)
_, err = db.Update(batch[10:20])
r.NoErrorf(err, "%T.Update(...)", db)
_, err = db.Update(batch[20:])
r.NoErrorf(err, "%T.Update(...)", db)
assertIteratorYields(r, it, keys[:10], vals[:10])
}
func TestIterOutlivesProposal(t *testing.T) {
r := require.New(t)
db := newTestDatabase(t)
keys, vals, batch := kvForTest(4)
p, err := db.Propose(batch[:2])
r.NoErrorf(err, "%T.Propose(...)", db)
it, err := p.Iter(nil)
r.NoErrorf(err, "%T.Iter()", p)
t.Cleanup(func() {
r.NoErrorf(it.Drop(), "%T.Drop()", it)
})
r.NoErrorf(p.Drop(), "%T.Drop()", p)
p2, err := db.Propose(batch[2:])
r.NoErrorf(err, "%T.Propose(...)", db)
r.NoErrorf(p2.Commit(), "%T.Commit(...)", db)
assertIteratorYields(r, it, keys[:2], vals[:2])
}