github.com/zuoyebang/bitalostable@v1.0.1-0.20240229032404-e3b99a834294/internal/metamorphic/meta_test.go (about)

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