package repository
import (
"math/rand"
"os"
"testing"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/util/lamport"
)
type RepoCreator func(t testing.TB, bare bool) TestedRepo
// Test suite for a Repo implementation
func RepoTest(t *testing.T, creator RepoCreator) {
for bare, name := range map[bool]string{
false: "Plain",
true: "Bare",
} {
t.Run(name, func(t *testing.T) {
repo := creator(t, bare)
t.Run("Data", func(t *testing.T) {
RepoDataTest(t, repo)
RepoDataSignatureTest(t, repo)
})
t.Run("Config", func(t *testing.T) {
RepoConfigTest(t, repo)
})
t.Run("Storage", func(t *testing.T) {
RepoStorageTest(t, repo)
})
t.Run("Index", func(t *testing.T) {
RepoIndexTest(t, repo)
})
t.Run("Clocks", func(t *testing.T) {
RepoClockTest(t, repo)
})
})
}
}
// helper to test a RepoConfig
func RepoConfigTest(t *testing.T, repo RepoConfig) {
testConfig(t, repo.LocalConfig())
}
func RepoStorageTest(t *testing.T, repo RepoStorage) {
storage := repo.LocalStorage()
err := storage.MkdirAll("foo/bar", 0755)
require.NoError(t, err)
f, err := storage.Create("foo/bar/foofoo")
require.NoError(t, err)
_, err = f.Write([]byte("hello"))
require.NoError(t, err)
err = f.Close()
require.NoError(t, err)
// remove all
err = storage.RemoveAll(".")
require.NoError(t, err)
fi, err := storage.ReadDir(".")
// a real FS would remove the root directory with RemoveAll and subsequent call would fail
// a memory FS would still have a virtual root and subsequent call would succeed
// not ideal, but will do for now
if err == nil {
require.Empty(t, fi)
} else {
require.True(t, os.IsNotExist(err))
}
}
func randomHash() Hash {
var letterRunes = "abcdef0123456789"
b := make([]byte, idLengthSHA256)
for i := range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return Hash(b)
}
// helper to test a RepoData
func RepoDataTest(t *testing.T, repo RepoData) {
// Blob
data := randomData()
blobHash1, err := repo.StoreData(data)
require.NoError(t, err)
require.True(t, blobHash1.IsValid())
blob1Read, err := repo.ReadData(blobHash1)
require.NoError(t, err)
require.Equal(t, data, blob1Read)
_, err = repo.ReadData(randomHash())
require.ErrorIs(t, err, ErrNotFound)
// Tree
blobHash2, err := repo.StoreData(randomData())
require.NoError(t, err)
blobHash3, err := repo.StoreData(randomData())
require.NoError(t, err)
tree1 := []TreeEntry{
{
ObjectType: Blob,
Hash: blobHash1,
Name: "blob1",
},
{
ObjectType: Blob,
Hash: blobHash2,
Name: "blob2",
},
}
treeHash1, err := repo.StoreTree(tree1)
require.NoError(t, err)
require.True(t, treeHash1.IsValid())
tree1Read, err := repo.ReadTree(treeHash1)
require.NoError(t, err)
require.ElementsMatch(t, tree1, tree1Read)
tree2 := []TreeEntry{
{
ObjectType: Tree,
Hash: treeHash1,
Name: "tree1",
},
{
ObjectType: Blob,
Hash: blobHash3,
Name: "blob3",
},
}
treeHash2, err := repo.StoreTree(tree2)
require.NoError(t, err)
require.True(t, treeHash2.IsValid())
tree2Read, err := repo.ReadTree(treeHash2)
require.NoError(t, err)
require.ElementsMatch(t, tree2, tree2Read)
_, err = repo.ReadTree(randomHash())
require.ErrorIs(t, err, ErrNotFound)
// Commit
commit1, err := repo.StoreCommit(treeHash1)
require.NoError(t, err)
require.True(t, commit1.IsValid())
// commit with a parent
commit2, err := repo.StoreCommit(treeHash2, commit1)
require.NoError(t, err)
require.True(t, commit2.IsValid())
// ReadTree should accept tree and commit hashes
tree1read, err := repo.ReadTree(commit1)
require.NoError(t, err)
require.Equal(t, tree1read, tree1)
c2, err := repo.ReadCommit(commit2)
require.NoError(t, err)
c2expected := Commit{Hash: commit2, Parents: []Hash{commit1}, TreeHash: treeHash2}
require.Equal(t, c2expected, c2)
_, err = repo.ReadCommit(randomHash())
require.ErrorIs(t, err, ErrNotFound)
// Ref
exist1, err := repo.RefExist("refs/bugs/ref1")
require.NoError(t, err)
require.False(t, exist1)
err = repo.UpdateRef("refs/bugs/ref1", commit2)
require.NoError(t, err)
exist1, err = repo.RefExist("refs/bugs/ref1")
require.NoError(t, err)
require.True(t, exist1)
h, err := repo.ResolveRef("refs/bugs/ref1")
require.NoError(t, err)
require.Equal(t, commit2, h)
ls, err := repo.ListRefs("refs/bugs")
require.NoError(t, err)
require.ElementsMatch(t, []string{"refs/bugs/ref1"}, ls)
err = repo.CopyRef("refs/bugs/ref1", "refs/bugs/ref2")
require.NoError(t, err)
ls, err = repo.ListRefs("refs/bugs")
require.NoError(t, err)
require.ElementsMatch(t, []string{"refs/bugs/ref1", "refs/bugs/ref2"}, ls)
commits, err := repo.ListCommits("refs/bugs/ref2")
require.NoError(t, err)
require.Equal(t, []Hash{commit1, commit2}, commits)
_, err = repo.ResolveRef("/refs/bugs/refnotexist")
require.ErrorIs(t, err, ErrNotFound)
err = repo.CopyRef("/refs/bugs/refnotexist", "refs/foo")
require.ErrorIs(t, err, ErrNotFound)
// Cleanup
err = repo.RemoveRef("refs/bugs/ref1")
require.NoError(t, err)
// RemoveRef is idempotent
err = repo.RemoveRef("refs/bugs/ref1")
require.NoError(t, err)
}
func RepoDataSignatureTest(t *testing.T, repo RepoData) {
data := randomData()
blobHash, err := repo.StoreData(data)
require.NoError(t, err)
treeHash, err := repo.StoreTree([]TreeEntry{
{
ObjectType: Blob,
Hash: blobHash,
Name: "blob",
},
})
require.NoError(t, err)
pgpEntity1, err := openpgp.NewEntity("", "", "", nil)
require.NoError(t, err)
keyring1 := openpgp.EntityList{pgpEntity1}
pgpEntity2, err := openpgp.NewEntity("", "", "", nil)
require.NoError(t, err)
keyring2 := openpgp.EntityList{pgpEntity2}
commitHash1, err := repo.StoreSignedCommit(treeHash, pgpEntity1)
require.NoError(t, err)
commit1, err := repo.ReadCommit(commitHash1)
require.NoError(t, err)
_, err = openpgp.CheckDetachedSignature(keyring1, commit1.SignedData, commit1.Signature, nil)
require.NoError(t, err)
_, err = openpgp.CheckDetachedSignature(keyring2, commit1.SignedData, commit1.Signature, nil)
require.Error(t, err)
commitHash2, err := repo.StoreSignedCommit(treeHash, pgpEntity1, commitHash1)
require.NoError(t, err)
commit2, err := repo.ReadCommit(commitHash2)
require.NoError(t, err)
_, err = openpgp.CheckDetachedSignature(keyring1, commit2.SignedData, commit2.Signature, nil)
require.NoError(t, err)
_, err = openpgp.CheckDetachedSignature(keyring2, commit2.SignedData, commit2.Signature, nil)
require.Error(t, err)
}
func RepoIndexTest(t *testing.T, repo RepoIndex) {
idx, err := repo.GetIndex("a")
require.NoError(t, err)
// simple indexing
err = idx.IndexOne("id1", []string{"foo", "bar", "foobar barfoo"})
require.NoError(t, err)
// batched indexing
indexer, closer := idx.IndexBatch()
err = indexer("id2", []string{"hello", "foo bar"})
require.NoError(t, err)
err = indexer("id3", []string{"Hola", "Esta bien"})
require.NoError(t, err)
err = closer()
require.NoError(t, err)
// search
res, err := idx.Search([]string{"foobar"})
require.NoError(t, err)
require.ElementsMatch(t, []string{"id1"}, res)
res, err = idx.Search([]string{"foo"})
require.NoError(t, err)
require.ElementsMatch(t, []string{"id1", "id2"}, res)
// re-indexing an item replace previous versions
err = idx.IndexOne("id2", []string{"hello"})
require.NoError(t, err)
res, err = idx.Search([]string{"foo"})
require.NoError(t, err)
require.ElementsMatch(t, []string{"id1"}, res)
err = idx.Clear()
require.NoError(t, err)
res, err = idx.Search([]string{"foo"})
require.NoError(t, err)
require.Empty(t, res)
}
// helper to test a RepoClock
func RepoClockTest(t *testing.T, repo RepoClock) {
allClocks, err := repo.AllClocks()
require.NoError(t, err)
require.Len(t, allClocks, 0)
clock, err := repo.GetOrCreateClock("foo")
require.NoError(t, err)
require.Equal(t, lamport.Time(1), clock.Time())
time, err := clock.Increment()
require.NoError(t, err)
require.Equal(t, lamport.Time(2), time)
require.Equal(t, lamport.Time(2), clock.Time())
clock2, err := repo.GetOrCreateClock("foo")
require.NoError(t, err)
require.Equal(t, lamport.Time(2), clock2.Time())
clock3, err := repo.GetOrCreateClock("bar")
require.NoError(t, err)
require.Equal(t, lamport.Time(1), clock3.Time())
allClocks, err = repo.AllClocks()
require.NoError(t, err)
require.Equal(t, map[string]lamport.Clock{
"foo": clock,
"bar": clock3,
}, allClocks)
}
func randomData() []byte {
var letterRunes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
b := make([]byte, 32)
for i := range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return b
}