github.com/cockroachdb/pebble@v1.1.1-0.20240513155919-3622ade60459/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 "sync" 23 "testing" 24 "time" 25 "unicode" 26 27 "github.com/cockroachdb/errors" 28 "github.com/cockroachdb/pebble/metamorphic" 29 "github.com/cockroachdb/pebble/vfs" 30 "github.com/stretchr/testify/require" 31 ) 32 33 var ( 34 factor int 35 seed int64 36 versions pebbleVersions 37 artifactsDir string 38 streamOutput bool 39 ) 40 41 func init() { 42 // NB: If you add new command-line flags, you should update the 43 // reproductionCommand function. 44 flag.Int64Var(&seed, "seed", 0, 45 `a pseudorandom number generator seed`) 46 flag.IntVar(&factor, "factor", 10, 47 `the number of data directories to carry forward 48 from one version's run to the subsequent version's runs.`) 49 flag.Var(&versions, "version", 50 `a comma-separated 3-tuple defining a Pebble version to test. 51 The expected format is <label>,<SHA>,<test-binary-path>. 52 The label should be a human-readable label describing the 53 version, for example, 'CRDB-22.1'. The SHA indicates the 54 exact commit sha of the version, and may be abbreviated. 55 The test binary path must point to a test binary of the 56 internal/metamorphic package built on the indicated SHA. 57 A test binary may be built with 'go test -c'. 58 59 This flag should be provided multiple times to indicate 60 the set of versions to test. The order of the versions 61 is significant and database states generated from earlier 62 versions will be used to initialize runs of subsequent 63 versions.`) 64 flag.StringVar(&artifactsDir, "artifacts", "", 65 `the path to a directory where test artifacts should be 66 moved on failure. Defaults to the current working directory.`) 67 flag.BoolVar(&streamOutput, "stream-output", false, 68 `stream TestMeta output to standard output`) 69 } 70 71 func reproductionCommand() string { 72 return fmt.Sprintf( 73 "SEED=%d FACTOR=%d ./scripts/run-crossversion-meta.sh %s\n", 74 seed, factor, versions.String(), 75 ) 76 } 77 78 // TestMetaCrossVersion performs cross-version metamorphic testing. 79 // 80 // It runs tests against the internal/metamorphic test binaries specified with 81 // multiple instances of the -version flag, exercising upgrade and migration 82 // code paths. 83 // 84 // More specifically, assume we are passed the following versions: 85 // 86 // --version 22.2,<sha>,meta-22-2.test --version 23.1,<sha>,meta-23-1.test 87 // 88 // TestMetaCrossVersion will: 89 // - run TestMeta on meta-22-2.test; 90 // - retain a random subset of the resulting directories (each directory is a 91 // store after a sequence of operations); 92 // - run TestMeta on meta-23.1.test once for every retained directory from the 93 // previous version (using it as initial state). 94 func TestMetaCrossVersion(t *testing.T) { 95 if seed == 0 { 96 seed = time.Now().UnixNano() 97 } 98 tempDir := t.TempDir() 99 t.Logf("Test directory: %s\n", tempDir) 100 t.Logf("Reproduction:\n %s\n", reproductionCommand()) 101 102 // Print all the versions supplied and ensure all the test binaries 103 // actually exist before proceeding. 104 for i, v := range versions { 105 if len(v.SHA) > 8 { 106 // Use shortened SHAs for readability. 107 versions[i].SHA = versions[i].SHA[:8] 108 } 109 absPath, err := filepath.Abs(v.TestBinaryPath) 110 if err != nil { 111 t.Fatal(err) 112 } 113 fi, err := os.Stat(absPath) 114 if err != nil { 115 t.Fatal(err) 116 } 117 versions[i].TestBinaryPath = absPath 118 t.Logf("%d: %s (Mode = %s)", i, v.String(), fi.Mode()) 119 } 120 121 // All randomness should be derived from `seed`. This makes reproducing a 122 // failure locally easier. 123 ctx := context.Background() 124 require.NoError(t, runCrossVersion(ctx, t, tempDir, versions, seed, factor)) 125 } 126 127 type pebbleVersion struct { 128 Label string 129 SHA string 130 TestBinaryPath string 131 } 132 133 type initialState struct { 134 desc string 135 path string 136 } 137 138 func (s initialState) String() string { 139 if s.desc == "" { 140 return "<empty>" 141 } 142 return s.desc 143 } 144 145 func runCrossVersion( 146 ctx context.Context, 147 t *testing.T, 148 tempDir string, 149 versions pebbleVersions, 150 seed int64, 151 factor int, 152 ) error { 153 prng := rand.New(rand.NewSource(seed)) 154 // Use prng to derive deterministic seeds to provide to the child 155 // metamorphic runs. The same seed is used for all runs on a particular 156 // Pebble version. 157 versionSeeds := make([]uint64, len(versions)) 158 for i := range versions { 159 versionSeeds[i] = prng.Uint64() 160 } 161 162 rootDir := filepath.Join(tempDir, strconv.FormatInt(seed, 10)) 163 if err := os.MkdirAll(rootDir, os.ModePerm); err != nil { 164 return err 165 } 166 167 // When run with test parallelism, multiple tests may fail concurrently. 168 // Only one should actually run the test failure logic which copies the root 169 // dir into the artifacts directory. 170 var fatalOnce sync.Once 171 172 // The outer for loop executes once per version being tested. It takes a 173 // list of initial states, populated by the previous version. The inner loop 174 // executes once per initial state, running the metamorphic test against the 175 // initial state. 176 // 177 // The number of states that are carried forward from one version to the 178 // next is fixed by `factor`. 179 initialStates := []initialState{{}} 180 for i := range versions { 181 t.Logf("Running tests with version %s with %d initial state(s).", versions[i].SHA, len(initialStates)) 182 histories, nextInitialStates, err := runVersion(ctx, t, &fatalOnce, rootDir, versions[i], versionSeeds[i], initialStates) 183 if err != nil { 184 return err 185 } 186 187 // All the initial states described the same state and all of this 188 // version's metamorphic runs used the same seed, so all of the 189 // resulting histories should be identical. 190 if h, diff := metamorphic.CompareHistories(t, histories); h > 0 { 191 fatalf(t, &fatalOnce, rootDir, "Metamorphic test divergence between %q and %q:\nDiff:\n%s", 192 nextInitialStates[0].desc, nextInitialStates[h].desc, diff) 193 } 194 195 // Prune the set of initial states we collected for this version, using 196 // the deterministic randomness of prng to pick which states we keep. 197 if len(nextInitialStates) > factor { 198 prng.Shuffle(len(nextInitialStates), func(i, j int) { 199 nextInitialStates[i], nextInitialStates[j] = nextInitialStates[j], nextInitialStates[i] 200 }) 201 // Delete the states that we're not going to use. 202 for _, s := range nextInitialStates[factor:] { 203 require.NoError(t, os.RemoveAll(s.path)) 204 } 205 nextInitialStates = nextInitialStates[:factor] 206 } 207 initialStates = nextInitialStates 208 } 209 return nil 210 } 211 212 func runVersion( 213 ctx context.Context, 214 t *testing.T, 215 fatalOnce *sync.Once, 216 rootDir string, 217 vers pebbleVersion, 218 seed uint64, 219 initialStates []initialState, 220 ) (histories []string, nextInitialStates []initialState, err error) { 221 // mu guards histories and nextInitialStates. The subtests may be run in 222 // parallel (via t.Parallel()). 223 var mu sync.Mutex 224 225 // The outer 'execution-<label>' subtest will block until all of the 226 // individual subtests have completed. 227 t.Run(fmt.Sprintf("execution-%s", vers.Label), func(t *testing.T) { 228 for j, s := range initialStates { 229 j, s := j, s // re-bind loop vars to scope 230 231 runID := fmt.Sprintf("%s_%s_%d_%03d", vers.Label, vers.SHA, seed, j) 232 r := metamorphicTestRun{ 233 seed: seed, 234 dir: filepath.Join(rootDir, runID), 235 vers: vers, 236 initialState: s, 237 testBinaryPath: vers.TestBinaryPath, 238 } 239 t.Run(s.desc, func(t *testing.T) { 240 t.Parallel() 241 require.NoError(t, os.MkdirAll(r.dir, os.ModePerm)) 242 243 var buf bytes.Buffer 244 var out io.Writer = &buf 245 if streamOutput { 246 out = io.MultiWriter(out, os.Stderr) 247 } 248 t.Logf(" Running test with version %s with initial state %s.", 249 vers.SHA, s) 250 if err := r.run(ctx, out); err != nil { 251 fatalf(t, fatalOnce, rootDir, "Metamorphic test failed: %s\nOutput:%s\n", err, buf.String()) 252 } 253 254 // dir is a directory containing the ops file and subdirectories for 255 // each run with a particular set of OPTIONS. For example: 256 // 257 // dir/ 258 // ops 259 // random-000/ 260 // random-001/ 261 // ... 262 // standard-000/ 263 // standard-001/ 264 // ... 265 dir := getRunDir(t, r.dir) 266 // subrunDirs contains the names of all dir's subdirectories. 267 subrunDirs := getDirs(t, dir) 268 269 mu.Lock() 270 defer mu.Unlock() 271 for _, subrunDir := range subrunDirs { 272 // Record the subrun as an initial state for the next version. 273 nextInitialStates = append(nextInitialStates, initialState{ 274 path: filepath.Join(dir, subrunDir), 275 desc: fmt.Sprintf("sha=%s-seed=%d-opts=%s(%s)", vers.SHA, seed, subrunDir, s.String()), 276 }) 277 histories = append(histories, filepath.Join(dir, subrunDir, "history")) 278 } 279 }) 280 } 281 }) 282 return histories, nextInitialStates, err 283 } 284 285 func fatalf(t testing.TB, fatalOnce *sync.Once, dir string, msg string, args ...interface{}) { 286 fatalOnce.Do(func() { 287 if artifactsDir == "" { 288 var err error 289 artifactsDir, err = os.Getwd() 290 require.NoError(t, err) 291 } 292 // When run with test parallelism, other subtests may still be running 293 // within subdirectories of `dir`. We copy instead of rename so that those 294 // substests don't also fail when we remove their files out from under them. 295 // Those additional failures would confuse the test output. 296 dst := filepath.Join(artifactsDir, filepath.Base(dir)) 297 t.Logf("Copying test dir %q to %q.", dir, dst) 298 _, err := vfs.Clone(vfs.Default, vfs.Default, dir, dst, vfs.CloneTryLink) 299 if err != nil { 300 t.Error(err) 301 } 302 t.Fatalf(msg, args...) 303 }) 304 } 305 306 type metamorphicTestRun struct { 307 seed uint64 308 dir string 309 vers pebbleVersion 310 initialState initialState 311 testBinaryPath string 312 } 313 314 func (r *metamorphicTestRun) run(ctx context.Context, output io.Writer) error { 315 args := []string{ 316 "-test.run", "TestMeta$", 317 "-seed", strconv.FormatUint(r.seed, 10), 318 "-keep", 319 // Use an op-count distribution that includes a low lower bound, so that 320 // some intermediary versions do very little work besides opening the 321 // database. This helps exercise state from version n that survives to 322 // versions ≥ n+2. 323 "-ops", "uniform:1-10000", 324 // Explicitly specify the location of the _meta directory. In Cockroach 325 // CI when built using bazel, the subprocesses may be given a different 326 // current working directory than the one provided below. To ensure we 327 // can find this run's artifacts, explicitly pass the intended dir. 328 "-dir", filepath.Join(r.dir, "_meta"), 329 } 330 // Propagate the verbose flag, if necessary. 331 if testing.Verbose() { 332 args = append(args, "-test.v") 333 } 334 if r.initialState.path != "" { 335 args = append(args, 336 "--initial-state", r.initialState.path, 337 "--initial-state-desc", r.initialState.desc) 338 } 339 cmd := exec.CommandContext(ctx, r.testBinaryPath, args...) 340 cmd.Dir = r.dir 341 cmd.Stderr = output 342 cmd.Stdout = output 343 344 // Print the command itself before executing it. 345 if testing.Verbose() { 346 fmt.Fprintln(output, cmd) 347 } 348 349 return cmd.Run() 350 } 351 352 func (v pebbleVersion) String() string { 353 return fmt.Sprintf("%s,%s,%s", v.Label, v.SHA, v.TestBinaryPath) 354 } 355 356 // pebbleVersions implements flag.Value for the -version flag. 357 type pebbleVersions []pebbleVersion 358 359 var _ flag.Value = (*pebbleVersions)(nil) 360 361 // String returns the SHAs of the versions. 362 func (f *pebbleVersions) String() string { 363 var buf bytes.Buffer 364 for i, v := range *f { 365 if i > 0 { 366 fmt.Fprint(&buf, " ") 367 } 368 fmt.Fprintf(&buf, v.SHA) 369 } 370 return buf.String() 371 } 372 373 // Set is part of the flag.Value interface; it is called once for every 374 // occurrence of the version flag. 375 func (f *pebbleVersions) Set(value string) error { 376 // Expected format is `<label>,<sha>,<path>`. 377 fields := strings.FieldsFunc(value, func(r rune) bool { return r == ',' || unicode.IsSpace(r) }) 378 if len(fields) != 3 { 379 return errors.Newf("unable to parse version %q", value) 380 } 381 *f = append(*f, pebbleVersion{ 382 Label: fields[0], 383 SHA: fields[1], 384 TestBinaryPath: fields[2], 385 }) 386 return nil 387 } 388 389 func getDirs(t testing.TB, dir string) (names []string) { 390 dirents, err := os.ReadDir(dir) 391 if err != nil { 392 t.Fatal(err) 393 } 394 for _, dirent := range dirents { 395 if dirent.IsDir() { 396 names = append(names, dirent.Name()) 397 } 398 } 399 return names 400 } 401 402 func getRunDir(t testing.TB, dir string) string { 403 metaDir := filepath.Join(dir, "_meta") 404 dirs := getDirs(t, metaDir) 405 if len(dirs) != 1 { 406 t.Fatalf("expected 1 directory, found %d", len(dirs)) 407 } 408 return filepath.Join(metaDir, dirs[0]) 409 }