github.com/cockroachdb/pebble@v1.1.1-0.20240513155919-3622ade60459/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 } 81 82 // Ensure that we unref any caches created, so invariants builds don't 83 // complain about the leaked ref counts. 84 maybeUnref := func(o *TestOptions) { 85 if o.Opts.Cache != nil { 86 o.Opts.Cache.Unref() 87 } 88 } 89 90 checkOptions := func(t *testing.T, o *TestOptions) { 91 s := optionsToString(o) 92 t.Logf("Serialized options:\n%s\n", s) 93 94 parsed := defaultTestOptions() 95 require.NoError(t, parseOptions(parsed, s, nil)) 96 maybeUnref(parsed) 97 got := optionsToString(parsed) 98 require.Equal(t, s, got) 99 t.Logf("Re-serialized options:\n%s\n", got) 100 101 // In some options, the closure obscures the underlying value. Check 102 // that the return values are equal. 103 require.Equal(t, o.Opts.Experimental.EnableValueBlocks == nil, parsed.Opts.Experimental.EnableValueBlocks == nil) 104 if o.Opts.Experimental.EnableValueBlocks != nil { 105 require.Equal(t, o.Opts.Experimental.EnableValueBlocks(), parsed.Opts.Experimental.EnableValueBlocks()) 106 } 107 require.Equal(t, o.Opts.Experimental.DisableIngestAsFlushable == nil, parsed.Opts.Experimental.DisableIngestAsFlushable == nil) 108 if o.Opts.Experimental.DisableIngestAsFlushable != nil { 109 require.Equal(t, o.Opts.Experimental.DisableIngestAsFlushable(), parsed.Opts.Experimental.DisableIngestAsFlushable()) 110 } 111 if o.Opts.Experimental.IngestSplit != nil && o.Opts.Experimental.IngestSplit() { 112 require.Equal(t, o.Opts.Experimental.IngestSplit(), parsed.Opts.Experimental.IngestSplit()) 113 } 114 require.Equal(t, o.Opts.MaxConcurrentCompactions(), parsed.Opts.MaxConcurrentCompactions()) 115 require.Equal(t, len(o.Opts.BlockPropertyCollectors), len(parsed.Opts.BlockPropertyCollectors)) 116 117 diff := pretty.Diff(o.Opts, parsed.Opts) 118 cleaned := diff[:0] 119 for _, d := range diff { 120 var ignored bool 121 for _, prefix := range ignorePrefixes { 122 if strings.HasPrefix(d, prefix) { 123 ignored = true 124 break 125 } 126 } 127 if !ignored { 128 cleaned = append(cleaned, d) 129 } 130 } 131 require.Equal(t, diff[:0], cleaned) 132 } 133 134 standard := standardOptions() 135 for i := range standard { 136 t.Run(fmt.Sprintf("standard-%03d", i), func(t *testing.T) { 137 defer maybeUnref(standard[i]) 138 checkOptions(t, standard[i]) 139 }) 140 } 141 rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) 142 for i := 0; i < 100; i++ { 143 t.Run(fmt.Sprintf("random-%03d", i), func(t *testing.T) { 144 o := randomOptions(rng, nil) 145 defer maybeUnref(o) 146 checkOptions(t, o) 147 }) 148 } 149 } 150 151 // TestBlockPropertiesParse ensures that the testkeys block property collector 152 // is in use by default. It runs a single OPTIONS run of the metamorphic tests 153 // and scans the resulting data directory to ensure there's at least one sstable 154 // with the property. It runs the test with the archive cleaner to avoid any 155 // flakiness from small working sets of keys. 156 func TestBlockPropertiesParse(t *testing.T) { 157 const fixedSeed = 1 158 const numOps = 10_000 159 metaDir := t.TempDir() 160 161 rng := rand.New(rand.NewSource(fixedSeed)) 162 ops := generate(rng, numOps, presetConfigs[0], newKeyManager()) 163 opsPath := filepath.Join(metaDir, "ops") 164 formattedOps := formatOps(ops) 165 require.NoError(t, os.WriteFile(opsPath, []byte(formattedOps), 0644)) 166 167 runDir := filepath.Join(metaDir, "run") 168 require.NoError(t, os.MkdirAll(runDir, os.ModePerm)) 169 optionsPath := filepath.Join(runDir, "OPTIONS") 170 opts := defaultTestOptions() 171 opts.Opts.EnsureDefaults() 172 opts.Opts.Cleaner = pebble.ArchiveCleaner{} 173 optionsStr := optionsToString(opts) 174 require.NoError(t, os.WriteFile(optionsPath, []byte(optionsStr), 0644)) 175 176 RunOnce(t, runDir, fixedSeed, filepath.Join(runDir, "history"), KeepData{}) 177 var foundTableBlockProperty bool 178 require.NoError(t, filepath.Walk(filepath.Join(runDir, "data"), 179 func(path string, info fs.FileInfo, err error) error { 180 if err != nil { 181 return err 182 } 183 if filepath.Ext(path) != ".sst" { 184 return nil 185 } 186 f, err := vfs.Default.Open(path) 187 if err != nil { 188 return err 189 } 190 readable, err := sstable.NewSimpleReadable(f) 191 if err != nil { 192 return err 193 } 194 r, err := sstable.NewReader(readable, opts.Opts.MakeReaderOptions()) 195 if err != nil { 196 return err 197 } 198 _, ok := r.Properties.UserProperties[opts.Opts.BlockPropertyCollectors[0]().Name()] 199 foundTableBlockProperty = foundTableBlockProperty || ok 200 return r.Close() 201 })) 202 require.True(t, foundTableBlockProperty) 203 } 204 205 func TestCustomOptionParser(t *testing.T) { 206 customOptionParsers := map[string]func(string) (CustomOption, bool){ 207 "foo": func(value string) (CustomOption, bool) { 208 return testCustomOption{name: "foo", value: value}, true 209 }, 210 } 211 212 o1 := defaultTestOptions() 213 o2 := defaultTestOptions() 214 215 require.NoError(t, parseOptions(o1, ` 216 [TestOptions] 217 foo=bar 218 `, customOptionParsers)) 219 require.NoError(t, parseOptions(o2, optionsToString(o1), customOptionParsers)) 220 defer o2.Opts.Cache.Unref() 221 222 for _, o := range []*TestOptions{o1, o2} { 223 require.Equal(t, 1, len(o.CustomOpts)) 224 require.Equal(t, "foo", o.CustomOpts[0].Name()) 225 require.Equal(t, "bar", o.CustomOpts[0].Value()) 226 } 227 } 228 229 type testCustomOption struct { 230 name, value string 231 } 232 233 func (o testCustomOption) Name() string { return o.name } 234 func (o testCustomOption) Value() string { return o.value } 235 func (o testCustomOption) Close(*pebble.Options) error { return nil } 236 func (o testCustomOption) Open(*pebble.Options) error { return nil }