├── README.md ├── .gitignore ├── .pfnci ├── script.bat ├── script.sh └── config.pbtxt ├── pkg ├── reporter │ ├── reporter.go │ └── sheets_reporter.go ├── xpytest │ ├── hint.go │ ├── xpytest_test.go │ └── xpytest.go ├── resourcebuckets │ ├── resource_buckets_test.go │ └── resource_buckets.go └── pytest │ ├── execute_test.go │ ├── execute_windows_test.go │ ├── execute.go │ ├── pytest.go │ └── pytest_test.go ├── go.mod ├── Makefile ├── cmd ├── debug_sheets_reporter │ └── main.go └── xpytest │ └── main.go ├── LICENSE ├── proto ├── test_case.proto └── test_case.pb.go └── go.sum /README.md: -------------------------------------------------------------------------------- 1 | # xpytest 2 | Parallelize pytest with more control. 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.pfnci/script.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set TEST_HOME=%CD% 4 | cd .. 5 | curl -o go.zip --insecure -L https://dl.google.com/go/go1.12.4.windows-amd64.zip 6 | 7z x go.zip 7 | set GOROOT=%CD%\go 8 | set PATH=%GOROOT%\bin;%PATH% 9 | set GO111MODULE=on 10 | 11 | cd %TEST_HOME% 12 | go get 13 | go test -v ./... 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | 12 | configs { 13 | key: "xpytest.unit.win" 14 | value { 15 | requirement { 16 | cpu: 2 17 | memory: 6 18 | image: "windows" 19 | } 20 | time_limit { 21 | seconds: 1800 22 | } 23 | command: ".pfnci\\script.bat" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Chainer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /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 without 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 creates 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 | -------------------------------------------------------------------------------- /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 | // Resource usage multiplier. 23 | float resource = 6; 24 | } 25 | 26 | message TestResult { 27 | enum Status { 28 | UNKNOWN = 0; 29 | SUCCESS = 1; 30 | INTERNAL = 2; 31 | FAILED = 3; 32 | TIMEOUT = 4; 33 | FLAKY = 5; 34 | } 35 | Status status = 1; 36 | 37 | // Test name (e.g., "tests/foo_tests/test_bar.py"). 38 | string name = 2; 39 | 40 | // Standard output. 41 | string stdout = 3; 42 | 43 | // Standard error. 44 | string stderr = 4; 45 | 46 | // Duration that the test took. 47 | float time = 5; 48 | } 49 | 50 | message HintFile { 51 | message Rule { 52 | // File name of a slow test (e.g.,"test_foo.py", "bar/test_foo.py"). Parent 53 | // directories can be omitted (i.e., "test_foo.py" can matches 54 | // "bar/test_foo.py"). 55 | string name = 1; 56 | 57 | // Deadline in seconds. 58 | float deadline = 2; 59 | 60 | // # of processes in pytest-xdist. 61 | int32 xdist = 3; 62 | 63 | // # of retries. For flaky tests. 64 | int32 retry = 4; 65 | 66 | // Resource usage multiplier (default: 1.0). 67 | float resource = 5; 68 | } 69 | // TODO(imos): Deprecate this once it is confirmed that no one uses this. 70 | repeated Rule slow_tests = 1; 71 | 72 | // A list of rules. If multiple rules matches a test target, a former rule 73 | // will override a latter rule. Test targets that no rule matches should be 74 | // deprioritized. 75 | repeated Rule rules = 2; 76 | } 77 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pkg/pytest/execute_test.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package pytest_test 4 | 5 | import ( 6 | "context" 7 | "testing" 8 | "time" 9 | 10 | "github.com/chainer/xpytest/pkg/pytest" 11 | xpytest_proto "github.com/chainer/xpytest/proto" 12 | ) 13 | 14 | func TestExecute(t *testing.T) { 15 | ctx := context.Background() 16 | r, err := pytest.Execute(ctx, []string{"true"}, time.Second, nil) 17 | if err != nil { 18 | t.Fatalf("failed to execute: %s", err) 19 | } 20 | if r.Status != xpytest_proto.TestResult_SUCCESS { 21 | t.Fatalf("unexpected status: %s", r.Status) 22 | } 23 | } 24 | 25 | func TestExecuteWithFailure(t *testing.T) { 26 | ctx := context.Background() 27 | r, err := pytest.Execute( 28 | ctx, []string{"bash", "-c", "exit 1"}, time.Second, nil) 29 | if err != nil { 30 | t.Fatalf("failed to execute: %s", err) 31 | } 32 | if r.Status != xpytest_proto.TestResult_FAILED { 33 | t.Fatalf("unexpected status: %s", r.Status) 34 | } 35 | } 36 | 37 | func TestExecuteWithNoTests(t *testing.T) { 38 | ctx := context.Background() 39 | r, err := pytest.Execute( 40 | ctx, []string{"bash", "-c", "exit 5"}, time.Second, nil) 41 | if err != nil { 42 | t.Fatalf("failed to execute: %s", err) 43 | } 44 | if r.Status != xpytest_proto.TestResult_SUCCESS { 45 | t.Fatalf("unexpected status: %s", r.Status) 46 | } 47 | } 48 | 49 | func TestExecuteWithTimeout(t *testing.T) { 50 | ctx := context.Background() 51 | r, err := pytest.Execute( 52 | ctx, []string{"sleep", "10"}, 100*time.Millisecond, nil) 53 | if err != nil { 54 | t.Fatalf("failed to execute: %s", err) 55 | } 56 | if r.Status != xpytest_proto.TestResult_TIMEOUT { 57 | t.Fatalf("unexpected status: %s", r.Status) 58 | } 59 | } 60 | 61 | func TestExecuteWithEnvironmentVariables(t *testing.T) { 62 | ctx := context.Background() 63 | r, err := pytest.Execute( 64 | ctx, []string{"bash", "-c", "echo $HOGE"}, 65 | time.Second, []string{"HOGE=PIYO"}) 66 | if err != nil { 67 | t.Fatalf("failed to execute: %s", err) 68 | } 69 | if r.Status != xpytest_proto.TestResult_SUCCESS { 70 | t.Fatalf("unexpected status: %s", r.Status) 71 | } 72 | if r.Stdout != "PIYO\n" { 73 | t.Fatalf("unexpected output: %s", r.Stdout) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pkg/pytest/execute_windows_test.go: -------------------------------------------------------------------------------- 1 | package pytest_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | "github.com/chainer/xpytest/pkg/pytest" 10 | xpytest_proto "github.com/chainer/xpytest/proto" 11 | ) 12 | 13 | func TestExecute(t *testing.T) { 14 | ctx := context.Background() 15 | equivalentTrueCmd := []string{"cmd", "/c", "sort < NUL > NUL"} 16 | r, err := pytest.Execute(ctx, equivalentTrueCmd, time.Second*3, nil) 17 | if err != nil { 18 | t.Fatalf("failed to execute: %s", err) 19 | } 20 | if r.Status != xpytest_proto.TestResult_SUCCESS { 21 | t.Fatalf("unexpected status: %s", r.Status) 22 | } 23 | } 24 | 25 | func TestExecuteWithFailure(t *testing.T) { 26 | ctx := context.Background() 27 | r, err := pytest.Execute( 28 | ctx, []string{"cmd", "/c", "powershell -Command exit 1"}, time.Second*3, nil) 29 | if err != nil { 30 | t.Fatalf("failed to execute: %s", err) 31 | } 32 | if r.Status != xpytest_proto.TestResult_FAILED { 33 | t.Fatalf("unexpected status: %s", r.Status) 34 | } 35 | } 36 | 37 | func TestExecuteWithNoTests(t *testing.T) { 38 | ctx := context.Background() 39 | r, err := pytest.Execute( 40 | ctx, []string{"cmd", "/c", "powershell -Command exit 5"}, time.Second*3, nil) 41 | if err != nil { 42 | t.Fatalf("failed to execute: %s", err) 43 | } 44 | if r.Status != xpytest_proto.TestResult_SUCCESS { 45 | t.Fatalf("unexpected status: %s", r.Status) 46 | } 47 | } 48 | 49 | func TestExecuteWithTimeout(t *testing.T) { 50 | ctx := context.Background() 51 | r, err := pytest.Execute( 52 | ctx, []string{"cmd", "/c", "ping localhost -n 10 > NUL"}, 53 | time.Millisecond*100, nil) 54 | if err != nil { 55 | t.Fatalf("failed to execute: %s", err) 56 | } 57 | if r.Status != xpytest_proto.TestResult_TIMEOUT { 58 | t.Fatalf("unexpected status: %s", r.Status) 59 | } 60 | } 61 | 62 | func TestExecuteWithEnvironmentVariables(t *testing.T) { 63 | ctx := context.Background() 64 | r, err := pytest.Execute( 65 | ctx, []string{"cmd", "/c", "echo %HOGE%"}, 66 | time.Second*3, []string{"HOGE=PIYO"}) 67 | if err != nil { 68 | t.Fatalf("failed to execute: %s", err) 69 | } 70 | if r.Status != xpytest_proto.TestResult_SUCCESS { 71 | t.Fatalf("unexpected status: %s", r.Status) 72 | } 73 | if r.Stdout != "PIYO\r\n" { 74 | t.Fatalf("unexpected output: %s", fmt.Sprintln("PIYO")) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /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/xpytest/xpytest_test.go: -------------------------------------------------------------------------------- 1 | package xpytest_test 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "sync/atomic" 7 | "testing" 8 | "time" 9 | 10 | "github.com/chainer/xpytest/pkg/pytest" 11 | "github.com/chainer/xpytest/pkg/xpytest" 12 | xpytest_proto "github.com/chainer/xpytest/proto" 13 | ) 14 | 15 | func TestXpytest(t *testing.T) { 16 | ctx := context.Background() 17 | 18 | lock := sync.WaitGroup{} 19 | total := int64(0) 20 | running := int64(0) 21 | lock.Add(1) 22 | base := &pytest.Pytest{ 23 | Executor: func( 24 | ctx context.Context, args []string, d time.Duration, x []string, 25 | ) (*xpytest_proto.TestResult, error) { 26 | defer atomic.AddInt64(&running, -1) 27 | defer atomic.AddInt64(&total, 1) 28 | atomic.AddInt64(&running, 1) 29 | lock.Wait() 30 | return &xpytest_proto.TestResult{ 31 | Status: xpytest_proto.TestResult_SUCCESS, 32 | }, nil 33 | }, 34 | } 35 | xpt := xpytest.NewXpytest(base) 36 | for i := 0; i < 100; i++ { 37 | xpt.Tests = append(xpt.GetTests(), &xpytest_proto.TestQuery{ 38 | Deadline: 1.0, 39 | }) 40 | } 41 | 42 | testGroup := sync.WaitGroup{} 43 | testGroup.Add(1) 44 | go func() { 45 | defer testGroup.Done() 46 | if err := xpt.Execute(ctx, 3, 4, nil); err != nil { 47 | t.Fatalf("failed to execute: %s", err) 48 | } 49 | }() 50 | 51 | for running < 12 { 52 | time.Sleep(10 * time.Millisecond) 53 | } 54 | time.Sleep(100 * time.Millisecond) 55 | if running != 12 { 56 | t.Fatalf("# of running jobs is unexpected: %d", running) 57 | } 58 | 59 | lock.Done() 60 | testGroup.Wait() 61 | 62 | if total != 100 { 63 | t.Fatalf("# of jobs is unexpected: %d", total) 64 | } 65 | } 66 | 67 | func TestXpytestWithResourceMultiplier(t *testing.T) { 68 | ctx := context.Background() 69 | 70 | lock := sync.WaitGroup{} 71 | running := int64(0) 72 | lock.Add(1) 73 | base := &pytest.Pytest{ 74 | Executor: func( 75 | ctx context.Context, args []string, d time.Duration, x []string, 76 | ) (*xpytest_proto.TestResult, error) { 77 | defer atomic.AddInt64(&running, -1) 78 | atomic.AddInt64(&running, 1) 79 | lock.Wait() 80 | return &xpytest_proto.TestResult{ 81 | Status: xpytest_proto.TestResult_SUCCESS, 82 | }, nil 83 | }, 84 | } 85 | xpt := xpytest.NewXpytest(base) 86 | for i := 0; i < 100; i++ { 87 | xpt.Tests = append(xpt.GetTests(), &xpytest_proto.TestQuery{ 88 | Deadline: 1.0, 89 | Resource: 2.0, 90 | }) 91 | } 92 | 93 | testGroup := sync.WaitGroup{} 94 | testGroup.Add(1) 95 | go func() { 96 | defer testGroup.Done() 97 | if err := xpt.Execute(ctx, 3, 4, nil); err != nil { 98 | t.Fatalf("failed to execute: %s", err) 99 | } 100 | }() 101 | 102 | for running < 6 { 103 | time.Sleep(10 * time.Millisecond) 104 | } 105 | time.Sleep(100 * time.Millisecond) 106 | if running != 6 { 107 | t.Fatalf("# of running jobs is unexpected: %d", running) 108 | } 109 | 110 | lock.Done() 111 | testGroup.Wait() 112 | } 113 | -------------------------------------------------------------------------------- /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 | "syscall" 13 | "time" 14 | 15 | "github.com/golang/protobuf/proto" 16 | 17 | xpytest_proto "github.com/chainer/xpytest/proto" 18 | ) 19 | 20 | // Execute executes a command. 21 | func Execute( 22 | ctx context.Context, args []string, deadline time.Duration, env []string, 23 | ) (*xpytest_proto.TestResult, error) { 24 | startTime := time.Now() 25 | 26 | type executeResult struct { 27 | testResult *xpytest_proto.TestResult 28 | err error 29 | } 30 | resultChan := make(chan *executeResult, 2) 31 | done := make(chan struct{}, 1) 32 | 33 | temporaryResult := &xpytest_proto.TestResult{} 34 | go func() { 35 | err := executeInternal( 36 | ctx, args, deadline, env, temporaryResult) 37 | resultChan <- &executeResult{testResult: temporaryResult, err: err} 38 | close(done) 39 | }() 40 | 41 | go func() { 42 | select { 43 | case <-done: 44 | case <-time.After(deadline + 5*time.Second): 45 | r := proto.Clone(temporaryResult).(*xpytest_proto.TestResult) 46 | r.Status = xpytest_proto.TestResult_TIMEOUT 47 | resultChan <- &executeResult{testResult: r, err: nil} 48 | fmt.Fprintf(os.Stderr, "[ERROR] command is hung up: %s\n", 49 | strings.Join(args, " ")) 50 | } 51 | }() 52 | 53 | result := <-resultChan 54 | if result.err != nil { 55 | return nil, result.err 56 | } 57 | 58 | result.testResult.Time = 59 | float32(time.Now().Sub(startTime)) / float32(time.Second) 60 | return result.testResult, nil 61 | } 62 | 63 | func executeInternal( 64 | ctx context.Context, args []string, deadline time.Duration, env []string, 65 | result *xpytest_proto.TestResult, 66 | ) error { 67 | // Prepare a Cmd object. 68 | if len(args) == 0 { 69 | return fmt.Errorf("# of args must be larger than 0") 70 | } 71 | cmd := exec.CommandContext(ctx, args[0], args[1:]...) 72 | 73 | // Open pipes. 74 | stdoutPipe, err := cmd.StdoutPipe() 75 | if err != nil { 76 | return fmt.Errorf("failed to get stdout pipe: %s", err) 77 | } 78 | stderrPipe, err := cmd.StderrPipe() 79 | if err != nil { 80 | return fmt.Errorf("failed to get stderr pipe: %s", err) 81 | } 82 | 83 | // Set environment variables. 84 | if env == nil { 85 | env = []string{} 86 | } 87 | env = append(env, os.Environ()...) 88 | cmd.Env = env 89 | 90 | // Start the command. 91 | if err := cmd.Start(); err != nil { 92 | return fmt.Errorf("failed to start command: %s", err) 93 | } 94 | 95 | // Prepare a wait group to maintain threads. 96 | wg := sync.WaitGroup{} 97 | async := func(f func()) { 98 | wg.Add(1) 99 | go func() { 100 | defer wg.Done() 101 | f() 102 | }() 103 | } 104 | 105 | // Run I/O threads. 106 | readAll := func(pipe io.ReadCloser, out *string) { 107 | s := bufio.NewReaderSize(pipe, 128) 108 | for { 109 | line, err := s.ReadSlice('\n') 110 | if err == io.EOF { 111 | break 112 | } else if err != nil && err != bufio.ErrBufferFull { 113 | if err.Error() != "read |0: file already closed" { 114 | fmt.Fprintf(os.Stderr, 115 | "[ERROR] failed to read from pipe: %s\n", err) 116 | } 117 | break 118 | } 119 | *out += string(line) 120 | } 121 | pipe.Close() 122 | } 123 | async(func() { readAll(stdoutPipe, &result.Stdout) }) 124 | async(func() { readAll(stderrPipe, &result.Stderr) }) 125 | 126 | // Run timer thread. 127 | var timeout bool 128 | cmdIsDone := make(chan struct{}, 1) 129 | async(func() { 130 | select { 131 | case <-cmdIsDone: 132 | case <-time.After(deadline): 133 | timeout = true 134 | cmd.Process.Kill() 135 | } 136 | }) 137 | 138 | // Wait for the command. 139 | err = cmd.Wait() 140 | if err != nil { 141 | fmt.Fprintf(os.Stderr, "[DEBUG] failed to wait a command: %s: %s\n", 142 | strings.Join(args, " "), err) 143 | cmd.Process.Kill() 144 | } 145 | close(cmdIsDone) 146 | wg.Wait() 147 | 148 | // Get the last line. 149 | if timeout { 150 | result.Status = xpytest_proto.TestResult_TIMEOUT 151 | } else if cmd.ProcessState.Success() { 152 | result.Status = xpytest_proto.TestResult_SUCCESS 153 | } else if hasNoTests := func() bool { 154 | // NOTE: pytest fails with code=5 if there are no tests: 155 | // https://docs.pytest.org/en/latest/usage.html 156 | if err, ok := err.(*exec.ExitError); ok { 157 | if s, ok := err.Sys().(syscall.WaitStatus); ok { 158 | return s.ExitStatus() == 5 159 | } 160 | } 161 | return false 162 | }(); hasNoTests { 163 | result.Status = xpytest_proto.TestResult_SUCCESS 164 | } else { 165 | result.Status = xpytest_proto.TestResult_FAILED 166 | } 167 | 168 | return nil 169 | } 170 | -------------------------------------------------------------------------------- /pkg/pytest/pytest.go: -------------------------------------------------------------------------------- 1 | package pytest 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | xpytest_proto "github.com/chainer/xpytest/proto" 11 | ) 12 | 13 | // Pytest represents one pytest execution. 14 | type Pytest struct { 15 | PythonCmd string 16 | MarkerExpression string 17 | Xdist int 18 | Files []string 19 | Executor func( 20 | context.Context, []string, time.Duration, []string, 21 | ) (*xpytest_proto.TestResult, error) 22 | Retry int 23 | Env []string 24 | Deadline time.Duration 25 | } 26 | 27 | // NewPytest creates a new Pytest object. 28 | func NewPytest(pythonCmd string) *Pytest { 29 | return &Pytest{PythonCmd: pythonCmd, Executor: Execute} 30 | } 31 | 32 | // Execute builds pytest parameters and runs pytest. 33 | func (p *Pytest) Execute( 34 | ctx context.Context, 35 | ) (*Result, error) { 36 | var finalResult *Result 37 | for trial := 0; trial == 0 || trial < p.Retry; trial++ { 38 | pr, err := p.execute(ctx) 39 | if err != nil { 40 | return nil, err 41 | } 42 | if trial == 0 { 43 | finalResult = pr 44 | } else if pr.Status == xpytest_proto.TestResult_SUCCESS { 45 | finalResult.Status = xpytest_proto.TestResult_FLAKY 46 | } 47 | finalResult.trial = trial 48 | if finalResult.Status != xpytest_proto.TestResult_FAILED { 49 | break 50 | } 51 | } 52 | return finalResult, nil 53 | } 54 | 55 | func (p *Pytest) execute( 56 | ctx context.Context, 57 | ) (*Result, error) { 58 | // Build command-line arguments. 59 | args := []string{p.PythonCmd, "-m", "pytest"} 60 | if p.MarkerExpression != "" { 61 | args = append(args, "-m", p.MarkerExpression) 62 | } 63 | if p.Xdist > 0 { 64 | args = append(args, "-n", fmt.Sprintf("%d", p.Xdist)) 65 | } 66 | if len(p.Files) == 0 { 67 | return nil, errors.New("Pytest.Files must not be empty") 68 | } 69 | args = append(args, p.Files...) 70 | 71 | // Check deadline. 72 | deadline := p.Deadline 73 | if deadline <= 0 { 74 | return nil, fmt.Errorf("Pytest.Deadline must be positive value") 75 | } 76 | 77 | // Execute pytest. 78 | r, err := p.Executor(ctx, args, deadline, p.Env) 79 | if err != nil { 80 | return nil, err 81 | } 82 | return newPytestResult(p, r), nil 83 | } 84 | 85 | // Result represents a pytest execution result. 86 | type Result struct { 87 | Status xpytest_proto.TestResult_Status 88 | Name string 89 | xdist int 90 | trial int 91 | duration float32 92 | summary string 93 | stdout string 94 | stderr string 95 | } 96 | 97 | func newPytestResult(p *Pytest, tr *xpytest_proto.TestResult) *Result { 98 | r := &Result{} 99 | if len(p.Files) > 0 { 100 | r.Name = p.Files[0] 101 | } 102 | r.Status = tr.GetStatus() 103 | result := "" 104 | if r.Status != xpytest_proto.TestResult_TIMEOUT { 105 | lines := strings.Split(strings.TrimSpace(tr.Stdout), "\n") 106 | lastLine := lines[len(lines)-1] 107 | if strings.HasPrefix(lastLine, "=") { 108 | result = strings.Trim(lastLine, "= ") 109 | } else { 110 | result = fmt.Sprintf("%s; %.0f seconds", r.Status, r.duration) 111 | r.Status = xpytest_proto.TestResult_INTERNAL 112 | } 113 | } 114 | r.xdist = p.Xdist 115 | r.duration = tr.GetTime() 116 | r.summary = func() string { 117 | if r.Status == xpytest_proto.TestResult_TIMEOUT { 118 | return fmt.Sprintf("%.0f seconds", r.duration) 119 | } 120 | return fmt.Sprintf("%s", result) 121 | }() 122 | shorten := func(s string) string { 123 | ss := strings.Split(s, "\n") 124 | if len(ss) > 500 { 125 | output := ss[0:250] 126 | output = append(output, 127 | fmt.Sprintf("...(%d lines skipped)...", len(ss)-500)) 128 | output = append(output, ss[len(ss)-250:]...) 129 | return strings.Join(output, "\n") 130 | } 131 | return s 132 | } 133 | r.stdout = shorten(tr.Stdout) 134 | r.stderr = shorten(tr.Stderr) 135 | return r 136 | } 137 | 138 | // Summary returns a one-line summary of the test result (e.g., 139 | // "[SUCCESS] test_foo.py (123 passed in 4.56 seconds)"). 140 | func (r *Result) Summary() string { 141 | ss := []string{} 142 | if r.summary != "" { 143 | ss = append(ss, r.summary) 144 | } 145 | if r.xdist > 0 { 146 | ss = append(ss, fmt.Sprintf("%d procs", r.xdist)) 147 | } 148 | if r.trial > 0 { 149 | ss = append(ss, fmt.Sprintf("%d trials", r.trial+1)) 150 | } 151 | s := strings.Join(ss, " * ") 152 | if s != "" { 153 | s = " (" + s + ")" 154 | } 155 | return fmt.Sprintf("[%s] %s%s", r.Status, r.Name, s) 156 | } 157 | 158 | // Output returns the test result. This returns outputs from STDOUT/STDERR in 159 | // addition to a one-line summary returned by Summary. 160 | func (r *Result) Output() string { 161 | if r.Status == xpytest_proto.TestResult_SUCCESS { 162 | return strings.TrimSpace(r.Summary() + "\n" + r.stderr) 163 | } 164 | return strings.TrimSpace(r.Summary() + "\n" + 165 | strings.TrimSpace(r.stdout+"\n"+r.stderr)) 166 | } 167 | -------------------------------------------------------------------------------- /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 TestPytestWithFlakyTest(t *testing.T) { 84 | ctx := context.Background() 85 | p := pytest.NewPytest("python3") 86 | trial := 0 87 | p.Executor = func( 88 | ctx context.Context, 89 | args []string, deadline time.Duration, env []string, 90 | ) (*xpytest_proto.TestResult, error) { 91 | trial++ 92 | if trial == 1 { 93 | return &xpytest_proto.TestResult{ 94 | Status: xpytest_proto.TestResult_FAILED, 95 | Stdout: "=== 1 failed, 122 passed in 1.23 seconds ===", 96 | }, nil 97 | } 98 | return &xpytest_proto.TestResult{ 99 | Status: xpytest_proto.TestResult_SUCCESS, 100 | Stdout: "=== 123 passed in 4.56 seconds ===", 101 | }, nil 102 | } 103 | p.Files = []string{"test_foo.py"} 104 | p.Deadline = time.Minute 105 | p.Retry = 2 106 | if r, err := p.Execute(ctx); err != nil { 107 | t.Fatalf("failed to execute: %s", err) 108 | } else if s := r.Summary(); s != 109 | "[FLAKY] test_foo.py"+ 110 | " (1 failed, 122 passed in 1.23 seconds * 2 trials)" { 111 | t.Fatalf("unexpected summary: %s", s) 112 | } 113 | } 114 | 115 | func TestPytestWithTimeoutTest(t *testing.T) { 116 | ctx := context.Background() 117 | p := pytest.NewPytest("python3") 118 | executor := &pytestExecutor{ 119 | TestResult: &xpytest_proto.TestResult{ 120 | Status: xpytest_proto.TestResult_TIMEOUT, 121 | Time: 61.234, 122 | }, 123 | } 124 | p.Executor = executor.Execute 125 | p.Files = []string{"test_foo.py"} 126 | p.Deadline = time.Minute 127 | if r, err := p.Execute(ctx); err != nil { 128 | t.Fatalf("failed to execute: %s", err) 129 | } else if s := r.Summary(); s != 130 | "[TIMEOUT] test_foo.py (61 seconds)" { 131 | t.Fatalf("unexpected summary: %s", s) 132 | } 133 | } 134 | 135 | func TestPytestWithOutput(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_FAILED, 141 | Stdout: "foo\nbar\nbaz\n=== 1 failed, 23 passed in 4.5 seconds ===", 142 | Stderr: "stderr", 143 | }, 144 | } 145 | p.Executor = executor.Execute 146 | p.Files = []string{"test_foo.py"} 147 | p.Deadline = time.Minute 148 | if r, err := p.Execute(ctx); err != nil { 149 | t.Fatalf("failed to execute: %s", err) 150 | } else if s := r.Summary(); s != 151 | "[FAILED] test_foo.py (1 failed, 23 passed in 4.5 seconds)" { 152 | t.Fatalf("unexpected summary: %s", s) 153 | } else if s := r.Output(); s != 154 | "[FAILED] test_foo.py (1 failed, 23 passed in 4.5 seconds)\n"+ 155 | "foo\nbar\nbaz\n"+ 156 | "=== 1 failed, 23 passed in 4.5 seconds ===\n"+ 157 | "stderr" { 158 | t.Fatalf("unexpected output: %s", s) 159 | } 160 | } 161 | 162 | func TestPytestWithLongOutput(t *testing.T) { 163 | ctx := context.Background() 164 | p := pytest.NewPytest("python3") 165 | executor := &pytestExecutor{ 166 | TestResult: &xpytest_proto.TestResult{ 167 | Status: xpytest_proto.TestResult_FAILED, 168 | Stdout: strings.Repeat("foo\n", 1000) + 169 | "=== 1 failed, 23 passed in 4.5 seconds ===", 170 | }, 171 | } 172 | p.Executor = executor.Execute 173 | p.Files = []string{"test_foo.py"} 174 | p.Deadline = time.Minute 175 | if r, err := p.Execute(ctx); err != nil { 176 | t.Fatalf("failed to execute: %s", err) 177 | } else if s := r.Summary(); s != 178 | "[FAILED] test_foo.py (1 failed, 23 passed in 4.5 seconds)" { 179 | t.Fatalf("unexpected summary: %s", s) 180 | } else if ss := strings.Split(r.Output(), "\n"); len(ss) != 502 && 181 | ss[251] != "...(701 lines skipped)..." { 182 | t.Fatalf("unexpected output: %d: %s", len(ss), ss) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /pkg/xpytest/xpytest.go: -------------------------------------------------------------------------------- 1 | package xpytest 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math" 7 | "path/filepath" 8 | "regexp" 9 | "runtime" 10 | "sort" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/bmatcuk/doublestar" 16 | 17 | "github.com/chainer/xpytest/pkg/pytest" 18 | "github.com/chainer/xpytest/pkg/reporter" 19 | "github.com/chainer/xpytest/pkg/resourcebuckets" 20 | xpytest_proto "github.com/chainer/xpytest/proto" 21 | ) 22 | 23 | const resourceResolution = 1000 24 | 25 | // Xpytest is a controller for pytest queries. 26 | type Xpytest struct { 27 | PytestBase *pytest.Pytest 28 | Tests []*xpytest_proto.TestQuery 29 | TestResults []*xpytest_proto.TestResult 30 | Status xpytest_proto.TestResult_Status 31 | } 32 | 33 | // NewXpytest creates a new Xpytest. 34 | func NewXpytest(base *pytest.Pytest) *Xpytest { 35 | return &Xpytest{PytestBase: base} 36 | } 37 | 38 | // GetTests returns test queries. 39 | func (x *Xpytest) GetTests() []*xpytest_proto.TestQuery { 40 | if x.Tests == nil { 41 | return []*xpytest_proto.TestQuery{} 42 | } 43 | return x.Tests 44 | } 45 | 46 | // AddTestsWithFilePattern adds test files based on the given file pattern. 47 | func (x *Xpytest) AddTestsWithFilePattern(pattern string) error { 48 | files, err := doublestar.Glob(pattern) 49 | if err != nil { 50 | return fmt.Errorf( 51 | "failed to find files with pattern: %s: %s", pattern, err) 52 | } 53 | for _, f := range files { 54 | if regexp.MustCompile(`^test_.*\.py$`).MatchString(filepath.Base(f)) { 55 | x.Tests = append(x.GetTests(), &xpytest_proto.TestQuery{File: f}) 56 | } 57 | } 58 | return nil 59 | } 60 | 61 | // ApplyHint applies hint information to test cases. 62 | // CAVEAT: This computation order is O(n^2). This can be improved by sorting by 63 | // suffixes. 64 | func (x *Xpytest) ApplyHint(h *xpytest_proto.HintFile) error { 65 | rules := append(h.GetRules(), h.GetSlowTests()...) 66 | for i := range rules { 67 | priority := i + 1 68 | rule := rules[len(rules)-i-1] 69 | for _, tq := range x.GetTests() { 70 | if tq.GetFile() == rule.GetName() || 71 | strings.HasSuffix(tq.GetFile(), string(filepath.Separator)+ 72 | rule.GetName()) { 73 | tq.Priority = int32(priority) 74 | if rule.GetDeadline() != 0 { 75 | tq.Deadline = rule.GetDeadline() 76 | } else { 77 | tq.Deadline = 600.0 78 | } 79 | if rule.GetXdist() != 0 { 80 | tq.Xdist = rule.GetXdist() 81 | } 82 | if rule.GetRetry() > 0 { 83 | tq.Retry = rule.GetRetry() 84 | } 85 | if rule.GetResource() > 0 { 86 | tq.Resource = rule.GetResource() 87 | } 88 | } 89 | } 90 | } 91 | return nil 92 | } 93 | 94 | // Execute runs tests. 95 | func (x *Xpytest) Execute( 96 | ctx context.Context, bucket int, thread int, 97 | reporter reporter.Reporter, 98 | ) error { 99 | tests := append([]*xpytest_proto.TestQuery{}, x.Tests...) 100 | 101 | sort.SliceStable(tests, func(i, j int) bool { 102 | a, b := tests[i], tests[j] 103 | if a.Priority == b.Priority { 104 | return a.File < b.File 105 | } 106 | return a.Priority > b.Priority 107 | }) 108 | 109 | if thread == 0 { 110 | thread = (runtime.NumCPU() + bucket - 1) / bucket 111 | } 112 | rb := resourcebuckets.NewResourceBuckets(bucket, thread*resourceResolution) 113 | resultChan := make(chan *pytest.Result, thread) 114 | 115 | printer := sync.WaitGroup{} 116 | printer.Add(1) 117 | go func() { 118 | defer printer.Done() 119 | passedTests := []*pytest.Result{} 120 | flakyTests := []*pytest.Result{} 121 | failedTests := []*pytest.Result{} 122 | for { 123 | r, ok := <-resultChan 124 | if !ok { 125 | break 126 | } 127 | fmt.Println(r.Output()) 128 | if r.Status == xpytest_proto.TestResult_SUCCESS { 129 | passedTests = append(passedTests, r) 130 | } else if r.Status == xpytest_proto.TestResult_FLAKY { 131 | flakyTests = append(flakyTests, r) 132 | } else { 133 | failedTests = append(failedTests, r) 134 | } 135 | } 136 | x.Status = xpytest_proto.TestResult_SUCCESS 137 | if len(flakyTests) > 0 { 138 | fmt.Printf("\n%s\n", horizon("FLAKY TESTS")) 139 | for _, t := range flakyTests { 140 | fmt.Printf("%s\n", t.Summary()) 141 | if reporter != nil { 142 | reporter.Log(ctx, t.Summary()) 143 | } 144 | } 145 | x.Status = xpytest_proto.TestResult_FLAKY 146 | } 147 | if len(failedTests) > 0 { 148 | fmt.Printf("\n%s\n", horizon("FAILED TESTS")) 149 | for _, t := range failedTests { 150 | fmt.Printf("%s\n", t.Summary()) 151 | } 152 | x.Status = xpytest_proto.TestResult_FAILED 153 | } 154 | fmt.Printf("\n%s\n", horizon("TEST SUMMARY")) 155 | fmt.Printf("%d failed, %d flaky, %d passed\n", 156 | len(failedTests), len(flakyTests), len(passedTests)) 157 | }() 158 | 159 | wg := sync.WaitGroup{} 160 | for _, t := range tests { 161 | t := t 162 | usage := rb.Acquire(func() int { 163 | req := float64(resourceResolution) 164 | if t.Xdist > 0 { 165 | req *= float64(t.Xdist) 166 | } 167 | if t.Resource > 0 { 168 | req *= float64(t.Resource) 169 | } 170 | return int(math.Ceil(req)) 171 | }()) 172 | wg.Add(1) 173 | go func() { 174 | defer wg.Done() 175 | defer rb.Release(usage) 176 | pt := *x.PytestBase 177 | pt.Files = []string{t.File} 178 | pt.Xdist = int(t.Xdist) 179 | if pt.Xdist > thread { 180 | pt.Xdist = thread 181 | } 182 | if t.Retry != 0 { 183 | pt.Retry = int(t.Retry) 184 | } 185 | pt.Env = []string{ 186 | fmt.Sprintf("CUDA_VISIBLE_DEVICES=%s", func() string { 187 | s := []string{} 188 | for i := 0; i < bucket; i++ { 189 | s = append(s, fmt.Sprintf("%d", (i+usage.Index)%bucket)) 190 | } 191 | return strings.Join(s, ",") 192 | }()), 193 | } 194 | if t.Deadline != 0 { 195 | pt.Deadline = time.Duration(t.Deadline*1e6) * time.Microsecond 196 | } 197 | r, err := pt.Execute(ctx) 198 | if err != nil { 199 | panic(fmt.Sprintf("failed execute pytest: %s: %s", t.File, err)) 200 | } 201 | resultChan <- r 202 | }() 203 | } 204 | wg.Wait() 205 | close(resultChan) 206 | printer.Wait() 207 | return nil 208 | } 209 | 210 | func horizon(title string) string { 211 | if title == "" { 212 | return strings.Repeat("=", 70) 213 | } 214 | title = " " + strings.TrimSpace(title) + " " 215 | s := strings.Repeat("=", (70-len(title))/2) + title 216 | return s + strings.Repeat("=", 70-len(s)) 217 | } 218 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_044fbc67b1ebf751, []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 | // Resource usage multiplier. 69 | Resource float32 `protobuf:"fixed32,6,opt,name=resource,proto3" json:"resource,omitempty"` 70 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 71 | XXX_unrecognized []byte `json:"-"` 72 | XXX_sizecache int32 `json:"-"` 73 | } 74 | 75 | func (m *TestQuery) Reset() { *m = TestQuery{} } 76 | func (m *TestQuery) String() string { return proto.CompactTextString(m) } 77 | func (*TestQuery) ProtoMessage() {} 78 | func (*TestQuery) Descriptor() ([]byte, []int) { 79 | return fileDescriptor_test_case_044fbc67b1ebf751, []int{0} 80 | } 81 | func (m *TestQuery) XXX_Unmarshal(b []byte) error { 82 | return xxx_messageInfo_TestQuery.Unmarshal(m, b) 83 | } 84 | func (m *TestQuery) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 85 | return xxx_messageInfo_TestQuery.Marshal(b, m, deterministic) 86 | } 87 | func (dst *TestQuery) XXX_Merge(src proto.Message) { 88 | xxx_messageInfo_TestQuery.Merge(dst, src) 89 | } 90 | func (m *TestQuery) XXX_Size() int { 91 | return xxx_messageInfo_TestQuery.Size(m) 92 | } 93 | func (m *TestQuery) XXX_DiscardUnknown() { 94 | xxx_messageInfo_TestQuery.DiscardUnknown(m) 95 | } 96 | 97 | var xxx_messageInfo_TestQuery proto.InternalMessageInfo 98 | 99 | func (m *TestQuery) GetFile() string { 100 | if m != nil { 101 | return m.File 102 | } 103 | return "" 104 | } 105 | 106 | func (m *TestQuery) GetPriority() int32 { 107 | if m != nil { 108 | return m.Priority 109 | } 110 | return 0 111 | } 112 | 113 | func (m *TestQuery) GetDeadline() float32 { 114 | if m != nil { 115 | return m.Deadline 116 | } 117 | return 0 118 | } 119 | 120 | func (m *TestQuery) GetXdist() int32 { 121 | if m != nil { 122 | return m.Xdist 123 | } 124 | return 0 125 | } 126 | 127 | func (m *TestQuery) GetRetry() int32 { 128 | if m != nil { 129 | return m.Retry 130 | } 131 | return 0 132 | } 133 | 134 | func (m *TestQuery) GetResource() float32 { 135 | if m != nil { 136 | return m.Resource 137 | } 138 | return 0 139 | } 140 | 141 | type TestResult struct { 142 | Status TestResult_Status `protobuf:"varint,1,opt,name=status,proto3,enum=xpytest.proto.TestResult_Status" json:"status,omitempty"` 143 | // Test name (e.g., "tests/foo_tests/test_bar.py"). 144 | Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` 145 | // Standard output. 146 | Stdout string `protobuf:"bytes,3,opt,name=stdout,proto3" json:"stdout,omitempty"` 147 | // Standard error. 148 | Stderr string `protobuf:"bytes,4,opt,name=stderr,proto3" json:"stderr,omitempty"` 149 | // Duration that the test took. 150 | Time float32 `protobuf:"fixed32,5,opt,name=time,proto3" json:"time,omitempty"` 151 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 152 | XXX_unrecognized []byte `json:"-"` 153 | XXX_sizecache int32 `json:"-"` 154 | } 155 | 156 | func (m *TestResult) Reset() { *m = TestResult{} } 157 | func (m *TestResult) String() string { return proto.CompactTextString(m) } 158 | func (*TestResult) ProtoMessage() {} 159 | func (*TestResult) Descriptor() ([]byte, []int) { 160 | return fileDescriptor_test_case_044fbc67b1ebf751, []int{1} 161 | } 162 | func (m *TestResult) XXX_Unmarshal(b []byte) error { 163 | return xxx_messageInfo_TestResult.Unmarshal(m, b) 164 | } 165 | func (m *TestResult) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 166 | return xxx_messageInfo_TestResult.Marshal(b, m, deterministic) 167 | } 168 | func (dst *TestResult) XXX_Merge(src proto.Message) { 169 | xxx_messageInfo_TestResult.Merge(dst, src) 170 | } 171 | func (m *TestResult) XXX_Size() int { 172 | return xxx_messageInfo_TestResult.Size(m) 173 | } 174 | func (m *TestResult) XXX_DiscardUnknown() { 175 | xxx_messageInfo_TestResult.DiscardUnknown(m) 176 | } 177 | 178 | var xxx_messageInfo_TestResult proto.InternalMessageInfo 179 | 180 | func (m *TestResult) GetStatus() TestResult_Status { 181 | if m != nil { 182 | return m.Status 183 | } 184 | return TestResult_UNKNOWN 185 | } 186 | 187 | func (m *TestResult) GetName() string { 188 | if m != nil { 189 | return m.Name 190 | } 191 | return "" 192 | } 193 | 194 | func (m *TestResult) GetStdout() string { 195 | if m != nil { 196 | return m.Stdout 197 | } 198 | return "" 199 | } 200 | 201 | func (m *TestResult) GetStderr() string { 202 | if m != nil { 203 | return m.Stderr 204 | } 205 | return "" 206 | } 207 | 208 | func (m *TestResult) GetTime() float32 { 209 | if m != nil { 210 | return m.Time 211 | } 212 | return 0 213 | } 214 | 215 | type HintFile struct { 216 | // TODO(imos): Deprecate this once it is confirmed that no one uses this. 217 | SlowTests []*HintFile_Rule `protobuf:"bytes,1,rep,name=slow_tests,json=slowTests,proto3" json:"slow_tests,omitempty"` 218 | // A list of rules. If multiple rules matches a test target, a former rule 219 | // will override a latter rule. Test targets that no rule matches should be 220 | // deprioritized. 221 | Rules []*HintFile_Rule `protobuf:"bytes,2,rep,name=rules,proto3" json:"rules,omitempty"` 222 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 223 | XXX_unrecognized []byte `json:"-"` 224 | XXX_sizecache int32 `json:"-"` 225 | } 226 | 227 | func (m *HintFile) Reset() { *m = HintFile{} } 228 | func (m *HintFile) String() string { return proto.CompactTextString(m) } 229 | func (*HintFile) ProtoMessage() {} 230 | func (*HintFile) Descriptor() ([]byte, []int) { 231 | return fileDescriptor_test_case_044fbc67b1ebf751, []int{2} 232 | } 233 | func (m *HintFile) XXX_Unmarshal(b []byte) error { 234 | return xxx_messageInfo_HintFile.Unmarshal(m, b) 235 | } 236 | func (m *HintFile) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 237 | return xxx_messageInfo_HintFile.Marshal(b, m, deterministic) 238 | } 239 | func (dst *HintFile) XXX_Merge(src proto.Message) { 240 | xxx_messageInfo_HintFile.Merge(dst, src) 241 | } 242 | func (m *HintFile) XXX_Size() int { 243 | return xxx_messageInfo_HintFile.Size(m) 244 | } 245 | func (m *HintFile) XXX_DiscardUnknown() { 246 | xxx_messageInfo_HintFile.DiscardUnknown(m) 247 | } 248 | 249 | var xxx_messageInfo_HintFile proto.InternalMessageInfo 250 | 251 | func (m *HintFile) GetSlowTests() []*HintFile_Rule { 252 | if m != nil { 253 | return m.SlowTests 254 | } 255 | return nil 256 | } 257 | 258 | func (m *HintFile) GetRules() []*HintFile_Rule { 259 | if m != nil { 260 | return m.Rules 261 | } 262 | return nil 263 | } 264 | 265 | type HintFile_Rule struct { 266 | // File name of a slow test (e.g.,"test_foo.py", "bar/test_foo.py"). Parent 267 | // directories can be omitted (i.e., "test_foo.py" can matches 268 | // "bar/test_foo.py"). 269 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` 270 | // Deadline in seconds. 271 | Deadline float32 `protobuf:"fixed32,2,opt,name=deadline,proto3" json:"deadline,omitempty"` 272 | // # of processes in pytest-xdist. 273 | Xdist int32 `protobuf:"varint,3,opt,name=xdist,proto3" json:"xdist,omitempty"` 274 | // # of retries. For flaky tests. 275 | Retry int32 `protobuf:"varint,4,opt,name=retry,proto3" json:"retry,omitempty"` 276 | // Resource usage multiplier (default: 1.0). 277 | Resource float32 `protobuf:"fixed32,5,opt,name=resource,proto3" json:"resource,omitempty"` 278 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 279 | XXX_unrecognized []byte `json:"-"` 280 | XXX_sizecache int32 `json:"-"` 281 | } 282 | 283 | func (m *HintFile_Rule) Reset() { *m = HintFile_Rule{} } 284 | func (m *HintFile_Rule) String() string { return proto.CompactTextString(m) } 285 | func (*HintFile_Rule) ProtoMessage() {} 286 | func (*HintFile_Rule) Descriptor() ([]byte, []int) { 287 | return fileDescriptor_test_case_044fbc67b1ebf751, []int{2, 0} 288 | } 289 | func (m *HintFile_Rule) XXX_Unmarshal(b []byte) error { 290 | return xxx_messageInfo_HintFile_Rule.Unmarshal(m, b) 291 | } 292 | func (m *HintFile_Rule) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 293 | return xxx_messageInfo_HintFile_Rule.Marshal(b, m, deterministic) 294 | } 295 | func (dst *HintFile_Rule) XXX_Merge(src proto.Message) { 296 | xxx_messageInfo_HintFile_Rule.Merge(dst, src) 297 | } 298 | func (m *HintFile_Rule) XXX_Size() int { 299 | return xxx_messageInfo_HintFile_Rule.Size(m) 300 | } 301 | func (m *HintFile_Rule) XXX_DiscardUnknown() { 302 | xxx_messageInfo_HintFile_Rule.DiscardUnknown(m) 303 | } 304 | 305 | var xxx_messageInfo_HintFile_Rule proto.InternalMessageInfo 306 | 307 | func (m *HintFile_Rule) GetName() string { 308 | if m != nil { 309 | return m.Name 310 | } 311 | return "" 312 | } 313 | 314 | func (m *HintFile_Rule) GetDeadline() float32 { 315 | if m != nil { 316 | return m.Deadline 317 | } 318 | return 0 319 | } 320 | 321 | func (m *HintFile_Rule) GetXdist() int32 { 322 | if m != nil { 323 | return m.Xdist 324 | } 325 | return 0 326 | } 327 | 328 | func (m *HintFile_Rule) GetRetry() int32 { 329 | if m != nil { 330 | return m.Retry 331 | } 332 | return 0 333 | } 334 | 335 | func (m *HintFile_Rule) GetResource() float32 { 336 | if m != nil { 337 | return m.Resource 338 | } 339 | return 0 340 | } 341 | 342 | func init() { 343 | proto.RegisterType((*TestQuery)(nil), "xpytest.proto.TestQuery") 344 | proto.RegisterType((*TestResult)(nil), "xpytest.proto.TestResult") 345 | proto.RegisterType((*HintFile)(nil), "xpytest.proto.HintFile") 346 | proto.RegisterType((*HintFile_Rule)(nil), "xpytest.proto.HintFile.Rule") 347 | proto.RegisterEnum("xpytest.proto.TestResult_Status", TestResult_Status_name, TestResult_Status_value) 348 | } 349 | 350 | func init() { 351 | proto.RegisterFile("xpytest/proto/test_case.proto", fileDescriptor_test_case_044fbc67b1ebf751) 352 | } 353 | 354 | var fileDescriptor_test_case_044fbc67b1ebf751 = []byte{ 355 | // 407 bytes of a gzipped FileDescriptorProto 356 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x51, 0x4d, 0x8b, 0xd4, 0x40, 357 | 0x10, 0xb5, 0xf3, 0xe5, 0xa4, 0x56, 0x25, 0x34, 0x22, 0x61, 0x51, 0x08, 0x39, 0xe5, 0x94, 0x85, 358 | 0xf1, 0x22, 0x78, 0x1a, 0xd6, 0x0c, 0x0e, 0x3b, 0x66, 0xb1, 0x93, 0x41, 0x3c, 0x2d, 0x71, 0x53, 359 | 0x42, 0x20, 0x3b, 0x19, 0xba, 0x3b, 0xb8, 0xb9, 0xf8, 0x57, 0xfc, 0x93, 0xde, 0xbc, 0x48, 0x75, 360 | 0xb2, 0xe3, 0x8c, 0x38, 0xe0, 0xad, 0x5e, 0xf5, 0xab, 0xea, 0xf7, 0xea, 0xc1, 0xab, 0xfb, 0xdd, 361 | 0xa0, 0x51, 0xe9, 0x8b, 0x9d, 0xec, 0x74, 0x77, 0x41, 0xe5, 0xcd, 0x6d, 0xa5, 0x30, 0x35, 0x98, 362 | 0x3f, 0x9d, 0x9e, 0x47, 0x18, 0xff, 0x60, 0xe0, 0x97, 0xa8, 0xf4, 0xc7, 0x1e, 0xe5, 0xc0, 0x39, 363 | 0x38, 0x5f, 0x9b, 0x16, 0x43, 0x16, 0xb1, 0xc4, 0x17, 0xa6, 0xe6, 0xe7, 0x30, 0xdb, 0xc9, 0xa6, 364 | 0x93, 0x8d, 0x1e, 0x42, 0x2b, 0x62, 0x89, 0x2b, 0xf6, 0x98, 0xde, 0x6a, 0xac, 0xea, 0xb6, 0xd9, 365 | 0x62, 0x68, 0x47, 0x2c, 0xb1, 0xc4, 0x1e, 0xf3, 0xe7, 0xe0, 0xde, 0xd7, 0x8d, 0xd2, 0xa1, 0x63, 366 | 0x86, 0x46, 0x40, 0x5d, 0x89, 0x5a, 0x0e, 0xa1, 0x3b, 0x76, 0x0d, 0xa0, 0x3d, 0x12, 0x55, 0xd7, 367 | 0xcb, 0x5b, 0x0c, 0xbd, 0x71, 0xcf, 0x03, 0x8e, 0x7f, 0x32, 0x00, 0x52, 0x28, 0x50, 0xf5, 0xad, 368 | 0xe6, 0x6f, 0xc0, 0x53, 0xba, 0xd2, 0xbd, 0x32, 0x22, 0x9f, 0xcd, 0xa3, 0xf4, 0xc8, 0x50, 0xfa, 369 | 0x87, 0x9a, 0x16, 0x86, 0x27, 0x26, 0x3e, 0x99, 0xdb, 0x56, 0x77, 0x68, 0x4c, 0xf8, 0xc2, 0xd4, 370 | 0xfc, 0x05, 0x6d, 0xab, 0xbb, 0x5e, 0x1b, 0xf9, 0xbe, 0x98, 0xd0, 0xd4, 0x47, 0x29, 0x8d, 0xfa, 371 | 0xb1, 0x8f, 0x52, 0xd2, 0x0e, 0xdd, 0xdc, 0xa1, 0x51, 0x6f, 0x09, 0x53, 0xc7, 0x25, 0x78, 0xe3, 372 | 0x4f, 0xfc, 0x0c, 0x1e, 0x6f, 0xf2, 0xab, 0xfc, 0xfa, 0x53, 0x1e, 0x3c, 0x22, 0x50, 0x6c, 0x2e, 373 | 0x2f, 0xb3, 0xa2, 0x08, 0x18, 0x7f, 0x02, 0xb3, 0x55, 0x5e, 0x66, 0x22, 0x5f, 0xac, 0x03, 0x8b, 374 | 0x03, 0x78, 0xcb, 0xc5, 0x6a, 0x9d, 0xbd, 0x0b, 0x6c, 0xa2, 0x95, 0xab, 0x0f, 0xd9, 0xf5, 0xa6, 375 | 0x0c, 0x1c, 0xee, 0x83, 0xbb, 0x5c, 0x2f, 0xae, 0x3e, 0x07, 0x6e, 0xfc, 0x8b, 0xc1, 0xec, 0x7d, 376 | 0xb3, 0xd5, 0x4b, 0xca, 0xe0, 0x2d, 0x80, 0x6a, 0xbb, 0x6f, 0x37, 0xe4, 0x93, 0x8c, 0xdb, 0xc9, 377 | 0xd9, 0xfc, 0xe5, 0x5f, 0xc6, 0x1f, 0xc8, 0xa9, 0xe8, 0x5b, 0x14, 0x3e, 0xf1, 0xe9, 0x16, 0x8a, 378 | 0xcf, 0xc1, 0x95, 0x7d, 0x8b, 0x2a, 0xb4, 0xfe, 0x63, 0x6e, 0xa4, 0x9e, 0x7f, 0x07, 0x87, 0xe0, 379 | 0xfe, 0x66, 0xec, 0xe0, 0x66, 0x87, 0xa1, 0x5b, 0xa7, 0x42, 0xb7, 0xff, 0x19, 0xba, 0x73, 0x2a, 380 | 0x74, 0xf7, 0x38, 0xf4, 0x2f, 0x9e, 0xd1, 0xf6, 0xfa, 0x77, 0x00, 0x00, 0x00, 0xff, 0xff, 0x1e, 381 | 0x55, 0x16, 0xfe, 0xcd, 0x02, 0x00, 0x00, 382 | } 383 | --------------------------------------------------------------------------------