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