blob: 2c2ee5d7efbf471d19bc42bb581574c44f30e2da [file] [log] [blame]
// Copyright 2024 The Go 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 firestore
import (
"context"
"flag"
"fmt"
"math/rand/v2"
"path/filepath"
"strings"
"testing"
"time"
"golang.org/x/oscar/internal/grpcrr"
"golang.org/x/oscar/internal/storage"
)
var (
project = flag.String("project", "", "project ID for testing")
database = flag.String("database", "", "Firestore database for testing")
)
func TestDB(t *testing.T) {
rr, fsProject, fsDatabase := openRR(t, "testdata/db.grpcrr")
defer func() {
if err := rr.Close(); err != nil {
t.Fatal(err)
}
}()
ctx := context.Background()
db, err := NewDB(ctx, &DBOptions{ProjectID: fsProject, Database: fsDatabase, ClientOptions: rr.ClientOptions()})
if err != nil {
t.Fatal(err)
}
// Use a fixed UID for deterministic record/replay.
db.uid = 1
defer db.Close()
// Make the timeout short, in case the locks exist from a previous run.
defer func(lt time.Duration) {
lockTimeout = lt
}(lockTimeout)
lockTimeout = 2 * time.Second
storage.TestDB(t, db)
// Test Batch.DeleteRange with MaybeApply.
b := db.Batch()
b.DeleteRange([]byte("a"), []byte("b"))
applied := b.MaybeApply()
if !applied {
t.Error("MaybeApply with DeleteRange did not apply")
}
}
func TestLock(t *testing.T) {
// The lock tests cannot be run with record/replay because they depend too much on time and state.
if *project == "" {
t.Skip("missing -project")
}
ctx := context.Background()
db, err := NewDB(ctx, &DBOptions{ProjectID: *project, Database: *database})
if err != nil {
t.Fatal(err)
}
defer db.Close()
defer func(lt time.Duration) {
lockTimeout = lt
}(lockTimeout)
lockTimeout = 2 * time.Second
t.Run("basic", func(t *testing.T) {
storage.TestDBLock(t, db)
})
t.Run("timeout", func(t *testing.T) {
c := make(chan struct{})
go func() {
db.Lock("L")
// The second Lock should wait until the first Lock times
// out, then it should succeed.
db.Lock("L")
db.Unlock("L")
close(c)
}()
select {
case <-c:
// Success.
case <-time.After(2 * lockTimeout):
t.Fatal("lock timeout didn't happen in time")
}
})
t.Run("owner", func(t *testing.T) {
// When run frequently, this test causes contention on the lock name.
// So pick a random name.
name := fmt.Sprintf("M%d", rand.IntN(10))
db.deleteLock(nil, name) // Ensure the lock is not present.
db2, err := NewDB(ctx, &DBOptions{ProjectID: *project, Database: *database})
if err != nil {
t.Fatal(err)
}
defer db2.Close()
func() {
defer func() { recover() }()
db.Lock(name)
db2.Unlock(name)
t.Error("unlock wrong owner did not panic")
}()
})
}
func TestErrors(t *testing.T) {
if !panics(func() {
var b batch
b.set("x", nil, 1)
}) {
t.Error("batch.set does not panic on nil value")
}
}
func openRR(t *testing.T, file string) (_ *grpcrr.RecordReplay, fsProject, fsDatabase string) {
rr, err := grpcrr.Open(filepath.FromSlash(file))
if err != nil {
t.Fatalf("grpcrr.Open: %v", err)
}
if rr.Recording() {
if *project == "" {
t.Fatal("recording requires -project")
}
// The -database flag can be omitted. We'll use the default one.
rr.SetInitial([]byte(*project + "," + *database))
return rr, *project, *database
}
// Allow -project and -database on replay because other tests might need them.
var found bool
fsProject, fsDatabase, found = strings.Cut(string(rr.Initial()), ",")
if !found {
t.Fatalf("%s: bad initial state", file)
}
return rr, fsProject, fsDatabase
}
func panics(f func()) (b bool) {
defer func() {
if recover() != nil {
b = true
}
}()
f()
return false
}