repo_cache_test.go

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