├── README.md ├── .pfnci ├── config.pbtxt └── script.sh ├── .gitignore ├── pkg ├── reporter │ ├── reporter.go │ └── sheets_reporter.go ├── xpytest │ ├── hint.go │ └── xpytest.go ├── pytest │ ├── execute_test.go │ ├── execute.go │ ├── pytest.go │ └── pytest_test.go └── resourcebuckets │ ├── resource_buckets_test.go │ └── resource_buckets.go ├── go.mod ├── Makefile ├── cmd ├── debug_sheets_reporter │ └── main.go └── xpytest │ └── main.go ├── proto ├── test_case.proto └── test_case.pb.go └── go.sum /README.md: -------------------------------------------------------------------------------- 1 | # xpytest -------------------------------------------------------------------------------- /.pfnci/config.pbtxt: -------------------------------------------------------------------------------- 1 | configs { 2 | key: "xpytest.unit" 3 | value { 4 | requirement { 5 | cpu: 2 6 | memory: 6 7 | } 8 | command: "bash .pfnci/script.sh" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Generated files 15 | bin/ 16 | generated/ 17 | -------------------------------------------------------------------------------- /pkg/reporter/reporter.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import "context" 4 | 5 | // Reporter reports lines. 6 | type Reporter interface { 7 | // Log appends a line to the report buffer. Reporter will not write lines 8 | // until Reporter.Flush is called. 9 | Log(context.Context, string) 10 | 11 | // Flush flushes the report buffer. 12 | Flush(context.Context) error 13 | } 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/chainer/xpytest 2 | 3 | require ( 4 | github.com/bmatcuk/doublestar v1.1.1 5 | github.com/golang/mock v1.2.0 // indirect 6 | github.com/golang/protobuf v1.2.0 7 | golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a 8 | google.golang.org/api v0.3.0 9 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 10 | gopkg.in/yaml.v2 v2.2.2 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /.pfnci/script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -uex 4 | 5 | gsutil cp \ 6 | gs://ro-pfn-public-ci/package/go/go1.12.linux-amd64.tar.gz \ 7 | go.tar.gz 8 | tar -xf go.tar.gz 9 | rm -rf /usr/local/go || true 10 | mv -f go /usr/local/ 11 | 12 | apt-get update -qq 13 | apt-get install -qqy libprotobuf-dev libprotoc-dev protobuf-compiler 14 | go get -u github.com/golang/protobuf/{proto,protoc-gen-go} 15 | 16 | make build 17 | make test 18 | -------------------------------------------------------------------------------- /pkg/xpytest/hint.go: -------------------------------------------------------------------------------- 1 | package xpytest 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | 7 | "github.com/golang/protobuf/proto" 8 | 9 | xpytest_proto "github.com/chainer/xpytest/proto" 10 | ) 11 | 12 | // LoadHintFile loads hint information from a hint file. 13 | func LoadHintFile(file string) (*xpytest_proto.HintFile, error) { 14 | buf, err := ioutil.ReadFile(file) 15 | if err != nil { 16 | return nil, fmt.Errorf("failed to read hint file: %s", err) 17 | } 18 | h := &xpytest_proto.HintFile{} 19 | if err := proto.UnmarshalText(string(buf), h); err != nil { 20 | return nil, fmt.Errorf("failed to parse hint file: %s", err) 21 | } 22 | return h, nil 23 | } 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build 2 | .PHONY: all 3 | 4 | build: bin/xpytest bin/debug_sheets_reporter 5 | .PHONY: build 6 | 7 | test: 8 | go test ./... 9 | .PHONY: test 10 | 11 | proto: generated 12 | protoc --proto_path=generated/proto --go_out=generated/proto \ 13 | generated/proto/xpytest/proto/*.proto 14 | .PHONY: proto 15 | 16 | clean: 17 | rm -rf generated 18 | .PHONY: clean 19 | 20 | generated: generated/proto 21 | .PHONY: generated 22 | 23 | generated/proto: 24 | mkdir -p generated/proto/xpytest/ 25 | ln -s "../../../proto" generated/proto/xpytest/proto 26 | 27 | bin: 28 | mkdir -p bin 29 | 30 | bin/%-linux: bin proto 31 | GOOS=linux GOARCH=amd64 go build -o bin/$*-linux ./cmd/$* 32 | gzip -f -k bin/$*-linux 33 | .PRECIOUS: bin/%-linux 34 | 35 | bin/%-darwin: bin proto 36 | GOOS=darwin GOARCH=amd64 go build -o bin/$*-darwin ./cmd/$* 37 | gzip -f -k bin/$*-darwin 38 | .PRECIOUS: bin/%-darwin 39 | 40 | bin/%: bin/%-linux bin/%-darwin 41 | ln -f -s $*-$$(uname | tr '[A-Z]' '[a-z]') bin/$* 42 | -------------------------------------------------------------------------------- /cmd/debug_sheets_reporter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/chainer/xpytest/pkg/reporter" 10 | ) 11 | 12 | var credential = flag.String( 13 | "credential", "", "JSON credential file for Google") 14 | var spreadsheetID = flag.String("spreadsheet_id", "", "spreadsheet ID to edit") 15 | 16 | func main() { 17 | ctx := context.Background() 18 | r, err := func() (reporter.Reporter, error) { 19 | if *credential != "" { 20 | return reporter.NewSheetsReporterWithCredential( 21 | ctx, *credential, *spreadsheetID) 22 | } 23 | return reporter.NewSheetsReporter(ctx, *spreadsheetID) 24 | }() 25 | if err != nil { 26 | panic(fmt.Sprintf("failed to create sheets reporter: %s", err)) 27 | } 28 | 29 | for i := 0; i < 10; i++ { 30 | r.Log(ctx, fmt.Sprintf("%s] test log %d", time.Now().String(), i)) 31 | } 32 | 33 | if err := r.Flush(ctx); err != nil { 34 | panic(fmt.Sprintf("failed to flush sheets reporter: %s", err)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /proto/test_case.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package xpytest.proto; 4 | 5 | message TestQuery { 6 | // File path to a python test. 7 | string file = 1; 8 | 9 | // Priority. A test with a higher priority should precede. This should be 10 | // useful to avoid slow tests from wasting time because of starting late. 11 | int32 priority = 2; 12 | 13 | // Deadline in seconds. 14 | float deadline = 3; 15 | 16 | // # of processes in pytest-xdist. 17 | int32 xdist = 4; 18 | 19 | // # of retries. 20 | int32 retry = 5; 21 | } 22 | 23 | message TestResult { 24 | enum Status { 25 | UNKNOWN = 0; 26 | SUCCESS = 1; 27 | INTERNAL = 2; 28 | FAILED = 3; 29 | TIMEOUT = 4; 30 | FLAKY = 5; 31 | } 32 | Status status = 1; 33 | 34 | // Test name (e.g., "tests/foo_tests/test_bar.py"). 35 | string name = 2; 36 | 37 | // Standard output. 38 | string stdout = 3; 39 | 40 | // Standard error. 41 | string stderr = 4; 42 | 43 | // Duration that the test took. 44 | float time = 5; 45 | } 46 | 47 | message HintFile { 48 | message SlowTest { 49 | // File name of a slow test (e.g.,"test_foo.py", "bar/test_foo.py"). Parent 50 | // directories can be omitted (i.e., "test_foo.py" can matches 51 | // "bar/test_foo.py"). 52 | string name = 1; 53 | 54 | // Deadline in seconds. 55 | float deadline = 2; 56 | 57 | // # of processes in pytest-xdist. 58 | int32 xdist = 3; 59 | 60 | // # of retries. For flaky tests. 61 | int32 retry = 4; 62 | } 63 | repeated SlowTest slow_tests = 1; 64 | } 65 | -------------------------------------------------------------------------------- /pkg/pytest/execute_test.go: -------------------------------------------------------------------------------- 1 | package pytest_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/chainer/xpytest/pkg/pytest" 9 | xpytest_proto "github.com/chainer/xpytest/proto" 10 | ) 11 | 12 | func TestExecute(t *testing.T) { 13 | ctx := context.Background() 14 | r, err := pytest.Execute(ctx, []string{"true"}, time.Second, nil) 15 | if err != nil { 16 | t.Fatalf("failed to execute: %s", err) 17 | } 18 | if r.Status != xpytest_proto.TestResult_SUCCESS { 19 | t.Fatalf("unexpected status: %s", r.Status) 20 | } 21 | } 22 | 23 | func TestExecuteWithFailure(t *testing.T) { 24 | ctx := context.Background() 25 | r, err := pytest.Execute(ctx, []string{"false"}, time.Second, nil) 26 | if err != nil { 27 | t.Fatalf("failed to execute: %s", err) 28 | } 29 | if r.Status != xpytest_proto.TestResult_FAILED { 30 | t.Fatalf("unexpected status: %s", r.Status) 31 | } 32 | } 33 | 34 | func TestExecuteWithTimeout(t *testing.T) { 35 | ctx := context.Background() 36 | r, err := pytest.Execute( 37 | ctx, []string{"sleep", "10"}, 100*time.Millisecond, nil) 38 | if err != nil { 39 | t.Fatalf("failed to execute: %s", err) 40 | } 41 | if r.Status != xpytest_proto.TestResult_TIMEOUT { 42 | t.Fatalf("unexpected status: %s", r.Status) 43 | } 44 | } 45 | 46 | func TestExecuteWithEnvironmentVariables(t *testing.T) { 47 | ctx := context.Background() 48 | r, err := pytest.Execute( 49 | ctx, []string{"bash", "-c", "echo $HOGE"}, 50 | time.Second, []string{"HOGE=PIYO"}) 51 | if err != nil { 52 | t.Fatalf("failed to execute: %s", err) 53 | } 54 | if r.Status != xpytest_proto.TestResult_SUCCESS { 55 | t.Fatalf("unexpected status: %s", r.Status) 56 | } 57 | if r.Stdout != "PIYO\n" { 58 | t.Fatalf("unexpected output: %s", r.Stdout) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pkg/resourcebuckets/resource_buckets_test.go: -------------------------------------------------------------------------------- 1 | package resourcebuckets_test 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | "testing" 7 | "time" 8 | 9 | "github.com/chainer/xpytest/pkg/resourcebuckets" 10 | ) 11 | 12 | func TestResourceBuckets(t *testing.T) { 13 | rb := resourcebuckets.NewResourceBuckets(2, 3) 14 | buckets := make([]int32, 2) 15 | wg := sync.WaitGroup{} 16 | for i := 0; i < 50; i++ { 17 | wg.Add(1) 18 | go func() { 19 | defer wg.Done() 20 | ru := rb.Acquire(1) 21 | v := atomic.AddInt32(&buckets[ru.Index], 1) 22 | if v > 3 { 23 | t.Errorf("exceeding capacity: %d", v) 24 | } 25 | time.Sleep(50 * time.Millisecond) 26 | atomic.AddInt32(&buckets[ru.Index], -1) 27 | rb.Release(ru) 28 | }() 29 | } 30 | wg.Wait() 31 | } 32 | 33 | func TestResourceBucketsIndexes(t *testing.T) { 34 | rb := resourcebuckets.NewResourceBuckets(4, 10) 35 | type TestCase struct { 36 | Capacity int 37 | Index int 38 | } 39 | tcs := []TestCase{ 40 | TestCase{Capacity: 1, Index: 0}, 41 | TestCase{Capacity: 2, Index: 1}, 42 | TestCase{Capacity: 3, Index: 2}, 43 | TestCase{Capacity: 4, Index: 3}, 44 | TestCase{Capacity: 5, Index: 0}, 45 | TestCase{Capacity: 6, Index: 1}, 46 | TestCase{Capacity: 7, Index: 2}, 47 | TestCase{Capacity: 2, Index: 3}, 48 | TestCase{Capacity: 2, Index: 0}, 49 | TestCase{Capacity: 2, Index: 3}, 50 | TestCase{Capacity: 1, Index: 0}, 51 | TestCase{Capacity: 1, Index: 1}, 52 | TestCase{Capacity: 1, Index: 3}, 53 | TestCase{Capacity: 1, Index: 0}, 54 | TestCase{Capacity: 1, Index: 1}, 55 | TestCase{Capacity: 1, Index: 3}, 56 | } 57 | for i, tc := range tcs { 58 | if idx := rb.Acquire(tc.Capacity).Index; idx != tc.Index { 59 | t.Errorf("[case #%d] unexpected index: actual=%d, expected=%d", 60 | i, idx, tc.Index) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /pkg/resourcebuckets/resource_buckets.go: -------------------------------------------------------------------------------- 1 | package resourcebuckets 2 | 3 | import "sync" 4 | 5 | // ResourceBuckets manages resource capacities. This enables worker threads to 6 | // use limited resources wihtout exceeding their capacities. 7 | type ResourceBuckets struct { 8 | buckets []int 9 | nextBucket int 10 | cond *sync.Cond 11 | } 12 | 13 | // ResourceUsage represents a usage of resource. 14 | type ResourceUsage struct { 15 | Index int 16 | Usage int 17 | } 18 | 19 | // NewResourceBuckets createa a new ResourceBuckets with buckets, each of which 20 | // has the same size of capacity. 21 | func NewResourceBuckets(size, capacityPerBucket int) *ResourceBuckets { 22 | buckets := make([]int, size) 23 | for i := range buckets { 24 | buckets[i] = capacityPerBucket 25 | } 26 | return &ResourceBuckets{ 27 | buckets: buckets, 28 | cond: sync.NewCond(&sync.Mutex{}), 29 | } 30 | } 31 | 32 | // Acquire acquires the given size of usage. This function blocks until the 33 | // size of usage can be acquired from the resources. 34 | func (rb *ResourceBuckets) Acquire(usage int) *ResourceUsage { 35 | rb.cond.L.Lock() 36 | defer rb.cond.L.Unlock() 37 | for rb.buckets[rb.nextBucket] < usage { 38 | rb.cond.Wait() 39 | } 40 | ru := &ResourceUsage{Index: rb.nextBucket, Usage: usage} 41 | rb.buckets[ru.Index] -= ru.Usage 42 | rb.setNextBucket() 43 | return ru 44 | } 45 | 46 | // Release releases the given resource usage. 47 | func (rb *ResourceBuckets) Release(ru *ResourceUsage) { 48 | rb.cond.L.Lock() 49 | defer rb.cond.L.Unlock() 50 | rb.buckets[ru.Index] += ru.Usage 51 | ru.Usage = 0 52 | rb.setNextBucket() 53 | rb.cond.Broadcast() 54 | } 55 | 56 | // setNextBucket recalculates rb.nextBucket. 57 | // CAVEAT: rb.cond.L must be locked when this is called. 58 | func (rb *ResourceBuckets) setNextBucket() { 59 | nextBucket := 0 60 | for i, b := range rb.buckets { 61 | if rb.buckets[nextBucket] < b { 62 | nextBucket = i 63 | } 64 | } 65 | rb.nextBucket = nextBucket 66 | } 67 | -------------------------------------------------------------------------------- /pkg/reporter/sheets_reporter.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | 9 | "golang.org/x/oauth2" 10 | "golang.org/x/oauth2/google" 11 | "google.golang.org/api/sheets/v4" 12 | ) 13 | 14 | type sheetsReporter struct { 15 | client *http.Client 16 | spreadsheetID string 17 | values [][]interface{} 18 | } 19 | 20 | // NewSheetsReporterWithCredential creates a reporter to store logs to a 21 | // spreadsheet with a JSON credential. 22 | func NewSheetsReporterWithCredential( 23 | ctx context.Context, cred, spreadsheetID string, 24 | ) (Reporter, error) { 25 | if buf, err := ioutil.ReadFile(cred); err != nil { 26 | return nil, err 27 | } else if conf, err := google.JWTConfigFromJSON( 28 | buf, sheets.SpreadsheetsScope); err != nil { 29 | return nil, err 30 | } else { 31 | return &sheetsReporter{ 32 | client: conf.Client(oauth2.NoContext), 33 | spreadsheetID: spreadsheetID, 34 | }, nil 35 | } 36 | } 37 | 38 | // NewSheetsReporter creates a reporter to store logs to a spreadsheet with 39 | // a default credential. 40 | func NewSheetsReporter( 41 | ctx context.Context, spreadsheetID string, 42 | ) (Reporter, error) { 43 | client, err := google.DefaultClient(ctx, sheets.SpreadsheetsScope) 44 | if err != nil { 45 | return nil, err 46 | } 47 | return &sheetsReporter{ 48 | client: client, 49 | spreadsheetID: spreadsheetID, 50 | }, nil 51 | } 52 | 53 | func (r *sheetsReporter) Log(ctx context.Context, msg string) { 54 | if r.values == nil { 55 | r.values = [][]interface{}{} 56 | } 57 | r.values = append(r.values, []interface{}{msg}) 58 | } 59 | 60 | func (r *sheetsReporter) Flush(ctx context.Context) error { 61 | svc, err := sheets.New(r.client) 62 | if err != nil { 63 | return fmt.Errorf("failed to get sheets client: %s", err) 64 | } 65 | _, err = svc.Spreadsheets.Values.Append( 66 | r.spreadsheetID, "A1", &sheets.ValueRange{Values: r.values}, 67 | ).ValueInputOption("RAW").Do() 68 | if err != nil { 69 | return fmt.Errorf("failed to append rows: %s", err) 70 | } 71 | r.values = nil 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /cmd/xpytest/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "time" 9 | 10 | xpytest_proto "github.com/chainer/xpytest/proto" 11 | 12 | "github.com/chainer/xpytest/pkg/pytest" 13 | "github.com/chainer/xpytest/pkg/reporter" 14 | "github.com/chainer/xpytest/pkg/xpytest" 15 | ) 16 | 17 | var python = flag.String("python", "python3", "python command") 18 | var markerExpression = flag.String("m", "not slow", "pytest marker expression") 19 | var retry = flag.Int("retry", 2, "number of retries") 20 | var credential = flag.String( 21 | "credential", "", "JSON credential file for Google") 22 | var spreadsheetID = flag.String("spreadsheet_id", "", "spreadsheet ID to edit") 23 | var hint = flag.String("hint", "", "hint file") 24 | var bucket = flag.Int("bucket", 1, "number of buckets") 25 | var thread = flag.Int("thread", 0, "number of threads per bucket") 26 | var reportName = flag.String("report_name", "", "name for reporter") 27 | 28 | func main() { 29 | flag.Parse() 30 | ctx := context.Background() 31 | 32 | base := pytest.NewPytest(*python) 33 | base.MarkerExpression = *markerExpression 34 | base.Retry = *retry 35 | base.Deadline = time.Minute 36 | xt := xpytest.NewXpytest(base) 37 | 38 | r, err := func() (reporter.Reporter, error) { 39 | if *spreadsheetID == "" { 40 | return nil, nil 41 | } 42 | if *credential != "" { 43 | return reporter.NewSheetsReporterWithCredential( 44 | ctx, *credential, *spreadsheetID) 45 | } 46 | return reporter.NewSheetsReporter(ctx, *spreadsheetID) 47 | }() 48 | if err != nil { 49 | panic(fmt.Sprintf("failed to initialize reporter: %s", err)) 50 | } 51 | if r != nil { 52 | if *reportName != "" { 53 | r.Log(ctx, *reportName) 54 | } else { 55 | r.Log(ctx, fmt.Sprintf("Time: %s", time.Now())) 56 | } 57 | } 58 | 59 | for _, arg := range flag.Args() { 60 | if err := xt.AddTestsWithFilePattern(arg); err != nil { 61 | panic(fmt.Sprintf("failed to add tests: %s", err)) 62 | } 63 | } 64 | 65 | if *hint != "" { 66 | if h, err := xpytest.LoadHintFile(*hint); err != nil { 67 | panic(fmt.Sprintf( 68 | "failed to read hint information from file: %s: %s", 69 | *hint, err)) 70 | } else if err := xt.ApplyHint(h); err != nil { 71 | panic(fmt.Sprintf("failed to apply hint: %s", err)) 72 | } 73 | } 74 | 75 | if err := xt.Execute(ctx, *bucket, *thread, r); err != nil { 76 | panic(fmt.Sprintf("failed to execute: %s", err)) 77 | } 78 | 79 | if r != nil { 80 | fmt.Fprintf(os.Stderr, "[DEBUG] flushing reporter...\n") 81 | if err := r.Flush(ctx); err != nil { 82 | fmt.Fprintf(os.Stderr, 83 | "[ERROR] failed to flush reporter: %s\n", err) 84 | } 85 | } 86 | 87 | fmt.Printf("Overall status: %s\n", xt.Status) 88 | if xt.Status != xpytest_proto.TestResult_SUCCESS && 89 | xt.Status != xpytest_proto.TestResult_FLAKY { 90 | os.Exit(1) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /pkg/pytest/execute.go: -------------------------------------------------------------------------------- 1 | package pytest 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/golang/protobuf/proto" 15 | 16 | xpytest_proto "github.com/chainer/xpytest/proto" 17 | ) 18 | 19 | // Execute executes a command. 20 | func Execute( 21 | ctx context.Context, args []string, deadline time.Duration, env []string, 22 | ) (*xpytest_proto.TestResult, error) { 23 | startTime := time.Now() 24 | 25 | type executeResult struct { 26 | testResult *xpytest_proto.TestResult 27 | err error 28 | } 29 | resultChan := make(chan *executeResult, 2) 30 | done := make(chan struct{}, 1) 31 | 32 | temporaryResult := &xpytest_proto.TestResult{} 33 | go func() { 34 | err := executeInternal( 35 | ctx, args, deadline, env, temporaryResult) 36 | resultChan <- &executeResult{testResult: temporaryResult, err: err} 37 | close(done) 38 | }() 39 | 40 | go func() { 41 | select { 42 | case <-done: 43 | case <-time.After(deadline + 5*time.Second): 44 | r := proto.Clone(temporaryResult).(*xpytest_proto.TestResult) 45 | r.Status = xpytest_proto.TestResult_TIMEOUT 46 | resultChan <- &executeResult{testResult: r, err: nil} 47 | fmt.Fprintf(os.Stderr, "[ERROR] command is hung up: %s\n", 48 | strings.Join(args, " ")) 49 | } 50 | }() 51 | 52 | result := <-resultChan 53 | if result.err != nil { 54 | return nil, result.err 55 | } 56 | 57 | result.testResult.Time = 58 | float32(time.Now().Sub(startTime)) / float32(time.Second) 59 | return result.testResult, nil 60 | } 61 | 62 | func executeInternal( 63 | ctx context.Context, args []string, deadline time.Duration, env []string, 64 | result *xpytest_proto.TestResult, 65 | ) error { 66 | // Prepare a Cmd object. 67 | if len(args) == 0 { 68 | return fmt.Errorf("# of args must be larger than 0") 69 | } 70 | cmd := exec.CommandContext(ctx, args[0], args[1:]...) 71 | 72 | // Open pipes. 73 | stdoutPipe, err := cmd.StdoutPipe() 74 | if err != nil { 75 | return fmt.Errorf("failed to get stdout pipe: %s", err) 76 | } 77 | stderrPipe, err := cmd.StderrPipe() 78 | if err != nil { 79 | return fmt.Errorf("failed to get stderr pipe: %s", err) 80 | } 81 | 82 | // Set environment variables. 83 | if env == nil { 84 | env = []string{} 85 | } 86 | env = append(env, os.Environ()...) 87 | cmd.Env = env 88 | 89 | // Start the command. 90 | if err := cmd.Start(); err != nil { 91 | return fmt.Errorf("failed to start command: %s", err) 92 | } 93 | 94 | // Prepare a wait group to maintain threads. 95 | wg := sync.WaitGroup{} 96 | async := func(f func()) { 97 | wg.Add(1) 98 | go func() { 99 | defer wg.Done() 100 | f() 101 | }() 102 | } 103 | 104 | // Run I/O threads. 105 | readAll := func(pipe io.ReadCloser, out *string) { 106 | s := bufio.NewReaderSize(pipe, 128) 107 | for { 108 | line, err := s.ReadSlice('\n') 109 | if err == io.EOF { 110 | break 111 | } else if err != nil && err != bufio.ErrBufferFull { 112 | if err.Error() != "read |0: file already closed" { 113 | fmt.Fprintf(os.Stderr, 114 | "[ERROR] failed to read from pipe: %s\n", err) 115 | } 116 | break 117 | } 118 | *out += string(line) 119 | } 120 | pipe.Close() 121 | } 122 | async(func() { readAll(stdoutPipe, &result.Stdout) }) 123 | async(func() { readAll(stderrPipe, &result.Stderr) }) 124 | 125 | // Run timer thread. 126 | var timeout bool 127 | cmdIsDone := make(chan struct{}, 1) 128 | async(func() { 129 | select { 130 | case <-cmdIsDone: 131 | case <-time.After(deadline): 132 | timeout = true 133 | cmd.Process.Kill() 134 | } 135 | }) 136 | 137 | // Wait for the command. 138 | if err := cmd.Wait(); err != nil { 139 | fmt.Fprintf(os.Stderr, "[DEBUG] failed to wait a command: %s: %s\n", 140 | strings.Join(args, " "), err) 141 | cmd.Process.Kill() 142 | } 143 | close(cmdIsDone) 144 | wg.Wait() 145 | 146 | // Get the last line. 147 | if timeout { 148 | result.Status = xpytest_proto.TestResult_TIMEOUT 149 | } else if cmd.ProcessState.Success() { 150 | result.Status = xpytest_proto.TestResult_SUCCESS 151 | } else { 152 | result.Status = xpytest_proto.TestResult_FAILED 153 | } 154 | 155 | return nil 156 | } 157 | -------------------------------------------------------------------------------- /pkg/pytest/pytest.go: -------------------------------------------------------------------------------- 1 | package pytest 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "regexp" 8 | "strings" 9 | "time" 10 | 11 | xpytest_proto "github.com/chainer/xpytest/proto" 12 | ) 13 | 14 | // Pytest represents one pytest execution. 15 | type Pytest struct { 16 | PythonCmd string 17 | MarkerExpression string 18 | Xdist int 19 | Files []string 20 | Executor func( 21 | context.Context, []string, time.Duration, []string, 22 | ) (*xpytest_proto.TestResult, error) 23 | Retry int 24 | Env []string 25 | Deadline time.Duration 26 | } 27 | 28 | // NewPytest creates a new Pytest object. 29 | func NewPytest(pythonCmd string) *Pytest { 30 | return &Pytest{PythonCmd: pythonCmd, Executor: Execute} 31 | } 32 | 33 | // Execute builds pytest parameters and runs pytest. 34 | func (p *Pytest) Execute( 35 | ctx context.Context, 36 | ) (*Result, error) { 37 | var finalResult *Result 38 | for trial := 0; trial == 0 || trial < p.Retry; trial++ { 39 | pr, err := p.execute(ctx) 40 | if err != nil { 41 | return nil, err 42 | } 43 | if trial == 0 { 44 | finalResult = pr 45 | } else if pr.Status == xpytest_proto.TestResult_SUCCESS { 46 | finalResult.Status = xpytest_proto.TestResult_FLAKY 47 | } 48 | finalResult.trial = trial 49 | if finalResult.Status != xpytest_proto.TestResult_FAILED { 50 | break 51 | } 52 | } 53 | return finalResult, nil 54 | } 55 | 56 | func (p *Pytest) execute( 57 | ctx context.Context, 58 | ) (*Result, error) { 59 | // Build command-line arguments. 60 | args := []string{p.PythonCmd, "-m", "pytest"} 61 | if p.MarkerExpression != "" { 62 | args = append(args, "-m", p.MarkerExpression) 63 | } 64 | if p.Xdist > 0 { 65 | args = append(args, "-n", fmt.Sprintf("%d", p.Xdist)) 66 | } 67 | if len(p.Files) == 0 { 68 | return nil, errors.New("Pytest.Files must not be empty") 69 | } 70 | args = append(args, p.Files...) 71 | 72 | // Check deadline. 73 | deadline := p.Deadline 74 | if deadline <= 0 { 75 | return nil, fmt.Errorf("Pytest.Deadline must be postiive value") 76 | } 77 | 78 | // Execute pytest. 79 | r, err := p.Executor(ctx, args, deadline, p.Env) 80 | if err != nil { 81 | return nil, err 82 | } 83 | return newPytestResult(p, r), nil 84 | } 85 | 86 | // Result represents a pytest execution result. 87 | type Result struct { 88 | Status xpytest_proto.TestResult_Status 89 | Name string 90 | xdist int 91 | trial int 92 | duration float32 93 | summary string 94 | stdout string 95 | stderr string 96 | } 97 | 98 | func newPytestResult(p *Pytest, tr *xpytest_proto.TestResult) *Result { 99 | r := &Result{} 100 | if len(p.Files) > 0 { 101 | r.Name = p.Files[0] 102 | } 103 | r.Status = tr.GetStatus() 104 | result := "" 105 | if r.Status != xpytest_proto.TestResult_TIMEOUT { 106 | lines := strings.Split(strings.TrimSpace(tr.Stdout), "\n") 107 | lastLine := lines[len(lines)-1] 108 | if strings.HasPrefix(lastLine, "=") { 109 | result = strings.Trim(lastLine, "= ") 110 | } else { 111 | result = fmt.Sprintf("%s; %.0f seconds", r.Status, r.duration) 112 | r.Status = xpytest_proto.TestResult_INTERNAL 113 | } 114 | // pytest's message for no tests. 115 | if regexp.MustCompile( 116 | `^\d+ deselected in \d+(\.\d+)? seconds$`).MatchString(result) { 117 | r.Status = xpytest_proto.TestResult_SUCCESS 118 | } 119 | // pytest-xdist's message for no tests. 120 | if regexp.MustCompile( 121 | `^no tests ran in \d+(\.\d+)? seconds$`).MatchString(result) { 122 | r.Status = xpytest_proto.TestResult_SUCCESS 123 | } 124 | } 125 | r.xdist = p.Xdist 126 | r.duration = tr.GetTime() 127 | r.summary = func() string { 128 | if r.Status == xpytest_proto.TestResult_TIMEOUT { 129 | return fmt.Sprintf("%.0f seconds", r.duration) 130 | } 131 | return fmt.Sprintf("%s", result) 132 | }() 133 | shorten := func(s string) string { 134 | ss := strings.Split(s, "\n") 135 | if len(ss) > 500 { 136 | output := ss[0:250] 137 | output = append(output, 138 | fmt.Sprintf("...(%d lines skipped)...", len(ss)-500)) 139 | output = append(output, ss[len(ss)-250:]...) 140 | return strings.Join(output, "\n") 141 | } 142 | return s 143 | } 144 | r.stdout = shorten(tr.Stdout) 145 | r.stderr = shorten(tr.Stderr) 146 | return r 147 | } 148 | 149 | // Summary returns a one-line summary of the test result (e.g., 150 | // "[SUCCESS] test_foo.py (123 passed in 4.56 seconds)"). 151 | func (r *Result) Summary() string { 152 | ss := []string{} 153 | if r.summary != "" { 154 | ss = append(ss, r.summary) 155 | } 156 | if r.xdist > 0 { 157 | ss = append(ss, fmt.Sprintf("%d procs", r.xdist)) 158 | } 159 | if r.trial > 0 { 160 | ss = append(ss, fmt.Sprintf("%d trials", r.trial+1)) 161 | } 162 | s := strings.Join(ss, " * ") 163 | if s != "" { 164 | s = " (" + s + ")" 165 | } 166 | return fmt.Sprintf("[%s] %s%s", r.Status, r.Name, s) 167 | } 168 | 169 | // Output returns the test result. This returns outputs from STDOUT/STDERR in 170 | // addition to a one-line summary returned by Summary. 171 | func (r *Result) Output() string { 172 | if r.Status == xpytest_proto.TestResult_SUCCESS { 173 | return strings.TrimSpace(r.Summary() + "\n" + r.stderr) 174 | } 175 | return strings.TrimSpace(r.Summary() + "\n" + 176 | strings.TrimSpace(r.stdout+"\n"+r.stderr)) 177 | } 178 | -------------------------------------------------------------------------------- /pkg/xpytest/xpytest.go: -------------------------------------------------------------------------------- 1 | package xpytest 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path" 7 | "regexp" 8 | "runtime" 9 | "sort" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/bmatcuk/doublestar" 15 | 16 | "github.com/chainer/xpytest/pkg/pytest" 17 | "github.com/chainer/xpytest/pkg/reporter" 18 | "github.com/chainer/xpytest/pkg/resourcebuckets" 19 | xpytest_proto "github.com/chainer/xpytest/proto" 20 | ) 21 | 22 | // Xpytest is a controller for pytest queries. 23 | type Xpytest struct { 24 | PytestBase *pytest.Pytest 25 | Tests []*xpytest_proto.TestQuery 26 | TestResults []*xpytest_proto.TestResult 27 | Status xpytest_proto.TestResult_Status 28 | } 29 | 30 | // NewXpytest creates a new Xpytest. 31 | func NewXpytest(base *pytest.Pytest) *Xpytest { 32 | return &Xpytest{PytestBase: base} 33 | } 34 | 35 | // GetTests returns test queries. 36 | func (x *Xpytest) GetTests() []*xpytest_proto.TestQuery { 37 | if x.Tests == nil { 38 | return []*xpytest_proto.TestQuery{} 39 | } 40 | return x.Tests 41 | } 42 | 43 | // AddTestsWithFilePattern adds test files based on the given file pattern. 44 | func (x *Xpytest) AddTestsWithFilePattern(pattern string) error { 45 | files, err := doublestar.Glob(pattern) 46 | if err != nil { 47 | return fmt.Errorf( 48 | "failed to find files with pattern: %s: %s", pattern, err) 49 | } 50 | for _, f := range files { 51 | if regexp.MustCompile(`^test_.*\.py$`).MatchString(path.Base(f)) { 52 | x.Tests = append(x.GetTests(), &xpytest_proto.TestQuery{File: f}) 53 | } 54 | } 55 | return nil 56 | } 57 | 58 | // ApplyHint applies hint information to test cases. 59 | // CAVEAT: This computation order is O(n^2). This can be improved by sorting by 60 | // suffixes. 61 | func (x *Xpytest) ApplyHint(h *xpytest_proto.HintFile) error { 62 | for i := range h.GetSlowTests() { 63 | priority := i + 1 64 | hint := h.GetSlowTests()[len(h.GetSlowTests())-i-1] 65 | for _, tq := range x.GetTests() { 66 | if tq.GetFile() == hint.GetName() || 67 | strings.HasSuffix(tq.GetFile(), "/"+hint.GetName()) { 68 | tq.Priority = int32(priority) 69 | if hint.GetDeadline() != 0 { 70 | tq.Deadline = hint.GetDeadline() 71 | } else { 72 | tq.Deadline = 600.0 73 | } 74 | if hint.GetXdist() != 0 { 75 | tq.Xdist = hint.GetXdist() 76 | } 77 | if hint.GetRetry() > 0 { 78 | tq.Retry = hint.GetRetry() 79 | } 80 | } 81 | } 82 | } 83 | return nil 84 | } 85 | 86 | // Execute runs tests. 87 | func (x *Xpytest) Execute( 88 | ctx context.Context, bucket int, thread int, 89 | reporter reporter.Reporter, 90 | ) error { 91 | tests := append([]*xpytest_proto.TestQuery{}, x.Tests...) 92 | 93 | sort.SliceStable(tests, func(i, j int) bool { 94 | a, b := tests[i], tests[j] 95 | if a.Priority == b.Priority { 96 | return a.File < b.File 97 | } 98 | return a.Priority > b.Priority 99 | }) 100 | 101 | if thread == 0 { 102 | thread = (runtime.NumCPU() + bucket - 1) / bucket 103 | } 104 | rb := resourcebuckets.NewResourceBuckets(bucket, thread) 105 | resultChan := make(chan *pytest.Result, thread) 106 | 107 | printer := sync.WaitGroup{} 108 | printer.Add(1) 109 | go func() { 110 | defer printer.Done() 111 | passedTests := []*pytest.Result{} 112 | flakyTests := []*pytest.Result{} 113 | failedTests := []*pytest.Result{} 114 | for { 115 | r, ok := <-resultChan 116 | if !ok { 117 | break 118 | } 119 | fmt.Println(r.Output()) 120 | if r.Status == xpytest_proto.TestResult_SUCCESS { 121 | passedTests = append(passedTests, r) 122 | } else if r.Status == xpytest_proto.TestResult_FLAKY { 123 | flakyTests = append(flakyTests, r) 124 | } else { 125 | failedTests = append(failedTests, r) 126 | } 127 | } 128 | x.Status = xpytest_proto.TestResult_SUCCESS 129 | if len(flakyTests) > 0 { 130 | fmt.Printf("\n%s\n", horizon("FLAKY TESTS")) 131 | for _, t := range flakyTests { 132 | fmt.Printf("%s\n", t.Summary()) 133 | if reporter != nil { 134 | reporter.Log(ctx, t.Summary()) 135 | } 136 | } 137 | x.Status = xpytest_proto.TestResult_FLAKY 138 | } 139 | if len(failedTests) > 0 { 140 | fmt.Printf("\n%s\n", horizon("FAILED TESTS")) 141 | for _, t := range failedTests { 142 | fmt.Printf("%s\n", t.Summary()) 143 | } 144 | x.Status = xpytest_proto.TestResult_FAILED 145 | } 146 | fmt.Printf("\n%s\n", horizon("TEST SUMMARY")) 147 | fmt.Printf("%d failed, %d flaky, %d passed\n", 148 | len(failedTests), len(flakyTests), len(passedTests)) 149 | }() 150 | 151 | wg := sync.WaitGroup{} 152 | for _, t := range tests { 153 | t := t 154 | usage := rb.Acquire(func() int { 155 | if t.Xdist > 0 { 156 | return int(t.Xdist) 157 | } 158 | return 1 159 | }()) 160 | wg.Add(1) 161 | go func() { 162 | defer wg.Done() 163 | defer rb.Release(usage) 164 | pt := *x.PytestBase 165 | pt.Files = []string{t.File} 166 | pt.Xdist = int(t.Xdist) 167 | if pt.Xdist > thread { 168 | pt.Xdist = thread 169 | } 170 | if t.Retry != 0 { 171 | pt.Retry = int(t.Retry) 172 | } 173 | pt.Env = []string{ 174 | fmt.Sprintf("CUDA_VISIBLE_DEVICES=%s", func() string { 175 | s := []string{} 176 | for i := 0; i < bucket; i++ { 177 | s = append(s, fmt.Sprintf("%d", (i+usage.Index)%bucket)) 178 | } 179 | return strings.Join(s, ",") 180 | }()), 181 | } 182 | if t.Deadline != 0 { 183 | pt.Deadline = time.Duration(t.Deadline*1e6) * time.Microsecond 184 | } 185 | r, err := pt.Execute(ctx) 186 | if err != nil { 187 | panic(fmt.Sprintf("failed execute pytest: %s: %s", t.File, err)) 188 | } 189 | resultChan <- r 190 | }() 191 | } 192 | wg.Wait() 193 | close(resultChan) 194 | printer.Wait() 195 | return nil 196 | } 197 | 198 | func horizon(title string) string { 199 | if title == "" { 200 | return strings.Repeat("=", 70) 201 | } 202 | title = " " + strings.TrimSpace(title) + " " 203 | s := strings.Repeat("=", (70-len(title))/2) + title 204 | return s + strings.Repeat("=", 70-len(s)) 205 | } 206 | -------------------------------------------------------------------------------- /pkg/pytest/pytest_test.go: -------------------------------------------------------------------------------- 1 | package pytest_test 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/chainer/xpytest/pkg/pytest" 10 | xpytest_proto "github.com/chainer/xpytest/proto" 11 | ) 12 | 13 | type pytestExecutor struct { 14 | // Input parameters. 15 | Args []string 16 | Deadline time.Duration 17 | Env []string 18 | 19 | // Output parameters. 20 | TestResult *xpytest_proto.TestResult 21 | Error error 22 | } 23 | 24 | func (p *pytestExecutor) Execute( 25 | ctx context.Context, 26 | args []string, deadline time.Duration, env []string, 27 | ) (*xpytest_proto.TestResult, error) { 28 | p.Args = args 29 | p.Deadline = deadline 30 | p.Env = env 31 | if p.TestResult == nil { 32 | p.TestResult = &xpytest_proto.TestResult{} 33 | } 34 | return p.TestResult, p.Error 35 | } 36 | 37 | func TestPytest(t *testing.T) { 38 | ctx := context.Background() 39 | p := pytest.NewPytest("python3") 40 | executor := &pytestExecutor{ 41 | TestResult: &xpytest_proto.TestResult{ 42 | Status: xpytest_proto.TestResult_SUCCESS, 43 | Stdout: "=== 123 passed in 4.56 seconds ===", 44 | }, 45 | } 46 | p.Executor = executor.Execute 47 | p.Files = []string{"test_foo.py"} 48 | p.Deadline = time.Minute 49 | if r, err := p.Execute(ctx); err != nil { 50 | t.Fatalf("failed to execute: %s", err) 51 | } else if strings.Join(executor.Args, ",") != 52 | "python3,-m,pytest,test_foo.py" { 53 | t.Fatalf("unexpected args: %s", executor.Args) 54 | } else if executor.Env != nil { 55 | t.Fatalf("unexpected envs: %s", executor.Env) 56 | } else if s := r.Summary(); s != 57 | "[SUCCESS] test_foo.py (123 passed in 4.56 seconds)" { 58 | t.Fatalf("unexpected summary: %s", s) 59 | } 60 | } 61 | 62 | func TestPytestWithXdist(t *testing.T) { 63 | ctx := context.Background() 64 | p := pytest.NewPytest("python3") 65 | executor := &pytestExecutor{ 66 | TestResult: &xpytest_proto.TestResult{ 67 | Status: xpytest_proto.TestResult_SUCCESS, 68 | Stdout: "=== 123 passed in 4.56 seconds ===", 69 | }, 70 | } 71 | p.Executor = executor.Execute 72 | p.Files = []string{"test_foo.py"} 73 | p.Deadline = time.Minute 74 | p.Xdist = 4 75 | if _, err := p.Execute(ctx); err != nil { 76 | t.Fatalf("failed to execute: %s", err) 77 | } else if strings.Join(executor.Args, ",") != 78 | "python3,-m,pytest,-n,4,test_foo.py" { 79 | t.Fatalf("unexpected args: %s", executor.Args) 80 | } 81 | } 82 | 83 | func TestPytestWhenAllTestsAreDeselected(t *testing.T) { 84 | ctx := context.Background() 85 | p := pytest.NewPytest("python3") 86 | executor := &pytestExecutor{ 87 | TestResult: &xpytest_proto.TestResult{ 88 | Status: xpytest_proto.TestResult_FAILED, 89 | Stdout: "=== 123 deselected in 1.23 seconds ===", 90 | }, 91 | } 92 | p.Executor = executor.Execute 93 | p.Files = []string{"test_foo.py"} 94 | p.Deadline = time.Minute 95 | if r, err := p.Execute(ctx); err != nil { 96 | t.Fatalf("failed to execute: %s", err) 97 | } else if s := r.Summary(); s != 98 | "[SUCCESS] test_foo.py (123 deselected in 1.23 seconds)" { 99 | t.Fatalf("unexpected summary: %s", s) 100 | } 101 | } 102 | 103 | func TestPytestWithFlakyTest(t *testing.T) { 104 | ctx := context.Background() 105 | p := pytest.NewPytest("python3") 106 | trial := 0 107 | p.Executor = func( 108 | ctx context.Context, 109 | args []string, deadline time.Duration, env []string, 110 | ) (*xpytest_proto.TestResult, error) { 111 | trial++ 112 | if trial == 1 { 113 | return &xpytest_proto.TestResult{ 114 | Status: xpytest_proto.TestResult_FAILED, 115 | Stdout: "=== 1 failed, 122 passed in 1.23 seconds ===", 116 | }, nil 117 | } 118 | return &xpytest_proto.TestResult{ 119 | Status: xpytest_proto.TestResult_SUCCESS, 120 | Stdout: "=== 123 passed in 4.56 seconds ===", 121 | }, nil 122 | } 123 | p.Files = []string{"test_foo.py"} 124 | p.Deadline = time.Minute 125 | p.Retry = 2 126 | if r, err := p.Execute(ctx); err != nil { 127 | t.Fatalf("failed to execute: %s", err) 128 | } else if s := r.Summary(); s != 129 | "[FLAKY] test_foo.py"+ 130 | " (1 failed, 122 passed in 1.23 seconds * 2 trials)" { 131 | t.Fatalf("unexpected summary: %s", s) 132 | } 133 | } 134 | 135 | func TestPytestWithTimeoutTest(t *testing.T) { 136 | ctx := context.Background() 137 | p := pytest.NewPytest("python3") 138 | executor := &pytestExecutor{ 139 | TestResult: &xpytest_proto.TestResult{ 140 | Status: xpytest_proto.TestResult_TIMEOUT, 141 | Time: 61.234, 142 | }, 143 | } 144 | p.Executor = executor.Execute 145 | p.Files = []string{"test_foo.py"} 146 | p.Deadline = time.Minute 147 | if r, err := p.Execute(ctx); err != nil { 148 | t.Fatalf("failed to execute: %s", err) 149 | } else if s := r.Summary(); s != 150 | "[TIMEOUT] test_foo.py (61 seconds)" { 151 | t.Fatalf("unexpected summary: %s", s) 152 | } 153 | } 154 | 155 | func TestPytestWithOutput(t *testing.T) { 156 | ctx := context.Background() 157 | p := pytest.NewPytest("python3") 158 | executor := &pytestExecutor{ 159 | TestResult: &xpytest_proto.TestResult{ 160 | Status: xpytest_proto.TestResult_FAILED, 161 | Stdout: "foo\nbar\nbaz\n=== 1 failed, 23 passed in 4.5 seconds ===", 162 | Stderr: "stderr", 163 | }, 164 | } 165 | p.Executor = executor.Execute 166 | p.Files = []string{"test_foo.py"} 167 | p.Deadline = time.Minute 168 | if r, err := p.Execute(ctx); err != nil { 169 | t.Fatalf("failed to execute: %s", err) 170 | } else if s := r.Summary(); s != 171 | "[FAILED] test_foo.py (1 failed, 23 passed in 4.5 seconds)" { 172 | t.Fatalf("unexpected summary: %s", s) 173 | } else if s := r.Output(); s != 174 | "[FAILED] test_foo.py (1 failed, 23 passed in 4.5 seconds)\n"+ 175 | "foo\nbar\nbaz\n"+ 176 | "=== 1 failed, 23 passed in 4.5 seconds ===\n"+ 177 | "stderr" { 178 | t.Fatalf("unexpected output: %s", s) 179 | } 180 | } 181 | 182 | func TestPytestWithLongOutput(t *testing.T) { 183 | ctx := context.Background() 184 | p := pytest.NewPytest("python3") 185 | executor := &pytestExecutor{ 186 | TestResult: &xpytest_proto.TestResult{ 187 | Status: xpytest_proto.TestResult_FAILED, 188 | Stdout: strings.Repeat("foo\n", 1000) + 189 | "=== 1 failed, 23 passed in 4.5 seconds ===", 190 | }, 191 | } 192 | p.Executor = executor.Execute 193 | p.Files = []string{"test_foo.py"} 194 | p.Deadline = time.Minute 195 | if r, err := p.Execute(ctx); err != nil { 196 | t.Fatalf("failed to execute: %s", err) 197 | } else if s := r.Summary(); s != 198 | "[FAILED] test_foo.py (1 failed, 23 passed in 4.5 seconds)" { 199 | t.Fatalf("unexpected summary: %s", s) 200 | } else if ss := strings.Split(r.Output(), "\n"); len(ss) != 502 && 201 | ss[251] != "...(701 lines skipped)..." { 202 | t.Fatalf("unexpected output: %d: %s", len(ss), ss) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /proto/test_case.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: xpytest/proto/test_case.proto 3 | 4 | package xpytest_proto 5 | 6 | import proto "github.com/golang/protobuf/proto" 7 | import fmt "fmt" 8 | import math "math" 9 | 10 | // Reference imports to suppress errors if they are not otherwise used. 11 | var _ = proto.Marshal 12 | var _ = fmt.Errorf 13 | var _ = math.Inf 14 | 15 | // This is a compile-time assertion to ensure that this generated file 16 | // is compatible with the proto package it is being compiled against. 17 | // A compilation error at this line likely means your copy of the 18 | // proto package needs to be updated. 19 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 20 | 21 | type TestResult_Status int32 22 | 23 | const ( 24 | TestResult_UNKNOWN TestResult_Status = 0 25 | TestResult_SUCCESS TestResult_Status = 1 26 | TestResult_INTERNAL TestResult_Status = 2 27 | TestResult_FAILED TestResult_Status = 3 28 | TestResult_TIMEOUT TestResult_Status = 4 29 | TestResult_FLAKY TestResult_Status = 5 30 | ) 31 | 32 | var TestResult_Status_name = map[int32]string{ 33 | 0: "UNKNOWN", 34 | 1: "SUCCESS", 35 | 2: "INTERNAL", 36 | 3: "FAILED", 37 | 4: "TIMEOUT", 38 | 5: "FLAKY", 39 | } 40 | var TestResult_Status_value = map[string]int32{ 41 | "UNKNOWN": 0, 42 | "SUCCESS": 1, 43 | "INTERNAL": 2, 44 | "FAILED": 3, 45 | "TIMEOUT": 4, 46 | "FLAKY": 5, 47 | } 48 | 49 | func (x TestResult_Status) String() string { 50 | return proto.EnumName(TestResult_Status_name, int32(x)) 51 | } 52 | func (TestResult_Status) EnumDescriptor() ([]byte, []int) { 53 | return fileDescriptor_test_case_2830263af61e5ce1, []int{1, 0} 54 | } 55 | 56 | type TestQuery struct { 57 | // File path to a python test. 58 | File string `protobuf:"bytes,1,opt,name=file,proto3" json:"file,omitempty"` 59 | // Priority. A test with a higher priority should precede. This should be 60 | // useful to avoid slow tests from wasting time because of starting late. 61 | Priority int32 `protobuf:"varint,2,opt,name=priority,proto3" json:"priority,omitempty"` 62 | // Deadline in seconds. 63 | Deadline float32 `protobuf:"fixed32,3,opt,name=deadline,proto3" json:"deadline,omitempty"` 64 | // # of processes in pytest-xdist. 65 | Xdist int32 `protobuf:"varint,4,opt,name=xdist,proto3" json:"xdist,omitempty"` 66 | // # of retries. 67 | Retry int32 `protobuf:"varint,5,opt,name=retry,proto3" json:"retry,omitempty"` 68 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 69 | XXX_unrecognized []byte `json:"-"` 70 | XXX_sizecache int32 `json:"-"` 71 | } 72 | 73 | func (m *TestQuery) Reset() { *m = TestQuery{} } 74 | func (m *TestQuery) String() string { return proto.CompactTextString(m) } 75 | func (*TestQuery) ProtoMessage() {} 76 | func (*TestQuery) Descriptor() ([]byte, []int) { 77 | return fileDescriptor_test_case_2830263af61e5ce1, []int{0} 78 | } 79 | func (m *TestQuery) XXX_Unmarshal(b []byte) error { 80 | return xxx_messageInfo_TestQuery.Unmarshal(m, b) 81 | } 82 | func (m *TestQuery) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 83 | return xxx_messageInfo_TestQuery.Marshal(b, m, deterministic) 84 | } 85 | func (dst *TestQuery) XXX_Merge(src proto.Message) { 86 | xxx_messageInfo_TestQuery.Merge(dst, src) 87 | } 88 | func (m *TestQuery) XXX_Size() int { 89 | return xxx_messageInfo_TestQuery.Size(m) 90 | } 91 | func (m *TestQuery) XXX_DiscardUnknown() { 92 | xxx_messageInfo_TestQuery.DiscardUnknown(m) 93 | } 94 | 95 | var xxx_messageInfo_TestQuery proto.InternalMessageInfo 96 | 97 | func (m *TestQuery) GetFile() string { 98 | if m != nil { 99 | return m.File 100 | } 101 | return "" 102 | } 103 | 104 | func (m *TestQuery) GetPriority() int32 { 105 | if m != nil { 106 | return m.Priority 107 | } 108 | return 0 109 | } 110 | 111 | func (m *TestQuery) GetDeadline() float32 { 112 | if m != nil { 113 | return m.Deadline 114 | } 115 | return 0 116 | } 117 | 118 | func (m *TestQuery) GetXdist() int32 { 119 | if m != nil { 120 | return m.Xdist 121 | } 122 | return 0 123 | } 124 | 125 | func (m *TestQuery) GetRetry() int32 { 126 | if m != nil { 127 | return m.Retry 128 | } 129 | return 0 130 | } 131 | 132 | type TestResult struct { 133 | Status TestResult_Status `protobuf:"varint,1,opt,name=status,proto3,enum=xpytest.proto.TestResult_Status" json:"status,omitempty"` 134 | // Test name (e.g., "tests/foo_tests/test_bar.py"). 135 | Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` 136 | // Standard output. 137 | Stdout string `protobuf:"bytes,3,opt,name=stdout,proto3" json:"stdout,omitempty"` 138 | // Standard error. 139 | Stderr string `protobuf:"bytes,4,opt,name=stderr,proto3" json:"stderr,omitempty"` 140 | // Duration that the test took. 141 | Time float32 `protobuf:"fixed32,5,opt,name=time,proto3" json:"time,omitempty"` 142 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 143 | XXX_unrecognized []byte `json:"-"` 144 | XXX_sizecache int32 `json:"-"` 145 | } 146 | 147 | func (m *TestResult) Reset() { *m = TestResult{} } 148 | func (m *TestResult) String() string { return proto.CompactTextString(m) } 149 | func (*TestResult) ProtoMessage() {} 150 | func (*TestResult) Descriptor() ([]byte, []int) { 151 | return fileDescriptor_test_case_2830263af61e5ce1, []int{1} 152 | } 153 | func (m *TestResult) XXX_Unmarshal(b []byte) error { 154 | return xxx_messageInfo_TestResult.Unmarshal(m, b) 155 | } 156 | func (m *TestResult) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 157 | return xxx_messageInfo_TestResult.Marshal(b, m, deterministic) 158 | } 159 | func (dst *TestResult) XXX_Merge(src proto.Message) { 160 | xxx_messageInfo_TestResult.Merge(dst, src) 161 | } 162 | func (m *TestResult) XXX_Size() int { 163 | return xxx_messageInfo_TestResult.Size(m) 164 | } 165 | func (m *TestResult) XXX_DiscardUnknown() { 166 | xxx_messageInfo_TestResult.DiscardUnknown(m) 167 | } 168 | 169 | var xxx_messageInfo_TestResult proto.InternalMessageInfo 170 | 171 | func (m *TestResult) GetStatus() TestResult_Status { 172 | if m != nil { 173 | return m.Status 174 | } 175 | return TestResult_UNKNOWN 176 | } 177 | 178 | func (m *TestResult) GetName() string { 179 | if m != nil { 180 | return m.Name 181 | } 182 | return "" 183 | } 184 | 185 | func (m *TestResult) GetStdout() string { 186 | if m != nil { 187 | return m.Stdout 188 | } 189 | return "" 190 | } 191 | 192 | func (m *TestResult) GetStderr() string { 193 | if m != nil { 194 | return m.Stderr 195 | } 196 | return "" 197 | } 198 | 199 | func (m *TestResult) GetTime() float32 { 200 | if m != nil { 201 | return m.Time 202 | } 203 | return 0 204 | } 205 | 206 | type HintFile struct { 207 | SlowTests []*HintFile_SlowTest `protobuf:"bytes,1,rep,name=slow_tests,json=slowTests,proto3" json:"slow_tests,omitempty"` 208 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 209 | XXX_unrecognized []byte `json:"-"` 210 | XXX_sizecache int32 `json:"-"` 211 | } 212 | 213 | func (m *HintFile) Reset() { *m = HintFile{} } 214 | func (m *HintFile) String() string { return proto.CompactTextString(m) } 215 | func (*HintFile) ProtoMessage() {} 216 | func (*HintFile) Descriptor() ([]byte, []int) { 217 | return fileDescriptor_test_case_2830263af61e5ce1, []int{2} 218 | } 219 | func (m *HintFile) XXX_Unmarshal(b []byte) error { 220 | return xxx_messageInfo_HintFile.Unmarshal(m, b) 221 | } 222 | func (m *HintFile) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 223 | return xxx_messageInfo_HintFile.Marshal(b, m, deterministic) 224 | } 225 | func (dst *HintFile) XXX_Merge(src proto.Message) { 226 | xxx_messageInfo_HintFile.Merge(dst, src) 227 | } 228 | func (m *HintFile) XXX_Size() int { 229 | return xxx_messageInfo_HintFile.Size(m) 230 | } 231 | func (m *HintFile) XXX_DiscardUnknown() { 232 | xxx_messageInfo_HintFile.DiscardUnknown(m) 233 | } 234 | 235 | var xxx_messageInfo_HintFile proto.InternalMessageInfo 236 | 237 | func (m *HintFile) GetSlowTests() []*HintFile_SlowTest { 238 | if m != nil { 239 | return m.SlowTests 240 | } 241 | return nil 242 | } 243 | 244 | type HintFile_SlowTest struct { 245 | // File name of a slow test (e.g.,"test_foo.py", "bar/test_foo.py"). Parent 246 | // directories can be omitted (i.e., "test_foo.py" can matches 247 | // "bar/test_foo.py"). 248 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` 249 | // Deadline in seconds. 250 | Deadline float32 `protobuf:"fixed32,2,opt,name=deadline,proto3" json:"deadline,omitempty"` 251 | // # of processes in pytest-xdist. 252 | Xdist int32 `protobuf:"varint,3,opt,name=xdist,proto3" json:"xdist,omitempty"` 253 | // # of retries. For flaky tests. 254 | Retry int32 `protobuf:"varint,4,opt,name=retry,proto3" json:"retry,omitempty"` 255 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 256 | XXX_unrecognized []byte `json:"-"` 257 | XXX_sizecache int32 `json:"-"` 258 | } 259 | 260 | func (m *HintFile_SlowTest) Reset() { *m = HintFile_SlowTest{} } 261 | func (m *HintFile_SlowTest) String() string { return proto.CompactTextString(m) } 262 | func (*HintFile_SlowTest) ProtoMessage() {} 263 | func (*HintFile_SlowTest) Descriptor() ([]byte, []int) { 264 | return fileDescriptor_test_case_2830263af61e5ce1, []int{2, 0} 265 | } 266 | func (m *HintFile_SlowTest) XXX_Unmarshal(b []byte) error { 267 | return xxx_messageInfo_HintFile_SlowTest.Unmarshal(m, b) 268 | } 269 | func (m *HintFile_SlowTest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 270 | return xxx_messageInfo_HintFile_SlowTest.Marshal(b, m, deterministic) 271 | } 272 | func (dst *HintFile_SlowTest) XXX_Merge(src proto.Message) { 273 | xxx_messageInfo_HintFile_SlowTest.Merge(dst, src) 274 | } 275 | func (m *HintFile_SlowTest) XXX_Size() int { 276 | return xxx_messageInfo_HintFile_SlowTest.Size(m) 277 | } 278 | func (m *HintFile_SlowTest) XXX_DiscardUnknown() { 279 | xxx_messageInfo_HintFile_SlowTest.DiscardUnknown(m) 280 | } 281 | 282 | var xxx_messageInfo_HintFile_SlowTest proto.InternalMessageInfo 283 | 284 | func (m *HintFile_SlowTest) GetName() string { 285 | if m != nil { 286 | return m.Name 287 | } 288 | return "" 289 | } 290 | 291 | func (m *HintFile_SlowTest) GetDeadline() float32 { 292 | if m != nil { 293 | return m.Deadline 294 | } 295 | return 0 296 | } 297 | 298 | func (m *HintFile_SlowTest) GetXdist() int32 { 299 | if m != nil { 300 | return m.Xdist 301 | } 302 | return 0 303 | } 304 | 305 | func (m *HintFile_SlowTest) GetRetry() int32 { 306 | if m != nil { 307 | return m.Retry 308 | } 309 | return 0 310 | } 311 | 312 | func init() { 313 | proto.RegisterType((*TestQuery)(nil), "xpytest.proto.TestQuery") 314 | proto.RegisterType((*TestResult)(nil), "xpytest.proto.TestResult") 315 | proto.RegisterType((*HintFile)(nil), "xpytest.proto.HintFile") 316 | proto.RegisterType((*HintFile_SlowTest)(nil), "xpytest.proto.HintFile.SlowTest") 317 | proto.RegisterEnum("xpytest.proto.TestResult_Status", TestResult_Status_name, TestResult_Status_value) 318 | } 319 | 320 | func init() { 321 | proto.RegisterFile("xpytest/proto/test_case.proto", fileDescriptor_test_case_2830263af61e5ce1) 322 | } 323 | 324 | var fileDescriptor_test_case_2830263af61e5ce1 = []byte{ 325 | // 376 bytes of a gzipped FileDescriptorProto 326 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x51, 0x4d, 0x6b, 0xdb, 0x40, 327 | 0x14, 0xec, 0xea, 0xab, 0xd2, 0x73, 0x5b, 0xc4, 0x52, 0x8a, 0x30, 0x14, 0x84, 0x4e, 0x3a, 0xc9, 328 | 0xd0, 0x5e, 0x7a, 0x2b, 0xc6, 0x95, 0xa9, 0xb1, 0x23, 0x93, 0x95, 0x4c, 0xc8, 0xc9, 0x28, 0xd1, 329 | 0x1a, 0x04, 0xb2, 0x65, 0x76, 0x57, 0xd8, 0x3a, 0xe7, 0x27, 0xe5, 0xaf, 0xe5, 0x07, 0x84, 0x5d, 330 | 0xc9, 0x8e, 0x43, 0x92, 0xdb, 0xcc, 0xec, 0xe8, 0x69, 0xde, 0x1b, 0xf8, 0x79, 0xdc, 0xb7, 0x82, 331 | 0x72, 0x31, 0xda, 0xb3, 0x5a, 0xd4, 0x23, 0x09, 0xd7, 0xf7, 0x39, 0xa7, 0x91, 0xe2, 0xf8, 0x6b, 332 | 0xff, 0xdc, 0xd1, 0xe0, 0x01, 0x81, 0x93, 0x51, 0x2e, 0xae, 0x1b, 0xca, 0x5a, 0x8c, 0xc1, 0xd8, 333 | 0x94, 0x15, 0xf5, 0x90, 0x8f, 0x42, 0x87, 0x28, 0x8c, 0x87, 0x60, 0xef, 0x59, 0x59, 0xb3, 0x52, 334 | 0xb4, 0x9e, 0xe6, 0xa3, 0xd0, 0x24, 0x67, 0x2e, 0xdf, 0x0a, 0x9a, 0x17, 0x55, 0xb9, 0xa3, 0x9e, 335 | 0xee, 0xa3, 0x50, 0x23, 0x67, 0x8e, 0xbf, 0x83, 0x79, 0x2c, 0x4a, 0x2e, 0x3c, 0x43, 0x7d, 0xd4, 336 | 0x11, 0xa9, 0x32, 0x2a, 0x58, 0xeb, 0x99, 0x9d, 0xaa, 0x48, 0xf0, 0x84, 0x00, 0x64, 0x0a, 0x42, 337 | 0x79, 0x53, 0x09, 0xfc, 0x07, 0x2c, 0x2e, 0x72, 0xd1, 0x70, 0x15, 0xe4, 0xdb, 0x2f, 0x3f, 0x7a, 338 | 0x15, 0x3a, 0x7a, 0xb1, 0x46, 0xa9, 0xf2, 0x91, 0xde, 0x2f, 0x17, 0xd8, 0xe5, 0x5b, 0xaa, 0x82, 339 | 0x3a, 0x44, 0x61, 0xfc, 0x43, 0x4e, 0x2b, 0xea, 0x46, 0xa8, 0x88, 0x0e, 0xe9, 0x59, 0xaf, 0x53, 340 | 0xc6, 0x54, 0xc2, 0x4e, 0xa7, 0x8c, 0xc9, 0x19, 0xa2, 0xdc, 0x52, 0x95, 0x50, 0x23, 0x0a, 0x07, 341 | 0x19, 0x58, 0xdd, 0x9f, 0xf0, 0x00, 0x3e, 0xaf, 0x92, 0x79, 0xb2, 0xbc, 0x49, 0xdc, 0x4f, 0x92, 342 | 0xa4, 0xab, 0xc9, 0x24, 0x4e, 0x53, 0x17, 0xe1, 0x2f, 0x60, 0xcf, 0x92, 0x2c, 0x26, 0xc9, 0x78, 343 | 0xe1, 0x6a, 0x18, 0xc0, 0x9a, 0x8e, 0x67, 0x8b, 0xf8, 0x9f, 0xab, 0x4b, 0x5b, 0x36, 0xbb, 0x8a, 344 | 0x97, 0xab, 0xcc, 0x35, 0xb0, 0x03, 0xe6, 0x74, 0x31, 0x9e, 0xdf, 0xba, 0x66, 0xf0, 0x88, 0xc0, 345 | 0xfe, 0x5f, 0xee, 0xc4, 0x54, 0xde, 0xf9, 0x2f, 0x00, 0xaf, 0xea, 0xc3, 0x5a, 0xee, 0x29, 0x17, 346 | 0xd7, 0xc3, 0xc1, 0x9b, 0xc5, 0x4f, 0xe6, 0x28, 0xad, 0xea, 0x83, 0xba, 0x82, 0xc3, 0x7b, 0xc4, 347 | 0x87, 0x1b, 0xb0, 0x4f, 0xf2, 0xf9, 0x0e, 0xe8, 0xe2, 0x0e, 0x97, 0x65, 0x69, 0x1f, 0x95, 0xa5, 348 | 0xbf, 0x5b, 0x96, 0x71, 0x51, 0xd6, 0x9d, 0xa5, 0xb2, 0xfc, 0x7e, 0x0e, 0x00, 0x00, 0xff, 0xff, 349 | 0x3a, 0x65, 0x89, 0x35, 0x69, 0x02, 0x00, 0x00, 350 | } 351 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= 3 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= 5 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 6 | github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= 7 | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= 8 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 9 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 10 | github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 11 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 12 | github.com/bmatcuk/doublestar v1.1.1 h1:YroD6BJCZBYx06yYFEWvUuKVWQn3vLLQAVmDmvTSaiQ= 13 | github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= 14 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= 17 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= 18 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 19 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 20 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 21 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 22 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 23 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 24 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 25 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 26 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 27 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 28 | github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 29 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 30 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 31 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 32 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 33 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 34 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 35 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 36 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 37 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 38 | github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= 39 | github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= 40 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 41 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 42 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 43 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 44 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 45 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 46 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 47 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 48 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 49 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 50 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 51 | github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= 52 | github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= 53 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 54 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 55 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 56 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 57 | github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= 58 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 59 | github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 60 | github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 61 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 62 | github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 63 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 64 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 65 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 66 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 67 | go.opencensus.io v0.19.1/go.mod h1:gug0GbSHa8Pafr0d2urOSgoXHZ6x/RUlaiT0d9pqb4A= 68 | go.opencensus.io v0.19.2 h1:ZZpq6xI6kv/LuE/5s5UQvBU5vMjvRnPb8PvJrIntAnc= 69 | go.opencensus.io v0.19.2/go.mod h1:NO/8qkisMZLZ1FCsKNqtJPwc8/TaclWyY0B6wcYNg9M= 70 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 71 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 72 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 73 | golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 74 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 75 | golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 76 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 77 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 78 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 79 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 80 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 81 | golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 82 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 83 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 84 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 85 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 86 | golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= 87 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 88 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 89 | golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 90 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 91 | golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA= 92 | golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 93 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 94 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 95 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 96 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI= 97 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 98 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 99 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 100 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 101 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 102 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 103 | golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 104 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= 105 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 106 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 107 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 108 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 109 | golang.org/x/tools v0.0.0-20181219222714-6e267b5cc78e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 110 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 111 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 112 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 113 | google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 114 | google.golang.org/api v0.2.0 h1:B5VXkdjt7K2Gm6fGBC9C9a1OAKJDT95cTqwet+2zib0= 115 | google.golang.org/api v0.2.0/go.mod h1:IfRCZScioGtypHNTlz3gFk67J8uePVW7uDTBzXuIkhU= 116 | google.golang.org/api v0.3.0 h1:UIJY20OEo3+tK5MBlcdx37kmdH6EnRjGkW78mc6+EeA= 117 | google.golang.org/api v0.3.0/go.mod h1:IuvZyQh8jgscv8qWfQ4ABd8m7hEudgBFM/EdhA3BnXw= 118 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 119 | google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 120 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 121 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 122 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 123 | google.golang.org/genproto v0.0.0-20181219182458-5a97ab628bfb/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= 124 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 h1:Lj2SnHtxkRGJDqnGaSjo+CCdIieEnwVazbOXILwQemk= 125 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 126 | google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 127 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 128 | google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8= 129 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 130 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 131 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 132 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 133 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 134 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 135 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 136 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 137 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 138 | honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 139 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 140 | --------------------------------------------------------------------------------