LCOV - code coverage report
Current view: top level - pebble/metamorphic - meta.go (source / functions) Hit Total Coverage
Test: 2024-01-18 08:16Z 4b750a69 - meta test only.lcov Lines: 103 397 25.9 %
Date: 2024-01-18 08:17:16 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=" + t.Name() + "$",
     225           0 :                 }
     226           0 :                 if runOpts.numInstances > 1 {
     227           0 :                         args = append(args, "--num-instances="+strconv.Itoa(runOpts.numInstances))
     228           0 :                 }
     229           0 :                 if runOpts.traceFile != "" {
     230           0 :                         args = append(args, "-test.trace="+filepath.Join(runDir, runOpts.traceFile))
     231           0 :                 }
     232             : 
     233           0 :                 binary := os.Args[0]
     234           0 :                 if runOpts.innerBinary != "" {
     235           0 :                         binary = runOpts.innerBinary
     236           0 :                 }
     237           0 :                 cmd := exec.Command(binary, args...)
     238           0 :                 out, err := cmd.CombinedOutput()
     239           0 :                 if err != nil {
     240           0 :                         t.Fatalf(`
     241           0 : ===== SEED =====
     242           0 : %d
     243           0 : ===== ERR =====
     244           0 : %v
     245           0 : ===== OUT =====
     246           0 : %s
     247           0 : ===== OPTIONS =====
     248           0 : %s
     249           0 : ===== OPS =====
     250           0 : %s
     251           0 : ===== HISTORY =====
     252           0 : %s
     253           0 : To reduce:  go test ./internal/metamorphic -tags invariants -run '%s$' --run-dir %s --try-to-reduce -v`,
     254           0 :                                 runOpts.seed,
     255           0 :                                 err,
     256           0 :                                 out,
     257           0 :                                 optionsStr,
     258           0 :                                 formattedOps,
     259           0 :                                 readFile(filepath.Join(runDir, "history")),
     260           0 :                                 topLevelTestName, runDir,
     261           0 :                         )
     262           0 :                 }
     263             :         }
     264             : 
     265             :         // Construct the various OPTIONS to test with.
     266           0 :         names, options, err := buildOptions(rng, runOpts)
     267           0 :         if err != nil {
     268           0 :                 t.Fatal(err)
     269           0 :         }
     270             : 
     271             :         // Run the options.
     272           0 :         t.Run("execution", func(t *testing.T) {
     273           0 :                 for _, name := range names {
     274           0 :                         name := name
     275           0 :                         t.Run(name, func(t *testing.T) {
     276           0 :                                 t.Parallel()
     277           0 :                                 runOptions(t, options[name])
     278           0 :                         })
     279             :                 }
     280             :         })
     281             :         // NB: The above 'execution' subtest will not complete until all of the
     282             :         // individual execution/ subtests have completed. The grouping within the
     283             :         // `execution` subtest ensures all the histories are available when we
     284             :         // proceed to comparing against the base history.
     285             : 
     286             :         // Don't bother comparing output if we've already failed.
     287           0 :         if t.Failed() {
     288           0 :                 return
     289           0 :         }
     290             : 
     291           0 :         t.Run("compare", func(t *testing.T) {
     292           0 :                 getHistoryPath := func(name string) string {
     293           0 :                         return filepath.Join(metaDir, name, "history")
     294           0 :                 }
     295             : 
     296           0 :                 base := readHistory(t, getHistoryPath(names[0]))
     297           0 :                 base = reorderHistory(base)
     298           0 :                 for i := 1; i < len(names); i++ {
     299           0 :                         t.Run(names[i], func(t *testing.T) {
     300           0 :                                 lines := readHistory(t, getHistoryPath(names[i]))
     301           0 :                                 lines = reorderHistory(lines)
     302           0 :                                 diff := difflib.UnifiedDiff{
     303           0 :                                         A:       base,
     304           0 :                                         B:       lines,
     305           0 :                                         Context: 5,
     306           0 :                                 }
     307           0 :                                 text, err := difflib.GetUnifiedDiffString(diff)
     308           0 :                                 require.NoError(t, err)
     309           0 :                                 if text != "" {
     310           0 :                                         // NB: We force an exit rather than using t.Fatal because the latter
     311           0 :                                         // will run another instance of the test if -count is specified, while
     312           0 :                                         // we're happy to exit on the first failure.
     313           0 :                                         optionsStrA := optionsToString(options[names[0]])
     314           0 :                                         optionsStrB := optionsToString(options[names[i]])
     315           0 : 
     316           0 :                                         fmt.Printf(`
     317           0 : ===== SEED =====
     318           0 : %d
     319           0 : ===== DIFF =====
     320           0 : %s/{%s,%s}
     321           0 : %s
     322           0 : ===== OPTIONS %s =====
     323           0 : %s
     324           0 : ===== OPTIONS %s =====
     325           0 : %s
     326           0 : ===== OPS =====
     327           0 : %s
     328           0 : To reduce:  go test ./internal/metamorphic -tags invariants -run '%s$' --compare "%s/{%s,%s}" --try-to-reduce -v
     329           0 : `,
     330           0 :                                                 runOpts.seed,
     331           0 :                                                 metaDir, names[0], names[i], text,
     332           0 :                                                 names[0], optionsStrA,
     333           0 :                                                 names[i], optionsStrB,
     334           0 :                                                 formattedOps,
     335           0 :                                                 topLevelTestName, metaDir, names[0], names[i])
     336           0 :                                         os.Exit(1)
     337           0 :                                 }
     338             :                         })
     339             :                 }
     340             :         })
     341             : }
     342             : 
     343             : func buildOptions(
     344             :         rng *rand.Rand, runOpts runAndCompareOptions,
     345           0 : ) (names []string, options map[string]*TestOptions, err error) {
     346           0 :         options = map[string]*TestOptions{}
     347           0 : 
     348           0 :         // Create the standard options.
     349           0 :         for i, opts := range standardOptions() {
     350           0 :                 name := fmt.Sprintf("standard-%03d", i)
     351           0 :                 names = append(names, name)
     352           0 :                 options[name] = opts
     353           0 :         }
     354             : 
     355             :         // Create the custom option runs, if any.
     356           0 :         for name, customOptsStr := range runOpts.customRuns {
     357           0 :                 options[name] = defaultTestOptions()
     358           0 :                 if err = parseOptions(options[name], customOptsStr, runOpts.customOptionParsers); err != nil {
     359           0 :                         return nil, nil, errors.Wrapf(err, "custom opts %q: %s", name, err)
     360           0 :                 }
     361             :         }
     362             :         // Sort the custom options names for determinism (they're currently in
     363             :         // random order from map iteration).
     364           0 :         sort.Strings(names[len(names)-len(runOpts.customRuns):])
     365           0 : 
     366           0 :         // Create random options. We make an arbitrary choice to run with as many
     367           0 :         // random options as we have standard options.
     368           0 :         nOpts := len(options)
     369           0 :         for i := 0; i < nOpts; i++ {
     370           0 :                 name := fmt.Sprintf("random-%03d", i)
     371           0 :                 names = append(names, name)
     372           0 :                 opts := RandomOptions(rng, runOpts.customOptionParsers)
     373           0 :                 options[name] = opts
     374           0 :         }
     375             : 
     376             :         // If the user provided the path to an initial database state to use, update
     377             :         // all the options to pull from it.
     378           0 :         if runOpts.initialStatePath != "" {
     379           0 :                 for _, o := range options {
     380           0 :                         o.initialStatePath, err = filepath.Abs(runOpts.initialStatePath)
     381           0 :                         if err != nil {
     382           0 :                                 return nil, nil, err
     383           0 :                         }
     384           0 :                         o.initialStateDesc = runOpts.initialStateDesc
     385             :                 }
     386             :         }
     387           0 :         return names, options, nil
     388             : }
     389             : 
     390             : type runOnceOptions struct {
     391             :         keep                bool
     392             :         maxThreads          int
     393             :         errorRate           float64
     394             :         failRegexp          *regexp.Regexp
     395             :         numInstances        int
     396             :         customOptionParsers map[string]func(string) (CustomOption, bool)
     397             : }
     398             : 
     399             : // A RunOnceOption configures the behavior of a single run of the metamorphic
     400             : // tests.
     401             : type RunOnceOption interface {
     402             :         applyOnce(*runOnceOptions)
     403             : }
     404             : 
     405             : // KeepData keeps the database directory, even on successful runs. If the test
     406             : // used an in-memory filesystem, the in-memory filesystem will be persisted to
     407             : // the run directory.
     408             : type KeepData struct{}
     409             : 
     410           0 : func (KeepData) apply(ro *runAndCompareOptions) { ro.keep = true }
     411           0 : func (KeepData) applyOnce(ro *runOnceOptions)   { ro.keep = true }
     412             : 
     413             : // InjectErrorsRate configures the run to inject errors into read-only
     414             : // filesystem operations and retry injected errors.
     415             : type InjectErrorsRate float64
     416             : 
     417           0 : func (r InjectErrorsRate) apply(ro *runAndCompareOptions) { ro.errorRate = float64(r) }
     418           0 : func (r InjectErrorsRate) applyOnce(ro *runOnceOptions)   { ro.errorRate = float64(r) }
     419             : 
     420             : // MaxThreads sets an upper bound on the number of parallel execution threads
     421             : // during replay.
     422             : type MaxThreads int
     423             : 
     424           0 : func (m MaxThreads) apply(ro *runAndCompareOptions) { ro.maxThreads = int(m) }
     425           1 : func (m MaxThreads) applyOnce(ro *runOnceOptions)   { ro.maxThreads = int(m) }
     426             : 
     427             : // FailOnMatch configures the run to fail immediately if the history matches the
     428             : // provided regular expression.
     429             : type FailOnMatch struct {
     430             :         *regexp.Regexp
     431             : }
     432             : 
     433           0 : func (f FailOnMatch) apply(ro *runAndCompareOptions) { ro.failRegexp = f.Regexp }
     434           0 : func (f FailOnMatch) applyOnce(ro *runOnceOptions)   { ro.failRegexp = f.Regexp }
     435             : 
     436             : // MultiInstance configures the number of pebble instances to create.
     437             : type MultiInstance int
     438             : 
     439           0 : func (m MultiInstance) apply(ro *runAndCompareOptions) { ro.numInstances = int(m) }
     440           1 : func (m MultiInstance) applyOnce(ro *runOnceOptions)   { ro.numInstances = int(m) }
     441             : 
     442             : // RunOnce performs one run of the metamorphic tests. RunOnce expects the
     443             : // directory named by `runDir` to already exist and contain an `OPTIONS` file
     444             : // containing the test run's configuration. The history of the run is persisted
     445             : // to a file at the path `historyPath`.
     446             : //
     447             : // The `seed` parameter is not functional; it's used for context in logging.
     448           1 : func RunOnce(t TestingT, runDir string, seed uint64, historyPath string, rOpts ...RunOnceOption) {
     449           1 :         runOpts := runOnceOptions{
     450           1 :                 customOptionParsers: map[string]func(string) (CustomOption, bool){},
     451           1 :         }
     452           1 :         for _, o := range rOpts {
     453           1 :                 o.applyOnce(&runOpts)
     454           1 :         }
     455             : 
     456           1 :         opsPath := filepath.Join(filepath.Dir(filepath.Clean(runDir)), "ops")
     457           1 :         opsData, err := os.ReadFile(opsPath)
     458           1 :         require.NoError(t, err)
     459           1 : 
     460           1 :         ops, err := parse(opsData, parserOpts{})
     461           1 :         require.NoError(t, err)
     462           1 : 
     463           1 :         optionsPath := filepath.Join(runDir, "OPTIONS")
     464           1 :         optionsData, err := os.ReadFile(optionsPath)
     465           1 :         require.NoError(t, err)
     466           1 : 
     467           1 :         // NB: It's important to use defaultTestOptions() here as the base into
     468           1 :         // which we parse the serialized options. It contains the relevant defaults,
     469           1 :         // like the appropriate block-property collectors.
     470           1 :         testOpts := defaultTestOptions()
     471           1 :         opts := testOpts.Opts
     472           1 :         require.NoError(t, parseOptions(testOpts, string(optionsData), runOpts.customOptionParsers))
     473           1 : 
     474           1 :         // Set up the filesystem to use for the test. Note that by default we use an
     475           1 :         // in-memory FS.
     476           1 :         if testOpts.useDisk {
     477           1 :                 opts.FS = vfs.Default
     478           1 :                 require.NoError(t, os.RemoveAll(opts.FS.PathJoin(runDir, "data")))
     479           1 :         } else {
     480           1 :                 opts.Cleaner = base.ArchiveCleaner{}
     481           1 :         }
     482             : 
     483             :         // Wrap the filesystem with one that will inject errors into read
     484             :         // operations with *errorRate probability.
     485           1 :         opts.FS = errorfs.Wrap(opts.FS, errorfs.ErrInjected.If(
     486           1 :                 dsl.And(errorfs.Reads, errorfs.Randomly(runOpts.errorRate, int64(seed))),
     487           1 :         ))
     488           1 : 
     489           1 :         // Bound testOpts.threads to runOpts.maxThreads.
     490           1 :         if runOpts.maxThreads < testOpts.Threads {
     491           0 :                 testOpts.Threads = runOpts.maxThreads
     492           0 :         }
     493             : 
     494           1 :         dir := opts.FS.PathJoin(runDir, "data")
     495           1 :         // Set up the initial database state if configured to start from a non-empty
     496           1 :         // database. By default tests start from an empty database, but split
     497           1 :         // version testing may configure a previous metamorphic tests's database
     498           1 :         // state as the initial state.
     499           1 :         if testOpts.initialStatePath != "" {
     500           0 :                 require.NoError(t, setupInitialState(dir, testOpts))
     501           0 :         }
     502             : 
     503           1 :         if opts.WALDir != "" {
     504           1 :                 if runOpts.numInstances > 1 {
     505           1 :                         // TODO(bilal): Allow opts to diverge on a per-instance basis, and use
     506           1 :                         // that to set unique WAL dirs for all instances in multi-instance mode.
     507           1 :                         opts.WALDir = ""
     508           1 :                 } else {
     509           1 :                         opts.WALDir = opts.FS.PathJoin(runDir, opts.WALDir)
     510           1 :                 }
     511             :         }
     512             : 
     513           1 :         historyFile, err := os.Create(historyPath)
     514           1 :         require.NoError(t, err)
     515           1 :         defer historyFile.Close()
     516           1 :         writers := []io.Writer{historyFile}
     517           1 : 
     518           1 :         if testing.Verbose() {
     519           0 :                 writers = append(writers, os.Stdout)
     520           0 :         }
     521           1 :         h := newHistory(runOpts.failRegexp, writers...)
     522           1 : 
     523           1 :         m := newTest(ops)
     524           1 :         require.NoError(t, m.init(h, dir, testOpts, runOpts.numInstances))
     525           1 : 
     526           1 :         if err := Execute(m); err != nil {
     527           0 :                 fmt.Fprintf(os.Stderr, "Seed: %d\n", seed)
     528           0 :                 fmt.Fprintln(os.Stderr, err)
     529           0 :                 m.maybeSaveData()
     530           0 :                 os.Exit(1)
     531           0 :         }
     532             : 
     533           1 :         if runOpts.keep && !testOpts.useDisk {
     534           0 :                 m.maybeSaveData()
     535           0 :         }
     536             : }
     537             : 
     538             : // Execute runs the provided test, writing the execution history into the Test's
     539             : // sink.
     540           1 : func Execute(m *Test) error {
     541           1 :         if m.testOpts.Threads <= 1 {
     542           1 :                 for m.step(m.h, nil /* optionalRecordf */) {
     543           1 :                         if err := m.h.Error(); err != nil {
     544           0 :                                 return err
     545           0 :                         }
     546             :                 }
     547           1 :                 return nil
     548             :         }
     549             : 
     550             :         // Run in parallel using an errgroup.
     551           1 :         eg, ctx := errgroup.WithContext(context.Background())
     552           1 :         for t := 0; t < m.testOpts.Threads; t++ {
     553           1 :                 t := t // bind loop var to scope
     554           1 :                 eg.Go(func() error {
     555           1 :                         for idx := 0; idx < len(m.ops); idx++ {
     556           1 :                                 // Skip any operations whose receiver object hashes to a
     557           1 :                                 // different thread. All operations with the same receiver
     558           1 :                                 // are performed from the same thread. This goroutine is
     559           1 :                                 // only responsible for executing operations that hash to
     560           1 :                                 // `t`.
     561           1 :                                 if hashThread(m.ops[idx].receiver(), m.testOpts.Threads) != t {
     562           1 :                                         continue
     563             :                                 }
     564             : 
     565             :                                 // Some operations have additional synchronization
     566             :                                 // dependencies. If this operation has any, wait for its
     567             :                                 // dependencies to complete before executing.
     568           1 :                                 for _, waitOnIdx := range m.opsWaitOn[idx] {
     569           1 :                                         select {
     570           0 :                                         case <-ctx.Done():
     571           0 :                                                 // Exit if some other thread already errored out.
     572           0 :                                                 return ctx.Err()
     573           1 :                                         case <-m.opsDone[waitOnIdx]:
     574             :                                         }
     575             :                                 }
     576             : 
     577           1 :                                 m.ops[idx].run(m, m.h.recorder(t, idx, nil /* optionalRecordf */))
     578           1 : 
     579           1 :                                 // If this operation has a done channel, close it so that
     580           1 :                                 // other operations that synchronize on this operation know
     581           1 :                                 // that it's been completed.
     582           1 :                                 if ch := m.opsDone[idx]; ch != nil {
     583           1 :                                         close(ch)
     584           1 :                                 }
     585             : 
     586           1 :                                 if err := m.h.Error(); err != nil {
     587           0 :                                         return err
     588           0 :                                 }
     589             :                         }
     590           1 :                         return nil
     591             :                 })
     592             :         }
     593           1 :         return eg.Wait()
     594             : }
     595             : 
     596           1 : func hashThread(objID objID, numThreads int) int {
     597           1 :         // Fibonacci hash https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/
     598           1 :         return int((11400714819323198485 * uint64(objID)) % uint64(numThreads))
     599           1 : }
     600             : 
     601             : // Compare runs the metamorphic tests in the provided runDirs and compares their
     602             : // histories.
     603           0 : func Compare(t TestingT, rootDir string, seed uint64, runDirs []string, rOpts ...RunOnceOption) {
     604           0 :         historyPaths := make([]string, len(runDirs))
     605           0 :         for i := 0; i < len(runDirs); i++ {
     606           0 :                 historyPath := filepath.Join(rootDir, runDirs[i]+"-"+time.Now().Format("060102-150405.000"))
     607           0 :                 runDirs[i] = filepath.Join(rootDir, runDirs[i])
     608           0 :                 _ = os.Remove(historyPath)
     609           0 :                 historyPaths[i] = historyPath
     610           0 :         }
     611           0 :         defer func() {
     612           0 :                 for _, path := range historyPaths {
     613           0 :                         _ = os.Remove(path)
     614           0 :                 }
     615             :         }()
     616             : 
     617           0 :         for i, runDir := range runDirs {
     618           0 :                 RunOnce(t, runDir, seed, historyPaths[i], rOpts...)
     619           0 :         }
     620             : 
     621           0 :         if t.Failed() {
     622           0 :                 return
     623           0 :         }
     624             : 
     625           0 :         i, diff := CompareHistories(t, historyPaths)
     626           0 :         if i != 0 {
     627           0 :                 fmt.Printf(`
     628           0 : ===== DIFF =====
     629           0 : %s/{%s,%s}
     630           0 : %s
     631           0 : `, rootDir, runDirs[0], runDirs[i], diff)
     632           0 :                 os.Exit(1)
     633           0 :         }
     634             : }
     635             : 
     636             : // TestingT is an interface wrapper around *testing.T
     637             : type TestingT interface {
     638             :         require.TestingT
     639             :         Failed() bool
     640             : }
     641             : 
     642           0 : func readFile(path string) string {
     643           0 :         history, err := os.ReadFile(path)
     644           0 :         if err != nil {
     645           0 :                 return fmt.Sprintf("err: %v", err)
     646           0 :         }
     647             : 
     648           0 :         return string(history)
     649             : }

Generated by: LCOV version 1.14