LCOV - code coverage report
Current view: top level - pebble/metamorphic - meta.go (source / functions) Hit Total Coverage
Test: 2024-06-27 08:15Z 18b77232 - tests + meta.lcov Lines: 268 428 62.6 %
Date: 2024-06-27 08:16:54 Functions: 0 0 -

          Line data    Source code
       1             : // Copyright 2023 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 provides a testing framework for running randomized tests
       6             : // over multiple Pebble databases with varying configurations. Logically
       7             : // equivalent operations should result in equivalent output across all
       8             : // configurations.
       9             : package metamorphic
      10             : 
      11             : import (
      12             :         "context"
      13             :         "fmt"
      14             :         "io"
      15             :         "os"
      16             :         "os/exec"
      17             :         "path"
      18             :         "path/filepath"
      19             :         "regexp"
      20             :         "sort"
      21             :         "strconv"
      22             :         "testing"
      23             :         "time"
      24             : 
      25             :         "github.com/cockroachdb/errors"
      26             :         "github.com/cockroachdb/pebble/internal/base"
      27             :         "github.com/cockroachdb/pebble/internal/dsl"
      28             :         "github.com/cockroachdb/pebble/internal/randvar"
      29             :         "github.com/cockroachdb/pebble/vfs"
      30             :         "github.com/cockroachdb/pebble/vfs/errorfs"
      31             :         "github.com/pmezard/go-difflib/difflib"
      32             :         "github.com/stretchr/testify/require"
      33             :         "golang.org/x/exp/rand"
      34             :         "golang.org/x/sync/errgroup"
      35             : )
      36             : 
      37             : type runAndCompareOptions struct {
      38             :         seed              uint64
      39             :         ops               randvar.Static
      40             :         previousOpsPath   string
      41             :         initialStatePath  string
      42             :         initialStateDesc  string
      43             :         traceFile         string
      44             :         innerBinary       string
      45             :         mutateTestOptions []func(*TestOptions)
      46             :         customRuns        map[string]string
      47             :         numInstances      int
      48             :         runOnceOptions
      49             : }
      50             : 
      51             : // A RunOption configures the behavior of RunAndCompare.
      52             : type RunOption interface {
      53             :         apply(*runAndCompareOptions)
      54             : }
      55             : 
      56             : // Seed configures generation to use the provided seed. Seed may be used to
      57             : // deterministically reproduce the same run.
      58             : type Seed uint64
      59             : 
      60           1 : func (s Seed) apply(ro *runAndCompareOptions) { ro.seed = uint64(s) }
      61             : 
      62             : // ExtendPreviousRun configures RunAndCompare to use the output of a previous
      63             : // metamorphic test run to seed the this run. It's used in the crossversion
      64             : // metamorphic tests, in which a data directory is upgraded through multiple
      65             : // versions of Pebble, exercising upgrade code paths and cross-version
      66             : // compatibility.
      67             : //
      68             : // The opsPath should be the filesystem path to the ops file containing the
      69             : // operations run within the previous iteration of the metamorphic test. It's
      70             : // used to inform operation generation to prefer using keys used in the previous
      71             : // run, which are therefore more likely to be "interesting."
      72             : //
      73             : // The initialStatePath argument should be the filesystem path to the data
      74             : // directory containing the database where the previous run of the metamorphic
      75             : // test left off.
      76             : //
      77             : // The initialStateDesc argument is presentational and should hold a
      78             : // human-readable description of the initial state.
      79           0 : func ExtendPreviousRun(opsPath, initialStatePath, initialStateDesc string) RunOption {
      80           0 :         return closureOpt(func(ro *runAndCompareOptions) {
      81           0 :                 ro.previousOpsPath = opsPath
      82           0 :                 ro.initialStatePath = initialStatePath
      83           0 :                 ro.initialStateDesc = initialStateDesc
      84           0 :         })
      85             : }
      86             : 
      87             : var (
      88             :         // UseDisk configures RunAndCompare to use the physical filesystem for all
      89             :         // generated runs.
      90           0 :         UseDisk = closureOpt(func(ro *runAndCompareOptions) {
      91           0 :                 ro.mutateTestOptions = append(ro.mutateTestOptions, func(to *TestOptions) { to.useDisk = true })
      92             :         })
      93             :         // UseInMemory configures RunAndCompare to use an in-memory virtual
      94             :         // filesystem for all generated runs.
      95           0 :         UseInMemory = closureOpt(func(ro *runAndCompareOptions) {
      96           0 :                 ro.mutateTestOptions = append(ro.mutateTestOptions, func(to *TestOptions) { to.useDisk = false })
      97             :         })
      98             : )
      99             : 
     100             : // OpCount configures the random variable for the number of operations to
     101             : // generate.
     102           1 : func OpCount(rv randvar.Static) RunOption {
     103           1 :         return closureOpt(func(ro *runAndCompareOptions) { ro.ops = rv })
     104             : }
     105             : 
     106             : // RuntimeTrace configures each test run to collect a runtime trace and output
     107             : // it with the provided filename.
     108           0 : func RuntimeTrace(name string) RunOption {
     109           0 :         return closureOpt(func(ro *runAndCompareOptions) { ro.traceFile = name })
     110             : }
     111             : 
     112             : // InnerBinary configures the binary that is called for each run. If not
     113             : // specified, this binary (os.Args[0]) is called.
     114           0 : func InnerBinary(path string) RunOption {
     115           0 :         return closureOpt(func(ro *runAndCompareOptions) { ro.innerBinary = path })
     116             : }
     117             : 
     118             : // ParseCustomTestOption adds support for parsing the provided CustomOption from
     119             : // OPTIONS files serialized by the metamorphic tests. This RunOption alone does
     120             : // not cause the metamorphic tests to run with any variant of the provided
     121             : // CustomOption set.
     122           0 : func ParseCustomTestOption(name string, parseFn func(value string) (CustomOption, bool)) RunOption {
     123           0 :         return closureOpt(func(ro *runAndCompareOptions) { ro.customOptionParsers[name] = parseFn })
     124             : }
     125             : 
     126             : // AddCustomRun adds an additional run of the metamorphic tests, using the
     127             : // provided OPTIONS file contents. The default options will be used, except
     128             : // those options that are overriden by the provided OPTIONS string.
     129           0 : func AddCustomRun(name string, serializedOptions string) RunOption {
     130           0 :         return closureOpt(func(ro *runAndCompareOptions) { ro.customRuns[name] = serializedOptions })
     131             : }
     132             : 
     133             : type closureOpt func(*runAndCompareOptions)
     134             : 
     135           1 : func (f closureOpt) apply(ro *runAndCompareOptions) { f(ro) }
     136             : 
     137           1 : func buildRunAndCompareOpts(rOpts []RunOption) runAndCompareOptions {
     138           1 :         runOpts := runAndCompareOptions{
     139           1 :                 ops:        randvar.NewUniform(1000, 10000),
     140           1 :                 customRuns: map[string]string{},
     141           1 :                 runOnceOptions: runOnceOptions{
     142           1 :                         customOptionParsers: map[string]func(string) (CustomOption, bool){},
     143           1 :                 },
     144           1 :         }
     145           1 :         for _, o := range rOpts {
     146           1 :                 o.apply(&runOpts)
     147           1 :         }
     148           1 :         if runOpts.seed == 0 {
     149           1 :                 runOpts.seed = uint64(time.Now().UnixNano())
     150           1 :         }
     151           1 :         return runOpts
     152             : }
     153             : 
     154             : // RunAndCompare runs the metamorphic tests, using the provided root directory
     155             : // to hold test data.
     156           1 : func RunAndCompare(t *testing.T, rootDir string, rOpts ...RunOption) {
     157           1 :         runOpts := buildRunAndCompareOpts(rOpts)
     158           1 :         require.NoError(t, os.MkdirAll(rootDir, 0755))
     159           1 :         metaDir, err := os.MkdirTemp(rootDir, time.Now().Format("060102-150405.000"))
     160           1 :         require.NoError(t, err)
     161           1 :         require.NoError(t, os.MkdirAll(metaDir, 0755))
     162           1 :         defer func() {
     163           1 :                 if !t.Failed() && !runOpts.keep {
     164           1 :                         _ = os.RemoveAll(metaDir)
     165           1 :                 }
     166             :         }()
     167             : 
     168           1 :         topLevelTestName := t.Name()
     169           1 :         for path.Dir(topLevelTestName) != "." {
     170           0 :                 topLevelTestName = path.Dir(topLevelTestName)
     171           0 :         }
     172             : 
     173           1 :         rng := rand.New(rand.NewSource(runOpts.seed))
     174           1 :         opCount := runOpts.ops.Uint64(rng)
     175           1 : 
     176           1 :         // Generate a new set of random ops, writing them to <dir>/ops. These will be
     177           1 :         // read by the child processes when performing a test run.
     178           1 :         km := newKeyManager(runOpts.numInstances)
     179           1 :         cfg := presetConfigs[rng.Intn(len(presetConfigs))]
     180           1 :         if runOpts.previousOpsPath != "" {
     181           0 :                 // During cross-version testing, we load keys from an `ops` file
     182           0 :                 // produced by a metamorphic test run of an earlier Pebble version.
     183           0 :                 // Seeding the keys ensure we generate interesting operations, including
     184           0 :                 // ones with key shadowing, merging, etc.
     185           0 :                 opsPath := filepath.Join(filepath.Dir(filepath.Clean(runOpts.previousOpsPath)), "ops")
     186           0 :                 opsData, err := os.ReadFile(opsPath)
     187           0 :                 require.NoError(t, err)
     188           0 :                 ops, err := parse(opsData, parserOpts{})
     189           0 :                 require.NoError(t, err)
     190           0 :                 loadPrecedingKeys(t, ops, &cfg, km)
     191           0 :         }
     192           1 :         if runOpts.numInstances > 1 {
     193           1 :                 // The multi-instance variant does not support all operations yet.
     194           1 :                 //
     195           1 :                 // TODO(bilal): Address this and use the default configs.
     196           1 :                 cfg = multiInstancePresetConfig
     197           1 :                 cfg.numInstances = runOpts.numInstances
     198           1 :         }
     199           1 :         ops := generate(rng, opCount, cfg, km)
     200           1 :         opsPath := filepath.Join(metaDir, "ops")
     201           1 :         formattedOps := formatOps(ops)
     202           1 :         require.NoError(t, os.WriteFile(opsPath, []byte(formattedOps), 0644))
     203           1 : 
     204           1 :         // runOptions performs a particular test run with the specified options. The
     205           1 :         // options are written to <run-dir>/OPTIONS and a child process is created to
     206           1 :         // actually execute the test.
     207           1 :         runOptions := func(t *testing.T, opts *TestOptions) {
     208           1 :                 if opts.Opts.Cache != nil {
     209           1 :                         defer opts.Opts.Cache.Unref()
     210           1 :                 }
     211           1 :                 for _, fn := range runOpts.mutateTestOptions {
     212           0 :                         fn(opts)
     213           0 :                 }
     214           1 :                 runDir := filepath.Join(metaDir, path.Base(t.Name()))
     215           1 :                 require.NoError(t, os.MkdirAll(runDir, 0755))
     216           1 : 
     217           1 :                 optionsPath := filepath.Join(runDir, "OPTIONS")
     218           1 :                 optionsStr := optionsToString(opts)
     219           1 :                 require.NoError(t, os.WriteFile(optionsPath, []byte(optionsStr), 0644))
     220           1 : 
     221           1 :                 args := []string{
     222           1 :                         "-keep=" + fmt.Sprint(runOpts.keep),
     223           1 :                         "-run-dir=" + runDir,
     224           1 :                         "-test.run=" + topLevelTestName + "$",
     225           1 :                         "--op-timeout=" + runOpts.opTimeout.String(),
     226           1 :                 }
     227           1 :                 if runOpts.numInstances > 1 {
     228           1 :                         args = append(args, "--num-instances="+strconv.Itoa(runOpts.numInstances))
     229           1 :                 }
     230           1 :                 if runOpts.traceFile != "" {
     231           0 :                         args = append(args, "-test.trace="+filepath.Join(runDir, runOpts.traceFile))
     232           0 :                 }
     233             : 
     234           1 :                 binary := os.Args[0]
     235           1 :                 if runOpts.innerBinary != "" {
     236           0 :                         binary = runOpts.innerBinary
     237           0 :                 }
     238           1 :                 cmd := exec.Command(binary, args...)
     239           1 :                 out, err := cmd.CombinedOutput()
     240           1 :                 if err != nil {
     241           0 :                         t.Fatalf(`
     242           0 : ===== SEED =====
     243           0 : %d
     244           0 : ===== ERR =====
     245           0 : %v
     246           0 : ===== OUT =====
     247           0 : %s
     248           0 : ===== OPTIONS =====
     249           0 : %s
     250           0 : ===== OPS =====
     251           0 : %s
     252           0 : ===== HISTORY =====
     253           0 : %s
     254           0 : To reduce:  go test ./internal/metamorphic -tags invariants -run '%s$' --run-dir %s --try-to-reduce -v`,
     255           0 :                                 runOpts.seed,
     256           0 :                                 err,
     257           0 :                                 out,
     258           0 :                                 optionsStr,
     259           0 :                                 formattedOps,
     260           0 :                                 readFile(filepath.Join(runDir, "history")),
     261           0 :                                 topLevelTestName, runDir,
     262           0 :                         )
     263           0 :                 }
     264             :         }
     265             : 
     266             :         // Construct the various OPTIONS to test with.
     267           1 :         names, options, err := buildOptions(rng, runOpts)
     268           1 :         if err != nil {
     269           0 :                 t.Fatal(err)
     270           0 :         }
     271             : 
     272             :         // Run the options.
     273           1 :         t.Run("execution", func(t *testing.T) {
     274           1 :                 for _, name := range names {
     275           1 :                         name := name
     276           1 :                         t.Run(name, func(t *testing.T) {
     277           1 :                                 t.Parallel()
     278           1 :                                 runOptions(t, options[name])
     279           1 :                         })
     280             :                 }
     281             :         })
     282             :         // NB: The above 'execution' subtest will not complete until all of the
     283             :         // individual execution/ subtests have completed. The grouping within the
     284             :         // `execution` subtest ensures all the histories are available when we
     285             :         // proceed to comparing against the base history.
     286             : 
     287             :         // Don't bother comparing output if we've already failed.
     288           1 :         if t.Failed() {
     289           0 :                 return
     290           0 :         }
     291             : 
     292           1 :         t.Run("compare", func(t *testing.T) {
     293           1 :                 getHistoryPath := func(name string) string {
     294           1 :                         return filepath.Join(metaDir, name, "history")
     295           1 :                 }
     296             : 
     297           1 :                 base := readHistory(t, getHistoryPath(names[0]))
     298           1 :                 base = reorderHistory(base)
     299           1 :                 for i := 1; i < len(names); i++ {
     300           1 :                         t.Run(names[i], func(t *testing.T) {
     301           1 :                                 lines := readHistory(t, getHistoryPath(names[i]))
     302           1 :                                 lines = reorderHistory(lines)
     303           1 :                                 diff := difflib.UnifiedDiff{
     304           1 :                                         A:       base,
     305           1 :                                         B:       lines,
     306           1 :                                         Context: 5,
     307           1 :                                 }
     308           1 :                                 text, err := difflib.GetUnifiedDiffString(diff)
     309           1 :                                 require.NoError(t, err)
     310           1 :                                 if text != "" {
     311           0 :                                         // NB: We force an exit rather than using t.Fatal because the latter
     312           0 :                                         // will run another instance of the test if -count is specified, while
     313           0 :                                         // we're happy to exit on the first failure.
     314           0 :                                         optionsStrA := optionsToString(options[names[0]])
     315           0 :                                         optionsStrB := optionsToString(options[names[i]])
     316           0 : 
     317           0 :                                         fmt.Printf(`
     318           0 : ===== SEED =====
     319           0 : %d
     320           0 : ===== DIFF =====
     321           0 : %s/{%s,%s}
     322           0 : %s
     323           0 : ===== OPTIONS %s =====
     324           0 : %s
     325           0 : ===== OPTIONS %s =====
     326           0 : %s
     327           0 : ===== OPS =====
     328           0 : %s
     329           0 : To reduce:  go test ./internal/metamorphic -tags invariants -run '%s$' --compare "%s/{%s,%s}" --try-to-reduce -v
     330           0 : `,
     331           0 :                                                 runOpts.seed,
     332           0 :                                                 metaDir, names[0], names[i], text,
     333           0 :                                                 names[0], optionsStrA,
     334           0 :                                                 names[i], optionsStrB,
     335           0 :                                                 formattedOps,
     336           0 :                                                 topLevelTestName, metaDir, names[0], names[i])
     337           0 :                                         os.Exit(1)
     338           0 :                                 }
     339             :                         })
     340             :                 }
     341             :         })
     342             : }
     343             : 
     344             : func buildOptions(
     345             :         rng *rand.Rand, runOpts runAndCompareOptions,
     346           1 : ) (names []string, options map[string]*TestOptions, err error) {
     347           1 :         options = map[string]*TestOptions{}
     348           1 : 
     349           1 :         // Create the standard options.
     350           1 :         for i, opts := range standardOptions() {
     351           1 :                 name := fmt.Sprintf("standard-%03d", i)
     352           1 :                 names = append(names, name)
     353           1 :                 options[name] = opts
     354           1 :         }
     355             : 
     356             :         // Create the custom option runs, if any.
     357           1 :         for name, customOptsStr := range runOpts.customRuns {
     358           0 :                 options[name] = defaultTestOptions()
     359           0 :                 if err = parseOptions(options[name], customOptsStr, runOpts.customOptionParsers); err != nil {
     360           0 :                         return nil, nil, errors.Wrapf(err, "custom opts %q: %s", name, err)
     361           0 :                 }
     362             :         }
     363             :         // Sort the custom options names for determinism (they're currently in
     364             :         // random order from map iteration).
     365           1 :         sort.Strings(names[len(names)-len(runOpts.customRuns):])
     366           1 : 
     367           1 :         // Create random options. We make an arbitrary choice to run with as many
     368           1 :         // random options as we have standard options.
     369           1 :         nOpts := len(options)
     370           1 :         for i := 0; i < nOpts; i++ {
     371           1 :                 name := fmt.Sprintf("random-%03d", i)
     372           1 :                 names = append(names, name)
     373           1 :                 opts := RandomOptions(rng, runOpts.customOptionParsers)
     374           1 :                 options[name] = opts
     375           1 :         }
     376             : 
     377             :         // If the user provided the path to an initial database state to use, update
     378             :         // all the options to pull from it.
     379           1 :         if runOpts.initialStatePath != "" {
     380           0 :                 for _, o := range options {
     381           0 :                         o.initialStatePath, err = filepath.Abs(runOpts.initialStatePath)
     382           0 :                         if err != nil {
     383           0 :                                 return nil, nil, err
     384           0 :                         }
     385           0 :                         o.initialStateDesc = runOpts.initialStateDesc
     386             :                 }
     387             :         }
     388           1 :         return names, options, nil
     389             : }
     390             : 
     391             : type runOnceOptions struct {
     392             :         // Note: when adding a new option, a new flag might need to be passed to the
     393             :         // "inner" execution of the test binary in RunAndCompare.
     394             : 
     395             :         keep       bool
     396             :         maxThreads int
     397             :         // opTimeout causes the test to fail if any one op takes longer than this.
     398             :         opTimeout           time.Duration
     399             :         errorRate           float64
     400             :         failRegexp          *regexp.Regexp
     401             :         numInstances        int
     402             :         customOptionParsers map[string]func(string) (CustomOption, bool)
     403             : }
     404             : 
     405             : // A RunOnceOption configures the behavior of a single run of the metamorphic
     406             : // tests.
     407             : type RunOnceOption interface {
     408             :         applyOnce(*runOnceOptions)
     409             : }
     410             : 
     411             : // KeepData keeps the database directory, even on successful runs. If the test
     412             : // used an in-memory filesystem, the in-memory filesystem will be persisted to
     413             : // the run directory.
     414             : type KeepData struct{}
     415             : 
     416           0 : func (KeepData) apply(ro *runAndCompareOptions) { ro.keep = true }
     417           1 : func (KeepData) applyOnce(ro *runOnceOptions)   { ro.keep = true }
     418             : 
     419             : // InjectErrorsRate configures the run to inject errors into read-only
     420             : // filesystem operations and retry injected errors.
     421             : type InjectErrorsRate float64
     422             : 
     423           0 : func (r InjectErrorsRate) apply(ro *runAndCompareOptions) { ro.errorRate = float64(r) }
     424           0 : func (r InjectErrorsRate) applyOnce(ro *runOnceOptions)   { ro.errorRate = float64(r) }
     425             : 
     426             : // MaxThreads sets an upper bound on the number of parallel execution threads
     427             : // during replay.
     428             : type MaxThreads int
     429             : 
     430           1 : func (m MaxThreads) apply(ro *runAndCompareOptions) { ro.maxThreads = int(m) }
     431           1 : func (m MaxThreads) applyOnce(ro *runOnceOptions)   { ro.maxThreads = int(m) }
     432             : 
     433             : // OpTimeout sets a timeout for each executed operation. A value of 0 means no
     434             : // timeout.
     435             : type OpTimeout time.Duration
     436             : 
     437           1 : func (t OpTimeout) apply(ro *runAndCompareOptions) { ro.opTimeout = time.Duration(t) }
     438           1 : func (t OpTimeout) applyOnce(ro *runOnceOptions)   { ro.opTimeout = time.Duration(t) }
     439             : 
     440             : // FailOnMatch configures the run to fail immediately if the history matches the
     441             : // provided regular expression.
     442             : type FailOnMatch struct {
     443             :         *regexp.Regexp
     444             : }
     445             : 
     446           0 : func (f FailOnMatch) apply(ro *runAndCompareOptions) { ro.failRegexp = f.Regexp }
     447           0 : func (f FailOnMatch) applyOnce(ro *runOnceOptions)   { ro.failRegexp = f.Regexp }
     448             : 
     449             : // MultiInstance configures the number of pebble instances to create.
     450             : type MultiInstance int
     451             : 
     452           1 : func (m MultiInstance) apply(ro *runAndCompareOptions) { ro.numInstances = int(m) }
     453           1 : func (m MultiInstance) applyOnce(ro *runOnceOptions)   { ro.numInstances = int(m) }
     454             : 
     455             : // RunOnce performs one run of the metamorphic tests. RunOnce expects the
     456             : // directory named by `runDir` to already exist and contain an `OPTIONS` file
     457             : // containing the test run's configuration. The history of the run is persisted
     458             : // to a file at the path `historyPath`.
     459             : //
     460             : // The `seed` parameter is not functional; it's used for context in logging.
     461           2 : func RunOnce(t TestingT, runDir string, seed uint64, historyPath string, rOpts ...RunOnceOption) {
     462           2 :         runOpts := runOnceOptions{
     463           2 :                 customOptionParsers: map[string]func(string) (CustomOption, bool){},
     464           2 :         }
     465           2 :         for _, o := range rOpts {
     466           2 :                 o.applyOnce(&runOpts)
     467           2 :         }
     468             : 
     469           2 :         opsPath := filepath.Join(filepath.Dir(filepath.Clean(runDir)), "ops")
     470           2 :         opsData, err := os.ReadFile(opsPath)
     471           2 :         require.NoError(t, err)
     472           2 : 
     473           2 :         ops, err := parse(opsData, parserOpts{})
     474           2 :         require.NoError(t, err)
     475           2 : 
     476           2 :         optionsPath := filepath.Join(runDir, "OPTIONS")
     477           2 :         optionsData, err := os.ReadFile(optionsPath)
     478           2 :         require.NoError(t, err)
     479           2 : 
     480           2 :         // NB: It's important to use defaultTestOptions() here as the base into
     481           2 :         // which we parse the serialized options. It contains the relevant defaults,
     482           2 :         // like the appropriate block-property collectors.
     483           2 :         testOpts := defaultTestOptions()
     484           2 :         opts := testOpts.Opts
     485           2 :         require.NoError(t, parseOptions(testOpts, string(optionsData), runOpts.customOptionParsers))
     486           2 : 
     487           2 :         // Set up the filesystem to use for the test. Note that by default we use an
     488           2 :         // in-memory FS.
     489           2 :         if testOpts.useDisk {
     490           1 :                 opts.FS = vfs.Default
     491           1 :                 require.NoError(t, os.RemoveAll(opts.FS.PathJoin(runDir, "data")))
     492           2 :         } else {
     493           2 :                 opts.Cleaner = base.ArchiveCleaner{}
     494           2 :         }
     495             :         // Wrap the filesystem with a VFS that will inject random latency if
     496             :         // the test options require it.
     497           2 :         if testOpts.ioLatencyProbability > 0 {
     498           1 :                 opts.FS = errorfs.Wrap(opts.FS, errorfs.RandomLatency(
     499           1 :                         errorfs.Randomly(testOpts.ioLatencyProbability, testOpts.ioLatencySeed),
     500           1 :                         testOpts.ioLatencyMean,
     501           1 :                         testOpts.ioLatencySeed,
     502           1 :                 ))
     503           1 :         }
     504             : 
     505             :         // Wrap the filesystem with one that will inject errors into read
     506             :         // operations with *errorRate probability.
     507           2 :         opts.FS = errorfs.Wrap(opts.FS, errorfs.ErrInjected.If(
     508           2 :                 dsl.And(errorfs.Reads, errorfs.Randomly(runOpts.errorRate, int64(seed))),
     509           2 :         ))
     510           2 : 
     511           2 :         // Bound testOpts.threads to runOpts.maxThreads.
     512           2 :         if runOpts.maxThreads < testOpts.Threads {
     513           1 :                 testOpts.Threads = runOpts.maxThreads
     514           1 :         }
     515             : 
     516           2 :         dir := opts.FS.PathJoin(runDir, "data")
     517           2 :         // Set up the initial database state if configured to start from a non-empty
     518           2 :         // database. By default tests start from an empty database, but split
     519           2 :         // version testing may configure a previous metamorphic tests's database
     520           2 :         // state as the initial state.
     521           2 :         if testOpts.initialStatePath != "" {
     522           0 :                 require.NoError(t, setupInitialState(dir, testOpts))
     523           0 :         }
     524             : 
     525           2 :         if testOpts.Opts.WALFailover != nil {
     526           1 :                 if runOpts.numInstances > 1 {
     527           1 :                         // TODO(bilal,jackson): Allow opts to diverge on a per-instance
     528           1 :                         // basis, and use that to set unique WAL dirs for all instances in
     529           1 :                         // multi-instance mode.
     530           1 :                         testOpts.Opts.WALFailover = nil
     531           1 :                 } else {
     532           1 :                         testOpts.Opts.WALFailover.Secondary.FS = opts.FS
     533           1 :                         testOpts.Opts.WALFailover.Secondary.Dirname = opts.FS.PathJoin(
     534           1 :                                 runDir, testOpts.Opts.WALFailover.Secondary.Dirname)
     535           1 :                 }
     536             :         }
     537             : 
     538           2 :         if opts.WALDir != "" {
     539           1 :                 if runOpts.numInstances > 1 {
     540           1 :                         // TODO(bilal): Allow opts to diverge on a per-instance basis, and use
     541           1 :                         // that to set unique WAL dirs for all instances in multi-instance mode.
     542           1 :                         opts.WALDir = ""
     543           1 :                 } else {
     544           1 :                         opts.WALDir = opts.FS.PathJoin(runDir, opts.WALDir)
     545           1 :                 }
     546             :         }
     547             : 
     548           2 :         historyFile, err := os.Create(historyPath)
     549           2 :         require.NoError(t, err)
     550           2 :         defer historyFile.Close()
     551           2 :         writers := []io.Writer{historyFile}
     552           2 : 
     553           2 :         if testing.Verbose() {
     554           0 :                 writers = append(writers, os.Stdout)
     555           0 :         }
     556           2 :         h := newHistory(runOpts.failRegexp, writers...)
     557           2 :         defer h.Close()
     558           2 : 
     559           2 :         m := newTest(ops)
     560           2 :         require.NoError(t, m.init(h, dir, testOpts, runOpts.numInstances, runOpts.opTimeout))
     561           2 : 
     562           2 :         if err := Execute(m); err != nil {
     563           0 :                 fmt.Fprintf(os.Stderr, "Seed: %d\n", seed)
     564           0 :                 fmt.Fprintln(os.Stderr, err)
     565           0 :         }
     566             : 
     567             :         // If we have unclosed databases, print LSM details. This will be the case
     568             :         // when we use --try-to-reduce.
     569           2 :         for i, db := range m.dbs {
     570           2 :                 if db != nil {
     571           0 :                         fmt.Fprintf(os.Stderr, "\ndb%d:\n%s", i+1, db.DebugString())
     572           0 :                         fmt.Fprintf(os.Stderr, "\n%s\n", db.LSMViewURL())
     573           0 :                 }
     574             :         }
     575             :         // Don't let the test pass if it issued a Fatalf.
     576           2 :         err = errors.CombineErrors(err, h.Error())
     577           2 : 
     578           2 :         if err != nil {
     579           0 :                 m.saveInMemoryData()
     580           0 :                 os.Exit(1)
     581           0 :         }
     582             : 
     583           2 :         if runOpts.keep && !testOpts.useDisk {
     584           1 :                 m.saveInMemoryData()
     585           1 :         }
     586             : }
     587             : 
     588             : // Execute runs the provided test, writing the execution history into the Test's
     589             : // sink.
     590           2 : func Execute(m *Test) error {
     591           2 :         if m.testOpts.Threads <= 1 {
     592           2 :                 for m.step(m.h, nil /* optionalRecordf */) {
     593           2 :                         if err := m.h.Error(); err != nil {
     594           0 :                                 return err
     595           0 :                         }
     596             :                 }
     597           2 :                 return nil
     598             :         }
     599             : 
     600             :         // Run in parallel using an errgroup.
     601           2 :         eg, ctx := errgroup.WithContext(context.Background())
     602           2 :         for t := 0; t < m.testOpts.Threads; t++ {
     603           2 :                 t := t // bind loop var to scope
     604           2 :                 eg.Go(func() error {
     605           2 :                         for idx := 0; idx < len(m.ops); idx++ {
     606           2 :                                 // Skip any operations whose receiver object hashes to a
     607           2 :                                 // different thread. All operations with the same receiver
     608           2 :                                 // are performed from the same thread. This goroutine is
     609           2 :                                 // only responsible for executing operations that hash to
     610           2 :                                 // `t`.
     611           2 :                                 if hashThread(m.ops[idx].receiver(), m.testOpts.Threads) != t {
     612           2 :                                         continue
     613             :                                 }
     614             : 
     615             :                                 // Some operations have additional synchronization
     616             :                                 // dependencies. If this operation has any, wait for its
     617             :                                 // dependencies to complete before executing.
     618           2 :                                 for _, waitOnIdx := range m.opsWaitOn[idx] {
     619           2 :                                         select {
     620           0 :                                         case <-ctx.Done():
     621           0 :                                                 // Exit if some other thread already errored out.
     622           0 :                                                 return ctx.Err()
     623           2 :                                         case <-m.opsDone[waitOnIdx]:
     624             :                                         }
     625             :                                 }
     626             : 
     627           2 :                                 m.runOp(idx, m.h.recorder(t, idx, nil /* optionalRecordf */))
     628           2 : 
     629           2 :                                 // If this operation has a done channel, close it so that
     630           2 :                                 // other operations that synchronize on this operation know
     631           2 :                                 // that it's been completed.
     632           2 :                                 if ch := m.opsDone[idx]; ch != nil {
     633           2 :                                         close(ch)
     634           2 :                                 }
     635             : 
     636           2 :                                 if err := m.h.Error(); err != nil {
     637           0 :                                         return err
     638           0 :                                 }
     639             :                         }
     640           2 :                         return nil
     641             :                 })
     642             :         }
     643           2 :         return eg.Wait()
     644             : }
     645             : 
     646           2 : func hashThread(objID objID, numThreads int) int {
     647           2 :         // Fibonacci hash https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/
     648           2 :         return int((11400714819323198485 * uint64(objID)) % uint64(numThreads))
     649           2 : }
     650             : 
     651             : // Compare runs the metamorphic tests in the provided runDirs and compares their
     652             : // histories.
     653           0 : func Compare(t TestingT, rootDir string, seed uint64, runDirs []string, rOpts ...RunOnceOption) {
     654           0 :         historyPaths := make([]string, len(runDirs))
     655           0 :         for i := 0; i < len(runDirs); i++ {
     656           0 :                 historyPath := filepath.Join(rootDir, runDirs[i]+"-"+time.Now().Format("060102-150405.000"))
     657           0 :                 runDirs[i] = filepath.Join(rootDir, runDirs[i])
     658           0 :                 _ = os.Remove(historyPath)
     659           0 :                 historyPaths[i] = historyPath
     660           0 :         }
     661           0 :         defer func() {
     662           0 :                 for _, path := range historyPaths {
     663           0 :                         _ = os.Remove(path)
     664           0 :                 }
     665             :         }()
     666             : 
     667           0 :         for i, runDir := range runDirs {
     668           0 :                 RunOnce(t, runDir, seed, historyPaths[i], rOpts...)
     669           0 :         }
     670             : 
     671           0 :         if t.Failed() {
     672           0 :                 return
     673           0 :         }
     674             : 
     675           0 :         i, diff := CompareHistories(t, historyPaths)
     676           0 :         if i != 0 {
     677           0 :                 fmt.Printf(`
     678           0 : ===== DIFF =====
     679           0 : %s/{%s,%s}
     680           0 : %s
     681           0 : `, rootDir, runDirs[0], runDirs[i], diff)
     682           0 :                 os.Exit(1)
     683           0 :         }
     684             : }
     685             : 
     686             : // TestingT is an interface wrapper around *testing.T
     687             : type TestingT interface {
     688             :         require.TestingT
     689             :         Failed() bool
     690             : }
     691             : 
     692           0 : func readFile(path string) string {
     693           0 :         history, err := os.ReadFile(path)
     694           0 :         if err != nil {
     695           0 :                 return fmt.Sprintf("err: %v", err)
     696           0 :         }
     697             : 
     698           0 :         return string(history)
     699             : }

Generated by: LCOV version 1.14