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