package dag
import (
"sort"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/repository"
)
func allEntities(t testing.TB, bugs <-chan entity.StreamedEntity[*Foo]) []*Foo {
t.Helper()
var result []*Foo
for streamed := range bugs {
require.NoError(t, streamed.Err)
result = append(result, streamed.Entity)
}
return result
}
func TestEntityPushPull(t *testing.T) {
repoA, repoB, _, id1, id2, resolvers, def := makeTestContextRemote(t)
// A --> remote --> B
e := New(def)
e.Append(newOp1(id1, "foo"))
err := e.Commit(repoA)
require.NoError(t, err)
_, err = Push(def, repoA, "remote")
require.NoError(t, err)
err = Pull(def, wrapper, repoB, resolvers, "remote", id1)
require.NoError(t, err)
entities := allEntities(t, ReadAll(def, wrapper, repoB, resolvers))
require.Len(t, entities, 1)
// B --> remote --> A
e = New(def)
e.Append(newOp2(id2, "bar"))
err = e.Commit(repoB)
require.NoError(t, err)
_, err = Push(def, repoB, "remote")
require.NoError(t, err)
err = Pull(def, wrapper, repoA, resolvers, "remote", id1)
require.NoError(t, err)
entities = allEntities(t, ReadAll(def, wrapper, repoB, resolvers))
require.Len(t, entities, 2)
}
func TestListLocalIds(t *testing.T) {
repoA, repoB, _, id1, id2, resolvers, def := makeTestContextRemote(t)
// A --> remote --> B
e := New(def)
e.Append(newOp1(id1, "foo"))
err := e.Commit(repoA)
require.NoError(t, err)
e = New(def)
e.Append(newOp2(id2, "bar"))
err = e.Commit(repoA)
require.NoError(t, err)
listLocalIds(t, def, repoA, 2)
listLocalIds(t, def, repoB, 0)
_, err = Push(def, repoA, "remote")
require.NoError(t, err)
_, err = Fetch(def, repoB, "remote")
require.NoError(t, err)
listLocalIds(t, def, repoA, 2)
listLocalIds(t, def, repoB, 0)
err = Pull(def, wrapper, repoB, resolvers, "remote", id1)
require.NoError(t, err)
listLocalIds(t, def, repoA, 2)
listLocalIds(t, def, repoB, 2)
}
func listLocalIds(t *testing.T, def Definition, repo repository.RepoData, expectedCount int) {
ids, err := ListLocalIds(def, repo)
require.NoError(t, err)
require.Len(t, ids, expectedCount)
}
func assertMergeResults(t *testing.T, expected []entity.MergeResult, results <-chan entity.MergeResult) {
t.Helper()
var allResults []entity.MergeResult
for result := range results {
allResults = append(allResults, result)
}
require.Equal(t, len(expected), len(allResults))
sort.Slice(allResults, func(i, j int) bool {
return allResults[i].Id < allResults[j].Id
})
sort.Slice(expected, func(i, j int) bool {
return expected[i].Id < expected[j].Id
})
for i, result := range allResults {
require.NoError(t, result.Err)
require.Equal(t, expected[i].Id, result.Id)
require.Equal(t, expected[i].Status, result.Status)
switch result.Status {
case entity.MergeStatusNew, entity.MergeStatusUpdated:
require.NotNil(t, result.Entity)
require.Equal(t, expected[i].Id, result.Entity.Id())
}
i++
}
}
func assertEqualRefs(t *testing.T, repoA, repoB repository.RepoData, prefix string) {
t.Helper()
refsA, err := repoA.ListRefs("")
require.NoError(t, err)
var refsAFiltered []string
for _, ref := range refsA {
if strings.HasPrefix(ref, prefix) {
refsAFiltered = append(refsAFiltered, ref)
}
}
refsB, err := repoB.ListRefs("")
require.NoError(t, err)
var refsBFiltered []string
for _, ref := range refsB {
if strings.HasPrefix(ref, prefix) {
refsBFiltered = append(refsBFiltered, ref)
}
}
require.NotEmpty(t, refsAFiltered)
require.Equal(t, refsAFiltered, refsBFiltered)
for _, ref := range refsAFiltered {
commitA, err := repoA.ResolveRef(ref)
require.NoError(t, err)
commitB, err := repoB.ResolveRef(ref)
require.NoError(t, err)
require.Equal(t, commitA, commitB)
}
}
func assertNotEqualRefs(t *testing.T, repoA, repoB repository.RepoData, prefix string) {
t.Helper()
refsA, err := repoA.ListRefs("")
require.NoError(t, err)
var refsAFiltered []string
for _, ref := range refsA {
if strings.HasPrefix(ref, prefix) {
refsAFiltered = append(refsAFiltered, ref)
}
}
refsB, err := repoB.ListRefs("")
require.NoError(t, err)
var refsBFiltered []string
for _, ref := range refsB {
if strings.HasPrefix(ref, prefix) {
refsBFiltered = append(refsBFiltered, ref)
}
}
require.NotEmpty(t, refsAFiltered)
require.Equal(t, refsAFiltered, refsBFiltered)
for _, ref := range refsAFiltered {
commitA, err := repoA.ResolveRef(ref)
require.NoError(t, err)
commitB, err := repoB.ResolveRef(ref)
require.NoError(t, err)
require.NotEqual(t, commitA, commitB)
}
}
func TestMerge(t *testing.T) {
repoA, repoB, _, id1, id2, resolvers, def := makeTestContextRemote(t)
// SCENARIO 1
// if the remote Entity doesn't exist locally, it's created
// 2 entities in repoA + push to remote
e1A := New(def)
e1A.Append(newOp1(id1, "foo"))
err := e1A.Commit(repoA)
require.NoError(t, err)
e2A := New(def)
e2A.Append(newOp2(id2, "bar"))
err = e2A.Commit(repoA)
require.NoError(t, err)
_, err = Push(def, repoA, "remote")
require.NoError(t, err)
// repoB: fetch + merge from remote
_, err = Fetch(def, repoB, "remote")
require.NoError(t, err)
results := MergeAll(def, wrapper, repoB, resolvers, "remote", id1)
assertMergeResults(t, []entity.MergeResult{
{
Id: e1A.Id(),
Status: entity.MergeStatusNew,
},
{
Id: e2A.Id(),
Status: entity.MergeStatusNew,
},
}, results)
assertEqualRefs(t, repoA, repoB, "refs/"+def.Namespace)
// SCENARIO 2
// if the remote and local Entity have the same state, nothing is changed
results = MergeAll(def, wrapper, repoB, resolvers, "remote", id1)
assertMergeResults(t, []entity.MergeResult{
{
Id: e1A.Id(),
Status: entity.MergeStatusNothing,
},
{
Id: e2A.Id(),
Status: entity.MergeStatusNothing,
},
}, results)
assertEqualRefs(t, repoA, repoB, "refs/"+def.Namespace)
// SCENARIO 3
// if the local Entity has new commits but the remote don't, nothing is changed
e1A.Append(newOp1(id1, "barbar"))
err = e1A.Commit(repoA)
require.NoError(t, err)
e2A.Append(newOp2(id2, "barbarbar"))
err = e2A.Commit(repoA)
require.NoError(t, err)
results = MergeAll(def, wrapper, repoA, resolvers, "remote", id1)
assertMergeResults(t, []entity.MergeResult{
{
Id: e1A.Id(),
Status: entity.MergeStatusNothing,
},
{
Id: e2A.Id(),
Status: entity.MergeStatusNothing,
},
}, results)
assertNotEqualRefs(t, repoA, repoB, "refs/"+def.Namespace)
// SCENARIO 4
// if the remote has new commit, the local bug is updated to match the same history
// (fast-forward update)
_, err = Push(def, repoA, "remote")
require.NoError(t, err)
_, err = Fetch(def, repoB, "remote")
require.NoError(t, err)
results = MergeAll(def, wrapper, repoB, resolvers, "remote", id1)
assertMergeResults(t, []entity.MergeResult{
{
Id: e1A.Id(),
Status: entity.MergeStatusUpdated,
},
{
Id: e2A.Id(),
Status: entity.MergeStatusUpdated,
},
}, results)
assertEqualRefs(t, repoA, repoB, "refs/"+def.Namespace)
// SCENARIO 5
// if both local and remote Entity have new commits (that is, we have a concurrent edition),
// a merge commit with an empty operationPack is created to join both branch and form a DAG.
e1A.Append(newOp1(id1, "barbarfoo"))
err = e1A.Commit(repoA)
require.NoError(t, err)
e2A.Append(newOp2(id2, "barbarbarfoo"))
err = e2A.Commit(repoA)
require.NoError(t, err)
e1B, err := Read(def, wrapper, repoB, resolvers, e1A.Id())
require.NoError(t, err)
e2B, err := Read(def, wrapper, repoB, resolvers, e2A.Id())
require.NoError(t, err)
e1B.Append(newOp1(id1, "barbarfoofoo"))
err = e1B.Commit(repoB)
require.NoError(t, err)
e2B.Append(newOp2(id2, "barbarbarfoofoo"))
err = e2B.Commit(repoB)
require.NoError(t, err)
_, err = Push(def, repoA, "remote")
require.NoError(t, err)
_, err = Fetch(def, repoB, "remote")
require.NoError(t, err)
results = MergeAll(def, wrapper, repoB, resolvers, "remote", id1)
assertMergeResults(t, []entity.MergeResult{
{
Id: e1A.Id(),
Status: entity.MergeStatusUpdated,
},
{
Id: e2A.Id(),
Status: entity.MergeStatusUpdated,
},
}, results)
assertNotEqualRefs(t, repoA, repoB, "refs/"+def.Namespace)
_, err = Push(def, repoB, "remote")
require.NoError(t, err)
_, err = Fetch(def, repoA, "remote")
require.NoError(t, err)
results = MergeAll(def, wrapper, repoA, resolvers, "remote", id1)
assertMergeResults(t, []entity.MergeResult{
{
Id: e1A.Id(),
Status: entity.MergeStatusUpdated,
},
{
Id: e2A.Id(),
Status: entity.MergeStatusUpdated,
},
}, results)
// make sure that the graphs become stable over multiple repo, due to the
// fast-forward
assertEqualRefs(t, repoA, repoB, "refs/"+def.Namespace)
}
func TestRemove(t *testing.T) {
repoA, _, _, id1, _, resolvers, def := makeTestContextRemote(t)
e := New(def)
e.Append(newOp1(id1, "foo"))
require.NoError(t, e.Commit(repoA))
_, err := Push(def, repoA, "remote")
require.NoError(t, err)
err = Remove(def, repoA, e.Id())
require.NoError(t, err)
_, err = Read(def, wrapper, repoA, resolvers, e.Id())
require.Error(t, err)
_, err = readRemote(def, wrapper, repoA, resolvers, "remote", e.Id())
require.Error(t, err)
// Remove is idempotent
err = Remove(def, repoA, e.Id())
require.NoError(t, err)
}
func TestRemoveAll(t *testing.T) {
repoA, _, _, id1, _, resolvers, def := makeTestContextRemote(t)
var ids []entity.Id
for i := 0; i < 10; i++ {
e := New(def)
e.Append(newOp1(id1, "foo"))
require.NoError(t, e.Commit(repoA))
ids = append(ids, e.Id())
}
_, err := Push(def, repoA, "remote")
require.NoError(t, err)
err = RemoveAll(def, repoA)
require.NoError(t, err)
for _, id := range ids {
_, err = Read(def, wrapper, repoA, resolvers, id)
require.Error(t, err)
_, err = readRemote(def, wrapper, repoA, resolvers, "remote", id)
require.Error(t, err)
}
// Remove is idempotent
err = RemoveAll(def, repoA)
require.NoError(t, err)
}