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  }