repo_cache_test.go

  1package cache
  2
  3import (
  4	"strings"
  5	"testing"
  6	"time"
  7
  8	"github.com/stretchr/testify/assert"
  9	"github.com/stretchr/testify/require"
 10
 11	"github.com/git-bug/git-bug/entities/bug"
 12	"github.com/git-bug/git-bug/entities/identity"
 13	"github.com/git-bug/git-bug/entity"
 14	"github.com/git-bug/git-bug/internal/test"
 15	"github.com/git-bug/git-bug/query"
 16	"github.com/git-bug/git-bug/repository"
 17)
 18
 19type observerEvent struct {
 20	typename string
 21	id       entity.Id
 22}
 23
 24var _ Observer = &observer{}
 25
 26type observer struct {
 27	created []observerEvent
 28	updated []observerEvent
 29	removed []observerEvent
 30}
 31
 32func (o *observer) EntityEvent(event EntityEventType, _ string, typename string, id entity.Id) {
 33	switch event {
 34	case EntityEventCreated:
 35		o.created = append(o.created, observerEvent{typename, id})
 36	case EntityEventUpdated:
 37		o.updated = append(o.updated, observerEvent{typename, id})
 38	case EntityEventRemoved:
 39		o.removed = append(o.removed, observerEvent{typename, id})
 40	}
 41}
 42
 43func TestCache(t *testing.T) {
 44	f := test.NewFlaky(t, &test.FlakyOptions{
 45		MaxAttempts: 5,
 46	})
 47
 48	f.Run(func(t testing.TB) {
 49		repo := repository.CreateGoGitTestRepo(t, false)
 50
 51		indexCount := func(t testing.TB, name string) uint64 {
 52			t.Helper()
 53			idx, err := repo.GetIndex(name)
 54			require.NoError(t, err)
 55			count, err := idx.DocCount()
 56			require.NoError(t, err)
 57			return count
 58		}
 59		assertOberserverEvent := func(obs observer, created, updated, removed int) {
 60			t.Helper()
 61			require.Len(t, obs.created, created)
 62			require.Len(t, obs.updated, updated)
 63			require.Len(t, obs.removed, removed)
 64		}
 65
 66		cache, err := NewRepoCacheNoEvents(repo)
 67		require.NoError(t, err)
 68
 69		var obsIdentity, obsBug observer
 70		require.NoError(t, cache.registerObserver("repotest", identity.Typename, &obsIdentity))
 71		require.NoError(t, cache.registerObserver("repotest", bug.Typename, &obsBug))
 72
 73		// Create, set and get user identity
 74		iden1, err := cache.Identities().New("René Descartes", "rene@descartes.fr")
 75		require.NoError(t, err)
 76		assertOberserverEvent(obsIdentity, 1, 0, 0)
 77		assertOberserverEvent(obsBug, 0, 0, 0)
 78		err = cache.SetUserIdentity(iden1)
 79		require.NoError(t, err)
 80		userIden, err := cache.GetUserIdentity()
 81		require.NoError(t, err)
 82		require.Equal(t, iden1.Id(), userIden.Id())
 83
 84		// it's possible to create two identical identities
 85		iden2, err := cache.Identities().New("René Descartes", "rene@descartes.fr")
 86		require.NoError(t, err)
 87		assertOberserverEvent(obsIdentity, 2, 0, 0)
 88		assertOberserverEvent(obsBug, 0, 0, 0)
 89
 90		// Two identical identities yield a different id
 91		require.NotEqual(t, iden1.Id(), iden2.Id())
 92
 93		// There are now two identities in the cache
 94		require.Len(t, cache.Identities().AllIds(), 2)
 95		require.Len(t, cache.identities.excerpts, 2)
 96		require.Len(t, cache.identities.cached, 2)
 97		require.Equal(t, uint64(2), indexCount(t, identity.Namespace))
 98		require.Equal(t, uint64(0), indexCount(t, bug.Namespace))
 99
100		// Create a bug
101		bug1, _, err := cache.Bugs().New("title", "message")
102		require.NoError(t, err)
103		assertOberserverEvent(obsIdentity, 2, 0, 0)
104		assertOberserverEvent(obsBug, 1, 0, 0)
105
106		// It's possible to create two identical bugs
107		bug2, _, err := cache.Bugs().New("title", "marker")
108		require.NoError(t, err)
109		assertOberserverEvent(obsIdentity, 2, 0, 0)
110		assertOberserverEvent(obsBug, 2, 0, 0)
111
112		// two identical bugs yield a different id
113		require.NotEqual(t, bug1.Id(), bug2.Id())
114
115		// There is now two bugs in the cache
116		require.Len(t, cache.Bugs().AllIds(), 2)
117		require.Len(t, cache.bugs.excerpts, 2)
118		require.Len(t, cache.bugs.cached, 2)
119		require.Equal(t, uint64(2), indexCount(t, identity.Namespace))
120		require.Equal(t, uint64(2), indexCount(t, bug.Namespace))
121
122		// Resolving
123		_, err = cache.Identities().Resolve(iden1.Id())
124		require.NoError(t, err)
125		_, err = cache.Identities().ResolveExcerpt(iden1.Id())
126		require.NoError(t, err)
127		_, err = cache.Identities().ResolvePrefix(iden1.Id().String()[:10])
128		require.NoError(t, err)
129
130		_, err = cache.Bugs().Resolve(bug1.Id())
131		require.NoError(t, err)
132		_, err = cache.Bugs().ResolveExcerpt(bug1.Id())
133		require.NoError(t, err)
134		_, err = cache.Bugs().ResolvePrefix(bug1.Id().String()[:10])
135		require.NoError(t, err)
136
137		// Querying
138		q, err := query.Parse("status:open author:descartes sort:edit-asc")
139		require.NoError(t, err)
140		res, err := cache.Bugs().Query(q)
141		require.NoError(t, err)
142		require.Len(t, res, 2)
143
144		q, err = query.Parse("status:open marker") // full-text search
145		require.NoError(t, err)
146		res, err = cache.Bugs().Query(q)
147		require.NoError(t, err)
148		require.Len(t, res, 1)
149
150		// Updating
151		_, _, err = bug1.AddComment("new comment")
152		require.NoError(t, err)
153		assertOberserverEvent(obsIdentity, 2, 0, 0)
154		assertOberserverEvent(obsBug, 2, 1, 0)
155
156		// Close
157		require.NoError(t, cache.Close())
158		require.Empty(t, cache.bugs.cached)
159		require.Empty(t, cache.bugs.excerpts)
160		require.Empty(t, cache.identities.cached)
161		require.Empty(t, cache.identities.excerpts)
162
163		// Reload, only excerpt are loaded, but as we need to load the identities used in the bugs
164		// to check the signatures, we also load the identity used above
165		cache, err = NewRepoCacheNoEvents(repo)
166		require.NoError(t, err)
167		require.NoError(t, cache.registerObserver("repotest", identity.Typename, &obsIdentity))
168		require.NoError(t, cache.registerObserver("repotest", bug.Typename, &obsBug))
169
170		require.Len(t, cache.bugs.cached, 0)
171		require.Len(t, cache.bugs.excerpts, 2)
172		require.Len(t, cache.identities.cached, 0)
173		require.Len(t, cache.identities.excerpts, 2)
174		require.Equal(t, uint64(2), indexCount(t, identity.Namespace))
175		require.Equal(t, uint64(2), indexCount(t, bug.Namespace))
176
177		// Resolving load from the disk
178		_, err = cache.Identities().Resolve(iden1.Id())
179		require.NoError(t, err)
180		_, err = cache.Identities().ResolveExcerpt(iden1.Id())
181		require.NoError(t, err)
182		_, err = cache.Identities().ResolvePrefix(iden1.Id().String()[:10])
183		require.NoError(t, err)
184
185		_, err = cache.Bugs().Resolve(bug1.Id())
186		require.NoError(t, err)
187		_, err = cache.Bugs().ResolveExcerpt(bug1.Id())
188		require.NoError(t, err)
189		_, err = cache.Bugs().ResolvePrefix(bug1.Id().String()[:10])
190		require.NoError(t, err)
191
192		require.Len(t, cache.bugs.cached, 1)
193		require.Len(t, cache.bugs.excerpts, 2)
194		require.Len(t, cache.identities.cached, 1)
195		require.Len(t, cache.identities.excerpts, 2)
196		require.Equal(t, uint64(2), indexCount(t, identity.Namespace))
197		require.Equal(t, uint64(2), indexCount(t, bug.Namespace))
198
199		// Remove + RemoveAll
200		err = cache.Identities().Remove(iden1.Id().String()[:10])
201		require.NoError(t, err)
202		assertOberserverEvent(obsIdentity, 2, 0, 1)
203		assertOberserverEvent(obsBug, 2, 1, 0)
204		err = cache.Bugs().Remove(bug1.Id().String()[:10])
205		require.NoError(t, err)
206		assertOberserverEvent(obsIdentity, 2, 0, 1)
207		assertOberserverEvent(obsBug, 2, 1, 1)
208		require.Len(t, cache.bugs.cached, 0)
209		require.Len(t, cache.bugs.excerpts, 1)
210		require.Len(t, cache.identities.cached, 0)
211		require.Len(t, cache.identities.excerpts, 1)
212		require.Equal(t, uint64(1), indexCount(t, identity.Namespace))
213		require.Equal(t, uint64(1), indexCount(t, bug.Namespace))
214
215		_, err = cache.Identities().New("René Descartes", "rene@descartes.fr")
216		require.NoError(t, err)
217		assertOberserverEvent(obsIdentity, 3, 0, 1)
218		assertOberserverEvent(obsBug, 2, 1, 1)
219		_, _, err = cache.Bugs().NewRaw(iden2, time.Now().Unix(), "title", "message", nil, nil)
220		require.NoError(t, err)
221		assertOberserverEvent(obsIdentity, 3, 0, 1)
222		assertOberserverEvent(obsBug, 3, 1, 1)
223
224		err = cache.RemoveAll()
225		require.NoError(t, err)
226		assertOberserverEvent(obsIdentity, 3, 0, 3)
227		assertOberserverEvent(obsBug, 3, 1, 3)
228		require.Len(t, cache.bugs.cached, 0)
229		require.Len(t, cache.bugs.excerpts, 0)
230		require.Len(t, cache.identities.cached, 0)
231		require.Len(t, cache.identities.excerpts, 0)
232		require.Equal(t, uint64(0), indexCount(t, identity.Namespace))
233		require.Equal(t, uint64(0), indexCount(t, bug.Namespace))
234
235		// Close
236		require.NoError(t, cache.Close())
237		require.Empty(t, cache.bugs.cached)
238		require.Empty(t, cache.bugs.excerpts)
239		require.Empty(t, cache.identities.cached)
240		require.Empty(t, cache.identities.excerpts)
241	})
242}
243
244func TestCachePushPull(t *testing.T) {
245	repoA, repoB, _ := repository.SetupGoGitReposAndRemote(t)
246
247	cacheA := createTestRepoCacheNoEvents(t, repoA)
248	cacheB := createTestRepoCacheNoEvents(t, repoB)
249
250	// Create, set and get user identity
251	reneA, err := cacheA.Identities().New("René Descartes", "rene@descartes.fr")
252	require.NoError(t, err)
253	err = cacheA.SetUserIdentity(reneA)
254	require.NoError(t, err)
255	isaacB, err := cacheB.Identities().New("Isaac Newton", "isaac@newton.uk")
256	require.NoError(t, err)
257	err = cacheB.SetUserIdentity(isaacB)
258	require.NoError(t, err)
259
260	// distribute the identity
261	_, err = cacheA.Push("origin")
262	require.NoError(t, err)
263	err = cacheB.Pull("origin")
264	require.NoError(t, err)
265
266	// Create a bug in A
267	_, _, err = cacheA.Bugs().New("bug1", "message")
268	require.NoError(t, err)
269
270	// A --> remote --> B
271	_, err = cacheA.Push("origin")
272	require.NoError(t, err)
273
274	err = cacheB.Pull("origin")
275	require.NoError(t, err)
276
277	require.Len(t, cacheB.Bugs().AllIds(), 1)
278
279	// retrieve and set identity
280	reneB, err := cacheB.Identities().Resolve(reneA.Id())
281	require.NoError(t, err)
282
283	err = cacheB.SetUserIdentity(reneB)
284	require.NoError(t, err)
285
286	// B --> remote --> A
287	_, _, err = cacheB.Bugs().New("bug2", "message")
288	require.NoError(t, err)
289
290	_, err = cacheB.Push("origin")
291	require.NoError(t, err)
292
293	err = cacheA.Pull("origin")
294	require.NoError(t, err)
295
296	require.Len(t, cacheA.Bugs().AllIds(), 2)
297}
298
299func TestRemove(t *testing.T) {
300	repo := repository.CreateGoGitTestRepo(t, false)
301	remoteA := repository.CreateGoGitTestRepo(t, true)
302	remoteB := repository.CreateGoGitTestRepo(t, true)
303
304	err := repo.AddRemote("remoteA", remoteA.GetLocalRemote())
305	require.NoError(t, err)
306
307	err = repo.AddRemote("remoteB", remoteB.GetLocalRemote())
308	require.NoError(t, err)
309
310	repoCache := createTestRepoCacheNoEvents(t, repo)
311
312	rene, err := repoCache.Identities().New("René Descartes", "rene@descartes.fr")
313	require.NoError(t, err)
314
315	err = repoCache.SetUserIdentity(rene)
316	require.NoError(t, err)
317
318	_, _, err = repoCache.Bugs().New("title", "message")
319	require.NoError(t, err)
320
321	// and one more for testing
322	b1, _, err := repoCache.Bugs().New("title", "message")
323	require.NoError(t, err)
324
325	_, err = repoCache.Push("remoteA")
326	require.NoError(t, err)
327
328	_, err = repoCache.Push("remoteB")
329	require.NoError(t, err)
330
331	_, err = repoCache.Fetch("remoteA")
332	require.NoError(t, err)
333
334	_, err = repoCache.Fetch("remoteB")
335	require.NoError(t, err)
336
337	err = repoCache.Bugs().Remove(b1.Id().String())
338	require.NoError(t, err)
339	assert.Len(t, repoCache.bugs.cached, 1)
340	assert.Len(t, repoCache.bugs.excerpts, 1)
341
342	_, err = repoCache.Bugs().Resolve(b1.Id())
343	assert.ErrorAs(t, entity.ErrNotFound{}, err)
344}
345
346func TestCacheEviction(t *testing.T) {
347	repo := repository.CreateGoGitTestRepo(t, false)
348	repoCache := createTestRepoCacheNoEvents(t, repo)
349	repoCache.setCacheSize(2)
350
351	require.Equal(t, 2, repoCache.bugs.maxLoaded)
352	require.Len(t, repoCache.bugs.cached, 0)
353	require.Equal(t, repoCache.bugs.lru.Len(), 0)
354
355	// Generating some bugs
356	rene, err := repoCache.Identities().New("René Descartes", "rene@descartes.fr")
357	require.NoError(t, err)
358	err = repoCache.SetUserIdentity(rene)
359	require.NoError(t, err)
360
361	bug1, _, err := repoCache.Bugs().New("title", "message")
362	require.NoError(t, err)
363
364	checkBugPresence(t, repoCache, bug1, true)
365	require.Len(t, repoCache.bugs.cached, 1)
366	require.Equal(t, 1, repoCache.bugs.lru.Len())
367
368	bug2, _, err := repoCache.Bugs().New("title", "message")
369	require.NoError(t, err)
370
371	checkBugPresence(t, repoCache, bug1, true)
372	checkBugPresence(t, repoCache, bug2, true)
373	require.Len(t, repoCache.bugs.cached, 2)
374	require.Equal(t, 2, repoCache.bugs.lru.Len())
375
376	// Number of bugs should not exceed max size of lruCache, oldest one should be evicted
377	bug3, _, err := repoCache.Bugs().New("title", "message")
378	require.NoError(t, err)
379
380	require.Len(t, repoCache.bugs.cached, 2)
381	require.Equal(t, 2, repoCache.bugs.lru.Len())
382	checkBugPresence(t, repoCache, bug1, false)
383	checkBugPresence(t, repoCache, bug2, true)
384	checkBugPresence(t, repoCache, bug3, true)
385
386	// Accessing bug should update position in lruCache, and therefore it should not be evicted
387	repoCache.bugs.lru.Get(bug2.Id())
388	oldestId, _ := repoCache.bugs.lru.GetOldest()
389	require.Equal(t, bug3.Id(), oldestId)
390
391	checkBugPresence(t, repoCache, bug1, false)
392	checkBugPresence(t, repoCache, bug2, true)
393	checkBugPresence(t, repoCache, bug3, true)
394	require.Len(t, repoCache.bugs.cached, 2)
395	require.Equal(t, 2, repoCache.bugs.lru.Len())
396}
397
398func TestLongDescription(t *testing.T) {
399	// See https://github.com/git-bug/git-bug/issues/606
400
401	text := strings.Repeat("x", 65536)
402
403	repo := repository.CreateGoGitTestRepo(t, false)
404
405	backend := createTestRepoCacheNoEvents(t, repo)
406
407	i, err := backend.Identities().New("René Descartes", "rene@descartes.fr")
408	require.NoError(t, err)
409
410	_, _, err = backend.Bugs().NewRaw(i, time.Now().Unix(), text, text, nil, nil)
411	require.NoError(t, err)
412}
413
414func checkBugPresence(t *testing.T, cache *RepoCache, bug *BugCache, presence bool) {
415	t.Helper()
416
417	id := bug.Id()
418	require.Equal(t, presence, cache.bugs.lru.Contains(id))
419	b, ok := cache.bugs.cached[id]
420	require.Equal(t, presence, ok)
421	if ok {
422		require.Equal(t, bug, b)
423	}
424}
425
426func createTestRepoCacheNoEvents(t *testing.T, repo repository.TestedRepo) *RepoCache {
427	t.Helper()
428
429	cache, err := NewRepoCacheNoEvents(repo)
430	require.NoError(t, err)
431
432	t.Cleanup(func() {
433		require.NoError(t, cache.Close())
434	})
435
436	return cache
437}