repo_testing.go

  1package repository
  2
  3import (
  4	"math/rand"
  5	"os"
  6	"testing"
  7
  8	"github.com/ProtonMail/go-crypto/openpgp"
  9	"github.com/stretchr/testify/require"
 10
 11	"github.com/MichaelMure/git-bug/util/lamport"
 12)
 13
 14type RepoCreator func(t testing.TB, bare bool) TestedRepo
 15
 16// Test suite for a Repo implementation
 17func RepoTest(t *testing.T, creator RepoCreator) {
 18	for bare, name := range map[bool]string{
 19		false: "Plain",
 20		true:  "Bare",
 21	} {
 22		t.Run(name, func(t *testing.T) {
 23			repo := creator(t, bare)
 24
 25			t.Run("Data", func(t *testing.T) {
 26				RepoDataTest(t, repo)
 27				RepoDataSignatureTest(t, repo)
 28			})
 29
 30			t.Run("Config", func(t *testing.T) {
 31				RepoConfigTest(t, repo)
 32			})
 33
 34			t.Run("Storage", func(t *testing.T) {
 35				RepoStorageTest(t, repo)
 36			})
 37
 38			t.Run("Index", func(t *testing.T) {
 39				RepoIndexTest(t, repo)
 40			})
 41
 42			t.Run("Clocks", func(t *testing.T) {
 43				RepoClockTest(t, repo)
 44			})
 45		})
 46	}
 47}
 48
 49// helper to test a RepoConfig
 50func RepoConfigTest(t *testing.T, repo RepoConfig) {
 51	testConfig(t, repo.LocalConfig())
 52}
 53
 54func RepoStorageTest(t *testing.T, repo RepoStorage) {
 55	storage := repo.LocalStorage()
 56
 57	err := storage.MkdirAll("foo/bar", 0755)
 58	require.NoError(t, err)
 59
 60	f, err := storage.Create("foo/bar/foofoo")
 61	require.NoError(t, err)
 62
 63	_, err = f.Write([]byte("hello"))
 64	require.NoError(t, err)
 65
 66	// remove all
 67	err = storage.RemoveAll(".")
 68	require.NoError(t, err)
 69
 70	fi, err := storage.ReadDir(".")
 71	// a real FS would remove the root directory with RemoveAll and subsequent call would fail
 72	// a memory FS would still have a virtual root and subsequent call would succeed
 73	// not ideal, but will do for now
 74	if err == nil {
 75		require.Empty(t, fi)
 76	} else {
 77		require.True(t, os.IsNotExist(err))
 78	}
 79}
 80
 81func randomHash() Hash {
 82	var letterRunes = "abcdef0123456789"
 83	b := make([]byte, idLengthSHA256)
 84	for i := range b {
 85		b[i] = letterRunes[rand.Intn(len(letterRunes))]
 86	}
 87	return Hash(b)
 88}
 89
 90// helper to test a RepoData
 91func RepoDataTest(t *testing.T, repo RepoData) {
 92	// Blob
 93
 94	data := randomData()
 95
 96	blobHash1, err := repo.StoreData(data)
 97	require.NoError(t, err)
 98	require.True(t, blobHash1.IsValid())
 99
100	blob1Read, err := repo.ReadData(blobHash1)
101	require.NoError(t, err)
102	require.Equal(t, data, blob1Read)
103
104	_, err = repo.ReadData(randomHash())
105	require.ErrorIs(t, err, ErrNotFound)
106
107	// Tree
108
109	blobHash2, err := repo.StoreData(randomData())
110	require.NoError(t, err)
111	blobHash3, err := repo.StoreData(randomData())
112	require.NoError(t, err)
113
114	tree1 := []TreeEntry{
115		{
116			ObjectType: Blob,
117			Hash:       blobHash1,
118			Name:       "blob1",
119		},
120		{
121			ObjectType: Blob,
122			Hash:       blobHash2,
123			Name:       "blob2",
124		},
125	}
126
127	treeHash1, err := repo.StoreTree(tree1)
128	require.NoError(t, err)
129	require.True(t, treeHash1.IsValid())
130
131	tree1Read, err := repo.ReadTree(treeHash1)
132	require.NoError(t, err)
133	require.ElementsMatch(t, tree1, tree1Read)
134
135	tree2 := []TreeEntry{
136		{
137			ObjectType: Tree,
138			Hash:       treeHash1,
139			Name:       "tree1",
140		},
141		{
142			ObjectType: Blob,
143			Hash:       blobHash3,
144			Name:       "blob3",
145		},
146	}
147
148	treeHash2, err := repo.StoreTree(tree2)
149	require.NoError(t, err)
150	require.True(t, treeHash2.IsValid())
151
152	tree2Read, err := repo.ReadTree(treeHash2)
153	require.NoError(t, err)
154	require.ElementsMatch(t, tree2, tree2Read)
155
156	_, err = repo.ReadTree(randomHash())
157	require.ErrorIs(t, err, ErrNotFound)
158
159	// Commit
160
161	commit1, err := repo.StoreCommit(treeHash1)
162	require.NoError(t, err)
163	require.True(t, commit1.IsValid())
164
165	// commit with a parent
166	commit2, err := repo.StoreCommit(treeHash2, commit1)
167	require.NoError(t, err)
168	require.True(t, commit2.IsValid())
169
170	// ReadTree should accept tree and commit hashes
171	tree1read, err := repo.ReadTree(commit1)
172	require.NoError(t, err)
173	require.Equal(t, tree1read, tree1)
174
175	c2, err := repo.ReadCommit(commit2)
176	require.NoError(t, err)
177	c2expected := Commit{Hash: commit2, Parents: []Hash{commit1}, TreeHash: treeHash2}
178	require.Equal(t, c2expected, c2)
179
180	_, err = repo.ReadCommit(randomHash())
181	require.ErrorIs(t, err, ErrNotFound)
182
183	// Ref
184
185	exist1, err := repo.RefExist("refs/bugs/ref1")
186	require.NoError(t, err)
187	require.False(t, exist1)
188
189	err = repo.UpdateRef("refs/bugs/ref1", commit2)
190	require.NoError(t, err)
191
192	exist1, err = repo.RefExist("refs/bugs/ref1")
193	require.NoError(t, err)
194	require.True(t, exist1)
195
196	h, err := repo.ResolveRef("refs/bugs/ref1")
197	require.NoError(t, err)
198	require.Equal(t, commit2, h)
199
200	ls, err := repo.ListRefs("refs/bugs")
201	require.NoError(t, err)
202	require.ElementsMatch(t, []string{"refs/bugs/ref1"}, ls)
203
204	err = repo.CopyRef("refs/bugs/ref1", "refs/bugs/ref2")
205	require.NoError(t, err)
206
207	ls, err = repo.ListRefs("refs/bugs")
208	require.NoError(t, err)
209	require.ElementsMatch(t, []string{"refs/bugs/ref1", "refs/bugs/ref2"}, ls)
210
211	commits, err := repo.ListCommits("refs/bugs/ref2")
212	require.NoError(t, err)
213	require.Equal(t, []Hash{commit1, commit2}, commits)
214
215	_, err = repo.ResolveRef("/refs/bugs/refnotexist")
216	require.ErrorIs(t, err, ErrNotFound)
217
218	err = repo.CopyRef("/refs/bugs/refnotexist", "refs/foo")
219	require.ErrorIs(t, err, ErrNotFound)
220
221	// Cleanup
222
223	err = repo.RemoveRef("refs/bugs/ref1")
224	require.NoError(t, err)
225
226	// RemoveRef is idempotent
227	err = repo.RemoveRef("refs/bugs/ref1")
228	require.NoError(t, err)
229}
230
231func RepoDataSignatureTest(t *testing.T, repo RepoData) {
232	data := randomData()
233
234	blobHash, err := repo.StoreData(data)
235	require.NoError(t, err)
236
237	treeHash, err := repo.StoreTree([]TreeEntry{
238		{
239			ObjectType: Blob,
240			Hash:       blobHash,
241			Name:       "blob",
242		},
243	})
244	require.NoError(t, err)
245
246	pgpEntity1, err := openpgp.NewEntity("", "", "", nil)
247	require.NoError(t, err)
248	keyring1 := openpgp.EntityList{pgpEntity1}
249
250	pgpEntity2, err := openpgp.NewEntity("", "", "", nil)
251	require.NoError(t, err)
252	keyring2 := openpgp.EntityList{pgpEntity2}
253
254	commitHash1, err := repo.StoreSignedCommit(treeHash, pgpEntity1)
255	require.NoError(t, err)
256
257	commit1, err := repo.ReadCommit(commitHash1)
258	require.NoError(t, err)
259
260	_, err = openpgp.CheckDetachedSignature(keyring1, commit1.SignedData, commit1.Signature, nil)
261	require.NoError(t, err)
262
263	_, err = openpgp.CheckDetachedSignature(keyring2, commit1.SignedData, commit1.Signature, nil)
264	require.Error(t, err)
265
266	commitHash2, err := repo.StoreSignedCommit(treeHash, pgpEntity1, commitHash1)
267	require.NoError(t, err)
268
269	commit2, err := repo.ReadCommit(commitHash2)
270	require.NoError(t, err)
271
272	_, err = openpgp.CheckDetachedSignature(keyring1, commit2.SignedData, commit2.Signature, nil)
273	require.NoError(t, err)
274
275	_, err = openpgp.CheckDetachedSignature(keyring2, commit2.SignedData, commit2.Signature, nil)
276	require.Error(t, err)
277}
278
279func RepoIndexTest(t *testing.T, repo RepoIndex) {
280	idx, err := repo.GetIndex("a")
281	require.NoError(t, err)
282
283	// simple indexing
284	err = idx.IndexOne("id1", []string{"foo", "bar", "foobar barfoo"})
285	require.NoError(t, err)
286
287	// batched indexing
288	indexer, closer := idx.IndexBatch()
289	err = indexer("id2", []string{"hello", "foo bar"})
290	require.NoError(t, err)
291	err = indexer("id3", []string{"Hola", "Esta bien"})
292	require.NoError(t, err)
293	err = closer()
294	require.NoError(t, err)
295
296	// search
297	res, err := idx.Search([]string{"foobar"})
298	require.NoError(t, err)
299	require.ElementsMatch(t, []string{"id1"}, res)
300
301	res, err = idx.Search([]string{"foo"})
302	require.NoError(t, err)
303	require.ElementsMatch(t, []string{"id1", "id2"}, res)
304
305	// re-indexing an item replace previous versions
306	err = idx.IndexOne("id2", []string{"hello"})
307	require.NoError(t, err)
308
309	res, err = idx.Search([]string{"foo"})
310	require.NoError(t, err)
311	require.ElementsMatch(t, []string{"id1"}, res)
312
313	err = idx.Clear()
314	require.NoError(t, err)
315
316	res, err = idx.Search([]string{"foo"})
317	require.NoError(t, err)
318	require.Empty(t, res)
319}
320
321// helper to test a RepoClock
322func RepoClockTest(t *testing.T, repo RepoClock) {
323	allClocks, err := repo.AllClocks()
324	require.NoError(t, err)
325	require.Len(t, allClocks, 0)
326
327	clock, err := repo.GetOrCreateClock("foo")
328	require.NoError(t, err)
329	require.Equal(t, lamport.Time(1), clock.Time())
330
331	time, err := clock.Increment()
332	require.NoError(t, err)
333	require.Equal(t, lamport.Time(2), time)
334	require.Equal(t, lamport.Time(2), clock.Time())
335
336	clock2, err := repo.GetOrCreateClock("foo")
337	require.NoError(t, err)
338	require.Equal(t, lamport.Time(2), clock2.Time())
339
340	clock3, err := repo.GetOrCreateClock("bar")
341	require.NoError(t, err)
342	require.Equal(t, lamport.Time(1), clock3.Time())
343
344	allClocks, err = repo.AllClocks()
345	require.NoError(t, err)
346	require.Equal(t, map[string]lamport.Clock{
347		"foo": clock,
348		"bar": clock3,
349	}, allClocks)
350}
351
352func randomData() []byte {
353	var letterRunes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
354	b := make([]byte, 32)
355	for i := range b {
356		b[i] = letterRunes[rand.Intn(len(letterRunes))]
357	}
358	return b
359}