interface.go raw
1 package ci
2
3 import (
4 "fmt"
5 "os"
6 "reflect"
7
8 "github.com/p9c/p9/pkg/walletdb"
9 )
10
11 // errSubTestFail is used to signal that a sub test returned false.
12 var errSubTestFail = fmt.Errorf("sub test failure")
13
14 // testContext is used to store context information about a running test which is passed into helper functions.
15 type testContext struct {
16 t Tester
17 db walletdb.DB
18 bucketDepth int
19 isWritable bool
20 }
21
22 // rollbackValues returns a copy of the provided map with all values set to an empty string. This is used to test that
23 // values are properly rolled back.
24 func rollbackValues(
25 values map[string]string,
26 ) map[string]string {
27 retMap := make(map[string]string, len(values))
28 for k := range values {
29 retMap[k] = ""
30 }
31 return retMap
32 }
33
34 // testGetValues checks that all of the provided key/value pairs can be retrieved from the database and the retrieved
35 // values match the provided values.
36 func testGetValues(
37 tc *testContext, bucket walletdb.ReadBucket, values map[string]string,
38 ) bool {
39 for k, v := range values {
40 var vBytes []byte
41 if v != "" {
42 vBytes = []byte(v)
43 }
44 gotValue := bucket.Get([]byte(k))
45 if !reflect.DeepEqual(gotValue, vBytes) {
46 tc.t.Errorf("Get: unexpected value - got %s, want %s",
47 gotValue, vBytes,
48 )
49 return false
50 }
51 }
52 return true
53 }
54
55 // testPutValues stores all of the provided key/value pairs in the provided bucket while checking for errors.
56 func testPutValues(
57 tc *testContext, bucket walletdb.ReadWriteBucket, values map[string]string,
58 ) bool {
59 for k, v := range values {
60 var vBytes []byte
61 if v != "" {
62 vBytes = []byte(v)
63 }
64 if e := bucket.Put([]byte(k), vBytes); E.Chk(e) {
65 tc.t.Errorf("Put: unexpected error: %v", e)
66 return false
67 }
68 }
69 return true
70 }
71
72 // testDeleteValues removes all of the provided key/value pairs from the provided bucket.
73 func testDeleteValues(
74 tc *testContext, bucket walletdb.ReadWriteBucket, values map[string]string,
75 ) bool {
76 for k := range values {
77 if e := bucket.Delete([]byte(k)); E.Chk(e) {
78 tc.t.Errorf("Delete: unexpected error: %v", e)
79 return false
80 }
81 }
82 return true
83 }
84
85 // testNestedReadWriteBucket reruns the testBucketInterface against a nested bucket along with a counter to only test a
86 // couple of level deep.
87 func testNestedReadWriteBucket(
88 tc *testContext, testBucket walletdb.ReadWriteBucket,
89 ) bool {
90 // Don't go more than 2 nested level deep.
91 if tc.bucketDepth > 1 {
92 return true
93 }
94 tc.bucketDepth++
95 defer func() {
96 tc.bucketDepth--
97 }()
98 return testReadWriteBucketInterface(tc, testBucket)
99 }
100
101 // testReadWriteBucketInterface ensures the bucket interface is working properly by exercising all of its functions.
102 func testReadWriteBucketInterface(
103 tc *testContext, bucket walletdb.ReadWriteBucket,
104 ) bool {
105 // keyValues holds the keys and values to use when putting values into the bucket.
106 var keyValues = map[string]string{
107 "bucketkey1": "foo1",
108 "bucketkey2": "foo2",
109 "bucketkey3": "foo3",
110 }
111 if !testPutValues(tc, bucket, keyValues) {
112 return false
113 }
114 if !testGetValues(tc, bucket, keyValues) {
115 return false
116 }
117 // Iterate all of the keys using ForEach while making sure the stored values are the expected values.
118 keysFound := make(map[string]struct{}, len(keyValues))
119 e := bucket.ForEach(func(k, v []byte) (e error) {
120 ks := string(k)
121 wantV, ok := keyValues[ks]
122 if !ok {
123 return fmt.Errorf("ForEach: key '%s' should "+
124 "exist", ks,
125 )
126 }
127 if !reflect.DeepEqual(v, []byte(wantV)) {
128 return fmt.Errorf("ForEach: value for key '%s' "+
129 "does not match - got %s, want %s",
130 ks, v, wantV,
131 )
132 }
133 keysFound[ks] = struct{}{}
134 return nil
135 },
136 )
137 if e != nil {
138 tc.t.Errorf("%v", e)
139 return false
140 }
141 // Ensure all keys were iterated.
142 for k := range keyValues {
143 if _, ok := keysFound[k]; !ok {
144 tc.t.Errorf("ForEach: key '%s' was not iterated "+
145 "when it should have been", k,
146 )
147 return false
148 }
149 }
150 // Delete the keys and ensure they were deleted.
151 if !testDeleteValues(tc, bucket, keyValues) {
152 return false
153 }
154 if !testGetValues(tc, bucket, rollbackValues(keyValues)) {
155 return false
156 }
157 // Ensure creating a new bucket works as expected.
158 testBucketName := []byte("testbucket")
159 testBucket, e := bucket.CreateBucket(testBucketName)
160 if e != nil {
161 tc.t.Errorf("CreateBucket: unexpected error: %v", e)
162 return false
163 }
164 if !testNestedReadWriteBucket(tc, testBucket) {
165 return false
166 }
167 // Ensure creating a bucket that already exists fails with the expected error.
168 wantErr := walletdb.ErrBucketExists
169 if _, e = bucket.CreateBucket(testBucketName); e != wantErr {
170 tc.t.Errorf("CreateBucket: unexpected error - got %v, "+
171 "want %v", e, wantErr,
172 )
173 return false
174 }
175 // Ensure CreateBucketIfNotExists returns an existing bucket.
176 testBucket, e = bucket.CreateBucketIfNotExists(testBucketName)
177 if e != nil {
178 tc.t.Errorf("CreateBucketIfNotExists: unexpected "+
179 "error: %v", e,
180 )
181 return false
182 }
183 if !testNestedReadWriteBucket(tc, testBucket) {
184 return false
185 }
186 // Ensure retrieving and existing bucket works as expected.
187 testBucket = bucket.NestedReadWriteBucket(testBucketName)
188 if !testNestedReadWriteBucket(tc, testBucket) {
189 return false
190 }
191 // Ensure deleting a bucket works as intended.
192 if e = bucket.DeleteNestedBucket(testBucketName); E.Chk(e) {
193 tc.t.Errorf("DeleteNestedBucket: unexpected error: %v", e)
194 return false
195 }
196 if b := bucket.NestedReadWriteBucket(testBucketName); b != nil {
197 tc.t.Errorf("DeleteNestedBucket: bucket '%s' still exists",
198 testBucketName,
199 )
200 return false
201 }
202 // Ensure deleting a bucket that doesn't exist returns the expected error.
203 wantErr = walletdb.ErrBucketNotFound
204 if e = bucket.DeleteNestedBucket(testBucketName); e != wantErr {
205 tc.t.Errorf("DeleteNestedBucket: unexpected error - got %v, "+
206 "want %v", e, wantErr,
207 )
208 return false
209 }
210 // Ensure CreateBucketIfNotExists creates a new bucket when it doesn't already exist.
211 testBucket, e = bucket.CreateBucketIfNotExists(testBucketName)
212 if e != nil {
213 tc.t.Errorf("CreateBucketIfNotExists: unexpected "+
214 "error: %v", e,
215 )
216 return false
217 }
218 if !testNestedReadWriteBucket(tc, testBucket) {
219 return false
220 }
221 // Delete the test bucket to avoid leaving it around for future calls.
222 if e := bucket.DeleteNestedBucket(testBucketName); E.Chk(e) {
223 tc.t.Errorf("DeleteNestedBucket: unexpected error: %v", e)
224 return false
225 }
226 if b := bucket.NestedReadWriteBucket(testBucketName); b != nil {
227 tc.t.Errorf("DeleteNestedBucket: bucket '%s' still exists",
228 testBucketName,
229 )
230 return false
231 }
232 return true
233 }
234
235 // testManualTxInterface ensures that manual transactions work as expected.
236 func testManualTxInterface(
237 tc *testContext, bucketKey []byte,
238 ) bool {
239 db := tc.db
240 // populateValues tests that populating values works as expected.
241 //
242 // When the writable flag is false, a read-only tranasction is created, standard bucket tests for read-only
243 // transactions are performed, and the Commit function is checked to ensure it fails as expected.
244 //
245 // Otherwise, a read-write transaction is created, the values are written, standard bucket tests for read-write
246 // transactions are performed, and then the transaction is either commited or rolled back depending on the flag.
247 populateValues := func(writable, rollback bool, putValues map[string]string) bool {
248 var dbtx walletdb.ReadTx
249 var rootBucket walletdb.ReadBucket
250 var e error
251 if writable {
252 dbtx, e = db.BeginReadWriteTx()
253 if e != nil {
254 tc.t.Errorf("BeginReadWriteTx: unexpected error %v", e)
255 return false
256 }
257 rootBucket = dbtx.(walletdb.ReadWriteTx).ReadWriteBucket(bucketKey)
258 } else {
259 dbtx, e = db.BeginReadTx()
260 if e != nil {
261 tc.t.Errorf("BeginReadTx: unexpected error %v", e)
262 return false
263 }
264 rootBucket = dbtx.ReadBucket(bucketKey)
265 }
266 if rootBucket == nil {
267 tc.t.Errorf("ReadWriteBucket/ReadBucket: unexpected nil root bucket")
268 _ = dbtx.Rollback()
269 return false
270 }
271 if writable {
272 tc.isWritable = writable
273 if !testReadWriteBucketInterface(tc, rootBucket.(walletdb.ReadWriteBucket)) {
274 _ = dbtx.Rollback()
275 return false
276 }
277 }
278 if !writable {
279 // Rollback the transaction.
280 if e := dbtx.Rollback(); E.Chk(e) {
281 tc.t.Errorf("Commit: unexpected error %v", e)
282 return false
283 }
284 } else {
285 rootBucket := rootBucket.(walletdb.ReadWriteBucket)
286 if !testPutValues(tc, rootBucket, putValues) {
287 return false
288 }
289 if rollback {
290 // Rollback the transaction.
291 if e := dbtx.Rollback(); E.Chk(e) {
292 tc.t.Errorf("Rollback: unexpected "+
293 "error %v", e,
294 )
295 return false
296 }
297 } else {
298 // The commit should succeed.
299 if e := dbtx.(walletdb.ReadWriteTx).Commit(); E.Chk(e) {
300 tc.t.Errorf("Commit: unexpected error "+
301 "%v", e,
302 )
303 return false
304 }
305 }
306 }
307 return true
308 }
309 // checkValues starts a read-only transaction and checks that all of the key/value pairs specified in the
310 // expectedValues parameter match what's in the database.
311 checkValues := func(expectedValues map[string]string) bool {
312 // Begin another read-only transaction to ensure...
313 dbtx, e := db.BeginReadTx()
314 if e != nil {
315 tc.t.Errorf("BeginReadTx: unexpected error %v", e)
316 return false
317 }
318 rootBucket := dbtx.ReadBucket(bucketKey)
319 if rootBucket == nil {
320 tc.t.Errorf("ReadBucket: unexpected nil root bucket")
321 _ = dbtx.Rollback()
322 return false
323 }
324 if !testGetValues(tc, rootBucket, expectedValues) {
325 _ = dbtx.Rollback()
326 return false
327 }
328 // Rollback the read-only transaction.
329 if e := dbtx.Rollback(); E.Chk(e) {
330 tc.t.Errorf("Commit: unexpected error %v", e)
331 return false
332 }
333 return true
334 }
335 // deleteValues starts a read-write transaction and deletes the keys in the passed key/value pairs.
336 deleteValues := func(values map[string]string) bool {
337 dbtx, e := db.BeginReadWriteTx()
338 if e != nil {
339 tc.t.Errorf("BeginReadWriteTx: unexpected error %v", e)
340 _ = dbtx.Rollback()
341 return false
342 }
343 rootBucket := dbtx.ReadWriteBucket(bucketKey)
344 if rootBucket == nil {
345 tc.t.Errorf("RootBucket: unexpected nil root bucket")
346 _ = dbtx.Rollback()
347 return false
348 }
349 // Delete the keys and ensure they were deleted.
350 if !testDeleteValues(tc, rootBucket, values) {
351 _ = dbtx.Rollback()
352 return false
353 }
354 if !testGetValues(tc, rootBucket, rollbackValues(values)) {
355 _ = dbtx.Rollback()
356 return false
357 }
358 // Commit the changes and ensure it was successful.
359 if e := dbtx.Commit(); E.Chk(e) {
360 tc.t.Errorf("Commit: unexpected error %v", e)
361 return false
362 }
363 return true
364 }
365 // keyValues holds the keys and values to use when putting values into a bucket.
366 var keyValues = map[string]string{
367 "umtxkey1": "foo1",
368 "umtxkey2": "foo2",
369 "umtxkey3": "foo3",
370 }
371 // Ensure that attempting populating the values using a read-only transaction fails as expected.
372 if !populateValues(false, true, keyValues) {
373 return false
374 }
375 if !checkValues(rollbackValues(keyValues)) {
376 return false
377 }
378 // Ensure that attempting populating the values using a read-write transaction and then rolling it back yields the
379 // expected values.
380 if !populateValues(true, true, keyValues) {
381 return false
382 }
383 if !checkValues(rollbackValues(keyValues)) {
384 return false
385 }
386 // Ensure that attempting populating the values using a read-write transaction and then committing it stores the
387 // expected values.
388 if !populateValues(true, false, keyValues) {
389 return false
390 }
391 if !checkValues(keyValues) {
392 return false
393 }
394 // Clean up the keys.
395 if !deleteValues(keyValues) {
396 return false
397 }
398 return true
399 }
400
401 // testNamespaceAndTxInterfaces creates a namespace using the provided key and tests all facets of it interface as well
402 // as transaction and bucket interfaces under it.
403 func testNamespaceAndTxInterfaces(
404 tc *testContext, namespaceKey string,
405 ) bool {
406 namespaceKeyBytes := []byte(namespaceKey)
407 e := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) (e error) {
408 _, e = tx.CreateTopLevelBucket(namespaceKeyBytes)
409 return e
410 },
411 )
412 if e != nil {
413 tc.t.Errorf("CreateTopLevelBucket: unexpected error: %v", e)
414 return false
415 }
416 defer func() {
417 // Remove the namespace now that the tests are done for it.
418 e = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) (e error) {
419 return tx.DeleteTopLevelBucket(namespaceKeyBytes)
420 },
421 )
422 if e != nil {
423 tc.t.Errorf("DeleteTopLevelBucket: unexpected error: %v", e)
424 return
425 }
426 }()
427 if !testManualTxInterface(tc, namespaceKeyBytes) {
428 return false
429 }
430 // keyValues holds the keys and values to use when putting values into a bucket.
431 var keyValues = map[string]string{
432 "mtxkey1": "foo1",
433 "mtxkey2": "foo2",
434 "mtxkey3": "foo3",
435 }
436 // Test the bucket interface via a managed read-only transaction.
437 e = walletdb.View(tc.db, func(tx walletdb.ReadTx) (e error) {
438 rootBucket := tx.ReadBucket(namespaceKeyBytes)
439 if rootBucket == nil {
440 return fmt.Errorf("ReadBucket: unexpected nil root bucket")
441 }
442 return nil
443 },
444 )
445 if e != nil {
446 if e != errSubTestFail {
447 tc.t.Errorf("%v", e)
448 }
449 return false
450 }
451 // Test the bucket interface via a managed read-write transaction. Also, put a series of values and force a rollback
452 // so the following code can ensure the values were not stored.
453 forceRollbackError := fmt.Errorf("force rollback")
454 e = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) (e error) {
455 rootBucket := tx.ReadWriteBucket(namespaceKeyBytes)
456 if rootBucket == nil {
457 return fmt.Errorf("ReadWriteBucket: unexpected nil root bucket")
458 }
459 tc.isWritable = true
460 if !testReadWriteBucketInterface(tc, rootBucket) {
461 return errSubTestFail
462 }
463 if !testPutValues(tc, rootBucket, keyValues) {
464 return errSubTestFail
465 }
466 // Return an error to force a rollback.
467 return forceRollbackError
468 },
469 )
470 if e != forceRollbackError {
471 if e == errSubTestFail {
472 return false
473 }
474 tc.t.Errorf("Update: inner function error not returned - got "+
475 "%v, want %v", e, forceRollbackError,
476 )
477 return false
478 }
479 // Ensure the values that should have not been stored due to the forced rollback above were not actually stored.
480 e = walletdb.View(tc.db, func(tx walletdb.ReadTx) (e error) {
481 rootBucket := tx.ReadBucket(namespaceKeyBytes)
482 if rootBucket == nil {
483 return fmt.Errorf("ReadBucket: unexpected nil root bucket")
484 }
485 if !testGetValues(tc, rootBucket, rollbackValues(keyValues)) {
486 return errSubTestFail
487 }
488 return nil
489 },
490 )
491 if e != nil {
492 if e != errSubTestFail {
493 tc.t.Errorf("%v", e)
494 }
495 return false
496 }
497 // Store a series of values via a managed read-write transaction.
498 e = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) (e error) {
499 rootBucket := tx.ReadWriteBucket(namespaceKeyBytes)
500 if rootBucket == nil {
501 return fmt.Errorf("ReadWriteBucket: unexpected nil root bucket")
502 }
503 if !testPutValues(tc, rootBucket, keyValues) {
504 return errSubTestFail
505 }
506 return nil
507 },
508 )
509 if e != nil {
510 if e != errSubTestFail {
511 tc.t.Errorf("%v", e)
512 }
513 return false
514 }
515 // Ensure the values stored above were committed as expected.
516 e = walletdb.View(tc.db, func(tx walletdb.ReadTx) (e error) {
517 rootBucket := tx.ReadBucket(namespaceKeyBytes)
518 if rootBucket == nil {
519 return fmt.Errorf("ReadBucket: unexpected nil root bucket")
520 }
521 if !testGetValues(tc, rootBucket, keyValues) {
522 return errSubTestFail
523 }
524 return nil
525 },
526 )
527 if e != nil {
528 E.Ln(e)
529 if e != errSubTestFail {
530 tc.t.Errorf("%v", e)
531 }
532 return false
533 }
534 // Clean up the values stored above in a managed read-write transaction.
535 e = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) (e error) {
536 rootBucket := tx.ReadWriteBucket(namespaceKeyBytes)
537 if rootBucket == nil {
538 return fmt.Errorf("ReadWriteBucket: unexpected nil root bucket")
539 }
540 if !testDeleteValues(tc, rootBucket, keyValues) {
541 return errSubTestFail
542 }
543 return nil
544 },
545 )
546 if e != nil {
547 E.Ln(e)
548 if e != errSubTestFail {
549 tc.t.Errorf("%v", e)
550 }
551 return false
552 }
553 return true
554 }
555
556 // testAdditionalErrors performs some tests for error cases not covered elsewhere in the tests and therefore improves
557 // negative test coverage.
558 func testAdditionalErrors(
559 tc *testContext,
560 ) bool {
561 ns3Key := []byte("ns3")
562 e := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) (e error) {
563 // Create a new namespace
564 rootBucket, e := tx.CreateTopLevelBucket(ns3Key)
565 if e != nil {
566 return fmt.Errorf("CreateTopLevelBucket: unexpected error: %v", e)
567 }
568 // Ensure CreateBucket returns the expected error when no bucket key is specified.
569 wantErr := walletdb.ErrBucketNameRequired
570 if _, e = rootBucket.CreateBucket(nil); e != wantErr {
571 return fmt.Errorf("CreateBucket: unexpected error - "+
572 "got %v, want %v", e, wantErr,
573 )
574 }
575 // Ensure DeleteNestedBucket returns the expected error when no bucket key is specified.
576 wantErr = walletdb.ErrIncompatibleValue
577 if e := rootBucket.DeleteNestedBucket(nil); e != wantErr {
578 return fmt.Errorf("DeleteNestedBucket: unexpected error - "+
579 "got %v, want %v", e, wantErr,
580 )
581 }
582 // Ensure Put returns the expected error when no key is specified.
583 wantErr = walletdb.ErrKeyRequired
584 if e := rootBucket.Put(nil, nil); e != wantErr {
585 return fmt.Errorf("put: unexpected error - got %v, want %v", e, wantErr)
586 }
587 return nil
588 },
589 )
590 if e != nil {
591 if e != errSubTestFail {
592 tc.t.Errorf("%v", e)
593 }
594 return false
595 }
596 // Ensure that attempting to rollback or commit a transaction that is already closed returns the expected error.
597 tx, e := tc.db.BeginReadWriteTx()
598 if e != nil {
599 tc.t.Errorf("Begin: unexpected error: %v", e)
600 return false
601 }
602 if e := tx.Rollback(); E.Chk(e) {
603 tc.t.Errorf("Rollback: unexpected error: %v", e)
604 return false
605 }
606 wantErr := walletdb.ErrTxClosed
607 if e := tx.Rollback(); e != wantErr {
608 tc.t.Errorf("Rollback: unexpected error - got %v, want %v", e,
609 wantErr,
610 )
611 return false
612 }
613 if e := tx.Commit(); e != wantErr {
614 tc.t.Errorf("Commit: unexpected error - got %v, want %v", e,
615 wantErr,
616 )
617 return false
618 }
619 return true
620 }
621
622 // TestInterface performs all interfaces tests for this database driver.
623 func TestInterface(
624 t Tester, dbType, dbPath string,
625 ) {
626 db, e := walletdb.Create(dbType, dbPath)
627 if e != nil {
628 t.Errorf("Failed to create test database (%s) %v", dbType, e)
629 return
630 }
631 defer func() {
632 if e := os.Remove(dbPath); E.Chk(e) {
633 }
634 }()
635 defer func() {
636 if e := db.Close(); E.Chk(e) {
637 }
638 }()
639 // Run all of the interface tests against the database. Create a test context to pass around.
640 context := testContext{t: t, db: db}
641 // Create a namespace and test the interface for it.
642 if !testNamespaceAndTxInterfaces(&context, "ns1") {
643 return
644 }
645 // Create a second namespace and test the interface for it.
646 if !testNamespaceAndTxInterfaces(&context, "ns2") {
647 return
648 }
649 // Chk a few more error conditions not covered elsewhere.
650 if !testAdditionalErrors(&context) {
651 return
652 }
653 }
654