LCOV - code coverage report
Current view: top level - pebble/metamorphic - meta.go (source / functions) Hit Total Coverage
Test: 2023-11-14 08:18Z c0b4bd44 - meta test only.lcov Lines: 114 383 29.8 %
Date: 2023-11-14 08:20:03 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/internal/testkeys"
      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           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             : // RunAndCompare runs the metamorphic tests, using the provided root directory
     138             : // to hold test data.
     139           0 : func RunAndCompare(t *testing.T, rootDir string, rOpts ...RunOption) {
     140           0 :         runOpts := runAndCompareOptions{
     141           0 :                 ops:        randvar.NewUniform(1000, 10000),
     142           0 :                 customRuns: map[string]string{},
     143           0 :                 runOnceOptions: runOnceOptions{
     144           0 :                         customOptionParsers: map[string]func(string) (CustomOption, bool){},
     145           0 :                 },
     146           0 :         }
     147           0 :         for _, o := range rOpts {
     148           0 :                 o.apply(&runOpts)
     149           0 :         }
     150           0 :         if runOpts.seed == 0 {
     151           0 :                 runOpts.seed = uint64(time.Now().UnixNano())
     152           0 :         }
     153             : 
     154           0 :         require.NoError(t, os.MkdirAll(rootDir, 0755))
     155           0 :         metaDir, err := os.MkdirTemp(rootDir, time.Now().Format("060102-150405.000"))
     156           0 :         require.NoError(t, err)
     157           0 :         require.NoError(t, os.MkdirAll(metaDir, 0755))
     158           0 :         defer func() {
     159           0 :                 if !t.Failed() && !runOpts.keep {
     160           0 :                         _ = os.RemoveAll(metaDir)
     161           0 :                 }
     162             :         }()
     163             : 
     164           0 :         rng := rand.New(rand.NewSource(runOpts.seed))
     165           0 :         opCount := runOpts.ops.Uint64(rng)
     166           0 : 
     167           0 :         // Generate a new set of random ops, writing them to <dir>/ops. These will be
     168           0 :         // read by the child processes when performing a test run.
     169           0 :         km := newKeyManager(runOpts.numInstances)
     170           0 :         cfg := presetConfigs[rng.Intn(len(presetConfigs))]
     171           0 :         if runOpts.previousOpsPath != "" {
     172           0 :                 // During cross-version testing, we load keys from an `ops` file
     173           0 :                 // produced by a metamorphic test run of an earlier Pebble version.
     174           0 :                 // Seeding the keys ensure we generate interesting operations, including
     175           0 :                 // ones with key shadowing, merging, etc.
     176           0 :                 opsPath := filepath.Join(filepath.Dir(filepath.Clean(runOpts.previousOpsPath)), "ops")
     177           0 :                 opsData, err := os.ReadFile(opsPath)
     178           0 :                 require.NoError(t, err)
     179           0 :                 ops, err := parse(opsData)
     180           0 :                 require.NoError(t, err)
     181           0 :                 loadPrecedingKeys(t, ops, &cfg, km)
     182           0 :         }
     183           0 :         if runOpts.numInstances > 1 {
     184           0 :                 // The multi-instance variant does not support all operations yet.
     185           0 :                 //
     186           0 :                 // TODO(bilal): Address this and use the default configs.
     187           0 :                 cfg = multiInstancePresetConfig
     188           0 :                 cfg.numInstances = runOpts.numInstances
     189           0 :         }
     190           0 :         ops := generate(rng, opCount, cfg, km)
     191           0 :         opsPath := filepath.Join(metaDir, "ops")
     192           0 :         formattedOps := formatOps(ops)
     193           0 :         require.NoError(t, os.WriteFile(opsPath, []byte(formattedOps), 0644))
     194           0 : 
     195           0 :         // runOptions performs a particular test run with the specified options. The
     196           0 :         // options are written to <run-dir>/OPTIONS and a child process is created to
     197           0 :         // actually execute the test.
     198           0 :         runOptions := func(t *testing.T, opts *TestOptions) {
     199           0 :                 if opts.Opts.Cache != nil {
     200           0 :                         defer opts.Opts.Cache.Unref()
     201           0 :                 }
     202           0 :                 for _, fn := range runOpts.mutateTestOptions {
     203           0 :                         fn(opts)
     204           0 :                 }
     205           0 :                 runDir := filepath.Join(metaDir, path.Base(t.Name()))
     206           0 :                 require.NoError(t, os.MkdirAll(runDir, 0755))
     207           0 : 
     208           0 :                 optionsPath := filepath.Join(runDir, "OPTIONS")
     209           0 :                 optionsStr := optionsToString(opts)
     210           0 :                 require.NoError(t, os.WriteFile(optionsPath, []byte(optionsStr), 0644))
     211           0 : 
     212           0 :                 args := []string{
     213           0 :                         "-keep=" + fmt.Sprint(runOpts.keep),
     214           0 :                         "-run-dir=" + runDir,
     215           0 :                         "-test.run=" + t.Name() + "$",
     216           0 :                 }
     217           0 :                 if runOpts.numInstances > 1 {
     218           0 :                         args = append(args, "--num-instances="+strconv.Itoa(runOpts.numInstances))
     219           0 :                 }
     220           0 :                 if runOpts.traceFile != "" {
     221           0 :                         args = append(args, "-test.trace="+filepath.Join(runDir, runOpts.traceFile))
     222           0 :                 }
     223             : 
     224           0 :                 binary := os.Args[0]
     225           0 :                 if runOpts.innerBinary != "" {
     226           0 :                         binary = runOpts.innerBinary
     227           0 :                 }
     228           0 :                 cmd := exec.Command(binary, args...)
     229           0 :                 out, err := cmd.CombinedOutput()
     230           0 :                 if err != nil {
     231           0 :                         t.Fatalf(`
     232           0 : ===== SEED =====
     233           0 : %d
     234           0 : ===== ERR =====
     235           0 : %v
     236           0 : ===== OUT =====
     237           0 : %s
     238           0 : ===== OPTIONS =====
     239           0 : %s
     240           0 : ===== OPS =====
     241           0 : %s
     242           0 : ===== HISTORY =====
     243           0 : %s`, runOpts.seed, err, out, optionsStr, formattedOps, readFile(filepath.Join(runDir, "history")))
     244           0 :                 }
     245             :         }
     246             : 
     247           0 :         var names []string
     248           0 :         options := map[string]*TestOptions{}
     249           0 : 
     250           0 :         // Create the standard options.
     251           0 :         for i, opts := range standardOptions() {
     252           0 :                 name := fmt.Sprintf("standard-%03d", i)
     253           0 :                 names = append(names, name)
     254           0 :                 options[name] = opts
     255           0 :         }
     256             : 
     257             :         // Create the custom option runs, if any.
     258           0 :         for name, customOptsStr := range runOpts.customRuns {
     259           0 :                 options[name] = defaultTestOptions()
     260           0 :                 if err := parseOptions(options[name], customOptsStr, runOpts.customOptionParsers); err != nil {
     261           0 :                         t.Fatalf("custom opts %q: %s", name, err)
     262           0 :                 }
     263             :         }
     264             :         // Sort the custom options names for determinism (they're currently in
     265             :         // random order from map iteration).
     266           0 :         sort.Strings(names[len(names)-len(runOpts.customRuns):])
     267           0 : 
     268           0 :         // Create random options. We make an arbitrary choice to run with as many
     269           0 :         // random options as we have standard options.
     270           0 :         nOpts := len(options)
     271           0 :         for i := 0; i < nOpts; i++ {
     272           0 :                 name := fmt.Sprintf("random-%03d", i)
     273           0 :                 names = append(names, name)
     274           0 :                 opts := randomOptions(rng, runOpts.customOptionParsers)
     275           0 :                 options[name] = opts
     276           0 :         }
     277             : 
     278             :         // If the user provided the path to an initial database state to use, update
     279             :         // all the options to pull from it.
     280           0 :         if runOpts.initialStatePath != "" {
     281           0 :                 for _, o := range options {
     282           0 :                         var err error
     283           0 :                         o.initialStatePath, err = filepath.Abs(runOpts.initialStatePath)
     284           0 :                         require.NoError(t, err)
     285           0 :                         o.initialStateDesc = runOpts.initialStateDesc
     286           0 :                 }
     287             :         }
     288             : 
     289             :         // Run the options.
     290           0 :         t.Run("execution", func(t *testing.T) {
     291           0 :                 for _, name := range names {
     292           0 :                         name := name
     293           0 :                         t.Run(name, func(t *testing.T) {
     294           0 :                                 t.Parallel()
     295           0 :                                 runOptions(t, options[name])
     296           0 :                         })
     297             :                 }
     298             :         })
     299             :         // NB: The above 'execution' subtest will not complete until all of the
     300             :         // individual execution/ subtests have completed. The grouping within the
     301             :         // `execution` subtest ensures all the histories are available when we
     302             :         // proceed to comparing against the base history.
     303             : 
     304             :         // Don't bother comparing output if we've already failed.
     305           0 :         if t.Failed() {
     306           0 :                 return
     307           0 :         }
     308             : 
     309           0 :         t.Run("compare", func(t *testing.T) {
     310           0 :                 getHistoryPath := func(name string) string {
     311           0 :                         return filepath.Join(metaDir, name, "history")
     312           0 :                 }
     313             : 
     314           0 :                 base := readHistory(t, getHistoryPath(names[0]))
     315           0 :                 base = reorderHistory(base)
     316           0 :                 for i := 1; i < len(names); i++ {
     317           0 :                         t.Run(names[i], func(t *testing.T) {
     318           0 :                                 lines := readHistory(t, getHistoryPath(names[i]))
     319           0 :                                 lines = reorderHistory(lines)
     320           0 :                                 diff := difflib.UnifiedDiff{
     321           0 :                                         A:       base,
     322           0 :                                         B:       lines,
     323           0 :                                         Context: 5,
     324           0 :                                 }
     325           0 :                                 text, err := difflib.GetUnifiedDiffString(diff)
     326           0 :                                 require.NoError(t, err)
     327           0 :                                 if text != "" {
     328           0 :                                         // NB: We force an exit rather than using t.Fatal because the latter
     329           0 :                                         // will run another instance of the test if -count is specified, while
     330           0 :                                         // we're happy to exit on the first failure.
     331           0 :                                         optionsStrA := optionsToString(options[names[0]])
     332           0 :                                         optionsStrB := optionsToString(options[names[i]])
     333           0 : 
     334           0 :                                         fmt.Printf(`
     335           0 :                 ===== SEED =====
     336           0 :                 %d
     337           0 :                 ===== DIFF =====
     338           0 :                 %s/{%s,%s}
     339           0 :                 %s
     340           0 :                 ===== OPTIONS %s =====
     341           0 :                 %s
     342           0 :                 ===== OPTIONS %s =====
     343           0 :                 %s
     344           0 :                 ===== OPS =====
     345           0 :                 %s
     346           0 :                 `, runOpts.seed, metaDir, names[0], names[i], text, names[0], optionsStrA, names[i], optionsStrB, formattedOps)
     347           0 :                                         os.Exit(1)
     348           0 :                                 }
     349             :                         })
     350             :                 }
     351             :         })
     352             : }
     353             : 
     354             : type runOnceOptions struct {
     355             :         keep                bool
     356             :         maxThreads          int
     357             :         errorRate           float64
     358             :         failRegexp          *regexp.Regexp
     359             :         numInstances        int
     360             :         customOptionParsers map[string]func(string) (CustomOption, bool)
     361             : }
     362             : 
     363             : // A RunOnceOption configures the behavior of a single run of the metamorphic
     364             : // tests.
     365             : type RunOnceOption interface {
     366             :         applyOnce(*runOnceOptions)
     367             : }
     368             : 
     369             : // KeepData keeps the database directory, even on successful runs. If the test
     370             : // used an in-memory filesystem, the in-memory filesystem will be persisted to
     371             : // the run directory.
     372             : type KeepData struct{}
     373             : 
     374           0 : func (KeepData) apply(ro *runAndCompareOptions) { ro.keep = true }
     375           0 : func (KeepData) applyOnce(ro *runOnceOptions)   { ro.keep = true }
     376             : 
     377             : // InjectErrorsRate configures the run to inject errors into read-only
     378             : // filesystem operations and retry injected errors.
     379             : type InjectErrorsRate float64
     380             : 
     381           0 : func (r InjectErrorsRate) apply(ro *runAndCompareOptions) { ro.errorRate = float64(r) }
     382           0 : func (r InjectErrorsRate) applyOnce(ro *runOnceOptions)   { ro.errorRate = float64(r) }
     383             : 
     384             : // MaxThreads sets an upper bound on the number of parallel execution threads
     385             : // during replay.
     386             : type MaxThreads int
     387             : 
     388           0 : func (m MaxThreads) apply(ro *runAndCompareOptions) { ro.maxThreads = int(m) }
     389           1 : func (m MaxThreads) applyOnce(ro *runOnceOptions)   { ro.maxThreads = int(m) }
     390             : 
     391             : // FailOnMatch configures the run to fail immediately if the history matches the
     392             : // provided regular expression.
     393             : type FailOnMatch struct {
     394             :         *regexp.Regexp
     395             : }
     396             : 
     397           0 : func (f FailOnMatch) apply(ro *runAndCompareOptions) { ro.failRegexp = f.Regexp }
     398           0 : func (f FailOnMatch) applyOnce(ro *runOnceOptions)   { ro.failRegexp = f.Regexp }
     399             : 
     400             : // MultiInstance configures the number of pebble instances to create.
     401             : type MultiInstance int
     402             : 
     403           0 : func (m MultiInstance) apply(ro *runAndCompareOptions) { ro.numInstances = int(m) }
     404           1 : func (m MultiInstance) applyOnce(ro *runOnceOptions)   { ro.numInstances = int(m) }
     405             : 
     406             : // RunOnce performs one run of the metamorphic tests. RunOnce expects the
     407             : // directory named by `runDir` to already exist and contain an `OPTIONS` file
     408             : // containing the test run's configuration. The history of the run is persisted
     409             : // to a file at the path `historyPath`.
     410             : //
     411             : // The `seed` parameter is not functional; it's used for context in logging.
     412           1 : func RunOnce(t TestingT, runDir string, seed uint64, historyPath string, rOpts ...RunOnceOption) {
     413           1 :         runOpts := runOnceOptions{
     414           1 :                 customOptionParsers: map[string]func(string) (CustomOption, bool){},
     415           1 :         }
     416           1 :         for _, o := range rOpts {
     417           1 :                 o.applyOnce(&runOpts)
     418           1 :         }
     419             : 
     420           1 :         opsPath := filepath.Join(filepath.Dir(filepath.Clean(runDir)), "ops")
     421           1 :         opsData, err := os.ReadFile(opsPath)
     422           1 :         require.NoError(t, err)
     423           1 : 
     424           1 :         ops, err := parse(opsData)
     425           1 :         require.NoError(t, err)
     426           1 :         _ = ops
     427           1 : 
     428           1 :         optionsPath := filepath.Join(runDir, "OPTIONS")
     429           1 :         optionsData, err := os.ReadFile(optionsPath)
     430           1 :         require.NoError(t, err)
     431           1 : 
     432           1 :         // NB: It's important to use defaultTestOptions() here as the base into
     433           1 :         // which we parse the serialized options. It contains the relevant defaults,
     434           1 :         // like the appropriate block-property collectors.
     435           1 :         testOpts := defaultTestOptions()
     436           1 :         opts := testOpts.Opts
     437           1 :         require.NoError(t, parseOptions(testOpts, string(optionsData), runOpts.customOptionParsers))
     438           1 : 
     439           1 :         // Always use our custom comparer which provides a Split method, splitting
     440           1 :         // keys at the trailing '@'.
     441           1 :         opts.Comparer = testkeys.Comparer
     442           1 :         // Use an archive cleaner to ease post-mortem debugging.
     443           1 :         opts.Cleaner = base.ArchiveCleaner{}
     444           1 : 
     445           1 :         // Set up the filesystem to use for the test. Note that by default we use an
     446           1 :         // in-memory FS.
     447           1 :         if testOpts.useDisk {
     448           1 :                 opts.FS = vfs.Default
     449           1 :                 require.NoError(t, os.RemoveAll(opts.FS.PathJoin(runDir, "data")))
     450           1 :         } else {
     451           1 :                 opts.Cleaner = base.ArchiveCleaner{}
     452           1 :                 if testOpts.strictFS {
     453           1 :                         opts.FS = vfs.NewStrictMem()
     454           1 :                 } else {
     455           1 :                         opts.FS = vfs.NewMem()
     456           1 :                 }
     457             :         }
     458           1 :         opts.WithFSDefaults()
     459           1 : 
     460           1 :         threads := testOpts.threads
     461           1 :         if runOpts.maxThreads < threads {
     462           0 :                 threads = runOpts.maxThreads
     463           0 :         }
     464             : 
     465           1 :         dir := opts.FS.PathJoin(runDir, "data")
     466           1 :         // Set up the initial database state if configured to start from a non-empty
     467           1 :         // database. By default tests start from an empty database, but split
     468           1 :         // version testing may configure a previous metamorphic tests's database
     469           1 :         // state as the initial state.
     470           1 :         if testOpts.initialStatePath != "" {
     471           0 :                 require.NoError(t, setupInitialState(dir, testOpts))
     472           0 :         }
     473             : 
     474             :         // Wrap the filesystem with one that will inject errors into read
     475             :         // operations with *errorRate probability.
     476           1 :         opts.FS = errorfs.Wrap(opts.FS, errorfs.ErrInjected.If(
     477           1 :                 dsl.And[errorfs.Op](errorfs.Reads, errorfs.Randomly(runOpts.errorRate, int64(seed))),
     478           1 :         ))
     479           1 : 
     480           1 :         if opts.WALDir != "" {
     481           1 :                 if runOpts.numInstances > 1 {
     482           1 :                         // TODO(bilal): Allow opts to diverge on a per-instance basis, and use
     483           1 :                         // that to set unique WAL dirs for all instances in multi-instance mode.
     484           1 :                         opts.WALDir = ""
     485           1 :                 } else {
     486           1 :                         opts.WALDir = opts.FS.PathJoin(runDir, opts.WALDir)
     487           1 :                 }
     488             :         }
     489             : 
     490           1 :         historyFile, err := os.Create(historyPath)
     491           1 :         require.NoError(t, err)
     492           1 :         defer historyFile.Close()
     493           1 :         writers := []io.Writer{historyFile}
     494           1 : 
     495           1 :         if testing.Verbose() {
     496           0 :                 writers = append(writers, os.Stdout)
     497           0 :         }
     498           1 :         h := newHistory(runOpts.failRegexp, writers...)
     499           1 : 
     500           1 :         m := newTest(ops)
     501           1 :         require.NoError(t, m.init(h, dir, testOpts, runOpts.numInstances))
     502           1 : 
     503           1 :         if threads <= 1 {
     504           1 :                 for m.step(h) {
     505           1 :                         if err := h.Error(); err != nil {
     506           0 :                                 fmt.Fprintf(os.Stderr, "Seed: %d\n", seed)
     507           0 :                                 fmt.Fprintln(os.Stderr, err)
     508           0 :                                 m.maybeSaveData()
     509           0 :                                 os.Exit(1)
     510           0 :                         }
     511             :                 }
     512           1 :         } else {
     513           1 :                 eg, ctx := errgroup.WithContext(context.Background())
     514           1 :                 for t := 0; t < threads; t++ {
     515           1 :                         t := t // bind loop var to scope
     516           1 :                         eg.Go(func() error {
     517           1 :                                 for idx := 0; idx < len(m.ops); idx++ {
     518           1 :                                         // Skip any operations whose receiver object hashes to a
     519           1 :                                         // different thread. All operations with the same receiver
     520           1 :                                         // are performed from the same thread. This goroutine is
     521           1 :                                         // only responsible for executing operations that hash to
     522           1 :                                         // `t`.
     523           1 :                                         if hashThread(m.ops[idx].receiver(), threads) != t {
     524           1 :                                                 continue
     525             :                                         }
     526             : 
     527             :                                         // Some operations have additional synchronization
     528             :                                         // dependencies. If this operation has any, wait for its
     529             :                                         // dependencies to complete before executing.
     530           1 :                                         for _, waitOnIdx := range m.opsWaitOn[idx] {
     531           1 :                                                 select {
     532           0 :                                                 case <-ctx.Done():
     533           0 :                                                         // Exit if some other thread already errored out.
     534           0 :                                                         return ctx.Err()
     535           1 :                                                 case <-m.opsDone[waitOnIdx]:
     536             :                                                 }
     537             :                                         }
     538             : 
     539           1 :                                         m.ops[idx].run(m, h.recorder(t, idx))
     540           1 : 
     541           1 :                                         // If this operation has a done channel, close it so that
     542           1 :                                         // other operations that synchronize on this operation know
     543           1 :                                         // that it's been completed.
     544           1 :                                         if ch := m.opsDone[idx]; ch != nil {
     545           1 :                                                 close(ch)
     546           1 :                                         }
     547             : 
     548           1 :                                         if err := h.Error(); err != nil {
     549           0 :                                                 return err
     550           0 :                                         }
     551             :                                 }
     552           1 :                                 return nil
     553             :                         })
     554             :                 }
     555           1 :                 if err := eg.Wait(); err != nil {
     556           0 :                         fmt.Fprintf(os.Stderr, "Seed: %d\n", seed)
     557           0 :                         fmt.Fprintln(os.Stderr, err)
     558           0 :                         m.maybeSaveData()
     559           0 :                         os.Exit(1)
     560           0 :                 }
     561             :         }
     562             : 
     563           1 :         if runOpts.keep && !testOpts.useDisk {
     564           0 :                 m.maybeSaveData()
     565           0 :         }
     566             : }
     567             : 
     568           1 : func hashThread(objID objID, numThreads int) int {
     569           1 :         // Fibonacci hash https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/
     570           1 :         return int((11400714819323198485 * uint64(objID)) % uint64(numThreads))
     571           1 : }
     572             : 
     573             : // Compare runs the metamorphic tests in the provided runDirs and compares their
     574             : // histories.
     575           0 : func Compare(t TestingT, rootDir string, seed uint64, runDirs []string, rOpts ...RunOnceOption) {
     576           0 :         historyPaths := make([]string, len(runDirs))
     577           0 :         for i := 0; i < len(runDirs); i++ {
     578           0 :                 historyPath := filepath.Join(rootDir, runDirs[i]+"-"+time.Now().Format("060102-150405.000"))
     579           0 :                 runDirs[i] = filepath.Join(rootDir, runDirs[i])
     580           0 :                 _ = os.Remove(historyPath)
     581           0 :                 historyPaths[i] = historyPath
     582           0 :         }
     583           0 :         defer func() {
     584           0 :                 for _, path := range historyPaths {
     585           0 :                         _ = os.Remove(path)
     586           0 :                 }
     587             :         }()
     588             : 
     589           0 :         for i, runDir := range runDirs {
     590           0 :                 RunOnce(t, runDir, seed, historyPaths[i], rOpts...)
     591           0 :         }
     592             : 
     593           0 :         if t.Failed() {
     594           0 :                 return
     595           0 :         }
     596             : 
     597           0 :         i, diff := CompareHistories(t, historyPaths)
     598           0 :         if i != 0 {
     599           0 :                 fmt.Printf(`
     600           0 : ===== DIFF =====
     601           0 : %s/{%s,%s}
     602           0 : %s
     603           0 : `, rootDir, runDirs[0], runDirs[i], diff)
     604           0 :                 os.Exit(1)
     605           0 :         }
     606             : }
     607             : 
     608             : // TestingT is an interface wrapper around *testing.T
     609             : type TestingT interface {
     610             :         require.TestingT
     611             :         Failed() bool
     612             : }
     613             : 
     614           0 : func readFile(path string) string {
     615           0 :         history, err := os.ReadFile(path)
     616           0 :         if err != nil {
     617           0 :                 return fmt.Sprintf("err: %v", err)
     618           0 :         }
     619             : 
     620           0 :         return string(history)
     621             : }

Generated by: LCOV version 1.14