2015-06-16 23:36:46 +03:00
|
|
|
///bin/true; exec /usr/bin/env go run "$0" "$@"
|
|
|
|
|
2015-04-22 06:12:20 +03:00
|
|
|
// Copyright 2015, Google Inc. All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
/*
|
|
|
|
test.go is a "Go script" for running Vitess tests. It runs each test in its own
|
|
|
|
Docker container for hermeticity and (potentially) parallelism. If a test fails,
|
|
|
|
this script will save the output in _test/ and continue with other tests.
|
|
|
|
|
|
|
|
Before using it, you should have Docker 1.5+ installed, and have your user in
|
|
|
|
the group that lets you run the docker command without sudo. The first time you
|
|
|
|
run against a given flavor, it may take some time for the corresponding
|
|
|
|
bootstrap image (vitess/bootstrap:<flavor>) to be downloaded.
|
|
|
|
|
|
|
|
It is meant to be run from the Vitess root, like so:
|
|
|
|
~/src/github.com/youtube/vitess$ go run test.go [args]
|
|
|
|
|
|
|
|
For a list of options, run:
|
|
|
|
$ go run test.go --help
|
|
|
|
*/
|
|
|
|
package main
|
|
|
|
|
2015-05-08 01:31:29 +03:00
|
|
|
// This Go script shouldn't rely on any packages that aren't in the standard
|
|
|
|
// library, since that would require the user to bootstrap before running it.
|
2015-04-22 06:12:20 +03:00
|
|
|
import (
|
2015-08-24 04:26:02 +03:00
|
|
|
"bytes"
|
2015-04-22 06:12:20 +03:00
|
|
|
"encoding/json"
|
2015-08-23 15:43:21 +03:00
|
|
|
"errors"
|
2015-04-22 06:12:20 +03:00
|
|
|
"flag"
|
|
|
|
"fmt"
|
2015-06-14 23:03:14 +03:00
|
|
|
"io"
|
2015-04-22 06:12:20 +03:00
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
2015-08-24 04:26:02 +03:00
|
|
|
"net/http"
|
|
|
|
"net/url"
|
2015-04-22 06:12:20 +03:00
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"os/signal"
|
|
|
|
"path"
|
2015-06-12 10:32:24 +03:00
|
|
|
"sort"
|
2015-08-24 04:26:02 +03:00
|
|
|
"strconv"
|
2015-08-23 12:56:01 +03:00
|
|
|
"strings"
|
2015-04-22 06:12:20 +03:00
|
|
|
"syscall"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
2015-06-12 10:32:24 +03:00
|
|
|
var usage = `Usage of test.go:
|
|
|
|
|
|
|
|
go run test.go [options] [test_name ...]
|
|
|
|
|
|
|
|
If one or more test names are provided, run only those tests.
|
|
|
|
Otherwise, run all tests in test/config.json.
|
|
|
|
`
|
|
|
|
|
2015-05-08 01:31:29 +03:00
|
|
|
// Flags
|
2015-04-22 06:12:20 +03:00
|
|
|
var (
|
|
|
|
flavor = flag.String("flavor", "mariadb", "bootstrap flavor to run against")
|
2015-06-12 10:41:52 +03:00
|
|
|
runCount = flag.Int("runs", 1, "run each test this many times")
|
2015-04-22 06:12:20 +03:00
|
|
|
retryMax = flag.Int("retry", 3, "max number of retries, to detect flaky tests")
|
|
|
|
logPass = flag.Bool("log-pass", false, "log test output even if it passes")
|
|
|
|
timeout = flag.Duration("timeout", 10*time.Minute, "timeout for each test")
|
2015-06-27 04:27:25 +03:00
|
|
|
pull = flag.Bool("pull", true, "re-pull the bootstrap image, in case it's been updated")
|
2015-08-23 12:56:01 +03:00
|
|
|
docker = flag.Bool("docker", true, "run tests with Docker")
|
2015-08-23 15:43:21 +03:00
|
|
|
shard = flag.Int("shard", -1, "if >=0, run the tests whose Shard matches")
|
|
|
|
reshard = flag.Int("reshard", 0, "if >0, check the stats and group tests into similarly-sized bins by average run time")
|
2015-08-24 04:26:02 +03:00
|
|
|
keepData = flag.Bool("keep-data", false, "don't delete the per-test VTDATAROOT subfolders")
|
|
|
|
printLog = flag.Bool("print-log", false, "print the log of each failed test (or all tests if -log-pass) to the console")
|
|
|
|
follow = flag.Bool("follow", false, "print test output as it runs, instead of waiting to see if it passes or fails")
|
|
|
|
|
|
|
|
remoteStats = flag.String("remote-stats", "", "url to send remote stats")
|
2015-06-05 01:54:29 +03:00
|
|
|
|
|
|
|
extraArgs = flag.String("extra-args", "", "extra args to pass to each test")
|
2015-04-22 06:12:20 +03:00
|
|
|
)
|
|
|
|
|
2015-08-23 12:56:01 +03:00
|
|
|
var vtDataRoot = os.Getenv("VTDATAROOT")
|
|
|
|
|
2015-08-23 15:43:21 +03:00
|
|
|
const (
|
|
|
|
statsFileName = "test/stats.json"
|
|
|
|
configFileName = "test/config.json"
|
|
|
|
)
|
|
|
|
|
2015-04-22 06:12:20 +03:00
|
|
|
// Config is the overall object serialized in test/config.json.
|
|
|
|
type Config struct {
|
2015-06-12 10:32:24 +03:00
|
|
|
Tests map[string]*Test
|
2015-04-22 06:12:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Test is an entry from the test/config.json file.
|
|
|
|
type Test struct {
|
2015-08-24 04:26:02 +03:00
|
|
|
File, Args, Command string
|
2015-05-08 01:31:29 +03:00
|
|
|
|
2015-08-23 13:33:46 +03:00
|
|
|
// Manual means it won't be run unless explicitly specified.
|
|
|
|
Manual bool
|
|
|
|
|
2015-08-23 15:43:21 +03:00
|
|
|
// Shard is used to split tests among workers.
|
|
|
|
Shard int
|
|
|
|
|
2015-08-24 04:26:02 +03:00
|
|
|
name string
|
2015-06-14 21:06:11 +03:00
|
|
|
cmd *exec.Cmd
|
|
|
|
runIndex int
|
2015-08-24 04:26:02 +03:00
|
|
|
|
|
|
|
pass, fail int
|
2015-04-22 06:12:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// run executes a single try.
|
2015-05-08 01:31:29 +03:00
|
|
|
// dir is the location of the vitess repo to use.
|
2015-08-23 12:56:01 +03:00
|
|
|
// dataDir is the VTDATAROOT to use for this run.
|
2015-06-14 23:03:14 +03:00
|
|
|
// returns the combined stdout+stderr and error.
|
2015-08-23 12:56:01 +03:00
|
|
|
func (t *Test) run(dir, dataDir string) ([]byte, error) {
|
2015-06-16 20:56:57 +03:00
|
|
|
testCmd := t.Command
|
|
|
|
if testCmd == "" {
|
2015-08-23 12:56:01 +03:00
|
|
|
testCmd = fmt.Sprintf("make build && test/%s -v --skip-build --keep-logs %s", t.File, t.Args)
|
|
|
|
if *docker {
|
|
|
|
// Teardown is unnecessary since Docker kills everything.
|
|
|
|
testCmd += " --skip-teardown"
|
|
|
|
}
|
2015-06-16 20:56:57 +03:00
|
|
|
if *extraArgs != "" {
|
|
|
|
testCmd += " " + *extraArgs
|
|
|
|
}
|
2015-06-05 01:54:29 +03:00
|
|
|
}
|
2015-08-23 12:56:01 +03:00
|
|
|
|
|
|
|
if *docker {
|
|
|
|
t.cmd = exec.Command(path.Join(dir, "docker/test/run.sh"), *flavor, testCmd)
|
|
|
|
} else {
|
|
|
|
t.cmd = exec.Command("bash", "-c", testCmd)
|
|
|
|
}
|
|
|
|
t.cmd.Dir = dir
|
|
|
|
|
|
|
|
// Put everything in a unique dir, so we can copy and/or safely delete it.
|
|
|
|
t.cmd.Env = updateEnv(os.Environ(), map[string]string{
|
|
|
|
"VTDATAROOT": dataDir,
|
|
|
|
})
|
2015-04-22 06:12:20 +03:00
|
|
|
|
|
|
|
// Stop the test if it takes too long.
|
|
|
|
done := make(chan struct{})
|
|
|
|
timer := time.NewTimer(*timeout)
|
|
|
|
defer timer.Stop()
|
|
|
|
go func() {
|
|
|
|
select {
|
|
|
|
case <-done:
|
|
|
|
case <-timer.C:
|
|
|
|
t.logf("timeout exceeded")
|
2015-08-23 12:56:01 +03:00
|
|
|
if t.cmd.Process != nil {
|
|
|
|
t.cmd.Process.Signal(syscall.SIGTERM)
|
2015-04-22 06:12:20 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2015-06-14 23:03:14 +03:00
|
|
|
defer close(done)
|
2015-08-24 04:26:02 +03:00
|
|
|
|
|
|
|
// Capture test output.
|
|
|
|
buf := &bytes.Buffer{}
|
|
|
|
t.cmd.Stdout = buf
|
|
|
|
if *follow {
|
|
|
|
t.cmd.Stdout = io.MultiWriter(t.cmd.Stdout, os.Stdout)
|
|
|
|
}
|
|
|
|
t.cmd.Stderr = t.cmd.Stdout
|
|
|
|
|
|
|
|
// Run the test.
|
|
|
|
err := t.cmd.Run()
|
|
|
|
if err == nil {
|
|
|
|
t.pass++
|
|
|
|
} else {
|
|
|
|
t.fail++
|
|
|
|
}
|
|
|
|
return buf.Bytes(), err
|
2015-04-22 06:12:20 +03:00
|
|
|
}
|
|
|
|
|
2015-05-08 01:31:29 +03:00
|
|
|
// stop will terminate the test if it's running.
|
|
|
|
// If the test is not running, it's a no-op.
|
|
|
|
func (t *Test) stop() {
|
|
|
|
if cmd := t.cmd; cmd != nil {
|
|
|
|
if proc := cmd.Process; proc != nil {
|
|
|
|
proc.Signal(syscall.SIGTERM)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *Test) logf(format string, v ...interface{}) {
|
2015-06-14 21:06:11 +03:00
|
|
|
if *runCount > 1 {
|
2015-08-24 04:26:02 +03:00
|
|
|
log.Printf("%v[%v/%v]: %v", t.name, t.runIndex+1, *runCount, fmt.Sprintf(format, v...))
|
2015-06-14 21:06:11 +03:00
|
|
|
} else {
|
2015-08-24 04:26:02 +03:00
|
|
|
log.Printf("%v: %v", t.name, fmt.Sprintf(format, v...))
|
2015-06-14 21:06:11 +03:00
|
|
|
}
|
2015-04-22 06:12:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
2015-06-12 10:32:24 +03:00
|
|
|
flag.Usage = func() {
|
|
|
|
os.Stderr.WriteString(usage)
|
|
|
|
os.Stderr.WriteString("\nOptions:\n")
|
|
|
|
flag.PrintDefaults()
|
|
|
|
}
|
2015-04-22 06:12:20 +03:00
|
|
|
flag.Parse()
|
|
|
|
|
2015-06-12 10:32:24 +03:00
|
|
|
startTime := time.Now()
|
|
|
|
|
2015-06-14 23:03:14 +03:00
|
|
|
// Make output directory.
|
|
|
|
outDir := path.Join("_test", fmt.Sprintf("%v.%v.%v", *flavor, startTime.Format("20060102-150405"), os.Getpid()))
|
|
|
|
if err := os.MkdirAll(outDir, os.FileMode(0755)); err != nil {
|
|
|
|
log.Fatalf("Can't create output directory: %v", err)
|
|
|
|
}
|
|
|
|
logFile, err := os.OpenFile(path.Join(outDir, "test.log"), os.O_RDWR|os.O_CREATE, 0644)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("Can't create log file: %v", err)
|
|
|
|
}
|
|
|
|
log.SetOutput(io.MultiWriter(os.Stderr, logFile))
|
|
|
|
log.Printf("Output directory: %v", outDir)
|
|
|
|
|
2015-04-22 06:12:20 +03:00
|
|
|
// Get test configs.
|
2015-08-23 15:43:21 +03:00
|
|
|
configData, err := ioutil.ReadFile(configFileName)
|
2015-04-22 06:12:20 +03:00
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("Can't read config file: %v", err)
|
|
|
|
}
|
|
|
|
var config Config
|
|
|
|
if err := json.Unmarshal(configData, &config); err != nil {
|
|
|
|
log.Fatalf("Can't parse config file: %v", err)
|
|
|
|
}
|
2015-08-23 12:56:01 +03:00
|
|
|
|
2015-08-23 15:43:21 +03:00
|
|
|
// Resharding.
|
|
|
|
if *reshard > 0 {
|
|
|
|
if err := reshardTests(&config, *reshard); err != nil {
|
2015-08-24 04:26:02 +03:00
|
|
|
log.Fatalf("resharding error: %v", err)
|
|
|
|
}
|
|
|
|
log.Printf("Saving updated config...")
|
|
|
|
data, err := json.MarshalIndent(config, "", "\t")
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("can't save new config: %v", err)
|
|
|
|
}
|
|
|
|
if err := ioutil.WriteFile(configFileName, data, 0644); err != nil {
|
|
|
|
log.Fatalf("can't write new config: %v", err)
|
2015-08-23 15:43:21 +03:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2015-08-23 12:56:01 +03:00
|
|
|
if *docker {
|
|
|
|
log.Printf("Bootstrap flavor: %v", *flavor)
|
|
|
|
|
|
|
|
// Re-pull image.
|
2015-08-24 06:21:05 +03:00
|
|
|
if *pull {
|
2015-08-23 12:56:01 +03:00
|
|
|
image := "vitess/bootstrap:" + *flavor
|
|
|
|
pullTime := time.Now()
|
|
|
|
log.Printf("Pulling %v...", image)
|
|
|
|
cmd := exec.Command("docker", "pull", image)
|
|
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
|
|
log.Fatalf("Can't pull image: %v\n%s", err, out)
|
|
|
|
}
|
|
|
|
log.Printf("Image pulled in %v", time.Since(pullTime))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if vtDataRoot == "" {
|
|
|
|
log.Fatalf("VTDATAROOT env var must be set in -docker=false mode. Make sure to source dev.env.")
|
2015-06-27 04:27:25 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-08-23 15:43:21 +03:00
|
|
|
// Pick the tests to run.
|
|
|
|
tests := selectedTests(&config)
|
2015-06-14 21:06:11 +03:00
|
|
|
|
|
|
|
// Duplicate tests.
|
|
|
|
if *runCount > 1 {
|
|
|
|
var dup []*Test
|
|
|
|
for _, t := range tests {
|
2015-06-12 10:41:52 +03:00
|
|
|
for i := 0; i < *runCount; i++ {
|
2015-06-14 21:06:11 +03:00
|
|
|
// Make a copy, since they're pointers.
|
|
|
|
test := *t
|
|
|
|
test.runIndex = i
|
|
|
|
dup = append(dup, &test)
|
2015-06-12 10:41:52 +03:00
|
|
|
}
|
2015-06-12 10:32:24 +03:00
|
|
|
}
|
2015-06-14 21:06:11 +03:00
|
|
|
tests = dup
|
2015-06-12 10:32:24 +03:00
|
|
|
}
|
|
|
|
|
2015-08-23 12:56:01 +03:00
|
|
|
vtTop := "."
|
|
|
|
tmpDir := ""
|
|
|
|
if *docker {
|
|
|
|
// Copy working repo to tmpDir.
|
|
|
|
// This doesn't work outside Docker since it messes up GOROOT.
|
|
|
|
tmpDir, err = ioutil.TempDir(os.TempDir(), "vt_")
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("Can't create temp dir in %v", os.TempDir())
|
|
|
|
}
|
|
|
|
log.Printf("Copying working repo to temp dir %v", tmpDir)
|
|
|
|
if out, err := exec.Command("cp", "-R", ".", tmpDir).CombinedOutput(); err != nil {
|
|
|
|
log.Fatalf("Can't copy working repo to temp dir %v: %v: %s", tmpDir, err, out)
|
|
|
|
}
|
|
|
|
// The temp copy needs permissive access so the Docker user can read it.
|
|
|
|
if out, err := exec.Command("chmod", "-R", "go=u", tmpDir).CombinedOutput(); err != nil {
|
|
|
|
log.Printf("Can't set permissions on temp dir %v: %v: %s", tmpDir, err, out)
|
|
|
|
}
|
|
|
|
vtTop = tmpDir
|
2015-05-08 01:31:29 +03:00
|
|
|
}
|
|
|
|
|
2015-04-22 06:12:20 +03:00
|
|
|
// Keep stats.
|
|
|
|
failed := 0
|
|
|
|
passed := 0
|
|
|
|
flaky := 0
|
|
|
|
|
2015-05-08 01:31:29 +03:00
|
|
|
// Listen for signals.
|
|
|
|
sigchan := make(chan os.Signal)
|
|
|
|
signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)
|
2015-04-22 06:12:20 +03:00
|
|
|
|
2015-05-08 01:31:29 +03:00
|
|
|
// Run tests.
|
|
|
|
stop := make(chan struct{}) // Close this to tell the loop to stop.
|
|
|
|
done := make(chan struct{}) // The loop closes this when it has stopped.
|
|
|
|
go func() {
|
|
|
|
defer func() {
|
|
|
|
signal.Stop(sigchan)
|
|
|
|
close(done)
|
|
|
|
}()
|
|
|
|
|
2015-06-12 10:32:24 +03:00
|
|
|
for _, test := range tests {
|
2015-05-08 01:31:29 +03:00
|
|
|
for try := 1; ; try++ {
|
|
|
|
select {
|
|
|
|
case <-stop:
|
|
|
|
test.logf("cancelled")
|
|
|
|
return
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
|
|
|
if try > *retryMax {
|
|
|
|
// Every try failed.
|
|
|
|
test.logf("retry limit exceeded")
|
|
|
|
failed++
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
test.logf("running (try %v/%v)...", try, *retryMax)
|
2015-08-23 12:56:01 +03:00
|
|
|
|
|
|
|
// Make a unique VTDATAROOT.
|
|
|
|
dataDir, err := ioutil.TempDir(vtDataRoot, "vt_")
|
|
|
|
if err != nil {
|
|
|
|
test.logf("Failed to create temporary subdir in VTDATAROOT: %v", vtDataRoot)
|
|
|
|
failed++
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
// Run the test.
|
2015-05-08 01:31:29 +03:00
|
|
|
start := time.Now()
|
2015-08-23 12:56:01 +03:00
|
|
|
output, err := test.run(vtTop, dataDir)
|
2015-08-23 15:43:21 +03:00
|
|
|
duration := time.Since(start)
|
2015-06-14 23:03:14 +03:00
|
|
|
|
2015-08-24 04:26:02 +03:00
|
|
|
// Save/print test output.
|
2015-06-14 23:03:14 +03:00
|
|
|
if err != nil || *logPass {
|
2015-08-24 04:26:02 +03:00
|
|
|
if *printLog {
|
|
|
|
test.logf("%s\n", output)
|
|
|
|
}
|
|
|
|
outFile := fmt.Sprintf("%v-%v.%v.log", test.name, test.runIndex+1, try)
|
2015-06-14 23:03:14 +03:00
|
|
|
test.logf("saving test output to %v", outFile)
|
|
|
|
if fileErr := ioutil.WriteFile(path.Join(outDir, outFile), output, os.FileMode(0644)); fileErr != nil {
|
|
|
|
test.logf("WriteFile error: %v", fileErr)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-08-23 12:56:01 +03:00
|
|
|
// Clean up the unique VTDATAROOT.
|
2015-08-24 04:26:02 +03:00
|
|
|
if !*keepData {
|
|
|
|
if err := os.RemoveAll(dataDir); err != nil {
|
|
|
|
test.logf("WARNING: can't remove temporary VTDATAROOT: ", err)
|
|
|
|
}
|
2015-08-23 12:56:01 +03:00
|
|
|
}
|
|
|
|
|
2015-06-14 23:03:14 +03:00
|
|
|
if err != nil {
|
2015-05-08 01:31:29 +03:00
|
|
|
// This try failed.
|
2015-08-23 15:43:21 +03:00
|
|
|
test.logf("FAILED (try %v/%v) in %v: %v", try, *retryMax, duration, err)
|
2015-08-24 04:26:02 +03:00
|
|
|
testFailed(test.name)
|
2015-05-08 01:31:29 +03:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2015-08-24 04:26:02 +03:00
|
|
|
testPassed(test.name, duration)
|
2015-08-23 15:43:21 +03:00
|
|
|
|
2015-05-08 01:31:29 +03:00
|
|
|
if try == 1 {
|
|
|
|
// Passed on the first try.
|
2015-08-23 15:43:21 +03:00
|
|
|
test.logf("PASSED in %v", duration)
|
2015-05-08 01:31:29 +03:00
|
|
|
passed++
|
|
|
|
} else {
|
|
|
|
// Passed, but not on the first try.
|
2015-08-23 15:43:21 +03:00
|
|
|
test.logf("FLAKY (1/%v passed in %v)", try, duration)
|
2015-05-08 01:31:29 +03:00
|
|
|
flaky++
|
2015-08-24 04:26:02 +03:00
|
|
|
testFlaked(test.name, try)
|
2015-05-08 01:31:29 +03:00
|
|
|
}
|
|
|
|
break
|
2015-04-22 06:12:20 +03:00
|
|
|
}
|
2015-05-08 01:31:29 +03:00
|
|
|
}
|
|
|
|
}()
|
2015-04-22 06:12:20 +03:00
|
|
|
|
2015-05-08 01:31:29 +03:00
|
|
|
// Stop the loop and kill child processes if we get a signal.
|
|
|
|
select {
|
|
|
|
case <-sigchan:
|
|
|
|
log.Printf("received signal, quitting")
|
|
|
|
// Stop the test loop and wait for it to quit.
|
|
|
|
close(stop)
|
|
|
|
<-done
|
|
|
|
// Terminate all existing tests.
|
2015-06-14 21:06:11 +03:00
|
|
|
for _, t := range tests {
|
2015-05-08 01:31:29 +03:00
|
|
|
t.stop()
|
2015-04-22 06:12:20 +03:00
|
|
|
}
|
2015-05-08 01:31:29 +03:00
|
|
|
case <-done:
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clean up temp dir.
|
2015-08-23 12:56:01 +03:00
|
|
|
if tmpDir != "" {
|
|
|
|
log.Printf("Removing temp dir %v", tmpDir)
|
|
|
|
if err := os.RemoveAll(tmpDir); err != nil {
|
|
|
|
log.Printf("Failed to remove temp dir: %v", err)
|
|
|
|
}
|
2015-04-22 06:12:20 +03:00
|
|
|
}
|
|
|
|
|
2015-08-24 04:26:02 +03:00
|
|
|
// Print summary.
|
|
|
|
log.Printf(strings.Repeat("=", 50))
|
|
|
|
for _, t := range tests {
|
|
|
|
switch {
|
|
|
|
case t.pass > 0 && t.fail == 0:
|
|
|
|
log.Printf("%-32s\tPASS", t.name)
|
|
|
|
case t.pass > 0 && t.fail > 0:
|
|
|
|
log.Printf("%-32s\tFLAKY (%v/%v failed)", t.name, t.fail, t.pass+t.fail)
|
|
|
|
case t.pass == 0 && t.fail > 0:
|
|
|
|
log.Printf("%-32s\tFAIL (%v tries)", t.name, t.fail)
|
|
|
|
case t.pass == 0 && t.fail == 0:
|
|
|
|
log.Printf("%-32s\tSKIPPED", t.name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
log.Printf(strings.Repeat("=", 50))
|
2015-06-12 10:32:24 +03:00
|
|
|
skipped := len(tests) - passed - flaky - failed
|
2015-05-08 01:31:29 +03:00
|
|
|
log.Printf("%v PASSED, %v FLAKY, %v FAILED, %v SKIPPED", passed, flaky, failed, skipped)
|
2015-06-12 10:32:24 +03:00
|
|
|
log.Printf("Total time: %v", time.Since(startTime))
|
2015-04-23 01:27:35 +03:00
|
|
|
|
2015-05-08 01:31:29 +03:00
|
|
|
if failed > 0 || skipped > 0 {
|
2015-04-23 01:27:35 +03:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
2015-04-22 06:12:20 +03:00
|
|
|
}
|
2015-08-23 12:56:01 +03:00
|
|
|
|
|
|
|
func updateEnv(orig []string, updates map[string]string) []string {
|
|
|
|
var env []string
|
|
|
|
for _, v := range orig {
|
|
|
|
parts := strings.SplitN(v, "=", 2)
|
|
|
|
if _, ok := updates[parts[0]]; !ok {
|
|
|
|
env = append(env, v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for k, v := range updates {
|
|
|
|
env = append(env, k+"="+v)
|
|
|
|
}
|
|
|
|
return env
|
|
|
|
}
|
2015-08-23 15:43:21 +03:00
|
|
|
|
|
|
|
type Stats struct {
|
|
|
|
TestStats map[string]TestStats
|
|
|
|
}
|
|
|
|
|
|
|
|
type TestStats struct {
|
|
|
|
Pass, Fail, Flake int
|
|
|
|
PassTime time.Duration
|
|
|
|
|
|
|
|
name string
|
|
|
|
}
|
|
|
|
|
2015-08-24 04:26:02 +03:00
|
|
|
func sendStats(values url.Values) {
|
|
|
|
if *remoteStats != "" {
|
|
|
|
log.Printf("Sending remote stats to %v", *remoteStats)
|
|
|
|
if _, err := http.PostForm(*remoteStats, values); err != nil {
|
|
|
|
log.Printf("Can't send remote stats: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-08-23 15:43:21 +03:00
|
|
|
func testPassed(name string, passTime time.Duration) {
|
2015-08-24 04:26:02 +03:00
|
|
|
sendStats(url.Values{
|
|
|
|
"test": {name},
|
|
|
|
"result": {"pass"},
|
|
|
|
"duration": {passTime.String()},
|
|
|
|
})
|
2015-08-23 15:43:21 +03:00
|
|
|
updateTestStats(name, func(ts *TestStats) {
|
|
|
|
totalTime := int64(ts.PassTime)*int64(ts.Pass) + int64(passTime)
|
|
|
|
ts.Pass++
|
|
|
|
ts.PassTime = time.Duration(totalTime / int64(ts.Pass))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func testFailed(name string) {
|
2015-08-24 04:26:02 +03:00
|
|
|
sendStats(url.Values{
|
|
|
|
"test": {name},
|
|
|
|
"result": {"fail"},
|
|
|
|
})
|
2015-08-23 15:43:21 +03:00
|
|
|
updateTestStats(name, func(ts *TestStats) {
|
|
|
|
ts.Fail++
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func testFlaked(name string, try int) {
|
2015-08-24 04:26:02 +03:00
|
|
|
sendStats(url.Values{
|
|
|
|
"test": {name},
|
|
|
|
"result": {"flake"},
|
|
|
|
"try": {strconv.FormatInt(int64(try), 10)},
|
|
|
|
})
|
2015-08-23 15:43:21 +03:00
|
|
|
updateTestStats(name, func(ts *TestStats) {
|
|
|
|
ts.Flake += try - 1
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func updateTestStats(name string, update func(*TestStats)) {
|
|
|
|
var stats Stats
|
|
|
|
|
|
|
|
data, err := ioutil.ReadFile(statsFileName)
|
|
|
|
if err != nil {
|
|
|
|
log.Print("Can't read stats file, starting new one.")
|
|
|
|
} else {
|
|
|
|
if err := json.Unmarshal(data, &stats); err != nil {
|
|
|
|
log.Printf("Can't parse stats file: %v", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if stats.TestStats == nil {
|
|
|
|
stats.TestStats = make(map[string]TestStats)
|
|
|
|
}
|
|
|
|
ts := stats.TestStats[name]
|
|
|
|
update(&ts)
|
|
|
|
stats.TestStats[name] = ts
|
|
|
|
|
|
|
|
data, err = json.MarshalIndent(stats, "", "\t")
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("Can't encode stats file: %v", err)
|
|
|
|
return
|
|
|
|
}
|
2015-08-24 04:26:02 +03:00
|
|
|
if err := ioutil.WriteFile(statsFileName, data, 0644); err != nil {
|
|
|
|
log.Printf("Can't write stats file: %v", err)
|
|
|
|
}
|
2015-08-23 15:43:21 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func reshardTests(config *Config, numShards int) error {
|
|
|
|
var stats Stats
|
|
|
|
|
2015-08-24 04:26:02 +03:00
|
|
|
var data []byte
|
|
|
|
if *remoteStats != "" {
|
|
|
|
log.Printf("Using remote stats for resharding: %v", *remoteStats)
|
|
|
|
resp, err := http.Get(*remoteStats)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if data, err = ioutil.ReadAll(resp.Body); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
var err error
|
|
|
|
data, err = ioutil.ReadFile(statsFileName)
|
|
|
|
if err != nil {
|
|
|
|
return errors.New("can't read stats file")
|
|
|
|
}
|
2015-08-23 15:43:21 +03:00
|
|
|
}
|
2015-08-24 04:26:02 +03:00
|
|
|
|
2015-08-23 15:43:21 +03:00
|
|
|
if err := json.Unmarshal(data, &stats); err != nil {
|
|
|
|
return fmt.Errorf("can't parse stats file: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sort tests by PassTime.
|
|
|
|
var tests []TestStats
|
|
|
|
var totalTime int64
|
|
|
|
for name, test := range stats.TestStats {
|
|
|
|
test.name = name
|
|
|
|
tests = append(tests, test)
|
|
|
|
totalTime += int64(test.PassTime)
|
|
|
|
}
|
|
|
|
sort.Sort(ByPassTime(tests))
|
|
|
|
|
|
|
|
// Group into shards.
|
|
|
|
max := totalTime / int64(numShards)
|
|
|
|
shards := make([][]TestStats, numShards)
|
|
|
|
sums := make([]int64, numShards)
|
2015-08-24 05:51:42 +03:00
|
|
|
// First pass: greedy approximation.
|
|
|
|
for len(tests) > 0 {
|
2015-08-23 15:43:21 +03:00
|
|
|
v := int64(tests[0].PassTime)
|
2015-08-24 05:51:42 +03:00
|
|
|
|
|
|
|
found := false
|
|
|
|
for n := range shards {
|
|
|
|
if sums[n]+v < max {
|
|
|
|
shards[n] = append(shards[n], tests[0])
|
|
|
|
sums[n] += v
|
|
|
|
tests = tests[1:]
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !found {
|
|
|
|
break
|
2015-08-23 15:43:21 +03:00
|
|
|
}
|
|
|
|
}
|
2015-08-24 05:51:42 +03:00
|
|
|
// Second pass: distribute the remainder.
|
|
|
|
for len(tests) > 0 {
|
|
|
|
nmin := 0
|
|
|
|
min := sums[0]
|
|
|
|
|
|
|
|
for n := range sums {
|
|
|
|
if sums[n] < min {
|
|
|
|
nmin = n
|
|
|
|
min = sums[n]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
shards[nmin] = append(shards[nmin], tests[0])
|
|
|
|
sums[nmin] += int64(tests[0].PassTime)
|
|
|
|
tests = tests[1:]
|
|
|
|
}
|
2015-08-23 15:43:21 +03:00
|
|
|
|
2015-08-24 04:26:02 +03:00
|
|
|
// Update config and print results.
|
2015-08-23 15:43:21 +03:00
|
|
|
for i, tests := range shards {
|
|
|
|
for _, t := range tests {
|
2015-08-24 04:26:02 +03:00
|
|
|
config.Tests[t.name].Shard = i
|
2015-08-23 15:43:21 +03:00
|
|
|
log.Printf("% 32v:\t%v\n", t.name, t.PassTime)
|
|
|
|
}
|
|
|
|
log.Printf("Shard %v total: %v\n", i, time.Duration(sums[i]))
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type ByPassTime []TestStats
|
|
|
|
|
|
|
|
func (a ByPassTime) Len() int { return len(a) }
|
|
|
|
func (a ByPassTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
|
|
func (a ByPassTime) Less(i, j int) bool { return a[i].PassTime > a[j].PassTime }
|
|
|
|
|
2015-08-24 04:26:02 +03:00
|
|
|
func getTestsSorted(names []string, testMap map[string]*Test) []*Test {
|
|
|
|
sort.Strings(names)
|
|
|
|
var tests []*Test
|
|
|
|
for _, name := range names {
|
|
|
|
t := testMap[name]
|
|
|
|
t.name = name
|
|
|
|
tests = append(tests, t)
|
|
|
|
}
|
|
|
|
return tests
|
|
|
|
}
|
|
|
|
|
2015-08-23 15:43:21 +03:00
|
|
|
func selectedTests(config *Config) []*Test {
|
|
|
|
var tests []*Test
|
|
|
|
if *shard >= 0 {
|
|
|
|
// Run the tests in a given shard.
|
|
|
|
// This can be combined with positional args.
|
2015-08-24 04:26:02 +03:00
|
|
|
var names []string
|
|
|
|
for name, t := range config.Tests {
|
2015-08-23 15:43:21 +03:00
|
|
|
if t.Shard == *shard {
|
2015-08-24 04:26:02 +03:00
|
|
|
t.name = name
|
|
|
|
names = append(names, name)
|
2015-08-23 15:43:21 +03:00
|
|
|
}
|
|
|
|
}
|
2015-08-24 04:26:02 +03:00
|
|
|
tests = getTestsSorted(names, config.Tests)
|
2015-08-23 15:43:21 +03:00
|
|
|
}
|
|
|
|
if flag.NArg() > 0 {
|
|
|
|
// Positional args for manual selection.
|
|
|
|
for _, name := range flag.Args() {
|
|
|
|
t, ok := config.Tests[name]
|
|
|
|
if !ok {
|
|
|
|
log.Fatalf("Unknown test: %v", name)
|
|
|
|
}
|
2015-08-24 04:26:02 +03:00
|
|
|
t.name = name
|
2015-08-23 15:43:21 +03:00
|
|
|
tests = append(tests, t)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if flag.NArg() == 0 && *shard < 0 {
|
|
|
|
// Run all tests.
|
|
|
|
var names []string
|
|
|
|
for name := range config.Tests {
|
|
|
|
if !config.Tests[name].Manual {
|
|
|
|
names = append(names, name)
|
|
|
|
}
|
|
|
|
}
|
2015-08-24 04:26:02 +03:00
|
|
|
tests = getTestsSorted(names, config.Tests)
|
2015-08-23 15:43:21 +03:00
|
|
|
}
|
|
|
|
return tests
|
|
|
|
}
|