// Copyright 2016 Attic Labs, Inc. All rights reserved. // Licensed under the Apache License, version 2.0: // http://www.apache.org/licenses/LICENSE-2.0 package datas import ( "testing" "github.com/attic-labs/noms/go/chunks" "github.com/attic-labs/noms/go/hash" "github.com/attic-labs/noms/go/types" "github.com/attic-labs/testify/suite" ) // writesOnCommit allows tests to adjust for how many writes databaseCommon performs on Commit() const writesOnCommit = 2 func TestLocalDatabase(t *testing.T) { suite.Run(t, &LocalDatabaseSuite{}) } func TestRemoteDatabase(t *testing.T) { suite.Run(t, &RemoteDatabaseSuite{}) } type DatabaseSuite struct { suite.Suite cs *chunks.TestStore ds Database makeDs func(chunks.ChunkStore) Database } type LocalDatabaseSuite struct { DatabaseSuite } func (suite *LocalDatabaseSuite) SetupTest() { suite.cs = chunks.NewTestStore() suite.makeDs = NewDatabase suite.ds = suite.makeDs(suite.cs) } type RemoteDatabaseSuite struct { DatabaseSuite } func (suite *RemoteDatabaseSuite) SetupTest() { suite.cs = chunks.NewTestStore() suite.makeDs = func(cs chunks.ChunkStore) Database { hbs := newHTTPBatchStoreForTest(cs) return &RemoteDatabaseClient{newDatabaseCommon(newCachingChunkHaver(hbs), types.NewValueStore(hbs), hbs)} } suite.ds = suite.makeDs(suite.cs) } func (suite *DatabaseSuite) TearDownTest() { suite.ds.Close() suite.cs.Close() } func (suite *DatabaseSuite) TestReadWriteCache() { var v types.Value = types.Bool(true) suite.NotEqual(hash.Hash{}, suite.ds.WriteValue(v)) r := suite.ds.WriteValue(v).TargetHash() commit := NewCommit().Set(ValueField, v) newDs, err := suite.ds.Commit("foo", commit) suite.NoError(err) suite.Equal(1, suite.cs.Writes-writesOnCommit) v = newDs.ReadValue(r) suite.True(v.Equals(types.Bool(true))) } func (suite *DatabaseSuite) TestReadWriteCachePersists() { var err error var v types.Value = types.Bool(true) suite.NotEqual(hash.Hash{}, suite.ds.WriteValue(v)) r := suite.ds.WriteValue(v) commit := NewCommit().Set(ValueField, v) suite.ds, err = suite.ds.Commit("foo", commit) suite.NoError(err) suite.Equal(1, suite.cs.Writes-writesOnCommit) newCommit := NewCommit().Set(ValueField, r).Set(ParentsField, types.NewSet(types.NewRef(commit))) suite.ds, err = suite.ds.Commit("foo", newCommit) suite.NoError(err) } func (suite *DatabaseSuite) TestWriteRefToNonexistentValue() { suite.Panics(func() { suite.ds.WriteValue(types.NewRef(types.Bool(true))) }) } func (suite *DatabaseSuite) TestTolerateUngettableRefs() { suite.Nil(suite.ds.ReadValue(hash.Hash{})) } func (suite *DatabaseSuite) TestDatabaseCommit() { datasetID := "ds1" datasets := suite.ds.Datasets() suite.Zero(datasets.Len()) // |a| a := types.String("a") aCommit := NewCommit().Set(ValueField, a) ds2, err := suite.ds.Commit(datasetID, aCommit) suite.NoError(err) // The old database still has no head. _, ok := suite.ds.MaybeHead(datasetID) suite.False(ok) _, ok = suite.ds.MaybeHeadRef(datasetID) suite.False(ok) // The new database has |a|. aCommit1 := ds2.Head(datasetID) suite.True(aCommit1.Get(ValueField).Equals(a)) aRef1 := ds2.HeadRef(datasetID) suite.Equal(aCommit1.Hash(), aRef1.TargetHash()) suite.Equal(uint64(1), aRef1.Height()) suite.ds = ds2 // |a| <- |b| b := types.String("b") bCommit := NewCommit().Set(ValueField, b).Set(ParentsField, types.NewSet(types.NewRef(aCommit))) suite.ds, err = suite.ds.Commit(datasetID, bCommit) suite.NoError(err) suite.True(suite.ds.Head(datasetID).Get(ValueField).Equals(b)) suite.Equal(uint64(2), suite.ds.HeadRef(datasetID).Height()) // |a| <- |b| // \----|c| // Should be disallowed. c := types.String("c") cCommit := NewCommit().Set(ValueField, c).Set(ParentsField, types.NewSet(types.NewRef(aCommit))) suite.ds, err = suite.ds.Commit(datasetID, cCommit) suite.Error(err) suite.True(suite.ds.Head(datasetID).Get(ValueField).Equals(b)) // |a| <- |b| <- |d| d := types.String("d") dCommit := NewCommit().Set(ValueField, d).Set(ParentsField, types.NewSet(types.NewRef(bCommit))) suite.ds, err = suite.ds.Commit(datasetID, dCommit) suite.NoError(err) suite.True(suite.ds.Head(datasetID).Get(ValueField).Equals(d)) suite.Equal(uint64(3), suite.ds.HeadRef(datasetID).Height()) // Attempt to recommit |b| with |a| as parent. // Should be disallowed. suite.ds, err = suite.ds.Commit(datasetID, bCommit) suite.Error(err) suite.True(suite.ds.Head(datasetID).Get(ValueField).Equals(d)) // Add a commit to a different datasetId _, err = suite.ds.Commit("otherDs", aCommit) suite.NoError(err) // Get a fresh database, and verify that both datasets are present newDs := suite.makeDs(suite.cs) datasets2 := newDs.Datasets() suite.Equal(uint64(2), datasets2.Len()) newDs.Close() } func (suite *DatabaseSuite) TestDatabaseDelete() { datasetID1, datasetID2 := "ds1", "ds2" datasets := suite.ds.Datasets() suite.Zero(datasets.Len()) // |a| var err error a := types.String("a") suite.ds, err = suite.ds.Commit(datasetID1, NewCommit().Set(ValueField, a)) suite.NoError(err) suite.True(suite.ds.Head(datasetID1).Get(ValueField).Equals(a)) // ds1; |a|, ds2: |b| b := types.String("b") suite.ds, err = suite.ds.Commit(datasetID2, NewCommit().Set(ValueField, b)) suite.NoError(err) suite.True(suite.ds.Head(datasetID2).Get(ValueField).Equals(b)) suite.ds, err = suite.ds.Delete(datasetID1) suite.NoError(err) suite.True(suite.ds.Head(datasetID2).Get(ValueField).Equals(b)) h, present := suite.ds.MaybeHead(datasetID1) suite.False(present, "Dataset %s should not be present, but head is %v", datasetID1, h.Get(ValueField)) // Get a fresh database, and verify that only ds1 is present newDs := suite.makeDs(suite.cs) datasets = newDs.Datasets() suite.Equal(uint64(1), datasets.Len()) _, present = suite.ds.MaybeHead(datasetID2) suite.True(present, "Dataset %s should be present", datasetID2) newDs.Close() } func (suite *DatabaseSuite) TestDatabaseDeleteConcurrent() { datasetID := "ds1" suite.Zero(suite.ds.Datasets().Len()) var err error // |a| a := types.String("a") aCommit := NewCommit().Set(ValueField, a) suite.ds, err = suite.ds.Commit(datasetID, aCommit) suite.NoError(err) // |a| <- |b| b := types.String("b") bCommit := NewCommit().Set(ValueField, b).Set(ParentsField, types.NewSet(types.NewRef(aCommit))) ds2, err := suite.ds.Commit(datasetID, bCommit) suite.NoError(err) suite.True(suite.ds.Head(datasetID).Get(ValueField).Equals(a)) suite.True(ds2.Head(datasetID).Get(ValueField).Equals(b)) suite.ds, err = suite.ds.Delete(datasetID) suite.NoError(err) h, present := suite.ds.MaybeHead(datasetID) suite.False(present, "Dataset %s should not be present, but head is %v", datasetID, h.Get(ValueField)) h, present = ds2.MaybeHead(datasetID) suite.True(present, "Dataset %s should be present", datasetID) // Get a fresh database, and verify that no databases are present newDs := suite.makeDs(suite.cs) suite.Equal(uint64(0), newDs.Datasets().Len()) newDs.Close() } func (suite *DatabaseSuite) TestDatabaseConcurrency() { datasetID := "ds1" var err error // Setup: // |a| <- |b| a := types.String("a") aCommit := NewCommit().Set(ValueField, a) suite.ds, err = suite.ds.Commit(datasetID, aCommit) b := types.String("b") bCommit := NewCommit().Set(ValueField, b).Set(ParentsField, types.NewSet(types.NewRef(aCommit))) suite.ds, err = suite.ds.Commit(datasetID, bCommit) suite.NoError(err) suite.True(suite.ds.Head(datasetID).Get(ValueField).Equals(b)) // Important to create this here. ds2 := suite.makeDs(suite.cs) // Change 1: // |a| <- |b| <- |c| c := types.String("c") cCommit := NewCommit().Set(ValueField, c).Set(ParentsField, types.NewSet(types.NewRef(bCommit))) suite.ds, err = suite.ds.Commit(datasetID, cCommit) suite.NoError(err) suite.True(suite.ds.Head(datasetID).Get(ValueField).Equals(c)) // Change 2: // |a| <- |b| <- |e| // Should be disallowed, Database returned by Commit() should have |c| as Head. e := types.String("e") eCommit := NewCommit().Set(ValueField, e).Set(ParentsField, types.NewSet(types.NewRef(bCommit))) ds2, err = ds2.Commit(datasetID, eCommit) suite.Error(err) suite.True(ds2.Head(datasetID).Get(ValueField).Equals(c)) } func (suite *DatabaseSuite) TestDatabaseHeightOfRefs() { r1 := suite.ds.WriteValue(types.String("hello")) suite.Equal(uint64(1), r1.Height()) r2 := suite.ds.WriteValue(r1) suite.Equal(uint64(2), r2.Height()) suite.Equal(uint64(3), suite.ds.WriteValue(r2).Height()) } func (suite *DatabaseSuite) TestDatabaseHeightOfCollections() { setOfStringType := types.MakeSetType(types.StringType) setOfRefOfStringType := types.MakeSetType(types.MakeRefType(types.StringType)) // Set v1 := types.String("hello") v2 := types.String("world") s1 := types.NewSet(v1, v2) suite.Equal(uint64(1), suite.ds.WriteValue(s1).Height()) // Set> s2 := types.NewSet(suite.ds.WriteValue(v1), suite.ds.WriteValue(v2)) suite.Equal(uint64(2), suite.ds.WriteValue(s2).Height()) // List> v3 := types.String("foo") v4 := types.String("bar") s3 := types.NewSet(v3, v4) l1 := types.NewList(s1, s3) suite.Equal(uint64(1), suite.ds.WriteValue(l1).Height()) // List> l2 := types.NewList(types.MakeListType(types.MakeRefType(setOfStringType)), suite.ds.WriteValue(s1), suite.ds.WriteValue(s3)) suite.Equal(uint64(2), suite.ds.WriteValue(l2).Height()) // List>> s4 := types.NewSet(suite.ds.WriteValue(v3), suite.ds.WriteValue(v4)) l3 := types.NewList(types.MakeListType(types.MakeRefType(setOfRefOfStringType)), suite.ds.WriteValue(s4)) suite.Equal(uint64(3), suite.ds.WriteValue(l3).Height()) // List | RefValue>>. l4 := types.NewList(s1, suite.ds.WriteValue(s3)) suite.Equal(uint64(2), suite.ds.WriteValue(l4).Height()) l5 := types.NewList(suite.ds.WriteValue(s1), s3) suite.Equal(uint64(2), suite.ds.WriteValue(l5).Height()) }