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 <andybons@golang.org>
Reviewed-by: Yury Smolsky <yury@smolsky.by>
diff --git a/README.md b/README.md
index e900ea6..33bdcfc 100644
--- a/README.md
+++ b/README.md
@@ -26,10 +26,19 @@
 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 @@
 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/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-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-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/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 @@
 	"go/token"
 	"io"
 	"io/ioutil"
-	stdlog "log"
 	"net/http"
 	"os"
 	"os/exec"
 	"path/filepath"
-	"reflect"
 	"runtime"
 	"strconv"
 	"strings"
@@ -38,7 +36,8 @@
 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 @@
 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 @@
 	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 @@
 		// 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 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 @@
 	}
 
 	// 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 @@
 	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 @@
 	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 @@
 			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 @@
 
 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 @@
 // 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 @@
 	}
 	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 @@
 	// message before any compile errors; strip it.
 	errs = strings.Replace(errs, "# command-line-arguments\n", "", 1)
 
-	return &response{Errors: errs}, nil
+	return errs, nil
 }