mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
The testing package is full of subtle magic, and one of the most subtle is this: t.Logf, etcetera, all write to a buffer which is then displayed after the test is run. Which means that, if you exit, the buffer is never displayed. This means that, if a test case can fail in a way that causes an instant exit, you don't hit defers, you don't get your log messages, you just get a mysterious exit of the process. We have two cases where backup commands were calling log.Fatal instead of returning an error. The error in question is displayed correctly and informatively if returned, so we return it. We also have one case where we were using os.Exit to avoid a deadlock. Instead, we make the thing that would deadlock conditional on the test not having failed. In the event that the test fails, we now print our failure message correctly, then also report an unclosed cluster. That's fine.
335 lines
9.4 KiB
Go
335 lines
9.4 KiB
Go
// Copyright 2022 Molecula Corp. (DBA FeatureBase).
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
package pilosa_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
pilosa "github.com/featurebasedb/featurebase/v3"
|
|
"github.com/featurebasedb/featurebase/v3/disco"
|
|
"github.com/featurebasedb/featurebase/v3/pql"
|
|
"github.com/featurebasedb/featurebase/v3/test"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// ShardWidth is a helper reference to use when testing.
|
|
const ShardWidth = pilosa.ShardWidth
|
|
|
|
// Ensure index can open and retrieve a field.
|
|
func TestIndex_CreateFieldIfNotExists(t *testing.T) {
|
|
_, index := test.MustOpenIndex(t)
|
|
|
|
// Create field.
|
|
f, err := index.CreateFieldIfNotExists("f", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
} else if f == nil {
|
|
t.Fatal("expected field")
|
|
}
|
|
|
|
// Retrieve existing field.
|
|
other, err := index.CreateFieldIfNotExists("f", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
} else if f.Field != other.Field {
|
|
t.Fatal("field mismatch")
|
|
}
|
|
|
|
if f.Field != index.Field("f") {
|
|
t.Fatal("field mismatch")
|
|
}
|
|
}
|
|
|
|
func TestIndex_CreateField(t *testing.T) {
|
|
// Ensure time quantum can be set appropriately on a new field.
|
|
t.Run("TimeQuantum", func(t *testing.T) {
|
|
t.Run("Explicit", func(t *testing.T) {
|
|
_, index := test.MustOpenIndex(t)
|
|
|
|
// Create field with explicit quantum.
|
|
f, err := index.CreateField("f", "", pilosa.OptFieldTypeTime(pilosa.TimeQuantum("YMDH"), "0"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
} else if q := f.TimeQuantum(); q != pilosa.TimeQuantum("YMDH") {
|
|
t.Fatalf("unexpected field time quantum: %s", q)
|
|
}
|
|
})
|
|
})
|
|
|
|
// Ensure time quantum can be set appropriately on a new field.
|
|
t.Run("TimeQuantumNoStandardView", func(t *testing.T) {
|
|
t.Run("Explicit", func(t *testing.T) {
|
|
_, index := test.MustOpenIndex(t)
|
|
|
|
// Create field with explicit quantum with no standard view
|
|
f, err := index.CreateField("f", "", pilosa.OptFieldTypeTime(pilosa.TimeQuantum("YMDH"), "0", true))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
} else if q := f.TimeQuantum(); q != pilosa.TimeQuantum("YMDH") {
|
|
t.Fatalf("unexpected field time quantum: %s", q)
|
|
}
|
|
})
|
|
})
|
|
|
|
// Ensure field can include range columns.
|
|
t.Run("BSIFields", func(t *testing.T) {
|
|
t.Run("Int", func(t *testing.T) {
|
|
_, index := test.MustOpenIndex(t)
|
|
|
|
// Create field with schema and verify it exists.
|
|
if f, err := index.CreateField("f", "", pilosa.OptFieldTypeInt(-990, 1000)); err != nil {
|
|
t.Fatal(err)
|
|
} else if !reflect.DeepEqual(f.Type(), pilosa.FieldTypeInt) {
|
|
t.Fatalf("unexpected type: %#v", f.Type())
|
|
}
|
|
|
|
// Reopen the index & verify the fields are loaded.
|
|
if err := index.Reopen(); err != nil {
|
|
t.Fatal(err)
|
|
} else if f := index.Field("f"); !reflect.DeepEqual(f.Type(), pilosa.FieldTypeInt) {
|
|
t.Fatalf("unexpected type after reopen: %#v", f.Type())
|
|
}
|
|
})
|
|
|
|
t.Run("Timestamp", func(t *testing.T) {
|
|
_, index := test.MustOpenIndex(t)
|
|
|
|
// Create field with schema and verify it exists.
|
|
if f, err := index.CreateField("f", "", pilosa.OptFieldTypeTimestamp(pilosa.DefaultEpoch, pilosa.TimeUnitSeconds)); err != nil {
|
|
t.Fatal(err)
|
|
} else if !reflect.DeepEqual(f.Type(), pilosa.FieldTypeTimestamp) {
|
|
t.Fatalf("unexpected type: %#v", f.Type())
|
|
}
|
|
|
|
// Reopen the index & verify the fields are loaded.
|
|
if err := index.Reopen(); err != nil {
|
|
t.Fatal(err)
|
|
} else if f := index.Field("f"); !reflect.DeepEqual(f.Type(), pilosa.FieldTypeTimestamp) {
|
|
t.Fatalf("unexpected type after reopen: %#v", f.Type())
|
|
}
|
|
})
|
|
|
|
// TODO: These errors don't apply here. Instead, we need these tests
|
|
// on field creation FieldOptions validation.
|
|
/*
|
|
t.Run("ErrRangeCacheAllowed", func(t *testing.T) {
|
|
_, index := test.MustOpenIndex(t)
|
|
|
|
if _, err := index.CreateField("f", pilosa.FieldOptions{
|
|
CacheType: pilosa.CacheTypeRanked,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("BSIFieldsWithCacheTypeNone", func(t *testing.T) {
|
|
_, index := test.MustOpenIndex(t)
|
|
if _, err := index.CreateField("f", pilosa.FieldOptions{
|
|
CacheType: pilosa.CacheTypeNone,
|
|
CacheSize: uint32(5),
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("ErrFieldFieldsAllowed", func(t *testing.T) {
|
|
_, index := test.MustOpenIndex(t)
|
|
|
|
if _, err := index.CreateField("f", pilosa.FieldOptions{
|
|
Fields: []*pilosa.Field{
|
|
{Name: "field0", Type: pilosa.FieldTypeInt},
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("ErrFieldNameRequired", func(t *testing.T) {
|
|
_, index := test.MustOpenIndex(t)
|
|
|
|
if _, err := index.CreateField("f", pilosa.FieldOptions{
|
|
Fields: []*pilosa.Field{
|
|
{Name: "", Type: pilosa.FieldTypeInt},
|
|
},
|
|
}); err != pilosa.ErrFieldNameRequired {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("ErrInvalidFieldType", func(t *testing.T) {
|
|
_, index := test.MustOpenIndex(t)
|
|
|
|
if _, err := index.CreateField("f", pilosa.FieldOptions{
|
|
Fields: []*pilosa.Field{
|
|
{Name: "field0", Type: "bad_type"},
|
|
},
|
|
}); err != pilosa.ErrInvalidFieldType {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("ErrInvalidBSIGroupRange", func(t *testing.T) {
|
|
_, index := test.MustOpenIndex(t)
|
|
|
|
if _, err := index.CreateField("f", pilosa.FieldOptions{
|
|
Fields: []*pilosa.Field{
|
|
{Name: "field0", Type: pilosa.FieldTypeInt, Min: 100, Max: 50},
|
|
},
|
|
}); err != pilosa.ErrInvalidBSIGroupRange {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
*/
|
|
})
|
|
|
|
t.Run("WithKeys", func(t *testing.T) {
|
|
// Don't allow an int field to be created with keys=true
|
|
t.Run("IntField", func(t *testing.T) {
|
|
_, index := test.MustOpenIndex(t)
|
|
|
|
_, err := index.CreateField("f", "", pilosa.OptFieldTypeInt(-1, 1), pilosa.OptFieldKeys())
|
|
if errors.Cause(err) != pilosa.ErrIntFieldWithKeys {
|
|
t.Fatal("int field cannot be created with keys=true")
|
|
}
|
|
})
|
|
|
|
// Don't allow a decimal field to be created with keys=true
|
|
t.Run("DecimalField", func(t *testing.T) {
|
|
_, index := test.MustOpenIndex(t)
|
|
|
|
_, err := index.CreateField("f", "", pilosa.OptFieldTypeDecimal(1, pql.NewDecimal(-1, 0), pql.NewDecimal(1, 0)), pilosa.OptFieldKeys())
|
|
if errors.Cause(err) != pilosa.ErrDecimalFieldWithKeys {
|
|
t.Fatal("decimal field cannot be created with keys=true")
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
// Ensure index can delete a field.
|
|
func TestIndex_DeleteField(t *testing.T) {
|
|
_, index := test.MustOpenIndex(t)
|
|
|
|
// Create field.
|
|
if _, err := index.CreateFieldIfNotExists("f", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Delete field & verify it's gone.
|
|
if err := index.DeleteField("f"); err != nil {
|
|
t.Fatal(err)
|
|
} else if index.Field("f") != nil {
|
|
t.Fatal("expected nil field")
|
|
}
|
|
|
|
// Delete again to make sure it errors.
|
|
err := index.DeleteField("f")
|
|
if !isNotFoundError(err) {
|
|
t.Fatalf("expected 'field not found' error, got: %#v", err)
|
|
}
|
|
}
|
|
|
|
// Ensure index can validate its name.
|
|
func TestIndex_InvalidName(t *testing.T) {
|
|
holder := test.NewHolder(t).Holder
|
|
index, err := pilosa.NewIndex(holder, holder.IndexPath("ABC"), "ABC")
|
|
if err == nil {
|
|
t.Fatalf("should have gotten an error on index name with caps")
|
|
}
|
|
if index != nil {
|
|
t.Fatalf("unexpected index name %v", index)
|
|
}
|
|
}
|
|
|
|
func isNotFoundError(err error) bool {
|
|
root := errors.Cause(err)
|
|
_, ok := root.(pilosa.NotFoundError)
|
|
return ok
|
|
}
|
|
|
|
// Ensure that after node/cluster restart, deleting and recreating a field
|
|
// does not cause a deadlock
|
|
// This is a regression test after a customer experienced the same deadlock.
|
|
// For details, check out https://molecula.atlassian.net/browse/CORE-919
|
|
func TestIndex_RecreateFieldOnRestart(t *testing.T) {
|
|
c := test.MustRunUnsharedCluster(t, 1)
|
|
defer func() {
|
|
// We anticipate a deadlock, and if we hit the deadlock,
|
|
// closing would ALSO deadlock, so we won't want to do that.
|
|
//
|
|
// The alternative, of trying to call os.Exit, prevents us
|
|
// from reporting anything at all, because testing doesn't
|
|
// actually display messages until it's done.
|
|
//
|
|
// So, if we're bailing because of a fatal error, we don't
|
|
// try to close the cluster, because Something Went Wrong and
|
|
// it may well have been a deadlock. We leak one cluster on
|
|
// a failed test, but we correctly report the test as failed
|
|
// before also reporting the unclosed resources.
|
|
if !t.Failed() {
|
|
c.Close()
|
|
}
|
|
}()
|
|
|
|
// create index
|
|
indexName := fmt.Sprintf("idx_%d", rand.Uint64())
|
|
holder := c.GetHolder(0)
|
|
_, err := holder.CreateIndex(indexName, "", pilosa.IndexOptions{
|
|
Keys: false,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// create field
|
|
fieldName := fmt.Sprintf("field_%d", rand.Uint64())
|
|
_, err = c.GetNode(0).API.CreateField(context.Background(), indexName, fieldName)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// set value
|
|
_, err = c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{
|
|
Index: indexName,
|
|
Query: fmt.Sprintf(`Set(1, %s=1)`, fieldName),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// restart node
|
|
node := c.GetNode(0)
|
|
if err := node.Reopen(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := c.AwaitState(disco.ClusterStateNormal, 10*time.Second); err != nil {
|
|
t.Fatalf("restarting cluster: %v", err)
|
|
}
|
|
|
|
// delete field
|
|
err = c.GetNode(0).API.DeleteField(context.Background(), indexName, fieldName)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// recreate field
|
|
errCh := make(chan error)
|
|
go func() {
|
|
_, err := c.GetNode(0).API.CreateField(context.Background(), indexName,
|
|
fieldName)
|
|
errCh <- err
|
|
}()
|
|
select {
|
|
case <-time.After(10 * time.Second):
|
|
t.Fatal("recreating field took too long")
|
|
case err := <-errCh:
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
}
|