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 }