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  }