blob: 58c34261187e7d1dbac958b9d3300471859ffe93 [file] [log] [blame]
package fs_expstore
import (
"context"
"fmt"
"sync"
"testing"
"github.com/google/uuid"
assert "github.com/stretchr/testify/require"
"go.skia.org/infra/go/firestore"
"go.skia.org/infra/go/testutils/unittest"
data "go.skia.org/infra/golden/go/testutils/data_three_devices"
"go.skia.org/infra/golden/go/types"
)
// TODO(kjlubick): These tests are marked as manual because the
// Firestore Emulator is not yet on the bots, due to some more complicated
// setup (e.g. chmod)
// TestGetExpectations writes some changes and then reads back the
// aggregated results.
func TestGetExpectations(t *testing.T) {
unittest.ManualTest(t)
unittest.RequiresFirestoreEmulator(t)
c := getTestFirestoreInstance(t)
f := New(c, MasterBranch, ReadWrite)
// Brand new instance should have no expectations
e, err := f.Get()
assert.NoError(t, err)
assert.Equal(t, types.Expectations{}, e)
ctx := context.Background()
err = f.AddChange(ctx, types.Expectations{
data.AlphaTest: {
data.AlphaUntriaged1Digest: types.POSITIVE,
data.AlphaGood1Digest: types.POSITIVE,
},
}, userOne)
assert.NoError(t, err)
err = f.AddChange(ctx, types.Expectations{
data.AlphaTest: {
data.AlphaBad1Digest: types.NEGATIVE,
data.AlphaUntriaged1Digest: types.UNTRIAGED, // overwrites previous
},
data.BetaTest: {
data.BetaGood1Digest: types.POSITIVE,
},
}, userTwo)
assert.NoError(t, err)
e, err = f.Get()
assert.NoError(t, err)
assert.Equal(t, types.Expectations{
data.AlphaTest: {
data.AlphaGood1Digest: types.POSITIVE,
data.AlphaBad1Digest: types.NEGATIVE,
data.AlphaUntriaged1Digest: types.UNTRIAGED,
},
data.BetaTest: {
data.BetaGood1Digest: types.POSITIVE,
},
}, e)
// Make sure that if we create a new view, we can read the results
// from the table to make the expectations
fr := New(c, MasterBranch, ReadOnly)
e, err = fr.Get()
assert.NoError(t, err)
assert.Equal(t, types.Expectations{
data.AlphaTest: {
data.AlphaGood1Digest: types.POSITIVE,
data.AlphaBad1Digest: types.NEGATIVE,
data.AlphaUntriaged1Digest: types.UNTRIAGED,
},
data.BetaTest: {
data.BetaGood1Digest: types.POSITIVE,
},
}, e)
}
// TestGetExpectationsRace writes a bunch of data from many go routines
// in an effort to catch any race conditions in the caching layer.
func TestGetExpectationsRace(t *testing.T) {
unittest.ManualTest(t)
unittest.RequiresFirestoreEmulator(t)
c := getTestFirestoreInstance(t)
f := New(c, MasterBranch, ReadWrite)
type entry struct {
Grouping types.TestName
Digest types.Digest
Label types.Label
}
entries := []entry{
{
Grouping: data.AlphaTest,
Digest: data.AlphaUntriaged1Digest,
Label: types.UNTRIAGED,
},
{
Grouping: data.AlphaTest,
Digest: data.AlphaBad1Digest,
Label: types.NEGATIVE,
},
{
Grouping: data.AlphaTest,
Digest: data.AlphaGood1Digest,
Label: types.POSITIVE,
},
{
Grouping: data.BetaTest,
Digest: data.BetaGood1Digest,
Label: types.POSITIVE,
},
{
Grouping: data.BetaTest,
Digest: data.BetaUntriaged1Digest,
Label: types.UNTRIAGED,
},
}
ctx := context.Background()
wg := sync.WaitGroup{}
for i := 0; i < 50; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
e := entries[i%len(entries)]
err := f.AddChange(ctx, types.Expectations{
e.Grouping: {
e.Digest: e.Label,
},
}, userOne)
assert.NoError(t, err)
}(i)
// Make sure we can read and write w/o races
if i%5 == 0 {
_, err := f.Get()
assert.NoError(t, err)
}
}
wg.Wait()
e, err := f.Get()
assert.NoError(t, err)
assert.Equal(t, types.Expectations{
data.AlphaTest: {
data.AlphaGood1Digest: types.POSITIVE,
data.AlphaBad1Digest: types.NEGATIVE,
data.AlphaUntriaged1Digest: types.UNTRIAGED,
},
data.BetaTest: {
data.BetaGood1Digest: types.POSITIVE,
data.BetaUntriaged1Digest: types.UNTRIAGED,
},
}, e)
}
// TestGetExpectationsBig writes 32^2=1024 entries
// to test the batch writing.
func TestGetExpectationsBig(t *testing.T) {
unittest.ManualTest(t)
unittest.RequiresFirestoreEmulator(t)
c := getTestFirestoreInstance(t)
f := New(c, MasterBranch, ReadWrite)
type entry struct {
Grouping types.TestName
Digest types.Digest
Label types.Label
}
// Write the expectations in two, non-overlapping blocks.
exp1 := makeBigExpectations(0, 16)
exp2 := makeBigExpectations(16, 32)
expected := exp1.DeepCopy()
expected.MergeExpectations(exp2)
ctx := context.Background()
wg := sync.WaitGroup{}
// Write them concurrently to test for races.
wg.Add(2)
go func() {
defer wg.Done()
err := f.AddChange(ctx, exp1, userOne)
assert.NoError(t, err)
}()
go func() {
defer wg.Done()
err := f.AddChange(ctx, exp2, userTwo)
assert.NoError(t, err)
}()
wg.Wait()
e, err := f.Get()
assert.NoError(t, err)
assert.Equal(t, expected, e)
// Make sure that if we create a new view, we can read the results
// from the table to make the expectations
fr := New(c, MasterBranch, ReadOnly)
e, err = fr.Get()
assert.NoError(t, err)
assert.Equal(t, expected, e)
}
// TestReadOnly ensures a read-only instance fails to write data.
func TestReadOnly(t *testing.T) {
unittest.SmallTest(t)
f := New(nil, MasterBranch, ReadOnly)
err := f.AddChange(context.Background(), types.Expectations{
data.AlphaTest: {
data.AlphaGood1Digest: types.POSITIVE,
},
}, userOne)
assert.Error(t, err)
assert.Contains(t, err.Error(), "read-only")
}
// Creates an empty firestore instance. The emulator keeps the tables in ram, but
// by appending a random nonce, we can be assured the collection we get is empty.
func getTestFirestoreInstance(t *testing.T) *firestore.Client {
randInstance := uuid.New().String()
c, err := firestore.NewClient(context.Background(), "should-use-emulator", "gold-test", ExpectationStoreCollection+randInstance, nil)
assert.NoError(t, err)
return c
}
// makeBigExpectations makes n tests named from start to end that each have 32 digests.
func makeBigExpectations(start, end int) types.Expectations {
e := types.Expectations{}
for i := start; i < end; i++ {
for j := 0; j < 32; j++ {
e.AddDigest(types.TestName(fmt.Sprintf("test-%03d", i)),
types.Digest(fmt.Sprintf("digest-%03d", j)), types.POSITIVE)
}
}
return e
}
const (
userOne = "userOne@example.com"
userTwo = "userTwo@example.com"
)