github.com/cockroachdb/pebble@v0.0.0-20231214172447-ab4952c5f87b/metamorphic/options_test.go (about)

     1  // Copyright 2022 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 metamorphic
     6  
     7  import (
     8  	"fmt"
     9  	"io/fs"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/cockroachdb/pebble"
    17  	"github.com/cockroachdb/pebble/internal/testkeys"
    18  	"github.com/cockroachdb/pebble/sstable"
    19  	"github.com/cockroachdb/pebble/vfs"
    20  	"github.com/kr/pretty"
    21  	"github.com/stretchr/testify/require"
    22  	"golang.org/x/exp/rand"
    23  )
    24  
    25  func TestSetupInitialState(t *testing.T) {
    26  	// Construct a small database in the test's TempDir.
    27  	initialStatePath := t.TempDir()
    28  	initialDataPath := vfs.Default.PathJoin(initialStatePath, "data")
    29  	{
    30  		d, err := pebble.Open(initialDataPath, &pebble.Options{})
    31  		require.NoError(t, err)
    32  		const maxKeyLen = 2
    33  		ks := testkeys.Alpha(maxKeyLen)
    34  		var key [maxKeyLen]byte
    35  		for i := int64(0); i < ks.Count(); i++ {
    36  			n := testkeys.WriteKey(key[:], ks, i)
    37  			require.NoError(t, d.Set(key[:n], key[:n], pebble.NoSync))
    38  			if i%100 == 0 {
    39  				require.NoError(t, d.Flush())
    40  			}
    41  		}
    42  		require.NoError(t, d.Close())
    43  	}
    44  	ls, err := vfs.Default.List(initialStatePath)
    45  	require.NoError(t, err)
    46  
    47  	// setupInitialState with an initial state path set to the test's TempDir
    48  	// should populate opts.opts.FS with the directory's contents.
    49  	opts := &TestOptions{
    50  		Opts:             defaultOptions(),
    51  		initialStatePath: initialStatePath,
    52  		initialStateDesc: "test",
    53  	}
    54  	require.NoError(t, setupInitialState("data", opts))
    55  	copied, err := opts.Opts.FS.List("")
    56  	require.NoError(t, err)
    57  	require.ElementsMatch(t, ls, copied)
    58  }
    59  
    60  func TestOptionsRoundtrip(t *testing.T) {
    61  	// Some fields must be ignored to avoid spurious diffs.
    62  	ignorePrefixes := []string{
    63  		// Pointers
    64  		"Cache:",
    65  		"Cache.",
    66  		"FS:",
    67  		"TableCache:",
    68  		// Function pointers
    69  		"BlockPropertyCollectors:",
    70  		"EventListener:",
    71  		"MaxConcurrentCompactions:",
    72  		"Experimental.DisableIngestAsFlushable:",
    73  		"Experimental.EnableValueBlocks:",
    74  		"Experimental.IneffectualSingleDeleteCallback:",
    75  		"Experimental.IngestSplit:",
    76  		"Experimental.RemoteStorage:",
    77  		"Experimental.SingleDeleteInvariantViolationCallback:",
    78  		// Floating points
    79  		"Experimental.PointTombstoneWeight:",
    80  		"Experimental.MultiLevelCompactionHeuristic.AddPropensity",
    81  	}
    82  
    83  	// Ensure that we unref any caches created, so invariants builds don't
    84  	// complain about the leaked ref counts.
    85  	maybeUnref := func(o *TestOptions) {
    86  		if o.Opts.Cache != nil {
    87  			o.Opts.Cache.Unref()
    88  		}
    89  	}
    90  
    91  	checkOptions := func(t *testing.T, o *TestOptions) {
    92  		s := optionsToString(o)
    93  		t.Logf("Serialized options:\n%s\n", s)
    94  
    95  		parsed := defaultTestOptions()
    96  		require.NoError(t, parseOptions(parsed, s, nil))
    97  		maybeUnref(parsed)
    98  		got := optionsToString(parsed)
    99  		require.Equal(t, s, got)
   100  		t.Logf("Re-serialized options:\n%s\n", got)
   101  
   102  		// In some options, the closure obscures the underlying value. Check
   103  		// that the return values are equal.
   104  		require.Equal(t, o.Opts.Experimental.EnableValueBlocks == nil, parsed.Opts.Experimental.EnableValueBlocks == nil)
   105  		if o.Opts.Experimental.EnableValueBlocks != nil {
   106  			require.Equal(t, o.Opts.Experimental.EnableValueBlocks(), parsed.Opts.Experimental.EnableValueBlocks())
   107  		}
   108  		require.Equal(t, o.Opts.Experimental.DisableIngestAsFlushable == nil, parsed.Opts.Experimental.DisableIngestAsFlushable == nil)
   109  		if o.Opts.Experimental.DisableIngestAsFlushable != nil {
   110  			require.Equal(t, o.Opts.Experimental.DisableIngestAsFlushable(), parsed.Opts.Experimental.DisableIngestAsFlushable())
   111  		}
   112  		if o.Opts.Experimental.IngestSplit != nil && o.Opts.Experimental.IngestSplit() {
   113  			require.Equal(t, o.Opts.Experimental.IngestSplit(), parsed.Opts.Experimental.IngestSplit())
   114  		}
   115  		require.Equal(t, o.Opts.MaxConcurrentCompactions(), parsed.Opts.MaxConcurrentCompactions())
   116  		require.Equal(t, len(o.Opts.BlockPropertyCollectors), len(parsed.Opts.BlockPropertyCollectors))
   117  
   118  		diff := pretty.Diff(o.Opts, parsed.Opts)
   119  		cleaned := diff[:0]
   120  		for _, d := range diff {
   121  			var ignored bool
   122  			for _, prefix := range ignorePrefixes {
   123  				if strings.HasPrefix(d, prefix) {
   124  					ignored = true
   125  					break
   126  				}
   127  			}
   128  			if !ignored {
   129  				cleaned = append(cleaned, d)
   130  			}
   131  		}
   132  		require.Equal(t, diff[:0], cleaned)
   133  	}
   134  
   135  	standard := standardOptions()
   136  	for i := range standard {
   137  		t.Run(fmt.Sprintf("standard-%03d", i), func(t *testing.T) {
   138  			defer maybeUnref(standard[i])
   139  			checkOptions(t, standard[i])
   140  		})
   141  	}
   142  	rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano())))
   143  	for i := 0; i < 100; i++ {
   144  		t.Run(fmt.Sprintf("random-%03d", i), func(t *testing.T) {
   145  			o := randomOptions(rng, nil)
   146  			defer maybeUnref(o)
   147  			checkOptions(t, o)
   148  		})
   149  	}
   150  }
   151  
   152  // TestBlockPropertiesParse ensures that the testkeys block property collector
   153  // is in use by default. It runs a single OPTIONS run of the metamorphic tests
   154  // and scans the resulting data directory to ensure there's at least one sstable
   155  // with the property. It runs the test with the archive cleaner to avoid any
   156  // flakiness from small working sets of keys.
   157  func TestBlockPropertiesParse(t *testing.T) {
   158  	const fixedSeed = 1
   159  	const numOps = 10_000
   160  	metaDir := t.TempDir()
   161  
   162  	rng := rand.New(rand.NewSource(fixedSeed))
   163  	ops := generate(rng, numOps, presetConfigs[0], newKeyManager(1 /* numInstances */))
   164  	opsPath := filepath.Join(metaDir, "ops")
   165  	formattedOps := formatOps(ops)
   166  	require.NoError(t, os.WriteFile(opsPath, []byte(formattedOps), 0644))
   167  
   168  	runDir := filepath.Join(metaDir, "run")
   169  	require.NoError(t, os.MkdirAll(runDir, os.ModePerm))
   170  	optionsPath := filepath.Join(runDir, "OPTIONS")
   171  	opts := defaultTestOptions()
   172  	opts.Opts.EnsureDefaults()
   173  	opts.Opts.Cleaner = pebble.ArchiveCleaner{}
   174  	optionsStr := optionsToString(opts)
   175  	require.NoError(t, os.WriteFile(optionsPath, []byte(optionsStr), 0644))
   176  
   177  	RunOnce(t, runDir, fixedSeed, filepath.Join(runDir, "history"), KeepData{})
   178  	var foundTableBlockProperty bool
   179  	require.NoError(t, filepath.Walk(filepath.Join(runDir, "data"),
   180  		func(path string, info fs.FileInfo, err error) error {
   181  			if err != nil {
   182  				return err
   183  			}
   184  			if filepath.Ext(path) != ".sst" {
   185  				return nil
   186  			}
   187  			f, err := vfs.Default.Open(path)
   188  			if err != nil {
   189  				return err
   190  			}
   191  			readable, err := sstable.NewSimpleReadable(f)
   192  			if err != nil {
   193  				return err
   194  			}
   195  			r, err := sstable.NewReader(readable, opts.Opts.MakeReaderOptions())
   196  			if err != nil {
   197  				return err
   198  			}
   199  			_, ok := r.Properties.UserProperties[opts.Opts.BlockPropertyCollectors[0]().Name()]
   200  			foundTableBlockProperty = foundTableBlockProperty || ok
   201  			return r.Close()
   202  		}))
   203  	require.True(t, foundTableBlockProperty)
   204  }
   205  
   206  func TestCustomOptionParser(t *testing.T) {
   207  	customOptionParsers := map[string]func(string) (CustomOption, bool){
   208  		"foo": func(value string) (CustomOption, bool) {
   209  			return testCustomOption{name: "foo", value: value}, true
   210  		},
   211  	}
   212  
   213  	o1 := defaultTestOptions()
   214  	o2 := defaultTestOptions()
   215  
   216  	require.NoError(t, parseOptions(o1, `
   217  [TestOptions]
   218    foo=bar
   219  `, customOptionParsers))
   220  	require.NoError(t, parseOptions(o2, optionsToString(o1), customOptionParsers))
   221  	defer o2.Opts.Cache.Unref()
   222  
   223  	for _, o := range []*TestOptions{o1, o2} {
   224  		require.Equal(t, 1, len(o.CustomOpts))
   225  		require.Equal(t, "foo", o.CustomOpts[0].Name())
   226  		require.Equal(t, "bar", o.CustomOpts[0].Value())
   227  	}
   228  }
   229  
   230  type testCustomOption struct {
   231  	name, value string
   232  }
   233  
   234  func (o testCustomOption) Name() string                { return o.name }
   235  func (o testCustomOption) Value() string               { return o.value }
   236  func (o testCustomOption) Close(*pebble.Options) error { return nil }
   237  func (o testCustomOption) Open(*pebble.Options) error  { return nil }