Files
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

503 lines
15 KiB
Go

// Copyright 2019 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// This file incorporates work covered by the following copyright and
// permission notice:
//
// 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 (
"bytes"
"context"
"fmt"
"sort"
"testing"
"time"
flatbuffers "github.com/dolthub/flatbuffers/v23/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/dolthub/dolt/go/gen/fb/serial"
"github.com/dolthub/dolt/go/store/chunks"
"github.com/dolthub/dolt/go/store/d"
"github.com/dolthub/dolt/go/store/hash"
"github.com/dolthub/dolt/go/store/prolly/tree"
"github.com/dolthub/dolt/go/store/types"
)
func mustHead(ds Dataset) types.Value {
s, ok := ds.MaybeHead()
if !ok {
panic("no head")
}
return s
}
func mustHeadValue(ds Dataset) types.Value {
val, ok, err := ds.MaybeHeadValue()
if err != nil {
panic("error getting head " + err.Error())
}
if !ok {
panic("no head")
}
return val
}
func mustString(str string, err error) string {
d.PanicIfError(err)
return str
}
func mustParentsClosure(t *testing.T, exists bool) func(types.Ref, bool, error) types.Ref {
return func(r types.Ref, got bool, err error) types.Ref {
t.Helper()
require.NoError(t, err)
require.Equal(t, exists, got)
return r
}
}
func mustType(t *types.Type, err error) *types.Type {
d.PanicIfError(err)
return t
}
func mustRef(ref types.Ref, err error) types.Ref {
d.PanicIfError(err)
return ref
}
func mustValue(val types.Value, err error) types.Value {
d.PanicIfError(err)
return val
}
func TestIsCommit(t *testing.T) {
assert := assert.New(t)
meta, err := NewCommitMetaWithAuthor("test", "test@test.com", "test commit", time.UnixMilli(0))
if err != nil {
t.Fatal(err)
}
meta.Committer.Date = meta.Committer.Date.Or(time.UnixMilli(0))
commitMsg, _ := commit_flatbuffer(hash.Hash{}, CommitOptions{
Meta: meta,
}, nil, hash.Hash{})
metaCommit := types.SerialMessage(commitMsg)
ok, err := IsCommit(metaCommit)
assert.NoError(err)
assert.True(ok)
rvBuilder := flatbuffers.NewBuilder(64)
serial.RootValueStart(rvBuilder)
notACommit := types.SerialMessage(serial.FinishMessage(
rvBuilder,
serial.RootValueEnd(rvBuilder),
[]byte(serial.RootValueFileID),
))
ok, err = IsCommit(notACommit)
assert.NoError(err)
assert.False(ok)
}
func mustCommitToTargetHashes(vrw types.ValueReadWriter, commits ...types.Value) []hash.Hash {
ret := make([]hash.Hash, len(commits))
for i, c := range commits {
r, err := types.NewRef(c, vrw.Format())
if err != nil {
panic(err)
}
ret[i] = r.TargetHash()
}
return ret
}
func commonAncWithSetClosure(ctx context.Context, c1, c2 *Commit, vr1, vr2 types.ValueReader, ns1, ns2 tree.NodeStore) (a hash.Hash, ok bool, err error) {
closure, err := NewSetCommitClosure(ctx, vr1, c1)
if err != nil {
return hash.Hash{}, false, err
}
return FindClosureCommonAncestor(ctx, closure, c2, vr2)
}
func commonAncWithLazyClosure(ctx context.Context, c1, c2 *Commit, vr1, vr2 types.ValueReader, ns1, ns2 tree.NodeStore) (a hash.Hash, ok bool, err error) {
closure := NewLazyCommitClosure(c1, vr1)
return FindClosureCommonAncestor(ctx, closure, c2, vr2)
}
// Assert that c is the common ancestor of a and b, using multiple common ancestor methods.
func assertCommonAncestor(t *testing.T, expected, a, b types.Value, ldb, rdb *database, desc string) {
type caFinder func(ctx context.Context, c1, c2 *Commit, vr1, vr2 types.ValueReader, ns1, ns2 tree.NodeStore) (a hash.Hash, ok bool, err error)
methods := map[string]caFinder{
"FindCommonAncestor": FindCommonAncestor,
"SetClosure": commonAncWithSetClosure,
"LazyClosure": commonAncWithLazyClosure,
"FindCommonAncestorUsingParentsList": findCommonAncestorUsingParentsList,
}
aref := mustRef(types.NewRef(a, ldb.Format()))
bref := mustRef(types.NewRef(b, rdb.Format()))
ac, err := LoadCommitRef(context.Background(), ldb, aref)
require.NoError(t, err)
bc, err := LoadCommitRef(context.Background(), rdb, bref)
require.NoError(t, err)
for name, method := range methods {
t.Run(fmt.Sprintf("%s/%s", name, desc), func(t *testing.T) {
assert := assert.New(t)
ctx := context.Background()
found, ok, err := method(ctx, ac, bc, ldb, rdb, ldb.ns, rdb.ns)
assert.NoError(err)
if assert.True(ok) {
tv, err := ldb.ReadValue(context.Background(), found)
assert.NoError(err)
ancestor := tv
expV, _ := GetCommittedValue(ctx, ldb, expected)
aV, _ := GetCommittedValue(ctx, ldb, a)
bV, _ := GetCommittedValue(ctx, rdb, b)
ancV, _ := GetCommittedValue(ctx, ldb, ancestor)
assert.True(
expected.Equals(ancestor),
"%s should be common ancestor of %s, %s. Got %s",
expV,
aV,
bV,
ancV,
)
}
})
}
}
// Add a commit and return it.
func addCommit(t *testing.T, db *database, datasetID string, val string, parents ...types.Value) (types.Value, hash.Hash) {
ds, err := db.GetDataset(context.Background(), datasetID)
assert.NoError(t, err)
epoch := CommitDateAt(time.UnixMilli(0))
meta := &CommitMeta{Author: CommitIdent{Date: epoch}, Committer: CommitIdent{Date: epoch}}
ds, err = db.Commit(context.Background(), ds, types.String(val), CommitOptions{Parents: mustCommitToTargetHashes(db, parents...), Meta: meta})
require.NoError(t, err)
return mustHead(ds), mustHeadAddr(ds)
}
func TestCommitParentsClosure(t *testing.T) {
assert := assert.New(t)
storage := &chunks.TestStorage{}
db := NewDatabase(storage.NewViewWithDefaultFormat()).(*database)
ctx := context.Background()
type expected struct {
height int
hash hash.Hash
}
assertCommitParentsClosure := func(v types.Value, es []expected) {
sort.Slice(es, func(i, j int) bool {
if es[i].height == es[j].height {
return bytes.Compare(es[i].hash[:], es[j].hash[:]) > 0
}
return es[i].height > es[j].height
})
c, err := commitPtr(db.Format(), v, nil)
if !assert.NoError(err) {
return
}
iter, err := newParentsClosureIterator(ctx, c, db, db.ns)
if !assert.NoError(err) {
return
}
if len(es) == 0 {
assert.Nil(iter)
return
}
for _, e := range es {
if !assert.True(iter.Next(ctx)) {
return
}
if !assert.Equal(e.hash, iter.Hash()) {
return
}
if !assert.Equal(uint64(e.height), iter.Height()) {
return
}
}
assert.False(iter.Next(ctx))
assert.NoError(iter.Err())
}
a, b, c, d := "ds-a", "ds-b", "ds-c", "ds-d"
a1, a1a := addCommit(t, db, a, "a1")
a2, a2a := addCommit(t, db, a, "a2", a1)
a3, a3a := addCommit(t, db, a, "a3", a2)
b1, b1a := addCommit(t, db, b, "b1", a1)
b2, b2a := addCommit(t, db, b, "b2", b1)
b3, b3a := addCommit(t, db, b, "b3", b2)
c1, c1a := addCommit(t, db, c, "c1", a3, b3)
d1, _ := addCommit(t, db, d, "d1", c1, b3)
assertCommitParentsClosure(a1, []expected{})
assertCommitParentsClosure(a2, []expected{
{1, a1a},
})
assertCommitParentsClosure(a3, []expected{
{2, a2a},
{1, a1a},
})
assertCommitParentsClosure(b1, []expected{
{1, a1a},
})
assertCommitParentsClosure(b2, []expected{
{2, b1a},
{1, a1a},
})
assertCommitParentsClosure(b3, []expected{
{3, b2a},
{2, b1a},
{1, a1a},
})
assertCommitParentsClosure(c1, []expected{
{4, b3a},
{3, b2a},
{3, a3a},
{2, b1a},
{2, a2a},
{1, a1a},
})
assertCommitParentsClosure(d1, []expected{
{5, c1a},
{4, b3a},
{3, b2a},
{3, a3a},
{2, b1a},
{2, a2a},
{1, a1a},
})
}
func TestFindCommonAncestor(t *testing.T) {
assert := assert.New(t)
storage := &chunks.TestStorage{}
db := NewDatabase(storage.NewViewWithDefaultFormat()).(*database)
// Build commit DAG
//
// ds-a: a1<-a2<-a3<-a4<-a5<-a6
// ^ ^ ^ |
// | \ \----\ /-/
// | \ \V
// ds-b: \ b3<-b4<-b5
// \
// \
// ds-c: c2<-c3
// /
// /
// V
// ds-d: d1<-d2
//
a, b, c, d := "ds-a", "ds-b", "ds-c", "ds-d"
a1, _ := addCommit(t, db, a, "a1")
d1, _ := addCommit(t, db, d, "d1")
a2, _ := addCommit(t, db, a, "a2", a1)
c2, _ := addCommit(t, db, c, "c2", a1)
d2, _ := addCommit(t, db, d, "d2", d1)
a3, _ := addCommit(t, db, a, "a3", a2)
b3, _ := addCommit(t, db, b, "b3", a2)
c3, _ := addCommit(t, db, c, "c3", c2, d2)
a4, _ := addCommit(t, db, a, "a4", a3)
b4, _ := addCommit(t, db, b, "b4", b3)
a5, _ := addCommit(t, db, a, "a5", a4)
b5, _ := addCommit(t, db, b, "b5", b4, a3)
a6, _ := addCommit(t, db, a, "a6", a5, b5)
assertCommonAncestor(t, a1, a1, a1, db, db, "all self")
assertCommonAncestor(t, a1, a1, a2, db, db, "one side self")
assertCommonAncestor(t, a2, a3, b3, db, db, "common parent")
assertCommonAncestor(t, a2, a4, b4, db, db, "common grandeparent")
assertCommonAncestor(t, a1, a6, c3, db, db, "traversing multiple parents on both sides")
// No common ancestor
ctx := context.Background()
d2c, err := LoadCommitRef(ctx, db, mustRef(types.NewRef(d2, db.Format())))
require.NoError(t, err)
a6c, err := LoadCommitRef(ctx, db, mustRef(types.NewRef(a6, db.Format())))
require.NoError(t, err)
found, ok, err := FindCommonAncestor(ctx, d2c, a6c, db, db, db.ns, db.ns)
require.NoError(t, err)
if !assert.False(ok) {
d2V, _ := GetCommittedValue(ctx, db, d2)
a6V, _ := GetCommittedValue(ctx, db, a6)
fTV, _ := db.ReadValue(ctx, found)
fV, _ := GetCommittedValue(ctx, db, fTV)
assert.Fail(
"Unexpected common ancestor!",
"Should be no common ancestor of %s, %s. Got %s",
d2V,
a6V,
fV,
)
}
assert.NoError(db.Close())
t.Run("DifferentVRWs", func(t *testing.T) {
storage = &chunks.TestStorage{}
db = NewDatabase(storage.NewViewWithDefaultFormat()).(*database)
defer db.Close()
rstorage := &chunks.TestStorage{}
rdb := NewDatabase(rstorage.NewViewWithDefaultFormat()).(*database)
defer rdb.Close()
// Rerun the tests when using two difference Databases for left and
// right commits. Both databases have all the previous commits.
a, b, c, d = "ds-a", "ds-b", "ds-c", "ds-d"
a1, _ = addCommit(t, db, a, "a1")
d1, _ = addCommit(t, db, d, "d1")
a2, _ = addCommit(t, db, a, "a2", a1)
c2, _ = addCommit(t, db, c, "c2", a1)
d2, _ = addCommit(t, db, d, "d2", d1)
a3, _ = addCommit(t, db, a, "a3", a2)
b3, _ = addCommit(t, db, b, "b3", a2)
c3, _ = addCommit(t, db, c, "c3", c2, d2)
a4, _ = addCommit(t, db, a, "a4", a3)
b4, _ = addCommit(t, db, b, "b4", b3)
a5, _ = addCommit(t, db, a, "a5", a4)
b5, _ = addCommit(t, db, b, "b5", b4, a3)
a6, _ = addCommit(t, db, a, "a6", a5, b5)
addCommit(t, rdb, a, "a1")
addCommit(t, rdb, d, "d1")
addCommit(t, rdb, a, "a2", a1)
addCommit(t, rdb, c, "c2", a1)
addCommit(t, rdb, d, "d2", d1)
addCommit(t, rdb, a, "a3", a2)
addCommit(t, rdb, b, "b3", a2)
addCommit(t, rdb, c, "c3", c2, d2)
addCommit(t, rdb, a, "a4", a3)
addCommit(t, rdb, b, "b4", b3)
addCommit(t, rdb, a, "a5", a4)
addCommit(t, rdb, b, "b5", b4, a3)
addCommit(t, rdb, a, "a6", a5, b5)
// Additionally, |db| has a6<-a7<-a8<-a9.
// |rdb| has a6<-ra7<-ra8<-ra9.
a7, _ := addCommit(t, db, a, "a7", a6)
a8, _ := addCommit(t, db, a, "a8", a7)
a9, _ := addCommit(t, db, a, "a9", a8)
ra7, _ := addCommit(t, rdb, a, "ra7", a6)
ra8, _ := addCommit(t, rdb, a, "ra8", ra7)
ra9, _ := addCommit(t, rdb, a, "ra9", ra8)
assertCommonAncestor(t, a1, a1, a1, db, rdb, "all self")
assertCommonAncestor(t, a1, a1, a2, db, rdb, "one side self")
assertCommonAncestor(t, a2, a3, b3, db, rdb, "common parent")
assertCommonAncestor(t, a2, a4, b4, db, rdb, "common grandeparent")
assertCommonAncestor(t, a1, a6, c3, db, rdb, "traversing multiple parents on both sides")
assertCommonAncestor(t, a6, a9, ra9, db, rdb, "common third parent")
a9c, err := CommitFromValue(db.Format(), a9)
require.NoError(t, err)
ra9c, err := CommitFromValue(rdb.Format(), ra9)
require.NoError(t, err)
_, _, err = FindCommonAncestor(context.Background(), ra9c, a9c, db, rdb, db.ns, rdb.ns)
assert.Error(err)
})
}
func TestFindCommonAncestorCrissCrossDeterministic(t *testing.T) {
// Criss-cross merge with two maximal common ancestors at the same height.
// Verifies that findCommonCommit returns deterministically.
//
// base
// / \
// A B
// | |
// a1 b1
// |\ /|
// | \/ |
// | /\ |
// |/ \|
// C D
//
// Maximal common ancestors of C and D: {a1, b1}
// findCommonCommit must pick the same one every time.
storage := &chunks.TestStorage{}
db := NewDatabase(storage.NewViewWithDefaultFormat()).(*database)
defer db.Close()
a, b, c, d := "ds-a", "ds-b", "ds-c", "ds-d"
base, _ := addCommit(t, db, a, "base")
a1, _ := addCommit(t, db, a, "a1", base)
b1, _ := addCommit(t, db, b, "b1", base)
c1, _ := addCommit(t, db, c, "c1", a1, b1)
d1, _ := addCommit(t, db, d, "d1", a1, b1)
// Verify determinism: run 20 times, expect the same result every time.
ctx := context.Background()
c1c, err := LoadCommitRef(ctx, db, mustRef(types.NewRef(c1, db.Format())))
require.NoError(t, err)
d1c, err := LoadCommitRef(ctx, db, mustRef(types.NewRef(d1, db.Format())))
require.NoError(t, err)
var firstHash hash.Hash
for i := 0; i < 20; i++ {
found, ok, err := findCommonAncestorUsingParentsList(ctx, c1c, d1c, db, db, db.ns, db.ns)
require.NoError(t, err)
require.True(t, ok, "should find common ancestor")
if i == 0 {
firstHash = found
} else {
assert.Equal(t, firstHash, found,
"run %d: expected deterministic result, got different hash", i)
}
}
// Also verify it's one of {a1, b1} (both are valid maximal LCAs).
a1Addr := mustRef(types.NewRef(a1, db.Format())).TargetHash()
b1Addr := mustRef(types.NewRef(b1, db.Format())).TargetHash()
assert.True(t, firstHash == a1Addr || firstHash == b1Addr,
"result should be one of the maximal common ancestors")
}
func TestPersistedCommitConsts(t *testing.T) {
// changing constants that are persisted requires a migration strategy
assert.Equal(t, "parents", parentsField)
assert.Equal(t, "parents_list", parentsListField)
assert.Equal(t, "parents_closure", parentsClosureField)
assert.Equal(t, "value", valueField)
assert.Equal(t, "meta", commitMetaField)
assert.Equal(t, "Commit", commitName)
}