aboutsummaryrefslogblamecommitdiffstats
path: root/entity/dag/entity_actions_test.go
blob: 6181614bcc5794daab893d7fdf99c0ca999a44d1 (plain) (tree)
1
2
3
4
5
6
7
8
9


           
              
                 



                                             
                                               


                                                   
                                                                                

                  
                         
                                    

                                                




                                                        
                                       
                                                                             
 
                             

                                    
 
                              

                               
                                           

                               
                                                                 

                               
                                                                           


                                   

                                    
 
                             

                               
                                           

                               
                                                                 

                               
                                                                          



                                     
                                                                             
 
                             


                                    

                               


                                    




                                      
                                           

                               
                                            




                                      
                                                                 










                                                                                              

































                                                                                                         







































































                                                                                        
                              
                                                                             




                                                                   


                                      

                               


                                      









                                            
                                                                          


                                                   
                                         


                                                      
                                         



                                                      
                                                               
 


                                                                                 
                                                                         


                                                   
                                         


                                                          
                                         



                                                          
                                                               
 


                                                                                       

                                         

                               

                                            

                               
                                                                         


                                                   
                                         


                                                          
                                         



                                                          
                                                                  
 


                                                                                           






                                            
                                                                         











                                                          
                                                               












                                                                                                    
                                                                  

                               
                                                                  















                                                  
                                                                         











                                                          
                                                                  






                                            
                                                                         













                                                                                 
                                                               
 

                               
                                                                       










                                            
                                                             

                             
                                                                             





                                        






























                                                                                 
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)
}