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