xref: /aosp_15_r20/external/golang-protobuf/integration_test.go (revision 1c12ee1efe575feb122dbf939ff15148a3b3e8f2)
1// 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
5//go:build ignore
6// +build ignore
7
8package main
9
10import (
11	"archive/tar"
12	"archive/zip"
13	"bytes"
14	"compress/gzip"
15	"crypto/sha256"
16	"flag"
17	"fmt"
18	"io"
19	"io/ioutil"
20	"net/http"
21	"os"
22	"os/exec"
23	"path/filepath"
24	"regexp"
25	"runtime"
26	"strings"
27	"sync"
28	"testing"
29	"time"
30
31	"google.golang.org/protobuf/internal/version"
32)
33
34var (
35	regenerate   = flag.Bool("regenerate", false, "regenerate files")
36	buildRelease = flag.Bool("buildRelease", false, "build release binaries")
37
38	protobufVersion = "22.0"
39	protobufSHA256  = "" // ignored if protobufVersion is a git hash
40
41	golangVersions = func() []string {
42		var vers []string
43		switch runtime.GOOS + "/" + runtime.GOARCH {
44		case "darwin/arm64":
45		default:
46			vers = []string{"1.13.15", "1.14.15", "1.15.15"}
47		}
48		return append(vers, "1.16.15", "1.17.13", "1.18.10", "1.19.6")
49	}()
50	golangLatest = golangVersions[len(golangVersions)-1]
51
52	staticcheckVersion = "2022.1.2"
53	staticcheckSHA256s = map[string]string{
54		"darwin/amd64": "baa35f8fb967ee2aacad57f026e3724fbf8d9b7ad8f682f4d44b2084a96e103b",
55		"darwin/arm64": "9f01a581eeea088d0a6272538360f6d84996d66ae554bfada8026fe24991daa0",
56		"linux/386":    "4cf74373e5d668b265d7a241b59ba7d26064f2cd6af50b77e62c2b3e2f3afb43",
57		"linux/amd64":  "6dbb7187e43812fa23363cdaaa90ab13544dd36e24d02e2347014e4cf265f06d",
58	}
59
60	// purgeTimeout determines the maximum age of unused sub-directories.
61	purgeTimeout = 30 * 24 * time.Hour // 1 month
62
63	// Variables initialized by mustInitDeps.
64	goPath       string
65	modulePath   string
66	protobufPath string
67)
68
69func Test(t *testing.T) {
70	mustInitDeps(t)
71	mustHandleFlags(t)
72
73	// Report dirt in the working tree quickly, rather than after
74	// going through all the presubmits.
75	//
76	// Fail the test late, so we can test uncommitted changes with -failfast.
77	gitDiff := mustRunCommand(t, "git", "diff", "HEAD")
78	if strings.TrimSpace(gitDiff) != "" {
79		fmt.Printf("WARNING: working tree contains uncommitted changes:\n%v\n", gitDiff)
80	}
81	gitUntracked := mustRunCommand(t, "git", "ls-files", "--others", "--exclude-standard")
82	if strings.TrimSpace(gitUntracked) != "" {
83		fmt.Printf("WARNING: working tree contains untracked files:\n%v\n", gitUntracked)
84	}
85
86	// Do the relatively fast checks up-front.
87	t.Run("GeneratedGoFiles", func(t *testing.T) {
88		diff := mustRunCommand(t, "go", "run", "-tags", "protolegacy", "./internal/cmd/generate-types")
89		if strings.TrimSpace(diff) != "" {
90			t.Fatalf("stale generated files:\n%v", diff)
91		}
92		diff = mustRunCommand(t, "go", "run", "-tags", "protolegacy", "./internal/cmd/generate-protos")
93		if strings.TrimSpace(diff) != "" {
94			t.Fatalf("stale generated files:\n%v", diff)
95		}
96	})
97	t.Run("FormattedGoFiles", func(t *testing.T) {
98		files := strings.Split(strings.TrimSpace(mustRunCommand(t, "git", "ls-files", "*.go")), "\n")
99		diff := mustRunCommand(t, append([]string{"gofmt", "-d"}, files...)...)
100		if strings.TrimSpace(diff) != "" {
101			t.Fatalf("unformatted source files:\n%v", diff)
102		}
103	})
104	t.Run("CopyrightHeaders", func(t *testing.T) {
105		files := strings.Split(strings.TrimSpace(mustRunCommand(t, "git", "ls-files", "*.go", "*.proto")), "\n")
106		mustHaveCopyrightHeader(t, files)
107	})
108
109	var wg sync.WaitGroup
110	sema := make(chan bool, (runtime.NumCPU()+1)/2)
111	for i := range golangVersions {
112		goVersion := golangVersions[i]
113		goLabel := "Go" + goVersion
114		runGo := func(label string, cmd command, args ...string) {
115			wg.Add(1)
116			sema <- true
117			go func() {
118				defer wg.Done()
119				defer func() { <-sema }()
120				t.Run(goLabel+"/"+label, func(t *testing.T) {
121					args[0] += goVersion
122					cmd.mustRun(t, args...)
123				})
124			}()
125		}
126
127		workDir := filepath.Join(goPath, "src", modulePath)
128		runGo("Normal", command{Dir: workDir}, "go", "test", "-race", "./...")
129		runGo("PureGo", command{Dir: workDir}, "go", "test", "-race", "-tags", "purego", "./...")
130		runGo("Reflect", command{Dir: workDir}, "go", "test", "-race", "-tags", "protoreflect", "./...")
131		if goVersion == golangLatest {
132			runGo("ProtoLegacy", command{Dir: workDir}, "go", "test", "-race", "-tags", "protolegacy", "./...")
133			runGo("ProtocGenGo", command{Dir: "cmd/protoc-gen-go/testdata"}, "go", "test")
134			runGo("Conformance", command{Dir: "internal/conformance"}, "go", "test", "-execute")
135
136			// Only run the 32-bit compatibility tests for Linux;
137			// avoid Darwin since 10.15 dropped support i386 code execution.
138			if runtime.GOOS == "linux" {
139				runGo("Arch32Bit", command{Dir: workDir, Env: append(os.Environ(), "GOARCH=386")}, "go", "test", "./...")
140			}
141		}
142	}
143	wg.Wait()
144
145	t.Run("GoStaticCheck", func(t *testing.T) {
146		checks := []string{
147			"all",     // start with all checks enabled
148			"-SA1019", // disable deprecated usage check
149			"-S*",     // disable code simplification checks
150			"-ST*",    // disable coding style checks
151			"-U*",     // disable unused declaration checks
152		}
153		out := mustRunCommand(t, "staticcheck", "-checks="+strings.Join(checks, ","), "-fail=none", "./...")
154
155		// Filter out findings from certain paths.
156		var findings []string
157		for _, finding := range strings.Split(strings.TrimSpace(out), "\n") {
158			switch {
159			case strings.HasPrefix(finding, "internal/testprotos/legacy/"):
160			default:
161				findings = append(findings, finding)
162			}
163		}
164		if len(findings) > 0 {
165			t.Fatalf("staticcheck findings:\n%v", strings.Join(findings, "\n"))
166		}
167	})
168	t.Run("CommittedGitChanges", func(t *testing.T) {
169		if strings.TrimSpace(gitDiff) != "" {
170			t.Fatalf("uncommitted changes")
171		}
172	})
173	t.Run("TrackedGitFiles", func(t *testing.T) {
174		if strings.TrimSpace(gitUntracked) != "" {
175			t.Fatalf("untracked files")
176		}
177	})
178}
179
180func mustInitDeps(t *testing.T) {
181	check := func(err error) {
182		t.Helper()
183		if err != nil {
184			t.Fatal(err)
185		}
186	}
187
188	// Determine the directory to place the test directory.
189	repoRoot, err := os.Getwd()
190	check(err)
191	testDir := filepath.Join(repoRoot, ".cache")
192	check(os.MkdirAll(testDir, 0775))
193
194	// Delete the current directory if non-empty,
195	// which only occurs if a dependency failed to initialize properly.
196	var workingDir string
197	finishedDirs := map[string]bool{}
198	defer func() {
199		if workingDir != "" {
200			os.RemoveAll(workingDir) // best-effort
201		}
202	}()
203	startWork := func(name string) string {
204		workingDir = filepath.Join(testDir, name)
205		return workingDir
206	}
207	finishWork := func() {
208		finishedDirs[workingDir] = true
209		workingDir = ""
210	}
211
212	// Delete other sub-directories that are no longer relevant.
213	defer func() {
214		now := time.Now()
215		fis, _ := ioutil.ReadDir(testDir)
216		for _, fi := range fis {
217			dir := filepath.Join(testDir, fi.Name())
218			if finishedDirs[dir] {
219				os.Chtimes(dir, now, now) // best-effort
220				continue
221			}
222			if now.Sub(fi.ModTime()) < purgeTimeout {
223				continue
224			}
225			fmt.Printf("delete %v\n", fi.Name())
226			os.RemoveAll(dir) // best-effort
227		}
228	}()
229
230	// The bin directory contains symlinks to each tool by version.
231	// It is safe to delete this directory and run the test script from scratch.
232	binPath := startWork("bin")
233	check(os.RemoveAll(binPath))
234	check(os.Mkdir(binPath, 0775))
235	check(os.Setenv("PATH", binPath+":"+os.Getenv("PATH")))
236	registerBinary := func(name, path string) {
237		check(os.Symlink(path, filepath.Join(binPath, name)))
238	}
239	finishWork()
240
241	// Download and build the protobuf toolchain.
242	// We avoid downloading the pre-compiled binaries since they do not contain
243	// the conformance test runner.
244	protobufPath = startWork("protobuf-" + protobufVersion)
245	if _, err := os.Stat(protobufPath); err != nil {
246		fmt.Printf("download %v\n", filepath.Base(protobufPath))
247		checkoutVersion := protobufVersion
248		if isCommit := strings.Trim(protobufVersion, "0123456789abcdef") == ""; !isCommit {
249			// release tags have "v" prefix
250			checkoutVersion = "v" + protobufVersion
251		}
252		command{Dir: testDir}.mustRun(t, "git", "clone", "https://github.com/protocolbuffers/protobuf", "protobuf-"+protobufVersion)
253		command{Dir: protobufPath}.mustRun(t, "git", "checkout", checkoutVersion)
254
255		fmt.Printf("build %v\n", filepath.Base(protobufPath))
256		command{Dir: protobufPath}.mustRun(t, "bazel", "build", ":protoc", "//conformance:conformance_test_runner")
257	}
258	check(os.Setenv("PROTOBUF_ROOT", protobufPath)) // for generate-protos
259	registerBinary("conform-test-runner", filepath.Join(protobufPath, "bazel-bin", "conformance", "conformance_test_runner"))
260	registerBinary("protoc", filepath.Join(protobufPath, "bazel-bin", "protoc"))
261	finishWork()
262
263	// Download each Go toolchain version.
264	for _, v := range golangVersions {
265		goDir := startWork("go" + v)
266		if _, err := os.Stat(goDir); err != nil {
267			fmt.Printf("download %v\n", filepath.Base(goDir))
268			url := fmt.Sprintf("https://dl.google.com/go/go%v.%v-%v.tar.gz", v, runtime.GOOS, runtime.GOARCH)
269			downloadArchive(check, goDir, url, "go", "") // skip SHA256 check as we fetch over https from a trusted domain
270		}
271		registerBinary("go"+v, filepath.Join(goDir, "bin", "go"))
272		finishWork()
273	}
274	registerBinary("go", filepath.Join(testDir, "go"+golangLatest, "bin", "go"))
275	registerBinary("gofmt", filepath.Join(testDir, "go"+golangLatest, "bin", "gofmt"))
276
277	// Download the staticcheck tool.
278	checkDir := startWork("staticcheck-" + staticcheckVersion)
279	if _, err := os.Stat(checkDir); err != nil {
280		fmt.Printf("download %v\n", filepath.Base(checkDir))
281		url := fmt.Sprintf("https://github.com/dominikh/go-tools/releases/download/%v/staticcheck_%v_%v.tar.gz", staticcheckVersion, runtime.GOOS, runtime.GOARCH)
282		downloadArchive(check, checkDir, url, "staticcheck", staticcheckSHA256s[runtime.GOOS+"/"+runtime.GOARCH])
283	}
284	registerBinary("staticcheck", filepath.Join(checkDir, "staticcheck"))
285	finishWork()
286
287	// GitHub actions sets GOROOT, which confuses invocations of the Go toolchain.
288	// Explicitly clear GOROOT, so each toolchain uses their default GOROOT.
289	check(os.Unsetenv("GOROOT"))
290
291	// Set a cache directory outside the test directory.
292	check(os.Setenv("GOCACHE", filepath.Join(repoRoot, ".gocache")))
293
294	// Setup GOPATH for pre-module support (i.e., go1.10 and earlier).
295	goPath = startWork("gopath")
296	modulePath = strings.TrimSpace(command{Dir: testDir}.mustRun(t, "go", "list", "-m", "-f", "{{.Path}}"))
297	check(os.RemoveAll(filepath.Join(goPath, "src")))
298	check(os.MkdirAll(filepath.Join(goPath, "src", filepath.Dir(modulePath)), 0775))
299	check(os.Symlink(repoRoot, filepath.Join(goPath, "src", modulePath)))
300	command{Dir: repoRoot}.mustRun(t, "go", "mod", "tidy")
301	command{Dir: repoRoot}.mustRun(t, "go", "mod", "vendor")
302	check(os.Setenv("GOPATH", goPath))
303	finishWork()
304}
305
306func downloadFile(check func(error), dstPath, srcURL string) {
307	resp, err := http.Get(srcURL)
308	check(err)
309	defer resp.Body.Close()
310
311	check(os.MkdirAll(filepath.Dir(dstPath), 0775))
312	f, err := os.Create(dstPath)
313	check(err)
314
315	_, err = io.Copy(f, resp.Body)
316	check(err)
317}
318
319func downloadArchive(check func(error), dstPath, srcURL, skipPrefix, wantSHA256 string) {
320	check(os.RemoveAll(dstPath))
321
322	resp, err := http.Get(srcURL)
323	check(err)
324	defer resp.Body.Close()
325
326	var r io.Reader = resp.Body
327	if wantSHA256 != "" {
328		b, err := ioutil.ReadAll(resp.Body)
329		check(err)
330		r = bytes.NewReader(b)
331
332		if gotSHA256 := fmt.Sprintf("%x", sha256.Sum256(b)); gotSHA256 != wantSHA256 {
333			check(fmt.Errorf("checksum validation error:\ngot  %v\nwant %v", gotSHA256, wantSHA256))
334		}
335	}
336
337	zr, err := gzip.NewReader(r)
338	check(err)
339
340	tr := tar.NewReader(zr)
341	for {
342		h, err := tr.Next()
343		if err == io.EOF {
344			return
345		}
346		check(err)
347
348		// Skip directories or files outside the prefix directory.
349		if len(skipPrefix) > 0 {
350			if !strings.HasPrefix(h.Name, skipPrefix) {
351				continue
352			}
353			if len(h.Name) > len(skipPrefix) && h.Name[len(skipPrefix)] != '/' {
354				continue
355			}
356		}
357
358		path := strings.TrimPrefix(strings.TrimPrefix(h.Name, skipPrefix), "/")
359		path = filepath.Join(dstPath, filepath.FromSlash(path))
360		mode := os.FileMode(h.Mode & 0777)
361		switch h.Typeflag {
362		case tar.TypeReg:
363			b, err := ioutil.ReadAll(tr)
364			check(err)
365			check(ioutil.WriteFile(path, b, mode))
366		case tar.TypeDir:
367			check(os.Mkdir(path, mode))
368		}
369	}
370}
371
372func mustHandleFlags(t *testing.T) {
373	if *regenerate {
374		t.Run("Generate", func(t *testing.T) {
375			fmt.Print(mustRunCommand(t, "go", "run", "-tags", "protolegacy", "./internal/cmd/generate-types", "-execute"))
376			fmt.Print(mustRunCommand(t, "go", "run", "-tags", "protolegacy", "./internal/cmd/generate-protos", "-execute"))
377			files := strings.Split(strings.TrimSpace(mustRunCommand(t, "git", "ls-files", "*.go")), "\n")
378			mustRunCommand(t, append([]string{"gofmt", "-w"}, files...)...)
379		})
380	}
381	if *buildRelease {
382		t.Run("BuildRelease", func(t *testing.T) {
383			v := version.String()
384			for _, goos := range []string{"linux", "darwin", "windows"} {
385				for _, goarch := range []string{"386", "amd64", "arm64"} {
386					// Avoid Darwin since 10.15 dropped support for i386.
387					if goos == "darwin" && goarch == "386" {
388						continue
389					}
390
391					binPath := filepath.Join("bin", fmt.Sprintf("protoc-gen-go.%v.%v.%v", v, goos, goarch))
392
393					// Build the binary.
394					cmd := command{Env: append(os.Environ(), "GOOS="+goos, "GOARCH="+goarch)}
395					cmd.mustRun(t, "go", "build", "-trimpath", "-ldflags", "-s -w -buildid=", "-o", binPath, "./cmd/protoc-gen-go")
396
397					// Archive and compress the binary.
398					in, err := ioutil.ReadFile(binPath)
399					if err != nil {
400						t.Fatal(err)
401					}
402					out := new(bytes.Buffer)
403					suffix := ""
404					comment := fmt.Sprintf("protoc-gen-go VERSION=%v GOOS=%v GOARCH=%v", v, goos, goarch)
405					switch goos {
406					case "windows":
407						suffix = ".zip"
408						zw := zip.NewWriter(out)
409						zw.SetComment(comment)
410						fw, _ := zw.Create("protoc-gen-go.exe")
411						fw.Write(in)
412						zw.Close()
413					default:
414						suffix = ".tar.gz"
415						gz, _ := gzip.NewWriterLevel(out, gzip.BestCompression)
416						gz.Comment = comment
417						tw := tar.NewWriter(gz)
418						tw.WriteHeader(&tar.Header{
419							Name: "protoc-gen-go",
420							Mode: int64(0775),
421							Size: int64(len(in)),
422						})
423						tw.Write(in)
424						tw.Close()
425						gz.Close()
426					}
427					if err := ioutil.WriteFile(binPath+suffix, out.Bytes(), 0664); err != nil {
428						t.Fatal(err)
429					}
430				}
431			}
432		})
433	}
434	if *regenerate || *buildRelease {
435		t.SkipNow()
436	}
437}
438
439var copyrightRegex = []*regexp.Regexp{
440	regexp.MustCompile(`^// Copyright \d\d\d\d The Go Authors\. All rights reserved.
441// Use of this source code is governed by a BSD-style
442// license that can be found in the LICENSE file\.
443`),
444	// Generated .pb.go files from main protobuf repo.
445	regexp.MustCompile(`^// Protocol Buffers - Google's data interchange format
446// Copyright \d\d\d\d Google Inc\.  All rights reserved\.
447`),
448}
449
450func mustHaveCopyrightHeader(t *testing.T, files []string) {
451	var bad []string
452File:
453	for _, file := range files {
454		b, err := ioutil.ReadFile(file)
455		if err != nil {
456			t.Fatal(err)
457		}
458		for _, re := range copyrightRegex {
459			if loc := re.FindIndex(b); loc != nil && loc[0] == 0 {
460				continue File
461			}
462		}
463		bad = append(bad, file)
464	}
465	if len(bad) > 0 {
466		t.Fatalf("files with missing/bad copyright headers:\n  %v", strings.Join(bad, "\n  "))
467	}
468}
469
470type command struct {
471	Dir string
472	Env []string
473}
474
475func (c command) mustRun(t *testing.T, args ...string) string {
476	t.Helper()
477	stdout := new(bytes.Buffer)
478	stderr := new(bytes.Buffer)
479	cmd := exec.Command(args[0], args[1:]...)
480	cmd.Dir = "."
481	if c.Dir != "" {
482		cmd.Dir = c.Dir
483	}
484	cmd.Env = os.Environ()
485	if c.Env != nil {
486		cmd.Env = c.Env
487	}
488	cmd.Env = append(cmd.Env, "PWD="+cmd.Dir)
489	cmd.Stdout = stdout
490	cmd.Stderr = stderr
491	if err := cmd.Run(); err != nil {
492		t.Fatalf("executing (%v): %v\n%s%s", strings.Join(args, " "), err, stdout.String(), stderr.String())
493	}
494	return stdout.String()
495}
496
497func mustRunCommand(t *testing.T, args ...string) string {
498	t.Helper()
499	return command{}.mustRun(t, args...)
500}
501