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}