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

     1  // Copyright 2022 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 crossversion builds on the metamorphic testing implemented in
     6  // internal/metamorphic, performing metamorphic testing across versions of
     7  // Pebble. This improves test coverage of upgrade and migration code paths.
     8  package crossversion
     9  
    10  import (
    11  	"bytes"
    12  	"context"
    13  	"flag"
    14  	"fmt"
    15  	"io"
    16  	"math/rand"
    17  	"os"
    18  	"os/exec"
    19  	"path/filepath"
    20  	"strconv"
    21  	"strings"
    22  	"testing"
    23  	"time"
    24  	"unicode"
    25  
    26  	"github.com/cockroachdb/errors"
    27  	"github.com/stretchr/testify/require"
    28  	"github.com/zuoyebang/bitalostable/internal/metamorphic"
    29  )
    30  
    31  var (
    32  	factor   int
    33  	seed     int64
    34  	versions bitalostableVersions
    35  )
    36  
    37  func init() {
    38  	// NB: If you add new command-line flags, you should update the
    39  	// reproductionCommand function.
    40  	flag.Int64Var(&seed, "seed", 0,
    41  		`a pseudorandom number generator seed`)
    42  	flag.IntVar(&factor, "factor", 10,
    43  		`the number of data directories to carry forward
    44  from one version's run to the subsequent version's runs.`)
    45  	flag.Var(&versions, "version",
    46  		`a comma-separated 3-tuple defining a Pebble version to test.
    47  The expected format is <label>,<SHA>,<test-binary-path>.
    48  The label should be a human-readable label describing the
    49  version, for example, 'CRDB 22.1'. The SHA indicates the
    50  exact commit sha of the version, and may be abbreviated.
    51  The test binary path must point to a test binary of the
    52  internal/metamorphic package built on the indicated SHA.
    53  A test binary may be built with 'go test -c'.
    54  
    55  This flag should be provided multiple times to indicate
    56  the set of versions to test. The order of the versions
    57  is significant and database states generated from earlier
    58  versions will be used to initialize runs of subsequent
    59  versions.`)
    60  }
    61  
    62  func reproductionCommand() string {
    63  	return fmt.Sprintf("go test -v -run 'TestMetaCrossVersion' --seed %d --factor %d %s\n",
    64  		seed, factor, versions.String())
    65  }
    66  
    67  // TestMetaCrossVersion performs cross-version metamorphic testing.
    68  func TestMetaCrossVersion(t *testing.T) {
    69  	if seed == 0 {
    70  		seed = time.Now().UnixNano()
    71  	}
    72  	t.Logf("Test directory: %s\n", t.TempDir())
    73  	t.Logf("Reproduction:\n  %s\n", reproductionCommand())
    74  
    75  	// Print all the versions supplied and ensure all the test binaries
    76  	// actually exist before proceeding.
    77  	for i, v := range versions {
    78  		if len(v.SHA) > 8 {
    79  			// Use shortened SHAs for readability.
    80  			versions[i].SHA = versions[i].SHA[:8]
    81  		}
    82  		if _, err := os.Stat(v.TestBinaryPath); err != nil {
    83  			t.Fatal(err)
    84  		}
    85  		t.Logf("%d: %s", i, v.String())
    86  	}
    87  
    88  	// All randomness should be derived from `seed`. This makes reproducing a
    89  	// failure locally easier.
    90  	ctx := context.Background()
    91  	require.NoError(t, runCrossVersion(ctx, t, versions, seed, factor))
    92  }
    93  
    94  type bitalostableVersion struct {
    95  	Label          string
    96  	SHA            string
    97  	TestBinaryPath string
    98  }
    99  
   100  type initialState struct {
   101  	desc string
   102  	path string
   103  }
   104  
   105  func (s initialState) String() string {
   106  	if s.desc == "" {
   107  		return "<empty>"
   108  	}
   109  	return s.desc
   110  }
   111  
   112  func runCrossVersion(
   113  	ctx context.Context, t *testing.T, versions bitalostableVersions, seed int64, factor int,
   114  ) error {
   115  	prng := rand.New(rand.NewSource(seed))
   116  	// Use prng to derive deterministic seeds to provide to the child
   117  	// metamorphic runs. The same seed is used for all runs on a particular
   118  	// Pebble version.
   119  	versionSeeds := make([]uint64, len(versions))
   120  	for i := range versions {
   121  		versionSeeds[i] = prng.Uint64()
   122  	}
   123  
   124  	rootDir := filepath.Join(t.TempDir(), strconv.FormatInt(seed, 10))
   125  	if err := os.MkdirAll(rootDir, os.ModePerm); err != nil {
   126  		return err
   127  	}
   128  
   129  	// The outer for loop executes once per version being tested. It takes a
   130  	// list of initial states, populated by the previous version. The inner loop
   131  	// executes once per initial state, running the metamorphic test against the
   132  	// initial state.
   133  	//
   134  	// The number of states that are carried forward from one version to the
   135  	// next is fixed by `factor`.
   136  	var buf bytes.Buffer
   137  	initialStates := []initialState{{}}
   138  	for i := range versions {
   139  		t.Logf("Running tests with version %s with %d initial state(s).", versions[i].SHA, len(initialStates))
   140  		var nextInitialStates []initialState
   141  		var histories []string
   142  		for j, s := range initialStates {
   143  			runID := fmt.Sprintf("%s_%d_%03d", versions[i].SHA, seed, j)
   144  
   145  			t.Logf("  Running test with version %s with initial state %s.", versions[i].SHA, s)
   146  			r := metamorphicTestRun{
   147  				seed:           versionSeeds[i],
   148  				dir:            filepath.Join(rootDir, runID),
   149  				vers:           versions[i],
   150  				initialState:   s,
   151  				testBinaryName: filepath.Base(versions[i].TestBinaryPath),
   152  			}
   153  			if err := os.MkdirAll(r.dir, os.ModePerm); err != nil {
   154  				return err
   155  			}
   156  			if err := os.Link(versions[i].TestBinaryPath, filepath.Join(r.dir, r.testBinaryName)); err != nil {
   157  				return err
   158  			}
   159  			err := r.run(ctx, &buf)
   160  			if err != nil {
   161  				t.Fatalf("Metamorphic test failed: %s\nOutput:%s\n", err, buf.String())
   162  			}
   163  
   164  			// dir is a directory containing the ops file and subdirectories for
   165  			// each run with a particular set of OPTIONS. For example:
   166  			//
   167  			// dir/
   168  			//   ops
   169  			//   random-000/
   170  			//   random-001/
   171  			//   ...
   172  			//   standard-000/
   173  			//   standard-001/
   174  			//   ...
   175  			dir := getRunDir(t, r.dir)
   176  			// subrunDirs contains the names of all dir's subdirectories.
   177  			subrunDirs := getDirs(t, dir)
   178  			for _, subrunDir := range subrunDirs {
   179  				// Record the subrun as an initial state for the next version.
   180  				nextInitialStates = append(nextInitialStates, initialState{
   181  					path: filepath.Join(dir, subrunDir),
   182  					desc: fmt.Sprintf("sha=%s-seed=%d-opts=%s(%s)", versions[i].SHA, versionSeeds[i], subrunDir, s.String()),
   183  				})
   184  				histories = append(histories, filepath.Join(dir, subrunDir, "history"))
   185  			}
   186  
   187  			buf.Reset()
   188  		}
   189  
   190  		// All the initial states described the same state and all of this
   191  		// version's metamorphic runs used the same seed, so all of the
   192  		// resulting histories should be identical.
   193  		if h, diff := metamorphic.CompareHistories(t, histories); h > 0 {
   194  			t.Fatalf("Metamorphic test divergence between %q and %q:\nDiff:\n%s",
   195  				nextInitialStates[0].desc, nextInitialStates[h].desc, diff)
   196  		}
   197  
   198  		// Prune the set of initial states we collected for this version, using
   199  		// the deterministic randomness of prng to pick which states we keep.
   200  		if len(nextInitialStates) > factor {
   201  			prng.Shuffle(len(nextInitialStates), func(i, j int) {
   202  				nextInitialStates[i], nextInitialStates[j] = nextInitialStates[j], nextInitialStates[i]
   203  			})
   204  			// Delete the states that we're not going to use.
   205  			for _, s := range nextInitialStates[factor:] {
   206  				require.NoError(t, os.RemoveAll(s.path))
   207  			}
   208  			nextInitialStates = nextInitialStates[:factor]
   209  		}
   210  		initialStates = nextInitialStates
   211  	}
   212  	return nil
   213  }
   214  
   215  type metamorphicTestRun struct {
   216  	seed           uint64
   217  	dir            string
   218  	vers           bitalostableVersion
   219  	initialState   initialState
   220  	testBinaryName string
   221  }
   222  
   223  func (r *metamorphicTestRun) run(ctx context.Context, output io.Writer) error {
   224  	args := []string{
   225  		"-test.run", "TestMeta$",
   226  		"-seed", strconv.FormatUint(r.seed, 10),
   227  		"-keep",
   228  	}
   229  	if r.initialState.path != "" {
   230  		args = append(args,
   231  			"--initial-state", r.initialState.path,
   232  			"--initial-state-desc", r.initialState.desc)
   233  	}
   234  	cmd := exec.CommandContext(ctx, filepath.Join(r.dir, r.testBinaryName), args...)
   235  	cmd.Dir = r.dir
   236  	cmd.Stderr = output
   237  	cmd.Stdout = output
   238  	return cmd.Run()
   239  }
   240  
   241  func (v bitalostableVersion) String() string {
   242  	return fmt.Sprintf("%s,%s,%s", v.Label, v.SHA, v.TestBinaryPath)
   243  }
   244  
   245  type bitalostableVersions []bitalostableVersion
   246  
   247  func (f *bitalostableVersions) String() string {
   248  	var buf bytes.Buffer
   249  	for i, v := range *f {
   250  		if i > 0 {
   251  			fmt.Fprint(&buf, " ")
   252  		}
   253  		fmt.Fprintf(&buf, "--version %s", v.String())
   254  	}
   255  	return buf.String()
   256  }
   257  
   258  func (f *bitalostableVersions) Set(value string) error {
   259  	// Expected format is `<label>,<sha>,<path>`.
   260  	fields := strings.FieldsFunc(value, func(r rune) bool { return r == ',' || unicode.IsSpace(r) })
   261  	if len(fields) != 3 {
   262  		return errors.Newf("unable to parse version %q", value)
   263  	}
   264  	*f = append(*f, bitalostableVersion{
   265  		Label:          fields[0],
   266  		SHA:            fields[1],
   267  		TestBinaryPath: fields[2],
   268  	})
   269  	return nil
   270  }
   271  
   272  func getDirs(t testing.TB, dir string) (names []string) {
   273  	dirents, err := os.ReadDir(dir)
   274  	if err != nil {
   275  		t.Fatal(err)
   276  	}
   277  	for _, dirent := range dirents {
   278  		if dirent.IsDir() {
   279  			names = append(names, dirent.Name())
   280  		}
   281  	}
   282  	return names
   283  }
   284  
   285  func getRunDir(t testing.TB, dir string) string {
   286  	metaDir := filepath.Join(dir, "_meta")
   287  	dirs := getDirs(t, metaDir)
   288  	if len(dirs) != 1 {
   289  		t.Fatalf("expected 1 directory, found %d", len(dirs))
   290  	}
   291  	return filepath.Join(metaDir, dirs[0])
   292  }