github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/cmd/smithtest/main.go (about) 1 // Copyright 2019 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 // smithtest is a tool to execute sqlsmith tests on cockroach demo 12 // instances. Failures are tracked, de-duplicated, reduced. Issues are 13 // prefilled for GitHub. 14 package main 15 16 import ( 17 "bufio" 18 "bytes" 19 "context" 20 gosql "database/sql" 21 "flag" 22 "fmt" 23 "io" 24 "log" 25 "math/rand" 26 "net/url" 27 "os" 28 "os/exec" 29 "regexp" 30 "strings" 31 "time" 32 33 "github.com/cockroachdb/cockroach/pkg/internal/sqlsmith" 34 "github.com/cockroachdb/cockroach/pkg/util/ctxgroup" 35 "github.com/cockroachdb/cockroach/pkg/util/syncutil" 36 "github.com/cockroachdb/cockroach/pkg/util/timeutil" 37 "github.com/cockroachdb/errors" 38 "github.com/google/go-github/github" 39 "github.com/jackc/pgx" 40 "github.com/lib/pq" 41 "github.com/pkg/browser" 42 ) 43 44 var ( 45 flags = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 46 cockroach = flags.String("cockroach", "./cockroach", "path to cockroach binary") 47 reduce = flags.String("reduce", "./bin/reduce", "path to reduce binary") 48 num = flags.Int("num", 1, "number of parallel testers") 49 ) 50 51 func usage() { 52 fmt.Fprintf(flags.Output(), "Usage of %s:\n", os.Args[0]) 53 flags.PrintDefaults() 54 os.Exit(1) 55 } 56 57 func main() { 58 if err := flags.Parse(os.Args[1:]); err != nil { 59 usage() 60 } 61 62 ctx := context.Background() 63 setup := WorkerSetup{ 64 cockroach: *cockroach, 65 reduce: *reduce, 66 github: github.NewClient(nil), 67 } 68 rand.Seed(timeutil.Now().UnixNano()) 69 70 setup.populateGitHubIssues(ctx) 71 72 fmt.Println("running...") 73 74 g := ctxgroup.WithContext(ctx) 75 for i := 0; i < *num; i++ { 76 g.GoCtx(setup.work) 77 } 78 if err := g.Wait(); err != nil { 79 log.Fatalf("%+v", err) 80 } 81 } 82 83 // WorkerSetup contains initialization and configuration for running smithers. 84 type WorkerSetup struct { 85 cockroach, reduce string 86 github *github.Client 87 } 88 89 // populateGitHubIssues populates seen with issues already in GitHub. 90 func (s WorkerSetup) populateGitHubIssues(ctx context.Context) { 91 var opts github.SearchOptions 92 for { 93 results, _, err := s.github.Search.Issues(ctx, "repo:cockroachdb/cockroach type:issue state:open label:C-bug label:O-sqlsmith", &opts) 94 if err != nil { 95 log.Fatal(err) 96 } 97 for _, issue := range results.Issues { 98 title := filterIssueTitle(issue.GetTitle()) 99 seenIssues[title] = true 100 fmt.Println("pre populate", title) 101 } 102 if results.GetIncompleteResults() { 103 opts.Page++ 104 continue 105 } 106 return 107 } 108 } 109 110 func (s WorkerSetup) work(ctx context.Context) error { 111 rnd := rand.New(rand.NewSource(rand.Int63())) 112 for { 113 if err := s.run(ctx, rnd); err != nil { 114 return err 115 } 116 } 117 } 118 119 var ( 120 // lock is used to both protect the seen map from concurrent access 121 // and prevent overuse of system resources. When the reducer needs to 122 // run it gets the exclusive write lock. When normal queries are being 123 // smithed, they use the communal read lock. Thus, the reducer being 124 // executed will pause the other testing queries and prevent 2 reducers 125 // from running at the same time. This should greatly speed up the time 126 // it takes for a single reduction run. 127 lock syncutil.RWMutex 128 // seenIssues tracks the seen github issues. 129 seenIssues = map[string]bool{} 130 131 connRE = regexp.MustCompile(`(?m)^sql:\s*(postgresql://.*)$`) 132 panicRE = regexp.MustCompile(`(?m)^(panic: .*?)( \[recovered\])?$`) 133 stackRE = regexp.MustCompile(`panic: .*\n\ngoroutine \d+ \[running\]:\n(?s:(.*))$`) 134 fatalRE = regexp.MustCompile(`(?m)^(fatal error: .*?)$`) 135 runtimeStackRE = regexp.MustCompile(`goroutine \d+ \[running\]:\n(?s:(.*?))\n\n`) 136 ) 137 138 // run is a single sqlsmith worker. It starts a new sqlsmither and in-memory 139 // single-node cluster. If an error is found it reduces and submits the 140 // issue. If an issue is successfully found, this function returns, causing 141 // the started cockroach instance to shut down. An error is only returned if 142 // something unexpected happened. That is, panics and internal errors will 143 // return nil, since they are expected. Something unexpected would be like the 144 // initialization SQL was unable to run. 145 func (s WorkerSetup) run(ctx context.Context, rnd *rand.Rand) error { 146 // Stop running after a while to get new setup and settings. 147 done := timeutil.Now().Add(time.Minute) 148 149 ctx, cancel := context.WithCancel(ctx) 150 defer cancel() 151 cmd := exec.CommandContext(ctx, s.cockroach, 152 "start-single-node", 153 "--port", "0", 154 "--http-port", "0", 155 "--insecure", 156 "--store=type=mem,size=1GB", 157 "--logtostderr", 158 ) 159 160 // Look for the connection string. 161 var pgdb *pgx.Conn 162 var db *gosql.DB 163 var output bytes.Buffer 164 165 stderr, err := cmd.StderrPipe() 166 if err != nil { 167 return err 168 } 169 if err := cmd.Start(); err != nil { 170 return errors.Wrap(err, "start") 171 } 172 173 scanner := bufio.NewScanner(io.TeeReader(stderr, &output)) 174 for scanner.Scan() { 175 line := scanner.Text() 176 if match := connRE.FindStringSubmatch(line); match != nil { 177 config, err := pgx.ParseURI(match[1]) 178 if err != nil { 179 return errors.Wrap(err, "parse uri") 180 } 181 pgdb, err = pgx.Connect(config) 182 if err != nil { 183 return errors.Wrap(err, "connect") 184 } 185 186 connector, err := pq.NewConnector(match[1]) 187 if err != nil { 188 return errors.Wrap(err, "connector error") 189 } 190 db = gosql.OpenDB(connector) 191 if err != nil { 192 return errors.Wrap(err, "connect") 193 } 194 fmt.Println("connected to", match[1]) 195 break 196 } 197 } 198 if err := scanner.Err(); err != nil { 199 fmt.Println(output.String()) 200 return errors.Wrap(err, "scanner error") 201 } 202 if db == nil { 203 fmt.Println(output.String()) 204 return errors.New("no DB address found") 205 } 206 fmt.Println("worker started") 207 208 initSQL := sqlsmith.Setups[sqlsmith.RandSetup(rnd)](rnd) 209 if _, err := pgdb.ExecEx(ctx, initSQL, nil); err != nil { 210 return errors.Wrap(err, "init") 211 } 212 213 setting := sqlsmith.Settings[sqlsmith.RandSetting(rnd)](rnd) 214 opts := append([]sqlsmith.SmitherOption{ 215 sqlsmith.DisableMutations(), 216 }, setting.Options...) 217 smither, err := sqlsmith.NewSmither(db, rnd, opts...) 218 if err != nil { 219 return errors.Wrap(err, "new smither") 220 } 221 for { 222 if timeutil.Now().After(done) { 223 return nil 224 } 225 226 // If lock is locked for writing (due to a found bug in another 227 // go routine), block here until it has finished reducing. 228 lock.RLock() 229 stmt := smither.Generate() 230 done := make(chan struct{}, 1) 231 go func() { 232 _, err = pgdb.ExecEx(ctx, stmt, nil) 233 done <- struct{}{} 234 }() 235 // Timeout slow statements by returning, which will cancel the 236 // command's context by the above defer. 237 select { 238 case <-time.After(10 * time.Second): 239 fmt.Printf("TIMEOUT:\n%s\n", stmt) 240 lock.RUnlock() 241 return nil 242 case <-done: 243 } 244 lock.RUnlock() 245 if err != nil { 246 if strings.Contains(err.Error(), "internal error") { 247 // Return from this function on internal 248 // errors. This causes the current cockroach 249 // instance to shut down and we start a new 250 // one. This is not strictly necessary, since 251 // internal errors don't mess up the rest of 252 // cockroach, but it's just easier to have a 253 // single logic flow in case of a found error, 254 // which is to shut down and start over (just 255 // like the panic case below). 256 return s.failure(ctx, initSQL, stmt, err) 257 } 258 259 } 260 // If we can't ping, check if the statement caused a panic. 261 if err := db.PingContext(ctx); err != nil { 262 input := fmt.Sprintf("%s; %s;", initSQL, stmt) 263 out, _ := exec.CommandContext(ctx, s.cockroach, "demo", "--empty", "-e", input).CombinedOutput() 264 var pqerr pq.Error 265 if match := stackRE.FindStringSubmatch(string(out)); match != nil { 266 pqerr.Detail = strings.TrimSpace(match[1]) 267 } 268 if match := panicRE.FindStringSubmatch(string(out)); match != nil { 269 // We found a panic as expected. 270 pqerr.Message = match[1] 271 return s.failure(ctx, initSQL, stmt, &pqerr) 272 } 273 // Not a panic. Maybe a fatal? 274 if match := runtimeStackRE.FindStringSubmatch(string(out)); match != nil { 275 pqerr.Detail = strings.TrimSpace(match[1]) 276 } 277 if match := fatalRE.FindStringSubmatch(string(out)); match != nil { 278 // A real bad non-panic error. 279 pqerr.Message = match[1] 280 return s.failure(ctx, initSQL, stmt, &pqerr) 281 } 282 // A panic was not found. Shut everything down by returning an error so it can be investigated. 283 fmt.Printf("output:\n%s\n", out) 284 fmt.Printf("Ping stmt:\n%s;\n", stmt) 285 return err 286 } 287 } 288 } 289 290 // failure de-duplicates, reduces, and files errors. It generally returns nil 291 // indicating that this was successfully filed and we should continue looking 292 // for errors. 293 func (s WorkerSetup) failure(ctx context.Context, initSQL, stmt string, err error) error { 294 var message, stack string 295 var pqerr pgx.PgError 296 if errors.As(err, &pqerr) { 297 stack = pqerr.Detail 298 message = pqerr.Message 299 } else { 300 message = err.Error() 301 } 302 filteredMessage := filterIssueTitle(regexp.QuoteMeta(message)) 303 message = fmt.Sprintf("sql: %s", message) 304 305 lock.Lock() 306 // Keep this locked for the remainder of the function so that smither 307 // tests won't run during the reducer, and only one reducer can run 308 // at once. 309 defer lock.Unlock() 310 sqlFilteredMessage := fmt.Sprintf("sql: %s", filteredMessage) 311 alreadySeen := seenIssues[sqlFilteredMessage] 312 if !alreadySeen { 313 seenIssues[sqlFilteredMessage] = true 314 } 315 if alreadySeen { 316 fmt.Println("already found", message) 317 return nil 318 } 319 fmt.Println("found", message) 320 input := fmt.Sprintf("%s\n\n%s;", initSQL, stmt) 321 fmt.Printf("SQL:\n%s\n\n", input) 322 323 // Run reducer. 324 cmd := exec.CommandContext(ctx, s.reduce, "-v", "-contains", filteredMessage) 325 cmd.Stdin = strings.NewReader(input) 326 cmd.Stderr = os.Stderr 327 var out bytes.Buffer 328 cmd.Stdout = &out 329 if err := cmd.Run(); err != nil { 330 fmt.Println(input) 331 return err 332 } 333 334 // Generate the pre-filled github issue. 335 makeBody := func() string { 336 return fmt.Sprintf("```\n%s\n```\n\n```\n%s\n```", strings.TrimSpace(out.String()), strings.TrimSpace(stack)) 337 } 338 query := url.Values{ 339 "title": []string{message}, 340 "labels": []string{"C-bug,O-sqlsmith"}, 341 "body": []string{makeBody()}, 342 } 343 url := url.URL{ 344 Scheme: "https", 345 Host: "github.com", 346 Path: "/cockroachdb/cockroach/issues/new", 347 RawQuery: query.Encode(), 348 } 349 const max = 8000 350 // Remove lines from the stack trace to shorten up the request so it's 351 // under the github limit. 352 for len(url.String()) > max { 353 last := strings.LastIndex(stack, "\n") 354 if last < 0 { 355 break 356 } 357 stack = stack[:last] 358 query["body"][0] = makeBody() 359 url.RawQuery = query.Encode() 360 } 361 if len(url.String()) > max { 362 fmt.Println(stmt) 363 return errors.New("request could not be shortened to max length") 364 } 365 366 if err := browser.OpenURL(url.String()); err != nil { 367 return err 368 } 369 370 return nil 371 } 372 373 // filterIssueTitle handles issue title where some words in the title can 374 // vary for identical issues. Usually things like number of bytes, IDs, or 375 // counts. These are converted into their regex equivalent so they can be 376 // correctly de-duplicated. 377 func filterIssueTitle(s string) string { 378 for _, reS := range []string{ 379 `given: .*, expected .*`, 380 `Datum is .*, not .*`, 381 `expected .*, found .*`, 382 `\d+`, 383 `\*tree\.D\w+`, 384 } { 385 re := regexp.MustCompile(reS) 386 s = re.ReplaceAllString(s, reS) 387 } 388 return s 389 }