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}