github.com/petermattis/pebble@v0.0.0-20190905164901-ab51a2166067/sstable/table_test.go (about) 1 // Copyright 2011 The LevelDB-Go and Pebble Authors. All rights reserved. Use 2 // of this source code is governed by a BSD-style license that can be found in 3 // the LICENSE file. 4 5 package sstable 6 7 import ( 8 "bufio" 9 "bytes" 10 "encoding/binary" 11 "fmt" 12 "io" 13 "math" 14 "os" 15 "path/filepath" 16 "sort" 17 "strings" 18 "testing" 19 "time" 20 21 "github.com/kr/pretty" 22 "github.com/petermattis/pebble/bloom" 23 "github.com/petermattis/pebble/internal/base" 24 "github.com/petermattis/pebble/vfs" 25 "github.com/stretchr/testify/require" 26 "golang.org/x/exp/rand" 27 ) 28 29 // nonsenseWords are words that aren't in testdata/h.txt. 30 var nonsenseWords = []string{ 31 // Edge cases. 32 "", 33 "\x00", 34 "\xff", 35 "`", 36 "a\x00", 37 "aaaaaa", 38 "pol\x00nius", 39 "youth\x00", 40 "youti", 41 "zzzzzz", 42 // Capitalized versions of actual words in testdata/h.txt. 43 "A", 44 "Hamlet", 45 "thEE", 46 "YOUTH", 47 // The following were generated by http://soybomb.com/tricks/words/ 48 "pectures", 49 "exectly", 50 "tricatrippian", 51 "recens", 52 "whiratroce", 53 "troped", 54 "balmous", 55 "droppewry", 56 "toilizing", 57 "crocias", 58 "eathrass", 59 "cheakden", 60 "speablett", 61 "skirinies", 62 "prefing", 63 "bonufacision", 64 } 65 66 var ( 67 wordCount = map[string]string{} 68 minWord = "" 69 maxWord = "" 70 ) 71 72 func init() { 73 f, err := os.Open(filepath.FromSlash("testdata/h.txt")) 74 if err != nil { 75 panic(err) 76 } 77 defer f.Close() 78 r := bufio.NewReader(f) 79 80 for first := true; ; { 81 s, err := r.ReadBytes('\n') 82 if err == io.EOF { 83 break 84 } 85 if err != nil { 86 panic(err) 87 } 88 k := strings.TrimSpace(string(s[8:])) 89 v := strings.TrimSpace(string(s[:8])) 90 wordCount[k] = v 91 92 if first { 93 first = false 94 minWord = k 95 maxWord = k 96 continue 97 } 98 if minWord > k { 99 minWord = k 100 } 101 if maxWord < k { 102 maxWord = k 103 } 104 } 105 106 if len(wordCount) != 1710 { 107 panic(fmt.Sprintf("h.txt entry count: got %d, want %d", len(wordCount), 1710)) 108 } 109 110 for _, s := range nonsenseWords { 111 if _, ok := wordCount[s]; ok { 112 panic(fmt.Sprintf("nonsense word %q was in h.txt", s)) 113 } 114 } 115 } 116 117 func check(f vfs.File, comparer *Comparer, fp FilterPolicy) error { 118 r, err := NewReader(f, 0, 0, &Options{ 119 Comparer: comparer, 120 Levels: []TableOptions{{ 121 FilterPolicy: fp, 122 }}, 123 }) 124 if err != nil { 125 return err 126 } 127 128 // Check that each key/value pair in wordCount is also in the table. 129 words := make([]string, 0, len(wordCount)) 130 for k, v := range wordCount { 131 words = append(words, k) 132 // Check using Get. 133 if v1, err := r.get([]byte(k)); string(v1) != string(v) || err != nil { 134 return fmt.Errorf("Get %q: got (%q, %v), want (%q, %v)", k, v1, err, v, error(nil)) 135 } else if len(v1) != cap(v1) { 136 return fmt.Errorf("Get %q: len(v1)=%d, cap(v1)=%d", k, len(v1), cap(v1)) 137 } 138 139 // Check using SeekGE. 140 i := iterAdapter{r.NewIter(nil /* lower */, nil /* upper */)} 141 if !i.SeekGE([]byte(k)) || string(i.Key().UserKey) != k { 142 return fmt.Errorf("Find %q: key was not in the table", k) 143 } 144 if k1 := i.Key().UserKey; len(k1) != cap(k1) { 145 return fmt.Errorf("Find %q: len(k1)=%d, cap(k1)=%d", k, len(k1), cap(k1)) 146 } 147 if string(i.Value()) != v { 148 return fmt.Errorf("Find %q: got value %q, want %q", k, i.Value(), v) 149 } 150 if v1 := i.Value(); len(v1) != cap(v1) { 151 return fmt.Errorf("Find %q: len(v1)=%d, cap(v1)=%d", k, len(v1), cap(v1)) 152 } 153 154 // Check using SeekLT. 155 if !i.SeekLT([]byte(k)) { 156 i.First() 157 } else { 158 i.Next() 159 } 160 if string(i.Key().UserKey) != k { 161 return fmt.Errorf("Find %q: key was not in the table", k) 162 } 163 if k1 := i.Key().UserKey; len(k1) != cap(k1) { 164 return fmt.Errorf("Find %q: len(k1)=%d, cap(k1)=%d", k, len(k1), cap(k1)) 165 } 166 if string(i.Value()) != v { 167 return fmt.Errorf("Find %q: got value %q, want %q", k, i.Value(), v) 168 } 169 if v1 := i.Value(); len(v1) != cap(v1) { 170 return fmt.Errorf("Find %q: len(v1)=%d, cap(v1)=%d", k, len(v1), cap(v1)) 171 } 172 173 if err := i.Close(); err != nil { 174 return err 175 } 176 } 177 178 // Check that nonsense words are not in the table. 179 for _, s := range nonsenseWords { 180 // Check using Get. 181 if _, err := r.get([]byte(s)); err != base.ErrNotFound { 182 return fmt.Errorf("Get %q: got %v, want ErrNotFound", s, err) 183 } 184 185 // Check using Find. 186 i := iterAdapter{r.NewIter(nil /* lower */, nil /* upper */)} 187 if i.SeekGE([]byte(s)) && s == string(i.Key().UserKey) { 188 return fmt.Errorf("Find %q: unexpectedly found key in the table", s) 189 } 190 if err := i.Close(); err != nil { 191 return err 192 } 193 } 194 195 // Check that the number of keys >= a given start key matches the expected number. 196 var countTests = []struct { 197 count int 198 start string 199 }{ 200 // cat h.txt | cut -c 9- | wc -l gives 1710. 201 {1710, ""}, 202 // cat h.txt | cut -c 9- | grep -v "^[a-b]" | wc -l gives 1522. 203 {1522, "c"}, 204 // cat h.txt | cut -c 9- | grep -v "^[a-j]" | wc -l gives 940. 205 {940, "k"}, 206 // cat h.txt | cut -c 9- | grep -v "^[a-x]" | wc -l gives 12. 207 {12, "y"}, 208 // cat h.txt | cut -c 9- | grep -v "^[a-z]" | wc -l gives 0. 209 {0, "~"}, 210 } 211 for _, ct := range countTests { 212 n, i := 0, iterAdapter{r.NewIter(nil /* lower */, nil /* upper */)} 213 for valid := i.SeekGE([]byte(ct.start)); valid; valid = i.Next() { 214 n++ 215 } 216 if n != ct.count { 217 return fmt.Errorf("count %q: got %d, want %d", ct.start, n, ct.count) 218 } 219 n = 0 220 for valid := i.Last(); valid; valid = i.Prev() { 221 if bytes.Compare(i.Key().UserKey, []byte(ct.start)) < 0 { 222 break 223 } 224 n++ 225 } 226 if n != ct.count { 227 return fmt.Errorf("count %q: got %d, want %d", ct.start, n, ct.count) 228 } 229 if err := i.Close(); err != nil { 230 return err 231 } 232 } 233 234 // Check lower/upper bounds behavior. Randomly choose a lower and upper bound 235 // and then guarantee that iteration finds the expected number if entries. 236 rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) 237 sort.Strings(words) 238 for i := 0; i < 10; i++ { 239 lowerIdx := -1 240 upperIdx := len(words) 241 if rng.Intn(5) != 0 { 242 lowerIdx = rng.Intn(len(words)) 243 } 244 if rng.Intn(5) != 0 { 245 upperIdx = rng.Intn(len(words)) 246 } 247 if lowerIdx > upperIdx { 248 lowerIdx, upperIdx = upperIdx, lowerIdx 249 } 250 251 var lower, upper []byte 252 if lowerIdx >= 0 { 253 lower = []byte(words[lowerIdx]) 254 } else { 255 lowerIdx = 0 256 } 257 if upperIdx < len(words) { 258 upper = []byte(words[upperIdx]) 259 } 260 261 i := iterAdapter{r.NewIter(lower, upper)} 262 263 { 264 // NB: the semantics of First are that it starts iteration from the 265 // beginning, not respecting the lower bound. 266 n := 0 267 for valid := i.First(); valid; valid = i.Next() { 268 n++ 269 } 270 if expected := upperIdx; expected != n { 271 return fmt.Errorf("expected %d, but found %d", expected, n) 272 } 273 } 274 275 { 276 // NB: the semantics of Last are that it starts iteration from the end, not 277 // respecting the upper bound. 278 n := 0 279 for valid := i.Last(); valid; valid = i.Prev() { 280 n++ 281 } 282 if expected := len(words) - lowerIdx; expected != n { 283 return fmt.Errorf("expected %d, but found %d", expected, n) 284 } 285 } 286 287 if lower != nil { 288 n := 0 289 for valid := i.SeekGE(lower); valid; valid = i.Next() { 290 n++ 291 } 292 if expected := upperIdx - lowerIdx; expected != n { 293 return fmt.Errorf("expected %d, but found %d", expected, n) 294 } 295 } 296 297 if upper != nil { 298 n := 0 299 for valid := i.SeekLT(upper); valid; valid = i.Prev() { 300 n++ 301 } 302 if expected := upperIdx - lowerIdx; expected != n { 303 return fmt.Errorf("expected %d, but found %d", expected, n) 304 } 305 } 306 307 if err := i.Close(); err != nil { 308 return err 309 } 310 } 311 312 return r.Close() 313 } 314 315 var ( 316 memFileSystem = vfs.NewMem() 317 tmpFileCount int 318 ) 319 320 func build( 321 compression Compression, 322 fp FilterPolicy, 323 ftype FilterType, 324 comparer *Comparer, 325 propCollector func() TablePropertyCollector, 326 blockSize int, 327 indexBlockSize int, 328 ) (vfs.File, error) { 329 // Create a sorted list of wordCount's keys. 330 keys := make([]string, len(wordCount)) 331 i := 0 332 for k := range wordCount { 333 keys[i] = k 334 i++ 335 } 336 sort.Strings(keys) 337 338 // Write the key/value pairs to a new table, in increasing key order. 339 filename := fmt.Sprintf("/tmp%d", tmpFileCount) 340 f0, err := memFileSystem.Create(filename) 341 if err != nil { 342 return nil, err 343 } 344 defer f0.Close() 345 tmpFileCount++ 346 347 opts := &Options{ 348 Merger: &base.Merger{ 349 Name: "nullptr", 350 }, 351 Comparer: comparer, 352 } 353 if propCollector != nil { 354 opts.TablePropertyCollectors = append(opts.TablePropertyCollectors, propCollector) 355 } 356 357 tableOpts := TableOptions{ 358 BlockSize: blockSize, 359 Compression: compression, 360 FilterPolicy: fp, 361 FilterType: ftype, 362 IndexBlockSize: indexBlockSize, 363 } 364 365 w := NewWriter(f0, opts, tableOpts) 366 // Use rangeDelV1Format for testing byte equality with RocksDB. 367 w.rangeDelV1Format = true 368 var rangeDelLength int 369 var rangeDelCounter int 370 var rangeDelStart InternalKey 371 for i, k := range keys { 372 v := wordCount[k] 373 ikey := base.MakeInternalKey([]byte(k), 0, InternalKeyKindSet) 374 if err := w.Add(ikey, []byte(v)); err != nil { 375 return nil, err 376 } 377 // This mirrors the logic in `make-table.cc`. It adds range deletions of 378 // increasing length for every 100 keys added. 379 if i % 100 == 0 { 380 rangeDelStart = ikey.Clone() 381 rangeDelCounter = 0 382 rangeDelLength++ 383 } 384 rangeDelCounter++ 385 386 if rangeDelCounter == rangeDelLength { 387 if err := w.DeleteRange(rangeDelStart.UserKey, ikey.UserKey); err != nil { 388 return nil, err 389 } 390 } 391 } 392 if err := w.Close(); err != nil { 393 return nil, err 394 } 395 396 // Re-open that filename for reading. 397 f1, err := memFileSystem.Open(filename) 398 if err != nil { 399 return nil, err 400 } 401 return f1, nil 402 } 403 404 func testReader(t *testing.T, filename string, comparer *Comparer, fp FilterPolicy) { 405 // Check that we can read a pre-made table. 406 f, err := os.Open(filepath.FromSlash("testdata/" + filename)) 407 if err != nil { 408 t.Error(err) 409 return 410 } 411 err = check(f, comparer, fp) 412 if err != nil { 413 t.Error(err) 414 return 415 } 416 } 417 418 func TestReaderLevelDB(t *testing.T) { testReader(t, "h.ldb", nil, nil) } 419 func TestReaderDefaultCompression(t *testing.T) { testReader(t, "h.sst", nil, nil) } 420 func TestReaderNoCompression(t *testing.T) { testReader(t, "h.no-compression.sst", nil, nil) } 421 func TestReaderBlockBloomIgnored(t *testing.T) { 422 testReader(t, "h.block-bloom.no-compression.sst", nil, nil) 423 } 424 func TestReaderTableBloomIgnored(t *testing.T) { 425 testReader(t, "h.table-bloom.no-compression.sst", nil, nil) 426 } 427 428 func TestReaderBloomUsed(t *testing.T) { 429 // wantActualNegatives is the minimum number of nonsense words (i.e. false 430 // positives or true negatives) to run through our filter. Some nonsense 431 // words might be rejected even before the filtering step, if they are out 432 // of the [minWord, maxWord] range of keys in the table. 433 wantActualNegatives := 0 434 for _, s := range nonsenseWords { 435 if minWord < s && s < maxWord { 436 wantActualNegatives++ 437 } 438 } 439 440 files := []struct { 441 path string 442 comparer *Comparer 443 }{ 444 {"h.table-bloom.no-compression.sst", nil}, 445 {"h.table-bloom.no-compression.prefix_extractor.no_whole_key_filter.sst", fixtureComparer}, 446 } 447 for _, tc := range files { 448 t.Run(tc.path, func(t *testing.T) { 449 for _, degenerate := range []bool{false, true} { 450 t.Run(fmt.Sprintf("degenerate=%t", degenerate), func(t *testing.T) { 451 c := &countingFilterPolicy{ 452 FilterPolicy: bloom.FilterPolicy(10), 453 degenerate: degenerate, 454 } 455 testReader(t, tc.path, tc.comparer, c) 456 457 if c.truePositives != len(wordCount) { 458 t.Errorf("degenerate=%t: true positives: got %d, want %d", degenerate, c.truePositives, len(wordCount)) 459 } 460 if c.falseNegatives != 0 { 461 t.Errorf("degenerate=%t: false negatives: got %d, want %d", degenerate, c.falseNegatives, 0) 462 } 463 464 if got := c.falsePositives + c.trueNegatives; got < wantActualNegatives { 465 t.Errorf("degenerate=%t: actual negatives (false positives + true negatives): "+ 466 "got %d (%d + %d), want >= %d", 467 degenerate, got, c.falsePositives, c.trueNegatives, wantActualNegatives) 468 } 469 470 if !degenerate { 471 // The true negative count should be much greater than the false 472 // positive count. 473 if c.trueNegatives < 10*c.falsePositives { 474 t.Errorf("degenerate=%t: true negative to false positive ratio (%d:%d) is too small", 475 degenerate, c.trueNegatives, c.falsePositives) 476 } 477 } 478 }) 479 } 480 }) 481 } 482 } 483 484 func TestBloomFilterFalsePositiveRate(t *testing.T) { 485 f, err := os.Open(filepath.FromSlash("testdata/h.table-bloom.no-compression.sst")) 486 if err != nil { 487 t.Fatal(err) 488 } 489 c := &countingFilterPolicy{ 490 FilterPolicy: bloom.FilterPolicy(1), 491 } 492 r, err := NewReader(f, 0, 0, &Options{ 493 Levels: []TableOptions{{ 494 FilterPolicy: c, 495 }}, 496 }) 497 if err != nil { 498 t.Fatal(err) 499 } 500 501 const n = 10000 502 // key is a buffer that will be re-used for n Get calls, each with a 503 // different key. The "m" in the 2-byte prefix means that the key falls in 504 // the [minWord, maxWord] range and so will not be rejected prior to 505 // applying the Bloom filter. The "!" in the 2-byte prefix means that the 506 // key is not actually in the table. The filter will only see actual 507 // negatives: false positives or true negatives. 508 key := []byte("m!....") 509 for i := 0; i < n; i++ { 510 binary.LittleEndian.PutUint32(key[2:6], uint32(i)) 511 r.get(key) 512 } 513 514 if c.truePositives != 0 { 515 t.Errorf("true positives: got %d, want 0", c.truePositives) 516 } 517 if c.falseNegatives != 0 { 518 t.Errorf("false negatives: got %d, want 0", c.falseNegatives) 519 } 520 if got := c.falsePositives + c.trueNegatives; got != n { 521 t.Errorf("actual negatives (false positives + true negatives): got %d (%d + %d), want %d", 522 got, c.falsePositives, c.trueNegatives, n) 523 } 524 525 // According the the comments in the C++ LevelDB code, the false positive 526 // rate should be approximately 1% for for bloom.FilterPolicy(10). The 10 527 // was the parameter used to write the .sst file. When reading the file, 528 // the 1 in the bloom.FilterPolicy(1) above doesn't matter, only the 529 // bloom.FilterPolicy matters. 530 if got := float64(100*c.falsePositives) / n; got < 0.2 || 5 < got { 531 t.Errorf("false positive rate: got %v%%, want approximately 1%%", got) 532 } 533 } 534 535 type countingFilterPolicy struct { 536 FilterPolicy 537 degenerate bool 538 539 truePositives int 540 falsePositives int 541 falseNegatives int 542 trueNegatives int 543 } 544 545 func (c *countingFilterPolicy) MayContain(ftype FilterType, filter, key []byte) bool { 546 got := true 547 if c.degenerate { 548 // When degenerate is true, we override the embedded FilterPolicy's 549 // MayContain method to always return true. Doing so is a valid, if 550 // inefficient, implementation of the FilterPolicy interface. 551 } else { 552 got = c.FilterPolicy.MayContain(ftype, filter, key) 553 } 554 _, want := wordCount[string(key)] 555 556 switch { 557 case got && want: 558 c.truePositives++ 559 case got && !want: 560 c.falsePositives++ 561 case !got && want: 562 c.falseNegatives++ 563 case !got && !want: 564 c.trueNegatives++ 565 } 566 return got 567 } 568 569 func TestWriterRoundTrip(t *testing.T) { 570 blockSizes := []int{100, 1000, 2048, 4096, math.MaxInt32} 571 for _, blockSize := range blockSizes { 572 for _, indexBlockSize := range blockSizes { 573 for name, fp := range map[string]FilterPolicy{ 574 "none": nil, 575 "bloom10bit": bloom.FilterPolicy(10), 576 } { 577 t.Run(fmt.Sprintf("bloom=%s", name), func(t *testing.T) { 578 f, err := build(base.DefaultCompression, fp, TableFilter, 579 nil, nil, blockSize, indexBlockSize) 580 if err != nil { 581 t.Fatal(err) 582 } 583 // Check that we can read a freshly made table. 584 585 err = check(f, nil, nil) 586 if err != nil { 587 t.Fatal(err) 588 } 589 }) 590 } 591 } 592 } 593 } 594 595 func TestFinalBlockIsWritten(t *testing.T) { 596 keys := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J"} 597 valueLengths := []int{0, 1, 22, 28, 33, 40, 50, 61, 87, 100, 143, 200} 598 xxx := bytes.Repeat([]byte("x"), valueLengths[len(valueLengths)-1]) 599 for _, blockSize := range []int{5, 10, 25, 50, 100} { 600 for _, indexBlockSize := range []int{5, 10, 25, 50, 100, math.MaxInt32} { 601 for nk := 0; nk <= len(keys); nk++ { 602 loop: 603 for _, vLen := range valueLengths { 604 got, memFS := 0, vfs.NewMem() 605 606 wf, err := memFS.Create("foo") 607 if err != nil { 608 t.Errorf("nk=%d, vLen=%d: memFS create: %v", nk, vLen, err) 609 continue 610 } 611 w := NewWriter(wf, nil, TableOptions{ 612 BlockSize: blockSize, 613 IndexBlockSize: indexBlockSize, 614 }) 615 for _, k := range keys[:nk] { 616 if err := w.Add(InternalKey{UserKey: []byte(k)}, xxx[:vLen]); err != nil { 617 t.Errorf("nk=%d, vLen=%d: set: %v", nk, vLen, err) 618 continue loop 619 } 620 } 621 if err := w.Close(); err != nil { 622 t.Errorf("nk=%d, vLen=%d: writer close: %v", nk, vLen, err) 623 continue 624 } 625 626 rf, err := memFS.Open("foo") 627 if err != nil { 628 t.Errorf("nk=%d, vLen=%d: memFS open: %v", nk, vLen, err) 629 continue 630 } 631 r, err := NewReader(rf, 0, 0, nil) 632 if err != nil { 633 t.Errorf("nk=%d, vLen=%d: reader open: %v", nk, vLen, err) 634 } 635 i := iterAdapter{r.NewIter(nil /* lower */, nil /* upper */)} 636 for valid := i.First(); valid; valid = i.Next() { 637 got++ 638 } 639 if err := i.Close(); err != nil { 640 t.Errorf("nk=%d, vLen=%d: Iterator close: %v", nk, vLen, err) 641 continue 642 } 643 if err := r.Close(); err != nil { 644 t.Errorf("nk=%d, vLen=%d: reader close: %v", nk, vLen, err) 645 continue 646 } 647 648 if got != nk { 649 t.Errorf("nk=%2d, vLen=%3d: got %2d keys, want %2d", nk, vLen, got, nk) 650 continue 651 } 652 } 653 } 654 } 655 } 656 } 657 658 func TestReaderGlobalSeqNum(t *testing.T) { 659 f, err := os.Open(filepath.FromSlash("testdata/h.sst")) 660 if err != nil { 661 t.Fatal(err) 662 } 663 r, err := NewReader(f, 0, 0, nil) 664 if err != nil { 665 t.Fatal(err) 666 } 667 defer r.Close() 668 669 const globalSeqNum = 42 670 r.Properties.GlobalSeqNum = globalSeqNum 671 672 i := iterAdapter{r.NewIter(nil /* lower */, nil /* upper */)} 673 for valid := i.First(); valid; valid = i.Next() { 674 if globalSeqNum != i.Key().SeqNum() { 675 t.Fatalf("expected %d, but found %d", globalSeqNum, i.Key().SeqNum()) 676 } 677 } 678 } 679 680 func TestMetaIndexEntriesSorted(t *testing.T) { 681 f, err := build(base.DefaultCompression, nil /* filter policy */, 682 TableFilter, nil, nil, 4096, 4096) 683 if err != nil { 684 t.Fatal(err) 685 } 686 687 r, err := NewReader(f, 0 /* dbNum */, 0 /* fileNum */, nil /* extra opts */) 688 if err != nil { 689 t.Fatal(err) 690 } 691 692 b, err := r.readBlock(r.metaIndexBH, nil /* transform */) 693 if err != nil { 694 t.Fatal(err) 695 } 696 i, err := newRawBlockIter(bytes.Compare, b.Get()) 697 b.Release() 698 if err != nil { 699 t.Fatal(err) 700 } 701 702 var keys []string 703 for valid := i.First(); valid; valid = i.Next() { 704 keys = append(keys, string(i.Key().UserKey)) 705 } 706 if !sort.StringsAreSorted(keys) { 707 t.Fatalf("metaindex block out of order: %v", keys) 708 } 709 710 if err := i.Close(); err != nil { 711 t.Fatal(err) 712 } 713 } 714 715 func TestFooterRoundTrip(t *testing.T) { 716 buf := make([]byte, 100+maxFooterLen) 717 for _, format := range []TableFormat{ 718 TableFormatRocksDBv2, 719 TableFormatLevelDB, 720 } { 721 t.Run(fmt.Sprintf("format=%d", format), func(t *testing.T) { 722 for _, checksum := range []uint8{checksumCRC32c} { 723 t.Run(fmt.Sprintf("checksum=%d", checksum), func(t *testing.T) { 724 footer := footer{ 725 format: format, 726 checksum: checksum, 727 metaindexBH: BlockHandle{Offset: 1, Length: 2}, 728 indexBH: BlockHandle{Offset: 3, Length: 4}, 729 } 730 for _, offset := range []int64{0, 1, 100} { 731 t.Run(fmt.Sprintf("offset=%d", offset), func(t *testing.T) { 732 mem := vfs.NewMem() 733 f, err := mem.Create("test") 734 if err != nil { 735 t.Fatal(err) 736 } 737 if _, err := f.Write(buf[:offset]); err != nil { 738 t.Fatal(err) 739 } 740 encoded := footer.encode(buf[100:]) 741 if _, err := f.Write(encoded); err != nil { 742 t.Fatal(err) 743 } 744 if err := f.Close(); err != nil { 745 t.Fatal(err) 746 } 747 footer.footerBH.Offset = uint64(offset) 748 footer.footerBH.Length = uint64(len(encoded)) 749 750 f, err = mem.Open("test") 751 if err != nil { 752 t.Fatal(err) 753 } 754 result, err := readFooter(f) 755 if err != nil { 756 t.Fatal(err) 757 } 758 if err := f.Close(); err != nil { 759 t.Fatal(err) 760 } 761 762 if diff := pretty.Diff(footer, result); diff != nil { 763 t.Fatalf("expected %+v, but found %+v\n%s", 764 footer, result, strings.Join(diff, "\n")) 765 } 766 }) 767 } 768 }) 769 } 770 }) 771 } 772 } 773 774 func TestReadFooter(t *testing.T) { 775 encode := func(format TableFormat, checksum uint8) string { 776 f := footer{ 777 format: format, 778 checksum: checksum, 779 } 780 return string(f.encode(make([]byte, maxFooterLen))) 781 } 782 783 testCases := []struct { 784 encoded string 785 expected string 786 }{ 787 {strings.Repeat("a", minFooterLen-1), "file size is too small"}, 788 {strings.Repeat("a", levelDBFooterLen), "bad magic number"}, 789 {strings.Repeat("a", rocksDBFooterLen), "bad magic number"}, 790 {encode(TableFormatLevelDB, 0)[1:], "file size is too small"}, 791 {encode(TableFormatRocksDBv2, 0)[1:], "footer too short"}, 792 {encode(TableFormatRocksDBv2, noChecksum), "unsupported checksum type"}, 793 {encode(TableFormatRocksDBv2, checksumXXHash), "unsupported checksum type"}, 794 } 795 for _, c := range testCases { 796 t.Run("", func(t *testing.T) { 797 mem := vfs.NewMem() 798 f, err := mem.Create("test") 799 if err != nil { 800 t.Fatal(err) 801 } 802 if _, err := f.Write([]byte(c.encoded)); err != nil { 803 t.Fatal(err) 804 } 805 if err := f.Close(); err != nil { 806 t.Fatal(err) 807 } 808 809 f, err = mem.Open("test") 810 if err != nil { 811 t.Fatal(err) 812 } 813 if _, err := readFooter(f); err == nil { 814 t.Fatalf("expected %q, but found success", c.expected) 815 } else if !strings.Contains(err.Error(), c.expected) { 816 t.Fatalf("expected %q, but found %v", c.expected, err) 817 } 818 }) 819 } 820 } 821 822 type errorPropCollector struct{} 823 824 func (errorPropCollector) Add(key InternalKey, _ []byte) error { 825 return fmt.Errorf("add %s failed", key) 826 } 827 828 func (errorPropCollector) Finish(_ map[string]string) error { 829 return fmt.Errorf("finish failed") 830 } 831 832 func (errorPropCollector) Name() string { 833 return "errorPropCollector" 834 } 835 836 func TestTablePropertyCollectorErrors(t *testing.T) { 837 mem := vfs.NewMem() 838 f, err := mem.Create("foo") 839 if err != nil { 840 t.Fatal(err) 841 } 842 843 opts := &Options{} 844 opts.TablePropertyCollectors = append(opts.TablePropertyCollectors, 845 func() TablePropertyCollector { 846 return errorPropCollector{} 847 }) 848 849 w := NewWriter(f, opts, TableOptions{}) 850 require.Regexp(t, `add a#0,1 failed`, w.Set([]byte("a"), []byte("b"))) 851 require.Regexp(t, `add c#0,0 failed`, w.Delete([]byte("c"))) 852 require.Regexp(t, `add d#0,15 failed`, w.DeleteRange([]byte("d"), []byte("e"))) 853 require.Regexp(t, `add f#0,2 failed`, w.Merge([]byte("f"), []byte("g"))) 854 require.Regexp(t, `finish failed`, w.Close()) 855 }