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 }