1// Copyright 2016 Keybase, Inc. All rights reserved. Use of
2// this source code is governed by the included BSD license.
3
4package libkb
5
6import (
7	"bytes"
8	"fmt"
9	"io/ioutil"
10	"os"
11	"path/filepath"
12	"sync"
13	"testing"
14	"time"
15
16	"github.com/stretchr/testify/require"
17)
18
19type teardowner struct {
20	sync.Mutex
21
22	actions  []func()
23	torndown bool
24}
25
26func (td *teardowner) register(teardownAction func()) {
27	td.Lock()
28	defer td.Unlock()
29	if td.torndown {
30		panic("already torndown")
31	}
32	td.actions = append(td.actions, teardownAction)
33}
34
35func (td *teardowner) teardown() {
36	td.Lock()
37	defer td.Unlock()
38	if td.torndown {
39		panic("already torndown")
40	}
41	for _, a := range td.actions {
42		a()
43	}
44}
45
46func createTempLevelDbForTest(tc *TestContext, td *teardowner) (*LevelDb, error) {
47	dir, err := ioutil.TempDir("", "level-db-test-")
48	if err != nil {
49		return nil, err
50	}
51
52	db := NewLevelDb(tc.G, func() string {
53		return filepath.Join(dir, "test.leveldb")
54	})
55
56	td.register(func() {
57		db.Close()
58		os.RemoveAll(dir)
59	})
60
61	return db, nil
62}
63
64func doSomeIO() error {
65	dir, err := ioutil.TempDir("", "level-db-test-")
66	if err != nil {
67		return err
68	}
69	return ioutil.WriteFile(filepath.Join(dir, "some-io"), []byte("O_O"), 0666)
70}
71
72func testLevelDbPut(db *LevelDb) (key DbKey, err error) {
73	key = DbKey{Key: "test-key", Typ: 0}
74	v := []byte{1, 2, 3, 4}
75	if err := db.Put(key, nil, v); err != nil {
76		return DbKey{}, err
77	}
78	if val, found, err := db.Get(key); err != nil {
79		return DbKey{}, err
80	} else if !found {
81		return DbKey{}, fmt.Errorf("stored object was not found by Get")
82	} else if !bytes.Equal(val, v) {
83		return DbKey{}, fmt.Errorf("stored object has incorrect data. expect %v, got %v", v, val)
84	}
85
86	return key, nil
87}
88
89func TestLevelDb(t *testing.T) {
90	var td teardowner
91
92	tests := []struct {
93		name     string
94		testBody func(t *testing.T)
95	}{
96		{
97			name: "simple", testBody: func(t *testing.T) {
98				tc := SetupTest(t, "LevelDb-simple", 0)
99				defer tc.Cleanup()
100				db, err := createTempLevelDbForTest(&tc, &td)
101				require.NoError(t, err)
102
103				key, err := testLevelDbPut(db)
104				require.NoError(t, err)
105
106				err = db.Delete(key)
107				require.NoError(t, err)
108
109				_, found, err := db.Get(key)
110				require.NoError(t, err)
111				require.False(t, found)
112			},
113		},
114		{
115			name: "cleaner", testBody: func(t *testing.T) {
116				tc := SetupTest(t, "LevelDb-cleaner", 0)
117				defer tc.Cleanup()
118				db, err := createTempLevelDbForTest(&tc, &td)
119				require.NoError(t, err)
120
121				key := DbKey{Key: "test-key", Typ: 0}
122				v, err := RandBytes(1024 * 1024)
123				require.NoError(t, err)
124				err = db.Put(key, nil, v)
125				require.NoError(t, err)
126
127				// this key will not be deleted since it is in the permanent
128				// table.
129				require.True(t, IsPermDbKey(DBDiskLRUEntries))
130				permKey := DbKey{Key: "test-key", Typ: DBDiskLRUEntries}
131				err = db.Put(permKey, nil, v)
132				require.NoError(t, err)
133
134				// cleaner will not clean the key since it was recently used
135				err = db.cleaner.clean(true /* force */)
136				require.NoError(t, err)
137				_, found, err := db.Get(key)
138				require.NoError(t, err)
139				require.True(t, found)
140				_, found, err = db.Get(permKey)
141				require.NoError(t, err)
142				require.True(t, found)
143
144				db.cleaner.clearCache()
145				err = db.cleaner.clean(true /* force */)
146				require.NoError(t, err)
147				_, found, err = db.Get(key)
148				require.NoError(t, err)
149				require.False(t, found)
150				_, found, err = db.Get(permKey)
151				require.NoError(t, err)
152				require.True(t, found)
153			},
154		},
155		{
156			name: "concurrent", testBody: func(t *testing.T) {
157				tc := SetupTest(t, "LevelDb-concurrent", 0)
158				defer tc.Cleanup()
159				db, err := createTempLevelDbForTest(&tc, &td)
160				require.NoError(t, err)
161
162				var wg sync.WaitGroup
163				wg.Add(2)
164				// synchronize between two doWhileOpenAndNukeIfCorrupted calls to know
165				// for sure they can happen concurrently.
166				ch := make(chan struct{})
167				go func() {
168					_ = db.doWhileOpenAndNukeIfCorrupted(func() error {
169						defer wg.Done()
170						select {
171						case <-time.After(8 * time.Second):
172							t.Error("doWhileOpenAndNukeIfCorrupted is not concurrent")
173						case <-ch:
174						}
175						return nil
176					})
177				}()
178				go func() {
179					_ = db.doWhileOpenAndNukeIfCorrupted(func() error {
180						defer wg.Done()
181						select {
182						case <-time.After(8 * time.Second):
183							t.Error("doWhileOpenAndNukeIfCorrupted does not support concurrent ops")
184						case ch <- struct{}{}:
185						}
186						return nil
187					})
188				}()
189				wg.Wait()
190			},
191		},
192		{
193			name: "nuke", testBody: func(t *testing.T) {
194				tc := SetupTest(t, "LevelDb-nuke", 0)
195				defer tc.Cleanup()
196				db, err := createTempLevelDbForTest(&tc, &td)
197				require.NoError(t, err)
198
199				key, err := testLevelDbPut(db)
200				require.NoError(t, err)
201
202				_, err = db.Nuke()
203				require.NoError(t, err)
204
205				_, found, err := db.Get(key)
206				require.NoError(t, err)
207				require.False(t, found)
208
209				// make sure db still works after nuking
210				_, err = testLevelDbPut(db)
211				require.NoError(t, err)
212			},
213		},
214		{
215			name: "use-after-close", testBody: func(t *testing.T) {
216				tc := SetupTest(t, "LevelDb-use-after-close", 0)
217				defer tc.Cleanup()
218				db, err := createTempLevelDbForTest(&tc, &td)
219				require.NoError(t, err)
220
221				// not closed yet; should be good
222				_, err = testLevelDbPut(db)
223				require.NoError(t, err)
224
225				err = db.Close()
226				require.NoError(t, err)
227
228				_, err = testLevelDbPut(db)
229				require.Error(t, err)
230
231				err = db.ForceOpen()
232				require.NoError(t, err)
233			},
234		},
235		{
236			name: "transactions", testBody: func(t *testing.T) {
237				tc := SetupTest(t, "LevelDb-transactions", 0)
238				defer tc.Cleanup()
239				db, err := createTempLevelDbForTest(&tc, &td)
240				require.NoError(t, err)
241
242				// have something in the DB
243				key, err := testLevelDbPut(db)
244				require.NoError(t, err)
245
246				var wg sync.WaitGroup
247				wg.Add(2)
248
249				// channels for communicating from first routine to 2nd.
250				chOpen := make(chan struct{})
251				chCommitted := make(chan struct{}, 1)
252
253				go func() {
254					defer wg.Done()
255
256					tr, err := db.OpenTransaction()
257					if err != nil {
258						t.Error(err)
259					}
260
261					select {
262					case <-time.After(8 * time.Second):
263						t.Errorf("timeout")
264					case chOpen <- struct{}{}:
265					}
266
267					if err = tr.Put(key, nil, []byte{41}); err != nil {
268						t.Error(err)
269					}
270
271					// We do some IO here to give Go's runtime a chance to schedule
272					// different routines and channel operations, to *hopefully* make
273					// sure:
274					// 1) The channel operation is done;
275					// 2) If there exists, any broken OpenTransaction() implementation
276					//		that does not block until this transaction finishes, the broken
277					//		OpenTransaction() would have has returned
278					if err = doSomeIO(); err != nil {
279						t.Error(err)
280					}
281
282					// we send to a buffered channel right before Commit() to make sure
283					// the channel is ready to read right after the commit
284					chCommitted <- struct{}{}
285
286					if err = tr.Commit(); err != nil {
287						t.Error(err)
288					}
289
290				}()
291
292				go func() {
293					defer wg.Done()
294
295					// wait until the other transaction has opened
296					select {
297					case <-time.After(8 * time.Second):
298						t.Error("timeout")
299					case <-chOpen:
300					}
301
302					tr, err := db.OpenTransaction()
303					select {
304					case <-chCommitted:
305						// fine
306					default:
307						t.Error("second transaction did not block until first one finished")
308					}
309					if err != nil {
310						t.Error(err)
311					}
312
313					d, found, err := tr.Get(key)
314					if err != nil {
315						t.Error(err)
316					}
317					if !found {
318						t.Errorf("key %v is not found", found)
319					}
320
321					if err = tr.Put(key, nil, []byte{d[0] + 1}); err != nil {
322						t.Error(err)
323					}
324					if err = tr.Commit(); err != nil {
325						t.Error(err)
326					}
327				}()
328
329				wg.Wait()
330
331				data, found, err := db.Get(key)
332				require.NoError(t, err)
333				require.True(t, found)
334				require.Len(t, data, 1)
335				require.EqualValues(t, 42, data[0])
336			},
337		},
338		{
339			name: "transaction-discard", testBody: func(t *testing.T) {
340				tc := SetupTest(t, "LevelDb-transaction-discard", 0)
341				defer tc.Cleanup()
342				db, err := createTempLevelDbForTest(&tc, &td)
343				require.NoError(t, err)
344
345				// have something in the DB
346				key, err := testLevelDbPut(db)
347				require.NoError(t, err)
348
349				tr, err := db.OpenTransaction()
350				require.NoError(t, err)
351				err = tr.Delete(key)
352				require.NoError(t, err)
353				tr.Discard()
354
355				_, found, err := db.Get(key)
356				require.NoError(t, err)
357				require.True(t, found)
358			},
359		},
360	}
361
362	for _, test := range tests {
363		if !t.Run(test.name, test.testBody) {
364			t.Fail() // mark as failed but continue with next test
365		}
366	}
367
368	td.teardown()
369}
370