LCOV - code coverage report
Current view: top level - pebble/metamorphic - meta.go (source / functions) Hit Total Coverage
Test: 2024-04-20 08:15Z 5b3f94f1 - meta test only.lcov Lines: 126 426 29.6 %
Date: 2024-04-20 08:16:18 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/pebble/internal/base"
      26             :         "github.com/cockroachdb/pebble/internal/dsl"
      27             :         "github.com/cockroachdb/pebble/internal/randvar"
      28             :         "github.com/cockroachdb/pebble/vfs"
      29             :         "github.com/cockroachdb/pebble/vfs/errorfs"
      30             :         "github.com/pkg/errors"
      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           0 : 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           0 : func OpCount(rv randvar.Static) RunOption {
     103           0 :         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           0 : func (f closureOpt) apply(ro *runAndCompareOptions) { f(ro) }
     136             : 
     137           0 : func buildRunAndCompareOpts(rOpts []RunOption) runAndCompareOptions {
     138           0 :         runOpts := runAndCompareOptions{
     139           0 :                 ops:        randvar.NewUniform(1000, 10000),
     140           0 :                 customRuns: map[string]string{},
     141           0 :                 runOnceOptions: runOnceOptions{
     142           0 :                         customOptionParsers: map[string]func(string) (CustomOption, bool){},
     143           0 :                 },
     144           0 :         }
     145           0 :         for _, o := range rOpts {
     146           0 :                 o.apply(&runOpts)
     147           0 :         }
     148           0 :         if runOpts.seed == 0 {
     149           0 :                 runOpts.seed = uint64(time.Now().UnixNano())
     150           0 :         }
     151           0 :         return runOpts
     152             : }
     153             : 
     154             : // RunAndCompare runs the metamorphic tests, using the provided root directory
     155             : // to hold test data.
     156           0 : func RunAndCompare(t *testing.T, rootDir string, rOpts ...RunOption) {
     157           0 :         runOpts := buildRunAndCompareOpts(rOpts)
     158           0 :         require.NoError(t, os.MkdirAll(rootDir, 0755))
     159           0 :         metaDir, err := os.MkdirTemp(rootDir, time.Now().Format("060102-150405.000"))
     160           0 :         require.NoError(t, err)
     161           0 :         require.NoError(t, os.MkdirAll(metaDir, 0755))
     162           0 :         defer func() {
     163           0 :                 if !t.Failed() && !runOpts.keep {
     164           0 :                         _ = os.RemoveAll(metaDir)
     165           0 :                 }
     166             :         }()
     167             : 
     168           0 :         topLevelTestName := t.Name()
     169           0 :         for path.Dir(topLevelTestName) != "." {
     170           0 :                 topLevelTestName = path.Dir(topLevelTestName)
     171           0 :         }
     172             : 
     173           0 :         rng := rand.New(rand.NewSource(runOpts.seed))
     174           0 :         opCount := runOpts.ops.Uint64(rng)
     175           0 : 
     176           0 :         // Generate a new set of random ops, writing them to <dir>/ops. These will be
     177           0 :         // read by the child processes when performing a test run.
     178           0 :         km := newKeyManager(runOpts.numInstances)
     179           0 :         cfg := presetConfigs[rng.Intn(len(presetConfigs))]
     180           0 :         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           0 :         if runOpts.numInstances > 1 {
     193           0 :                 // The multi-instance variant does not support all operations yet.
     194           0 :                 //
     195           0 :                 // TODO(bilal): Address this and use the default configs.
     196           0 :                 cfg = multiInstancePresetConfig
     197           0 :                 cfg.numInstances = runOpts.numInstances
     198           0 :         }
     199           0 :         ops := generate(rng, opCount, cfg, km)
     200           0 :         opsPath := filepath.Join(metaDir, "ops")
     201           0 :         formattedOps := formatOps(ops)
     202           0 :         require.NoError(t, os.WriteFile(opsPath, []byte(formattedOps), 0644))
     203           0 : 
     204           0 :         // runOptions performs a particular test run with the specified options. The
     205           0 :         // options are written to <run-dir>/OPTIONS and a child process is created to
     206           0 :         // actually execute the test.
     207           0 :         runOptions := func(t *testing.T, opts *TestOptions) {
     208           0 :                 if opts.Opts.Cache != nil {
     209           0 :                         defer opts.Opts.Cache.Unref()
     210           0 :                 }
     211           0 :                 for _, fn := range runOpts.mutateTestOptions {
     212           0 :                         fn(opts)
     213           0 :                 }
     214           0 :                 runDir := filepath.Join(metaDir, path.Base(t.Name()))
     215           0 :                 require.NoError(t, os.MkdirAll(runDir, 0755))
     216           0 : 
     217           0 :                 optionsPath := filepath.Join(runDir, "OPTIONS")
     218           0 :                 optionsStr := optionsToString(opts)
     219           0 :                 require.NoError(t, os.WriteFile(optionsPath, []byte(optionsStr), 0644))
     220           0 : 
     221           0 :                 args := []string{
     222           0 :                         "-keep=" + fmt.Sprint(runOpts.keep),
     223           0 :                         "-run-dir=" + runDir,
     224           0 :                         "-test.run=" + topLevelTestName + "$",
     225           0 :                         "--op-timeout=" + runOpts.opTimeout.String(),
     226           0 :                 }
     227           0 :                 if runOpts.numInstances > 1 {
     228           0 :                         args = append(args, "--num-instances="+strconv.Itoa(runOpts.numInstances))
     229           0 :                 }
     230           0 :                 if runOpts.traceFile != "" {
     231           0 :                         args = append(args, "-test.trace="+filepath.Join(runDir, runOpts.traceFile))
     232           0 :                 }
     233             : 
     234           0 :                 binary := os.Args[0]
     235           0 :                 if runOpts.innerBinary != "" {
     236           0 :                         binary = runOpts.innerBinary
     237           0 :                 }
     238           0 :                 cmd := exec.Command(binary, args...)
     239           0 :                 out, err := cmd.CombinedOutput()
     240           0 :                 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           0 :         names, options, err := buildOptions(rng, runOpts)
     268           0 :         if err != nil {
     269           0 :                 t.Fatal(err)
     270           0 :         }
     271             : 
     272             :         // Run the options.
     273           0 :         t.Run("execution", func(t *testing.T) {
     274           0 :                 for _, name := range names {
     275           0 :                         name := name
     276           0 :                         t.Run(name, func(t *testing.T) {
     277           0 :                                 t.Parallel()
     278           0 :                                 runOptions(t, options[name])
     279           0 :                         })
     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           0 :         if t.Failed() {
     289           0 :                 return
     290           0 :         }
     291             : 
     292           0 :         t.Run("compare", func(t *testing.T) {
     293           0 :                 getHistoryPath := func(name string) string {
     294           0 :                         return filepath.Join(metaDir, name, "history")
     295           0 :                 }
     296             : 
     297           0 :                 base := readHistory(t, getHistoryPath(names[0]))
     298           0 :                 base = reorderHistory(base)
     299           0 :                 for i := 1; i < len(names); i++ {
     300           0 :                         t.Run(names[i], func(t *testing.T) {
     301           0 :                                 lines := readHistory(t, getHistoryPath(names[i]))
     302           0 :                                 lines = reorderHistory(lines)
     303           0 :                                 diff := difflib.UnifiedDiff{
     304           0 :                                         A:       base,
     305           0 :                                         B:       lines,
     306           0 :                                         Context: 5,
     307           0 :                                 }
     308           0 :                                 text, err := difflib.GetUnifiedDiffString(diff)
     309           0 :                                 require.NoError(t, err)
     310           0 :                                 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           0 : ) (names []string, options map[string]*TestOptions, err error) {
     347           0 :         options = map[string]*TestOptions{}
     348           0 : 
     349           0 :         // Create the standard options.
     350           0 :         for i, opts := range standardOptions() {
     351           0 :                 name := fmt.Sprintf("standard-%03d", i)
     352           0 :                 names = append(names, name)
     353           0 :                 options[name] = opts
     354           0 :         }
     355             : 
     356             :         // Create the custom option runs, if any.
     357           0 :         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           0 :         sort.Strings(names[len(names)-len(runOpts.customRuns):])
     366           0 : 
     367           0 :         // Create random options. We make an arbitrary choice to run with as many
     368           0 :         // random options as we have standard options.
     369           0 :         nOpts := len(options)
     370           0 :         for i := 0; i < nOpts; i++ {
     371           0 :                 name := fmt.Sprintf("random-%03d", i)
     372           0 :                 names = append(names, name)
     373           0 :                 opts := RandomOptions(rng, runOpts.customOptionParsers)
     374           0 :                 options[name] = opts
     375           0 :         }
     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           0 :         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           0 :         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           0 : 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           0 : 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           0 : 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           0 : 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           1 : func RunOnce(t TestingT, runDir string, seed uint64, historyPath string, rOpts ...RunOnceOption) {
     462           1 :         runOpts := runOnceOptions{
     463           1 :                 customOptionParsers: map[string]func(string) (CustomOption, bool){},
     464           1 :         }
     465           1 :         for _, o := range rOpts {
     466           1 :                 o.applyOnce(&runOpts)
     467           1 :         }
     468             : 
     469           1 :         opsPath := filepath.Join(filepath.Dir(filepath.Clean(runDir)), "ops")
     470           1 :         opsData, err := os.ReadFile(opsPath)
     471           1 :         require.NoError(t, err)
     472           1 : 
     473           1 :         ops, err := parse(opsData, parserOpts{})
     474           1 :         require.NoError(t, err)
     475           1 : 
     476           1 :         optionsPath := filepath.Join(runDir, "OPTIONS")
     477           1 :         optionsData, err := os.ReadFile(optionsPath)
     478           1 :         require.NoError(t, err)
     479           1 : 
     480           1 :         // NB: It's important to use defaultTestOptions() here as the base into
     481           1 :         // which we parse the serialized options. It contains the relevant defaults,
     482           1 :         // like the appropriate block-property collectors.
     483           1 :         testOpts := defaultTestOptions()
     484           1 :         opts := testOpts.Opts
     485           1 :         require.NoError(t, parseOptions(testOpts, string(optionsData), runOpts.customOptionParsers))
     486           1 : 
     487           1 :         // Set up the filesystem to use for the test. Note that by default we use an
     488           1 :         // in-memory FS.
     489           1 :         if testOpts.useDisk {
     490           1 :                 opts.FS = vfs.Default
     491           1 :                 require.NoError(t, os.RemoveAll(opts.FS.PathJoin(runDir, "data")))
     492           1 :         } else {
     493           1 :                 opts.Cleaner = base.ArchiveCleaner{}
     494           1 :         }
     495             :         // Wrap the filesystem with a VFS that will inject random latency if
     496             :         // the test options require it.
     497           1 :         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           1 :         opts.FS = errorfs.Wrap(opts.FS, errorfs.ErrInjected.If(
     508           1 :                 dsl.And(errorfs.Reads, errorfs.Randomly(runOpts.errorRate, int64(seed))),
     509           1 :         ))
     510           1 : 
     511           1 :         // Bound testOpts.threads to runOpts.maxThreads.
     512           1 :         if runOpts.maxThreads < testOpts.Threads {
     513           0 :                 testOpts.Threads = runOpts.maxThreads
     514           0 :         }
     515             : 
     516           1 :         dir := opts.FS.PathJoin(runDir, "data")
     517           1 :         // Set up the initial database state if configured to start from a non-empty
     518           1 :         // database. By default tests start from an empty database, but split
     519           1 :         // version testing may configure a previous metamorphic tests's database
     520           1 :         // state as the initial state.
     521           1 :         if testOpts.initialStatePath != "" {
     522           0 :                 require.NoError(t, setupInitialState(dir, testOpts))
     523           0 :         }
     524             : 
     525           1 :         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           1 :         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           1 :         historyFile, err := os.Create(historyPath)
     549           1 :         require.NoError(t, err)
     550           1 :         defer historyFile.Close()
     551           1 :         writers := []io.Writer{historyFile}
     552           1 : 
     553           1 :         if testing.Verbose() {
     554           0 :                 writers = append(writers, os.Stdout)
     555           0 :         }
     556           1 :         h := newHistory(runOpts.failRegexp, writers...)
     557           1 :         defer h.Close()
     558           1 : 
     559           1 :         m := newTest(ops)
     560           1 :         require.NoError(t, m.init(h, dir, testOpts, runOpts.numInstances, runOpts.opTimeout))
     561           1 : 
     562           1 :         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           1 :         for i, db := range m.dbs {
     570           1 :                 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             : 
     576           1 :         if err != nil {
     577           0 :                 m.saveInMemoryData()
     578           0 :                 os.Exit(1)
     579           0 :         }
     580             : 
     581           1 :         if runOpts.keep && !testOpts.useDisk {
     582           0 :                 m.saveInMemoryData()
     583           0 :         }
     584             : }
     585             : 
     586             : // Execute runs the provided test, writing the execution history into the Test's
     587             : // sink.
     588           1 : func Execute(m *Test) error {
     589           1 :         if m.testOpts.Threads <= 1 {
     590           1 :                 for m.step(m.h, nil /* optionalRecordf */) {
     591           1 :                         if err := m.h.Error(); err != nil {
     592           0 :                                 return err
     593           0 :                         }
     594             :                 }
     595           1 :                 return nil
     596             :         }
     597             : 
     598             :         // Run in parallel using an errgroup.
     599           1 :         eg, ctx := errgroup.WithContext(context.Background())
     600           1 :         for t := 0; t < m.testOpts.Threads; t++ {
     601           1 :                 t := t // bind loop var to scope
     602           1 :                 eg.Go(func() error {
     603           1 :                         for idx := 0; idx < len(m.ops); idx++ {
     604           1 :                                 // Skip any operations whose receiver object hashes to a
     605           1 :                                 // different thread. All operations with the same receiver
     606           1 :                                 // are performed from the same thread. This goroutine is
     607           1 :                                 // only responsible for executing operations that hash to
     608           1 :                                 // `t`.
     609           1 :                                 if hashThread(m.ops[idx].receiver(), m.testOpts.Threads) != t {
     610           1 :                                         continue
     611             :                                 }
     612             : 
     613             :                                 // Some operations have additional synchronization
     614             :                                 // dependencies. If this operation has any, wait for its
     615             :                                 // dependencies to complete before executing.
     616           1 :                                 for _, waitOnIdx := range m.opsWaitOn[idx] {
     617           1 :                                         select {
     618           0 :                                         case <-ctx.Done():
     619           0 :                                                 // Exit if some other thread already errored out.
     620           0 :                                                 return ctx.Err()
     621           1 :                                         case <-m.opsDone[waitOnIdx]:
     622             :                                         }
     623             :                                 }
     624             : 
     625           1 :                                 m.runOp(idx, m.h.recorder(t, idx, nil /* optionalRecordf */))
     626           1 : 
     627           1 :                                 // If this operation has a done channel, close it so that
     628           1 :                                 // other operations that synchronize on this operation know
     629           1 :                                 // that it's been completed.
     630           1 :                                 if ch := m.opsDone[idx]; ch != nil {
     631           1 :                                         close(ch)
     632           1 :                                 }
     633             : 
     634           1 :                                 if err := m.h.Error(); err != nil {
     635           0 :                                         return err
     636           0 :                                 }
     637             :                         }
     638           1 :                         return nil
     639             :                 })
     640             :         }
     641           1 :         return eg.Wait()
     642             : }
     643             : 
     644           1 : func hashThread(objID objID, numThreads int) int {
     645           1 :         // Fibonacci hash https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/
     646           1 :         return int((11400714819323198485 * uint64(objID)) % uint64(numThreads))
     647           1 : }
     648             : 
     649             : // Compare runs the metamorphic tests in the provided runDirs and compares their
     650             : // histories.
     651           0 : func Compare(t TestingT, rootDir string, seed uint64, runDirs []string, rOpts ...RunOnceOption) {
     652           0 :         historyPaths := make([]string, len(runDirs))
     653           0 :         for i := 0; i < len(runDirs); i++ {
     654           0 :                 historyPath := filepath.Join(rootDir, runDirs[i]+"-"+time.Now().Format("060102-150405.000"))
     655           0 :                 runDirs[i] = filepath.Join(rootDir, runDirs[i])
     656           0 :                 _ = os.Remove(historyPath)
     657           0 :                 historyPaths[i] = historyPath
     658           0 :         }
     659           0 :         defer func() {
     660           0 :                 for _, path := range historyPaths {
     661           0 :                         _ = os.Remove(path)
     662           0 :                 }
     663             :         }()
     664             : 
     665           0 :         for i, runDir := range runDirs {
     666           0 :                 RunOnce(t, runDir, seed, historyPaths[i], rOpts...)
     667           0 :         }
     668             : 
     669           0 :         if t.Failed() {
     670           0 :                 return
     671           0 :         }
     672             : 
     673           0 :         i, diff := CompareHistories(t, historyPaths)
     674           0 :         if i != 0 {
     675           0 :                 fmt.Printf(`
     676           0 : ===== DIFF =====
     677           0 : %s/{%s,%s}
     678           0 : %s
     679           0 : `, rootDir, runDirs[0], runDirs[i], diff)
     680           0 :                 os.Exit(1)
     681           0 :         }
     682             : }
     683             : 
     684             : // TestingT is an interface wrapper around *testing.T
     685             : type TestingT interface {
     686             :         require.TestingT
     687             :         Failed() bool
     688             : }
     689             : 
     690           0 : func readFile(path string) string {
     691           0 :         history, err := os.ReadFile(path)
     692           0 :         if err != nil {
     693           0 :                 return fmt.Sprintf("err: %v", err)
     694           0 :         }
     695             : 
     696           0 :         return string(history)
     697             : }

Generated by: LCOV version 1.14