test.bash: rewrite the integration test in Go

This CL switches the integration test to be written in Go instead of bash.
The benefits are:
* The logic for setting up the dependencies is more robust and
handles better a situation where the dependency failed to initialize
(due to network trouble, FS permission problems, etc).
* The logic does a better job at cleaning up stale dependencies.
For example, my .cache folder has go1.9.4, go1.9.5, go1.10.5, and go1.10.6
folders even though we don't use them anymore.
* Being able to run only a subset of the integration test since you can
pass "-run" to the script.
* A signifcant amount of complexity in the test.bash script was running
the tests in parallel. This is trivial to do in Go.

The major detriment is that Go is more verbose as a "scripting" language.

Change-Id: Id7e5b303fb305fdbc0368bdf809dbf29fca1d983
Reviewed-on: https://go-review.googlesource.com/c/164861
Reviewed-by: Herbie Ong <herbie@google.com>
diff --git a/integration_test.go b/integration_test.go
new file mode 100644
index 0000000..2115ff1
--- /dev/null
+++ b/integration_test.go
@@ -0,0 +1,312 @@
+// Copyright 2019 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.
+
+// +build integration
+
+package protobuf
+
+import (
+	"archive/tar"
+	"bytes"
+	"compress/gzip"
+	"flag"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"regexp"
+	"runtime"
+	"strings"
+	"testing"
+)
+
+var (
+	regenerate = flag.Bool("regenerate", false, "regenerate files")
+
+	protobufVersion = "3.6.1"
+	golangVersions  = []string{"1.9.7", "1.10.8", "1.11.5", "1.12"}
+	golangLatest    = golangVersions[len(golangVersions)-1]
+
+	// List of directories to avoid auto-deleting. After updating the versions,
+	// it is worth placing the previous versions here for some time since
+	// other developers may still have working branches using the old versions.
+	keepDirs = []string{"go1.10.7", "go1.11.4"}
+
+	// Variables initialized by mustInitDeps.
+	goPath     string
+	modulePath string
+)
+
+func Test(t *testing.T) {
+	mustInitDeps(t)
+
+	if *regenerate {
+		t.Run("Generate", func(t *testing.T) {
+			fmt.Print(mustRunCommand(t, ".", "go", "run", "./internal/cmd/generate-types", "-execute"))
+			fmt.Print(mustRunCommand(t, ".", "go", "run", "./internal/cmd/generate-protos", "-execute"))
+			files := strings.Split(strings.TrimSpace(mustRunCommand(t, ".", "git", "ls-files", "*.go")), "\n")
+			mustRunCommand(t, ".", append([]string{"gofmt", "-w"}, files...)...)
+		})
+		t.SkipNow()
+	}
+
+	for _, v := range golangVersions {
+		t.Run("Go"+v, func(t *testing.T) {
+			runGo := func(label string, args ...string) {
+				args[0] += v
+				t.Run(label, func(t *testing.T) {
+					t.Parallel()
+					mustRunCommand(t, filepath.Join(goPath, "src", modulePath), args...)
+				})
+			}
+			// TODO: "go build" does not descend into testdata,
+			// which means that generated .pb.go files are not being built.
+			runGo("Build", "go", "build", "./...")
+			runGo("TestNormal", "go", "test", "-race", "./...")
+			runGo("TestPureGo", "go", "test", "-race", "-tags", "purego", "./...")
+			if v == golangLatest {
+				runGo("TestLegacy", "go", "test", "-race", "-tags", "proto1_legacy", "./...")
+			}
+		})
+	}
+
+	t.Run("GeneratedGoFiles", func(t *testing.T) {
+		diff := mustRunCommand(t, ".", "go", "run", "./internal/cmd/generate-types")
+		if strings.TrimSpace(diff) != "" {
+			t.Fatalf("stale generated files:\n%v", diff)
+		}
+		diff = mustRunCommand(t, ".", "go", "run", "./internal/cmd/generate-protos")
+		if strings.TrimSpace(diff) != "" {
+			t.Fatalf("stale generated files:\n%v", diff)
+		}
+	})
+	t.Run("FormattedGoFiles", func(t *testing.T) {
+		files := strings.Split(strings.TrimSpace(mustRunCommand(t, ".", "git", "ls-files", "*.go")), "\n")
+		diff := mustRunCommand(t, ".", append([]string{"gofmt", "-d"}, files...)...)
+		if strings.TrimSpace(diff) != "" {
+			t.Fatalf("unformatted source files:\n%v", diff)
+		}
+	})
+	t.Run("CommittedGitChanges", func(t *testing.T) {
+		diff := mustRunCommand(t, ".", "git", "diff", "--no-prefix", "HEAD")
+		if strings.TrimSpace(diff) != "" {
+			t.Fatalf("uncommitted changes:\n%v", diff)
+		}
+	})
+	t.Run("TrackedGitFiles", func(t *testing.T) {
+		diff := mustRunCommand(t, ".", "git", "ls-files", "--others", "--exclude-standard")
+		if strings.TrimSpace(diff) != "" {
+			t.Fatalf("untracked files:\n%v", diff)
+		}
+	})
+}
+
+func mustInitDeps(t *testing.T) {
+	check := func(err error) {
+		t.Helper()
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	// Determine the directory to place the test directory.
+	repoRoot, err := os.Getwd()
+	check(err)
+	testDir := filepath.Join(repoRoot, ".cache")
+	check(os.MkdirAll(testDir, 0775))
+
+	// Delete the current directory if non-empty,
+	// which only occurs if a dependency failed to initialize properly.
+	var workingDir string
+	defer func() {
+		if workingDir != "" {
+			os.RemoveAll(workingDir) // best-effort
+		}
+	}()
+
+	// Delete other sub-directories that are no longer relevant.
+	defer func() {
+		subDirs := map[string]bool{"bin": true, "gocache": true, "gopath": true}
+		subDirs["protobuf-"+protobufVersion] = true
+		for _, v := range golangVersions {
+			subDirs["go"+v] = true
+		}
+		for _, d := range keepDirs {
+			subDirs[d] = true
+		}
+
+		fis, _ := ioutil.ReadDir(testDir)
+		for _, fi := range fis {
+			if !subDirs[fi.Name()] {
+				fmt.Printf("delete %v\n", fi.Name())
+				os.RemoveAll(filepath.Join(testDir, fi.Name())) // best-effort
+			}
+		}
+	}()
+
+	// The bin directory contains symlinks to each tool by version.
+	// It is safe to delete this directory and run the test script from scratch.
+	binPath := filepath.Join(testDir, "bin")
+	check(os.RemoveAll(binPath))
+	check(os.Mkdir(binPath, 0775))
+	check(os.Setenv("PATH", binPath+":"+os.Getenv("PATH")))
+	registerBinary := func(name, path string) {
+		check(os.Symlink(path, filepath.Join(binPath, name)))
+	}
+
+	// Download and build the protobuf toolchain.
+	// We avoid downloading the pre-compiled binaries since they do not contain
+	// the conformance test runner.
+	workingDir = filepath.Join(testDir, "protobuf-"+protobufVersion)
+	if _, err := os.Stat(workingDir); err != nil {
+		fmt.Printf("download %v\n", filepath.Base(workingDir))
+		url := fmt.Sprintf("https://github.com/google/protobuf/releases/download/v%v/protobuf-all-%v.tar.gz", protobufVersion, protobufVersion)
+		downloadArchive(check, workingDir, url, 1)
+
+		fmt.Printf("build %v\n", filepath.Base(workingDir))
+		mustRunCommand(t, workingDir, "./configure")
+		mustRunCommand(t, workingDir, "make")
+		mustRunCommand(t, filepath.Join(workingDir, "conformance"), "make")
+	}
+	patchProtos(check, workingDir)
+	check(os.Setenv("PROTOBUF_ROOT", workingDir)) // for generate-protos
+	registerBinary("conform-test-runner", filepath.Join(workingDir, "conformance", "conformance-test-runner"))
+	registerBinary("protoc", filepath.Join(workingDir, "src", "protoc"))
+	workingDir = ""
+
+	// Download each Go toolchain version.
+	for _, v := range golangVersions {
+		workingDir = filepath.Join(testDir, "go"+v)
+		if _, err := os.Stat(workingDir); err != nil {
+			fmt.Printf("download %v\n", filepath.Base(workingDir))
+			url := fmt.Sprintf("https://dl.google.com/go/go%v.%v-%v.tar.gz", v, runtime.GOOS, runtime.GOARCH)
+			downloadArchive(check, workingDir, url, 1)
+		}
+		registerBinary("go"+v, filepath.Join(workingDir, "bin", "go"))
+	}
+	registerBinary("go", filepath.Join(testDir, "go"+golangLatest, "bin", "go"))
+	registerBinary("gofmt", filepath.Join(testDir, "go"+golangLatest, "bin", "gofmt"))
+	workingDir = ""
+
+	// Travis-CI sets GOROOT, which confuses invocations of the Go toolchain.
+	// Explicitly clear GOROOT, so each toolchain uses their default GOROOT.
+	check(os.Unsetenv("GOROOT"))
+
+	// Set a cache directory within the test directory.
+	check(os.Setenv("GOCACHE", filepath.Join(testDir, "gocache")))
+
+	// Setup GOPATH for pre-module support (i.e., go1.10 and earlier).
+	goPath = filepath.Join(testDir, "gopath")
+	modulePath = strings.TrimSpace(mustRunCommand(t, testDir, "go", "list", "-m", "-f", "{{.Path}}"))
+	check(os.RemoveAll(filepath.Join(goPath, "src")))
+	check(os.MkdirAll(filepath.Join(goPath, "src", filepath.Dir(modulePath)), 0775))
+	check(os.Symlink(repoRoot, filepath.Join(goPath, "src", modulePath)))
+	mustRunCommand(t, repoRoot, "go", "mod", "tidy")
+	mustRunCommand(t, repoRoot, "go", "mod", "vendor")
+	check(os.Setenv("GOPATH", goPath))
+}
+
+func downloadArchive(check func(error), dstPath, srcURL string, skipPrefixes int) {
+	check(os.RemoveAll(dstPath))
+
+	resp, err := http.Get(srcURL)
+	check(err)
+	defer resp.Body.Close()
+
+	zr, err := gzip.NewReader(resp.Body)
+	check(err)
+
+	tr := tar.NewReader(zr)
+	for {
+		h, err := tr.Next()
+		if err == io.EOF {
+			return
+		}
+		check(err)
+
+		path := strings.Join(strings.Split(h.Name, "/")[skipPrefixes:], "/")
+		path = filepath.Join(dstPath, filepath.FromSlash(path))
+		mode := os.FileMode(h.Mode & 0777)
+		switch h.Typeflag {
+		case tar.TypeReg:
+			b, err := ioutil.ReadAll(tr)
+			check(err)
+			check(ioutil.WriteFile(path, b, mode))
+		case tar.TypeDir:
+			check(os.Mkdir(path, mode))
+		}
+	}
+}
+
+// patchProtos patches proto files with v2 locations of Go packages.
+// TODO: Commit these changes upstream.
+func patchProtos(check func(error), repoRoot string) {
+	javaPackageRx := regexp.MustCompile(`^option\s+java_package\s*=\s*".*"\s*;\s*$`)
+	goPackageRx := regexp.MustCompile(`^option\s+go_package\s*=\s*".*"\s*;\s*$`)
+	files := map[string]string{
+		"src/google/protobuf/any.proto":             "github.com/golang/protobuf/v2/types/known;known_proto",
+		"src/google/protobuf/api.proto":             "github.com/golang/protobuf/v2/types/known;known_proto",
+		"src/google/protobuf/duration.proto":        "github.com/golang/protobuf/v2/types/known;known_proto",
+		"src/google/protobuf/empty.proto":           "github.com/golang/protobuf/v2/types/known;known_proto",
+		"src/google/protobuf/field_mask.proto":      "github.com/golang/protobuf/v2/types/known;known_proto",
+		"src/google/protobuf/source_context.proto":  "github.com/golang/protobuf/v2/types/known;known_proto",
+		"src/google/protobuf/struct.proto":          "github.com/golang/protobuf/v2/types/known;known_proto",
+		"src/google/protobuf/timestamp.proto":       "github.com/golang/protobuf/v2/types/known;known_proto",
+		"src/google/protobuf/type.proto":            "github.com/golang/protobuf/v2/types/known;known_proto",
+		"src/google/protobuf/wrappers.proto":        "github.com/golang/protobuf/v2/types/known;known_proto",
+		"src/google/protobuf/descriptor.proto":      "github.com/golang/protobuf/v2/types/descriptor;descriptor_proto",
+		"src/google/protobuf/compiler/plugin.proto": "github.com/golang/protobuf/v2/types/plugin;plugin_proto",
+		"conformance/conformance.proto":             "github.com/golang/protobuf/v2/internal/testprotos/conformance;conformance_proto",
+	}
+	for pbpath, gopath := range files {
+		b, err := ioutil.ReadFile(filepath.Join(repoRoot, pbpath))
+		check(err)
+		ss := strings.Split(string(b), "\n")
+
+		// Locate java_package and (possible) go_package options.
+		javaPackageIdx, goPackageIdx := -1, -1
+		for i, s := range ss {
+			if javaPackageIdx < 0 && javaPackageRx.MatchString(s) {
+				javaPackageIdx = i
+			}
+			if goPackageIdx < 0 && goPackageRx.MatchString(s) {
+				goPackageIdx = i
+			}
+		}
+
+		// Ensure the proto file has the correct go_package option.
+		opt := `option go_package = "` + gopath + `";`
+		if goPackageIdx >= 0 {
+			if ss[goPackageIdx] == opt {
+				continue // no changes needed
+			}
+			ss[goPackageIdx] = opt
+		} else {
+			// Insert go_package option before java_package option.
+			ss = append(ss[:javaPackageIdx], append([]string{opt}, ss[javaPackageIdx:]...)...)
+		}
+
+		fmt.Println("patch " + pbpath)
+		b = []byte(strings.Join(ss, "\n"))
+		check(ioutil.WriteFile(filepath.Join(repoRoot, pbpath), b, 0664))
+	}
+}
+
+func mustRunCommand(t *testing.T, dir string, args ...string) string {
+	t.Helper()
+	stdout := new(bytes.Buffer)
+	combined := new(bytes.Buffer)
+	cmd := exec.Command(args[0], args[1:]...)
+	cmd.Dir = dir
+	cmd.Env = append(os.Environ(), "PWD="+dir)
+	cmd.Stdout = io.MultiWriter(stdout, combined)
+	cmd.Stderr = combined
+	if err := cmd.Run(); err != nil {
+		t.Fatalf("executing (%v): %v\n%s", strings.Join(args, " "), err, combined.String())
+	}
+	return stdout.String()
+}
diff --git a/regenerate.bash b/regenerate.bash
new file mode 100755
index 0000000..869099e
--- /dev/null
+++ b/regenerate.bash
@@ -0,0 +1,8 @@
+#!/bin/bash
+# Copyright 2018 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.
+
+cd "$(git rev-parse --show-toplevel)"
+go test -v -timeout 60m -tags integration "$@" -regenerate
+exit $?
diff --git a/test.bash b/test.bash
index ad06c41..3a28e59 100755
--- a/test.bash
+++ b/test.bash
@@ -3,208 +3,6 @@
 # Use of this source code is governed by a BSD-style
 # license that can be found in the LICENSE file.
 
-REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-
-function print() { echo -e "\x1b[1m> $@\x1b[0m"; }
-
-# The test directory contains the Go and protobuf toolchains used for testing.
-# The bin directory contains symlinks to each tool by version.
-# It is safe to delete this directory and run the test script from scratch.
-TEST_DIR=$REPO_ROOT/.cache
-mkdir -p $TEST_DIR/bin
-function register_binary() {
-	rm -f $TEST_DIR/bin/$1 # best-effort delete
-	ln -s $TEST_DIR/$2 $TEST_DIR/bin/$1
-}
-export PATH=$TEST_DIR/bin:$PATH
-cd $TEST_DIR # install toolchains relative to test directory
-
-# Download and build the protobuf toolchain.
-# We avoid downloading the pre-compiled binaries since they do not contain
-# the conformance test runner.
-PROTOBUF_VERSION=3.6.1
-PROTOBUF_DIR="protobuf-$PROTOBUF_VERSION"
-if [ ! -d $PROTOBUF_DIR ]; then
-	print "download and build $PROTOBUF_DIR"
-	(curl -s -L https://github.com/google/protobuf/releases/download/v$PROTOBUF_VERSION/protobuf-all-$PROTOBUF_VERSION.tar.gz | tar -zxf -) || exit 1
-	(cd $PROTOBUF_DIR && ./configure && make && cd conformance && make) || exit 1
-fi
-register_binary conformance-test-runner $PROTOBUF_DIR/conformance/conformance-test-runner
-register_binary protoc $PROTOBUF_DIR/src/protoc
-export PROTOBUF_ROOT=$TEST_DIR/$PROTOBUF_DIR
-
-# Patch proto files in the toolchain with new locations of Go packages.
-# TODO: these changes should be committed upstream.
-if ! grep -q "option go_package =" $PROTOBUF_ROOT/conformance/conformance.proto; then
-(cat << EOF
---- a/conformance/conformance.proto	2018-07-30 15:16:10.000000000 -0700
-+++ b/conformance/conformance.proto	2019-01-20 03:03:47.000000000 -0800
-@@ -32,0 +33 @@
-+option go_package = "github.com/golang/protobuf/v2/internal/testprotos/conformance;conformance_proto";
---- a/src/google/protobuf/any.proto	2018-07-23 13:56:42.000000000 -0700
-+++ b/src/google/protobuf/any.proto	2019-01-20 02:58:13.000000000 -0800
-@@ -36 +36 @@
--option go_package = "github.com/golang/protobuf/ptypes/any";
-+option go_package = "github.com/golang/protobuf/v2/types/known;known_proto";
---- a/src/google/protobuf/api.proto	2018-07-23 13:56:42.000000000 -0700
-+++ b/src/google/protobuf/api.proto	2019-01-20 03:00:58.000000000 -0800
-@@ -43 +43 @@
--option go_package = "google.golang.org/genproto/protobuf/api;api";
-+option go_package = "github.com/golang/protobuf/v2/types/known;known_proto";
---- a/src/google/protobuf/compiler/plugin.proto	2018-07-23 13:56:42.000000000 -0700
-+++ b/src/google/protobuf/compiler/plugin.proto	2019-01-20 03:03:49.000000000 -0800
-@@ -52 +52 @@
--option go_package = "github.com/golang/protobuf/protoc-gen-go/plugin;plugin_go";
-+option go_package = "github.com/golang/protobuf/v2/types/plugin;plugin_proto";
---- a/src/google/protobuf/descriptor.proto	2018-07-23 13:56:42.000000000 -0700
-+++ b/src/google/protobuf/descriptor.proto	2019-01-20 03:03:52.000000000 -0800
-@@ -43 +43 @@
--option go_package = "github.com/golang/protobuf/protoc-gen-go/descriptor;descriptor";
-+option go_package = "github.com/golang/protobuf/v2/types/descriptor;descriptor_proto";
---- a/src/google/protobuf/duration.proto	2018-07-23 13:56:42.000000000 -0700
-+++ b/src/google/protobuf/duration.proto	2019-01-20 03:00:55.000000000 -0800
-@@ -37 +37 @@
--option go_package = "github.com/golang/protobuf/ptypes/duration";
-+option go_package = "github.com/golang/protobuf/v2/types/known;known_proto";
---- a/src/google/protobuf/empty.proto	2018-07-23 13:56:42.000000000 -0700
-+++ b/src/google/protobuf/empty.proto	2019-01-20 03:00:52.000000000 -0800
-@@ -36 +36 @@
--option go_package = "github.com/golang/protobuf/ptypes/empty";
-+option go_package = "github.com/golang/protobuf/v2/types/known;known_proto";
---- a/src/google/protobuf/field_mask.proto	2018-07-23 13:56:42.000000000 -0700
-+++ b/src/google/protobuf/field_mask.proto	2019-01-20 03:00:50.000000000 -0800
-@@ -40 +40 @@
--option go_package = "google.golang.org/genproto/protobuf/field_mask;field_mask";
-+option go_package = "github.com/golang/protobuf/v2/types/known;known_proto";
---- a/src/google/protobuf/source_context.proto	2018-07-23 13:56:42.000000000 -0700
-+++ b/src/google/protobuf/source_context.proto	2019-01-20 03:00:47.000000000 -0800
-@@ -40 +40 @@
--option go_package = "google.golang.org/genproto/protobuf/source_context;source_context";
-+option go_package = "github.com/golang/protobuf/v2/types/known;known_proto";
---- a/src/google/protobuf/struct.proto	2018-07-23 13:56:42.000000000 -0700
-+++ b/src/google/protobuf/struct.proto	2019-01-20 03:00:42.000000000 -0800
-@@ -37 +37 @@
--option go_package = "github.com/golang/protobuf/ptypes/struct;structpb";
-+option go_package = "github.com/golang/protobuf/v2/types/known;known_proto";
---- a/src/google/protobuf/timestamp.proto	2018-07-23 13:56:42.000000000 -0700
-+++ b/src/google/protobuf/timestamp.proto	2019-01-20 03:00:40.000000000 -0800
-@@ -37 +37 @@
--option go_package = "github.com/golang/protobuf/ptypes/timestamp";
-+option go_package = "github.com/golang/protobuf/v2/types/known;known_proto";
---- a/src/google/protobuf/type.proto	2018-07-23 13:56:42.000000000 -0700
-+++ b/src/google/protobuf/type.proto	2019-01-20 03:03:44.000000000 -0800
-@@ -44 +44 @@
--option go_package = "google.golang.org/genproto/protobuf/ptype;ptype";
-+option go_package = "github.com/golang/protobuf/v2/types/known;known_proto";
---- a/src/google/protobuf/wrappers.proto	2018-07-23 13:56:42.000000000 -0700
-+++ b/src/google/protobuf/wrappers.proto	2019-01-20 03:03:45.000000000 -0800
-@@ -42 +42 @@
--option go_package = "github.com/golang/protobuf/ptypes/wrappers";
-+option go_package = "github.com/golang/protobuf/v2/types/known;known_proto";
-EOF
-) | patch -d $PROTOBUF_ROOT -p1
-fi
-
-# Download each Go toolchain version.
-GO_LATEST=go1.11.4
-GO_VERSIONS=(go1.9.7 go1.10.7 $GO_LATEST)
-for GO_VERSION in ${GO_VERSIONS[@]}; do
-	if [ ! -d $GO_VERSION ]; then
-		print "download $GO_VERSION"
-		GOOS=$(uname | tr '[:upper:]' '[:lower:]')
-		(mkdir $GO_VERSION && curl -s -L https://dl.google.com/go/$GO_VERSION.$GOOS-amd64.tar.gz | tar -zxf - -C $GO_VERSION --strip-components 1) || exit 1
-	fi
-	register_binary $GO_VERSION $GO_VERSION/bin/go
-done
-register_binary go $GO_LATEST/bin/go
-register_binary gofmt $GO_LATEST/bin/gofmt
-
-# Travis-CI sets GOROOT, which confuses later invocations of the Go toolchains.
-# Explicitly clear GOROOT, so each toolchain uses their default GOROOT.
-unset GOROOT
-
-# Setup GOPATH for pre-module support.
-export GOPATH=$TEST_DIR/gopath
-MODULE_PATH=$(cd $REPO_ROOT && go list -m -f "{{.Path}}")
-rm -rf gopath/src # best-effort delete
-mkdir -p gopath/src/$(dirname $MODULE_PATH)
-(cd gopath/src/$(dirname $MODULE_PATH) && ln -s $REPO_ROOT $(basename $MODULE_PATH))
-
-# Download dependencies using modules.
-# For pre-module support, dump the dependencies in a vendor directory.
-(cd $REPO_ROOT && go mod tidy && go mod vendor) || exit 1
-
-# Regenerate Go source files.
-if [ "$1" == "-regenerate" ]; then
-	cd $REPO_ROOT
-	go run ./internal/cmd/generate-types  -execute || exit 1
-	go run ./internal/cmd/generate-protos -execute || exit 1
-	gofmt -w $(git ls-files '*.go') || exit 1
-	exit 0
-fi
-
-# Run tests across every supported version of Go.
-LABELS=()
-PIDS=()
-OUTS=()
-function cleanup() { for OUT in ${OUTS[@]}; do rm $OUT; done; }
-trap cleanup EXIT
-for GO_VERSION in ${GO_VERSIONS[@]}; do
-	# Run the go command in a background process.
-	function go() {
-		# Use a per-version Go cache to work around bugs in Go build caching.
-		# See https://golang.org/issue/26883
-		GO_CACHE="$TEST_DIR/cache.$GO_VERSION"
-		LABELS+=("$(echo "$GO_VERSION $@")")
-		OUT=$(mktemp)
-		(cd $GOPATH/src/$MODULE_PATH && GOCACHE=$GO_CACHE $GO_VERSION "$@" &> $OUT) &
-		PIDS+=($!)
-		OUTS+=($OUT)
-	}
-
-	# TODO: "go build" does not descend into testdata, which means that
-	# generated .pb.go files are not being built.
-	go build ./...
-	go test -race ./...
-	go test -race -tags purego ./...
-	go test -race -tags proto1_legacy ./...
-
-	unset go # to avoid confusing later invocations of "go"
-done
-
-# Wait for all processes to finish.
-RET=0
-for I in ${!PIDS[@]}; do
-	print "${LABELS[$I]}"
-	if ! wait ${PIDS[$I]}; then
-		cat ${OUTS[$I]} # only output upon error
-		RET=1
-	fi
-done
-
-# Run commands that produce output when there is a failure.
-function check() {
-	OUT=$(cd $REPO_ROOT && "$@" 2>&1)
-	if [ ! -z "$OUT" ]; then
-		print "$@"
-		echo "$OUT"
-		RET=1
-	fi
-}
-
-# Check for stale or unformatted source files.
-check go run ./internal/cmd/generate-types
-check go run ./internal/cmd/generate-protos
-check gofmt -d $(cd $REPO_ROOT && git ls-files '*.go')
-
-# Check for changed or untracked files.
-check git diff --no-prefix HEAD
-check git ls-files --others --exclude-standard
-
-# Print termination status.
-if [ $RET -eq 0 ]; then
-	echo -e "\x1b[32;1mPASS\x1b[0m"
-else
-	echo -e "\x1b[31;1mFAIL\x1b[0m"
-fi
-exit $RET
+cd "$(git rev-parse --show-toplevel)"
+go test -v -timeout 60m -tags integration "$@"
+exit $?