blob: a6fe2b6717fba24c10a4a2ec85d3a1cc842c7d41 [file] [log] [blame]
Joe Tsai707894e2019-03-01 12:50:52 -08001// Copyright 2019 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
Joe Tsaif3987842019-03-02 13:35:17 -08005// +build ignore
Joe Tsai707894e2019-03-01 12:50:52 -08006
Joe Tsaif3987842019-03-02 13:35:17 -08007package main
Joe Tsai707894e2019-03-01 12:50:52 -08008
9import (
10 "archive/tar"
11 "bytes"
12 "compress/gzip"
13 "flag"
14 "fmt"
15 "io"
16 "io/ioutil"
17 "net/http"
18 "os"
19 "os/exec"
20 "path/filepath"
Damien Neil3a185602020-02-21 09:16:19 -080021 "regexp"
Joe Tsai707894e2019-03-01 12:50:52 -080022 "runtime"
23 "strings"
Joe Tsai62200db2019-07-11 17:34:10 -070024 "sync"
Joe Tsai707894e2019-03-01 12:50:52 -080025 "testing"
Joe Tsaif3987842019-03-02 13:35:17 -080026 "time"
Joe Tsai4f3de442019-08-07 19:33:51 -070027
Joe Tsai4ab2bc92020-03-10 17:38:07 -070028 "google.golang.org/protobuf/internal/version"
Joe Tsai707894e2019-03-01 12:50:52 -080029)
30
31var (
Joe Tsai4f3de442019-08-07 19:33:51 -070032 regenerate = flag.Bool("regenerate", false, "regenerate files")
33 buildRelease = flag.Bool("buildRelease", false, "build release binaries")
Joe Tsai707894e2019-03-01 12:50:52 -080034
Joe Tsai387873d2020-04-28 14:44:38 -070035 protobufVersion = "4b4e6674" // pre-release of 3.12.x
Joe Tsaif75a3382019-12-19 17:42:41 -080036 golangVersions = []string{"1.9.7", "1.10.8", "1.11.13", "1.12.17", "1.13.8", "1.14"}
Joe Tsai707894e2019-03-01 12:50:52 -080037 golangLatest = golangVersions[len(golangVersions)-1]
38
Joe Tsai9e88bc02019-03-12 01:30:40 -070039 // purgeTimeout determines the maximum age of unused sub-directories.
40 purgeTimeout = 30 * 24 * time.Hour // 1 month
Joe Tsai707894e2019-03-01 12:50:52 -080041
42 // Variables initialized by mustInitDeps.
Herbie Ongd64dceb2019-04-25 01:19:57 -070043 goPath string
44 modulePath string
45 protobufPath string
Joe Tsai707894e2019-03-01 12:50:52 -080046)
47
48func Test(t *testing.T) {
49 mustInitDeps(t)
Joe Tsai4f3de442019-08-07 19:33:51 -070050 mustHandleFlags(t)
Joe Tsai707894e2019-03-01 12:50:52 -080051
Damien Neil4d8936d2020-02-21 10:32:56 -080052 // Report dirt in the working tree quickly, rather than after
53 // going through all the presubmits.
54 //
55 // Fail the test late, so we can test uncommitted changes with -failfast.
Joe Tsaif75a3382019-12-19 17:42:41 -080056 gitDiff := mustRunCommand(t, "git", "diff", "HEAD")
57 if strings.TrimSpace(gitDiff) != "" {
58 fmt.Printf("WARNING: working tree contains uncommitted changes:\n%v\n", gitDiff)
Damien Neil4d8936d2020-02-21 10:32:56 -080059 }
Joe Tsaif75a3382019-12-19 17:42:41 -080060 gitUntracked := mustRunCommand(t, "git", "ls-files", "--others", "--exclude-standard")
61 if strings.TrimSpace(gitUntracked) != "" {
62 fmt.Printf("WARNING: working tree contains untracked files:\n%v\n", gitUntracked)
Damien Neil4d8936d2020-02-21 10:32:56 -080063 }
64
65 // Do the relatively fast checks up-front.
66 t.Run("GeneratedGoFiles", func(t *testing.T) {
67 diff := mustRunCommand(t, "go", "run", "-tags", "protolegacy", "./internal/cmd/generate-types")
68 if strings.TrimSpace(diff) != "" {
69 t.Fatalf("stale generated files:\n%v", diff)
70 }
71 diff = mustRunCommand(t, "go", "run", "-tags", "protolegacy", "./internal/cmd/generate-protos")
72 if strings.TrimSpace(diff) != "" {
73 t.Fatalf("stale generated files:\n%v", diff)
74 }
75 })
76 t.Run("FormattedGoFiles", func(t *testing.T) {
77 files := strings.Split(strings.TrimSpace(mustRunCommand(t, "git", "ls-files", "*.go")), "\n")
78 diff := mustRunCommand(t, append([]string{"gofmt", "-d"}, files...)...)
79 if strings.TrimSpace(diff) != "" {
80 t.Fatalf("unformatted source files:\n%v", diff)
81 }
82 })
83 t.Run("CopyrightHeaders", func(t *testing.T) {
84 files := strings.Split(strings.TrimSpace(mustRunCommand(t, "git", "ls-files", "*.go", "*.proto")), "\n")
85 mustHaveCopyrightHeader(t, files)
86 })
87
Joe Tsai62200db2019-07-11 17:34:10 -070088 var wg sync.WaitGroup
89 sema := make(chan bool, (runtime.NumCPU()+1)/2)
90 for i := range golangVersions {
91 goVersion := golangVersions[i]
92 goLabel := "Go" + goVersion
93 runGo := func(label, workDir string, args ...string) {
94 wg.Add(1)
95 sema <- true
96 go func() {
97 defer wg.Done()
98 defer func() { <-sema }()
99 t.Run(goLabel+"/"+label, func(t *testing.T) {
100 args[0] += goVersion
Joe Tsai7164af52019-08-07 19:27:43 -0700101 command{Dir: workDir}.mustRun(t, args...)
Joe Tsai707894e2019-03-01 12:50:52 -0800102 })
Joe Tsai62200db2019-07-11 17:34:10 -0700103 }()
104 }
105
106 workDir := filepath.Join(goPath, "src", modulePath)
107 runGo("Normal", workDir, "go", "test", "-race", "./...")
108 runGo("PureGo", workDir, "go", "test", "-race", "-tags", "purego", "./...")
109 runGo("Reflect", workDir, "go", "test", "-race", "-tags", "protoreflect", "./...")
110 if goVersion == golangLatest {
Joe Tsai1799d112019-08-08 13:31:59 -0700111 runGo("ProtoLegacy", workDir, "go", "test", "-race", "-tags", "protolegacy", "./...")
Joe Tsai62200db2019-07-11 17:34:10 -0700112 runGo("ProtocGenGo", "cmd/protoc-gen-go/testdata", "go", "test")
Joe Tsaidd271b62019-08-16 01:09:33 -0700113 runGo("Conformance", "internal/conformance", "go", "test", "-execute")
Joe Tsai62200db2019-07-11 17:34:10 -0700114 }
Joe Tsai707894e2019-03-01 12:50:52 -0800115 }
Joe Tsai62200db2019-07-11 17:34:10 -0700116 wg.Wait()
Joe Tsai707894e2019-03-01 12:50:52 -0800117
Joe Tsai707894e2019-03-01 12:50:52 -0800118 t.Run("CommittedGitChanges", func(t *testing.T) {
Joe Tsaif75a3382019-12-19 17:42:41 -0800119 if strings.TrimSpace(gitDiff) != "" {
Damien Neil4d8936d2020-02-21 10:32:56 -0800120 t.Fatalf("uncommitted changes")
Joe Tsai707894e2019-03-01 12:50:52 -0800121 }
122 })
123 t.Run("TrackedGitFiles", func(t *testing.T) {
Joe Tsaif75a3382019-12-19 17:42:41 -0800124 if strings.TrimSpace(gitUntracked) != "" {
Damien Neil4d8936d2020-02-21 10:32:56 -0800125 t.Fatalf("untracked files")
Joe Tsai707894e2019-03-01 12:50:52 -0800126 }
127 })
128}
129
130func mustInitDeps(t *testing.T) {
131 check := func(err error) {
132 t.Helper()
133 if err != nil {
134 t.Fatal(err)
135 }
136 }
137
138 // Determine the directory to place the test directory.
139 repoRoot, err := os.Getwd()
140 check(err)
141 testDir := filepath.Join(repoRoot, ".cache")
142 check(os.MkdirAll(testDir, 0775))
143
Joe Tsaif3987842019-03-02 13:35:17 -0800144 // Travis-CI has a hard-coded timeout where it kills the test after
145 // 10 minutes of a lack of activity on stdout.
146 // We work around this restriction by periodically printing the timestamp.
147 ticker := time.NewTicker(5 * time.Minute)
148 done := make(chan struct{})
149 go func() {
150 now := time.Now()
151 for {
152 select {
153 case t := <-ticker.C:
154 fmt.Printf("\tt=%0.fmin\n", t.Sub(now).Minutes())
155 case <-done:
156 return
157 }
158 }
159 }()
160 defer close(done)
161 defer ticker.Stop()
162
Joe Tsai707894e2019-03-01 12:50:52 -0800163 // Delete the current directory if non-empty,
164 // which only occurs if a dependency failed to initialize properly.
165 var workingDir string
166 defer func() {
167 if workingDir != "" {
168 os.RemoveAll(workingDir) // best-effort
169 }
170 }()
171
172 // Delete other sub-directories that are no longer relevant.
173 defer func() {
Joe Tsaif75a3382019-12-19 17:42:41 -0800174 subDirs := map[string]bool{"bin": true, "gopath": true}
Joe Tsai707894e2019-03-01 12:50:52 -0800175 subDirs["protobuf-"+protobufVersion] = true
176 for _, v := range golangVersions {
177 subDirs["go"+v] = true
178 }
Joe Tsai707894e2019-03-01 12:50:52 -0800179
Joe Tsai9e88bc02019-03-12 01:30:40 -0700180 now := time.Now()
Joe Tsai707894e2019-03-01 12:50:52 -0800181 fis, _ := ioutil.ReadDir(testDir)
182 for _, fi := range fis {
Joe Tsai9e88bc02019-03-12 01:30:40 -0700183 if subDirs[fi.Name()] {
184 os.Chtimes(filepath.Join(testDir, fi.Name()), now, now) // best-effort
185 continue
Joe Tsai707894e2019-03-01 12:50:52 -0800186 }
Joe Tsai9e88bc02019-03-12 01:30:40 -0700187 if now.Sub(fi.ModTime()) < purgeTimeout {
188 continue
189 }
190 fmt.Printf("delete %v\n", fi.Name())
191 os.RemoveAll(filepath.Join(testDir, fi.Name())) // best-effort
Joe Tsai707894e2019-03-01 12:50:52 -0800192 }
193 }()
194
195 // The bin directory contains symlinks to each tool by version.
196 // It is safe to delete this directory and run the test script from scratch.
197 binPath := filepath.Join(testDir, "bin")
198 check(os.RemoveAll(binPath))
199 check(os.Mkdir(binPath, 0775))
200 check(os.Setenv("PATH", binPath+":"+os.Getenv("PATH")))
201 registerBinary := func(name, path string) {
202 check(os.Symlink(path, filepath.Join(binPath, name)))
203 }
204
205 // Download and build the protobuf toolchain.
206 // We avoid downloading the pre-compiled binaries since they do not contain
207 // the conformance test runner.
208 workingDir = filepath.Join(testDir, "protobuf-"+protobufVersion)
Herbie Ongd64dceb2019-04-25 01:19:57 -0700209 protobufPath = workingDir
210 if _, err := os.Stat(protobufPath); err != nil {
211 fmt.Printf("download %v\n", filepath.Base(protobufPath))
Joe Tsai387873d2020-04-28 14:44:38 -0700212 if isCommit := strings.Trim(protobufVersion, "0123456789abcdef") == ""; isCommit {
213 command{Dir: testDir}.mustRun(t, "git", "clone", "https://github.com/protocolbuffers/protobuf", "protobuf-"+protobufVersion)
214 command{Dir: protobufPath}.mustRun(t, "git", "checkout", protobufVersion)
215 } else {
216 url := fmt.Sprintf("https://github.com/google/protobuf/releases/download/v%v/protobuf-all-%v.tar.gz", protobufVersion, protobufVersion)
217 downloadArchive(check, protobufPath, url, "protobuf-"+protobufVersion)
218 }
Joe Tsai707894e2019-03-01 12:50:52 -0800219
Herbie Ongd64dceb2019-04-25 01:19:57 -0700220 fmt.Printf("build %v\n", filepath.Base(protobufPath))
Joe Tsai7164af52019-08-07 19:27:43 -0700221 command{Dir: protobufPath}.mustRun(t, "./autogen.sh")
222 command{Dir: protobufPath}.mustRun(t, "./configure")
223 command{Dir: protobufPath}.mustRun(t, "make")
224 command{Dir: filepath.Join(protobufPath, "conformance")}.mustRun(t, "make")
Joe Tsai707894e2019-03-01 12:50:52 -0800225 }
Herbie Ongd64dceb2019-04-25 01:19:57 -0700226 check(os.Setenv("PROTOBUF_ROOT", protobufPath)) // for generate-protos
227 registerBinary("conform-test-runner", filepath.Join(protobufPath, "conformance", "conformance-test-runner"))
228 registerBinary("protoc", filepath.Join(protobufPath, "src", "protoc"))
Joe Tsai707894e2019-03-01 12:50:52 -0800229 workingDir = ""
230
231 // Download each Go toolchain version.
232 for _, v := range golangVersions {
233 workingDir = filepath.Join(testDir, "go"+v)
234 if _, err := os.Stat(workingDir); err != nil {
235 fmt.Printf("download %v\n", filepath.Base(workingDir))
236 url := fmt.Sprintf("https://dl.google.com/go/go%v.%v-%v.tar.gz", v, runtime.GOOS, runtime.GOARCH)
Joe Tsaif3987842019-03-02 13:35:17 -0800237 downloadArchive(check, workingDir, url, "go")
Joe Tsai707894e2019-03-01 12:50:52 -0800238 }
239 registerBinary("go"+v, filepath.Join(workingDir, "bin", "go"))
240 }
241 registerBinary("go", filepath.Join(testDir, "go"+golangLatest, "bin", "go"))
242 registerBinary("gofmt", filepath.Join(testDir, "go"+golangLatest, "bin", "gofmt"))
243 workingDir = ""
244
245 // Travis-CI sets GOROOT, which confuses invocations of the Go toolchain.
246 // Explicitly clear GOROOT, so each toolchain uses their default GOROOT.
247 check(os.Unsetenv("GOROOT"))
248
Joe Tsai6a2180f2019-07-11 16:34:17 -0700249 // Set a cache directory outside the test directory.
250 check(os.Setenv("GOCACHE", filepath.Join(repoRoot, ".gocache")))
Joe Tsai707894e2019-03-01 12:50:52 -0800251
252 // Setup GOPATH for pre-module support (i.e., go1.10 and earlier).
253 goPath = filepath.Join(testDir, "gopath")
Joe Tsai7164af52019-08-07 19:27:43 -0700254 modulePath = strings.TrimSpace(command{Dir: testDir}.mustRun(t, "go", "list", "-m", "-f", "{{.Path}}"))
Joe Tsai707894e2019-03-01 12:50:52 -0800255 check(os.RemoveAll(filepath.Join(goPath, "src")))
256 check(os.MkdirAll(filepath.Join(goPath, "src", filepath.Dir(modulePath)), 0775))
257 check(os.Symlink(repoRoot, filepath.Join(goPath, "src", modulePath)))
Joe Tsai7164af52019-08-07 19:27:43 -0700258 command{Dir: repoRoot}.mustRun(t, "go", "mod", "tidy")
259 command{Dir: repoRoot}.mustRun(t, "go", "mod", "vendor")
Joe Tsai707894e2019-03-01 12:50:52 -0800260 check(os.Setenv("GOPATH", goPath))
261}
262
Damien Neila80229e2019-06-20 12:53:48 -0700263func downloadFile(check func(error), dstPath, srcURL string) {
264 resp, err := http.Get(srcURL)
265 check(err)
266 defer resp.Body.Close()
267
268 check(os.MkdirAll(filepath.Dir(dstPath), 0775))
269 f, err := os.Create(dstPath)
270 check(err)
271
272 _, err = io.Copy(f, resp.Body)
273 check(err)
274}
275
Joe Tsaif3987842019-03-02 13:35:17 -0800276func downloadArchive(check func(error), dstPath, srcURL, skipPrefix string) {
Joe Tsai707894e2019-03-01 12:50:52 -0800277 check(os.RemoveAll(dstPath))
278
279 resp, err := http.Get(srcURL)
280 check(err)
281 defer resp.Body.Close()
282
283 zr, err := gzip.NewReader(resp.Body)
284 check(err)
285
286 tr := tar.NewReader(zr)
287 for {
288 h, err := tr.Next()
289 if err == io.EOF {
290 return
291 }
292 check(err)
293
Joe Tsaif3987842019-03-02 13:35:17 -0800294 // Skip directories or files outside the prefix directory.
295 if len(skipPrefix) > 0 {
296 if !strings.HasPrefix(h.Name, skipPrefix) {
297 continue
298 }
299 if len(h.Name) > len(skipPrefix) && h.Name[len(skipPrefix)] != '/' {
300 continue
301 }
302 }
303
304 path := strings.TrimPrefix(strings.TrimPrefix(h.Name, skipPrefix), "/")
Joe Tsai707894e2019-03-01 12:50:52 -0800305 path = filepath.Join(dstPath, filepath.FromSlash(path))
306 mode := os.FileMode(h.Mode & 0777)
307 switch h.Typeflag {
308 case tar.TypeReg:
309 b, err := ioutil.ReadAll(tr)
310 check(err)
311 check(ioutil.WriteFile(path, b, mode))
312 case tar.TypeDir:
313 check(os.Mkdir(path, mode))
314 }
315 }
316}
317
Joe Tsai4f3de442019-08-07 19:33:51 -0700318func mustHandleFlags(t *testing.T) {
319 if *regenerate {
320 t.Run("Generate", func(t *testing.T) {
321 fmt.Print(mustRunCommand(t, "go", "run", "-tags", "protolegacy", "./internal/cmd/generate-types", "-execute"))
322 fmt.Print(mustRunCommand(t, "go", "run", "-tags", "protolegacy", "./internal/cmd/generate-protos", "-execute"))
323 files := strings.Split(strings.TrimSpace(mustRunCommand(t, "git", "ls-files", "*.go")), "\n")
324 mustRunCommand(t, append([]string{"gofmt", "-w"}, files...)...)
325 })
326 }
327 if *buildRelease {
328 t.Run("BuildRelease", func(t *testing.T) {
Joe Tsai4ab2bc92020-03-10 17:38:07 -0700329 v := version.String()
Joe Tsai4f3de442019-08-07 19:33:51 -0700330 for _, goos := range []string{"linux", "darwin", "windows"} {
331 for _, goarch := range []string{"386", "amd64"} {
332 binPath := filepath.Join("bin", fmt.Sprintf("protoc-gen-go.%v.%v.%v", v, goos, goarch))
333
334 // Build the binary.
335 cmd := command{Env: append(os.Environ(), "GOOS="+goos, "GOARCH="+goarch)}
Joe Tsai576cfb32019-09-04 22:50:42 -0700336 cmd.mustRun(t, "go", "build", "-trimpath", "-ldflags", "-s -w -buildid=", "-o", binPath, "./cmd/protoc-gen-go")
Joe Tsai4f3de442019-08-07 19:33:51 -0700337
338 // Archive and compress the binary.
339 in, err := ioutil.ReadFile(binPath)
340 if err != nil {
341 t.Fatal(err)
342 }
343 out := new(bytes.Buffer)
344 gz, _ := gzip.NewWriterLevel(out, gzip.BestCompression)
345 gz.Comment = fmt.Sprintf("protoc-gen-go VERSION=%v GOOS=%v GOARCH=%v", v, goos, goarch)
346 tw := tar.NewWriter(gz)
347 tw.WriteHeader(&tar.Header{
348 Name: "protoc-gen-go",
349 Mode: int64(0775),
350 Size: int64(len(in)),
351 })
352 tw.Write(in)
353 tw.Close()
354 gz.Close()
355 if err := ioutil.WriteFile(binPath+".tar.gz", out.Bytes(), 0664); err != nil {
356 t.Fatal(err)
357 }
358 }
359 }
360 })
361 }
362 if *regenerate || *buildRelease {
363 t.SkipNow()
364 }
365}
366
Damien Neil3a185602020-02-21 09:16:19 -0800367var copyrightRegex = []*regexp.Regexp{
368 regexp.MustCompile(`^// Copyright \d\d\d\d The Go Authors\. All rights reserved.
369// Use of this source code is governed by a BSD-style
370// license that can be found in the LICENSE file\.
371`),
372 // Generated .pb.go files from main protobuf repo.
373 regexp.MustCompile(`^// Protocol Buffers - Google's data interchange format
374// Copyright \d\d\d\d Google Inc\. All rights reserved\.
375`),
376}
377
Damien Neil3a185602020-02-21 09:16:19 -0800378func mustHaveCopyrightHeader(t *testing.T, files []string) {
379 var bad []string
380File:
381 for _, file := range files {
Damien Neil3a185602020-02-21 09:16:19 -0800382 b, err := ioutil.ReadFile(file)
383 if err != nil {
384 t.Fatal(err)
385 }
386 for _, re := range copyrightRegex {
387 if loc := re.FindIndex(b); loc != nil && loc[0] == 0 {
388 continue File
389 }
390 }
391 bad = append(bad, file)
392 }
393 if len(bad) > 0 {
394 t.Fatalf("files with missing/bad copyright headers:\n %v", strings.Join(bad, "\n "))
395 }
396}
397
Joe Tsai7164af52019-08-07 19:27:43 -0700398type command struct {
399 Dir string
400 Env []string
401}
402
403func (c command) mustRun(t *testing.T, args ...string) string {
Joe Tsai707894e2019-03-01 12:50:52 -0800404 t.Helper()
405 stdout := new(bytes.Buffer)
Herbie Ong4630b3d2019-03-19 16:42:01 -0700406 stderr := new(bytes.Buffer)
Joe Tsai707894e2019-03-01 12:50:52 -0800407 cmd := exec.Command(args[0], args[1:]...)
Joe Tsai7164af52019-08-07 19:27:43 -0700408 cmd.Dir = "."
409 if c.Dir != "" {
410 cmd.Dir = c.Dir
411 }
412 cmd.Env = os.Environ()
413 if c.Env != nil {
414 cmd.Env = c.Env
415 }
416 cmd.Env = append(cmd.Env, "PWD="+cmd.Dir)
Herbie Ong4630b3d2019-03-19 16:42:01 -0700417 cmd.Stdout = stdout
418 cmd.Stderr = stderr
Joe Tsai707894e2019-03-01 12:50:52 -0800419 if err := cmd.Run(); err != nil {
Herbie Ong4630b3d2019-03-19 16:42:01 -0700420 t.Fatalf("executing (%v): %v\n%s%s", strings.Join(args, " "), err, stdout.String(), stderr.String())
Joe Tsai707894e2019-03-01 12:50:52 -0800421 }
422 return stdout.String()
423}
Damien Neila80229e2019-06-20 12:53:48 -0700424
Joe Tsai7164af52019-08-07 19:27:43 -0700425func mustRunCommand(t *testing.T, args ...string) string {
426 t.Helper()
427 return command{}.mustRun(t, args...)
428}