1
2
3
4
5 package main
6
7 import (
8 "archive/tar"
9 "archive/zip"
10 "bytes"
11 "compress/gzip"
12 "crypto/sha256"
13 "flag"
14 "fmt"
15 "io"
16 "io/fs"
17 "io/ioutil"
18 "net/http"
19 "os"
20 "os/exec"
21 "path/filepath"
22 "reflect"
23 "regexp"
24 "runtime"
25 "runtime/debug"
26 "strings"
27 "sync"
28 "testing"
29 "time"
30
31 "google.golang.org/protobuf/internal/version"
32 )
33
34 var (
35 regenerate = flag.Bool("regenerate", false, "regenerate files")
36 buildRelease = flag.Bool("buildRelease", false, "build release binaries")
37
38 protobufVersion = "25.1"
39
40 golangVersions = func() []string {
41
42 return []string{
43 "1.17.13",
44 "1.18.10",
45 "1.19.13",
46 "1.20.12",
47 "1.21.5",
48 }
49 }()
50 golangLatest = golangVersions[len(golangVersions)-1]
51
52 staticcheckVersion = "2023.1.6"
53 staticcheckSHA256s = map[string]string{
54 "darwin/amd64": "b14a0cbd3c238713f5f9db41550893ea7d75d8d7822491c7f4e33e2fe43f6305",
55 "darwin/arm64": "f1c869abe6be2c6ab727dc9d6049766c947534766d71a1798c12a37526ea2b6f",
56 "linux/386": "02859a7c44c7b5ab41a70d9b8107c01ab8d2c94075bae3d0b02157aff743ca42",
57 "linux/amd64": "45337834da5dc7b8eff01cb6b3837e3759503cfbb8edf36b09e42f32bccb1f6e",
58 }
59
60
61 purgeTimeout = 30 * 24 * time.Hour
62
63
64 modulePath string
65 protobufPath string
66 )
67
68 func TestIntegration(t *testing.T) {
69 if testing.Short() {
70 t.Skip("skipping integration test in short mode")
71 }
72 if os.Getenv("GO_BUILDER_NAME") != "" {
73
74 if race() {
75 t.Skip("skipping integration test in race mode on builders")
76 }
77
78
79 if os.Getenv("GO_PROTOBUF_INTEGRATION_TEST_RUNNING") == "1" {
80 t.Skip("protobuf integration test is already running, skipping nested invocation")
81 }
82 os.Setenv("GO_PROTOBUF_INTEGRATION_TEST_RUNNING", "1")
83 } else if flag.Lookup("test.run").Value.String() != "^TestIntegration$" {
84 t.Skip("not running integration test if not explicitly requested via test.bash")
85 }
86
87 mustInitDeps(t)
88 mustHandleFlags(t)
89
90
91
92
93
94 gitDiff := mustRunCommand(t, "git", "diff", "HEAD")
95 if strings.TrimSpace(gitDiff) != "" {
96 fmt.Printf("WARNING: working tree contains uncommitted changes:\n%v\n", gitDiff)
97 }
98 gitUntracked := mustRunCommand(t, "git", "ls-files", "--others", "--exclude-standard")
99 if strings.TrimSpace(gitUntracked) != "" {
100 fmt.Printf("WARNING: working tree contains untracked files:\n%v\n", gitUntracked)
101 }
102
103
104 t.Run("GeneratedGoFiles", func(t *testing.T) {
105 diff := mustRunCommand(t, "go", "run", "-tags", "protolegacy", "./internal/cmd/generate-types")
106 if strings.TrimSpace(diff) != "" {
107 t.Fatalf("stale generated files:\n%v", diff)
108 }
109 diff = mustRunCommand(t, "go", "run", "-tags", "protolegacy", "./internal/cmd/generate-protos")
110 if strings.TrimSpace(diff) != "" {
111 t.Fatalf("stale generated files:\n%v", diff)
112 }
113 })
114 t.Run("FormattedGoFiles", func(t *testing.T) {
115 files := strings.Split(strings.TrimSpace(mustRunCommand(t, "git", "ls-files", "*.go")), "\n")
116 diff := mustRunCommand(t, append([]string{"gofmt", "-d"}, files...)...)
117 if strings.TrimSpace(diff) != "" {
118 t.Fatalf("unformatted source files:\n%v", diff)
119 }
120 })
121 t.Run("CopyrightHeaders", func(t *testing.T) {
122 files := strings.Split(strings.TrimSpace(mustRunCommand(t, "git", "ls-files", "*.go", "*.proto")), "\n")
123 mustHaveCopyrightHeader(t, files)
124 })
125
126 var wg sync.WaitGroup
127 sema := make(chan bool, (runtime.NumCPU()+1)/2)
128 for i := range golangVersions {
129 goVersion := golangVersions[i]
130 goLabel := "Go" + goVersion
131 runGo := func(label string, cmd command, args ...string) {
132 wg.Add(1)
133 sema <- true
134 go func() {
135 defer wg.Done()
136 defer func() { <-sema }()
137 t.Run(goLabel+"/"+label, func(t *testing.T) {
138 args[0] += goVersion
139 cmd.mustRun(t, args...)
140 })
141 }()
142 }
143
144 runGo("Normal", command{}, "go", "test", "-race", "./...")
145 runGo("PureGo", command{}, "go", "test", "-race", "-tags", "purego", "./...")
146 runGo("Reflect", command{}, "go", "test", "-race", "-tags", "protoreflect", "./...")
147 if goVersion == golangLatest {
148 runGo("ProtoLegacy", command{}, "go", "test", "-race", "-tags", "protolegacy", "./...")
149 runGo("ProtocGenGo", command{Dir: "cmd/protoc-gen-go/testdata"}, "go", "test")
150 runGo("Conformance", command{Dir: "internal/conformance"}, "go", "test", "-execute")
151
152
153
154 if runtime.GOOS == "linux" {
155 runGo("Arch32Bit", command{Env: append(os.Environ(), "GOARCH=386")}, "go", "test", "./...")
156 }
157 }
158 }
159 wg.Wait()
160
161 t.Run("GoStaticCheck", func(t *testing.T) {
162 checks := []string{
163 "all",
164 "-SA1019",
165 "-S*",
166 "-ST*",
167 "-U*",
168 }
169 out := mustRunCommand(t, "staticcheck", "-checks="+strings.Join(checks, ","), "-fail=none", "./...")
170
171
172 var findings []string
173 for _, finding := range strings.Split(strings.TrimSpace(out), "\n") {
174 switch {
175 case strings.HasPrefix(finding, "internal/testprotos/legacy/"):
176 default:
177 findings = append(findings, finding)
178 }
179 }
180 if len(findings) > 0 {
181 t.Fatalf("staticcheck findings:\n%v", strings.Join(findings, "\n"))
182 }
183 })
184 t.Run("CommittedGitChanges", func(t *testing.T) {
185 if strings.TrimSpace(gitDiff) != "" {
186 t.Fatalf("uncommitted changes")
187 }
188 })
189 t.Run("TrackedGitFiles", func(t *testing.T) {
190 if strings.TrimSpace(gitUntracked) != "" {
191 t.Fatalf("untracked files")
192 }
193 })
194 }
195
196 func mustInitDeps(t *testing.T) {
197 check := func(err error) {
198 t.Helper()
199 if err != nil {
200 t.Fatal(err)
201 }
202 }
203
204
205 repoRoot, err := os.Getwd()
206 check(err)
207 testDir := filepath.Join(repoRoot, ".cache")
208 check(os.MkdirAll(testDir, 0775))
209
210
211
212 var workingDir string
213 finishedDirs := map[string]bool{}
214 defer func() {
215 if workingDir != "" {
216 os.RemoveAll(workingDir)
217 }
218 }()
219 startWork := func(name string) string {
220 workingDir = filepath.Join(testDir, name)
221 return workingDir
222 }
223 finishWork := func() {
224 finishedDirs[workingDir] = true
225 workingDir = ""
226 }
227
228
229 defer func() {
230 now := time.Now()
231 fis, _ := ioutil.ReadDir(testDir)
232 for _, fi := range fis {
233 dir := filepath.Join(testDir, fi.Name())
234 if finishedDirs[dir] {
235 os.Chtimes(dir, now, now)
236 continue
237 }
238 if now.Sub(fi.ModTime()) < purgeTimeout {
239 continue
240 }
241 fmt.Printf("delete %v\n", fi.Name())
242 os.RemoveAll(dir)
243 }
244 }()
245
246
247
248 binPath := startWork("bin")
249 check(os.RemoveAll(binPath))
250 check(os.Mkdir(binPath, 0775))
251 check(os.Setenv("PATH", binPath+":"+os.Getenv("PATH")))
252 registerBinary := func(name, path string) {
253 check(os.Symlink(path, filepath.Join(binPath, name)))
254 }
255 finishWork()
256
257
258 protobufPath = startWork("protobuf-" + protobufVersion)
259 if _, err := os.Stat(protobufPath); err != nil {
260 fmt.Printf("download %v\n", filepath.Base(protobufPath))
261 checkoutVersion := protobufVersion
262 if isCommit := strings.Trim(protobufVersion, "0123456789abcdef") == ""; !isCommit {
263
264 checkoutVersion = "v" + protobufVersion
265 }
266 command{Dir: testDir}.mustRun(t, "git", "clone", "https://github.com/protocolbuffers/protobuf", "protobuf-"+protobufVersion)
267 command{Dir: protobufPath}.mustRun(t, "git", "checkout", checkoutVersion)
268
269 if os.Getenv("GO_BUILDER_NAME") != "" {
270
271
272
273 protocPath, err := exec.LookPath("protoc")
274 check(err)
275 confTestRunnerPath, err := exec.LookPath("conformance_test_runner")
276 check(err)
277 check(os.MkdirAll(filepath.Join(protobufPath, "bazel-bin", "conformance"), 0775))
278 check(os.Symlink(protocPath, filepath.Join(protobufPath, "bazel-bin", "protoc")))
279 check(os.Symlink(confTestRunnerPath, filepath.Join(protobufPath, "bazel-bin", "conformance", "conformance_test_runner")))
280 } else {
281
282
283
284 fmt.Printf("build %v\n", filepath.Base(protobufPath))
285 env := os.Environ()
286 if runtime.GOOS == "darwin" {
287
288 env = append(env, "CC=clang")
289 }
290 command{
291 Dir: protobufPath,
292 Env: env,
293 }.mustRun(t, "bazel", "build", ":protoc", "//conformance:conformance_test_runner")
294 }
295 }
296 check(os.Setenv("PROTOBUF_ROOT", protobufPath))
297 registerBinary("conform-test-runner", filepath.Join(protobufPath, "bazel-bin", "conformance", "conformance_test_runner"))
298 registerBinary("protoc", filepath.Join(protobufPath, "bazel-bin", "protoc"))
299 finishWork()
300
301
302 for _, v := range golangVersions {
303 goDir := startWork("go" + v)
304 if _, err := os.Stat(goDir); err != nil {
305 fmt.Printf("download %v\n", filepath.Base(goDir))
306 url := fmt.Sprintf("https://dl.google.com/go/go%v.%v-%v.tar.gz", v, runtime.GOOS, runtime.GOARCH)
307 downloadArchive(check, goDir, url, "go", "")
308 }
309 registerBinary("go"+v, filepath.Join(goDir, "bin", "go"))
310 finishWork()
311 }
312 registerBinary("go", filepath.Join(testDir, "go"+golangLatest, "bin", "go"))
313 registerBinary("gofmt", filepath.Join(testDir, "go"+golangLatest, "bin", "gofmt"))
314
315
316 checkDir := startWork("staticcheck-" + staticcheckVersion)
317 if _, err := os.Stat(checkDir); err != nil {
318 fmt.Printf("download %v\n", filepath.Base(checkDir))
319 url := fmt.Sprintf("https://github.com/dominikh/go-tools/releases/download/%v/staticcheck_%v_%v.tar.gz", staticcheckVersion, runtime.GOOS, runtime.GOARCH)
320 downloadArchive(check, checkDir, url, "staticcheck", staticcheckSHA256s[runtime.GOOS+"/"+runtime.GOARCH])
321 }
322 registerBinary("staticcheck", filepath.Join(checkDir, "staticcheck"))
323 finishWork()
324
325
326
327 check(os.Unsetenv("GOROOT"))
328
329
330 check(os.Setenv("GOCACHE", filepath.Join(repoRoot, ".gocache")))
331 }
332
333 func downloadFile(check func(error), dstPath, srcURL string, perm fs.FileMode) {
334 resp, err := http.Get(srcURL)
335 check(err)
336 defer resp.Body.Close()
337 if resp.StatusCode != http.StatusOK {
338 body, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<10))
339 check(fmt.Errorf("GET %q: non-200 OK status code: %v body: %q", srcURL, resp.Status, body))
340 }
341
342 check(os.MkdirAll(filepath.Dir(dstPath), 0775))
343 f, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
344 check(err)
345
346 _, err = io.Copy(f, resp.Body)
347 check(err)
348
349 check(f.Close())
350 }
351
352 func downloadArchive(check func(error), dstPath, srcURL, skipPrefix, wantSHA256 string) {
353 check(os.RemoveAll(dstPath))
354
355 resp, err := http.Get(srcURL)
356 check(err)
357 defer resp.Body.Close()
358 if resp.StatusCode != http.StatusOK {
359 body, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<10))
360 check(fmt.Errorf("GET %q: non-200 OK status code: %v body: %q", srcURL, resp.Status, body))
361 }
362
363 var r io.Reader = resp.Body
364 if wantSHA256 != "" {
365 b, err := ioutil.ReadAll(resp.Body)
366 check(err)
367 r = bytes.NewReader(b)
368
369 if gotSHA256 := fmt.Sprintf("%x", sha256.Sum256(b)); gotSHA256 != wantSHA256 {
370 check(fmt.Errorf("checksum validation error:\ngot %v\nwant %v", gotSHA256, wantSHA256))
371 }
372 }
373
374 zr, err := gzip.NewReader(r)
375 check(err)
376
377 tr := tar.NewReader(zr)
378 for {
379 h, err := tr.Next()
380 if err == io.EOF {
381 return
382 }
383 check(err)
384
385
386 if len(skipPrefix) > 0 {
387 if !strings.HasPrefix(h.Name, skipPrefix) {
388 continue
389 }
390 if len(h.Name) > len(skipPrefix) && h.Name[len(skipPrefix)] != '/' {
391 continue
392 }
393 }
394
395 path := strings.TrimPrefix(strings.TrimPrefix(h.Name, skipPrefix), "/")
396 path = filepath.Join(dstPath, filepath.FromSlash(path))
397 mode := os.FileMode(h.Mode & 0777)
398 switch h.Typeflag {
399 case tar.TypeReg:
400 b, err := ioutil.ReadAll(tr)
401 check(err)
402 check(ioutil.WriteFile(path, b, mode))
403 case tar.TypeDir:
404 check(os.Mkdir(path, mode))
405 }
406 }
407 }
408
409 func mustHandleFlags(t *testing.T) {
410 if *regenerate {
411 t.Run("Generate", func(t *testing.T) {
412 fmt.Print(mustRunCommand(t, "go", "generate", "./internal/cmd/generate-types"))
413 fmt.Print(mustRunCommand(t, "go", "generate", "./internal/cmd/generate-protos"))
414 files := strings.Split(strings.TrimSpace(mustRunCommand(t, "git", "ls-files", "*.go")), "\n")
415 mustRunCommand(t, append([]string{"gofmt", "-w"}, files...)...)
416 })
417 }
418 if *buildRelease {
419 t.Run("BuildRelease", func(t *testing.T) {
420 v := version.String()
421 for _, goos := range []string{"linux", "darwin", "windows"} {
422 for _, goarch := range []string{"386", "amd64", "arm64"} {
423
424 if goos == "darwin" && goarch == "386" {
425 continue
426 }
427
428 binPath := filepath.Join("bin", fmt.Sprintf("protoc-gen-go.%v.%v.%v", v, goos, goarch))
429
430
431 cmd := command{Env: append(os.Environ(), "GOOS="+goos, "GOARCH="+goarch)}
432 cmd.mustRun(t, "go", "build", "-trimpath", "-ldflags", "-s -w -buildid=", "-o", binPath, "./cmd/protoc-gen-go")
433
434
435 in, err := ioutil.ReadFile(binPath)
436 if err != nil {
437 t.Fatal(err)
438 }
439 out := new(bytes.Buffer)
440 suffix := ""
441 comment := fmt.Sprintf("protoc-gen-go VERSION=%v GOOS=%v GOARCH=%v", v, goos, goarch)
442 switch goos {
443 case "windows":
444 suffix = ".zip"
445 zw := zip.NewWriter(out)
446 zw.SetComment(comment)
447 fw, _ := zw.Create("protoc-gen-go.exe")
448 fw.Write(in)
449 zw.Close()
450 default:
451 suffix = ".tar.gz"
452 gz, _ := gzip.NewWriterLevel(out, gzip.BestCompression)
453 gz.Comment = comment
454 tw := tar.NewWriter(gz)
455 tw.WriteHeader(&tar.Header{
456 Name: "protoc-gen-go",
457 Mode: int64(0775),
458 Size: int64(len(in)),
459 })
460 tw.Write(in)
461 tw.Close()
462 gz.Close()
463 }
464 if err := ioutil.WriteFile(binPath+suffix, out.Bytes(), 0664); err != nil {
465 t.Fatal(err)
466 }
467 }
468 }
469 })
470 }
471 if *regenerate || *buildRelease {
472 t.SkipNow()
473 }
474 }
475
476 var copyrightRegex = []*regexp.Regexp{
477 regexp.MustCompile(`^// Copyright \d\d\d\d The Go Authors\. All rights reserved.
478 // Use of this source code is governed by a BSD-style
479 // license that can be found in the LICENSE file\.
480 `),
481
482 regexp.MustCompile(`^// Protocol Buffers - Google's data interchange format
483 // Copyright \d\d\d\d Google Inc\. All rights reserved\.
484 `),
485 }
486
487 func mustHaveCopyrightHeader(t *testing.T, files []string) {
488 var bad []string
489 File:
490 for _, file := range files {
491 b, err := ioutil.ReadFile(file)
492 if err != nil {
493 t.Fatal(err)
494 }
495 for _, re := range copyrightRegex {
496 if loc := re.FindIndex(b); loc != nil && loc[0] == 0 {
497 continue File
498 }
499 }
500 bad = append(bad, file)
501 }
502 if len(bad) > 0 {
503 t.Fatalf("files with missing/bad copyright headers:\n %v", strings.Join(bad, "\n "))
504 }
505 }
506
507 type command struct {
508 Dir string
509 Env []string
510 }
511
512 func (c command) mustRun(t *testing.T, args ...string) string {
513 t.Helper()
514 stdout := new(bytes.Buffer)
515 stderr := new(bytes.Buffer)
516 cmd := exec.Command(args[0], args[1:]...)
517 cmd.Dir = "."
518 if c.Dir != "" {
519 cmd.Dir = c.Dir
520 }
521 cmd.Env = os.Environ()
522 if c.Env != nil {
523 cmd.Env = c.Env
524 }
525 cmd.Env = append(cmd.Env, "PWD="+cmd.Dir)
526 cmd.Stdout = stdout
527 cmd.Stderr = stderr
528 if err := cmd.Run(); err != nil {
529 t.Fatalf("executing (%v): %v\n%s%s", strings.Join(args, " "), err, stdout.String(), stderr.String())
530 }
531 return stdout.String()
532 }
533
534 func mustRunCommand(t *testing.T, args ...string) string {
535 t.Helper()
536 return command{}.mustRun(t, args...)
537 }
538
539
540
541
542
543 func race() bool {
544 bi, ok := debug.ReadBuildInfo()
545 if !ok {
546 return false
547 }
548
549
550 s := reflect.ValueOf(bi).Elem().FieldByName("Settings")
551 if !s.IsValid() {
552 return false
553 }
554 for i := 0; i < s.Len(); i++ {
555 if s.Index(i).FieldByName("Key").String() == "-race" {
556 return s.Index(i).FieldByName("Value").String() == "true"
557 }
558 }
559 return false
560 }
561
View as plain text