github.com/zuoyebang/bitalostable@v1.0.1-0.20240229032404-e3b99a834294/internal/metamorphic/meta_test.go (about) 1 // Copyright 2019 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 "flag" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "os" 13 "os/exec" 14 "path" 15 "path/filepath" 16 "strings" 17 "testing" 18 "time" 19 20 "github.com/pmezard/go-difflib/difflib" 21 "github.com/stretchr/testify/require" 22 "github.com/zuoyebang/bitalostable" 23 "github.com/zuoyebang/bitalostable/internal/base" 24 "github.com/zuoyebang/bitalostable/internal/errorfs" 25 "github.com/zuoyebang/bitalostable/internal/randvar" 26 "github.com/zuoyebang/bitalostable/internal/testkeys" 27 "github.com/zuoyebang/bitalostable/vfs" 28 "golang.org/x/exp/rand" 29 ) 30 31 // TODO(peter): 32 // 33 // Miscellaneous: 34 // - Add support for different comparers. In particular, allow reverse 35 // comparers and a comparer which supports Comparer.Split (by splitting off 36 // a variable length suffix). 37 // - DeleteRange can be used to replace Delete, stressing the DeleteRange 38 // implementation. 39 // - Add support for Writer.LogData 40 41 var ( 42 dir = flag.String("dir", "_meta", 43 "the directory storing test state") 44 fs = flag.String("fs", "rand", 45 `force the tests to use either memory or disk-backed filesystems (valid: "mem", "disk", "rand")`) 46 // TODO: default error rate to a non-zero value. Currently, retrying is 47 // non-deterministic because of the Ierator.*WithLimit() methods since 48 // they may say that the Iterator is not valid, but be positioned at a 49 // certain key that can be returned in the future if the limit is changed. 50 // Since that key is hidden from clients of Iterator, the retryableIter 51 // using SeekGE will not necessarily position the Iterator that saw an 52 // injected error at the same place as an Iterator that did not see that 53 // error. 54 errorRate = flag.Float64("error-rate", 0.0, 55 "rate of errors injected into filesystem operations (0 ≤ r < 1)") 56 failRE = flag.String("fail", "", 57 "fail the test if the supplied regular expression matches the output") 58 traceFile = flag.String("trace-file", "", 59 "write an execution trace to `<run-dir>/file`") 60 keep = flag.Bool("keep", false, 61 "keep the DB directory even on successful runs") 62 seed = flag.Uint64("seed", 0, 63 "a pseudorandom number generator seed") 64 ops = randvar.NewFlag("uniform:5000-10000") 65 runDir = flag.String("run-dir", "", 66 "the specific configuration to (re-)run (used for post-mortem debugging)") 67 compare = flag.String("compare", "", 68 `comma separated list of options files to compare. The result of each run is compared with 69 the result of the run from the first options file in the list. Example, -compare 70 random-003,standard-000. The dir flag should have the directory containing these directories. 71 Example, -dir _meta/200610-203012.077`) 72 73 // The following options may be used for split-version metamorphic testing. 74 // To perform split-version testing, the client runs the metamorphic tests 75 // on an earlier Pebble SHA passing the `--keep` flag. The client then 76 // switches to the later Pebble SHA, setting the below options to point to 77 // the `ops` file and one of the previous run's data directories. 78 previousOps = flag.String("previous-ops", "", 79 "path to an ops file, used to prepopulate the set of keys operations draw from") 80 initialStatePath = flag.String("initial-state", "", 81 "path to a database's data directory, used to prepopulate the test run's databases") 82 initialStateDesc = flag.String("initial-state-desc", "", 83 `a human-readable description of the initial database state. 84 If set this parameter is written to the OPTIONS to aid in 85 debugging. It's intended to describe the lineage of a 86 database's state, including sufficient information for 87 reproduction (eg, SHA, prng seed, etc).`) 88 ) 89 90 func init() { 91 flag.Var(ops, "ops", "") 92 } 93 94 func testCompareRun(t *testing.T, compare string) { 95 runDirs := strings.Split(compare, ",") 96 historyPaths := make([]string, len(runDirs)) 97 for i := 0; i < len(runDirs); i++ { 98 historyPath := filepath.Join(*dir, runDirs[i]+"-"+time.Now().Format("060102-150405.000")) 99 runDirs[i] = filepath.Join(*dir, runDirs[i]) 100 _ = os.Remove(historyPath) 101 historyPaths[i] = historyPath 102 } 103 defer func() { 104 for _, path := range historyPaths { 105 _ = os.Remove(path) 106 } 107 }() 108 109 for i, runDir := range runDirs { 110 testMetaRun(t, runDir, *seed, historyPaths[i]) 111 } 112 113 if t.Failed() { 114 return 115 } 116 117 i, diff := CompareHistories(t, historyPaths) 118 if i != 0 { 119 fmt.Printf(` 120 ===== DIFF ===== 121 %s/{%s,%s} 122 %s 123 `, *dir, runDirs[0], runDirs[i], diff) 124 os.Exit(1) 125 } 126 } 127 128 func testMetaRun(t *testing.T, runDir string, seed uint64, historyPath string) { 129 opsPath := filepath.Join(filepath.Dir(filepath.Clean(runDir)), "ops") 130 opsData, err := ioutil.ReadFile(opsPath) 131 require.NoError(t, err) 132 133 ops, err := parse(opsData) 134 require.NoError(t, err) 135 _ = ops 136 137 optionsPath := filepath.Join(runDir, "OPTIONS") 138 optionsData, err := ioutil.ReadFile(optionsPath) 139 require.NoError(t, err) 140 141 opts := &bitalostable.Options{} 142 testOpts := &testOptions{opts: opts} 143 require.NoError(t, parseOptions(testOpts, string(optionsData))) 144 145 // Always use our custom comparer which provides a Split method, splitting 146 // keys at the trailing '@'. 147 opts.Comparer = testkeys.Comparer 148 // Use an archive cleaner to ease post-mortem debugging. 149 opts.Cleaner = base.ArchiveCleaner{} 150 151 // Set up the filesystem to use for the test. Note that by default we use an 152 // in-memory FS. 153 if testOpts.useDisk { 154 opts.FS = vfs.Default 155 require.NoError(t, os.RemoveAll(opts.FS.PathJoin(runDir, "data"))) 156 } else { 157 opts.Cleaner = base.ArchiveCleaner{} 158 if testOpts.strictFS { 159 opts.FS = vfs.NewStrictMem() 160 } else { 161 opts.FS = vfs.NewMem() 162 } 163 } 164 165 dir := opts.FS.PathJoin(runDir, "data") 166 // Set up the initial database state if configured to start from a non-empty 167 // database. By default tests start from an empty database, but split 168 // version testing may configure a previous metamorphic tests's database 169 // state as the initial state. 170 if testOpts.initialStatePath != "" { 171 require.NoError(t, setupInitialState(dir, testOpts)) 172 } 173 174 // Wrap the filesystem with one that will inject errors into read 175 // operations with *errorRate probability. 176 opts.FS = errorfs.Wrap(opts.FS, errorfs.WithProbability(errorfs.OpKindRead, *errorRate)) 177 178 if opts.WALDir != "" { 179 opts.WALDir = opts.FS.PathJoin(runDir, opts.WALDir) 180 } 181 182 historyFile, err := os.Create(historyPath) 183 require.NoError(t, err) 184 defer historyFile.Close() 185 writers := []io.Writer{historyFile} 186 187 if testing.Verbose() { 188 writers = append(writers, os.Stdout) 189 } 190 h := newHistory(*failRE, writers...) 191 192 m := newTest(ops) 193 require.NoError(t, m.init(h, dir, testOpts)) 194 for m.step(h) { 195 if err := h.Error(); err != nil { 196 fmt.Fprintf(os.Stderr, "Seed: %d\n", seed) 197 fmt.Fprintln(os.Stderr, err) 198 m.maybeSaveData() 199 os.Exit(1) 200 } 201 } 202 203 if *keep && !testOpts.useDisk { 204 m.maybeSaveData() 205 } 206 } 207 208 // TestMeta generates a random set of operations to run, then runs the test 209 // with different options. See standardOptions() for the set of options that 210 // are always run, and randomOptions() for the randomly generated options. The 211 // number of operations to generate is determined by the `--ops` flag. If a 212 // failure occurs, the output is kept in `_meta/<test>`, though note that a 213 // subsequent invocation will overwrite that output. A test can be re-run by 214 // using the `--run-dir` flag. For example: 215 // 216 // go test -v -run TestMeta --run-dir _meta/standard-017 217 // 218 // This will reuse the existing operations present in _meta/ops, rather than 219 // generating a new set. 220 // 221 // The generated operations and options are generated deterministically from a 222 // pseudorandom number generator seed. If a failure occurs, the seed is 223 // printed, and the full suite of tests may be re-run using the `--seed` flag: 224 // 225 // go test -v -run TestMeta --seed 1594395154492165000 226 // 227 // This will generate a new `_meta/<test>` directory, with the same operations 228 // and options. This must be run on the same commit SHA as the original 229 // failure, otherwise changes to the metamorphic tests may cause the generated 230 // operations and options to differ. 231 func TestMeta(t *testing.T) { 232 if *compare != "" { 233 testCompareRun(t, *compare) 234 return 235 } 236 237 if *runDir != "" { 238 // The --run-dir flag is specified either in the child process (see 239 // runOptions() below) or the user specified it manually in order to re-run 240 // a test. 241 testMetaRun(t, *runDir, *seed, filepath.Join(*runDir, "history")) 242 return 243 } 244 245 // Setting the default seed here rather than in the flag's default value 246 // ensures each run uses a new seed when using the Go test `-count` flag. 247 seed := *seed 248 if seed == 0 { 249 seed = uint64(time.Now().UnixNano()) 250 } 251 252 rootName := t.Name() 253 254 // Cleanup any previous state. 255 metaDir := filepath.Join(*dir, time.Now().Format("060102-150405.000")) 256 require.NoError(t, os.RemoveAll(metaDir)) 257 require.NoError(t, os.MkdirAll(metaDir, 0755)) 258 defer func() { 259 if !t.Failed() && !*keep { 260 _ = os.RemoveAll(metaDir) 261 } 262 }() 263 264 rng := rand.New(rand.NewSource(seed)) 265 opCount := ops.Uint64(rng) 266 267 // Generate a new set of random ops, writing them to <dir>/ops. These will be 268 // read by the child processes when performing a test run. 269 km := newKeyManager() 270 cfg := defaultConfig() 271 if *previousOps != "" { 272 // During split-version testing, we load keys from an `ops` file 273 // produced by a metamorphic test run of an earlier Pebble version. 274 // Seeding the keys ensure we generate interesting operations, including 275 // ones with key shadowing, merging, etc. 276 opsPath := filepath.Join(filepath.Dir(filepath.Clean(*previousOps)), "ops") 277 opsData, err := ioutil.ReadFile(opsPath) 278 require.NoError(t, err) 279 ops, err := parse(opsData) 280 require.NoError(t, err) 281 loadPrecedingKeys(t, ops, &cfg, km) 282 } 283 ops := generate(rng, opCount, cfg, km) 284 opsPath := filepath.Join(metaDir, "ops") 285 formattedOps := formatOps(ops) 286 require.NoError(t, ioutil.WriteFile(opsPath, []byte(formattedOps), 0644)) 287 288 // Perform a particular test run with the specified options. The options are 289 // written to <run-dir>/OPTIONS and a child process is created to actually 290 // execute the test. 291 runOptions := func(t *testing.T, opts *testOptions) { 292 if opts.opts.Cache != nil { 293 defer opts.opts.Cache.Unref() 294 } 295 296 runDir := filepath.Join(metaDir, path.Base(t.Name())) 297 require.NoError(t, os.MkdirAll(runDir, 0755)) 298 299 // If the filesystem type was forced, all tests will use that value. 300 switch *fs { 301 case "rand": 302 // No-op. Use the generated value for the filesystem. 303 case "disk": 304 opts.useDisk = true 305 case "mem": 306 opts.useDisk = false 307 default: 308 t.Fatalf("unknown forced filesystem type: %q", *fs) 309 } 310 311 optionsPath := filepath.Join(runDir, "OPTIONS") 312 optionsStr := optionsToString(opts) 313 require.NoError(t, ioutil.WriteFile(optionsPath, []byte(optionsStr), 0644)) 314 315 args := []string{ 316 "-keep=" + fmt.Sprint(*keep), 317 "-run-dir=" + runDir, 318 "-test.run=" + rootName + "$", 319 } 320 if *traceFile != "" { 321 args = append(args, "-test.trace="+filepath.Join(runDir, *traceFile)) 322 } 323 324 cmd := exec.Command(os.Args[0], args...) 325 out, err := cmd.CombinedOutput() 326 if err != nil { 327 t.Fatalf(` 328 ===== SEED ===== 329 %d 330 ===== ERR ===== 331 %v 332 ===== OUT ===== 333 %s 334 ===== OPTIONS ===== 335 %s 336 ===== OPS ===== 337 %s 338 ===== HISTORY ===== 339 %s`, seed, err, out, optionsStr, formattedOps, readFile(filepath.Join(runDir, "history"))) 340 } 341 } 342 343 // Create the standard options. 344 var names []string 345 options := map[string]*testOptions{} 346 for i, opts := range standardOptions() { 347 name := fmt.Sprintf("standard-%03d", i) 348 names = append(names, name) 349 options[name] = opts 350 } 351 352 // Create random options. We make an arbitrary choice to run with as many 353 // random options as we have standard options. 354 nOpts := len(options) 355 for i := 0; i < nOpts; i++ { 356 name := fmt.Sprintf("random-%03d", i) 357 names = append(names, name) 358 opts := randomOptions(rng) 359 options[name] = opts 360 } 361 362 // If the user provided the path to an initial database state to use, update 363 // all the options to pull from it. 364 if *initialStatePath != "" { 365 for _, o := range options { 366 var err error 367 o.initialStatePath, err = filepath.Abs(*initialStatePath) 368 require.NoError(t, err) 369 o.initialStateDesc = *initialStateDesc 370 } 371 } 372 373 // Run the options. 374 for _, name := range names { 375 t.Run(name, func(t *testing.T) { 376 runOptions(t, options[name]) 377 }) 378 } 379 380 // Don't bother comparing output if we've already failed. 381 if t.Failed() { 382 return 383 } 384 385 getHistoryPath := func(name string) string { 386 return filepath.Join(metaDir, name, "history") 387 388 } 389 390 base := readHistory(t, getHistoryPath(names[0])) 391 for i := 1; i < len(names); i++ { 392 lines := readHistory(t, getHistoryPath(names[i])) 393 diff := difflib.UnifiedDiff{ 394 A: base, 395 B: lines, 396 Context: 5, 397 } 398 text, err := difflib.GetUnifiedDiffString(diff) 399 require.NoError(t, err) 400 if text != "" { 401 // NB: We force an exit rather than using t.Fatal because the latter 402 // will run another instance of the test if -count is specified, while 403 // we're happy to exit on the first failure. 404 optionsStrA := optionsToString(options[names[0]]) 405 optionsStrB := optionsToString(options[names[i]]) 406 407 fmt.Printf(` 408 ===== SEED ===== 409 %d 410 ===== DIFF ===== 411 %s/{%s,%s} 412 %s 413 ===== OPTIONS %s ===== 414 %s 415 ===== OPTIONS %s ===== 416 %s 417 ===== OPS ===== 418 %s 419 `, seed, metaDir, names[0], names[i], text, names[0], optionsStrA, names[i], optionsStrB, formattedOps) 420 os.Exit(1) 421 } 422 } 423 } 424 425 func readFile(path string) string { 426 history, err := ioutil.ReadFile(path) 427 if err != nil { 428 return fmt.Sprintf("err: %v", err) 429 } 430 431 return string(history) 432 }