featurebase/rbf/rbf_test.go
2022-09-06 09:39:22 -07:00

192 lines
5.5 KiB
Go

// Copyright 2022 Molecula Corp. (DBA FeatureBase).
// SPDX-License-Identifier: Apache-2.0
package rbf_test
import (
"flag"
"fmt"
"io"
"math/rand"
"os"
"runtime"
"sort"
"testing"
"github.com/featurebasedb/featurebase/v3/logger"
"github.com/featurebasedb/featurebase/v3/rbf"
rbfcfg "github.com/featurebasedb/featurebase/v3/rbf/cfg"
"github.com/featurebasedb/featurebase/v3/testhook"
)
var quickCheckN *int = flag.Int("quickchecks", 10, "The number of iterations for each quickcheck")
// Ensure root record helper functions work to read & write root records.
func TestReadWriteRootRecord(t *testing.T) {
t.Run("OK", func(t *testing.T) {
buf := make([]byte, 26)
// Write records.
if remaining, err := rbf.WriteRootRecord(buf, &rbf.RootRecord{Pgno: 10, Name: "foo"}); err != nil {
t.Fatal(err)
} else if remaining, err = rbf.WriteRootRecord(remaining, &rbf.RootRecord{Pgno: 11, Name: "bar"}); err != nil {
t.Fatal(err)
} else if _, err := rbf.WriteRootRecord(remaining, &rbf.RootRecord{Pgno: 12, Name: "baz"}); err != io.ErrShortBuffer {
t.Fatalf("unexpected error: %#v", err) // buffer too short
}
// Read records back.
if rec, remaining, err := rbf.ReadRootRecord(buf); err != nil {
t.Fatal(err)
} else if got, want := *rec, (rbf.RootRecord{Pgno: 10, Name: "foo"}); got != want {
t.Fatalf("ReadRootRecord=%#v, want %#v", got, want)
} else if rec, remaining, err = rbf.ReadRootRecord(remaining); err != nil {
t.Fatal(err)
} else if got, want := *rec, (rbf.RootRecord{Pgno: 11, Name: "bar"}); got != want {
t.Fatalf("ReadRootRecord=%#v, want %#v", got, want)
} else if rec, _, _ := rbf.ReadRootRecord(remaining); rec != nil {
t.Fatalf("expected nil record, got %#v", rec)
}
})
}
// NewDB returns a new instance of DB with a temporary path.
func NewDB(tb testing.TB, cfg ...*rbfcfg.Config) *rbf.DB {
path, err := testhook.TempDir(tb, "rbfdb")
if err != nil {
panic(err)
}
return NewDBAt(tb, path, cfg...)
}
// NewDBAt returns a new instance of DB with a given path.
func NewDBAt(tb testing.TB, path string, cfg ...*rbfcfg.Config) *rbf.DB {
var cfg0 *rbfcfg.Config
if len(cfg) > 0 {
cfg0 = cfg[0]
}
return rbf.NewDB(path, cfg0)
}
// MustOpenDB returns a db opened on a temporary file. On error, fail test.
func MustOpenDB(tb testing.TB, cfg ...*rbfcfg.Config) *rbf.DB {
tb.Helper()
path, err := testhook.TempDir(tb, "rbfdb")
if err != nil {
panic(err)
}
return MustOpenDBAt(tb, path, cfg...)
}
// MustOpenDBAt returns a db opened on an existing file. On error, fail test.
func MustOpenDBAt(tb testing.TB, path string, cfg ...*rbfcfg.Config) *rbf.DB {
tb.Helper()
if len(cfg) == 0 || cfg[0] == nil {
newconf := rbfcfg.NewDefaultConfig()
newconf.Logger = logger.NewLogfLogger(tb)
cfg = []*rbfcfg.Config{newconf}
} else if cfg[0].Logger == nil {
cfg[0].Logger = logger.NewLogfLogger(tb)
}
db := NewDBAt(tb, path, cfg...)
if err := db.Open(); err != nil {
tb.Fatal(err)
}
return db
}
// MustCloseDB closes db. On error, fail test.
// This function also also performs an integrity check on the DB.
func MustCloseDB(tb testing.TB, db *rbf.DB) {
tb.Helper()
if err := db.Check(); err != nil && err != rbf.ErrClosed {
tb.Fatal(err)
}
MustCloseDBNoCheck(tb, db)
}
// MustCloseDBNoCheck closes db. On error, fail test.
func MustCloseDBNoCheck(tb testing.TB, db *rbf.DB) {
tb.Helper()
if n := db.TxN(); n != 0 {
tb.Fatalf("db still has %d active transactions; must closed before closing db", n)
} else if err := db.Close(); err != nil && err != rbf.ErrClosed {
tb.Fatal(err)
} else if err := os.RemoveAll(db.Path); err != nil {
tb.Fatal(err)
}
}
// MustReopenDB closes and reopens a database.
func MustReopenDB(tb testing.TB, db *rbf.DB) *rbf.DB {
tb.Helper()
if err := db.Check(); err != nil {
tb.Fatal(err)
} else if err := db.Close(); err != nil {
tb.Fatal(err)
}
other := rbf.NewDB(db.Path, nil)
if err := other.Open(); err != nil {
tb.Fatal(err)
}
return other
}
// MustBegin returns a new transaction or fails.
func MustBegin(tb testing.TB, db *rbf.DB, writable bool) *rbf.Tx {
tb.Helper()
tx, err := db.Begin(writable)
if err != nil {
tb.Fatal(err)
}
return tx
}
var _ = MustAddRandom
// MustAddRandom adds values to a bitmap in a random order.
func MustAddRandom(tb testing.TB, rand *rand.Rand, tx *rbf.Tx, name string, values ...uint64) {
tb.Helper()
for _, i := range rand.Perm(len(values)) {
v := values[i]
if _, err := tx.Add(name, v); err != nil {
tb.Fatalf("Add(%d) i=%d err=%q", v, i, err)
}
}
}
// GenerateValues returns a sorted list of random values.
func GenerateValues(rand *rand.Rand, n int) []uint64 {
a := make([]uint64, n)
for i := range a {
// Cram them into the first quarter of the shard, so we'll
// see more updates to existing things in smallish random
// tests.
a[i] = uint64(rand.Intn(rbf.ShardWidth / 4))
}
sort.Slice(a, func(i, j int) bool { return a[i] < a[j] })
prev := uint64(0)
// eliminate duplicates by bumping things a bit
for i := range a {
if a[i] <= prev {
a[i] = prev + 1
}
prev = a[i]
}
return a
}
// QuickCheck executes fn multiple times with a different PRNG.
func QuickCheck(t *testing.T, fn func(t *testing.T, rand *rand.Rand)) {
for i := 0; i < *quickCheckN; i++ {
t.Run(fmt.Sprint(i), func(t *testing.T) {
fn(t, rand.New(rand.NewSource(int64(i))))
})
}
}
func highbits(v uint64) uint64 { return v >> 16 }
func lowbits(v uint64) uint16 { return uint16(v & 0xFFFF) }
// is32Bit returns true if the architecture is 32-bit.
func is32Bit() bool { return runtime.GOARCH == "386" }