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}