github.com/cockroachdb/pebble@v0.0.0-20231214172447-ab4952c5f87b/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 "bytes" 9 "context" 10 "encoding/binary" 11 "fmt" 12 "math" 13 "os" 14 "path/filepath" 15 "sort" 16 "strings" 17 "testing" 18 "time" 19 20 "github.com/cockroachdb/errors" 21 "github.com/cockroachdb/pebble/bloom" 22 "github.com/cockroachdb/pebble/internal/base" 23 "github.com/cockroachdb/pebble/objstorage/objstorageprovider" 24 "github.com/cockroachdb/pebble/vfs" 25 "github.com/kr/pretty" 26 "github.com/stretchr/testify/require" 27 "golang.org/x/exp/rand" 28 ) 29 30 func check(fs vfs.FS, filename string, comparer *Comparer, fp FilterPolicy) error { 31 opts := ReaderOptions{ 32 Comparer: comparer, 33 } 34 if fp != nil { 35 opts.Filters = map[string]FilterPolicy{ 36 fp.Name(): fp, 37 } 38 } 39 40 f, err := fs.Open(filename) 41 if err != nil { 42 return err 43 } 44 45 r, err := newReader(f, opts) 46 if err != nil { 47 return err 48 } 49 50 // Check that each key/value pair in wordCount is also in the table. 51 wordCount := hamletWordCount() 52 words := make([]string, 0, len(wordCount)) 53 for k, v := range wordCount { 54 words = append(words, k) 55 // Check using Get. 56 if v1, err := r.get([]byte(k)); string(v1) != string(v) || err != nil { 57 return errors.Errorf("Get %q: got (%q, %v), want (%q, %v)", k, v1, err, v, error(nil)) 58 } else if len(v1) != cap(v1) { 59 return errors.Errorf("Get %q: len(v1)=%d, cap(v1)=%d", k, len(v1), cap(v1)) 60 } 61 62 // Check using SeekGE. 63 iter, err := r.NewIter(nil /* lower */, nil /* upper */) 64 if err != nil { 65 return err 66 } 67 i := newIterAdapter(iter) 68 if !i.SeekGE([]byte(k), base.SeekGEFlagsNone) || string(i.Key().UserKey) != k { 69 return errors.Errorf("Find %q: key was not in the table", k) 70 } 71 if k1 := i.Key().UserKey; len(k1) != cap(k1) { 72 return errors.Errorf("Find %q: len(k1)=%d, cap(k1)=%d", k, len(k1), cap(k1)) 73 } 74 if string(i.Value()) != v { 75 return errors.Errorf("Find %q: got value %q, want %q", k, i.Value(), v) 76 } 77 if v1 := i.Value(); len(v1) != cap(v1) { 78 return errors.Errorf("Find %q: len(v1)=%d, cap(v1)=%d", k, len(v1), cap(v1)) 79 } 80 81 // Check using SeekLT. 82 if !i.SeekLT([]byte(k), base.SeekLTFlagsNone) { 83 i.First() 84 } else { 85 i.Next() 86 } 87 if string(i.Key().UserKey) != k { 88 return errors.Errorf("Find %q: key was not in the table", k) 89 } 90 if k1 := i.Key().UserKey; len(k1) != cap(k1) { 91 return errors.Errorf("Find %q: len(k1)=%d, cap(k1)=%d", k, len(k1), cap(k1)) 92 } 93 if string(i.Value()) != v { 94 return errors.Errorf("Find %q: got value %q, want %q", k, i.Value(), v) 95 } 96 if v1 := i.Value(); len(v1) != cap(v1) { 97 return errors.Errorf("Find %q: len(v1)=%d, cap(v1)=%d", k, len(v1), cap(v1)) 98 } 99 100 if err := i.Close(); err != nil { 101 return err 102 } 103 } 104 105 // Check that nonsense words are not in the table. 106 for _, s := range hamletNonsenseWords { 107 // Check using Get. 108 if _, err := r.get([]byte(s)); err != base.ErrNotFound { 109 return errors.Errorf("Get %q: got %v, want ErrNotFound", s, err) 110 } 111 112 // Check using Find. 113 iter, err := r.NewIter(nil /* lower */, nil /* upper */) 114 if err != nil { 115 return err 116 } 117 i := newIterAdapter(iter) 118 if i.SeekGE([]byte(s), base.SeekGEFlagsNone) && s == string(i.Key().UserKey) { 119 return errors.Errorf("Find %q: unexpectedly found key in the table", s) 120 } 121 if err := i.Close(); err != nil { 122 return err 123 } 124 } 125 126 // Check that the number of keys >= a given start key matches the expected number. 127 var countTests = []struct { 128 count int 129 start string 130 }{ 131 // cat h.txt | cut -c 9- | wc -l gives 1710. 132 {1710, ""}, 133 // cat h.txt | cut -c 9- | grep -v "^[a-b]" | wc -l gives 1522. 134 {1522, "c"}, 135 // cat h.txt | cut -c 9- | grep -v "^[a-j]" | wc -l gives 940. 136 {940, "k"}, 137 // cat h.txt | cut -c 9- | grep -v "^[a-x]" | wc -l gives 12. 138 {12, "y"}, 139 // cat h.txt | cut -c 9- | grep -v "^[a-z]" | wc -l gives 0. 140 {0, "~"}, 141 } 142 for _, ct := range countTests { 143 iter, err := r.NewIter(nil /* lower */, nil /* upper */) 144 if err != nil { 145 return err 146 } 147 n, i := 0, newIterAdapter(iter) 148 for valid := i.SeekGE([]byte(ct.start), base.SeekGEFlagsNone); valid; valid = i.Next() { 149 n++ 150 } 151 if n != ct.count { 152 return errors.Errorf("count %q: got %d, want %d", ct.start, n, ct.count) 153 } 154 n = 0 155 for valid := i.Last(); valid; valid = i.Prev() { 156 if bytes.Compare(i.Key().UserKey, []byte(ct.start)) < 0 { 157 break 158 } 159 n++ 160 } 161 if n != ct.count { 162 return errors.Errorf("count %q: got %d, want %d", ct.start, n, ct.count) 163 } 164 if err := i.Close(); err != nil { 165 return err 166 } 167 } 168 169 // Check lower/upper bounds behavior. Randomly choose a lower and upper bound 170 // and then guarantee that iteration finds the expected number if entries. 171 rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) 172 sort.Strings(words) 173 for i := 0; i < 10; i++ { 174 lowerIdx := -1 175 upperIdx := len(words) 176 if rng.Intn(5) != 0 { 177 lowerIdx = rng.Intn(len(words)) 178 } 179 if rng.Intn(5) != 0 { 180 upperIdx = rng.Intn(len(words)) 181 } 182 if lowerIdx > upperIdx { 183 lowerIdx, upperIdx = upperIdx, lowerIdx 184 } 185 186 var lower, upper []byte 187 if lowerIdx >= 0 { 188 lower = []byte(words[lowerIdx]) 189 } else { 190 lowerIdx = 0 191 } 192 if upperIdx < len(words) { 193 upper = []byte(words[upperIdx]) 194 } 195 196 iter, err := r.NewIter(lower, upper) 197 if err != nil { 198 return err 199 } 200 i := newIterAdapter(iter) 201 202 if lower == nil { 203 n := 0 204 for valid := i.First(); valid; valid = i.Next() { 205 n++ 206 } 207 if expected := upperIdx; expected != n { 208 return errors.Errorf("expected %d, but found %d", expected, n) 209 } 210 } 211 212 if upper == nil { 213 n := 0 214 for valid := i.Last(); valid; valid = i.Prev() { 215 n++ 216 } 217 if expected := len(words) - lowerIdx; expected != n { 218 return errors.Errorf("expected %d, but found %d", expected, n) 219 } 220 } 221 222 if lower != nil { 223 n := 0 224 for valid := i.SeekGE(lower, base.SeekGEFlagsNone); valid; valid = i.Next() { 225 n++ 226 } 227 if expected := upperIdx - lowerIdx; expected != n { 228 return errors.Errorf("expected %d, but found %d", expected, n) 229 } 230 } 231 232 if upper != nil { 233 n := 0 234 for valid := i.SeekLT(upper, base.SeekLTFlagsNone); valid; valid = i.Prev() { 235 n++ 236 } 237 if expected := upperIdx - lowerIdx; expected != n { 238 return errors.Errorf("expected %d, but found %d", expected, n) 239 } 240 } 241 242 if err := i.Close(); err != nil { 243 return err 244 } 245 } 246 247 return r.Close() 248 } 249 250 func testReader(t *testing.T, filename string, comparer *Comparer, fp FilterPolicy) { 251 // Check that we can read a pre-made table. 252 err := check(vfs.Default, filepath.FromSlash("testdata/"+filename), comparer, fp) 253 if err != nil { 254 t.Error(err) 255 return 256 } 257 } 258 259 func TestReaderDefaultCompression(t *testing.T) { testReader(t, "h.sst", nil, nil) } 260 func TestReaderNoCompression(t *testing.T) { testReader(t, "h.no-compression.sst", nil, nil) } 261 func TestReaderTableBloom(t *testing.T) { 262 testReader(t, "h.table-bloom.no-compression.sst", nil, nil) 263 } 264 265 func TestReaderBloomUsed(t *testing.T) { 266 wordCount := hamletWordCount() 267 words := wordCount.SortedKeys() 268 269 // wantActualNegatives is the minimum number of nonsense words (i.e. false 270 // positives or true negatives) to run through our filter. Some nonsense 271 // words might be rejected even before the filtering step, if they are out 272 // of the [minWord, maxWord] range of keys in the table. 273 wantActualNegatives := 0 274 for _, s := range hamletNonsenseWords { 275 if words[0] < s && s < words[len(words)-1] { 276 wantActualNegatives++ 277 } 278 } 279 280 files := []struct { 281 path string 282 comparer *Comparer 283 }{ 284 {"h.table-bloom.no-compression.sst", nil}, 285 {"h.table-bloom.no-compression.prefix_extractor.no_whole_key_filter.sst", fixtureComparer}, 286 } 287 for _, tc := range files { 288 t.Run(tc.path, func(t *testing.T) { 289 for _, degenerate := range []bool{false, true} { 290 t.Run(fmt.Sprintf("degenerate=%t", degenerate), func(t *testing.T) { 291 c := &countingFilterPolicy{ 292 FilterPolicy: bloom.FilterPolicy(10), 293 degenerate: degenerate, 294 } 295 testReader(t, tc.path, tc.comparer, c) 296 297 if c.truePositives != len(wordCount) { 298 t.Errorf("degenerate=%t: true positives: got %d, want %d", degenerate, c.truePositives, len(wordCount)) 299 } 300 if c.falseNegatives != 0 { 301 t.Errorf("degenerate=%t: false negatives: got %d, want %d", degenerate, c.falseNegatives, 0) 302 } 303 304 if got := c.falsePositives + c.trueNegatives; got < wantActualNegatives { 305 t.Errorf("degenerate=%t: actual negatives (false positives + true negatives): "+ 306 "got %d (%d + %d), want >= %d", 307 degenerate, got, c.falsePositives, c.trueNegatives, wantActualNegatives) 308 } 309 310 if !degenerate { 311 // The true negative count should be much greater than the false 312 // positive count. 313 if c.trueNegatives < 10*c.falsePositives { 314 t.Errorf("degenerate=%t: true negative to false positive ratio (%d:%d) is too small", 315 degenerate, c.trueNegatives, c.falsePositives) 316 } 317 } 318 }) 319 } 320 }) 321 } 322 } 323 324 func TestBloomFilterFalsePositiveRate(t *testing.T) { 325 f, err := os.Open(filepath.FromSlash("testdata/h.table-bloom.no-compression.sst")) 326 require.NoError(t, err) 327 328 c := &countingFilterPolicy{ 329 FilterPolicy: bloom.FilterPolicy(1), 330 } 331 r, err := newReader(f, ReaderOptions{ 332 Filters: map[string]FilterPolicy{ 333 c.Name(): c, 334 }, 335 }) 336 require.NoError(t, err) 337 338 const n = 10000 339 // key is a buffer that will be re-used for n Get calls, each with a 340 // different key. The "m" in the 2-byte prefix means that the key falls in 341 // the [minWord, maxWord] range and so will not be rejected prior to 342 // applying the Bloom filter. The "!" in the 2-byte prefix means that the 343 // key is not actually in the table. The filter will only see actual 344 // negatives: false positives or true negatives. 345 key := []byte("m!....") 346 for i := 0; i < n; i++ { 347 binary.LittleEndian.PutUint32(key[2:6], uint32(i)) 348 r.get(key) 349 } 350 351 if c.truePositives != 0 { 352 t.Errorf("true positives: got %d, want 0", c.truePositives) 353 } 354 if c.falseNegatives != 0 { 355 t.Errorf("false negatives: got %d, want 0", c.falseNegatives) 356 } 357 if got := c.falsePositives + c.trueNegatives; got != n { 358 t.Errorf("actual negatives (false positives + true negatives): got %d (%d + %d), want %d", 359 got, c.falsePositives, c.trueNegatives, n) 360 } 361 362 // According the the comments in the C++ LevelDB code, the false positive 363 // rate should be approximately 1% for for bloom.FilterPolicy(10). The 10 364 // was the parameter used to write the .sst file. When reading the file, 365 // the 1 in the bloom.FilterPolicy(1) above doesn't matter, only the 366 // bloom.FilterPolicy matters. 367 if got := float64(100*c.falsePositives) / n; got < 0.2 || 5 < got { 368 t.Errorf("false positive rate: got %v%%, want approximately 1%%", got) 369 } 370 371 require.NoError(t, r.Close()) 372 } 373 374 type countingFilterPolicy struct { 375 FilterPolicy 376 degenerate bool 377 378 truePositives int 379 falsePositives int 380 falseNegatives int 381 trueNegatives int 382 } 383 384 func (c *countingFilterPolicy) MayContain(ftype FilterType, filter, key []byte) bool { 385 got := true 386 if c.degenerate { 387 // When degenerate is true, we override the embedded FilterPolicy's 388 // MayContain method to always return true. Doing so is a valid, if 389 // inefficient, implementation of the FilterPolicy interface. 390 } else { 391 got = c.FilterPolicy.MayContain(ftype, filter, key) 392 } 393 wordCount := hamletWordCount() 394 _, want := wordCount[string(key)] 395 396 switch { 397 case got && want: 398 c.truePositives++ 399 case got && !want: 400 c.falsePositives++ 401 case !got && want: 402 c.falseNegatives++ 403 case !got && !want: 404 c.trueNegatives++ 405 } 406 return got 407 } 408 409 func TestWriterRoundTrip(t *testing.T) { 410 blockSizes := []int{100, 1000, 2048, 4096, math.MaxInt32} 411 for _, blockSize := range blockSizes { 412 for _, indexBlockSize := range blockSizes { 413 for name, fp := range map[string]FilterPolicy{ 414 "none": nil, 415 "bloom10bit": bloom.FilterPolicy(10), 416 } { 417 t.Run(fmt.Sprintf("bloom=%s", name), func(t *testing.T) { 418 fs := vfs.NewMem() 419 err := buildHamletTestSST( 420 fs, "test.sst", DefaultCompression, fp, TableFilter, 421 nil /* comparer */, nil /* propCollector */, blockSize, indexBlockSize, 422 ) 423 require.NoError(t, err) 424 // Check that we can read a freshly made table. 425 require.NoError(t, check(fs, "test.sst", nil, nil)) 426 }) 427 } 428 } 429 } 430 } 431 432 func TestFinalBlockIsWritten(t *testing.T) { 433 keys := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J"} 434 valueLengths := []int{0, 1, 22, 28, 33, 40, 50, 61, 87, 100, 143, 200} 435 xxx := bytes.Repeat([]byte("x"), valueLengths[len(valueLengths)-1]) 436 for _, blockSize := range []int{5, 10, 25, 50, 100} { 437 for _, indexBlockSize := range []int{5, 10, 25, 50, 100, math.MaxInt32} { 438 for nk := 0; nk <= len(keys); nk++ { 439 loop: 440 for _, vLen := range valueLengths { 441 got, memFS := 0, vfs.NewMem() 442 443 wf, err := memFS.Create("foo") 444 if err != nil { 445 t.Errorf("nk=%d, vLen=%d: memFS create: %v", nk, vLen, err) 446 continue 447 } 448 w := NewWriter(objstorageprovider.NewFileWritable(wf), WriterOptions{ 449 BlockSize: blockSize, 450 IndexBlockSize: indexBlockSize, 451 }) 452 for _, k := range keys[:nk] { 453 if err := w.Add(InternalKey{UserKey: []byte(k)}, xxx[:vLen]); err != nil { 454 t.Errorf("nk=%d, vLen=%d: set: %v", nk, vLen, err) 455 continue loop 456 } 457 } 458 if err := w.Close(); err != nil { 459 t.Errorf("nk=%d, vLen=%d: writer close: %v", nk, vLen, err) 460 continue 461 } 462 463 rf, err := memFS.Open("foo") 464 if err != nil { 465 t.Errorf("nk=%d, vLen=%d: memFS open: %v", nk, vLen, err) 466 continue 467 } 468 r, err := newReader(rf, ReaderOptions{}) 469 if err != nil { 470 t.Errorf("nk=%d, vLen=%d: reader open: %v", nk, vLen, err) 471 } 472 iter, err := r.NewIter(nil /* lower */, nil /* upper */) 473 require.NoError(t, err) 474 i := newIterAdapter(iter) 475 for valid := i.First(); valid; valid = i.Next() { 476 got++ 477 } 478 if err := i.Close(); err != nil { 479 t.Errorf("nk=%d, vLen=%d: Iterator close: %v", nk, vLen, err) 480 continue 481 } 482 if err := r.Close(); err != nil { 483 t.Errorf("nk=%d, vLen=%d: reader close: %v", nk, vLen, err) 484 continue 485 } 486 487 if got != nk { 488 t.Errorf("nk=%2d, vLen=%3d: got %2d keys, want %2d", nk, vLen, got, nk) 489 continue 490 } 491 } 492 } 493 } 494 } 495 } 496 497 func TestReaderGlobalSeqNum(t *testing.T) { 498 f, err := os.Open(filepath.FromSlash("testdata/h.sst")) 499 require.NoError(t, err) 500 501 r, err := newReader(f, ReaderOptions{}) 502 require.NoError(t, err) 503 504 const globalSeqNum = 42 505 r.Properties.GlobalSeqNum = globalSeqNum 506 507 iter, err := r.NewIter(nil /* lower */, nil /* upper */) 508 require.NoError(t, err) 509 i := newIterAdapter(iter) 510 for valid := i.First(); valid; valid = i.Next() { 511 if globalSeqNum != i.Key().SeqNum() { 512 t.Fatalf("expected %d, but found %d", globalSeqNum, i.Key().SeqNum()) 513 } 514 } 515 require.NoError(t, i.Close()) 516 require.NoError(t, r.Close()) 517 } 518 519 func TestMetaIndexEntriesSorted(t *testing.T) { 520 fs := vfs.NewMem() 521 err := buildHamletTestSST(fs, "test.sst", DefaultCompression, nil, /* filter policy */ 522 TableFilter, nil, nil, 4096, 4096) 523 require.NoError(t, err) 524 f, err := fs.Open("test.sst") 525 require.NoError(t, err) 526 527 r, err := newReader(f, ReaderOptions{}) 528 require.NoError(t, err) 529 530 b, err := r.readBlock( 531 context.Background(), r.metaIndexBH, nil, nil, nil, nil, nil) 532 require.NoError(t, err) 533 defer b.Release() 534 535 i, err := newRawBlockIter(bytes.Compare, b.Get()) 536 require.NoError(t, err) 537 538 var keys []string 539 for valid := i.First(); valid; valid = i.Next() { 540 keys = append(keys, string(i.Key().UserKey)) 541 } 542 if !sort.StringsAreSorted(keys) { 543 t.Fatalf("metaindex block out of order: %v", keys) 544 } 545 546 require.NoError(t, i.Close()) 547 require.NoError(t, r.Close()) 548 } 549 550 func TestFooterRoundTrip(t *testing.T) { 551 buf := make([]byte, 100+maxFooterLen) 552 for format := TableFormatLevelDB; format < TableFormatMax; format++ { 553 t.Run(fmt.Sprintf("format=%s", format), func(t *testing.T) { 554 checksums := []ChecksumType{ChecksumTypeCRC32c} 555 if format != TableFormatLevelDB { 556 checksums = []ChecksumType{ChecksumTypeCRC32c, ChecksumTypeXXHash64} 557 } 558 for _, checksum := range checksums { 559 t.Run(fmt.Sprintf("checksum=%d", checksum), func(t *testing.T) { 560 footer := footer{ 561 format: format, 562 checksum: checksum, 563 metaindexBH: BlockHandle{Offset: 1, Length: 2}, 564 indexBH: BlockHandle{Offset: 3, Length: 4}, 565 } 566 for _, offset := range []int64{0, 1, 100} { 567 t.Run(fmt.Sprintf("offset=%d", offset), func(t *testing.T) { 568 mem := vfs.NewMem() 569 f, err := mem.Create("test") 570 require.NoError(t, err) 571 572 _, err = f.Write(buf[:offset]) 573 require.NoError(t, err) 574 575 encoded := footer.encode(buf[100:]) 576 _, err = f.Write(encoded) 577 require.NoError(t, err) 578 require.NoError(t, f.Close()) 579 580 footer.footerBH.Offset = uint64(offset) 581 footer.footerBH.Length = uint64(len(encoded)) 582 583 f, err = mem.Open("test") 584 require.NoError(t, err) 585 586 readable, err := NewSimpleReadable(f) 587 require.NoError(t, err) 588 589 result, err := readFooter(readable) 590 require.NoError(t, err) 591 require.NoError(t, readable.Close()) 592 593 if diff := pretty.Diff(footer, result); diff != nil { 594 t.Fatalf("expected %+v, but found %+v\n%s", 595 footer, result, strings.Join(diff, "\n")) 596 } 597 }) 598 } 599 }) 600 } 601 }) 602 } 603 } 604 605 func TestReadFooter(t *testing.T) { 606 encode := func(format TableFormat, checksum ChecksumType) string { 607 f := footer{ 608 format: format, 609 checksum: checksum, 610 } 611 return string(f.encode(make([]byte, maxFooterLen))) 612 } 613 614 testCases := []struct { 615 encoded string 616 expected string 617 }{ 618 {strings.Repeat("a", minFooterLen-1), "file size is too small"}, 619 {strings.Repeat("a", levelDBFooterLen), "bad magic number"}, 620 {strings.Repeat("a", rocksDBFooterLen), "bad magic number"}, 621 {encode(TableFormatLevelDB, 0)[1:], "file size is too small"}, 622 {encode(TableFormatRocksDBv2, 0)[1:], "footer too short"}, 623 {encode(TableFormatRocksDBv2, ChecksumTypeNone), "unsupported checksum type"}, 624 {encode(TableFormatRocksDBv2, ChecksumTypeXXHash), "unsupported checksum type"}, 625 } 626 for _, c := range testCases { 627 t.Run("", func(t *testing.T) { 628 mem := vfs.NewMem() 629 f, err := mem.Create("test") 630 require.NoError(t, err) 631 632 _, err = f.Write([]byte(c.encoded)) 633 require.NoError(t, err) 634 require.NoError(t, f.Close()) 635 636 f, err = mem.Open("test") 637 require.NoError(t, err) 638 639 readable, err := NewSimpleReadable(f) 640 require.NoError(t, err) 641 642 if _, err := readFooter(readable); err == nil { 643 t.Fatalf("expected %q, but found success", c.expected) 644 } else if !strings.Contains(err.Error(), c.expected) { 645 t.Fatalf("expected %q, but found %v", c.expected, err) 646 } 647 }) 648 } 649 } 650 651 type errorPropCollector struct{} 652 653 func (errorPropCollector) Add(key InternalKey, _ []byte) error { 654 return errors.Errorf("add %s failed", key) 655 } 656 657 func (errorPropCollector) Finish(_ map[string]string) error { 658 return errors.Errorf("finish failed") 659 } 660 661 func (errorPropCollector) Name() string { 662 return "errorPropCollector" 663 } 664 665 func TestTablePropertyCollectorErrors(t *testing.T) { 666 667 var testcases map[string]func(w *Writer) error = map[string]func(w *Writer) error{ 668 "add a#0,1 failed": func(w *Writer) error { 669 return w.Set([]byte("a"), []byte("b")) 670 }, 671 "add c#0,0 failed": func(w *Writer) error { 672 return w.Delete([]byte("c")) 673 }, 674 "add d#0,15 failed": func(w *Writer) error { 675 return w.DeleteRange([]byte("d"), []byte("e")) 676 }, 677 "add f#0,2 failed": func(w *Writer) error { 678 return w.Merge([]byte("f"), []byte("g")) 679 }, 680 "finish failed": func(w *Writer) error { 681 return w.Close() 682 }, 683 } 684 685 for e, fun := range testcases { 686 mem := vfs.NewMem() 687 f, err := mem.Create("foo") 688 require.NoError(t, err) 689 690 var opts WriterOptions 691 opts.TablePropertyCollectors = append(opts.TablePropertyCollectors, 692 func() TablePropertyCollector { 693 return errorPropCollector{} 694 }) 695 696 w := NewWriter(objstorageprovider.NewFileWritable(f), opts) 697 698 require.Regexp(t, e, fun(w)) 699 } 700 }