From 1cc919a72964db4e1eda88259a83004bfd1ddfad Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 10 May 2019 18:19:28 +0000 Subject: [PATCH] playground: let clients request vet check in same HTTP request as compile+run Also, move the tests to their own file and extend them a bit, give them names, and make the test step more verbose (only visible in docker build anyway). They were hogging up the sandbox file. Updates golang/go#31970 Change-Id: Id710ea613c77a5b16cc5e79545c0812d0f4650e3 Reviewed-on: https://go-review.googlesource.com/c/playground/+/176598 Reviewed-by: Andrew Bonventre Reviewed-by: Yury Smolsky --- README.md | 11 +- go.mod | 3 +- go.sum | 10 +- sandbox.go | 404 +++++------------------------------------------- tests.go | 444 +++++++++++++++++++++++++++++++++++++++++++++++++++++ vet.go | 38 ++++- 6 files changed, 537 insertions(+), 373 deletions(-) create mode 100644 tests.go diff --git a/README.md b/README.md index e900ea6..33bdcfc 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,19 @@ Building the playground Docker container takes more than the default 10 minute t gcloud config set app/cloud_build_timeout 1200 # 20 mins ``` +Alternatively, to avoid Cloud Build and build locally: + +``` +make docker +docker tag playground:latest gcr.io/golang-org/playground:latest +docker push gcr.io/golang-org/playground:latest +gcloud --project=golang-org --account=you@google.com app deploy app.yaml --image-url=gcr.io/golang-org/playground:latest +``` + Then: ``` -gcloud --project=golang-org --account=person@example.com app deploy app.yaml +gcloud --project=golang-org --account=you@google.com app deploy app.yaml ``` # Contributing diff --git a/go.mod b/go.mod index 522d24a..f68be74 100644 --- a/go.mod +++ b/go.mod @@ -5,5 +5,6 @@ go 1.12 require ( cloud.google.com/go v0.38.0 github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668 - golang.org/x/tools v0.0.0-20190509153222-73554e0f7805 + golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 // indirect + golang.org/x/tools v0.0.0-20190513214131-2a413a02cc73 ) diff --git a/go.sum b/go.sum index 9bf9295..8e9d91d 100644 --- a/go.sum +++ b/go.sum @@ -6,12 +6,14 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668 h1:U/lr3Dgy4WK+hNk4tyD+nuGjpVLPEHuJSFXMw11/HPA= github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -33,6 +35,8 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 h1:6M3SDHlHHDCx2PcQw3S4KsR170vGqDhJDOmpVd4Hjak= +golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -40,6 +44,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= @@ -51,11 +56,12 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190509153222-73554e0f7805 h1:1ufBXAsTpUhSmmPXEEs5PrGQSfnBhsjAd2SmVhp9xrY= -golang.org/x/tools v0.0.0-20190509153222-73554e0f7805/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190513214131-2a413a02cc73 h1:zHwPzzQF2U6W4cSM2929cb7MvpB6dLYu9dHwYjOv+ag= +golang.org/x/tools v0.0.0-20190513214131-2a413a02cc73/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= google.golang.org/api v0.4.0 h1:KKgc1aqhV8wDPbDzlDtpvyjZFY3vjz85FP7p4wcQUyI= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= diff --git a/sandbox.go b/sandbox.go index 56ab9e3..56e9613 100644 --- a/sandbox.go +++ b/sandbox.go @@ -19,12 +19,10 @@ import ( "go/token" "io" "io/ioutil" - stdlog "log" "net/http" "os" "os/exec" "path/filepath" - "reflect" "runtime" "strconv" "strings" @@ -38,7 +36,8 @@ import ( const ( maxRunTime = 2 * time.Second - // progName is the program name in compiler errors + // progName is the implicit program name written to the temp + // dir and used in compiler and vet errors. progName = "prog.go" ) @@ -47,7 +46,8 @@ const ( var nonCachingErrors = []string{"out of memory", "cannot allocate memory"} type request struct { - Body string + Body string + WithVet bool // whether client supports vet response in a /compile request (Issue 31970) } type response struct { @@ -56,6 +56,14 @@ type response struct { Status int IsTest bool TestsFailed int + + // VetErrors, if non-empty, contains any vet errors. It is + // only populated if request.WithVet was true. + VetErrors string `json:",omitempty"` + // VetOK reports whether vet ran & passsed. It is only + // populated if request.WithVet was true. Only one of + // VetErrors or VetOK can be non-zero. + VetOK bool `json:",omitempty"` } // commandHandler returns an http.HandlerFunc. @@ -77,6 +85,7 @@ func (s *server) commandHandler(cachePrefix string, cmdFunc func(*request) (*res // are updated to always send JSON, this check is in place. if b := r.FormValue("body"); b != "" { req.Body = b + req.WithVet, _ = strconv.ParseBool(r.FormValue("withVet")) } else if err := json.NewDecoder(r.Body).Decode(&req); err != nil { s.log.Errorf("error decoding request: %v", err) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) @@ -182,7 +191,7 @@ func isTest(name, prefix string) bool { func getTestProg(src []byte) []byte { fset := token.NewFileSet() // Early bail for most cases. - f, err := parser.ParseFile(fset, "main.go", src, parser.ImportsOnly) + f, err := parser.ParseFile(fset, progName, src, parser.ImportsOnly) if err != nil || f.Name.Name != "main" { return nil } @@ -199,7 +208,7 @@ func getTestProg(src []byte) []byte { } // Parse everything and extract test names. - f, err = parser.ParseFile(fset, "main.go", src, parser.ParseComments) + f, err = parser.ParseFile(fset, progName, src, parser.ParseComments) if err != nil { return nil } @@ -303,7 +312,7 @@ func compileAndRun(req *request) (*response, error) { defer os.RemoveAll(tmpDir) src := []byte(req.Body) - in := filepath.Join(tmpDir, "main.go") + in := filepath.Join(tmpDir, progName) if err := ioutil.WriteFile(in, src, 0400); err != nil { return nil, fmt.Errorf("error creating temp file %q: %v", in, err) } @@ -326,29 +335,29 @@ func compileAndRun(req *request) (*response, error) { exe := filepath.Join(tmpDir, "a.out") goCache := filepath.Join(tmpDir, "gocache") cmd := exec.Command("go", "build", "-o", exe, in) + var goPath string cmd.Env = []string{"GOOS=nacl", "GOARCH=amd64p32", "GOCACHE=" + goCache} - if allowModuleDownloads(src) { + useModules := allowModuleDownloads(src) + if useModules { // Create a GOPATH just for modules to be downloaded // into GOPATH/pkg/mod. - gopath, err := ioutil.TempDir("", "gopath") + goPath, err = ioutil.TempDir("", "gopath") if err != nil { return nil, fmt.Errorf("error creating temp directory: %v", err) } - defer os.RemoveAll(gopath) - cmd.Env = append(cmd.Env, "GO111MODULE=on", "GOPROXY=https://proxy.golang.org", "GOPATH="+gopath) + defer os.RemoveAll(goPath) + cmd.Env = append(cmd.Env, "GO111MODULE=on", "GOPROXY=https://proxy.golang.org") } else { - - cmd.Env = append(cmd.Env, - "GO111MODULE=off", // in case it becomes on by default later - "GOPATH="+os.Getenv("GOPATH"), // contains old code.google.com/p/go-tour, etc - ) + goPath = os.Getenv("GOPATH") // contains old code.google.com/p/go-tour, etc + cmd.Env = append(cmd.Env, "GO111MODULE=off") // in case it becomes on by default later } + cmd.Env = append(cmd.Env, "GOPATH="+goPath) if out, err := cmd.CombinedOutput(); err != nil { if _, ok := err.(*exec.ExitError); ok { // Return compile errors to the user. // Rewrite compiler errors to refer to progName - // instead of '/tmp/sandbox1234/main.go'. + // instead of '/tmp/sandbox1234/prog.go'. errs := strings.Replace(string(out), in, progName, -1) // "go build", invoked with a file name, puts this odd @@ -394,7 +403,21 @@ func compileAndRun(req *request) (*response, error) { fails += strings.Count(e.Message, failedTestPattern) } } - return &response{Events: events, Status: status, IsTest: testParam != "", TestsFailed: fails}, nil + var vetOut string + if req.WithVet { + vetOut, err = vetCheckInDir(tmpDir, goPath, useModules) + if err != nil { + return nil, fmt.Errorf("running vet: %v", err) + } + } + return &response{ + Events: events, + Status: status, + IsTest: testParam != "", + TestsFailed: fails, + VetErrors: vetOut, + VetOK: req.WithVet && vetOut == "", + }, nil } // allowModuleDownloads reports whether the code snippet in src should be allowed @@ -433,348 +456,3 @@ import "fmt" func main() { fmt.Print("ok") } ` - -func (s *server) test() { - if err := s.healthCheck(); err != nil { - stdlog.Fatal(err) - } - - // Enable module downloads for testing: - defer func(old string) { os.Setenv("ALLOW_PLAY_MODULE_DOWNLOADS", old) }(os.Getenv("ALLOW_PLAY_MODULE_DOWNLOADS")) - os.Setenv("ALLOW_PLAY_MODULE_DOWNLOADS", "true") - - for _, t := range tests { - resp, err := compileAndRun(&request{Body: t.prog}) - if err != nil { - stdlog.Fatal(err) - } - if t.wantEvents != nil { - if !reflect.DeepEqual(resp.Events, t.wantEvents) { - stdlog.Fatalf("resp.Events = %q, want %q", resp.Events, t.wantEvents) - } - continue - } - if t.errors != "" { - if resp.Errors != t.errors { - stdlog.Fatalf("resp.Errors = %q, want %q", resp.Errors, t.errors) - } - continue - } - if resp.Errors != "" { - stdlog.Fatal(resp.Errors) - } - if len(resp.Events) == 0 { - stdlog.Fatalf("unexpected output: %q, want %q", "", t.want) - } - var b strings.Builder - for _, e := range resp.Events { - b.WriteString(e.Message) - } - if !strings.Contains(b.String(), t.want) { - stdlog.Fatalf("unexpected output: %q, want %q", b.String(), t.want) - } - } - fmt.Println("OK") -} - -var tests = []struct { - prog, want, errors string - wantEvents []Event -}{ - {prog: ` -package main - -import "time" - -func main() { - loc, err := time.LoadLocation("America/New_York") - if err != nil { - panic(err.Error()) - } - println(loc.String()) -} -`, want: "America/New_York"}, - - {prog: ` -package main - -import ( - "fmt" - "time" -) - -func main() { - fmt.Println(time.Now()) -} -`, want: "2009-11-10 23:00:00 +0000 UTC"}, - - {prog: ` -package main - -import ( - "fmt" - "time" -) - -func main() { - t1 := time.Tick(time.Second * 3) - t2 := time.Tick(time.Second * 7) - t3 := time.Tick(time.Second * 11) - end := time.After(time.Second * 19) - want := "112131211" - var got []byte - for { - var c byte - select { - case <-t1: - c = '1' - case <-t2: - c = '2' - case <-t3: - c = '3' - case <-end: - if g := string(got); g != want { - fmt.Printf("got %q, want %q\n", g, want) - } else { - fmt.Println("timers fired as expected") - } - return - } - got = append(got, c) - } -} -`, want: "timers fired as expected"}, - - {prog: ` -package main - -import ( - "code.google.com/p/go-tour/pic" - "code.google.com/p/go-tour/reader" - "code.google.com/p/go-tour/tree" - "code.google.com/p/go-tour/wc" -) - -var ( - _ = pic.Show - _ = reader.Validate - _ = tree.New - _ = wc.Test -) - -func main() { - println("ok") -} -`, want: "ok"}, - {prog: ` -package test - -func main() { - println("test") -} -`, want: "", errors: "package name must be main"}, - {prog: ` -package main - -import ( - "fmt" - "os" - "path/filepath" -) - -func main() { - filepath.Walk("/", func(path string, info os.FileInfo, err error) error { - fmt.Println(path) - return nil - }) -} -`, want: `/ -/dev -/dev/null -/dev/random -/dev/urandom -/dev/zero -/etc -/etc/group -/etc/hosts -/etc/passwd -/etc/resolv.conf -/tmp -/usr -/usr/local -/usr/local/go -/usr/local/go/lib -/usr/local/go/lib/time -/usr/local/go/lib/time/zoneinfo.zip`}, - {prog: ` -package main - -import "testing" - -func TestSanity(t *testing.T) { - if 1+1 != 2 { - t.Error("uhh...") - } -} -`, want: `=== RUN TestSanity ---- PASS: TestSanity (0.00s) -PASS`}, - - {prog: ` -package main - -func TestSanity(t *testing.T) { - t.Error("uhh...") -} - -func ExampleNotExecuted() { - // Output: it should not run -} -`, want: "", errors: "prog.go:4:20: undefined: testing\n"}, - - {prog: ` -package main - -import ( - "fmt" - "testing" -) - -func TestSanity(t *testing.T) { - t.Error("uhh...") -} - -func main() { - fmt.Println("test") -} -`, want: "test"}, - - {prog: ` -package main//comment - -import "fmt" - -func ExampleOutput() { - fmt.Println("The output") - // Output: The output -} -`, want: `=== RUN ExampleOutput ---- PASS: ExampleOutput (0.00s) -PASS`}, - - {prog: ` -package main//comment - -import "fmt" - -func ExampleUnorderedOutput() { - fmt.Println("2") - fmt.Println("1") - fmt.Println("3") - // Unordered output: 3 - // 2 - // 1 -} -`, want: `=== RUN ExampleUnorderedOutput ---- PASS: ExampleUnorderedOutput (0.00s) -PASS`}, - - {prog: ` -package main - -import "fmt" - -func ExampleEmptyOutput() { - // Output: -} - -func ExampleEmptyOutputFail() { - fmt.Println("1") - // Output: -} -`, want: `=== RUN ExampleEmptyOutput ---- PASS: ExampleEmptyOutput (0.00s) -=== RUN ExampleEmptyOutputFail ---- FAIL: ExampleEmptyOutputFail (0.00s) -got: -1 -want: - -FAIL`}, - - // Run program without executing this example function. - {prog: ` -package main - -func ExampleNoOutput() { - panic(1) -} -`, want: `testing: warning: no tests to run -PASS`}, - - {prog: ` -package main - -import "fmt" - -func ExampleShouldNotRun() { - fmt.Println("The output") - // Output: The output -} - -func main() { - fmt.Println("Main") -} -`, want: "Main"}, - - {prog: ` -package main - -import ( - "fmt" - "os" -) - -func main() { - fmt.Fprintln(os.Stdout, "A") - fmt.Fprintln(os.Stderr, "B") - fmt.Fprintln(os.Stdout, "A") - fmt.Fprintln(os.Stdout, "A") -} -`, want: "A\nB\nA\nA\n"}, - - // Integration test for runtime.write fake timestamps. - {prog: ` -package main - -import ( - "fmt" - "os" - "time" -) - -func main() { - fmt.Fprintln(os.Stdout, "A") - fmt.Fprintln(os.Stderr, "B") - fmt.Fprintln(os.Stdout, "A") - fmt.Fprintln(os.Stdout, "A") - time.Sleep(time.Second) - fmt.Fprintln(os.Stderr, "B") - time.Sleep(time.Second) - fmt.Fprintln(os.Stdout, "A") -} -`, wantEvents: []Event{ - {"A\n", "stdout", 0}, - {"B\n", "stderr", time.Nanosecond}, - {"A\nA\n", "stdout", time.Nanosecond}, - {"B\n", "stderr", time.Second - 2*time.Nanosecond}, - {"A\n", "stdout", time.Second}, - }}, - - // Test third-party imports: - {prog: ` -package main -import ("fmt"; "github.com/bradfitz/iter") -func main() { for i := range iter.N(5) { fmt.Println(i) } } -`, want: "0\n1\n2\n3\n4\n"}, -} diff --git a/tests.go b/tests.go new file mode 100644 index 0000000..437da2b --- /dev/null +++ b/tests.go @@ -0,0 +1,444 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Test tests are linked into the main binary and are run as part of +// the Docker build step. + +package main + +import ( + "fmt" + stdlog "log" + "os" + "reflect" + "strings" + "time" +) + +type compileTest struct { + name string // test name + prog, want, errors string + withVet bool + wantEvents []Event + wantVetErrors string +} + +func (s *server) test() { + if err := s.healthCheck(); err != nil { + stdlog.Fatal(err) + } + + // Enable module downloads for testing: + defer func(old string) { os.Setenv("ALLOW_PLAY_MODULE_DOWNLOADS", old) }(os.Getenv("ALLOW_PLAY_MODULE_DOWNLOADS")) + os.Setenv("ALLOW_PLAY_MODULE_DOWNLOADS", "true") + + for i, t := range tests { + fmt.Printf("testing case %d (%q)...\n", i, t.name) + resp, err := compileAndRun(&request{Body: t.prog, WithVet: t.withVet}) + if err != nil { + stdlog.Fatal(err) + } + if t.wantEvents != nil { + if !reflect.DeepEqual(resp.Events, t.wantEvents) { + stdlog.Fatalf("resp.Events = %q, want %q", resp.Events, t.wantEvents) + } + continue + } + if t.errors != "" { + if resp.Errors != t.errors { + stdlog.Fatalf("resp.Errors = %q, want %q", resp.Errors, t.errors) + } + continue + } + if resp.Errors != "" { + stdlog.Fatal(resp.Errors) + } + if resp.VetErrors != t.wantVetErrors { + stdlog.Fatalf("resp.VetErrs = %q, want %q", resp.VetErrors, t.wantVetErrors) + } + if len(resp.Events) == 0 { + stdlog.Fatalf("unexpected output: %q, want %q", "", t.want) + } + var b strings.Builder + for _, e := range resp.Events { + b.WriteString(e.Message) + } + if !strings.Contains(b.String(), t.want) { + stdlog.Fatalf("unexpected output: %q, want %q", b.String(), t.want) + } + } + fmt.Println("OK") +} + +var tests = []compileTest{ + { + name: "timezones_available", + prog: ` +package main + +import "time" + +func main() { + loc, err := time.LoadLocation("America/New_York") + if err != nil { + panic(err.Error()) + } + println(loc.String()) +} +`, want: "America/New_York"}, + + { + name: "faketime_works", + prog: ` +package main + +import ( + "fmt" + "time" +) + +func main() { + fmt.Println(time.Now()) +} +`, want: "2009-11-10 23:00:00 +0000 UTC"}, + + { + name: "faketime_tickers", + prog: ` +package main + +import ( + "fmt" + "time" +) + +func main() { + t1 := time.Tick(time.Second * 3) + t2 := time.Tick(time.Second * 7) + t3 := time.Tick(time.Second * 11) + end := time.After(time.Second * 19) + want := "112131211" + var got []byte + for { + var c byte + select { + case <-t1: + c = '1' + case <-t2: + c = '2' + case <-t3: + c = '3' + case <-end: + if g := string(got); g != want { + fmt.Printf("got %q, want %q\n", g, want) + } else { + fmt.Println("timers fired as expected") + } + return + } + got = append(got, c) + } +} +`, want: "timers fired as expected"}, + + { + name: "old_tour_pkgs_in_gopath", + prog: ` +package main + +import ( + "code.google.com/p/go-tour/pic" + "code.google.com/p/go-tour/reader" + "code.google.com/p/go-tour/tree" + "code.google.com/p/go-tour/wc" +) + +var ( + _ = pic.Show + _ = reader.Validate + _ = tree.New + _ = wc.Test +) + +func main() { + println("ok") +} +`, want: "ok"}, + { + name: "must_be_package_main", + prog: ` +package test + +func main() { + println("test") +} +`, want: "", errors: "package name must be main"}, + { + name: "filesystem_contents", + prog: ` +package main + +import ( + "fmt" + "os" + "path/filepath" +) + +func main() { + filepath.Walk("/", func(path string, info os.FileInfo, err error) error { + fmt.Println(path) + return nil + }) +} +`, want: `/ +/dev +/dev/null +/dev/random +/dev/urandom +/dev/zero +/etc +/etc/group +/etc/hosts +/etc/passwd +/etc/resolv.conf +/tmp +/usr +/usr/local +/usr/local/go +/usr/local/go/lib +/usr/local/go/lib/time +/usr/local/go/lib/time/zoneinfo.zip`}, + + { + name: "test_passes", + prog: ` +package main + +import "testing" + +func TestSanity(t *testing.T) { + if 1+1 != 2 { + t.Error("uhh...") + } +} +`, want: `=== RUN TestSanity +--- PASS: TestSanity (0.00s) +PASS`}, + + { + name: "test_without_import", + prog: ` +package main + +func TestSanity(t *testing.T) { + t.Error("uhh...") +} + +func ExampleNotExecuted() { + // Output: it should not run +} +`, want: "", errors: "prog.go:4:20: undefined: testing\n"}, + + { + name: "test_with_import_ignored", + prog: ` +package main + +import ( + "fmt" + "testing" +) + +func TestSanity(t *testing.T) { + t.Error("uhh...") +} + +func main() { + fmt.Println("test") +} +`, want: "test"}, + + { + name: "example_runs", + prog: ` +package main//comment + +import "fmt" + +func ExampleOutput() { + fmt.Println("The output") + // Output: The output +} +`, want: `=== RUN ExampleOutput +--- PASS: ExampleOutput (0.00s) +PASS`}, + + { + name: "example_unordered", + prog: ` +package main//comment + +import "fmt" + +func ExampleUnorderedOutput() { + fmt.Println("2") + fmt.Println("1") + fmt.Println("3") + // Unordered output: 3 + // 2 + // 1 +} +`, want: `=== RUN ExampleUnorderedOutput +--- PASS: ExampleUnorderedOutput (0.00s) +PASS`}, + + { + name: "example_fail", + prog: ` +package main + +import "fmt" + +func ExampleEmptyOutput() { + // Output: +} + +func ExampleEmptyOutputFail() { + fmt.Println("1") + // Output: +} +`, want: `=== RUN ExampleEmptyOutput +--- PASS: ExampleEmptyOutput (0.00s) +=== RUN ExampleEmptyOutputFail +--- FAIL: ExampleEmptyOutputFail (0.00s) +got: +1 +want: + +FAIL`}, + + // Run program without executing this example function. + { + name: "example_no_output_skips_run", + prog: ` +package main + +func ExampleNoOutput() { + panic(1) +} +`, want: `testing: warning: no tests to run +PASS`}, + + { + name: "example_output", + prog: ` +package main + +import "fmt" + +func ExampleShouldNotRun() { + fmt.Println("The output") + // Output: The output +} + +func main() { + fmt.Println("Main") +} +`, want: "Main"}, + + { + name: "stdout_stderr_merge", + prog: ` +package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Fprintln(os.Stdout, "A") + fmt.Fprintln(os.Stderr, "B") + fmt.Fprintln(os.Stdout, "A") + fmt.Fprintln(os.Stdout, "A") +} +`, want: "A\nB\nA\nA\n"}, + + // Integration test for runtime.write fake timestamps. + { + name: "faketime_write_interaction", + prog: ` +package main + +import ( + "fmt" + "os" + "time" +) + +func main() { + fmt.Fprintln(os.Stdout, "A") + fmt.Fprintln(os.Stderr, "B") + fmt.Fprintln(os.Stdout, "A") + fmt.Fprintln(os.Stdout, "A") + time.Sleep(time.Second) + fmt.Fprintln(os.Stderr, "B") + time.Sleep(time.Second) + fmt.Fprintln(os.Stdout, "A") +} +`, wantEvents: []Event{ + {"A\n", "stdout", 0}, + {"B\n", "stderr", time.Nanosecond}, + {"A\nA\n", "stdout", time.Nanosecond}, + {"B\n", "stderr", time.Second - 2*time.Nanosecond}, + {"A\n", "stdout", time.Second}, + }}, + + { + name: "third_party_imports", + prog: ` +package main +import ("fmt"; "github.com/bradfitz/iter") +func main() { for i := range iter.N(5) { fmt.Println(i) } } +`, want: "0\n1\n2\n3\n4\n"}, + + { + name: "compile_with_vet", + withVet: true, + wantVetErrors: "prog.go:5:2: Printf format %v reads arg #1, but call has 0 args\n", + prog: ` +package main +import "fmt" +func main() { + fmt.Printf("hi %v") +} +`, + }, + + { + name: "compile_without_vet", + withVet: false, + prog: ` +package main +import "fmt" +func main() { + fmt.Printf("hi %v") +} +`, + }, + + { + name: "compile_modules_with_vet", + withVet: true, + wantVetErrors: "prog.go:6:2: Printf format %v reads arg #1, but call has 0 args\n", + prog: ` +package main +import ("fmt"; "github.com/bradfitz/iter") +func main() { + for i := range iter.N(5) { fmt.Println(i) } + fmt.Printf("hi %v") +} +`, + }, +} diff --git a/vet.go b/vet.go index b4925db..78df95c 100644 --- a/vet.go +++ b/vet.go @@ -16,6 +16,11 @@ import ( // vetCheck runs the "vet" tool on the source code in req.Body. // In case of no errors it returns an empty, non-nil *response. // Otherwise &response.Errors contains found errors. +// +// Deprecated: this is the handler for the legacy /vet endpoint; use +// the /compile (compileAndRun) handler instead with the WithVet +// boolean set. This code path doesn't support modules and only exists +// as a temporary compatiblity bridge to older javascript clients. func vetCheck(req *request) (*response, error) { tmpDir, err := ioutil.TempDir("", "vet") if err != nil { @@ -23,23 +28,44 @@ func vetCheck(req *request) (*response, error) { } defer os.RemoveAll(tmpDir) - in := filepath.Join(tmpDir, "main.go") + in := filepath.Join(tmpDir, progName) if err := ioutil.WriteFile(in, []byte(req.Body), 0400); err != nil { return nil, fmt.Errorf("error creating temp file %q: %v", in, err) } + const useModules = false // legacy handler; no modules (see func comment) + vetOutput, err := vetCheckInDir(tmpDir, os.Getenv("GOPATH"), useModules) + if err != nil { + // This is about errors running vet, not vet returning output. + return nil, err + } + return &response{Errors: vetOutput}, nil +} +// vetCheckInDir runs go vet in the provided directory, using the +// provided GOPATH value, and whether modules are enabled. The +// returned error is only about whether go vet was able to run, not +// whether vet reported problem. The returned value is ("", nil) if +// vet successfully found nothing, and (non-empty, nil) if vet ran and +// found issues. +func vetCheckInDir(dir, goPath string, modules bool) (output string, execErr error) { + in := filepath.Join(dir, progName) cmd := exec.Command("go", "vet", in) // Linux go binary is not built with CGO_ENABLED=0. // Prevent vet to compile packages in cgo mode. // See #26307. - cmd.Env = append(os.Environ(), "CGO_ENABLED=0") + cmd.Env = append(os.Environ(), "CGO_ENABLED=0", "GOPATH="+goPath) + if modules { + cmd.Env = append(cmd.Env, + "GO111MODULE=on", + "GOPROXY=https://proxy.golang.org", + ) + } out, err := cmd.CombinedOutput() if err == nil { - return &response{}, nil + return "", nil } - if _, ok := err.(*exec.ExitError); !ok { - return nil, fmt.Errorf("error vetting go source: %v", err) + return "", fmt.Errorf("error vetting go source: %v", err) } // Rewrite compiler errors to refer to progName @@ -50,5 +76,5 @@ func vetCheck(req *request) (*response, error) { // message before any compile errors; strip it. errs = strings.Replace(errs, "# command-line-arguments\n", "", 1) - return &response{Errors: errs}, nil + return errs, nil }